//! Represents a defined symbol.

/// Allocated address value of this symbol.
value: u64 = 0,

/// Offset into the linker's intern table.
name: MachO.String = .{},

/// File where this symbol is defined.
file: File.Index = 0,

/// Reference to Atom containing this symbol if any.
/// Use `getAtom` to get the pointer to the atom.
atom_ref: MachO.Ref = .{ .index = 0, .file = 0 },

/// Assigned output section index for this symbol.
out_n_sect: u8 = 0,

/// Index of the source nlist this symbol references.
/// Use `getNlist` to pull the nlist from the relevant file.
nlist_idx: u32 = 0,

/// Misc flags for the symbol packaged as packed struct for compression.
flags: Flags = .{},

sect_flags: std.atomic.Value(u8) = std.atomic.Value(u8).init(0),

visibility: Visibility = .local,

extra: u32 = 0,

pub fn isLocal(symbol: Symbol) bool {
    return !(symbol.flags.import or symbol.flags.@"export");
}

pub fn isSymbolStab(symbol: Symbol, macho_file: *MachO) bool {
    const file = symbol.getFile(macho_file) orelse return false;
    return switch (file) {
        .object => symbol.getNlist(macho_file).n_type.bits.is_stab != 0,
        else => false,
    };
}

pub fn isTlvInit(symbol: Symbol, macho_file: *MachO) bool {
    const name = symbol.getName(macho_file);
    return std.mem.indexOf(u8, name, "$tlv$init") != null;
}

pub fn weakRef(symbol: Symbol, macho_file: *MachO) bool {
    const file = symbol.getFile(macho_file).?;
    const is_dylib_weak = switch (file) {
        .dylib => |x| x.weak,
        else => false,
    };
    return is_dylib_weak or symbol.flags.weak_ref;
}

pub fn getName(symbol: Symbol, macho_file: *MachO) [:0]const u8 {
    return switch (symbol.getFile(macho_file).?) {
        inline else => |x| x.getString(symbol.name),
    };
}

pub fn getAtom(symbol: Symbol, macho_file: *MachO) ?*Atom {
    return symbol.atom_ref.getAtom(macho_file);
}

pub fn getOutputSectionIndex(symbol: Symbol, macho_file: *MachO) u8 {
    if (symbol.getAtom(macho_file)) |atom| return atom.out_n_sect;
    return symbol.out_n_sect;
}

pub fn getSectionFlags(symbol: Symbol) SectionFlags {
    return @bitCast(symbol.sect_flags.load(.seq_cst));
}

pub fn setSectionFlags(symbol: *Symbol, flags: SectionFlags) void {
    _ = symbol.sect_flags.fetchOr(@bitCast(flags), .seq_cst);
}

pub fn getFile(symbol: Symbol, macho_file: *MachO) ?File {
    return macho_file.getFile(symbol.file);
}

/// Asserts file is an object.
pub fn getNlist(symbol: Symbol, macho_file: *MachO) macho.nlist_64 {
    const file = symbol.getFile(macho_file).?;
    return switch (file) {
        .dylib => unreachable,
        .zig_object => unreachable,
        .object => |x| x.symtab.items(.nlist)[symbol.nlist_idx],
        .internal => |x| x.symtab.items[symbol.nlist_idx],
    };
}

pub fn getSize(symbol: Symbol, macho_file: *MachO) u64 {
    const file = symbol.getFile(macho_file).?;
    assert(file == .object);
    return file.object.symtab.items(.size)[symbol.nlist_idx];
}

pub fn getDylibOrdinal(symbol: Symbol, macho_file: *MachO) ?u16 {
    assert(symbol.flags.import);
    const file = symbol.getFile(macho_file) orelse return null;
    return switch (file) {
        .dylib => |x| x.ordinal,
        else => null,
    };
}

