const std = @import("std");
const mem = std.mem;
const Allocator = mem.Allocator;
const assert = std.debug.assert;
const Tree = @import("Tree.zig");
const Token = Tree.Token;
const TokenIndex = Tree.TokenIndex;
const NodeIndex = Tree.NodeIndex;
const Type = @import("Type.zig");
const Parser = @import("Parser.zig");
const Value = @import("Value.zig");
const StringId = @import("StringInterner.zig").StringId;

const SymbolStack = @This();

pub const Symbol = struct {
    name: StringId,
    ty: Type,
    tok: TokenIndex,
    node: NodeIndex = .none,
    kind: Kind,
    val: Value,
};

pub const Kind = enum {
    typedef,
    @"struct",
    @"union",
    @"enum",
    decl,
    def,
    enumeration,
    constexpr,
};

scopes: std.ArrayListUnmanaged(Scope) = .{},
/// allocations from nested scopes are retained after popping; `active_len` is the number
/// of currently-active items in `scopes`.
active_len: usize = 0,

const Scope = struct {
    vars: std.AutoHashMapUnmanaged(StringId, Symbol) = .{},
    tags: std.AutoHashMapUnmanaged(StringId, Symbol) = .{},

    fn deinit(self: *Scope, allocator: Allocator) void {
        self.vars.deinit(allocator);
        self.tags.deinit(allocator);
    }

    fn clearRetainingCapacity(self: *Scope) void {
        self.vars.clearRetainingCapacity();
        self.tags.clearRetainingCapacity();
    }
};

pub fn deinit(s: *SymbolStack, gpa: Allocator) void {
    std.debug.assert(s.active_len == 0); // all scopes should have been popped
    for (s.scopes.items) |*scope| {
        scope.deinit(gpa);
    }
    s.scopes.deinit(gpa);
    s.* = undefined;
}

pub fn pushScope(s: *SymbolStack, p: *Parser) !void {
    if (s.active_len + 1 > s.scopes.items.len) {
        try s.scopes.append(p.gpa, .{});
        s.active_len = s.scopes.items.len;
    } else {
        s.scopes.items[s.active_len].clearRetainingCapacity();
        s.active_len += 1;
    }
}

pub fn popScope(s: *SymbolStack) void {
    s.active_len -= 1;
}

pub fn findTypedef(s: *SymbolStack, p: *Parser, name: StringId, name_tok: TokenIndex, no_type_yet: bool) !?Symbol {
    const prev = s.lookup(name, .vars) orelse s.lookup(name, .tags) orelse return null;
    switch (prev.kind) {
        .typedef => return prev,
        .@"struct" => {
            if (no_type_yet) return null;
            try p.errStr(.must_use_struct, name_tok, p.tokSlice(name_tok));
            return prev;
        },
        .@"union" => {
            if (no_type_yet) return null;
            try p.errStr(.must_use_union, name_tok, p.tokSlice(name_tok));
            return prev;
        },
        .@"enum" => {
            if (no_type_yet) return null;
            try p.errStr(.must_use_enum, name_tok, p.tokSlice(name_tok));
            return prev;
        },
        else => return null,
    }
}

pub fn findSymbol(s: *SymbolStack, name: StringId) ?Symbol {
    return s.lookup(name, .vars);
}

pub fn findTag(
    s: *SymbolStack,
    p: *Parser,
    name: StringId,
    kind: Token.Id,
    name_tok: TokenIndex,
    next_tok_id: Token.Id,
) !?Symbol {
    // `tag Name;` should always result in a new type if in a new scope.
    const prev = (if (next_tok_id == .semicolon) s.get(name, .tags) else s.lookup(name, .tags)) orelse return null;
    switch (prev.kind) {
        .@"enum" => if (kind == .keyword_enum) return prev,
        .@"struct" => if (kind == .keyword_struct) return prev,
        .@"union" => if (kind == .keyword_union) return prev,
        else => unreachable,
    }
    if (s.get(name, .tags) == null) return null;
    try p.errStr(.wrong_tag, name_tok, p.tokSlice(name_tok));
    try p.errTok(.previous_definition, prev.tok);
    return null;
}

const ScopeKind = enum {
    /// structs, enums, unions
    tags,
    /// everything else
    vars,
};

