pub const GotSection = struct {
    symbols: std.ArrayList(MachO.Ref) = .empty,

    pub const Index = u32;

    pub fn deinit(got: *GotSection, allocator: Allocator) void {
        got.symbols.deinit(allocator);
    }

    pub fn addSymbol(got: *GotSection, ref: MachO.Ref, macho_file: *MachO) !void {
        const gpa = macho_file.base.comp.gpa;
        const index = @as(Index, @intCast(got.symbols.items.len));
        const entry = try got.symbols.addOne(gpa);
        entry.* = ref;
        const symbol = ref.getSymbol(macho_file).?;
        symbol.setSectionFlags(.{ .has_got = true });
        symbol.addExtra(.{ .got = index }, macho_file);
    }

    pub fn getAddress(got: GotSection, index: Index, macho_file: *MachO) u64 {
        assert(index < got.symbols.items.len);
        const header = macho_file.sections.items(.header)[macho_file.got_sect_index.?];
        return header.addr + index * @sizeOf(u64);
    }

    pub fn size(got: GotSection) usize {
        return got.symbols.items.len * @sizeOf(u64);
    }

    pub fn write(got: GotSection, macho_file: *MachO, writer: *Writer) !void {
        const tracy = trace(@src());
        defer tracy.end();
        for (got.symbols.items) |ref| {
            const sym = ref.getSymbol(macho_file).?;
            const value = if (sym.flags.import) @as(u64, 0) else sym.getAddress(.{}, macho_file);
            try writer.writeInt(u64, value, .little);
        }
    }

    const Format = struct {
        got: GotSection,
        macho_file: *MachO,

        pub fn print(f: Format, w: *Writer) Writer.Error!void {
            for (f.got.symbols.items, 0..) |ref, i| {
                const symbol = ref.getSymbol(f.macho_file).?;
                try w.print("  {d}@0x{x} => {f}@0x{x} ({s})\n", .{
                    i,
                    symbol.getGotAddress(f.macho_file),
                    ref,
                    symbol.getAddress(.{}, f.macho_file),
                    symbol.getName(f.macho_file),
                });
            }
        }
    };

    pub fn fmt(got: GotSection, macho_file: *MachO) std.fmt.Alt(Format, Format.print) {
        return .{ .data = .{ .got = got, .macho_file = macho_file } };
    }
};

pub const StubsSection = struct {
    symbols: std.ArrayList(MachO.Ref) = .empty,

    pub const Index = u32;

    pub fn deinit(stubs: *StubsSection, allocator: Allocator) void {
        stubs.symbols.deinit(allocator);
    }

    pub fn addSymbol(stubs: *StubsSection, ref: MachO.Ref, macho_file: *MachO) !void {
        const gpa = macho_file.base.comp.gpa;
        const index = @as(Index, @intCast(stubs.symbols.items.len));
        const entry = try stubs.symbols.addOne(gpa);
        entry.* = ref;
        const symbol = ref.getSymbol(macho_file).?;
        symbol.addExtra(.{ .stubs = index }, macho_file);
    }

    pub fn getAddress(stubs: StubsSection, index: Index, macho_file: *MachO) u64 {
        assert(index < stubs.symbols.items.len);
        const header = macho_file.sections.items(.header)[macho_file.stubs_sect_index.?];
        return header.addr + index * header.reserved2;
    }

    pub fn size(stubs: StubsSection, macho_file: *MachO) usize {
        const header = macho_file.sections.items(.header)[macho_file.stubs_sect_index.?];
        return stubs.symbols.items.len * header.reserved2;
    }

    pub fn write(stubs: StubsSection, macho_file: *MachO, writer: *Writer) !void {
        const tracy = trace(@src());
        defer tracy.end();
        const cpu_arch = macho_file.getTarget().cpu.arch;
        const laptr_sect = macho_file.sections.items(.header)[macho_file.la_symbol_ptr_sect_index.?];

        for (stubs.symbols.items, 0..) |ref, idx| {
            const sym = ref.getSymbol(macho_file).?;
            const source = sym.getAddress(.{ .stubs = true }, macho_file);
            const target = laptr_sect.addr + idx * @sizeOf(u64);
            switch (cpu_arch) {
                .x86_64 => {
                    try writer.writeAll(&.{ 0xff, 0x25 });
                    try writer.writeInt(i32, @intCast(target - source - 2 - 4), .little);
                },
                .aarch64 => {
                    const Instruction = aarch64.encoding.Instruction;
                    // TODO relax if possible
                    const pages = try aarch64.calcNumberOfPages(@intCast(source), @intCast(target));
                    try writer.writeInt(u32, @bitCast(Instruction.adrp(.x16, pages << 12)), .little);
                    try writer.writeInt(u32, @bitCast(Instruction.ldr(
                        .x16,
                        .{ .unsigned_offset = .{ .base = .x16, .offset = @as(u12, @truncate(target)) } },
                    )), .little);
                    try writer.writeInt(u32, @bitCast(Instruction.br(.x16)), .little);
                },
                else => unreachable,
            }
        }
    }

    pub fn fmt(stubs: StubsSection, macho_file: *MachO) std.fmt.Alt(Format, Format.print) {
        return .{ .data = .{ .stubs = stubs, .macho_file = macho_file } };
    }

    const Format = struct {
        stubs: StubsSection,
        macho_file: *MachO,

        pub fn print(f: Format, w: *Writer) Writer.Error!void {
            for (f.stubs.symbols.items, 0..) |ref, i| {
                const symbol = ref.getSymbol(f.macho_file).?;
                try w.print("  {d}@0x{x} => {f}@0x{x} ({s})\n", .{
                    i,
                    symbol.getStubsAddress(f.macho_file),
                    ref,
                    symbol.getAddress(.{}, f.macho_file),
                    symbol.getName(f.macho_file),
                });
            }
        }
    };
};

