pub fn gcAtoms(macho_file: *MachO) !void {
    const gpa = macho_file.base.comp.gpa;

    var objects = try std.array_list.Managed(File.Index).initCapacity(gpa, macho_file.objects.items.len + 1);
    defer objects.deinit();
    for (macho_file.objects.items) |index| objects.appendAssumeCapacity(index);
    if (macho_file.internal_object) |index| objects.appendAssumeCapacity(index);

    var roots = std.array_list.Managed(*Atom).init(gpa);
    defer roots.deinit();

    try collectRoots(&roots, objects.items, macho_file);
    mark(roots.items, objects.items, macho_file);
    prune(objects.items, macho_file);
}

fn collectRoots(roots: *std.array_list.Managed(*Atom), objects: []const File.Index, macho_file: *MachO) !void {
    for (objects) |index| {
        const object = macho_file.getFile(index).?;
        for (object.getSymbols(), 0..) |*sym, i| {
            const ref = object.getSymbolRef(@intCast(i), macho_file);
            const file = ref.getFile(macho_file) orelse continue;
            if (file.getIndex() != index) continue;
            if (sym.flags.no_dead_strip or (macho_file.base.isDynLib() and sym.visibility == .global))
                try markSymbol(sym, roots, macho_file);
        }

        for (object.getAtoms()) |atom_index| {
            const atom = object.getAtom(atom_index) orelse continue;
            const isec = atom.getInputSection(macho_file);
            switch (isec.type()) {
                macho.S_MOD_INIT_FUNC_POINTERS,
                macho.S_MOD_TERM_FUNC_POINTERS,
                => if (markAtom(atom)) try roots.append(atom),

                else => if (isec.isDontDeadStrip() and markAtom(atom)) {
                    try roots.append(atom);
                },
            }
        }
    }

    for (macho_file.objects.items) |index| {
        const object = macho_file.getFile(index).?.object;
        for (object.unwind_records_indexes.items) |cu_index| {
            const cu = object.getUnwindRecord(cu_index);
            if (!cu.alive) continue;
            if (cu.getFde(macho_file)) |fde| {
                if (fde.getCie(macho_file).getPersonality(macho_file)) |sym| try markSymbol(sym, roots, macho_file);
            } else if (cu.getPersonality(macho_file)) |sym| try markSymbol(sym, roots, macho_file);
        }
    }

    if (macho_file.getInternalObject()) |obj| {
        for (obj.force_undefined.items) |sym_index| {
            const ref = obj.getSymbolRef(sym_index, macho_file);
            if (ref.getFile(macho_file) != null) {
                const sym = ref.getSymbol(macho_file).?;
                try markSymbol(sym, roots, macho_file);
            }
        }

        for (&[_]?Symbol.Index{
            obj.entry_index,
            obj.dyld_stub_binder_index,
            obj.objc_msg_send_index,
        }) |index| {
            if (index) |idx| {
                const ref = obj.getSymbolRef(idx, macho_file);
                if (ref.getFile(macho_file) != null) {
                    const sym = ref.getSymbol(macho_file).?;
                    try markSymbol(sym, roots, macho_file);
                }
            }
        }
    }
}

fn markSymbol(sym: *Symbol, roots: *std.array_list.Managed(*Atom), macho_file: *MachO) !void {
    const atom = sym.getAtom(macho_file) orelse return;
    if (markAtom(atom)) try roots.append(atom);
}

fn markAtom(atom: *Atom) bool {
    const already_visited = atom.visited.swap(true, .seq_cst);
    return atom.isAlive() and !already_visited;
}

fn mark(roots: []*Atom, objects: []const File.Index, macho_file: *MachO) void {
    for (roots) |root| {
        markLive(root, macho_file);
    }

    var loop: bool = true;
    while (loop) {
        loop = false;

        for (objects) |index| {
            const file = macho_file.getFile(index).?;
            for (file.getAtoms()) |atom_index| {
                const atom = file.getAtom(atom_index) orelse continue;
                const isec = atom.getInputSection(macho_file);
                if (isec.isDontDeadStripIfReferencesLive() and
                    !(mem.eql(u8, isec.sectName(), "__eh_frame") or
                        mem.eql(u8, isec.sectName(), "__compact_unwind") or
                        isec.attrs() & macho.S_ATTR_DEBUG != 0) and
                    !atom.isAlive() and refersLive(atom, macho_file))
                {
                    markLive(atom, macho_file);
                    loop = true;
                }
            }
        }
    }
}

