const std = @import("../../std.zig");

pub const AbbrevOp = union(enum) {
    literal: u32, // 0
    fixed: u16, // 1
    fixed_runtime: type, // 1
    vbr: u16, // 2
    char6: void, // 4
    blob: void, // 5
    array_fixed: u16, // 3, 1
    array_fixed_runtime: type, // 3, 1
    array_vbr: u16, // 3, 2
    array_char6: void, // 3, 4
};

pub const Error = error{OutOfMemory};

pub fn BitcodeWriter(comptime types: []const type) type {
    return struct {
        const BcWriter = @This();

        buffer: std.array_list.Managed(u32),
        bit_buffer: u32 = 0,
        bit_count: u5 = 0,

        widths: [types.len]u16,

        pub fn getTypeWidth(self: BcWriter, comptime Type: type) u16 {
            return self.widths[comptime std.mem.findScalar(type, types, Type).?];
        }

        pub fn init(allocator: std.mem.Allocator, widths: [types.len]u16) BcWriter {
            return .{
                .buffer = std.array_list.Managed(u32).init(allocator),
                .widths = widths,
            };
        }

        pub fn deinit(self: BcWriter) void {
            self.buffer.deinit();
        }

        pub fn toOwnedSlice(self: *BcWriter) Error![]const u32 {
            std.debug.assert(self.bit_count == 0);
            return self.buffer.toOwnedSlice();
        }

        pub fn length(self: BcWriter) usize {
            std.debug.assert(self.bit_count == 0);
            return self.buffer.items.len;
        }

        pub fn writeBits(self: *BcWriter, value: anytype, bits: u16) Error!void {
            if (bits == 0) return;

            var in_buffer = bufValue(value, 32);
            var in_bits = bits;

            // Store input bits in buffer if they fit otherwise store as many as possible and flush
            if (self.bit_count > 0) {
                const bits_remaining = 31 - self.bit_count + 1;
                const n: u5 = @intCast(@min(bits_remaining, in_bits));
                const v = @as(u32, @truncate(in_buffer)) << self.bit_count;
                self.bit_buffer |= v;
                in_buffer >>= n;

                self.bit_count +%= n;
                in_bits -= n;

                if (self.bit_count != 0) return;
                try self.buffer.append(std.mem.nativeToLittle(u32, self.bit_buffer));
                self.bit_buffer = 0;
            }

            // Write 32-bit chunks of input bits
            while (in_bits >= 32) {
                try self.buffer.append(std.mem.nativeToLittle(u32, @truncate(in_buffer)));

                in_buffer >>= 31;
                in_buffer >>= 1;
                in_bits -= 32;
            }

            // Store remaining input bits in buffer
            if (in_bits > 0) {
                self.bit_count = @intCast(in_bits);
                self.bit_buffer = @truncate(in_buffer);
            }
        }

        pub fn writeVbr(self: *BcWriter, value: anytype, comptime vbr_bits: usize) Error!void {
            comptime {
                std.debug.assert(vbr_bits > 1);
                if (@bitSizeOf(@TypeOf(value)) > 64) @compileError("Unsupported VBR block type: " ++ @typeName(@TypeOf(value)));
            }

            var in_buffer = bufValue(value, vbr_bits);

            const continue_bit = @as(@TypeOf(in_buffer), 1) << @intCast(vbr_bits - 1);
            const mask = continue_bit - 1;

            // If input is larger than one VBR block can store
            // then store vbr_bits - 1 bits and a continue bit
            while (in_buffer > mask) {
                try self.writeBits(in_buffer & mask | continue_bit, vbr_bits);
                in_buffer >>= @intCast(vbr_bits - 1);
            }

            // Store remaining bits
            try self.writeBits(in_buffer, vbr_bits);
        }

        pub fn bitsVbr(value: anytype, comptime vbr_bits: usize) u16 {
            comptime {
                std.debug.assert(vbr_bits > 1);
                if (@bitSizeOf(@TypeOf(value)) > 64) @compileError("Unsupported VBR block type: " ++ @typeName(@TypeOf(value)));
            }

            var bits: u16 = 0;

            var in_buffer = bufValue(value, vbr_bits);

            const continue_bit = @as(@TypeOf(in_buffer), 1) << @intCast(vbr_bits - 1);
            const mask = continue_bit - 1;

            // If input is larger than one VBR block can store
            // then store vbr_bits - 1 bits and a continue bit
            while (in_buffer > mask) {
                bits += @intCast(vbr_bits);
                in_buffer >>= @intCast(vbr_bits - 1);
            }

            // Store remaining bits
            bits += @intCast(vbr_bits);
            return bits;
        }

        pub fn write6BitChar(self: *BcWriter, c: u8) Error!void {
            try self.writeBits(charTo6Bit(c), 6);
        }

        pub fn writeBlob(self: *BcWriter, blob: []const u8) Error!void {
            const blob_word_size = std.mem.alignForward(usize, blob.len, 4);
            try self.buffer.ensureUnusedCapacity(blob_word_size + 1);
            self.alignTo32() catch unreachable;

            const slice = self.buffer.addManyAsSliceAssumeCapacity(blob_word_size / 4);
            const slice_bytes = std.mem.sliceAsBytes(slice);
            @memcpy(slice_bytes[0..blob.len], blob);
            @memset(slice_bytes[blob.len..], 0);
        }

        pub fn alignTo32(self: *BcWriter) Error!void {
            if (self.bit_count == 0) return;

            try self.buffer.append(std.mem.nativeToLittle(u32, self.bit_buffer));
            self.bit_buffer = 0;
            self.bit_count = 0;
        }

        pub fn enterTopBlock(self: *BcWriter, comptime SubBlock: type) Error!BlockWriter(SubBlock) {
            return BlockWriter(SubBlock).init(self, 2, true);
        }

        fn BlockWriter(comptime Block: type) type {
            return struct {
                const Self = @This();

                // The minimum abbrev id length based on the number of abbrevs present in the block
                pub const abbrev_len = std.math.log2_int_ceil(
                    u6,
                    4 + (if (@hasDecl(Block, "abbrevs")) Block.abbrevs.len else 0),
                );

                start: usize,
                bitcode: *BcWriter,

                pub fn init(bitcode: *BcWriter, comptime parent_abbrev_len: u6, comptime define_abbrevs: bool) Error!Self {
                    try bitcode.writeBits(1, parent_abbrev_len);
                    try bitcode.writeVbr(Block.id, 8);
                    try bitcode.writeVbr(abbrev_len, 4);
                    try bitcode.alignTo32();

                    // We store the index of the block size and store a dummy value as the number of words in the block
                    const start = bitcode.length();
                    try bitcode.writeBits(0, 32);

                    var self = Self{
                        .start = start,
                        .bitcode = bitcode,
                    };

                    // Predefine all block abbrevs
                    if (define_abbrevs) {
                        inline for (Block.abbrevs) |Abbrev| {
                            try self.defineAbbrev(&Abbrev.ops);
                        }
                    }

                    return self;
                }

                pub fn enterSubBlock(self: Self, comptime SubBlock: type, comptime define_abbrevs: bool) Error!BlockWriter(SubBlock) {
                    return BlockWriter(SubBlock).init(self.bitcode, abbrev_len, define_abbrevs);
                }

                pub fn end(self: *Self) Error!void {
                    try self.bitcode.writeBits(0, abbrev_len);
                    try self.bitcode.alignTo32();

                    // Set the number of words in the block at the start of the block
                    self.bitcode.buffer.items[self.start] = std.mem.nativeToLittle(u32, @truncate(self.bitcode.length() - self.start - 1));
                }

                pub fn writeUnabbrev(self: *Self, code: u32, values: []const u64) Error!void {
                    try self.bitcode.writeBits(3, abbrev_len);
                    try self.bitcode.writeVbr(code, 6);
                    try self.bitcode.writeVbr(values.len, 6);
                    for (values) |val| {
                        try self.bitcode.writeVbr(val, 6);
                    }
                }

                pub fn writeAbbrev(self: *Self, params: anytype) Error!void {
                    return self.writeAbbrevAdapted(params, struct {
                        pub fn get(_: @This(), param: anytype) @TypeOf(param) {
                            return param;
                        }
                    }{});
                }

                pub fn abbrevId(comptime Abbrev: type) u32 {
                    inline for (Block.abbrevs, 0..) |abbrev, i| {
                        if (Abbrev == abbrev) return i + 4;
                    }

                    @compileError("Unknown abbrev: " ++ @typeName(Abbrev));
                }

                pub fn writeAbbrevAdapted(
                    self: *Self,
                    params: anytype,
                    adapter: anytype,
                ) Error!void {
                    const Abbrev = @TypeOf(params);

                    try self.bitcode.writeBits(comptime abbrevId(Abbrev), abbrev_len);

                    const field_names = comptime std.meta.fieldNames(Abbrev);

                    // This abbreviation might only contain literals
                    if (field_names.len == 0) return;

                    comptime var field_index: usize = 0;
                    inline for (Abbrev.ops) |ty| {
                        const param = @field(params, field_names[field_index]);
                        switch (ty) {
                            .literal => continue,
                            .fixed => |len| try self.bitcode.writeBits(adapter.get(param), len),
                            .fixed_runtime => |width_ty| try self.bitcode.writeBits(
                                adapter.get(param),
                                self.bitcode.getTypeWidth(width_ty),
                            ),
                            .vbr => |len| try self.bitcode.writeVbr(adapter.get(param), len),
                            .char6 => try self.bitcode.write6BitChar(adapter.get(param)),
                            .blob => {
                                try self.bitcode.writeVbr(param.len, 6);
                                try self.bitcode.writeBlob(param);
                            },
                            .array_fixed => |len| {
                                try self.bitcode.writeVbr(param.len, 6);
                                for (param) |x| {
                                    try self.bitcode.writeBits(adapter.get(x), len);
                                }
                            },
                            .array_fixed_runtime => |width_ty| {
                                try self.bitcode.writeVbr(param.len, 6);
                                for (param) |x| {
                                    try self.bitcode.writeBits(
                                        adapter.get(x),
                                        self.bitcode.getTypeWidth(width_ty),
                                    );
                                }
                            },
                            .array_vbr => |len| {
                                try self.bitcode.writeVbr(param.len, 6);
                                for (param) |x| {
                                    try self.bitcode.writeVbr(adapter.get(x), len);
                                }
                            },
                            .array_char6 => {
                                try self.bitcode.writeVbr(param.len, 6);
                                for (param) |x| {
                                    try self.bitcode.write6BitChar(adapter.get(x));
                                }
                            },
                        }
                        field_index += 1;
                        if (field_index == field_names.len) break;
                    }
                }

                pub fn defineAbbrev(self: *Self, comptime ops: []const AbbrevOp) Error!void {
                    const bitcode = self.bitcode;
                    try bitcode.writeBits(2, abbrev_len);

                    // ops.len is not accurate because arrays are actually two ops
                    try bitcode.writeVbr(blk: {
                        var count: usize = 0;
                        inline for (ops) |op| {
                            count += switch (op) {
                                .literal, .fixed, .fixed_runtime, .vbr, .char6, .blob => 1,
                                .array_fixed, .array_fixed_runtime, .array_vbr, .array_char6 => 2,
                            };
                        }
                        break :blk count;
                    }, 5);

                    inline for (ops) |op| {
                        switch (op) {
                            .literal => |value| {
                                try bitcode.writeBits(1, 1);
                                try bitcode.writeVbr(value, 8);
                            },
                            .fixed => |width| {
                                try bitcode.writeBits(0, 1);
                                try bitcode.writeBits(1, 3);
                                try bitcode.writeVbr(width, 5);
                            },
                            .fixed_runtime => |width_ty| {
                                try bitcode.writeBits(0, 1);
                                try bitcode.writeBits(1, 3);
                                try bitcode.writeVbr(bitcode.getTypeWidth(width_ty), 5);
                            },
                            .vbr => |width| {
                                try bitcode.writeBits(0, 1);
                                try bitcode.writeBits(2, 3);
                                try bitcode.writeVbr(width, 5);
                            },
                            .char6 => {
                                try bitcode.writeBits(0, 1);
                                try bitcode.writeBits(4, 3);
                            },
                            .blob => {
                                try bitcode.writeBits(0, 1);
                                try bitcode.writeBits(5, 3);
                            },
                            .array_fixed => |width| {
                                // Array op
                                try bitcode.writeBits(0, 1);
                                try bitcode.writeBits(3, 3);

                                // Fixed or VBR op
                                try bitcode.writeBits(0, 1);
                                try bitcode.writeBits(1, 3);
                                try bitcode.writeVbr(width, 5);
                            },
                            .array_fixed_runtime => |width_ty| {
                                // Array op
                                try bitcode.writeBits(0, 1);
                                try bitcode.writeBits(3, 3);

                                // Fixed or VBR op
                                try bitcode.writeBits(0, 1);
                                try bitcode.writeBits(1, 3);
                                try bitcode.writeVbr(bitcode.getTypeWidth(width_ty), 5);
                            },
                            .array_vbr => |width| {
                                // Array op
                                try bitcode.writeBits(0, 1);
                                try bitcode.writeBits(3, 3);

                                // Fixed or VBR op
                                try bitcode.writeBits(0, 1);
                                try bitcode.writeBits(2, 3);
                                try bitcode.writeVbr(width, 5);
                            },
                            .array_char6 => {
                                // Array op
                                try bitcode.writeBits(0, 1);
                                try bitcode.writeBits(3, 3);

                                // Char6 op
                                try bitcode.writeBits(0, 1);
                                try bitcode.writeBits(4, 3);
                            },
                        }
                    }
                }
            };
        }
    };
}

