//! This file contains the functionality for emitting RISC-V MIR as machine code

bin_file: *link.File,
lower: Lower,
debug_output: link.File.DebugInfoOutput,
w: *std.Io.Writer,

prev_di_line: u32,
prev_di_column: u32,
/// Relative to the beginning of `code`.
prev_di_pc: usize,

code_offset_mapping: std.AutoHashMapUnmanaged(Mir.Inst.Index, usize) = .empty,
relocs: std.ArrayList(Reloc) = .empty,

pub const Error = Lower.Error || std.Io.Writer.Error || error{
    EmitFail,
};

pub fn emitMir(emit: *Emit) Error!void {
    const gpa = emit.bin_file.comp.gpa;
    log.debug("mir instruction len: {}", .{emit.lower.mir.instructions.len});
    for (0..emit.lower.mir.instructions.len) |mir_i| {
        const mir_index: Mir.Inst.Index = @intCast(mir_i);
        try emit.code_offset_mapping.putNoClobber(
            emit.lower.allocator,
            mir_index,
            @intCast(emit.w.end),
        );
        const lowered = try emit.lower.lowerMir(mir_index, .{ .allow_frame_locs = true });
        var lowered_relocs = lowered.relocs;
        for (lowered.insts, 0..) |lowered_inst, lowered_index| {
            const start_offset: u32 = @intCast(emit.w.end);
            try emit.w.writeInt(u32, lowered_inst.toU32(), .little);

            while (lowered_relocs.len > 0 and
                lowered_relocs[0].lowered_inst_index == lowered_index) : ({
                lowered_relocs = lowered_relocs[1..];
            }) switch (lowered_relocs[0].target) {
                .inst => |target| try emit.relocs.append(emit.lower.allocator, .{
                    .source = start_offset,
                    .target = target,
                    .offset = 0,
                    .fmt = std.meta.activeTag(lowered_inst),
                }),
                .load_symbol_reloc => |symbol| {
                    const elf_file = emit.bin_file.cast(.elf).?;
                    const zo = elf_file.zigObjectPtr().?;

                    const atom_ptr = zo.symbol(@intFromEnum(symbol.atom_index)).atom(elf_file).?;
                    const sym = zo.symbol(@intFromEnum(symbol.sym_index));

                    if (emit.lower.pic) {
                        return emit.fail("know when to emit GOT relocation for symbol '{s}'", .{sym.name(elf_file)});
                    }

                    const hi_r_type: u32 = @intFromEnum(std.elf.R_RISCV.HI20);
                    const lo_r_type: u32 = @intFromEnum(std.elf.R_RISCV.LO12_I);

                    try atom_ptr.addReloc(gpa, .{
                        .r_offset = start_offset,
                        .r_info = (@as(u64, @intFromEnum(symbol.sym_index)) << 32) | hi_r_type,
                        .r_addend = 0,
                    }, zo);

                    try atom_ptr.addReloc(gpa, .{
                        .r_offset = start_offset + 4,
                        .r_info = (@as(u64, @intFromEnum(symbol.sym_index)) << 32) | lo_r_type,
                        .r_addend = 0,
                    }, zo);
                },
                .load_tlv_reloc => |symbol| {
                    const elf_file = emit.bin_file.cast(.elf).?;
                    const zo = elf_file.zigObjectPtr().?;

                    const atom_ptr = zo.symbol(@intFromEnum(symbol.atom_index)).atom(elf_file).?;

                    const R_RISCV = std.elf.R_RISCV;

                    try atom_ptr.addReloc(gpa, .{
                        .r_offset = start_offset,
                        .r_info = (@as(u64, @intFromEnum(symbol.sym_index)) << 32) | @intFromEnum(R_RISCV.TPREL_HI20),
                        .r_addend = 0,
                    }, zo);

                    try atom_ptr.addReloc(gpa, .{
                        .r_offset = start_offset + 4,
                        .r_info = (@as(u64, @intFromEnum(symbol.sym_index)) << 32) | @intFromEnum(R_RISCV.TPREL_ADD),
                        .r_addend = 0,
                    }, zo);

                    try atom_ptr.addReloc(gpa, .{
                        .r_offset = start_offset + 8,
                        .r_info = (@as(u64, @intFromEnum(symbol.sym_index)) << 32) | @intFromEnum(R_RISCV.TPREL_LO12_I),
                        .r_addend = 0,
                    }, zo);
                },
                .call_extern_fn_reloc => |symbol| {
                    const elf_file = emit.bin_file.cast(.elf).?;
                    const zo = elf_file.zigObjectPtr().?;
                    const atom_ptr = zo.symbol(@intFromEnum(symbol.atom_index)).atom(elf_file).?;

                    const r_type: u32 = @intFromEnum(std.elf.R_RISCV.CALL_PLT);

                    try atom_ptr.addReloc(gpa, .{
                        .r_offset = start_offset,
                        .r_info = (@as(u64, @intFromEnum(symbol.sym_index)) << 32) | r_type,
                        .r_addend = 0,
                    }, zo);
                },
            };
        }
        std.debug.assert(lowered_relocs.len == 0);

        if (lowered.insts.len == 0) {
            const mir_inst = emit.lower.mir.instructions.get(mir_index);
            switch (mir_inst.tag) {
                else => unreachable,
                .pseudo_dbg_prologue_end => {
                    switch (emit.debug_output) {
                        .dwarf => |dw| {
                            try dw.setPrologueEnd();
                            log.debug("mirDbgPrologueEnd (line={d}, col={d})", .{
                                emit.prev_di_line, emit.prev_di_column,
                            });
                            try emit.dbgAdvancePCAndLine(emit.prev_di_line, emit.prev_di_column);
                        },
                        .none => {},
                    }
                },
                .pseudo_dbg_line_column => try emit.dbgAdvancePCAndLine(
                    mir_inst.data.pseudo_dbg_line_column.line,
                    mir_inst.data.pseudo_dbg_line_column.column,
                ),
                .pseudo_dbg_epilogue_begin => {
                    switch (emit.debug_output) {
                        .dwarf => |dw| {
                            try dw.setEpilogueBegin();
                            log.debug("mirDbgEpilogueBegin (line={d}, col={d})", .{
                                emit.prev_di_line, emit.prev_di_column,
                            });
                            try emit.dbgAdvancePCAndLine(emit.prev_di_line, emit.prev_di_column);
                        },
                        .none => {},
                    }
                },
                .pseudo_dead => {},
            }
        }
    }
    try emit.fixupRelocs();
}