/// Return the Symbol for `name` (or null if not found) in the innermost scope
pub fn get(s: *SymbolStack, name: StringId, kind: ScopeKind) ?Symbol {
    return switch (kind) {
        .vars => s.scopes.items[s.active_len - 1].vars.get(name),
        .tags => s.scopes.items[s.active_len - 1].tags.get(name),
    };
}

/// Return the Symbol for `name` (or null if not found) in the nearest active scope,
/// starting at the innermost.
fn lookup(s: *SymbolStack, name: StringId, kind: ScopeKind) ?Symbol {
    var i = s.active_len;
    while (i > 0) {
        i -= 1;
        switch (kind) {
            .vars => if (s.scopes.items[i].vars.get(name)) |sym| return sym,
            .tags => if (s.scopes.items[i].tags.get(name)) |sym| return sym,
        }
    }
    return null;
}

/// Define a symbol in the innermost scope. Does not issue diagnostics or check correctness
/// with regard to the C standard.
pub fn define(s: *SymbolStack, allocator: Allocator, symbol: Symbol) !void {
    switch (symbol.kind) {
        .constexpr, .def, .decl, .enumeration, .typedef => {
            try s.scopes.items[s.active_len - 1].vars.put(allocator, symbol.name, symbol);
        },
        .@"struct", .@"union", .@"enum" => {
            try s.scopes.items[s.active_len - 1].tags.put(allocator, symbol.name, symbol);
        },
    }
}

pub fn defineTypedef(
    s: *SymbolStack,
    p: *Parser,
    name: StringId,
    ty: Type,
    tok: TokenIndex,
    node: NodeIndex,
) !void {
    if (s.get(name, .vars)) |prev| {
        switch (prev.kind) {
            .typedef => {
                if (!ty.eql(prev.ty, p.comp, true)) {
                    try p.errStr(.redefinition_of_typedef, tok, try p.typePairStrExtra(ty, " vs ", prev.ty));
                    if (prev.tok != 0) try p.errTok(.previous_definition, prev.tok);
                }
            },
            .enumeration, .decl, .def, .constexpr => {
                try p.errStr(.redefinition_different_sym, tok, p.tokSlice(tok));
                try p.errTok(.previous_definition, prev.tok);
            },
            else => unreachable,
        }
    }
    try s.define(p.gpa, .{
        .kind = .typedef,
        .name = name,
        .tok = tok,
        .ty = ty,
        .node = node,
        .val = .{},
    });
}

pub fn defineSymbol(
    s: *SymbolStack,
    p: *Parser,
    name: StringId,
    ty: Type,
    tok: TokenIndex,
    node: NodeIndex,
    val: Value,
    constexpr: bool,
) !void {
    if (s.get(name, .vars)) |prev| {
        switch (prev.kind) {
            .enumeration => {
                try p.errStr(.redefinition_different_sym, tok, p.tokSlice(tok));
                try p.errTok(.previous_definition, prev.tok);
            },
            .decl => {
                if (!ty.eql(prev.ty, p.comp, true)) {
                    try p.errStr(.redefinition_incompatible, tok, p.tokSlice(tok));
                    try p.errTok(.previous_definition, prev.tok);
                }
            },
            .def, .constexpr => {
                try p.errStr(.redefinition, tok, p.tokSlice(tok));
                try p.errTok(.previous_definition, prev.tok);
            },
            .typedef => {
                try p.errStr(.redefinition_different_sym, tok, p.tokSlice(tok));
                try p.errTok(.previous_definition, prev.tok);
            },
            else => unreachable,
        }
    }

    try s.define(p.gpa, .{
        .kind = if (constexpr) .constexpr else .def,
        .name = name,
        .tok = tok,
        .ty = ty,
        .node = node,
        .val = val,
    });
}

/// Get a pointer to the named symbol in the innermost scope.
/// Asserts that a symbol with the name exists.
pub fn getPtr(s: *SymbolStack, name: StringId, kind: ScopeKind) *Symbol {
    return switch (kind) {
        .tags => s.scopes.items[s.active_len - 1].tags.getPtr(name).?,
        .vars => s.scopes.items[s.active_len - 1].vars.getPtr(name).?,
    };
}

