pub const Cie = struct {
    /// Includes 4byte size cell.
    offset: u32,
    out_offset: u32 = 0,
    size: u32,
    lsda_size: ?enum { p32, p64 } = null,
    personality: ?Personality = null,
    file: File.Index = 0,
    alive: bool = false,

    pub fn parse(cie: *Cie, macho_file: *MachO) !void {
        const tracy = trace(@src());
        defer tracy.end();

        const data = cie.getData(macho_file);
        const aug = std.mem.sliceTo(@as([*:0]const u8, @ptrCast(data.ptr + 9)), 0);

        if (aug[0] != 'z') return; // TODO should we error out?

        var stream = std.io.fixedBufferStream(data[9 + aug.len + 1 ..]);
        var creader = std.io.countingReader(stream.reader());
        const reader = creader.reader();

        _ = try leb.readUleb128(u64, reader); // code alignment factor
        _ = try leb.readUleb128(u64, reader); // data alignment factor
        _ = try leb.readUleb128(u64, reader); // return address register
        _ = try leb.readUleb128(u64, reader); // augmentation data length

        for (aug[1..]) |ch| switch (ch) {
            'R' => {
                const enc = try reader.readByte();
                if (enc != DW_EH_PE.pcrel | DW_EH_PE.absptr) {
                    @panic("unexpected pointer encoding"); // TODO error
                }
            },
            'P' => {
                const enc = try reader.readByte();
                if (enc != DW_EH_PE.pcrel | DW_EH_PE.indirect | DW_EH_PE.sdata4) {
                    @panic("unexpected personality pointer encoding"); // TODO error
                }
                _ = try reader.readInt(u32, .little); // personality pointer
            },
            'L' => {
                const enc = try reader.readByte();
                switch (enc & DW_EH_PE.type_mask) {
                    DW_EH_PE.sdata4 => cie.lsda_size = .p32,
                    DW_EH_PE.absptr => cie.lsda_size = .p64,
                    else => unreachable, // TODO error
                }
            },
            else => @panic("unexpected augmentation string"), // TODO error
        };
    }

    pub inline fn getSize(cie: Cie) u32 {
        return cie.size + 4;
    }

    pub fn getObject(cie: Cie, macho_file: *MachO) *Object {
        const file = macho_file.getFile(cie.file).?;
        return file.object;
    }

    pub fn getData(cie: Cie, macho_file: *MachO) []const u8 {
        const object = cie.getObject(macho_file);
        return object.eh_frame_data.items[cie.offset..][0..cie.getSize()];
    }

    pub fn getPersonality(cie: Cie, macho_file: *MachO) ?*Symbol {
        const personality = cie.personality orelse return null;
        const object = cie.getObject(macho_file);
        return object.getSymbolRef(personality.index, macho_file).getSymbol(macho_file);
    }

    pub fn eql(cie: Cie, other: Cie, macho_file: *MachO) bool {
        if (!std.mem.eql(u8, cie.getData(macho_file), other.getData(macho_file))) return false;
        if (cie.personality != null and other.personality != null) {
            if (cie.personality.?.index != other.personality.?.index) return false;
        }
        if (cie.personality != null or other.personality != null) return false;
        return true;
    }

    pub fn format(
        cie: Cie,
        comptime unused_fmt_string: []const u8,
        options: std.fmt.FormatOptions,
        writer: anytype,
    ) !void {
        _ = cie;
        _ = unused_fmt_string;
        _ = options;
        _ = writer;
        @compileError("do not format CIEs directly");
    }

    pub fn fmt(cie: Cie, macho_file: *MachO) std.fmt.Formatter(format2) {
        return .{ .data = .{
            .cie = cie,
            .macho_file = macho_file,
        } };
    }

    const FormatContext = struct {
        cie: Cie,
        macho_file: *MachO,
    };

    fn format2(
        ctx: FormatContext,
        comptime unused_fmt_string: []const u8,
        options: std.fmt.FormatOptions,
        writer: anytype,
    ) !void {
        _ = unused_fmt_string;
        _ = options;
        const cie = ctx.cie;
        try writer.print("@{x} : size({x})", .{
            cie.offset,
            cie.getSize(),
        });
        if (!cie.alive) try writer.writeAll(" : [*]");
    }

    pub const Index = u32;

    pub const Personality = struct {
        index: Symbol.Index = 0,
        offset: u32 = 0,
    };
};