pub const StubsHelperSection = struct {
    pub inline fn preambleSize(cpu_arch: std.Target.Cpu.Arch) usize {
        return switch (cpu_arch) {
            .x86_64 => 16,
            .aarch64 => 6 * @sizeOf(u32),
            else => 0,
        };
    }

    pub inline fn entrySize(cpu_arch: std.Target.Cpu.Arch) usize {
        return switch (cpu_arch) {
            .x86_64 => 10,
            .aarch64 => 3 * @sizeOf(u32),
            else => 0,
        };
    }

    pub fn size(stubs_helper: StubsHelperSection, macho_file: *MachO) usize {
        const tracy = trace(@src());
        defer tracy.end();
        _ = stubs_helper;
        const cpu_arch = macho_file.getTarget().cpu.arch;
        var s: usize = preambleSize(cpu_arch);
        for (macho_file.stubs.symbols.items) |ref| {
            const sym = ref.getSymbol(macho_file).?;
            if (sym.flags.weak) continue;
            s += entrySize(cpu_arch);
        }
        return s;
    }

    pub fn write(stubs_helper: StubsHelperSection, macho_file: *MachO, writer: *Writer) !void {
        const tracy = trace(@src());
        defer tracy.end();

        try stubs_helper.writePreamble(macho_file, writer);

        const cpu_arch = macho_file.getTarget().cpu.arch;
        const sect = macho_file.sections.items(.header)[macho_file.stubs_helper_sect_index.?];
        const preamble_size = preambleSize(cpu_arch);
        const entry_size = entrySize(cpu_arch);

        var idx: usize = 0;
        for (macho_file.stubs.symbols.items) |ref| {
            const sym = ref.getSymbol(macho_file).?;
            if (sym.flags.weak) continue;
            const offset = macho_file.lazy_bind_section.offsets.items[idx];
            const source: i64 = @intCast(sect.addr + preamble_size + entry_size * idx);
            const target: i64 = @intCast(sect.addr);
            switch (cpu_arch) {
                .x86_64 => {
                    try writer.writeByte(0x68);
                    try writer.writeInt(u32, offset, .little);
                    try writer.writeByte(0xe9);
                    try writer.writeInt(i32, @intCast(target - source - 6 - 4), .little);
                },
                .aarch64 => {
                    const Instruction = aarch64.encoding.Instruction;
                    if (entry_size % Instruction.size != 0) return error.UnexpectedRemainder;
                    try writer.writeInt(u32, @bitCast(
                        Instruction.ldr(.w16, .{ .literal = std.math.cast(i21, entry_size - Instruction.size) orelse
                            return error.Overflow }),
                    ), .little);
                    const disp = math.cast(i28, @as(i64, @intCast(target)) - @as(i64, @intCast(source + 4))) orelse
                        return error.Overflow;
                    try writer.writeInt(u32, @bitCast(Instruction.b(disp)), .little);
                    try writer.writeInt(u32, @bitCast(Instruction.udf(0x0)), .little);
                },
                else => unreachable,
            }
            idx += 1;
        }
    }

    fn writePreamble(stubs_helper: StubsHelperSection, macho_file: *MachO, writer: *Writer) !void {
        _ = stubs_helper;
        const obj = macho_file.getInternalObject().?;
        const cpu_arch = macho_file.getTarget().cpu.arch;
        const sect = macho_file.sections.items(.header)[macho_file.stubs_helper_sect_index.?];
        const dyld_private_addr = target: {
            const sym = obj.getDyldPrivateRef(macho_file).?.getSymbol(macho_file).?;
            break :target sym.getAddress(.{}, macho_file);
        };
        const dyld_stub_binder_addr = target: {
            const sym = obj.getDyldStubBinderRef(macho_file).?.getSymbol(macho_file).?;
            break :target sym.getGotAddress(macho_file);
        };
        switch (cpu_arch) {
            .x86_64 => {
                try writer.writeAll(&.{ 0x4c, 0x8d, 0x1d });
                try writer.writeInt(i32, @intCast(dyld_private_addr - sect.addr - 3 - 4), .little);
                try writer.writeAll(&.{ 0x41, 0x53, 0xff, 0x25 });
                try writer.writeInt(i32, @intCast(dyld_stub_binder_addr - sect.addr - 11 - 4), .little);
                try writer.writeByte(0x90);
            },
            .aarch64 => {
                const Instruction = aarch64.encoding.Instruction;
                {
                    // TODO relax if possible
                    const pages = try aarch64.calcNumberOfPages(@intCast(sect.addr), @intCast(dyld_private_addr));
                    try writer.writeInt(Instruction.Backing, @bitCast(Instruction.adrp(.x17, pages << 12)), .little);
                    try writer.writeInt(Instruction.Backing, @bitCast(
                        Instruction.add(.x17, .x17, .{ .immediate = @as(u12, @truncate(dyld_private_addr)) }),
                    ), .little);
                }
                try writer.writeInt(Instruction.Backing, @bitCast(
                    Instruction.stp(.x16, .x17, .{ .pre_index = .{ .base = .sp, .index = -16 } }),
                ), .little);
                {
                    // TODO relax if possible
                    const pages = try aarch64.calcNumberOfPages(@intCast(sect.addr + 12), @intCast(dyld_stub_binder_addr));
                    try writer.writeInt(Instruction.Backing, @bitCast(Instruction.adrp(.x16, pages << 12)), .little);
                    try writer.writeInt(Instruction.Backing, @bitCast(Instruction.ldr(
                        .x16,
                        .{ .unsigned_offset = .{ .base = .x16, .offset = @as(u12, @truncate(dyld_stub_binder_addr)) } },
                    )), .little);
                }
                try writer.writeInt(Instruction.Backing, @bitCast(Instruction.br(.x16)), .little);
            },
            else => unreachable,
        }
    }
};