pub fn declareSymbol(
    s: *SymbolStack,
    p: *Parser,
    name: StringId,
    ty: Type,
    tok: TokenIndex,
    node: NodeIndex,
) !void {
    if (s.get(name, .vars)) |prev| {
        switch (prev.kind) {
            .enumeration => {
                try p.errStr(.redefinition_different_sym, tok, p.tokSlice(tok));
                try p.errTok(.previous_definition, prev.tok);
            },
            .decl => {
                if (!ty.eql(prev.ty, p.comp, true)) {
                    try p.errStr(.redefinition_incompatible, tok, p.tokSlice(tok));
                    try p.errTok(.previous_definition, prev.tok);
                }
            },
            .def, .constexpr => {
                if (!ty.eql(prev.ty, p.comp, true)) {
                    try p.errStr(.redefinition_incompatible, tok, p.tokSlice(tok));
                    try p.errTok(.previous_definition, prev.tok);
                } else {
                    return;
                }
            },
            .typedef => {
                try p.errStr(.redefinition_different_sym, tok, p.tokSlice(tok));
                try p.errTok(.previous_definition, prev.tok);
            },
            else => unreachable,
        }
    }
    try s.define(p.gpa, .{
        .kind = .decl,
        .name = name,
        .tok = tok,
        .ty = ty,
        .node = node,
        .val = .{},
    });
}

pub fn defineParam(s: *SymbolStack, p: *Parser, name: StringId, ty: Type, tok: TokenIndex) !void {
    if (s.get(name, .vars)) |prev| {
        switch (prev.kind) {
            .enumeration, .decl, .def, .constexpr => {
                try p.errStr(.redefinition_of_parameter, tok, p.tokSlice(tok));
                try p.errTok(.previous_definition, prev.tok);
            },
            .typedef => {
                try p.errStr(.redefinition_different_sym, tok, p.tokSlice(tok));
                try p.errTok(.previous_definition, prev.tok);
            },
            else => unreachable,
        }
    }
    if (ty.is(.fp16) and !p.comp.hasHalfPrecisionFloatABI()) {
        try p.errStr(.suggest_pointer_for_invalid_fp16, tok, "parameters");
    }
    try s.define(p.gpa, .{
        .kind = .def,
        .name = name,
        .tok = tok,
        .ty = ty,
        .val = .{},
    });
}

pub fn defineTag(
    s: *SymbolStack,
    p: *Parser,
    name: StringId,
    kind: Token.Id,
    tok: TokenIndex,
) !?Symbol {
    const prev = s.get(name, .tags) orelse return null;
    switch (prev.kind) {
        .@"enum" => {
            if (kind == .keyword_enum) return prev;
            try p.errStr(.wrong_tag, tok, p.tokSlice(tok));
            try p.errTok(.previous_definition, prev.tok);
            return null;
        },
        .@"struct" => {
            if (kind == .keyword_struct) return prev;
            try p.errStr(.wrong_tag, tok, p.tokSlice(tok));
            try p.errTok(.previous_definition, prev.tok);
            return null;
        },
        .@"union" => {
            if (kind == .keyword_union) return prev;
            try p.errStr(.wrong_tag, tok, p.tokSlice(tok));
            try p.errTok(.previous_definition, prev.tok);
            return null;
        },
        else => unreachable,
    }
}

pub fn defineEnumeration(
    s: *SymbolStack,
    p: *Parser,
    name: StringId,
    ty: Type,
    tok: TokenIndex,
    val: Value,
) !void {
    if (s.get(name, .vars)) |prev| {
        switch (prev.kind) {
            .enumeration => {
                try p.errStr(.redefinition, tok, p.tokSlice(tok));
                try p.errTok(.previous_definition, prev.tok);
                return;
            },
            .decl, .def, .constexpr => {
                try p.errStr(.redefinition_different_sym, tok, p.tokSlice(tok));
                try p.errTok(.previous_definition, prev.tok);
                return;
            },
            .typedef => {
                try p.errStr(.redefinition_different_sym, tok, p.tokSlice(tok));
                try p.errTok(.previous_definition, prev.tok);
            },
            else => unreachable,
        }
    }
    try s.define(p.gpa, .{
        .kind = .enumeration,
        .name = name,
        .tok = tok,
        .ty = ty,
        .val = val,
    });
}
