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

const assert = std.debug.assert;

const SimpleItem = @This();

id: Store.Id,
quantity: u64,

pub fn init(id: Store.Id, quantity: u64) SimpleItem {
    return SimpleItem{ .id = id, .quantity = quantity };
}

pub fn parse(store: *Store, slot_obj: std.json.ObjectMap) !?SimpleItem {
    const code = try json_utils.getStringRequired(slot_obj, "code");
    if (code.len == 0) return null;

    const quantity = try json_utils.getIntegerRequired(slot_obj, "quantity");
    if (quantity < 0) return error.InvalidQuantity;

    return SimpleItem{
        .id = try store.items.getOrReserveId(code),
        .quantity = @intCast(quantity),
    };
}

pub fn BoundedArray(comptime slot_count: u32) type {
    const Items = std.BoundedArray(SimpleItem, slot_count);

    return struct {
        items: Items,

        pub fn init() @This() {
            return @This(){ .items = Items.init(0) catch unreachable };
        }

        pub fn parse(store: *Store, slots_array: std.json.Array) !@This() {
            var slots = Items.init(0) catch unreachable;

            for (slots_array.items) |slot_value| {
                const slot_obj = json_utils.asObject(slot_value) orelse return error.InvalidType;

                if (try SimpleItem.parse(store, slot_obj)) |slot| {
                    try slots.append(slot);
                }
            }

            return @This(){ .items = slots };
        }

        fn findSlotIndex(self: *const @This(), id: Store.Id) ?usize {
            for (0.., self.items.slice()) |i, *slot| {
                if (slot.id == id) {
                    return i;
                }
            }

            return null;
        }

        fn findSlot(self: *@This(), id: Store.Id) ?*SimpleItem {
            if (self.findSlotIndex(id)) |index| {
                return &self.items.buffer[index];
            }

            return null;
        }

        pub fn remove(self: *@This(), id: Store.Id, quantity: u64) void {
            const slot_index = self.findSlotIndex(id) orelse unreachable;
            const slot = self.items.get(slot_index);
            assert(slot.quantity >= quantity);

            slot.quantity -= quantity;
            if (slot.quantity == 0) {
                self.items.swapRemove(slot_index);
            }
        }

        pub fn add(self: *@This(), id: Store.Id, quantity: u64) !void {
            if (quantity == 0) return;

            if (self.findSlot(id)) |slot| {
                slot.quantity += quantity;
            } else {
                try self.items.append(SimpleItem.init(id, quantity));
            }
        }

        pub fn addSlice(self: *@This(), items: []const SimpleItem) void {
            for (items) |item| {
                self.add(item.id, item.quantity);
            }
        }

        pub fn removeSlice(self: *@This(), items: []const SimpleItem) void {
            for (items) |item| {
                self.remove(item.id, item.quantity);
            }
        }

        pub fn getQuantity(self: *const @This(), id: Store.Id) u64 {
            if (self.findSlotIndex(id)) |index| {
                return self.items.get(index).quantity;
            }

            return 0;
        }

        pub fn totalQuantity(self: *const @This()) u64 {
            var count: u64 = 0;
            for (self.items.constSlice()) |slot| {
                count += slot.quantity;
            }
            return count;
        }

        pub fn slice(self: *@This()) []SimpleItem {
            return self.items.slice();
        }

        pub fn format(
            self: @This(),
            comptime fmt: []const u8,
            options: std.fmt.FormatOptions,
            writer: anytype,
        ) !void {
            _ = fmt;
            _ = options;

            try writer.print("{s}{{ ", .{@typeName(@This())});

            for (self.items.slice()) |item| {
                try writer.print("{}, ", .{item});
            }

            try writer.writeAll("}");
        }
    };
}
