const std = @import("std");
const Environment = @import("./env.zig");
const strings = @import("./string_immutable.zig");
const bun = @import("./global.zig");

// -- Failed Experiment --
// Delete this code later
// -- Failed Experiment --
// Writing tons of < 8 byte chunks is kind of expensive
// because we have to loop through them to copy and then again to encode
// It would be faster if we could use SIMD
// but the behavior is out of our control
// so instead, we copy the _unencoded_ bytes to a buffer
// Then, just before we send it over the network, we encode it, usually in-place
// The caveat is if the encoding changes
// pub const Delayer = struct {
//     last_write: u32 = 0,
//     last_encoding: Encoding = Encoding.bytes,

//     const log = bun.Output.scoped(.Delayer, true);
//     pub const Encoding = enum {
//         bytes,
//         utf16,
//         latin1,
//     };

//     fn flushLatin1(this: *Delayer, list_: BabyList(u8), allocator: std.mem.Allocator) !BabyList(u8) {
//         var list = list_;
//         var remain = list.slice()[this.last_write..];
//         const element_count = strings.elementLengthLatin1IntoUTF8([]const u8, remain);
//         log("flushLatin1({any}, {any})", .{ .element_count = element_count, .remain = remain.len });
//         // common case: nothing to do, it's just ascii
//         if (element_count == remain.len) {
//             this.last_write += @truncate(u32, remain.len);

//             return list;
//         }

//         std.debug.assert(element_count > remain.len);
//         var arraylist = list.listManaged(allocator);
//         // assert we have enough room
//         try arraylist.ensureUnusedCapacity(element_count - remain.len);

//         list.update(arraylist);
//         var read_remain = arraylist.items.ptr[this.last_write..arraylist.items.len];
//         var write_remain = arraylist.items.ptr[this.last_write .. arraylist.items.len + (element_count - remain.len)];
//         std.debug.assert(write_remain.len > 0);
//         std.debug.assert(read_remain.len > 0);
//         std.debug.assert(write_remain.len > read_remain.len);
//         this.last_write += @truncate(u32, write_remain.len);
//         list.len += @intCast(u32, element_count - remain.len);

//         // faster path: stack allocated buffer
//         if (write_remain.len <= 4096) {
//             var buf: [4096]u8 = undefined;
//             const result = strings.copyLatin1IntoUTF8(&buf, []const u8, remain);
//             std.debug.assert(@as(usize, result.written) == write_remain.len);
//             std.debug.assert(@as(usize, result.read) == read_remain.len);
//             @memcpy(write_remain.ptr, &buf, write_remain.len);
//         } else {
//             // slow path
//             var temp_buf = try allocator.dupe(u8, read_remain);
//             defer allocator.free(temp_buf);
//             const result = strings.copyLatin1IntoUTF8(write_remain, []const u8, temp_buf);

//             std.debug.assert(@as(usize, result.written) == write_remain.len);
//             std.debug.assert(@as(usize, result.read) == read_remain.len);
//         }

//         return list;
//     }
//     fn flushUTF16(this: *Delayer, list_: BabyList(u8), allocator: std.mem.Allocator) !BabyList(u8) {
//         var list = list_;
//         var remain = std.mem.bytesAsSlice(u16, list.slice()[this.last_write..]);
//         const element_count = strings.elementLengthUTF16IntoUTF8(@TypeOf(remain), remain) * 2;
//         log("flushUTF16({any}, {any})", .{ .element_count = element_count, .remain = remain.len });

//         var arraylist = list.listManaged(allocator);

//         // assert we have enough room
//         const grow = element_count - list.slice()[this.last_write..].len;
//         try arraylist.ensureUnusedCapacity(grow);
//         list.update(arraylist);
//         var write_remain = arraylist.items.ptr[this.last_write .. arraylist.items.len + grow];
//         this.last_write += @truncate(u32, grow);
//         list.len += @intCast(u32, grow);

//         var buf: [4096]u8 = undefined;

//         if (element_count < buf.len) {
//             const result = strings.copyUTF16IntoUTF8(&buf, @TypeOf(remain), remain);
//             std.debug.assert(@as(usize, result.written * 2) == write_remain.len);
//             std.debug.assert(@as(usize, result.read) == remain.len);
//             @memcpy(write_remain.ptr, &buf, write_remain.len);
//         } else {
//             // slow path
//             var temp_buf = try allocator.alloc(u16, remain.len);
//             @memcpy(std.mem.sliceAsBytes(temp_buf).ptr, std.mem.sliceAsBytes(remain).ptr, std.mem.sliceAsBytes(remain).len);
//             defer allocator.free(temp_buf);
//             const result = strings.copyUTF16IntoUTF8(write_remain, @TypeOf(temp_buf), temp_buf);
//             std.debug.assert(@as(usize, result.written * 2) == write_remain.len);
//             std.debug.assert(@as(usize, result.read) == remain.len);
//         }

