const std = @import("std");

const aro = @import("aro");

const ast = @import("ast.zig");
const Translator = @import("Translator.zig");

const Scope = @This();

pub const SymbolTable = std.StringArrayHashMapUnmanaged(ast.Node);
pub const AliasList = std.ArrayList(struct {
    alias: []const u8,
    name: []const u8,
});

/// Associates a container (structure or union) with its relevant member functions.
pub const ContainerMemberFns = struct {
    container_decl_ptr: *ast.Node,
    member_fns: std.ArrayList(*ast.Payload.Func) = .empty,
};
pub const ContainerMemberFnsHashMap = std.AutoArrayHashMapUnmanaged(aro.QualType, ContainerMemberFns);

id: Id,
parent: ?*Scope,

pub const Id = enum {
    block,
    root,
    condition,
    loop,
    do_loop,
};

/// Used for the scope of condition expressions, for example `if (cond)`.
/// The block is lazily initialized because it is only needed for rare
/// cases of comma operators being used.
pub const Condition = struct {
    base: Scope,
    block: ?Block = null,

    fn getBlockScope(cond: *Condition, t: *Translator) !*Block {
        if (cond.block) |*b| return b;
        cond.block = try Block.init(t, &cond.base, true);
        return &cond.block.?;
    }

    pub fn deinit(cond: *Condition) void {
        if (cond.block) |*b| b.deinit();
    }
};

/// Represents an in-progress Node.Block. This struct is stack-allocated.
/// When it is deinitialized, it produces an Node.Block which is allocated
/// into the main arena.
pub const Block = struct {
    base: Scope,
    translator: *Translator,
    statements: std.ArrayList(ast.Node),
    variables: AliasList,
    mangle_count: u32 = 0,
    label: ?[]const u8 = null,

    /// By default all variables are discarded, since we do not know in advance if they
    /// will be used. This maps the variable's name to the Discard payload, so that if
    /// the variable is subsequently referenced we can indicate that the discard should
    /// be skipped during the intermediate AST -> Zig AST render step.
    variable_discards: std.StringArrayHashMapUnmanaged(*ast.Payload.Discard),

    /// When the block corresponds to a function, keep track of the return type
    /// so that the return expression can be cast, if necessary
    return_type: ?aro.QualType = null,

    /// C static local variables are wrapped in a block-local struct. The struct
    /// is named `mangle(static_local_ + name)` and the Zig variable within the
    /// struct keeps the name of the C variable.
    pub const static_local_prefix = "static_local";

    /// C extern local variables are wrapped in a block-local struct. The struct
    /// is named `mangle(extern_local + name)` and the Zig variable within the
    /// struct keeps the name of the C variable.
    pub const extern_local_prefix = "extern_local";

    pub fn init(t: *Translator, parent: *Scope, labeled: bool) !Block {
        var blk: Block = .{
            .base = .{
                .id = .block,
                .parent = parent,
            },
            .translator = t,
            .statements = .empty,
            .variables = .empty,
            .variable_discards = .empty,
        };
        if (labeled) {
            blk.label = try blk.makeMangledName("blk");
        }
        return blk;
    }

    pub fn deinit(block: *Block) void {
        block.statements.deinit(block.translator.gpa);
        block.variables.deinit(block.translator.gpa);
        block.variable_discards.deinit(block.translator.gpa);
        block.* = undefined;
    }

    pub fn complete(block: *Block) !ast.Node {
        const arena = block.translator.arena;
        if (block.base.parent.?.id == .do_loop) {
            // We reserve 1 extra statement if the parent is a do_loop. This is in case of
            // do while, we want to put `if (cond) break;` at the end.
            const alloc_len = block.statements.items.len + @intFromBool(block.base.parent.?.id == .do_loop);
            var stmts = try arena.alloc(ast.Node, alloc_len);
            stmts.len = block.statements.items.len;
            @memcpy(stmts[0..block.statements.items.len], block.statements.items);
            return ast.Node.Tag.block.create(arena, .{
                .label = block.label,
                .stmts = stmts,
            });
        }
        if (block.statements.items.len == 0) return ast.Node.Tag.empty_block.init();
        return ast.Node.Tag.block.create(arena, .{
            .label = block.label,
            .stmts = try arena.dupe(ast.Node, block.statements.items),
        });
    }

    /// Given the desired name, return a name that does not shadow anything from outer scopes.
    /// Inserts the returned name into the scope.
    /// The name will not be visible to callers of getAlias.
    pub fn reserveMangledName(block: *Block, name: []const u8) ![]const u8 {
        return block.createMangledName(name, true, null);
    }

    /// Same as reserveMangledName, but enables the alias immediately.
    pub fn makeMangledName(block: *Block, name: []const u8) ![]const u8 {
        return block.createMangledName(name, false, null);
    }

    pub fn createMangledName(block: *Block, name: []const u8, reservation: bool, prefix_opt: ?[]const u8) ![]const u8 {
        const arena = block.translator.arena;
        const name_copy = try arena.dupe(u8, name);
        const alias_base = if (prefix_opt) |prefix|
            try std.fmt.allocPrint(arena, "{s}_{s}", .{ prefix, name })
        else
            name;
        var proposed_name = alias_base;
        while (block.contains(proposed_name)) {
            block.mangle_count += 1;
            proposed_name = try std.fmt.allocPrint(arena, "{s}_{d}", .{ alias_base, block.mangle_count });
        }
        const new_mangle = try block.variables.addOne(block.translator.gpa);
        if (reservation) {
            new_mangle.* = .{ .name = name_copy, .alias = name_copy };
        } else {
            new_mangle.* = .{ .name = name_copy, .alias = proposed_name };
        }
        return proposed_name;
    }

    fn getAlias(block: *Block, name: []const u8) ?[]const u8 {
        for (block.variables.items) |p| {
            if (std.mem.eql(u8, p.name, name))
                return p.alias;
        }
        return block.base.parent.?.getAlias(name);
    }

    fn localContains(block: *Block, name: []const u8) bool {
        for (block.variables.items) |p| {
            if (std.mem.eql(u8, p.alias, name))
                return true;
        }
        return false;
    }

    fn contains(block: *Block, name: []const u8) bool {
        if (block.localContains(name))
            return true;
        return block.base.parent.?.contains(name);
    }

    pub fn discardVariable(block: *Block, name: []const u8) Translator.Error!void {
        const gpa = block.translator.gpa;
        const arena = block.translator.arena;
        const name_node = try ast.Node.Tag.identifier.create(arena, name);
        const discard = try ast.Node.Tag.discard.create(arena, .{ .should_skip = false, .value = name_node });
        try block.statements.append(gpa, discard);
        try block.variable_discards.putNoClobber(gpa, name, discard.castTag(.discard).?);
    }
};