fn markLive(atom: *Atom, macho_file: *MachO) void {
    assert(atom.visited.load(.seq_cst));
    atom.setAlive(true);
    track_live_log.debug("{f}marking live atom({d},{s})", .{
        track_live_level,
        atom.atom_index,
        atom.getName(macho_file),
    });

    if (build_options.enable_logging)
        track_live_level.incr();

    for (atom.getRelocs(macho_file)) |rel| {
        const target_atom = switch (rel.tag) {
            .local => rel.getTargetAtom(atom.*, macho_file),
            .@"extern" => blk: {
                const ref = rel.getTargetSymbolRef(atom.*, macho_file);
                break :blk if (ref.getSymbol(macho_file)) |sym| sym.getAtom(macho_file) else null;
            },
        };
        if (target_atom) |ta| {
            if (markAtom(ta)) markLive(ta, macho_file);
        }
    }

    const file = atom.getFile(macho_file);
    for (atom.getUnwindRecords(macho_file)) |cu_index| {
        const cu = file.object.getUnwindRecord(cu_index);
        const cu_atom = cu.getAtom(macho_file);
        if (markAtom(cu_atom)) markLive(cu_atom, macho_file);

        if (cu.getLsdaAtom(macho_file)) |lsda| {
            if (markAtom(lsda)) markLive(lsda, macho_file);
        }
        if (cu.getFde(macho_file)) |fde| {
            const fde_atom = fde.getAtom(macho_file);
            if (markAtom(fde_atom)) markLive(fde_atom, macho_file);

            if (fde.getLsdaAtom(macho_file)) |lsda| {
                if (markAtom(lsda)) markLive(lsda, macho_file);
            }
        }
    }
}

fn refersLive(atom: *Atom, macho_file: *MachO) bool {
    for (atom.getRelocs(macho_file)) |rel| {
        const target_atom = switch (rel.tag) {
            .local => rel.getTargetAtom(atom.*, macho_file),
            .@"extern" => blk: {
                const ref = rel.getTargetSymbolRef(atom.*, macho_file);
                break :blk if (ref.getSymbol(macho_file)) |sym| sym.getAtom(macho_file) else null;
            },
        };
        if (target_atom) |ta| {
            if (ta.isAlive()) return true;
        }
    }
    return false;
}

fn prune(objects: []const File.Index, macho_file: *MachO) void {
    for (objects) |index| {
        const file = macho_file.getFile(index).?;
        for (file.getAtoms()) |atom_index| {
            const atom = file.getAtom(atom_index) orelse continue;
            if (!atom.visited.load(.seq_cst)) {
                if (atom.alive.cmpxchgStrong(true, false, .seq_cst, .seq_cst) == null) {
                    atom.markUnwindRecordsDead(macho_file);
                }
            }
        }
    }
}

const Level = struct {
    value: usize = 0,

    fn incr(self: *@This()) void {
        self.value += 1;
    }

    pub fn format(self: *const @This(), w: *Writer) Writer.Error!void {
        try w.splatByteAll(' ', self.value);
    }
};

var track_live_level: Level = .{};

const assert = std.debug.assert;
const build_options = @import("build_options");
const log = std.log.scoped(.dead_strip);
const macho = std.macho;
const math = std.math;
const mem = std.mem;
const trace = @import("../../tracy.zig").trace;
const track_live_log = std.log.scoped(.dead_strip_track_live);
const std = @import("std");
const Writer = std.Io.Writer;

const Allocator = mem.Allocator;
const Atom = @import("Atom.zig");
const File = @import("file.zig").File;
const MachO = @import("../MachO.zig");
const Symbol = @import("Symbol.zig");