pub const Fde = struct {
    /// Includes 4byte size cell.
    offset: u32,
    out_offset: u32 = 0,
    size: u32,
    cie: Cie.Index,
    atom: Atom.Index = 0,
    atom_offset: u32 = 0,
    lsda: Atom.Index = 0,
    lsda_offset: u32 = 0,
    lsda_ptr_offset: u32 = 0,
    file: File.Index = 0,
    alive: bool = true,

    pub fn parse(fde: *Fde, macho_file: *MachO) !void {
        const tracy = trace(@src());
        defer tracy.end();

        const data = fde.getData(macho_file);
        const object = fde.getObject(macho_file);
        const sect = object.sections.items(.header)[object.eh_frame_sect_index.?];

        // Parse target atom index
        const pc_begin = std.mem.readInt(i64, data[8..][0..8], .little);
        const taddr: u64 = @intCast(@as(i64, @intCast(sect.addr + fde.offset + 8)) + pc_begin);
        fde.atom = object.findAtom(taddr) orelse {
            try macho_file.reportParseError2(object.index, "{s},{s}: 0x{x}: invalid function reference in FDE", .{
                sect.segName(), sect.sectName(), fde.offset + 8,
            });
            return error.MalformedObject;
        };
        const atom = fde.getAtom(macho_file);
        fde.atom_offset = @intCast(taddr - atom.getInputAddress(macho_file));

        // Associate with a CIE
        const cie_ptr = std.mem.readInt(u32, data[4..8], .little);
        const cie_offset = fde.offset + 4 - cie_ptr;
        const cie_index = for (object.cies.items, 0..) |cie, cie_index| {
            if (cie.offset == cie_offset) break @as(Cie.Index, @intCast(cie_index));
        } else null;
        if (cie_index) |cie| {
            fde.cie = cie;
        } else {
            try macho_file.reportParseError2(object.index, "no matching CIE found for FDE at offset {x}", .{
                fde.offset,
            });
            return error.MalformedObject;
        }

        const cie = fde.getCie(macho_file);

        // Parse LSDA atom index if any
        if (cie.lsda_size) |lsda_size| {
            var stream = std.io.fixedBufferStream(data[24..]);
            var creader = std.io.countingReader(stream.reader());
            const reader = creader.reader();
            _ = try leb.readUleb128(u64, reader); // augmentation length
            fde.lsda_ptr_offset = @intCast(creader.bytes_read + 24);
            const lsda_ptr = switch (lsda_size) {
                .p32 => try reader.readInt(i32, .little),
                .p64 => try reader.readInt(i64, .little),
            };
            const lsda_addr: u64 = @intCast(@as(i64, @intCast(sect.addr + fde.offset + fde.lsda_ptr_offset)) + lsda_ptr);
            fde.lsda = object.findAtom(lsda_addr) orelse {
                try macho_file.reportParseError2(object.index, "{s},{s}: 0x{x}: invalid LSDA reference in FDE", .{
                    sect.segName(), sect.sectName(), fde.offset + fde.lsda_ptr_offset,
                });
                return error.MalformedObject;
            };
            const lsda_atom = fde.getLsdaAtom(macho_file).?;
            fde.lsda_offset = @intCast(lsda_addr - lsda_atom.getInputAddress(macho_file));
        }
    }

    pub inline fn getSize(fde: Fde) u32 {
        return fde.size + 4;
    }

    pub fn getObject(fde: Fde, macho_file: *MachO) *Object {
        const file = macho_file.getFile(fde.file).?;
        return file.object;
    }

    pub fn getData(fde: Fde, macho_file: *MachO) []const u8 {
        const object = fde.getObject(macho_file);
        return object.eh_frame_data.items[fde.offset..][0..fde.getSize()];
    }

    pub fn getCie(fde: Fde, macho_file: *MachO) *const Cie {
        const object = fde.getObject(macho_file);
        return &object.cies.items[fde.cie];
    }

    pub fn getAtom(fde: Fde, macho_file: *MachO) *Atom {
        return fde.getObject(macho_file).getAtom(fde.atom).?;
    }

    pub fn getLsdaAtom(fde: Fde, macho_file: *MachO) ?*Atom {
        return fde.getObject(macho_file).getAtom(fde.lsda);
    }

    pub fn format(
        fde: Fde,
        comptime unused_fmt_string: []const u8,
        options: std.fmt.FormatOptions,
        writer: anytype,
    ) !void {
        _ = fde;
        _ = unused_fmt_string;
        _ = options;
        _ = writer;
        @compileError("do not format FDEs directly");
    }

    pub fn fmt(fde: Fde, macho_file: *MachO) std.fmt.Formatter(format2) {
        return .{ .data = .{
            .fde = fde,
            .macho_file = macho_file,
        } };
    }

    const FormatContext = struct {
        fde: Fde,
        macho_file: *MachO,
    };

    fn format2(
        ctx: FormatContext,
        comptime unused_fmt_string: []const u8,
        options: std.fmt.FormatOptions,
        writer: anytype,
    ) !void {
        _ = unused_fmt_string;
        _ = options;
        const fde = ctx.fde;
        const macho_file = ctx.macho_file;
        try writer.print("@{x} : size({x}) : cie({d}) : {s}", .{
            fde.offset,
            fde.getSize(),
            fde.cie,
            fde.getAtom(macho_file).getName(macho_file),
        });
        if (!fde.alive) try writer.writeAll(" : [*]");
    }

    pub const Index = u32;
};

