const std = @import("std");
const ast = @import("../ast.zig");
const types = @import("types.zig");
const errors = @import("../errors.zig");
const program_index_mod = @import("program_index.zig");

const Node = ast.Node;
const TypeId = types.TypeId;
const TypeTable = types.TypeTable;
const StringId = types.StringId;
const ProgramIndex = program_index_mod.ProgramIndex;

/// Explicit, caller-supplied resolution context (architecture Principle 2):
/// the inputs that steer AST type-node resolution, replacing ad-hoc mutable
/// `Lowering` fields (`type_bindings`, `pack_*`, `comptime_value_bindings`,
/// `target_type`, …). A2.1 defines the shape; fields are consumed as later
/// phases move the cases that need them (generics/aliases A2.2, packs A2.3).
pub const ResolveEnv = struct {
    type_bindings: ?*const std.StringHashMap(TypeId) = null,
    pack_bindings: ?*const std.StringHashMap([]const TypeId) = null,
    pack_arg_types: ?*const std.StringHashMap([]const TypeId) = null,
    pack_constraints: ?*const std.StringHashMap([]const u8) = null,
    comptime_values: ?*const std.StringHashMap(i64) = null,
    target_type: ?TypeId = null,
};

/// Canonical AST-type-node → `TypeId` resolver (architecture phase A2). As of
/// A2.1 it owns the primitive-keyword table and the structural compound type
/// constructors. Later phases fold in generics/aliases (A2.2) and pack
/// projections (A2.3) and retire `src/ir/type_bridge.zig` (Principle 1).
///
/// Holds borrowed references only — constructed cheaply by value at each call
/// site (`Lowering.typeResolver()`), so it always reflects current state.
pub const TypeResolver = struct {
    alloc: std.mem.Allocator,
    types: *TypeTable,
    diagnostics: ?*errors.DiagnosticList,
    index: *ProgramIndex,

    /// Builtin primitive keyword → `TypeId`; `null` for any non-primitive name
    /// (the caller then continues with generic / alias / named-struct
    /// resolution). Single source of truth for the builtin keyword set.
    /// Namespaced (no `self`) — primitive resolution is stateless.
    pub fn resolvePrimitive(name: []const u8) ?TypeId {
        if (name.len == 0) return null;
        if (std.mem.eql(u8, name, "s64")) return .s64;
        if (std.mem.eql(u8, name, "s32")) return .s32;
        if (std.mem.eql(u8, name, "s16")) return .s16;
        if (std.mem.eql(u8, name, "s8")) return .s8;
        if (std.mem.eql(u8, name, "u64")) return .u64;
        if (std.mem.eql(u8, name, "u32")) return .u32;
        if (std.mem.eql(u8, name, "u16")) return .u16;
        if (std.mem.eql(u8, name, "u8")) return .u8;
        if (std.mem.eql(u8, name, "f32")) return .f32;
        if (std.mem.eql(u8, name, "f64")) return .f64;
        if (std.mem.eql(u8, name, "bool")) return .bool;
        if (std.mem.eql(u8, name, "string")) return .string;
        if (std.mem.eql(u8, name, "void")) return .void;
        if (std.mem.eql(u8, name, "Any")) return .any;
        // `Type` values are runtime-representable as Any-shaped pairs
        // `{ tag = .any.index(), value = TypeId.index() }`, so `Type` maps to
        // `.any` and routes through the existing Any infrastructure.
        if (std.mem.eql(u8, name, "Type")) return .any;
        if (std.mem.eql(u8, name, "noreturn")) return .noreturn;
        if (std.mem.eql(u8, name, "usize")) return .usize;
        if (std.mem.eql(u8, name, "isize")) return .isize;
        return null;
    }

    /// Single owner of structural AST-type-shape construction. Builds the
    /// shapes whose `TypeId` is fully determined by their node kind plus their
    /// element types resolved through `inner.resolveInner`: `*T`, `[*]T`, `[]T`,
    /// `?T`, `[N]T`, `(P...) -> R` functions, plain `Closure(P...) -> R`, and
    /// plain positional/named tuples. Element recursion goes through `inner`, so
    /// the caller's resolution mode is preserved — the compiler's stateful path
    /// passes `*Lowering` (generic/pack-binding aware), `type_bridge` passes a
    /// binding-free adapter. Both call THIS; there is no second compound/shape
    /// algorithm (architecture A2.3b — `resolveCompound` is the single owner).
    ///
    /// Namespaced (no `self`): only the `TypeTable` is needed, so `type_bridge`
    /// (which has no `ProgramIndex`/diagnostics) can call it too.
    ///
    /// Returns `null` for shapes that depend on caller pack/binding STATE and so
    /// can't be built here: pack-shaped `Closure(..p)` and spread tuples
    /// `(..xs)` (the stateful caller routes these to `PackResolver`), plus
    /// names, parameterized types, pack-index, and `Self`. OOM yields the
    /// `.unresolved` sentinel, never a fabricated type.
    pub fn resolveCompound(table: *TypeTable, node: *const Node, inner: anytype) ?TypeId {
        return switch (node.data) {
            .pointer_type_expr => |pt| table.ptrTo(inner.resolveInner(pt.pointee_type)),
            .many_pointer_type_expr => |mp| table.manyPtrTo(inner.resolveInner(mp.element_type)),
            .slice_type_expr => |st| table.sliceOf(inner.resolveInner(st.element_type)),
            .optional_type_expr => |ot| table.optionalOf(inner.resolveInner(ot.inner_type)),
            .array_type_expr => |at| blk: {
                const elem = inner.resolveInner(at.element_type);
                const len: u32 = if (at.length.data == .int_literal) @intCast(at.length.data.int_literal.value) else 0;
                break :blk table.arrayOf(elem, len);
            },
            .function_type_expr => |ft| blk: {
                var param_ids = std.ArrayList(TypeId).empty;
                defer param_ids.deinit(table.alloc);
                for (ft.param_types) |pt| param_ids.append(table.alloc, inner.resolveInner(pt)) catch return .unresolved;
                const ret_ty = if (ft.return_type) |rt| inner.resolveInner(rt) else TypeId.void;
                const cc: types.TypeInfo.CallConv = switch (ft.call_conv) {
                    .default => .default,
                    .c => .c,
                };
                break :blk table.functionTypeCC(param_ids.items, ret_ty, cc);
            },
            .closure_type_expr => |ct| blk: {
                // Pack-shaped `Closure(..p)` needs caller pack state to expand —
                // defer to PackResolver (stateful) by returning null.
                if (ct.pack_name != null) break :blk null;
                var param_ids = std.ArrayList(TypeId).empty;
                defer param_ids.deinit(table.alloc);
                for (ct.param_types) |pt| param_ids.append(table.alloc, inner.resolveInner(pt)) catch return .unresolved;
                const ret_ty = if (ct.return_type) |rt| inner.resolveInner(rt) else TypeId.void;
                break :blk table.closureType(param_ids.items, ret_ty);
            },
            .tuple_type_expr => |tt| blk: {
                // A spread field `(..xs)` expands to many fields via the pack
                // state — defer to PackResolver by returning null.
                for (tt.field_types) |ft| if (ft.data == .spread_expr) break :blk null;
                var field_ids = std.ArrayList(TypeId).empty;
                defer field_ids.deinit(table.alloc);
                for (tt.field_types) |ft| field_ids.append(table.alloc, inner.resolveInner(ft)) catch return .unresolved;
                // Preserve field names for a named tuple `(x: T, y: U)` when the
                // name and field counts agree (so `t.x` resolves).
                var name_ids: ?[]const StringId = null;
                if (tt.field_names) |names| {
                    if (names.len == field_ids.items.len) {
                        var ids = std.ArrayList(StringId).empty;
                        for (names) |n| ids.append(table.alloc, table.internString(n)) catch return .unresolved;
                        name_ids = ids.toOwnedSlice(table.alloc) catch null;
                    }
                }
                break :blk table.intern(.{ .tuple = .{
                    .fields = table.alloc.dupe(TypeId, field_ids.items) catch return .unresolved,
                    .names = name_ids,
                } });
            },
            else => null,
        };
    }

    /// Generic type-param binding lookup (`$T`, or a bare return-type `T`).
    /// Reads the caller-supplied `ResolveEnv` rather than hidden `Lowering`
    /// state. Returns null when there are no active bindings or the name is
    /// unbound (the caller then continues with primitive / alias / struct
    /// resolution, or returns `.unresolved` for an unbound generic `$R`).
    pub fn resolveBinding(node: *const Node, env: ResolveEnv) ?TypeId {
        const tb = env.type_bindings orelse return null;
        return switch (node.data) {
            .type_expr => |te| tb.get(te.name),
            .identifier => |id| tb.get(id.name),
            else => null,
        };
    }

    /// Resolve a bare type NAME to a `TypeId`: primitive → arbitrary-width int
    /// (`s1`–`u64`) → string-form pointer/slice/optional prefixes → already-
    /// registered named type → alias (`alias_map`) → fresh empty-struct stub.
    /// `alias_map` is the single-source alias table (owned by `ProgramIndex`);
    /// callers pass it explicitly — Lowering via the index (`resolveName`),
    /// `type_bridge` via the alias map threaded through `resolveAstType`. The
    /// stub fall-through preserves long-standing behavior for as-yet-
    /// unregistered names.
    pub fn resolveNamed(name: []const u8, table: *TypeTable, alias_map: ?*const std.StringHashMap(TypeId)) TypeId {
        if (resolvePrimitive(name)) |id| return id;
        // Arbitrary bit-width integers: s1-s64, u1-u64.
        if (name.len >= 2 and (name[0] == 's' or name[0] == 'u')) {
            if (std.fmt.parseInt(u8, name[1..], 10)) |width| {
                if (width >= 1 and width <= 64) {
                    return if (name[0] == 's') table.intern(.{ .signed = width }) else table.intern(.{ .unsigned = width });
                }
            } else |_| {}
        }
        // Sentinel-terminated slice: [:0]u8 → string.
        if (name.len >= 5 and name[0] == '[' and name[1] == ':') {
            if (std.mem.indexOfScalar(u8, name, ']')) |close| {
                const sentinel = name[2..close];
                const elem = name[close + 1 ..];
                if (std.mem.eql(u8, sentinel, "0") and std.mem.eql(u8, elem, "u8")) return .string;
            }
        }
        // Many-pointer: [*]T.
        if (name.len >= 4 and name[0] == '[' and name[1] == '*' and name[2] == ']') {
            return table.manyPtrTo(resolveNamed(name[3..], table, alias_map));
        }
        // Pointer: *T.
        if (name.len >= 2 and name[0] == '*') {
            return table.ptrTo(resolveNamed(name[1..], table, alias_map));
        }
        // Optional: ?T.
        if (name.len >= 2 and name[0] == '?') {
            return table.optionalOf(resolveNamed(name[1..], table, alias_map));
        }
        // Named struct/enum/union — already-registered wins, then alias, then
        // a fresh empty-struct stub for an as-yet-unregistered name.
        const name_id = table.internString(name);
        if (table.findByName(name_id)) |existing| return existing;
        if (alias_map) |amap| {
            if (amap.get(name)) |alias_ty| return alias_ty;
        }
        return table.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } });
    }

    /// Resolve a bare type name through the canonical alias source
    /// (`ProgramIndex.type_alias_map`).
    pub fn resolveName(self: TypeResolver, name: []const u8) TypeId {
        return resolveNamed(name, self.types, &self.index.type_alias_map);
    }
};
