const std = @import("std");
const ast = @import("../ast.zig");
const types = @import("types.zig");
const type_bridge = @import("type_bridge.zig");
const lower = @import("lower.zig");
const inst = @import("inst.zig");

const Node = ast.Node;
const TypeId = types.TypeId;
const FuncId = inst.FuncId;
const BuiltinId = inst.BuiltinId;
const Lowering = lower.Lowering;

/// The classification of a call expression: which dispatch path lowering will
/// take, the IR type the call evaluates to, and the properties (selected
/// target, enum variant, receiver / `__sx_ctx` prepend, default-arg expansion)
/// that path implies.
///
/// `plan(c)` is the single point that recognises a call form; `resultType(c)`
/// is the thin "just the type" projection (`plan(c).return_type`). This step
/// (A3.2 convergence sub-step 2) builds the plan object and routes typing
/// through it; `lowerCall` still owns its own dispatch and is rerouted onto
/// the plan in sub-step 3.
pub const CallPlan = struct {
    kind: Kind,
    return_type: TypeId,
    target: Target = .none,
    /// Enum / tagged-union variant tag, for the construction kinds.
    variant: ?u32 = null,
    /// Lowering prepends the receiver as arg 0 (UFCS / instance-method forms).
    prepends_receiver: bool = false,
    /// Lowering prepends the implicit `__sx_ctx` as arg 0.
    prepends_ctx: bool = false,
    /// The caller omits trailing positional args the callee provides defaults
    /// for, so lowering splices them in (`expandCallDefaults` / `appendDefaultArgs`).
    expands_defaults: bool = false,

    pub const Kind = enum {
        builtin,
        reflection,
        generic_fn,
        /// A plain free function — resolved (`target.func`) or known only by
        /// AST and lowered lazily (`target.named`).
        direct_fn,
        closure,
        fn_pointer,
        protocol_dispatch,
        struct_method,
        /// Free-function UFCS: `recv.fn(args)` → `fn(recv, args)`, where `fn`
        /// is a plain free function and `recv` is a value (not a namespace /
        /// type prefix). Distinct from `namespace_fn` precisely because the
        /// receiver IS prepended (`prepends_receiver`).
        free_fn_ufcs,
        foreign_instance,
        foreign_static,
        /// `pkg.fn(args)` — the receiver is a namespace / module prefix, NOT a
        /// value, so nothing is prepended.
        namespace_fn,
        enum_construct,
        enum_shorthand,
        unresolved,
    };

    /// What `plan` selected. The active arm is disambiguated by `kind`:
    /// e.g. a `.named` under `.reflection` is a builtin name, under
    /// `.direct_fn` a lazily-lowered fn, under `.closure` a binding.
    pub const Target = union(enum) {
        none,
        builtin: BuiltinId,
        /// A resolved (lowered) free / method / namespace function.
        func: FuncId,
        /// A callee carried by name — reflection builtin, generic / lazy fn,
        /// closure / fn-pointer binding, or a not-yet-lowered namespace fn.
        named: []const u8,
        /// Protocol method, by index in the protocol's method table.
        protocol_method: u32,
        /// Foreign-class method (Obj-C / JNI), with its static-ness.
        foreign_method: struct { name: []const u8, is_static: bool },
        /// Enum / tagged-union type under construction.
        constructed: TypeId,
    };
};