pub const Iterator = struct {
    data: []const u8,
    pos: u32 = 0,

    pub const Record = struct {
        tag: enum { fde, cie },
        offset: u32,
        size: u32,
    };

    pub fn next(it: *Iterator) !?Record {
        if (it.pos >= it.data.len) return null;

        var stream = std.io.fixedBufferStream(it.data[it.pos..]);
        const reader = stream.reader();

        const size = try reader.readInt(u32, .little);
        if (size == 0xFFFFFFFF) @panic("DWARF CFI is 32bit on macOS");

        const id = try reader.readInt(u32, .little);
        const record = Record{
            .tag = if (id == 0) .cie else .fde,
            .offset = it.pos,
            .size = size,
        };
        it.pos += size + 4;

        return record;
    }
};

pub fn calcSize(macho_file: *MachO) !u32 {
    const tracy = trace(@src());
    defer tracy.end();

    var offset: u32 = 0;

    var cies = std.ArrayList(Cie).init(macho_file.base.comp.gpa);
    defer cies.deinit();

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

        outer: for (object.cies.items) |*cie| {
            for (cies.items) |other| {
                if (other.eql(cie.*, macho_file)) {
                    // We already have a CIE record that has the exact same contents, so instead of
                    // duplicating them, we mark this one dead and set its output offset to be
                    // equal to that of the alive record. This way, we won't have to rewrite
                    // Fde.cie_index field when committing the records to file.
                    cie.out_offset = other.out_offset;
                    continue :outer;
                }
            }
            cie.alive = true;
            cie.out_offset = offset;
            offset += cie.getSize();
            try cies.append(cie.*);
        }
    }

    for (macho_file.objects.items) |index| {
        const object = macho_file.getFile(index).?.object;
        for (object.fdes.items) |*fde| {
            if (!fde.alive) continue;
            fde.out_offset = offset;
            offset += fde.getSize();
        }
    }

    return offset;
}

pub fn calcNumRelocs(macho_file: *MachO) u32 {
    const tracy = trace(@src());
    defer tracy.end();

    var nreloc: u32 = 0;

    for (macho_file.objects.items) |index| {
        const object = macho_file.getFile(index).?.object;
        for (object.cies.items) |cie| {
            if (!cie.alive) continue;
            if (cie.getPersonality(macho_file)) |_| {
                nreloc += 1; // personality
            }
        }
    }

    return nreloc;
}

pub fn write(macho_file: *MachO, buffer: []u8) void {
    const tracy = trace(@src());
    defer tracy.end();

    const sect = macho_file.sections.items(.header)[macho_file.eh_frame_sect_index.?];
    const addend: i64 = switch (macho_file.getTarget().cpu.arch) {
        .x86_64 => 4,
        else => 0,
    };

    for (macho_file.objects.items) |index| {
        const object = macho_file.getFile(index).?.object;
        for (object.cies.items) |cie| {
            if (!cie.alive) continue;

            @memcpy(buffer[cie.out_offset..][0..cie.getSize()], cie.getData(macho_file));

            if (cie.getPersonality(macho_file)) |sym| {
                const offset = cie.out_offset + cie.personality.?.offset;
                const saddr = sect.addr + offset;
                const taddr = sym.getGotAddress(macho_file);
                std.mem.writeInt(
                    i32,
                    buffer[offset..][0..4],
                    @intCast(@as(i64, @intCast(taddr)) - @as(i64, @intCast(saddr)) + addend),
                    .little,
                );
            }
        }
    }

    for (macho_file.objects.items) |index| {
        const object = macho_file.getFile(index).?.object;
        for (object.fdes.items) |fde| {
            if (!fde.alive) continue;

            @memcpy(buffer[fde.out_offset..][0..fde.getSize()], fde.getData(macho_file));

            {
                const offset = fde.out_offset + 4;
                const value = offset - fde.getCie(macho_file).out_offset;
                std.mem.writeInt(u32, buffer[offset..][0..4], value, .little);
            }

            {
                const offset = fde.out_offset + 8;
                const saddr = sect.addr + offset;
                const taddr = fde.getAtom(macho_file).getAddress(macho_file);
                std.mem.writeInt(
                    i64,
                    buffer[offset..][0..8],
                    @as(i64, @intCast(taddr)) - @as(i64, @intCast(saddr)),
                    .little,
                );
            }

            if (fde.getLsdaAtom(macho_file)) |atom| {
                const offset = fde.out_offset + fde.lsda_ptr_offset;
                const saddr = sect.addr + offset;
                const taddr = atom.getAddress(macho_file) + fde.lsda_offset;
                switch (fde.getCie(macho_file).lsda_size.?) {
                    .p32 => std.mem.writeInt(
                        i32,
                        buffer[offset..][0..4],
                        @intCast(@as(i64, @intCast(taddr)) - @as(i64, @intCast(saddr)) + addend),
                        .little,
                    ),
                    .p64 => std.mem.writeInt(
                        i64,
                        buffer[offset..][0..8],
                        @as(i64, @intCast(taddr)) - @as(i64, @intCast(saddr)),
                        .little,
                    ),
                }
            }
        }
    }
}