pub const LaSymbolPtrSection = struct {
    pub fn size(laptr: LaSymbolPtrSection, macho_file: *MachO) usize {
        _ = laptr;
        return macho_file.stubs.symbols.items.len * @sizeOf(u64);
    }

    pub fn write(laptr: LaSymbolPtrSection, macho_file: *MachO, writer: *Writer) !void {
        const tracy = trace(@src());
        defer tracy.end();
        _ = laptr;
        const cpu_arch = macho_file.getTarget().cpu.arch;
        const sect = macho_file.sections.items(.header)[macho_file.stubs_helper_sect_index.?];
        var stub_helper_idx: u32 = 0;
        for (macho_file.stubs.symbols.items) |ref| {
            const sym = ref.getSymbol(macho_file).?;
            if (sym.flags.weak) {
                const value = sym.getAddress(.{ .stubs = false }, macho_file);
                try writer.writeInt(u64, @intCast(value), .little);
            } else {
                const value = sect.addr + StubsHelperSection.preambleSize(cpu_arch) +
                    StubsHelperSection.entrySize(cpu_arch) * stub_helper_idx;
                stub_helper_idx += 1;
                try writer.writeInt(u64, @intCast(value), .little);
            }
        }
    }
};

pub const TlvPtrSection = struct {
    symbols: std.ArrayList(MachO.Ref) = .empty,

    pub const Index = u32;

    pub fn deinit(tlv: *TlvPtrSection, allocator: Allocator) void {
        tlv.symbols.deinit(allocator);
    }

    pub fn addSymbol(tlv: *TlvPtrSection, ref: MachO.Ref, macho_file: *MachO) !void {
        const gpa = macho_file.base.comp.gpa;
        const index = @as(Index, @intCast(tlv.symbols.items.len));
        const entry = try tlv.symbols.addOne(gpa);
        entry.* = ref;
        const symbol = ref.getSymbol(macho_file).?;
        symbol.addExtra(.{ .tlv_ptr = index }, macho_file);
    }

    pub fn getAddress(tlv: TlvPtrSection, index: Index, macho_file: *MachO) u64 {
        assert(index < tlv.symbols.items.len);
        const header = macho_file.sections.items(.header)[macho_file.tlv_ptr_sect_index.?];
        return header.addr + index * @sizeOf(u64);
    }

    pub fn size(tlv: TlvPtrSection) usize {
        return tlv.symbols.items.len * @sizeOf(u64);
    }

    pub fn write(tlv: TlvPtrSection, macho_file: *MachO, writer: *Writer) !void {
        const tracy = trace(@src());
        defer tracy.end();

        for (tlv.symbols.items) |ref| {
            const sym = ref.getSymbol(macho_file).?;
            if (sym.flags.import) {
                try writer.writeInt(u64, 0, .little);
            } else {
                try writer.writeInt(u64, sym.getAddress(.{}, macho_file), .little);
            }
        }
    }

    pub fn fmt(tlv: TlvPtrSection, macho_file: *MachO) std.fmt.Alt(Format, Format.print) {
        return .{ .data = .{ .tlv = tlv, .macho_file = macho_file } };
    }

    const Format = struct {
        tlv: TlvPtrSection,
        macho_file: *MachO,

        pub fn print(f: Format, w: *Writer) Writer.Error!void {
            for (f.tlv.symbols.items, 0..) |ref, i| {
                const symbol = ref.getSymbol(f.macho_file).?;
                try w.print("  {d}@0x{x} => {f}@0x{x} ({s})\n", .{
                    i,
                    symbol.getTlvPtrAddress(f.macho_file),
                    ref,
                    symbol.getAddress(.{}, f.macho_file),
                    symbol.getName(f.macho_file),
                });
            }
        }
    };
};

