const std = @import("std");
const expect = std.testing.expect;

const strings = @import("string_immutable.zig");
const js_lexer = @import("js_lexer.zig");

const string = @import("string_types.zig").string;
const stringZ = @import("string_types.zig").stringZ;
const CodePoint = @import("string_types.zig").CodePoint;

pub const MutableString = struct {
    allocator: std.mem.Allocator,
    list: std.ArrayListUnmanaged(u8),

    pub fn init2048(allocator: std.mem.Allocator) !MutableString {
        return MutableString.init(allocator, 2048);
    }

    pub const Writer = std.io.Writer(*@This(), anyerror, MutableString.writeAll);
    pub fn writer(self: *MutableString) Writer {
        return Writer{
            .context = self,
        };
    }

    pub fn deinit(str: *MutableString) void {
        if (str.list.capacity > 0) {
            str.list.expandToCapacity();
            str.list.clearAndFree(str.allocator);
        }
    }

    pub fn growIfNeeded(self: *MutableString, amount: usize) !void {
        try self.list.ensureUnusedCapacity(self.allocator, amount);
    }

    pub fn write(self: *MutableString, bytes: anytype) !usize {
        try self.list.appendSlice(self.allocator, bytes);
        return bytes.len;
    }

    pub fn bufferedWriter(self: *MutableString) BufferedWriter {
        return BufferedWriter{ .context = self };
    }

    pub fn init(allocator: std.mem.Allocator, capacity: usize) !MutableString {
        return MutableString{ .allocator = allocator, .list = if (capacity > 0)
            try std.ArrayListUnmanaged(u8).initCapacity(allocator, capacity)
        else
            std.ArrayListUnmanaged(u8){} };
    }

    pub fn initEmpty(allocator: std.mem.Allocator) MutableString {
        return MutableString{ .allocator = allocator, .list = .{} };
    }

    pub const ensureUnusedCapacity = growIfNeeded;

    pub fn initCopy(allocator: std.mem.Allocator, str: anytype) !MutableString {
        var mutable = try MutableString.init(allocator, std.mem.len(str));
        try mutable.copy(str);
        return mutable;
    }

    // Convert it to an ASCII identifier. Note: If you change this to a non-ASCII
    // identifier, you're going to potentially cause trouble with non-BMP code
    // points in target environments that don't support bracketed Unicode escapes.

    pub fn ensureValidIdentifier(str: string, allocator: std.mem.Allocator) !string {
        if (str.len == 0) {
            return "_";
        }

        var iterator = strings.CodepointIterator.init(str);
        var cursor = strings.CodepointIterator.Cursor{};

        var has_needed_gap = false;
        var needs_gap = false;
        var start_i: usize = 0;

        if (!iterator.next(&cursor)) return "_";

        const JSLexerTables = @import("./js_lexer_tables.zig");

        // Common case: no gap necessary. No allocation necessary.
        needs_gap = !js_lexer.isIdentifierStart(cursor.c);
        if (!needs_gap) {
            // Are there any non-alphanumeric chars at all?
            while (iterator.next(&cursor)) {
                if (!js_lexer.isIdentifierContinue(cursor.c) or cursor.width > 1) {
                    needs_gap = true;
                    start_i = cursor.i;
                    break;
                }
            }
        }

        if (!needs_gap and str.len >= 3 and str.len <= 10) {
            return JSLexerTables.StrictModeReservedWordsRemap.get(str) orelse str;
        }

        if (needs_gap) {
            var mutable = try MutableString.initCopy(allocator, str[0..start_i]);
            needs_gap = false;

            var slice = str[start_i..];
            iterator = strings.CodepointIterator.init(slice);
            cursor = strings.CodepointIterator.Cursor{};

            while (iterator.next(&cursor)) {
                if (js_lexer.isIdentifierContinue(cursor.c) and cursor.width == 1) {
                    if (needs_gap) {
                        try mutable.appendChar('_');
                        needs_gap = false;
                        has_needed_gap = true;
                    }
                    try mutable.append(slice[cursor.i .. cursor.i + @as(u32, cursor.width)]);
                } else if (!needs_gap) {
                    needs_gap = true;
                    // skip the code point, replace it with a single _
                }
            }

            // If it ends with an emoji
            if (needs_gap) {
                try mutable.appendChar('_');
                needs_gap = false;
                has_needed_gap = true;
            }

            return mutable.list.toOwnedSlice(allocator);
        }

        return str;
    }

    pub fn len(self: *const MutableString) usize {
        return self.list.items.len;
    }

    pub fn copy(self: *MutableString, str: anytype) !void {
        try self.list.ensureTotalCapacity(self.allocator, std.mem.len(str[0..]));

        if (self.list.items.len == 0) {
            try self.list.insertSlice(self.allocator, 0, str);
        } else {
            try self.list.replaceRange(self.allocator, 0, std.mem.len(str[0..]), str[0..]);
        }
    }

    pub inline fn growBy(self: *MutableString, amount: usize) !void {
        try self.list.ensureUnusedCapacity(self.allocator, amount);
    }

    pub inline fn appendSlice(self: *MutableString, slice: []const u8) !void {
        try self.list.appendSlice(self.allocator, slice);
    }

    pub inline fn reset(
        self: *MutableString,
    ) void {
        self.list.shrinkRetainingCapacity(0);
    }

    pub fn inflate(self: *MutableString, amount: usize) !void {
        try self.list.resize(self.allocator, amount);
    }

    pub inline fn appendChar(self: *MutableString, char: u8) !void {
        try self.list.append(self.allocator, char);
    }
    pub inline fn appendCharAssumeCapacity(self: *MutableString, char: u8) void {
        self.list.appendAssumeCapacity(char);
    }
    pub inline fn append(self: *MutableString, char: []const u8) !void {
        try self.list.appendSlice(self.allocator, char);
    }
    pub inline fn appendAssumeCapacity(self: *MutableString, char: []const u8) void {
        self.list.appendSliceAssumeCapacity(
            char,
        );
    }
    pub inline fn lenI(self: *MutableString) i32 {
        return @intCast(i32, self.list.items.len);
    }

    pub fn toOwnedSlice(self: *MutableString) string {
        return self.list.toOwnedSlice(self.allocator);
    }

    pub fn toOwnedSliceLeaky(self: *MutableString) []u8 {
        return self.list.items;
    }

    pub fn toOwnedSentinelLeaky(self: *MutableString) [:0]u8 {
        if (self.list.items.len > 0 and self.list.items[self.list.items.len - 1] != 0) {
            self.list.append(
                self.allocator,
                0,
            ) catch unreachable;
        }

        return self.list.items[0 .. self.list.items.len - 1 :0];
    }

    pub fn toOwnedSliceLength(self: *MutableString, length: usize) string {
        self.list.shrinkAndFree(self.allocator, length);
        return self.list.toOwnedSlice(self.allocator);
    }

    // pub fn deleteAt(self: *MutableString, i: usize)  {
    //     self.list.swapRemove(i);
    // }

    pub fn containsChar(self: *const MutableString, char: u8) bool {
        return self.indexOfChar(char) != null;
    }

    pub fn indexOfChar(self: *const MutableString, char: u8) ?usize {
        return strings.indexOfChar(self.list.items, char);
    }

    pub fn lastIndexOfChar(self: *const MutableString, char: u8) ?usize {
        return strings.lastIndexOfChar(self.list.items, char);
    }

    pub fn lastIndexOf(self: *const MutableString, str: u8) ?usize {
        return strings.lastIndexOfChar(self.list.items, str);
    }

    pub fn indexOf(self: *const MutableString, str: u8) ?usize {
        return std.mem.indexOf(u8, self.list.items, str);
    }

    pub fn eql(self: *MutableString, other: anytype) bool {
        return std.mem.eql(u8, self.list.items, other);
    }

    pub fn toSocketBuffers(self: *MutableString, comptime count: usize, ranges: anytype) [count]std.x.os.Buffer {
        var buffers: [count]std.x.os.Buffer = undefined;
        comptime var i: usize = 0;
        inline while (i < count) : (i += 1) {
            buffers[i] = std.x.os.Buffer.from(self.list.items[ranges[i][0]..ranges[i][1]]);
        }
        return buffers;
    }

    pub const BufferedWriter = struct {
        context: *MutableString,
        buffer: [max]u8 = undefined,
        pos: usize = 0,

        const max = 2048;

        pub const Writer = std.io.Writer(*BufferedWriter, anyerror, BufferedWriter.writeAll);

        inline fn remain(this: *BufferedWriter) []u8 {
            return this.buffer[this.pos..];
        }

        pub fn flush(this: *BufferedWriter) !void {
            _ = try this.context.writeAll(this.buffer[0..this.pos]);
            this.pos = 0;
        }

        pub fn writeAll(this: *BufferedWriter, bytes: []const u8) anyerror!usize {
            var pending = bytes;

            if (pending.len >= max) {
                try this.flush();
                try this.context.append(pending);
                return pending.len;
            }

            if (pending.len > 0) {
                if (pending.len + this.pos > max) {
                    try this.flush();
                }
                @memcpy(this.remain().ptr, pending.ptr, pending.len);
                this.pos += pending.len;
            }

            return pending.len;
        }

        const E = @import("./js_ast.zig").E;

        /// Write a E.String to the buffer.
        /// This automatically encodes UTF-16 into UTF-8 using
        /// the same code path as TextEncoder
        pub fn writeString(this: *BufferedWriter, bytes: *E.String) anyerror!usize {
            if (bytes.isUTF8()) {
                return try this.writeAll(bytes.slice(this.context.allocator));
            }

            return try this.writeAll16(bytes.slice16());
        }

        /// Write a UTF-16 string to the (UTF-8) buffer
        /// This automatically encodes UTF-16 into UTF-8 using
        /// the same code path as TextEncoder
        pub fn writeAll16(this: *BufferedWriter, bytes: []const u16) anyerror!usize {
            var pending = bytes;

            if (pending.len >= max) {
                try this.flush();
                try this.context.list.ensureUnusedCapacity(this.context.allocator, bytes.len * 2);
                const decoded = strings.copyUTF16IntoUTF8(
                    this.remain()[0 .. bytes.len * 2],
                    []const u16,
                    bytes,
                );
                this.context.list.items.len += @as(usize, decoded.written);
                return pending.len;
            }

            if (pending.len > 0) {
                if ((pending.len * 2) + this.pos > max) {
                    try this.flush();
                }
                const decoded = strings.copyUTF16IntoUTF8(
                    this.remain()[0 .. bytes.len * 2],
                    []const u16,
                    bytes,
                );
                this.pos += @as(usize, decoded.written);
            }

            return pending.len;
        }

        pub fn writeHTMLAttributeValueString(this: *BufferedWriter, str: *E.String) anyerror!void {
            if (str.isUTF8()) {
                try this.writeHTMLAttributeValue(str.slice(this.context.allocator));
                return;
            }

            try this.writeHTMLAttributeValue16(str.slice16());
        }

        pub fn writeHTMLAttributeValue(this: *BufferedWriter, bytes: []const u8) anyerror!void {
            var slice = bytes;
            while (slice.len > 0) {
                // TODO: SIMD
                if (strings.indexOfAny(slice, "\"<>")) |j| {
                    _ = try this.writeAll(slice[0..j]);
                    _ = switch (slice[j]) {
                        '"' => try this.writeAll("&quot;"),
                        '<' => try this.writeAll("&lt;"),
                        '>' => try this.writeAll("&gt;"),
                        else => unreachable,
                    };

                    slice = slice[j + 1 ..];
                    continue;
                }

                _ = try this.writeAll(slice);
                break;
            }
        }

        pub fn writeHTMLAttributeValue16(this: *BufferedWriter, bytes: []const u16) anyerror!void {
            var slice = bytes;
            while (slice.len > 0) {
                if (strings.indexOfAny16(slice, "\"<>")) |j| {
                    // this won't handle strings larger than 4 GB
                    // that's fine though, 4 GB of SSR'd HTML is quite a lot...
                    _ = try this.writeAll16(slice[0..j]);
                    _ = switch (slice[j]) {
                        '"' => try this.writeAll("&quot;"),
                        '<' => try this.writeAll("&lt;"),
                        '>' => try this.writeAll("&gt;"),
                        else => unreachable,
                    };

                    slice = slice[j + 1 ..];
                    continue;
                }

                _ = try this.writeAll16(slice);
                break;
            }
        }

        pub fn writer(this: *BufferedWriter) BufferedWriter.Writer {
            return BufferedWriter.Writer{ .context = this };
        }
    };

    pub fn writeAll(self: *MutableString, bytes: string) !usize {
        try self.list.appendSlice(self.allocator, bytes);
        return bytes.len;
    }
};

test "MutableString" {
    const alloc = std.heap.page_allocator;

    var str = try MutableString.initCopy(alloc, "hello");
    try expect(str.eql("hello"));
}

test "MutableString.ensureValidIdentifier" {
    const alloc = std.heap.page_allocator;

    try std.testing.expectEqualStrings("jquery", try MutableString.ensureValidIdentifier("jquery", alloc));
    try std.testing.expectEqualStrings("jquery_foo", try MutableString.ensureValidIdentifier("jquery😋foo", alloc));
}

test "MutableString BufferedWriter" {
    const alloc = std.heap.page_allocator;

    var str = try MutableString.init(alloc, 0);
    var buffered_writer = str.bufferedWriter();
    var writer = buffered_writer.writer();
    try writer.writeAll("hello world hello world hello world hello world hello world hello world");
    try writer.context.flush();
    str = writer.context.context.*;
    try std.testing.expectEqualStrings("hello world hello world hello world hello world hello world hello world", str.toOwnedSlice());
}