//         return list;
//     }
//     pub fn writeUTF16(this: *Delayer, list_: BabyList(u8), str: []const u16, allocator: std.mem.Allocator) !BabyList(u8) {
//         var list = list_;
//         log("writeUTF16({any}, {any})", .{ .delayer = this, .len = str.len });

//         {
//             switch (this.last_encoding) {
//                 .latin1 => {
//                     list = try this.flushLatin1(list, allocator);
//                     this.last_write = list.len;
//                     var arraylist = list.listManaged(allocator);
//                     var bytes = std.mem.sliceAsBytes(str);
//                     try arraylist.ensureUnusedCapacity(bytes.len);
//                     @memcpy(arraylist.items.ptr + arraylist.items.len, bytes.ptr, bytes.len);
//                     this.last_encoding = .utf16;
//                     list.update(arraylist);
//                     list.len += @intCast(u32, bytes.len);
//                     return list;
//                 },
//                 .bytes, .utf16 => |enc| {
//                     if (enc == .bytes) {
//                         this.last_write = list.len;
//                         this.last_encoding = .utf16;
//                     }
//                     var arraylist = list.listManaged(allocator);
//                     var bytes = std.mem.sliceAsBytes(str);
//                     try arraylist.ensureUnusedCapacity(bytes.len);
//                     @memcpy(arraylist.items.ptr + arraylist.items.len, bytes.ptr, bytes.len);
//                     list.update(arraylist);
//                     list.len += @intCast(u32, bytes.len);
//                 },
//             }
//         }

//         return list;
//     }

//     pub fn flush(this: *Delayer, list_: BabyList(u8), allocator: std.mem.Allocator) !BabyList(u8) {
//         if (this.last_encoding == .bytes) {
//             std.debug.assert(this.last_write == list_.len);
//             return list_;
//         }

//         var list = list_;
//         switch (this.last_encoding) {
//             .utf16 => {
//                 list = try this.flushUTF16(list_, allocator);
//                 this.last_write = list.len;
//                 this.last_encoding = .bytes;
//             },
//             .latin1 => {
//                 list = try this.flushLatin1(list_, allocator);
//                 this.last_write = list.len;
//                 this.last_encoding = .bytes;
//             },
//             .bytes => unreachable,
//         }

//         return list;
//     }

//     pub fn writeLatin1(this: *Delayer, list_: BabyList(u8), str: []const u8, allocator: std.mem.Allocator) !BabyList(u8) {
//         var list = list_;
//         log("writeLatin1({any}, {s})", .{ .delayer = this, .str = str });

//         {
//             switch (this.last_encoding) {
//                 .utf16 => {
//                     list = try this.flushUTF16(list, allocator);
//                     this.last_write = list.len;
//                     var arraylist = list.listManaged(allocator);
//                     var bytes = std.mem.sliceAsBytes(str);
//                     try arraylist.ensureUnusedCapacity(bytes.len);
//                     @memcpy(arraylist.items.ptr + arraylist.items.len, bytes.ptr, bytes.len);
//                     this.last_encoding = .latin1;
//                     list.update(arraylist);
//                     list.len += @intCast(u32, bytes.len);
//                     return list;
//                 },
//                 .bytes, .latin1 => |enc| {
//                     if (enc == .bytes) {
//                         this.last_write = list.len;
//                         this.last_encoding = .latin1;
//                     }
//                     var arraylist = list.listManaged(allocator);
//                     var bytes = std.mem.sliceAsBytes(str);
//                     try arraylist.ensureUnusedCapacity(bytes.len);
//                     @memcpy(arraylist.items.ptr + arraylist.items.len, bytes.ptr, bytes.len);
//                     list.update(arraylist);
//                     list.len += @intCast(u32, bytes.len);
//                 },
//             }
//         }

//         return list;
//     }

//     pub fn writeBytes(this: *Delayer, list_: BabyList(u8), str: []const u8, allocator: std.mem.Allocator) !BabyList(u8) {
//         var list = list_;
//         log("writeBytes({any}, {any})", .{ .delayer = this, .str = str });

//         {
//             switch (this.last_encoding) {
//                 .utf16 => {
//                     list = try this.flushUTF16(list, allocator);
//                 },
//                 .latin1 => {
//                     list = try this.flushLatin1(list, allocator);
//                 },
//                 else => {},
//             }

//             var arraylist = list.listManaged(allocator);
//             var bytes = std.mem.sliceAsBytes(str);
//             try arraylist.ensureUnusedCapacity(bytes.len);
//             @memcpy(arraylist.items.ptr + arraylist.items.len, bytes.ptr, bytes.len);
//             list.update(arraylist);
//             list.len += @intCast(u32, bytes.len);
//             this.last_write = list.len;
//             this.last_encoding = .bytes;
//         }

//         return list;
//     }
// };

