const std = @import("../std.zig");
const io = std.io;
const testing = std.testing;
const assert = std.debug.assert;
const math = std.math;

/// Creates a stream which allows for writing bit fields to another stream
pub fn BitWriter(comptime endian: std.builtin.Endian, comptime WriterType: type) type {
    return struct {
        forward_writer: WriterType,
        bit_buffer: u8,
        bit_count: u4,

        pub const Error = WriterType.Error;
        pub const Writer = io.Writer(*Self, Error, write);

        const Self = @This();
        const u8_bit_count = @bitSizeOf(u8);
        const u4_bit_count = @bitSizeOf(u4);

        pub fn init(forward_writer: WriterType) Self {
            return Self{
                .forward_writer = forward_writer,
                .bit_buffer = 0,
                .bit_count = 0,
            };
        }

        /// Write the specified number of bits to the stream from the least significant bits of
        ///  the specified unsigned int value. Bits will only be written to the stream when there
        ///  are enough to fill a byte.
        pub fn writeBits(self: *Self, value: anytype, bits: usize) Error!void {
            if (bits == 0) return;

            const U = @TypeOf(value);
            comptime assert(@typeInfo(U).Int.signedness == .unsigned);

            //by extending the buffer to a minimum of u8 we can cover a number of edge cases
            // related to shifting and casting.
            const u_bit_count = @bitSizeOf(U);
            const buf_bit_count = bc: {
                assert(u_bit_count >= bits);
                break :bc if (u_bit_count <= u8_bit_count) u8_bit_count else u_bit_count;
            };
            const Buf = std.meta.Int(.unsigned, buf_bit_count);
            const BufShift = math.Log2Int(Buf);

            const buf_value = @as(Buf, @intCast(value));

            const high_byte_shift = @as(BufShift, @intCast(buf_bit_count - u8_bit_count));
            var in_buffer = switch (endian) {
                .big => buf_value << @as(BufShift, @intCast(buf_bit_count - bits)),
                .little => buf_value,
            };
            var in_bits = bits;

            if (self.bit_count > 0) {
                const bits_remaining = u8_bit_count - self.bit_count;
                const n = @as(u3, @intCast(if (bits_remaining > bits) bits else bits_remaining));
                switch (endian) {
                    .big => {
                        const shift = @as(BufShift, @intCast(high_byte_shift + self.bit_count));
                        const v = @as(u8, @intCast(in_buffer >> shift));
                        self.bit_buffer |= v;
                        in_buffer <<= n;
                    },
                    .little => {
                        const v = @as(u8, @truncate(in_buffer)) << @as(u3, @intCast(self.bit_count));
                        self.bit_buffer |= v;
                        in_buffer >>= n;
                    },
                }
                self.bit_count += n;
                in_bits -= n;

                //if we didn't fill the buffer, it's because bits < bits_remaining;
                if (self.bit_count != u8_bit_count) return;
                try self.forward_writer.writeByte(self.bit_buffer);
                self.bit_buffer = 0;
                self.bit_count = 0;
            }
            //at this point we know bit_buffer is empty

            //copy bytes until we can't fill one anymore, then leave the rest in bit_buffer
            while (in_bits >= u8_bit_count) {
                switch (endian) {
                    .big => {
                        const v = @as(u8, @intCast(in_buffer >> high_byte_shift));
                        try self.forward_writer.writeByte(v);
                        in_buffer <<= @as(u3, @intCast(u8_bit_count - 1));
                        in_buffer <<= 1;
                    },
                    .little => {
                        const v = @as(u8, @truncate(in_buffer));
                        try self.forward_writer.writeByte(v);
                        in_buffer >>= @as(u3, @intCast(u8_bit_count - 1));
                        in_buffer >>= 1;
                    },
                }
                in_bits -= u8_bit_count;
            }

            if (in_bits > 0) {
                self.bit_count = @as(u4, @intCast(in_bits));
                self.bit_buffer = switch (endian) {
                    .big => @as(u8, @truncate(in_buffer >> high_byte_shift)),
                    .little => @as(u8, @truncate(in_buffer)),
                };
            }
        }

        /// Flush any remaining bits to the stream.
        pub fn flushBits(self: *Self) Error!void {
            if (self.bit_count == 0) return;
            try self.forward_writer.writeByte(self.bit_buffer);
            self.bit_buffer = 0;
            self.bit_count = 0;
        }

        pub fn write(self: *Self, buffer: []const u8) Error!usize {
            // TODO: I'm not sure this is a good idea, maybe flushBits should be forced
            if (self.bit_count > 0) {
                for (buffer) |b|
                    try self.writeBits(b, u8_bit_count);
                return buffer.len;
            }

            return self.forward_writer.write(buffer);
        }

        pub fn writer(self: *Self) Writer {
            return .{ .context = self };
        }
    };
}

pub fn bitWriter(
    comptime endian: std.builtin.Endian,
    underlying_stream: anytype,
) BitWriter(endian, @TypeOf(underlying_stream)) {
    return BitWriter(endian, @TypeOf(underlying_stream)).init(underlying_stream);
}

test "api coverage" {
    var mem_be = [_]u8{0} ** 2;
    var mem_le = [_]u8{0} ** 2;

    var mem_out_be = io.fixedBufferStream(&mem_be);
    var bit_stream_be = bitWriter(.big, mem_out_be.writer());

    try bit_stream_be.writeBits(@as(u2, 1), 1);
    try bit_stream_be.writeBits(@as(u5, 2), 2);
    try bit_stream_be.writeBits(@as(u128, 3), 3);
    try bit_stream_be.writeBits(@as(u8, 4), 4);
    try bit_stream_be.writeBits(@as(u9, 5), 5);
    try bit_stream_be.writeBits(@as(u1, 1), 1);

    try testing.expect(mem_be[0] == 0b11001101 and mem_be[1] == 0b00001011);

    mem_out_be.pos = 0;

    try bit_stream_be.writeBits(@as(u15, 0b110011010000101), 15);
    try bit_stream_be.flushBits();
    try testing.expect(mem_be[0] == 0b11001101 and mem_be[1] == 0b00001010);

    mem_out_be.pos = 0;
    try bit_stream_be.writeBits(@as(u32, 0b110011010000101), 16);
    try testing.expect(mem_be[0] == 0b01100110 and mem_be[1] == 0b10000101);

    try bit_stream_be.writeBits(@as(u0, 0), 0);

    var mem_out_le = io.fixedBufferStream(&mem_le);
    var bit_stream_le = bitWriter(.little, mem_out_le.writer());

    try bit_stream_le.writeBits(@as(u2, 1), 1);
    try bit_stream_le.writeBits(@as(u5, 2), 2);
    try bit_stream_le.writeBits(@as(u128, 3), 3);
    try bit_stream_le.writeBits(@as(u8, 4), 4);
    try bit_stream_le.writeBits(@as(u9, 5), 5);
    try bit_stream_le.writeBits(@as(u1, 1), 1);

    try testing.expect(mem_le[0] == 0b00011101 and mem_le[1] == 0b10010101);

    mem_out_le.pos = 0;
    try bit_stream_le.writeBits(@as(u15, 0b110011010000101), 15);
    try bit_stream_le.flushBits();
    try testing.expect(mem_le[0] == 0b10000101 and mem_le[1] == 0b01100110);

    mem_out_le.pos = 0;
    try bit_stream_le.writeBits(@as(u32, 0b1100110100001011), 16);
    try testing.expect(mem_le[0] == 0b00001011 and mem_le[1] == 0b11001101);

    try bit_stream_le.writeBits(@as(u0, 0), 0);
}
