const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Compilation = @import("../Compilation.zig");
const llvm = @import("llvm/bindings.zig");
const link = @import("../link.zig");
const log = std.log.scoped(.codegen);
const math = std.math;

const Module = @import("../Module.zig");
const TypedValue = @import("../TypedValue.zig");
const ir = @import("../air.zig");
const Inst = ir.Inst;

const Value = @import("../value.zig").Value;
const Type = @import("../type.zig").Type;

const LazySrcLoc = Module.LazySrcLoc;

pub fn targetTriple(allocator: *Allocator, target: std.Target) ![:0]u8 {
    const llvm_arch = switch (target.cpu.arch) {
        .arm => "arm",
        .armeb => "armeb",
        .aarch64 => "aarch64",
        .aarch64_be => "aarch64_be",
        .aarch64_32 => "aarch64_32",
        .arc => "arc",
        .avr => "avr",
        .bpfel => "bpfel",
        .bpfeb => "bpfeb",
        .csky => "csky",
        .hexagon => "hexagon",
        .mips => "mips",
        .mipsel => "mipsel",
        .mips64 => "mips64",
        .mips64el => "mips64el",
        .msp430 => "msp430",
        .powerpc => "powerpc",
        .powerpcle => "powerpcle",
        .powerpc64 => "powerpc64",
        .powerpc64le => "powerpc64le",
        .r600 => "r600",
        .amdgcn => "amdgcn",
        .riscv32 => "riscv32",
        .riscv64 => "riscv64",
        .sparc => "sparc",
        .sparcv9 => "sparcv9",
        .sparcel => "sparcel",
        .s390x => "s390x",
        .tce => "tce",
        .tcele => "tcele",
        .thumb => "thumb",
        .thumbeb => "thumbeb",
        .i386 => "i386",
        .x86_64 => "x86_64",
        .xcore => "xcore",
        .nvptx => "nvptx",
        .nvptx64 => "nvptx64",
        .le32 => "le32",
        .le64 => "le64",
        .amdil => "amdil",
        .amdil64 => "amdil64",
        .hsail => "hsail",
        .hsail64 => "hsail64",
        .spir => "spir",
        .spir64 => "spir64",
        .kalimba => "kalimba",
        .shave => "shave",
        .lanai => "lanai",
        .wasm32 => "wasm32",
        .wasm64 => "wasm64",
        .renderscript32 => "renderscript32",
        .renderscript64 => "renderscript64",
        .ve => "ve",
        .spu_2 => return error.LLVMBackendDoesNotSupportSPUMarkII,
        .spirv32 => return error.LLVMBackendDoesNotSupportSPIRV,
        .spirv64 => return error.LLVMBackendDoesNotSupportSPIRV,
    };

    const llvm_os = switch (target.os.tag) {
        .freestanding => "unknown",
        .ananas => "ananas",
        .cloudabi => "cloudabi",
        .dragonfly => "dragonfly",
        .freebsd => "freebsd",
        .fuchsia => "fuchsia",
        .ios => "ios",
        .kfreebsd => "kfreebsd",
        .linux => "linux",
        .lv2 => "lv2",
        .macos => "macosx",
        .netbsd => "netbsd",
        .openbsd => "openbsd",
        .solaris => "solaris",
        .windows => "windows",
        .zos => "zos",
        .haiku => "haiku",
        .minix => "minix",
        .rtems => "rtems",
        .nacl => "nacl",
        .aix => "aix",
        .cuda => "cuda",
        .nvcl => "nvcl",
        .amdhsa => "amdhsa",
        .ps4 => "ps4",
        .elfiamcu => "elfiamcu",
        .tvos => "tvos",
        .watchos => "watchos",
        .mesa3d => "mesa3d",
        .contiki => "contiki",
        .amdpal => "amdpal",
        .hermit => "hermit",
        .hurd => "hurd",
        .wasi => "wasi",
        .emscripten => "emscripten",
        .uefi => "windows",
        .opencl => return error.LLVMBackendDoesNotSupportOpenCL,
        .glsl450 => return error.LLVMBackendDoesNotSupportGLSL450,
        .vulkan => return error.LLVMBackendDoesNotSupportVulkan,
        .other => "unknown",
    };

    const llvm_abi = switch (target.abi) {
        .none => "unknown",
        .gnu => "gnu",
        .gnuabin32 => "gnuabin32",
        .gnuabi64 => "gnuabi64",
        .gnueabi => "gnueabi",
        .gnueabihf => "gnueabihf",
        .gnux32 => "gnux32",
        .gnuilp32 => "gnuilp32",
        .code16 => "code16",
        .eabi => "eabi",
        .eabihf => "eabihf",
        .android => "android",
        .musl => "musl",
        .musleabi => "musleabi",
        .musleabihf => "musleabihf",
        .msvc => "msvc",
        .itanium => "itanium",
        .cygnus => "cygnus",
        .coreclr => "coreclr",
        .simulator => "simulator",
        .macabi => "macabi",
    };

    return std.fmt.allocPrintZ(allocator, "{s}-unknown-{s}-{s}", .{ llvm_arch, llvm_os, llvm_abi });
}

