/// credits to Andrew Kelley
/// strings.zig
const std = @import("std");
const mem = std.mem;
const assert = std.debug.assert;

const Allocator = std.mem.Allocator;
const Self = @This();

const max_load_percent = std.hash_map.default_max_load_percentage;

string_bytes: std.ArrayListUnmanaged(u8) = .empty,
string_table: StringIndex.Table = .empty,

pub const empty = Self{
    .string_bytes = .empty,
    .string_table = .empty,
};

pub fn deinit(self: *Self, allocator: Allocator) void {
    self.string_bytes.deinit(allocator);
    self.string_table.deinit(allocator);
}

pub const StringIndex = enum(u32) {
    _,

    pub const Table = std.HashMapUnmanaged(StringIndex, void, TableContext, max_load_percent);

    pub const TableContext = struct {
        bytes: []const u8,

        pub fn eql(_: @This(), a: StringIndex, b: StringIndex) bool {
            return a == b;
        }

        pub fn hash(ctx: @This(), key: StringIndex) u64 {
            return std.hash_map.hashString(mem.sliceTo(ctx.bytes[@intFromEnum(key)..], 0));
        }
    };

    pub const TableIndexAdapter = struct {
        bytes: []const u8,

        pub fn eql(ctx: @This(), a: []const u8, b: StringIndex) bool {
            return mem.eql(u8, a, mem.sliceTo(ctx.bytes[@intFromEnum(b)..], 0));
        }

        pub fn hash(_: @This(), adapted_key: []const u8) u64 {
            assert(mem.indexOfScalar(u8, adapted_key, 0) == null);
            return std.hash_map.hashString(adapted_key);
        }
    };

    pub fn slice(index: StringIndex, state: *const Self) [:0]const u8 {
        const start_slice = state.string_bytes.items[@intFromEnum(index)..];
        return start_slice[0..mem.indexOfScalar(u8, start_slice, 0).? :0];
    }

    pub fn iterator(start: StringIndex, bytes: []const u8) Iterator {
        return .{
            .bytes = bytes,
            .pos = @intFromEnum(start),
        };
    }

    pub const Iterator = struct {
        bytes: []const u8,
        pos: usize = 0,

        pub fn next(self: *Iterator) ?[:0]const u8 {
            if (self.pos >= self.bytes.len) return null;

            // Find the next null terminator starting from current position
            const end_pos = mem.indexOfScalarPos(u8, self.bytes, self.pos, 0) orelse {
                // No null found: return remaining bytes (invalid, but handle gracefully)
                const s = self.bytes[self.pos..];
                self.pos = self.bytes.len;
                return s;
            };

            const s = self.bytes[self.pos..end_pos :0];
            self.pos = end_pos + 1; // Skip the null terminator
            return s;
        }
    };
};

pub fn add(state: *Self, allocator: Allocator, bytes: []const u8) !StringIndex {
    try state.string_bytes.ensureUnusedCapacity(allocator, bytes.len + 1);

    const gop = try state.string_table.getOrPutContextAdapted(
        allocator,
        bytes,
        StringIndex.TableIndexAdapter{ .bytes = state.string_bytes.items },
        StringIndex.TableContext{ .bytes = state.string_bytes.items },
    );
    if (gop.found_existing) return gop.key_ptr.*;

    const new_off: StringIndex = @enumFromInt(state.string_bytes.items.len);

    state.string_bytes.appendSliceAssumeCapacity(bytes);
    state.string_bytes.appendAssumeCapacity(0);

    gop.key_ptr.* = new_off;

    return new_off;
}