pub fn getSymbolRank(symbol: Symbol, macho_file: *MachO) u32 {
    const file = symbol.getFile(macho_file) orelse return std.math.maxInt(u32);
    const in_archive = switch (file) {
        .object => |x| !x.alive,
        else => false,
    };
    return file.getSymbolRank(.{
        .archive = in_archive,
        .weak = symbol.flags.weak,
        .tentative = symbol.flags.tentative,
    });
}

pub fn getAddress(symbol: Symbol, opts: struct {
    stubs: bool = true,
    trampoline: bool = true,
}, macho_file: *MachO) u64 {
    if (opts.stubs) {
        if (symbol.getSectionFlags().stubs) {
            return symbol.getStubsAddress(macho_file);
        } else if (symbol.getSectionFlags().objc_stubs) {
            return symbol.getObjcStubsAddress(macho_file);
        }
    }
    if (symbol.flags.trampoline and opts.trampoline) {
        return symbol.getTrampolineAddress(macho_file);
    }
    if (symbol.getAtom(macho_file)) |atom| return atom.getAddress(macho_file) + symbol.value;
    return symbol.value;
}

pub fn getGotAddress(symbol: Symbol, macho_file: *MachO) u64 {
    if (!symbol.getSectionFlags().has_got) return 0;
    const extra = symbol.getExtra(macho_file);
    return macho_file.got.getAddress(extra.got, macho_file);
}

pub fn getStubsAddress(symbol: Symbol, macho_file: *MachO) u64 {
    if (!symbol.getSectionFlags().stubs) return 0;
    const extra = symbol.getExtra(macho_file);
    return macho_file.stubs.getAddress(extra.stubs, macho_file);
}

pub fn getObjcStubsAddress(symbol: Symbol, macho_file: *MachO) u64 {
    if (!symbol.getSectionFlags().objc_stubs) return 0;
    const extra = symbol.getExtra(macho_file);
    return macho_file.objc_stubs.getAddress(extra.objc_stubs, macho_file);
}

pub fn getObjcSelrefsAddress(symbol: Symbol, macho_file: *MachO) u64 {
    if (!symbol.getSectionFlags().objc_stubs) return 0;
    const extra = symbol.getExtra(macho_file);
    const file = symbol.getFile(macho_file).?;
    return switch (file) {
        .dylib, .zig_object => unreachable,
        inline else => |x| x.symbols.items[extra.objc_selrefs].getAddress(.{}, macho_file),
    };
}

pub fn getTlvPtrAddress(symbol: Symbol, macho_file: *MachO) u64 {
    if (!symbol.getSectionFlags().tlv_ptr) return 0;
    const extra = symbol.getExtra(macho_file);
    return macho_file.tlv_ptr.getAddress(extra.tlv_ptr, macho_file);
}

pub fn getTrampolineAddress(symbol: Symbol, macho_file: *MachO) u64 {
    if (!symbol.flags.trampoline) return 0;
    const zo = macho_file.getZigObject().?;
    const index = symbol.getExtra(macho_file).trampoline;
    return zo.symbols.items[index].getAddress(.{}, macho_file);
}

pub fn getOutputSymtabIndex(symbol: Symbol, macho_file: *MachO) ?u32 {
    if (!symbol.flags.output_symtab) return null;
    assert(!symbol.isSymbolStab(macho_file));
    const file = symbol.getFile(macho_file).?;
    const symtab_ctx = switch (file) {
        inline else => |x| x.output_symtab_ctx,
    };
    var idx = symbol.getExtra(macho_file).symtab;
    if (symbol.isLocal()) {
        idx += symtab_ctx.ilocal;
    } else if (symbol.flags.@"export") {
        idx += symtab_ctx.iexport;
    } else {
        assert(symbol.flags.import);
        idx += symtab_ctx.iimport;
    }
    return idx;
}

const AddExtraOpts = struct {
    got: ?u32 = null,
    stubs: ?u32 = null,
    objc_stubs: ?u32 = null,
    objc_selrefs: ?u32 = null,
    tlv_ptr: ?u32 = null,
    symtab: ?u32 = null,
    trampoline: ?u32 = null,
};