/// Call result typing (architecture phase A3.2), extracted from
/// `Lowering.inferExprType`'s call arm. Discovers the IR type a call
/// expression evaluates to — across builtins / reflection builtins, generic
/// and plain free functions (lowered or lazy via `fn_ast_map`), closure /
/// function-typed locals, protocol dispatch, foreign-class instance/static
/// methods, struct (UFCS) methods, qualified namespace calls, and
/// enum/tagged-union construction.
///
/// A `*Lowering` facade (Principle 5, like `ExprTyper` / `PackResolver`): call
/// typing reads live lexical-scope / target-type state and the function /
/// foreign-class / protocol resolver helpers, so it borrows `*Lowering` rather
/// than re-threading every field.
pub const CallResolver = struct {
    l: *Lowering,

    /// Infer the IR type a call expression evaluates to (without lowering it).
    pub fn resultType(self: CallResolver, c: *const ast.Call) TypeId {
        return self.plan(c).return_type;
    }

    /// Classify a call: pick the dispatch kind / target / variant and derive
    /// the result type and prepend / default-expansion properties. The single
    /// source of truth for "what kind of call is this?".
    pub fn plan(self: CallResolver, c: *const ast.Call) CallPlan {
        if (c.callee.data == .identifier) {
            const bare_name = c.callee.data.identifier.name;
            // Resolve local function name (bare → mangled) and UFCS aliases
            const name = blk: {
                const scoped = if (self.l.scope) |scope| scope.lookupFn(bare_name) orelse bare_name else bare_name;
                if (self.l.program_index.ufcs_alias_map.get(bare_name)) |target| {
                    break :blk if (self.l.scope) |scope| scope.lookupFn(target) orelse target else target;
                }
                break :blk scoped;
            };
            if (Lowering.resolveBuiltin(bare_name)) |bid| {
                const rt: TypeId = switch (bid) {
                    .sqrt, .sin, .cos, .floor => blk: {
                        if (c.args.len > 0) {
                            const arg_ty = self.l.inferExprType(c.args[0]);
                            if (arg_ty == .f32) break :blk TypeId.f32;
                        }
                        break :blk TypeId.f64;
                    },
                    .size_of, .align_of => .s64,
                    .cast => if (c.args.len > 0) self.l.resolveTypeArg(c.args[0]) else .unresolved,
                    else => .unresolved,
                };
                return .{ .kind = .builtin, .return_type = rt, .target = .{ .builtin = bid } };
            }
            // Reflection builtins live outside `resolveBuiltin`'s table (their
            // lowering goes through `tryLowerReflectionCall`, not the
            // `BuiltinId` dispatch). Recognize them here so pack-fn callers
            // mangle their results with the right tag.
            if (std.mem.eql(u8, bare_name, "type_name")) return refl(bare_name, .string);
            if (std.mem.eql(u8, bare_name, "type_eq")) return refl(bare_name, .bool);
            if (std.mem.eql(u8, bare_name, "has_impl")) return refl(bare_name, .bool);
            if (std.mem.eql(u8, bare_name, "field_count")) return refl(bare_name, .s64);
            if (std.mem.eql(u8, bare_name, "field_index")) return refl(bare_name, .s64);
            if (std.mem.eql(u8, bare_name, "field_name")) return refl(bare_name, .string);
            if (std.mem.eql(u8, bare_name, "error_tag_name")) return refl(bare_name, .string);
            if (std.mem.eql(u8, bare_name, "is_comptime")) return refl(bare_name, .bool);
            if (std.mem.eql(u8, bare_name, "__interp_print_frames")) return refl(bare_name, .void);
            if (std.mem.eql(u8, bare_name, "__trace_resolve_frame"))
                return refl(bare_name, self.l.module.types.findByName(self.l.module.types.internString("Frame")) orelse .unresolved);
            if (std.mem.eql(u8, bare_name, "is_flags")) return refl(bare_name, .bool);
            if (std.mem.eql(u8, bare_name, "type_of")) return refl(bare_name, .any);
            if (std.mem.eql(u8, bare_name, "field_value")) return refl(bare_name, .any);
            // Generic function — infer return type via type bindings.
            if (self.l.program_index.fn_ast_map.get(name)) |fd| {
                if (fd.type_params.len > 0) {
                    return .{
                        .kind = .generic_fn,
                        .return_type = self.l.genericResolver().inferGenericReturnType(fd, c),
                        .target = .{ .named = name },
                        .expands_defaults = defaultsFor(fd, c.args.len),
                    };
                }
            }
            // Declared (lowered) function — return type from its signature.
            if (self.l.resolveFuncByName(name)) |fid| {
                const func = &self.l.module.functions.items[@intFromEnum(fid)];
                return .{
                    .kind = .direct_fn,
                    .return_type = func.ret,
                    .target = .{ .func = fid },
                    .prepends_ctx = func.has_implicit_ctx,
                    .expands_defaults = if (self.l.program_index.fn_ast_map.get(name)) |fd| defaultsFor(fd, c.args.len) else false,
                };
            }
            // Not lowered yet (lazy lowering): take the return type from the
            // declared AST. A void/return-less fn is void — not an
            // `.unresolved` guess.
            if (self.l.program_index.fn_ast_map.get(name)) |fd| {
                return .{
                    .kind = .direct_fn,
                    .return_type = if (fd.return_type) |rt| self.l.resolveType(rt) else .void,
                    .target = .{ .named = name },
                    .expands_defaults = defaultsFor(fd, c.args.len),
                };
            }
            // Local closure- / function-typed binding (e.g. a `cb: Closure(...)
            // -> R` or bare `cb: (T) -> R` parameter) — extract its declared
            // return type so `try` / `catch` on the call see the (possibly
            // failable) result.
            if (self.l.scope) |scope| {
                if (scope.lookup(bare_name)) |binding| {
                    if (!binding.ty.isBuiltin()) {
                        const ti = self.l.module.types.get(binding.ty);
                        if (ti == .closure) return .{
                            .kind = .closure,
                            .return_type = ti.closure.ret,
                            .target = .{ .named = bare_name },
                            .prepends_ctx = self.l.implicit_ctx_enabled,
                        };
                        if (ti == .function) return .{
                            .kind = .fn_pointer,
                            .return_type = ti.function.ret,
                            .target = .{ .named = bare_name },
                            .prepends_ctx = self.l.implicit_ctx_enabled and ti.function.call_conv != .c,
                        };
                    }
                }
            }
        } else if (c.callee.data == .field_access) {
            const cfa = c.callee.data.field_access;
            const recv_ty = self.l.inferExprType(cfa.object);
            // Receiver is a protocol type → protocol method dispatch.
            if (self.l.getProtocolInfo(recv_ty)) |proto_info| {
                for (proto_info.methods, 0..) |m, mi| {
                    if (std.mem.eql(u8, m.name, cfa.field)) return .{
                        .kind = .protocol_dispatch,
                        .return_type = m.ret_type,
                        .target = .{ .protocol_method = @intCast(mi) },
                        .prepends_receiver = true,
                    };
                }
            }
            // Foreign-class instance method: look up the method's declared
            // return type so chained calls (e.g.
            // `UIWindow.alloc().initWithWindowScene(scene)`) resolve.
            {
                var recv_inner = recv_ty;
                if (!recv_inner.isBuiltin()) {
                    const ri = self.l.module.types.get(recv_inner);
                    if (ri == .pointer) recv_inner = ri.pointer.pointee;
                }
                if (!recv_inner.isBuiltin()) {
                    const inner_info = self.l.module.types.get(recv_inner);
                    if (inner_info == .@"struct") {
                        const sn = self.l.module.types.getString(inner_info.@"struct".name);
                        if (self.l.program_index.foreign_class_map.get(sn)) |fcd| {
                            for (fcd.members) |m| switch (m) {
                                .method => |md| if (!md.is_static and std.mem.eql(u8, md.name, cfa.field)) {
                                    return .{
                                        .kind = .foreign_instance,
                                        .return_type = self.l.resolveForeignMethodReturnType(fcd, md),
                                        .target = .{ .foreign_method = .{ .name = md.name, .is_static = false } },
                                        .prepends_receiver = true,
                                    };
                                },
                                else => {},
                            };
                        }
                    }
                }
            }
            // Instance method call: obj.method(args) → StructName.method.
            {
                var obj_ty = recv_ty;
                if (!obj_ty.isBuiltin()) {
                    const oi = self.l.module.types.get(obj_ty);
                    if (oi == .pointer) obj_ty = oi.pointer.pointee;
                }
                if (!obj_ty.isBuiltin()) {
                    const oi = self.l.module.types.get(obj_ty);
                    if (oi == .@"struct") {
                        const struct_name = self.l.module.types.getString(oi.@"struct".name);
                        const qualified = std.fmt.allocPrint(self.l.alloc, "{s}.{s}", .{ struct_name, cfa.field }) catch cfa.field;
                        // Generic #compiler method dispatch — return type from declaration.
                        if (self.l.program_index.fn_ast_map.get(qualified)) |method_fd| {
                            if (method_fd.body.data == .compiler_expr) {
                                return .{
                                    .kind = .struct_method,
                                    .return_type = if (method_fd.return_type) |rt| type_bridge.resolveAstType(rt, &self.l.module.types, &self.l.program_index.type_alias_map) else .void,
                                    .target = .{ .named = qualified },
                                    .prepends_receiver = true,
                                    .expands_defaults = defaultsFor(method_fd, c.args.len + 1),
                                };
                            }
                        }
                        if (self.l.resolveFuncByName(qualified)) |fid| {
                            const func = &self.l.module.functions.items[@intFromEnum(fid)];
                            return .{
                                .kind = .struct_method,
                                .return_type = func.ret,
                                .target = .{ .func = fid },
                                .prepends_receiver = true,
                                .prepends_ctx = func.has_implicit_ctx,
                                .expands_defaults = if (self.l.program_index.fn_ast_map.get(qualified)) |fd| defaultsFor(fd, c.args.len + 1) else false,
                            };
                        }
                    }
                }
            }
            // Free-function UFCS: `recv.fn(args)` → `fn(recv, args)`. lowerCall
            // reaches this only when the receiver is a VALUE (the
            // `is_namespace == false` path), in which case it prepends the
            // receiver and fixes up a `*T` first param. Mirror that boundary so
            // the plan carries `prepends_receiver`, distinct from a true
            // namespace call (`pkg.fn()`), which must NOT prepend.
            if (self.objectIsValue(cfa.object)) {
                if (self.l.resolveFuncByName(cfa.field)) |fid| {
                    const func = &self.l.module.functions.items[@intFromEnum(fid)];
                    return .{
                        .kind = .free_fn_ufcs,
                        .return_type = func.ret,
                        .target = .{ .func = fid },
                        .prepends_receiver = true,
                        .prepends_ctx = func.has_implicit_ctx,
                        .expands_defaults = if (self.l.program_index.fn_ast_map.get(cfa.field)) |fd| defaultsFor(fd, c.args.len + 1) else false,
                    };
                }
                if (self.l.program_index.fn_ast_map.get(cfa.field)) |bfd| {
                    return .{
                        .kind = .free_fn_ufcs,
                        .return_type = if (bfd.return_type) |rt| self.l.resolveType(rt) else .void,
                        .target = .{ .named = cfa.field },
                        .prepends_receiver = true,
                        .expands_defaults = defaultsFor(bfd, c.args.len + 1),
                    };
                }
            }
            // Type.variant(args) — qualified construction; foreign static; or a
            // qualified namespace function. Reached for namespace / type
            // prefixes (and inert for value receivers handled above).
            const type_name = switch (cfa.object.data) {
                .identifier => |id| id.name,
                .type_expr => |te| te.name,
                else => null,
            };
            if (type_name) |tn| {
                // Foreign-class static method: `Alias.static_method(args)`.
                if (self.l.program_index.foreign_class_map.get(tn)) |fcd| {
                    for (fcd.members) |m| switch (m) {
                        .method => |md| if (md.is_static and std.mem.eql(u8, md.name, cfa.field)) {
                            return .{
                                .kind = .foreign_static,
                                .return_type = self.l.resolveForeignMethodReturnType(fcd, md),
                                .target = .{ .foreign_method = .{ .name = md.name, .is_static = true } },
                            };
                        },
                        else => {},
                    };
                }
                const type_name_id = self.l.module.types.internString(tn);
                if (self.l.module.types.findByName(type_name_id)) |ty| {
                    const ti = self.l.module.types.get(ty);
                    if (ti == .tagged_union or ti == .@"enum") return .{
                        .kind = .enum_construct,
                        .return_type = ty,
                        .target = .{ .constructed = ty },
                        .variant = self.l.resolveVariantIndex(ty, cfa.field),
                    };
                }
                // Qualified function call. `resolveFuncByName` only finds
                // ALREADY-LOWERED functions; namespace imports are typically
                // lowered lazily on demand, so a fresh `pkg.hello()` call site
                // may resolve through `fn_ast_map` first. Without this, the
                // call's return type silently falls through to `.unresolved`
                // and any pack-fn caller (e.g. `print("{}\n", pkg.hello())`)
                // mangles the arg, mis-tagging the actual string in the Any box.
                const qualified = std.fmt.allocPrint(self.l.alloc, "{s}.{s}", .{ tn, cfa.field }) catch cfa.field;
                if (self.l.resolveFuncByName(qualified)) |fid| {
                    const func = &self.l.module.functions.items[@intFromEnum(fid)];
                    return .{
                        .kind = .namespace_fn,
                        .return_type = func.ret,
                        .target = .{ .func = fid },
                        .prepends_ctx = func.has_implicit_ctx,
                        .expands_defaults = if (self.l.program_index.fn_ast_map.get(qualified)) |fd| defaultsFor(fd, c.args.len) else false,
                    };
                }
                if (self.l.program_index.fn_ast_map.get(qualified)) |qfd| {
                    return .{
                        .kind = .namespace_fn,
                        .return_type = if (qfd.return_type) |rt| self.l.resolveType(rt) else .void,
                        .target = .{ .named = qualified },
                        .expands_defaults = defaultsFor(qfd, c.args.len),
                    };
                }
                // Namespace aliases sometimes register the function under its
                // bare name (matches `lowerCall`'s effective-name resolution).
                if (self.l.program_index.fn_ast_map.get(cfa.field)) |bfd| {
                    return .{
                        .kind = .namespace_fn,
                        .return_type = if (bfd.return_type) |rt| self.l.resolveType(rt) else .void,
                        .target = .{ .named = cfa.field },
                        .expands_defaults = defaultsFor(bfd, c.args.len),
                    };
                }
            }
        } else if (c.callee.data == .enum_literal) {
            // .Variant(args) — dot-shorthand construction. Result type is
            // whatever target type is in scope; absent one it stays unresolved.
            const rt = self.l.target_type orelse .unresolved;
            var variant: ?u32 = null;
            if (self.l.target_type) |tgt| {
                if (!tgt.isBuiltin()) {
                    const ti = self.l.module.types.get(tgt);
                    if (ti == .tagged_union or ti == .@"enum")
                        variant = self.l.resolveVariantIndex(tgt, c.callee.data.enum_literal.name);
                }
            }
            return .{
                .kind = .enum_shorthand,
                .return_type = rt,
                .target = if (variant != null) .{ .constructed = rt } else .none,
                .variant = variant,
            };
        }
        return .{ .kind = .unresolved, .return_type = .unresolved };
    }

    fn refl(name: []const u8, rt: TypeId) CallPlan {
        return .{ .kind = .reflection, .return_type = rt, .target = .{ .named = name } };
    }

    /// True when a field-access receiver is a value (so `recv.fn(...)` is a
    /// method / UFCS call), false when it is a bare namespace / type prefix
    /// (so `pkg.fn(...)` is a namespace call). This is exactly the negation of
    /// `lowerCall`'s `is_namespace`: a non-identifier object is always a value;
    /// an identifier / type_expr is a value iff it names a local or a global.
    /// `pub` so `lowerCall` sources its namespace/value boundary here rather
    /// than re-deriving it — one definition, shared by typing and lowering.
    pub fn objectIsValue(self: CallResolver, obj: *const Node) bool {
        const obj_name: []const u8 = switch (obj.data) {
            .identifier => |id| id.name,
            .type_expr => |te| te.name,
            else => return true,
        };
        if (self.l.scope) |scope| {
            if (scope.lookup(obj_name) != null) return true;
        }
        return self.l.program_index.global_names.contains(obj_name);
    }

    /// True when a call supplying `supplied` leading params (user args plus a
    /// prepended receiver for methods) omits a trailing param the callee
    /// defaults — i.e. lowering will splice that default in.
    fn defaultsFor(fd: *const ast.FnDecl, supplied: usize) bool {
        if (supplied >= fd.params.len) return false;
        return fd.params[supplied].default_expr != null;
    }
};