pub fn writeRelocs(macho_file: *MachO, code: []u8, relocs: []macho.relocation_info) error{Overflow}!void {
    const tracy = trace(@src());
    defer tracy.end();

    const cpu_arch = macho_file.getTarget().cpu.arch;
    const sect = macho_file.sections.items(.header)[macho_file.eh_frame_sect_index.?];
    const addend: i64 = switch (cpu_arch) {
        .x86_64 => 4,
        else => 0,
    };

    var i: usize = 0;
    for (macho_file.objects.items) |index| {
        const object = macho_file.getFile(index).?.object;
        for (object.cies.items) |cie| {
            if (!cie.alive) continue;

            @memcpy(code[cie.out_offset..][0..cie.getSize()], cie.getData(macho_file));

            if (cie.getPersonality(macho_file)) |sym| {
                const r_address = math.cast(i32, cie.out_offset + cie.personality.?.offset) orelse return error.Overflow;
                const r_symbolnum = math.cast(u24, sym.getOutputSymtabIndex(macho_file).?) orelse return error.Overflow;
                relocs[i] = .{
                    .r_address = r_address,
                    .r_symbolnum = r_symbolnum,
                    .r_length = 2,
                    .r_extern = 1,
                    .r_pcrel = 1,
                    .r_type = switch (cpu_arch) {
                        .aarch64 => @intFromEnum(macho.reloc_type_arm64.ARM64_RELOC_POINTER_TO_GOT),
                        .x86_64 => @intFromEnum(macho.reloc_type_x86_64.X86_64_RELOC_GOT),
                        else => unreachable,
                    },
                };
                i += 1;
            }
        }
    }

    for (macho_file.objects.items) |index| {
        const object = macho_file.getFile(index).?.object;
        for (object.fdes.items) |fde| {
            if (!fde.alive) continue;

            @memcpy(code[fde.out_offset..][0..fde.getSize()], fde.getData(macho_file));

            {
                const offset = fde.out_offset + 4;
                const value = offset - fde.getCie(macho_file).out_offset;
                std.mem.writeInt(u32, code[offset..][0..4], value, .little);
            }

            {
                const offset = fde.out_offset + 8;
                const saddr = sect.addr + offset;
                const taddr = fde.getAtom(macho_file).getAddress(macho_file);
                std.mem.writeInt(
                    i64,
                    code[offset..][0..8],
                    @as(i64, @intCast(taddr)) - @as(i64, @intCast(saddr)),
                    .little,
                );
            }

            if (fde.getLsdaAtom(macho_file)) |atom| {
                const offset = fde.out_offset + fde.lsda_ptr_offset;
                const saddr = sect.addr + offset;
                const taddr = atom.getAddress(macho_file) + fde.lsda_offset;
                switch (fde.getCie(macho_file).lsda_size.?) {
                    .p32 => std.mem.writeInt(
                        i32,
                        code[offset..][0..4],
                        @intCast(@as(i64, @intCast(taddr)) - @as(i64, @intCast(saddr)) + addend),
                        .little,
                    ),
                    .p64 => std.mem.writeInt(
                        i64,
                        code[offset..][0..8],
                        @as(i64, @intCast(taddr)) - @as(i64, @intCast(saddr)),
                        .little,
                    ),
                }
            }
        }
    }

    assert(relocs.len == i);
}

const assert = std.debug.assert;
const leb = std.leb;
const macho = std.macho;
const math = std.math;
const mem = std.mem;
const std = @import("std");
const trace = @import("../../tracy.zig").trace;

const Allocator = std.mem.Allocator;
const Atom = @import("Atom.zig");
const DW_EH_PE = std.dwarf.EH.PE;
const File = @import("file.zig").File;
const MachO = @import("../MachO.zig");
const Object = @import("Object.zig");
const Symbol = @import("Symbol.zig");