pub fn addExtra(symbol: *Symbol, opts: AddExtraOpts, macho_file: *MachO) void {
    var extra = symbol.getExtra(macho_file);
    inline for (@typeInfo(@TypeOf(opts)).@"struct".field_names) |field_name| {
        if (@field(opts, field_name)) |x| {
            @field(extra, field_name) = x;
        }
    }
    symbol.setExtra(extra, macho_file);
}

pub inline fn getExtra(symbol: Symbol, macho_file: *MachO) Extra {
    return switch (symbol.getFile(macho_file).?) {
        inline else => |x| x.getSymbolExtra(symbol.extra),
    };
}

pub inline fn setExtra(symbol: Symbol, extra: Extra, macho_file: *MachO) void {
    return switch (symbol.getFile(macho_file).?) {
        inline else => |x| x.setSymbolExtra(symbol.extra, extra),
    };
}

pub fn setOutputSym(symbol: Symbol, macho_file: *MachO, out: *macho.nlist_64) void {
    if (symbol.isLocal()) {
        out.n_type = .{ .bits = .{
            .ext = false,
            .type = if (symbol.flags.abs) .abs else .sect,
            .pext = false,
            .is_stab = 0,
        } };
        out.n_sect = if (symbol.flags.abs) 0 else @intCast(symbol.getOutputSectionIndex(macho_file) + 1);
        out.n_desc = @bitCast(@as(u16, 0));
        out.n_value = symbol.getAddress(.{ .stubs = false }, macho_file);

        switch (symbol.visibility) {
            .hidden => out.n_type.bits.pext = true,
            else => {},
        }
    } else if (symbol.flags.@"export") {
        assert(symbol.visibility == .global);
        out.n_type = .{ .bits = .{
            .ext = true,
            .type = if (symbol.flags.abs) .abs else .sect,
            .pext = false,
            .is_stab = 0,
        } };
        out.n_sect = if (symbol.flags.abs) 0 else @intCast(symbol.getOutputSectionIndex(macho_file) + 1);
        out.n_value = symbol.getAddress(.{ .stubs = false }, macho_file);
        out.n_desc = @bitCast(@as(u16, 0));

        if (symbol.flags.weak) {
            out.n_desc.weak_def_or_ref_to_weak = true;
        }
        if (symbol.flags.dyn_ref) {
            out.n_desc.referenced_dynamically = true;
        }
    } else {
        assert(symbol.visibility == .global);
        out.n_type = .{ .bits = .{
            .ext = true,
            .type = .undf,
            .pext = false,
            .is_stab = 0,
        } };
        out.n_sect = 0;
        out.n_value = 0;
        out.n_desc = @bitCast(@as(u16, 0));

        // TODO:
        // const ord: u16 = if (macho_file.options.namespace == .flat)
        //     @as(u8, @bitCast(macho.BIND_SPECIAL_DYLIB_FLAT_LOOKUP))
        // else if (symbol.getDylibOrdinal(macho_file)) |ord|
        //     ord
        // else
        //     macho.BIND_SPECIAL_DYLIB_SELF;
        const ord: u16 = if (symbol.getDylibOrdinal(macho_file)) |ord|
            ord
        else
            macho.BIND_SPECIAL_DYLIB_SELF;
        out.n_desc = @bitCast(macho.N_SYMBOL_RESOLVER * ord);

        if (symbol.flags.weak) {
            out.n_desc.weak_def_or_ref_to_weak = true;
        }

        if (symbol.weakRef(macho_file)) {
            out.n_desc.weak_ref = true;
        }
    }
}

pub fn fmt(symbol: Symbol, macho_file: *MachO) std.fmt.Alt(Format, Format.default) {
    return .{ .data = .{
        .symbol = symbol,
        .macho_file = macho_file,
    } };
}