pub const ObjcStubsSection = struct {
    symbols: std.ArrayList(MachO.Ref) = .empty,

    pub fn deinit(objc: *ObjcStubsSection, allocator: Allocator) void {
        objc.symbols.deinit(allocator);
    }

    pub fn entrySize(cpu_arch: std.Target.Cpu.Arch) u8 {
        return switch (cpu_arch) {
            .x86_64 => 13,
            .aarch64 => 8 * @sizeOf(u32),
            else => unreachable,
        };
    }

    pub fn addSymbol(objc: *ObjcStubsSection, ref: MachO.Ref, macho_file: *MachO) !void {
        const gpa = macho_file.base.comp.gpa;
        const index = @as(Index, @intCast(objc.symbols.items.len));
        const entry = try objc.symbols.addOne(gpa);
        entry.* = ref;
        const symbol = ref.getSymbol(macho_file).?;
        symbol.addExtra(.{ .objc_stubs = index }, macho_file);
    }

    pub fn getAddress(objc: ObjcStubsSection, index: Index, macho_file: *MachO) u64 {
        assert(index < objc.symbols.items.len);
        const header = macho_file.sections.items(.header)[macho_file.objc_stubs_sect_index.?];
        return header.addr + index * entrySize(macho_file.getTarget().cpu.arch);
    }

    pub fn size(objc: ObjcStubsSection, macho_file: *MachO) usize {
        return objc.symbols.items.len * entrySize(macho_file.getTarget().cpu.arch);
    }

    pub fn write(objc: ObjcStubsSection, macho_file: *MachO, writer: *Writer) !void {
        const tracy = trace(@src());
        defer tracy.end();

        const obj = macho_file.getInternalObject().?;

        for (objc.symbols.items, 0..) |ref, idx| {
            const sym = ref.getSymbol(macho_file).?;
            const addr = objc.getAddress(@intCast(idx), macho_file);
            switch (macho_file.getTarget().cpu.arch) {
                .x86_64 => {
                    try writer.writeAll(&.{ 0x48, 0x8b, 0x35 });
                    {
                        const target = sym.getObjcSelrefsAddress(macho_file);
                        const source = addr;
                        try writer.writeInt(i32, @intCast(target - source - 3 - 4), .little);
                    }
                    try writer.writeAll(&.{ 0xff, 0x25 });
                    {
                        const target_sym = obj.getObjcMsgSendRef(macho_file).?.getSymbol(macho_file).?;
                        const target = target_sym.getGotAddress(macho_file);
                        const source = addr + 7;
                        try writer.writeInt(i32, @intCast(target - source - 2 - 4), .little);
                    }
                },
                .aarch64 => {
                    const Instruction = aarch64.encoding.Instruction;
                    {
                        const target = sym.getObjcSelrefsAddress(macho_file);
                        const source = addr;
                        const pages = try aarch64.calcNumberOfPages(@intCast(source), @intCast(target));
                        try writer.writeInt(u32, @bitCast(Instruction.adrp(.x1, pages << 12)), .little);
                        try writer.writeInt(u32, @bitCast(Instruction.ldr(
                            .x1,
                            .{ .unsigned_offset = .{ .base = .x1, .offset = @as(u12, @truncate(target)) } },
                        )), .little);
                    }
                    {
                        const target_sym = obj.getObjcMsgSendRef(macho_file).?.getSymbol(macho_file).?;
                        const target = target_sym.getGotAddress(macho_file);
                        const source = addr + 2 * @sizeOf(u32);
                        const pages = try aarch64.calcNumberOfPages(@intCast(source), @intCast(target));
                        try writer.writeInt(u32, @bitCast(Instruction.adrp(.x16, pages << 12)), .little);
                        try writer.writeInt(u32, @bitCast(Instruction.ldr(
                            .x16,
                            .{ .unsigned_offset = .{ .base = .x16, .offset = @as(u12, @truncate(target)) } },
                        )), .little);
                    }
                    try writer.writeInt(u32, @bitCast(Instruction.br(.x16)), .little);
                    try writer.writeInt(u32, @bitCast(Instruction.brk(0x1)), .little);
                    try writer.writeInt(u32, @bitCast(Instruction.brk(0x1)), .little);
                    try writer.writeInt(u32, @bitCast(Instruction.brk(0x1)), .little);
                },
                else => unreachable,
            }
        }
    }

    pub fn fmt(objc: ObjcStubsSection, macho_file: *MachO) std.fmt.Alt(Format, Format.print) {
        return .{ .data = .{ .objc = objc, .macho_file = macho_file } };
    }

    const Format = struct {
        objc: ObjcStubsSection,
        macho_file: *MachO,

        pub fn print(f: Format, w: *Writer) Writer.Error!void {
            for (f.objc.symbols.items, 0..) |ref, i| {
                const symbol = ref.getSymbol(f.macho_file).?;
                try w.print("  {d}@0x{x} => {f}@0x{x} ({s})\n", .{
                    i,
                    symbol.getObjcStubsAddress(f.macho_file),
                    ref,
                    symbol.getAddress(.{}, f.macho_file),
                    symbol.getName(f.macho_file),
                });
            }
        }
    };

    pub const Index = u32;
};