pub const Root = struct {
    base: Scope,
    translator: *Translator,
    sym_table: SymbolTable,
    blank_macros: std.StringArrayHashMapUnmanaged(void),
    nodes: std.ArrayList(ast.Node),
    container_member_fns_map: ContainerMemberFnsHashMap,

    pub fn init(t: *Translator) Root {
        return .{
            .base = .{
                .id = .root,
                .parent = null,
            },
            .translator = t,
            .sym_table = .empty,
            .blank_macros = .empty,
            .nodes = .empty,
            .container_member_fns_map = .empty,
        };
    }

    pub fn deinit(root: *Root) void {
        root.sym_table.deinit(root.translator.gpa);
        root.blank_macros.deinit(root.translator.gpa);
        root.nodes.deinit(root.translator.gpa);
        for (root.container_member_fns_map.values()) |*members| {
            members.member_fns.deinit(root.translator.gpa);
        }
        root.container_member_fns_map.deinit(root.translator.gpa);
    }

    /// Check if the global scope contains this name, without looking into the "future", e.g.
    /// ignore the preprocessed decl and macro names.
    pub fn containsNow(root: *Root, name: []const u8) bool {
        return root.sym_table.contains(name);
    }

    /// Check if the global scope contains the name, includes all decls that haven't been translated yet.
    pub fn contains(root: *Root, name: []const u8) bool {
        return root.containsNow(name) or root.translator.global_names.contains(name) or root.translator.weak_global_names.contains(name);
    }

    pub fn addMemberFunction(root: *Root, func_ty: aro.Type.Func, func: *ast.Payload.Func) !void {
        std.debug.assert(func.data.name != null);
        if (func_ty.params.len == 0) return;

        const param1_base = func_ty.params[0].qt.base(root.translator.comp);
        const container_qt = if (param1_base.type == .pointer)
            param1_base.type.pointer.child.base(root.translator.comp).qt
        else
            param1_base.qt;

        if (root.container_member_fns_map.getPtr(container_qt)) |members| {
            try members.member_fns.append(root.translator.gpa, func);
        }
    }

    pub fn processContainerMemberFns(root: *Root) !void {
        const gpa = root.translator.gpa;
        const arena = root.translator.arena;

        var member_names: std.StringArrayHashMapUnmanaged(void) = .empty;
        defer member_names.deinit(gpa);
        for (root.container_member_fns_map.values()) |members| {
            member_names.clearRetainingCapacity();
            const decls_ptr = switch (members.container_decl_ptr.tag()) {
                .@"struct", .@"union" => blk_record: {
                    const payload: *ast.Payload.Container = @alignCast(@fieldParentPtr("base", members.container_decl_ptr.ptr_otherwise));
                    // Avoid duplication with field names
                    for (payload.data.fields) |field| {
                        try member_names.put(gpa, field.name, {});
                    }
                    break :blk_record &payload.data.decls;
                },
                .opaque_literal => blk_opaque: {
                    const container_decl = try ast.Node.Tag.@"opaque".create(arena, .{
                        .layout = .none,
                        .fields = &.{},
                        .decls = &.{},
                    });
                    members.container_decl_ptr.* = container_decl;
                    break :blk_opaque &container_decl.castTag(.@"opaque").?.data.decls;
                },
                else => return,
            };

            const old_decls = decls_ptr.*;
            const new_decls = try arena.alloc(ast.Node, old_decls.len + members.member_fns.items.len * 2);
            @memcpy(new_decls[0..old_decls.len], old_decls);
            // Assume the allocator of payload.data.decls is arena,
            // so don't add arena.free(old_variables).
            const func_ref_vars = new_decls[old_decls.len..];
            var count: u32 = 0;

            // Add members without mangling them - only fields may cause name conflicts
            for (members.member_fns.items) |func| {
                const func_name = func.data.name.?;
                const member_name_slot = try member_names.getOrPutValue(gpa, func_name, {});
                if (member_name_slot.found_existing) continue;
                func_ref_vars[count] = try ast.Node.Tag.pub_var_simple.create(arena, .{
                    .name = func_name,
                    .init = try ast.Node.Tag.root_ref.create(arena, func_name),
                });
                count += 1;
            }

            for (members.member_fns.items) |func| {
                const func_name = func.data.name.?;
                const func_name_trimmed = std.mem.trimEnd(u8, func_name, "_");
                const last_idx = std.mem.findLast(u8, func_name_trimmed, "_") orelse continue;
                const func_name_alias = func_name[last_idx + 1 ..];
                const member_name_slot = try member_names.getOrPutValue(gpa, func_name_alias, {});
                if (member_name_slot.found_existing) continue;
                func_ref_vars[count] = try ast.Node.Tag.pub_var_simple.create(arena, .{
                    .name = func_name_alias,
                    .init = try ast.Node.Tag.root_ref.create(arena, func_name),
                });
                count += 1;
            }

            decls_ptr.* = new_decls[0 .. old_decls.len + count];
        }
    }
};