const Format = struct {
    symbol: Symbol,
    macho_file: *MachO,

    fn default(f: Format, w: *Writer) Writer.Error!void {
        const symbol = f.symbol;
        try w.print("%{d} : {s} : @{x}", .{
            symbol.nlist_idx,
            symbol.getName(f.macho_file),
            symbol.getAddress(.{}, f.macho_file),
        });
        if (symbol.getFile(f.macho_file)) |file| {
            if (symbol.getOutputSectionIndex(f.macho_file) != 0) {
                try w.print(" : sect({d})", .{symbol.getOutputSectionIndex(f.macho_file)});
            }
            if (symbol.getAtom(f.macho_file)) |atom| {
                try w.print(" : atom({d})", .{atom.atom_index});
            }
            var buf: [3]u8 = @splat('_');
            if (symbol.flags.@"export") buf[0] = 'E';
            if (symbol.flags.import) buf[1] = 'I';
            switch (symbol.visibility) {
                .local => buf[2] = 'L',
                .hidden => buf[2] = 'H',
                .global => buf[2] = 'G',
            }
            try w.print(" : {s}", .{&buf});
            if (symbol.flags.weak) try w.writeAll(" : weak");
            if (symbol.isSymbolStab(f.macho_file)) try w.writeAll(" : stab");
            switch (file) {
                .zig_object => |x| try w.print(" : zig_object({d})", .{x.index}),
                .internal => |x| try w.print(" : internal({d})", .{x.index}),
                .object => |x| try w.print(" : object({d})", .{x.index}),
                .dylib => |x| try w.print(" : dylib({d})", .{x.index}),
            }
        } else try w.writeAll(" : unresolved");
    }
};

pub const Flags = packed struct {
    /// Whether the symbol is imported at runtime.
    import: bool = false,

    /// Whether the symbol is exported at runtime.
    @"export": bool = false,

    /// Whether this symbol is weak.
    weak: bool = false,

    /// Whether this symbol is weakly referenced.
    weak_ref: bool = false,

    /// Whether this symbol is dynamically referenced.
    dyn_ref: bool = false,

    /// Whether this symbol was marked as N_NO_DEAD_STRIP.
    no_dead_strip: bool = false,

    /// Whether this symbol can be interposed at runtime.
    interposable: bool = false,

    /// Whether this symbol is absolute.
    abs: bool = false,

    /// Whether this symbol is a tentative definition.
    tentative: bool = false,

    /// Whether this symbol is a thread-local variable.
    tlv: bool = false,

    /// Whether the symbol makes into the output symtab or not.
    output_symtab: bool = false,

    /// ZigObject specific flags
    /// Whether the symbol has a trampoline
    trampoline: bool = false,
};

pub const SectionFlags = packed struct(u8) {
    /// Whether the symbol contains __got indirection.
    needs_got: bool = false,
    has_got: bool = false,

    /// Whether the symbols contains __stubs indirection.
    stubs: bool = false,

    /// Whether the symbol has a TLV pointer.
    tlv_ptr: bool = false,

    /// Whether the symbol contains __objc_stubs indirection.
    objc_stubs: bool = false,

    _: u3 = 0,
};

pub const Visibility = enum {
    global,
    hidden,
    local,

    pub fn rank(vis: Visibility) u2 {
        return switch (vis) {
            .local => 2,
            .hidden => 1,
            .global => 0,
        };
    }
};

pub const Extra = struct {
    got: u32 = 0,
    stubs: u32 = 0,
    objc_stubs: u32 = 0,
    objc_selrefs: u32 = 0,
    tlv_ptr: u32 = 0,
    symtab: u32 = 0,
    trampoline: u32 = 0,
};

pub const Index = u32;

const assert = std.debug.assert;
const macho = std.macho;
const std = @import("std");
const Writer = std.Io.Writer;

const Atom = @import("Atom.zig");
const File = @import("file.zig").File;
const MachO = @import("../MachO.zig");
const Nlist = Object.Nlist;
const Object = @import("Object.zig");
const Symbol = @This();
const ZigGotSection = @import("synthetic.zig").ZigGotSection;