pub const Indsymtab = struct {
    pub inline fn nsyms(ind: Indsymtab, macho_file: *MachO) u32 {
        _ = ind;
        return @intCast(macho_file.stubs.symbols.items.len * 2 + macho_file.got.symbols.items.len);
    }

    pub fn updateSize(ind: *Indsymtab, macho_file: *MachO) !void {
        macho_file.dysymtab_cmd.nindirectsyms = ind.nsyms(macho_file);
    }

    pub fn write(ind: Indsymtab, macho_file: *MachO, writer: *Writer) !void {
        const tracy = trace(@src());
        defer tracy.end();

        _ = ind;

        for (macho_file.stubs.symbols.items) |ref| {
            const sym = ref.getSymbol(macho_file).?;
            if (sym.getOutputSymtabIndex(macho_file)) |idx| {
                try writer.writeInt(u32, idx, .little);
            }
        }

        for (macho_file.got.symbols.items) |ref| {
            const sym = ref.getSymbol(macho_file).?;
            if (sym.getOutputSymtabIndex(macho_file)) |idx| {
                try writer.writeInt(u32, idx, .little);
            }
        }

        for (macho_file.stubs.symbols.items) |ref| {
            const sym = ref.getSymbol(macho_file).?;
            if (sym.getOutputSymtabIndex(macho_file)) |idx| {
                try writer.writeInt(u32, idx, .little);
            }
        }
    }
};