pub const Object = struct {
    llvm_module: *const llvm.Module,
    context: *const llvm.Context,
    target_machine: *const llvm.TargetMachine,
    object_pathZ: [:0]const u8,

    pub fn create(allocator: *Allocator, sub_path: []const u8, options: link.Options) !*Object {
        const self = try allocator.create(Object);
        errdefer allocator.destroy(self);

        const obj_basename = try std.zig.binNameAlloc(allocator, .{
            .root_name = options.root_name,
            .target = options.target,
            .output_mode = .Obj,
        });
        defer allocator.free(obj_basename);

        const o_directory = options.module.?.zig_cache_artifact_directory;
        const object_path = try o_directory.join(allocator, &[_][]const u8{obj_basename});
        defer allocator.free(object_path);

        const object_pathZ = try allocator.dupeZ(u8, object_path);
        errdefer allocator.free(object_pathZ);

        const context = llvm.Context.create();
        errdefer context.dispose();

        initializeLLVMTargets();

        const root_nameZ = try allocator.dupeZ(u8, options.root_name);
        defer allocator.free(root_nameZ);
        const llvm_module = llvm.Module.createWithName(root_nameZ.ptr, context);
        errdefer llvm_module.dispose();

        const llvm_target_triple = try targetTriple(allocator, options.target);
        defer allocator.free(llvm_target_triple);

        var error_message: [*:0]const u8 = undefined;
        var target: *const llvm.Target = undefined;
        if (llvm.Target.getFromTriple(llvm_target_triple.ptr, &target, &error_message).toBool()) {
            defer llvm.disposeMessage(error_message);

            const stderr = std.io.getStdErr().writer();
            try stderr.print(
                \\Zig is expecting LLVM to understand this target: '{s}'
                \\However LLVM responded with: "{s}"
                \\
            ,
                .{ llvm_target_triple, error_message },
            );
            return error.InvalidLLVMTriple;
        }

        const opt_level: llvm.CodeGenOptLevel = if (options.optimize_mode == .Debug) .None else .Aggressive;
        const target_machine = llvm.TargetMachine.create(
            target,
            llvm_target_triple.ptr,
            "",
            "",
            opt_level,
            .Static,
            .Default,
        );
        errdefer target_machine.dispose();

        self.* = .{
            .llvm_module = llvm_module,
            .context = context,
            .target_machine = target_machine,
            .object_pathZ = object_pathZ,
        };
        return self;
    }

    pub fn deinit(self: *Object, allocator: *Allocator) void {
        self.target_machine.dispose();
        self.llvm_module.dispose();
        self.context.dispose();

        allocator.free(self.object_pathZ);
        allocator.destroy(self);
    }

    fn initializeLLVMTargets() void {
        llvm.initializeAllTargets();
        llvm.initializeAllTargetInfos();
        llvm.initializeAllTargetMCs();
        llvm.initializeAllAsmPrinters();
        llvm.initializeAllAsmParsers();
    }

    pub fn flushModule(self: *Object, comp: *Compilation) !void {
        if (comp.verbose_llvm_ir) {
            const dump = self.llvm_module.printToString();
            defer llvm.disposeMessage(dump);

            const stderr = std.io.getStdErr().writer();
            try stderr.writeAll(std.mem.spanZ(dump));
        }

        {
            var error_message: [*:0]const u8 = undefined;
            // verifyModule always allocs the error_message even if there is no error
            defer llvm.disposeMessage(error_message);

            if (self.llvm_module.verify(.ReturnStatus, &error_message).toBool()) {
                const stderr = std.io.getStdErr().writer();
                try stderr.print("broken LLVM module found: {s}\nThis is a bug in the Zig compiler.", .{error_message});
                return error.BrokenLLVMModule;
            }
        }

        var error_message: [*:0]const u8 = undefined;
        if (self.target_machine.emitToFile(
            self.llvm_module,
            self.object_pathZ.ptr,
            .ObjectFile,
            &error_message,
        ).toBool()) {
            defer llvm.disposeMessage(error_message);

            const stderr = std.io.getStdErr().writer();
            try stderr.print("LLVM failed to emit file: {s}\n", .{error_message});
            return error.FailedToEmit;
        }
    }

    pub fn updateDecl(self: *Object, module: *Module, decl: *Module.Decl) !void {
        var dg: DeclGen = .{
            .object = self,
            .module = module,
            .decl = decl,
            .err_msg = null,
            .gpa = module.gpa,
        };
        dg.genDecl() catch |err| switch (err) {
            error.CodegenFail => {
                decl.analysis = .codegen_failure;
                try module.failed_decls.put(module.gpa, decl, dg.err_msg.?);
                dg.err_msg = null;
                return;
            },
            else => |e| return e,
        };
    }
};