pub fn deinit(emit: *Emit) void {
    emit.relocs.deinit(emit.lower.allocator);
    emit.code_offset_mapping.deinit(emit.lower.allocator);
    emit.* = undefined;
}

const Reloc = struct {
    /// Offset of the instruction.
    source: usize,
    /// Target of the relocation.
    target: Mir.Inst.Index,
    /// Offset of the relocation within the instruction.
    offset: u32,
    /// Format of the instruction, used to determine how to modify it.
    fmt: encoding.Lir.Format,
};

fn fixupRelocs(emit: *Emit) Error!void {
    for (emit.relocs.items) |reloc| {
        log.debug("target inst: {f}", .{emit.lower.mir.instructions.get(reloc.target)});
        const target = emit.code_offset_mapping.get(reloc.target) orelse
            return emit.fail("relocation target not found!", .{});

        const disp = @as(i32, @intCast(target)) - @as(i32, @intCast(reloc.source));
        const code = emit.w.buffered()[reloc.source + reloc.offset ..][0..4];

        switch (reloc.fmt) {
            .J => riscv_util.writeInstJ(code, @bitCast(disp)),
            .B => riscv_util.writeInstB(code, @bitCast(disp)),
            else => return emit.fail("tried to reloc format type {s}", .{@tagName(reloc.fmt)}),
        }
    }
}

fn dbgAdvancePCAndLine(emit: *Emit, line: u32, column: u32) Error!void {
    const delta_line = @as(i33, line) - @as(i33, emit.prev_di_line);
    const delta_pc: usize = emit.w.end - emit.prev_di_pc;
    log.debug("  (advance pc={d} and line={d})", .{ delta_pc, delta_line });
    switch (emit.debug_output) {
        .dwarf => |dw| {
            if (column != emit.prev_di_column) try dw.setColumn(column);
            if (delta_line == 0) return; // TODO: fix these edge cases.
            try dw.advancePCAndLine(delta_line, delta_pc);
            emit.prev_di_line = line;
            emit.prev_di_column = column;
            emit.prev_di_pc = emit.w.end;
        },
        .none => {},
    }
}

fn fail(emit: *Emit, comptime format: []const u8, args: anytype) Error {
    return switch (emit.lower.fail(format, args)) {
        error.LowerFail => error.EmitFail,
        else => |e| e,
    };
}

const link = @import("../../link.zig");
const log = std.log.scoped(.emit);
const mem = std.mem;
const std = @import("std");

const Emit = @This();
const Lower = @import("Lower.zig");
const Mir = @import("Mir.zig");
const riscv_util = @import("../../link/riscv.zig");
const Elf = @import("../../link/Elf.zig");
const encoding = @import("encoding.zig");