/// This is like ArrayList except it stores the length and capacity as u32
/// In practice, it is very unusual to have lengths above 4 GB
///
/// This lets us have array lists which occupy the same amount of space as a slice
pub fn BabyList(comptime Type: type) type {
    return struct {
        const ListType = @This();
        ptr: [*]Type = undefined,
        len: u32 = 0,
        cap: u32 = 0,

        pub inline fn init(items: []const Type) ListType {
            @setRuntimeSafety(false);
            return ListType{
                // Remove the const qualifier from the items
                .ptr = @intToPtr([*]Type, @ptrToInt(items.ptr)),

                .len = @truncate(u32, items.len),
                .cap = @truncate(u32, items.len),
            };
        }

        pub inline fn fromList(list_: anytype) ListType {
            @setRuntimeSafety(false);

            if (comptime Environment.allow_assert) {
                std.debug.assert(list_.items.len <= list_.capacity);
            }

            return ListType{
                .ptr = list_.items.ptr,
                .len = @truncate(u32, list_.items.len),
                .cap = @truncate(u32, list_.capacity),
            };
        }

        pub fn update(this: *ListType, list_: anytype) void {
            @setRuntimeSafety(false);
            this.ptr = list_.items.ptr;
            this.len = @truncate(u32, list_.items.len);
            this.cap = @truncate(u32, list_.capacity);

            if (comptime Environment.allow_assert) {
                std.debug.assert(this.len <= this.cap);
            }
        }

        pub fn list(this: ListType) std.ArrayListUnmanaged(Type) {
            return std.ArrayListUnmanaged(Type){
                .items = this.ptr[0..this.len],
                .capacity = this.cap,
            };
        }

        pub fn listManaged(this: ListType, allocator: std.mem.Allocator) std.ArrayList(Type) {
            return std.ArrayList(Type){
                .items = this.ptr[0..this.len],
                .capacity = this.cap,
                .allocator = allocator,
            };
        }

        pub inline fn first(this: ListType) ?*Type {
            return if (this.len > 0) this.ptr[0] else @as(?*Type, null);
        }

        pub inline fn last(this: ListType) ?*Type {
            return if (this.len > 0) &this.ptr[this.len - 1] else @as(?*Type, null);
        }

        pub inline fn first_(this: ListType) Type {
            return this.ptr[0];
        }

        pub fn one(allocator: std.mem.Allocator, value: Type) !ListType {
            var items = try allocator.allocAdvanced(Type, @alignOf(Type), 1, .exact);
            items[0] = value;
            return ListType{
                .ptr = @ptrCast([*]Type, items.ptr),
                .len = 1,
                .cap = 1,
            };
        }

        pub inline fn @"[0]"(this: ListType) Type {
            return this.ptr[0];
        }
        const OOM = error{OutOfMemory};

        pub fn push(this: *ListType, allocator: std.mem.Allocator, value: Type) OOM!void {
            var list_ = this.list();
            try list_.append(allocator, value);
            this.update(list_);
        }

        pub inline fn slice(this: ListType) []Type {
            @setRuntimeSafety(false);
            return this.ptr[0..this.len];
        }

        pub fn write(this: *@This(), allocator: std.mem.Allocator, str: []const u8) !u32 {
            if (comptime Type != u8)
                @compileError("Unsupported for type " ++ @typeName(Type));
            const initial = this.len;
            var list_ = this.listManaged(allocator);
            try list_.appendSlice(str);
            this.update(list_);
            return this.len - initial;
        }
        pub fn writeLatin1(this: *@This(), allocator: std.mem.Allocator, str: []const u8) !u32 {
            if (comptime Type != u8)
                @compileError("Unsupported for type " ++ @typeName(Type));
            const initial = this.len;
            const old = this.listManaged(allocator);
            const new = try strings.allocateLatin1IntoUTF8WithList(old, old.items.len, []const u8, str);
            this.update(new);
            return this.len - initial;
        }
        pub fn writeUTF16(this: *@This(), allocator: std.mem.Allocator, str: []const u16) !u32 {
            if (comptime Type != u8)
                @compileError("Unsupported for type " ++ @typeName(Type));

            var list_ = this.listManaged(allocator);
            const initial = this.len;
            {
                defer this.update(list_);
                try list_.ensureTotalCapacityPrecise(list_.items.len + str.len + 4);

                var remain = str;
                while (remain.len > 0) {
                    const orig_len = list_.items.len;

                    var slice_ = list_.items.ptr[orig_len..list_.capacity];
                    const result = strings.copyUTF16IntoUTF8(slice_, []const u16, remain);
                    remain = remain[result.read..];
                    list_.items.len += @as(usize, result.written);
                    if (remain.len > 0) {
                        try list_.ensureTotalCapacityPrecise(list_.items.len + strings.elementLengthUTF16IntoUTF8([]const u16, remain));
                        continue;
                    }
                    if (result.read == 0 or result.written == 0) break;
                }
            }

            if (comptime Environment.allow_assert) {
                // sanity check that encoding produced a consistent result
                var allocated = try strings.toUTF8Alloc(allocator, str);
                defer allocator.free(allocated);
                const encoded = this.ptr[initial..this.len];
                std.testing.expectEqualStrings(allocated, encoded) catch unreachable;
            }
            return this.len - initial;
        }
    };
}