pub const DeclGen = struct {
    object: *Object,
    module: *Module,
    decl: *Module.Decl,
    err_msg: ?*Module.ErrorMsg,

    gpa: *Allocator,

    fn todo(self: *DeclGen, comptime format: []const u8, args: anytype) error{ OutOfMemory, CodegenFail } {
        @setCold(true);
        assert(self.err_msg == null);
        const src_loc = @as(LazySrcLoc, .{ .node_offset = 0 }).toSrcLocWithDecl(self.decl);
        self.err_msg = try Module.ErrorMsg.create(self.gpa, src_loc, "TODO (LLVM): " ++ format, args);
        return error.CodegenFail;
    }

    fn llvmModule(self: *DeclGen) *const llvm.Module {
        return self.object.llvm_module;
    }

    fn context(self: *DeclGen) *const llvm.Context {
        return self.object.context;
    }

    fn genDecl(self: *DeclGen) !void {
        const decl = self.decl;
        assert(decl.has_tv);

        log.debug("gen: {s} type: {}, value: {}", .{ decl.name, decl.ty, decl.val });

        if (decl.val.castTag(.function)) |func_payload| {
            const func = func_payload.data;

            const llvm_func = try self.resolveLLVMFunction(func.owner_decl);

            // This gets the LLVM values from the function and stores them in `self.args`.
            const fn_param_len = func.owner_decl.ty.fnParamLen();
            var args = try self.gpa.alloc(*const llvm.Value, fn_param_len);

            for (args) |*arg, i| {
                arg.* = llvm.getParam(llvm_func, @intCast(c_uint, i));
            }

            // We remove all the basic blocks of a function to support incremental
            // compilation!
            // TODO: remove all basic blocks if functions can have more than one
            if (llvm_func.getFirstBasicBlock()) |bb| {
                bb.deleteBasicBlock();
            }

            const builder = self.context().createBuilder();

            const entry_block = self.context().appendBasicBlock(llvm_func, "Entry");
            builder.positionBuilderAtEnd(entry_block);

            var fg: FuncGen = .{
                .dg = self,
                .builder = builder,
                .args = args,
                .arg_index = 0,
                .func_inst_table = .{},
                .entry_block = entry_block,
                .latest_alloca_inst = null,
                .llvm_func = llvm_func,
                .blocks = .{},
            };
            defer fg.deinit();

            try fg.genBody(func.body);
        } else if (decl.val.castTag(.extern_fn)) |extern_fn| {
            _ = try self.resolveLLVMFunction(extern_fn.data);
        } else {
            _ = try self.resolveGlobalDecl(decl);
        }
    }

    /// If the llvm function does not exist, create it
    fn resolveLLVMFunction(self: *DeclGen, func: *Module.Decl) !*const llvm.Value {
        // TODO: do we want to store this in our own datastructure?
        if (self.llvmModule().getNamedFunction(func.name)) |llvm_fn| return llvm_fn;

        assert(func.has_tv);
        const zig_fn_type = func.ty;
        const return_type = zig_fn_type.fnReturnType();

        const fn_param_len = zig_fn_type.fnParamLen();

        const fn_param_types = try self.gpa.alloc(Type, fn_param_len);
        defer self.gpa.free(fn_param_types);
        zig_fn_type.fnParamTypes(fn_param_types);

        const llvm_param = try self.gpa.alloc(*const llvm.Type, fn_param_len);
        defer self.gpa.free(llvm_param);

        for (fn_param_types) |fn_param, i| {
            llvm_param[i] = try self.getLLVMType(fn_param);
        }

        const fn_type = llvm.Type.functionType(
            try self.getLLVMType(return_type),
            if (fn_param_len == 0) null else llvm_param.ptr,
            @intCast(c_uint, fn_param_len),
            .False,
        );
        const llvm_fn = self.llvmModule().addFunction(func.name, fn_type);

        if (return_type.tag() == .noreturn) {
            self.addFnAttr(llvm_fn, "noreturn");
        }

        return llvm_fn;
    }

    fn resolveGlobalDecl(self: *DeclGen, decl: *Module.Decl) error{ OutOfMemory, CodegenFail }!*const llvm.Value {
        // TODO: do we want to store this in our own datastructure?
        if (self.llvmModule().getNamedGlobal(decl.name)) |val| return val;

        assert(decl.has_tv);

        // TODO: remove this redundant `getLLVMType`, it is also called in `genTypedValue`.
        const llvm_type = try self.getLLVMType(decl.ty);
        const val = try self.genTypedValue(.{ .ty = decl.ty, .val = decl.val }, null);
        const global = self.llvmModule().addGlobal(llvm_type, decl.name);
        llvm.setInitializer(global, val);

        // TODO ask the Decl if it is const
        // https://github.com/ziglang/zig/issues/7582

        return global;
    }

    fn getLLVMType(self: *DeclGen, t: Type) error{ OutOfMemory, CodegenFail }!*const llvm.Type {
        log.debug("getLLVMType for {}", .{t});
        switch (t.zigTypeTag()) {
            .Void => return self.context().voidType(),
            .NoReturn => return self.context().voidType(),
            .Int => {
                const info = t.intInfo(self.module.getTarget());
                return self.context().intType(info.bits);
            },
            .Bool => return self.context().intType(1),
            .Pointer => {
                if (t.isSlice()) {
                    return self.todo("implement slices", .{});
                } else {
                    const elem_type = try self.getLLVMType(t.elemType());
                    return elem_type.pointerType(0);
                }
            },
            .Array => {
                const elem_type = try self.getLLVMType(t.elemType());
                return elem_type.arrayType(@intCast(c_uint, t.abiSize(self.module.getTarget())));
            },
            .Optional => {
                if (!t.isPtrLikeOptional()) {
                    var buf: Type.Payload.ElemType = undefined;
                    const child_type = t.optionalChild(&buf);

                    var optional_types: [2]*const llvm.Type = .{
                        try self.getLLVMType(child_type),
                        self.context().intType(1),
                    };
                    return self.context().structType(&optional_types, 2, .False);
                } else {
                    return self.todo("implement optional pointers as actual pointers", .{});
                }
            },
            .ComptimeInt => unreachable,
            .ComptimeFloat => unreachable,
            .Type => unreachable,
            .Undefined => unreachable,
            .Null => unreachable,
            .EnumLiteral => unreachable,

            .BoundFn => @panic("TODO remove BoundFn from the language"),

            .Float,
            .Struct,
            .ErrorUnion,
            .ErrorSet,
            .Enum,
            .Union,
            .Fn,
            .Opaque,
            .Frame,
            .AnyFrame,
            .Vector,
            => return self.todo("implement getLLVMType for type '{}'", .{t}),
        }
    }

    // TODO: figure out a way to remove the FuncGen argument
    fn genTypedValue(self: *DeclGen, tv: TypedValue, fg: ?*FuncGen) error{ OutOfMemory, CodegenFail }!*const llvm.Value {
        const llvm_type = try self.getLLVMType(tv.ty);

        if (tv.val.isUndef())
            return llvm_type.getUndef();

        switch (tv.ty.zigTypeTag()) {
            .Bool => return if (tv.val.toBool()) llvm_type.constAllOnes() else llvm_type.constNull(),
            .Int => {
                var bigint_space: Value.BigIntSpace = undefined;
                const bigint = tv.val.toBigInt(&bigint_space);

                if (bigint.eqZero()) return llvm_type.constNull();

                if (bigint.limbs.len != 1) {
                    return self.todo("implement bigger bigint", .{});
                }
                const llvm_int = llvm_type.constInt(bigint.limbs[0], .False);
                if (!bigint.positive) {
                    return llvm.constNeg(llvm_int);
                }
                return llvm_int;
            },
            .Pointer => switch (tv.val.tag()) {
                .decl_ref => {
                    const decl = tv.val.castTag(.decl_ref).?.data;
                    const val = try self.resolveGlobalDecl(decl);

                    const usize_type = try self.getLLVMType(Type.initTag(.usize));

                    // TODO: second index should be the index into the memory!
                    var indices: [2]*const llvm.Value = .{
                        usize_type.constNull(),
                        usize_type.constNull(),
                    };

                    // TODO: consider using buildInBoundsGEP2 for opaque pointers
                    return fg.?.builder.buildInBoundsGEP(val, &indices, 2, "");
                },
                .ref_val => {
                    const elem_value = tv.val.castTag(.ref_val).?.data;
                    const elem_type = tv.ty.castPointer().?.data;
                    const alloca = fg.?.buildAlloca(try self.getLLVMType(elem_type));
                    _ = fg.?.builder.buildStore(try self.genTypedValue(.{ .ty = elem_type, .val = elem_value }, fg), alloca);
                    return alloca;
                },
                else => return self.todo("implement const of pointer type '{}'", .{tv.ty}),
            },
            .Array => {
                if (tv.val.castTag(.bytes)) |payload| {
                    const zero_sentinel = if (tv.ty.sentinel()) |sentinel| blk: {
                        if (sentinel.tag() == .zero) break :blk true;
                        return self.todo("handle other sentinel values", .{});
                    } else false;

                    return self.context().constString(payload.data.ptr, @intCast(c_uint, payload.data.len), llvm.Bool.fromBool(!zero_sentinel));
                } else {
                    return self.todo("handle more array values", .{});
                }
            },
            .Optional => {
                if (!tv.ty.isPtrLikeOptional()) {
                    var buf: Type.Payload.ElemType = undefined;
                    const child_type = tv.ty.optionalChild(&buf);
                    const llvm_child_type = try self.getLLVMType(child_type);

                    if (tv.val.tag() == .null_value) {
                        var optional_values: [2]*const llvm.Value = .{
                            llvm_child_type.constNull(),
                            self.context().intType(1).constNull(),
                        };
                        return self.context().constStruct(&optional_values, 2, .False);
                    } else {
                        var optional_values: [2]*const llvm.Value = .{
                            try self.genTypedValue(.{ .ty = child_type, .val = tv.val }, fg),
                            self.context().intType(1).constAllOnes(),
                        };
                        return self.context().constStruct(&optional_values, 2, .False);
                    }
                } else {
                    return self.todo("implement const of optional pointer", .{});
                }
            },
            else => return self.todo("implement const of type '{}'", .{tv.ty}),
        }
    }

    // Helper functions
    fn addAttr(self: *DeclGen, val: *const llvm.Value, index: llvm.AttributeIndex, name: []const u8) void {
        const kind_id = llvm.getEnumAttributeKindForName(name.ptr, name.len);
        assert(kind_id != 0);
        const llvm_attr = self.context().createEnumAttribute(kind_id, 0);
        val.addAttributeAtIndex(index, llvm_attr);
    }

    fn addFnAttr(self: *DeclGen, val: *const llvm.Value, attr_name: []const u8) void {
        // TODO: improve this API, `addAttr(-1, attr_name)`
        self.addAttr(val, std.math.maxInt(llvm.AttributeIndex), attr_name);
    }
};