pub const DataInCode = struct {
    entries: std.ArrayList(Entry) = .empty,

    pub fn deinit(dice: *DataInCode, allocator: Allocator) void {
        dice.entries.deinit(allocator);
    }

    pub fn size(dice: DataInCode) usize {
        return dice.entries.items.len * @sizeOf(macho.data_in_code_entry);
    }

    pub fn updateSize(dice: *DataInCode, macho_file: *MachO) !void {
        const gpa = macho_file.base.comp.gpa;

        for (macho_file.objects.items) |index| {
            const object = macho_file.getFile(index).?.object;
            const dices = object.getDataInCode();

            try dice.entries.ensureUnusedCapacity(gpa, dices.len);

            var next_dice: usize = 0;
            for (object.getAtoms()) |atom_index| {
                if (next_dice >= dices.len) break;
                const atom = object.getAtom(atom_index) orelse continue;
                const start_off = atom.getInputAddress(macho_file);
                const end_off = start_off + atom.size;
                const start_dice = next_dice;

                if (end_off < dices[next_dice].offset) continue;

                while (next_dice < dices.len and
                    dices[next_dice].offset < end_off) : (next_dice += 1)
                {}

                if (atom.isAlive()) for (dices[start_dice..next_dice]) |d| {
                    dice.entries.appendAssumeCapacity(.{
                        .atom_ref = .{ .index = atom_index, .file = index },
                        .offset = @intCast(d.offset - start_off),
                        .length = d.length,
                        .kind = d.kind,
                    });
                };
            }
        }

        macho_file.data_in_code_cmd.datasize = math.cast(u32, dice.size()) orelse return error.Overflow;
    }

    pub fn write(dice: DataInCode, macho_file: *MachO, writer: *Writer) !void {
        const base_address = if (!macho_file.base.isRelocatable())
            macho_file.getTextSegment().vmaddr
        else
            0;
        for (dice.entries.items) |entry| {
            const atom_address = entry.atom_ref.getAtom(macho_file).?.getAddress(macho_file);
            const offset = atom_address + entry.offset - base_address;
            try writer.writeStruct(@as(macho.data_in_code_entry, .{
                .offset = @intCast(offset),
                .length = entry.length,
                .kind = entry.kind,
            }), .little);
        }
    }

    const Entry = struct {
        atom_ref: MachO.Ref,
        offset: u32,
        length: u16,
        kind: u16,
    };
};

const std = @import("std");
const aarch64 = @import("../aarch64.zig");
const assert = std.debug.assert;
const macho = std.macho;
const math = std.math;
const Allocator = std.mem.Allocator;
const Writer = std.Io.Writer;

const trace = @import("../../tracy.zig").trace;
const MachO = @import("../MachO.zig");
const Symbol = @import("Symbol.zig");