pub fn findBlockScope(inner: *Scope, t: *Translator) !*Block {
    var scope = inner;
    while (true) {
        switch (scope.id) {
            .root => unreachable,
            .block => return @fieldParentPtr("base", scope),
            .condition => return @as(*Condition, @fieldParentPtr("base", scope)).getBlockScope(t),
            else => scope = scope.parent.?,
        }
    }
}

pub fn findBlockReturnType(inner: *Scope) aro.QualType {
    var scope = inner;
    while (true) {
        switch (scope.id) {
            .root => unreachable,
            .block => {
                const block: *Block = @fieldParentPtr("base", scope);
                if (block.return_type) |qt| return qt;
                scope = scope.parent.?;
            },
            else => scope = scope.parent.?,
        }
    }
}

pub fn getAlias(scope: *Scope, name: []const u8) ?[]const u8 {
    return switch (scope.id) {
        .root => null,
        .block => @as(*Block, @fieldParentPtr("base", scope)).getAlias(name),
        .loop, .do_loop, .condition => scope.parent.?.getAlias(name),
    };
}

fn contains(scope: *Scope, name: []const u8) bool {
    return switch (scope.id) {
        .root => @as(*Root, @fieldParentPtr("base", scope)).contains(name),
        .block => @as(*Block, @fieldParentPtr("base", scope)).contains(name),
        .loop, .do_loop, .condition => scope.parent.?.contains(name),
    };
}

/// Appends a node to the first block scope if inside a function, or to the root tree if not.
pub fn appendNode(inner: *Scope, node: ast.Node) !void {
    var scope = inner;
    while (true) {
        switch (scope.id) {
            .root => {
                const root: *Root = @fieldParentPtr("base", scope);
                return root.nodes.append(root.translator.gpa, node);
            },
            .block => {
                const block: *Block = @fieldParentPtr("base", scope);
                return block.statements.append(block.translator.gpa, node);
            },
            else => scope = scope.parent.?,
        }
    }
}

pub fn skipVariableDiscard(inner: *Scope, name: []const u8) void {
    if (true) {
        // TODO: due to 'local variable is never mutated' errors, we can
        // only skip discards if a variable is used as an lvalue, which
        // we don't currently have detection for in translate-c.
        // Once #17584 is completed, perhaps we can do away with this
        // logic entirely, and instead rely on render to fixup code.
        return;
    }
    var scope = inner;
    while (true) {
        switch (scope.id) {
            .root => return,
            .block => {
                const block: *Block = @fieldParentPtr("base", scope);
                if (block.variable_discards.get(name)) |discard| {
                    discard.data.should_skip = true;
                    return;
                }
            },
            else => {},
        }
        scope = scope.parent.?;
    }
}