fn charTo6Bit(c: u8) u8 {
    return switch (c) {
        'a'...'z' => c - 'a',
        'A'...'Z' => c - 'A' + 26,
        '0'...'9' => c - '0' + 52,
        '.' => 62,
        '_' => 63,
        else => @panic("Failed to encode byte as 6-bit char"),
    };
}

fn BufType(comptime T: type, comptime min_len: usize) type {
    return @Int(.unsigned, @max(min_len, @bitSizeOf(switch (@typeInfo(T)) {
        .comptime_int => u32,
        .int => |info| if (info.signedness == .unsigned)
            T
        else
            @compileError("Unsupported type: " ++ @typeName(T)),
        .@"enum" => |info| info.tag_type,
        .bool => u1,
        .@"struct" => |info| switch (info.layout) {
            .auto, .@"extern" => @compileError("Unsupported type: " ++ @typeName(T)),
            .@"packed" => @Int(.unsigned, @bitSizeOf(T)),
        },
        else => @compileError("Unsupported type: " ++ @typeName(T)),
    })));
}

fn bufValue(value: anytype, comptime min_len: usize) BufType(@TypeOf(value), min_len) {
    return switch (@typeInfo(@TypeOf(value))) {
        .comptime_int, .int => @intCast(value),
        .@"enum" => @backingInt(value),
        .bool => @intFromBool(value),
        .@"struct" => @intCast(@as(@Int(.unsigned, @bitSizeOf(@TypeOf(value))), @bitCast(value))),
        else => unreachable,
    };
}