pub const FuncGen = struct {
    dg: *DeclGen,

    builder: *const llvm.Builder,

    /// This stores the LLVM values used in a function, such that they can be
    /// referred to in other instructions. This table is cleared before every function is generated.
    /// TODO: Change this to a stack of Branch. Currently we store all the values from all the blocks
    /// in here, however if a block ends, the instructions can be thrown away.
    func_inst_table: std.AutoHashMapUnmanaged(*Inst, *const llvm.Value),

    /// These fields are used to refer to the LLVM value of the function paramaters in an Arg instruction.
    args: []*const llvm.Value,
    arg_index: usize,

    entry_block: *const llvm.BasicBlock,
    /// This fields stores the last alloca instruction, such that we can append more alloca instructions
    /// to the top of the function.
    latest_alloca_inst: ?*const llvm.Value,

    llvm_func: *const llvm.Value,

    /// This data structure is used to implement breaking to blocks.
    blocks: std.AutoHashMapUnmanaged(*Inst.Block, struct {
        parent_bb: *const llvm.BasicBlock,
        break_bbs: *BreakBasicBlocks,
        break_vals: *BreakValues,
    }),

    const BreakBasicBlocks = std.ArrayListUnmanaged(*const llvm.BasicBlock);
    const BreakValues = std.ArrayListUnmanaged(*const llvm.Value);

    fn deinit(self: *FuncGen) void {
        self.builder.dispose();
        self.func_inst_table.deinit(self.gpa());
        self.gpa().free(self.args);
        self.blocks.deinit(self.gpa());
    }

    fn todo(self: *FuncGen, comptime format: []const u8, args: anytype) error{ OutOfMemory, CodegenFail } {
        @setCold(true);
        return self.dg.todo(format, args);
    }

    fn llvmModule(self: *FuncGen) *const llvm.Module {
        return self.dg.object.llvm_module;
    }

    fn context(self: *FuncGen) *const llvm.Context {
        return self.dg.object.context;
    }

    fn gpa(self: *FuncGen) *Allocator {
        return self.dg.gpa;
    }

    fn resolveInst(self: *FuncGen, inst: *ir.Inst) !*const llvm.Value {
        if (inst.value()) |val| {
            return self.dg.genTypedValue(.{ .ty = inst.ty, .val = val }, self);
        }
        if (self.func_inst_table.get(inst)) |value| return value;

        return self.todo("implement global llvm values (or the value is not in the func_inst_table table)", .{});
    }

    fn genBody(self: *FuncGen, body: ir.Body) error{ OutOfMemory, CodegenFail }!void {
        for (body.instructions) |inst| {
            const opt_value = switch (inst.tag) {
                .add => try self.genAdd(inst.castTag(.add).?),
                .alloc => try self.genAlloc(inst.castTag(.alloc).?),
                .arg => try self.genArg(inst.castTag(.arg).?),
                .bitcast => try self.genBitCast(inst.castTag(.bitcast).?),
                .block => try self.genBlock(inst.castTag(.block).?),
                .br => try self.genBr(inst.castTag(.br).?),
                .breakpoint => try self.genBreakpoint(inst.castTag(.breakpoint).?),
                .br_void => try self.genBrVoid(inst.castTag(.br_void).?),
                .call => try self.genCall(inst.castTag(.call).?),
                .cmp_eq => try self.genCmp(inst.castTag(.cmp_eq).?, .eq),
                .cmp_gt => try self.genCmp(inst.castTag(.cmp_gt).?, .gt),
                .cmp_gte => try self.genCmp(inst.castTag(.cmp_gte).?, .gte),
                .cmp_lt => try self.genCmp(inst.castTag(.cmp_lt).?, .lt),
                .cmp_lte => try self.genCmp(inst.castTag(.cmp_lte).?, .lte),
                .cmp_neq => try self.genCmp(inst.castTag(.cmp_neq).?, .neq),
                .condbr => try self.genCondBr(inst.castTag(.condbr).?),
                .intcast => try self.genIntCast(inst.castTag(.intcast).?),
                .is_non_null => try self.genIsNonNull(inst.castTag(.is_non_null).?, false),
                .is_non_null_ptr => try self.genIsNonNull(inst.castTag(.is_non_null_ptr).?, true),
                .is_null => try self.genIsNull(inst.castTag(.is_null).?, false),
                .is_null_ptr => try self.genIsNull(inst.castTag(.is_null_ptr).?, true),
                .load => try self.genLoad(inst.castTag(.load).?),
                .loop => try self.genLoop(inst.castTag(.loop).?),
                .not => try self.genNot(inst.castTag(.not).?),
                .ret => try self.genRet(inst.castTag(.ret).?),
                .retvoid => self.genRetVoid(inst.castTag(.retvoid).?),
                .store => try self.genStore(inst.castTag(.store).?),
                .sub => try self.genSub(inst.castTag(.sub).?),
                .unreach => self.genUnreach(inst.castTag(.unreach).?),
                .optional_payload => try self.genOptionalPayload(inst.castTag(.optional_payload).?, false),
                .optional_payload_ptr => try self.genOptionalPayload(inst.castTag(.optional_payload_ptr).?, true),
                .dbg_stmt => blk: {
                    // TODO: implement debug info
                    break :blk null;
                },
                else => |tag| return self.todo("implement TZIR instruction: {}", .{tag}),
            };
            if (opt_value) |val| try self.func_inst_table.putNoClobber(self.gpa(), inst, val);
        }
    }

    fn genCall(self: *FuncGen, inst: *Inst.Call) !?*const llvm.Value {
        if (inst.func.value()) |func_value| {
            const fn_decl = if (func_value.castTag(.extern_fn)) |extern_fn|
                extern_fn.data
            else if (func_value.castTag(.function)) |func_payload|
                func_payload.data.owner_decl
            else
                unreachable;

            assert(fn_decl.has_tv);
            const zig_fn_type = fn_decl.ty;
            const llvm_fn = try self.dg.resolveLLVMFunction(fn_decl);

            const num_args = inst.args.len;

            const llvm_param_vals = try self.gpa().alloc(*const llvm.Value, num_args);
            defer self.gpa().free(llvm_param_vals);

            for (inst.args) |arg, i| {
                llvm_param_vals[i] = try self.resolveInst(arg);
            }

            // TODO: LLVMBuildCall2 handles opaque function pointers, according to llvm docs
            //       Do we need that?
            const call = self.builder.buildCall(
                llvm_fn,
                if (num_args == 0) null else llvm_param_vals.ptr,
                @intCast(c_uint, num_args),
                "",
            );

            const return_type = zig_fn_type.fnReturnType();
            if (return_type.tag() == .noreturn) {
                _ = self.builder.buildUnreachable();
            }

            // No need to store the LLVM value if the return type is void or noreturn
            if (!return_type.hasCodeGenBits()) return null;

            return call;
        } else {
            return self.todo("implement calling runtime known function pointer", .{});
        }
    }

    fn genRetVoid(self: *FuncGen, inst: *Inst.NoOp) ?*const llvm.Value {
        _ = self.builder.buildRetVoid();
        return null;
    }

    fn genRet(self: *FuncGen, inst: *Inst.UnOp) !?*const llvm.Value {
        if (!inst.operand.ty.hasCodeGenBits()) {
            // TODO: in astgen these instructions should turn into `retvoid` instructions.
            _ = self.builder.buildRetVoid();
            return null;
        }
        _ = self.builder.buildRet(try self.resolveInst(inst.operand));
        return null;
    }

    fn genCmp(self: *FuncGen, inst: *Inst.BinOp, op: math.CompareOperator) !?*const llvm.Value {
        const lhs = try self.resolveInst(inst.lhs);
        const rhs = try self.resolveInst(inst.rhs);

        if (!inst.base.ty.isInt())
            if (inst.base.ty.tag() != .bool)
                return self.todo("implement 'genCmp' for type {}", .{inst.base.ty});

        const is_signed = inst.base.ty.isSignedInt();
        const operation = switch (op) {
            .eq => .EQ,
            .neq => .NE,
            .lt => @as(llvm.IntPredicate, if (is_signed) .SLT else .ULT),
            .lte => @as(llvm.IntPredicate, if (is_signed) .SLE else .ULE),
            .gt => @as(llvm.IntPredicate, if (is_signed) .SGT else .UGT),
            .gte => @as(llvm.IntPredicate, if (is_signed) .SGE else .UGE),
        };

        return self.builder.buildICmp(operation, lhs, rhs, "");
    }

    fn genBlock(self: *FuncGen, inst: *Inst.Block) !?*const llvm.Value {
        const parent_bb = self.context().createBasicBlock("Block");

        // 5 breaks to a block seems like a reasonable default.
        var break_bbs = try BreakBasicBlocks.initCapacity(self.gpa(), 5);
        var break_vals = try BreakValues.initCapacity(self.gpa(), 5);
        try self.blocks.putNoClobber(self.gpa(), inst, .{
            .parent_bb = parent_bb,
            .break_bbs = &break_bbs,
            .break_vals = &break_vals,
        });
        defer {
            assert(self.blocks.remove(inst));
            break_bbs.deinit(self.gpa());
            break_vals.deinit(self.gpa());
        }

        try self.genBody(inst.body);

        self.llvm_func.appendExistingBasicBlock(parent_bb);
        self.builder.positionBuilderAtEnd(parent_bb);

        // If the block does not return a value, we dont have to create a phi node.
        if (!inst.base.ty.hasCodeGenBits()) return null;

        const phi_node = self.builder.buildPhi(try self.dg.getLLVMType(inst.base.ty), "");
        phi_node.addIncoming(
            break_vals.items.ptr,
            break_bbs.items.ptr,
            @intCast(c_uint, break_vals.items.len),
        );
        return phi_node;
    }

    fn genBr(self: *FuncGen, inst: *Inst.Br) !?*const llvm.Value {
        var block = self.blocks.get(inst.block).?;

        // If the break doesn't break a value, then we don't have to add
        // the values to the lists.
        if (!inst.operand.ty.hasCodeGenBits()) {
            // TODO: in astgen these instructions should turn into `br_void` instructions.
            _ = self.builder.buildBr(block.parent_bb);
        } else {
            const val = try self.resolveInst(inst.operand);

            // For the phi node, we need the basic blocks and the values of the
            // break instructions.
            try block.break_bbs.append(self.gpa(), self.builder.getInsertBlock());
            try block.break_vals.append(self.gpa(), val);

            _ = self.builder.buildBr(block.parent_bb);
        }
        return null;
    }

    fn genBrVoid(self: *FuncGen, inst: *Inst.BrVoid) !?*const llvm.Value {
        var block = self.blocks.get(inst.block).?;
        _ = self.builder.buildBr(block.parent_bb);
        return null;
    }

    fn genCondBr(self: *FuncGen, inst: *Inst.CondBr) !?*const llvm.Value {
        const condition_value = try self.resolveInst(inst.condition);

        const then_block = self.context().appendBasicBlock(self.llvm_func, "Then");
        const else_block = self.context().appendBasicBlock(self.llvm_func, "Else");
        {
            const prev_block = self.builder.getInsertBlock();
            defer self.builder.positionBuilderAtEnd(prev_block);

            self.builder.positionBuilderAtEnd(then_block);
            try self.genBody(inst.then_body);

            self.builder.positionBuilderAtEnd(else_block);
            try self.genBody(inst.else_body);
        }
        _ = self.builder.buildCondBr(condition_value, then_block, else_block);
        return null;
    }

    fn genLoop(self: *FuncGen, inst: *Inst.Loop) !?*const llvm.Value {
        const loop_block = self.context().appendBasicBlock(self.llvm_func, "Loop");
        _ = self.builder.buildBr(loop_block);

        self.builder.positionBuilderAtEnd(loop_block);
        try self.genBody(inst.body);

        _ = self.builder.buildBr(loop_block);
        return null;
    }

    fn genNot(self: *FuncGen, inst: *Inst.UnOp) !?*const llvm.Value {
        return self.builder.buildNot(try self.resolveInst(inst.operand), "");
    }

    fn genUnreach(self: *FuncGen, inst: *Inst.NoOp) ?*const llvm.Value {
        _ = self.builder.buildUnreachable();
        return null;
    }

    fn genIsNonNull(self: *FuncGen, inst: *Inst.UnOp, operand_is_ptr: bool) !?*const llvm.Value {
        const operand = try self.resolveInst(inst.operand);

        if (operand_is_ptr) {
            const index_type = self.context().intType(32);

            var indices: [2]*const llvm.Value = .{
                index_type.constNull(),
                index_type.constInt(1, .False),
            };

            return self.builder.buildLoad(self.builder.buildInBoundsGEP(operand, &indices, 2, ""), "");
        } else {
            return self.builder.buildExtractValue(operand, 1, "");
        }
    }

    fn genIsNull(self: *FuncGen, inst: *Inst.UnOp, operand_is_ptr: bool) !?*const llvm.Value {
        return self.builder.buildNot((try self.genIsNonNull(inst, operand_is_ptr)).?, "");
    }

    fn genOptionalPayload(self: *FuncGen, inst: *Inst.UnOp, operand_is_ptr: bool) !?*const llvm.Value {
        const operand = try self.resolveInst(inst.operand);

        if (operand_is_ptr) {
            const index_type = self.context().intType(32);

            var indices: [2]*const llvm.Value = .{
                index_type.constNull(),
                index_type.constNull(),
            };

            return self.builder.buildInBoundsGEP(operand, &indices, 2, "");
        } else {
            return self.builder.buildExtractValue(operand, 0, "");
        }
    }

    fn genAdd(self: *FuncGen, inst: *Inst.BinOp) !?*const llvm.Value {
        const lhs = try self.resolveInst(inst.lhs);
        const rhs = try self.resolveInst(inst.rhs);

        if (!inst.base.ty.isInt())
            return self.todo("implement 'genAdd' for type {}", .{inst.base.ty});

        return if (inst.base.ty.isSignedInt())
            self.builder.buildNSWAdd(lhs, rhs, "")
        else
            self.builder.buildNUWAdd(lhs, rhs, "");
    }

    fn genSub(self: *FuncGen, inst: *Inst.BinOp) !?*const llvm.Value {
        const lhs = try self.resolveInst(inst.lhs);
        const rhs = try self.resolveInst(inst.rhs);

        if (!inst.base.ty.isInt())
            return self.todo("implement 'genSub' for type {}", .{inst.base.ty});

        return if (inst.base.ty.isSignedInt())
            self.builder.buildNSWSub(lhs, rhs, "")
        else
            self.builder.buildNUWSub(lhs, rhs, "");
    }

    fn genIntCast(self: *FuncGen, inst: *Inst.UnOp) !?*const llvm.Value {
        const val = try self.resolveInst(inst.operand);

        const signed = inst.base.ty.isSignedInt();
        // TODO: Should we use intcast here or just a simple bitcast?
        //       LLVM does truncation vs bitcast (+signed extension) in the intcast depending on the sizes
        return self.builder.buildIntCast2(val, try self.dg.getLLVMType(inst.base.ty), llvm.Bool.fromBool(signed), "");
    }

    fn genBitCast(self: *FuncGen, inst: *Inst.UnOp) !?*const llvm.Value {
        const val = try self.resolveInst(inst.operand);
        const dest_type = try self.dg.getLLVMType(inst.base.ty);

        return self.builder.buildBitCast(val, dest_type, "");
    }

    fn genArg(self: *FuncGen, inst: *Inst.Arg) !?*const llvm.Value {
        const arg_val = self.args[self.arg_index];
        self.arg_index += 1;

        const ptr_val = self.buildAlloca(try self.dg.getLLVMType(inst.base.ty));
        _ = self.builder.buildStore(arg_val, ptr_val);
        return self.builder.buildLoad(ptr_val, "");
    }

    fn genAlloc(self: *FuncGen, inst: *Inst.NoOp) !?*const llvm.Value {
        // buildAlloca expects the pointee type, not the pointer type, so assert that
        // a Payload.PointerSimple is passed to the alloc instruction.
        const pointee_type = inst.base.ty.castPointer().?.data;

        // TODO: figure out a way to get the name of the var decl.
        // TODO: set alignment and volatile
        return self.buildAlloca(try self.dg.getLLVMType(pointee_type));
    }

    /// Use this instead of builder.buildAlloca, because this function makes sure to
    /// put the alloca instruction at the top of the function!
    fn buildAlloca(self: *FuncGen, t: *const llvm.Type) *const llvm.Value {
        const prev_block = self.builder.getInsertBlock();
        defer self.builder.positionBuilderAtEnd(prev_block);

        if (self.latest_alloca_inst) |latest_alloc| {
            // builder.positionBuilder adds it before the instruction,
            // but we want to put it after the last alloca instruction.
            self.builder.positionBuilder(self.entry_block, latest_alloc.getNextInstruction().?);
        } else {
            // There might have been other instructions emitted before the
            // first alloca has been generated. However the alloca should still
            // be first in the function.
            if (self.entry_block.getFirstInstruction()) |first_inst| {
                self.builder.positionBuilder(self.entry_block, first_inst);
            }
        }

        const val = self.builder.buildAlloca(t, "");
        self.latest_alloca_inst = val;
        return val;
    }

    fn genStore(self: *FuncGen, inst: *Inst.BinOp) !?*const llvm.Value {
        const val = try self.resolveInst(inst.rhs);
        const ptr = try self.resolveInst(inst.lhs);
        _ = self.builder.buildStore(val, ptr);
        return null;
    }

    fn genLoad(self: *FuncGen, inst: *Inst.UnOp) !?*const llvm.Value {
        const ptr_val = try self.resolveInst(inst.operand);
        return self.builder.buildLoad(ptr_val, "");
    }

    fn genBreakpoint(self: *FuncGen, inst: *Inst.NoOp) !?*const llvm.Value {
        const llvn_fn = self.getIntrinsic("llvm.debugtrap");
        _ = self.builder.buildCall(llvn_fn, null, 0, "");
        return null;
    }

    fn getIntrinsic(self: *FuncGen, name: []const u8) *const llvm.Value {
        const id = llvm.lookupIntrinsicID(name.ptr, name.len);
        assert(id != 0);
        // TODO: add support for overload intrinsics by passing the prefix of the intrinsic
        //       to `lookupIntrinsicID` and then passing the correct types to
        //       `getIntrinsicDeclaration`
        return self.llvmModule().getIntrinsicDeclaration(id, null, 0);
    }
};
