// zig fmt: off
const std = @import("std");
const s2s = @import("s2s");
const Item = @import("./schemas/item.zig");
const Character = @import("./schemas/character.zig");
const Task = @import("./schemas/task.zig");
const Monster = @import("./schemas/monster.zig");
const Resource = @import("./schemas/resource.zig");
const Map = @import("./schemas/map.zig");
const Position = @import("./schemas/position.zig");

const Skin = Character.Skin;

pub const Id = usize;

const Store = @This();

const max_character_images = std.meta.fields(Skin).len;
const max_character_image_code = size: {
    var size = Skin.toString(.men1).len;
    for (std.meta.fields(Skin)) |field| {
        size = @max(size, Skin.toString(@enumFromInt(field.value)).len);
    }

    break :size size;
};

pub const Image = struct {
    pub const Category = enum {
        character,
        item,
        monster,
        map,
        resource,
        effect
    };

    pub const max_code_size = size: {
        var size: usize = 0;

        size = @max(size, Item.max_code_size);
        size = @max(size, max_character_image_code);
        size = @max(size, Monster.max_code_size);
        size = @max(size, Resource.max_code_size);
        size = @max(size, Map.max_skin_size);
        // TODO: effect code size

        break :size size;
    };
    pub const Code = std.BoundedArray(u8, max_code_size);

    code: Code,
    width: u32,
    height: u32,
    rgba_offset: u32,
};

const Images = struct {
    const ImagesArray = std.ArrayListUnmanaged(Image);
    const IdArray = std.ArrayListUnmanaged(Id);
    const CategoryMap = std.EnumArray(Image.Category, IdArray);

    const Options = struct {
        max_rgba_data: u32,

        max_items: u32,
        max_monsters: u32,
        max_maps: u32,
        max_resources: u32,
        max_effects: u32,
    };

    rgba_data: []u8,
    rgba_fba: std.heap.FixedBufferAllocator,
    category_mapping: CategoryMap,
    images: ImagesArray,

    pub fn initCapacity(allocator: std.mem.Allocator, opts: Options) !Images {
        const max_characters = std.meta.fields(Character.Skin).len;

        var max_images: u32 = 0;
        max_images += @intCast(max_characters);
        max_images += opts.max_items;
        max_images += opts.max_monsters;
        max_images += opts.max_maps;
        max_images += opts.max_resources;
        max_images += opts.max_effects;

        const rgba_data = try allocator.alloc(u8, opts.max_rgba_data);
        errdefer allocator.free(rgba_data);

        var images = try ImagesArray.initCapacity(allocator, max_images);
        errdefer images.deinit(allocator);

        var character_images = try IdArray.initCapacity(allocator, max_characters);
        errdefer character_images.deinit(allocator);

        var item_images = try IdArray.initCapacity(allocator, opts.max_items);
        errdefer item_images.deinit(allocator);

        var monster_images = try IdArray.initCapacity(allocator, opts.max_monsters);
        errdefer monster_images.deinit(allocator);

        var map_images = try IdArray.initCapacity(allocator, opts.max_maps);
        errdefer map_images.deinit(allocator);

        var resource_images = try IdArray.initCapacity(allocator, opts.max_resources);
        errdefer resource_images.deinit(allocator);

        var effect_images = try IdArray.initCapacity(allocator, opts.max_effects);
        errdefer effect_images.deinit(allocator);

        const category_mapping = CategoryMap.init(.{
            .character = character_images,
            .item = item_images,
            .monster = monster_images,
            .resource = resource_images,
            .effect = effect_images,
            .map = map_images
        });

        return Images{
            .rgba_data = rgba_data,
            .rgba_fba = std.heap.FixedBufferAllocator.init(rgba_data),
            .category_mapping = category_mapping,
            .images = images
        };
    }

    pub fn deinit(self: *Images, allocator: std.mem.Allocator) void {
        allocator.free(self.rgba_data);
        self.images.deinit(allocator);
        self.rgba_fba.reset();

        var iter = self.category_mapping.iterator();
        while (iter.next()) |id_array| {
            id_array.value.deinit(allocator);
        }
    }

    pub fn get(self: *Images, id: Id) ?*Image {
        if (id < self.images.items.len) {
            return &self.images.items[id];
        }

        return null;
    }

    pub fn getId(self: *Images, category: Image.Category, code: []const u8) ?Id {
        const id_array = self.category_mapping.get(category);
        for (id_array.items) |id| {
            const image = self.images.items[id];
            const image_code = image.code.slice();
            if (std.mem.eql(u8, image_code, code)) {
                return id;
            }
        }

        return null;
    }

    pub fn getRGBA(self: *Images, id: Id) ?[]u8 {
        const image = self.get(id) orelse return null;
        const rgba_start = image.rgba_offset;
        const rgba_stop = image.rgba_offset + image.width * image.height * 4;

        return self.rgba_data[rgba_start..rgba_stop];
    }

    pub fn append(self: *Images, category: Image.Category, code: []const u8, width: u32, height: u32) !Id {
        if (self.images.unusedCapacitySlice().len == 0) {
            return error.OutOfMemory;
        }

        var image_ids = self.category_mapping.getPtr(category);
        if (image_ids.unusedCapacitySlice().len == 0) {
            return error.OutOfMemory;
        }

        const rgba_allocator = self.rgba_fba.allocator();
        const rgba = try rgba_allocator.alloc(u8, width * height * 4);
        errdefer rgba_allocator.free(rgba);

        const image_id = self.images.items.len;
        self.images.appendAssumeCapacity(Image{
            .rgba_offset = @intCast(@intFromPtr(rgba.ptr) - @intFromPtr(self.rgba_data.ptr)),
            .width = width,
            .height = height,
            .code = try Image.Code.fromSlice(code)
        });
        errdefer _ = self.images.pop();

        image_ids.appendAssumeCapacity(image_id);

        return image_id;
    }
};

fn Repository(comptime Object: type, comptime name_field: []const u8) type {
    const dummy: Object = undefined;
    const Name: type = @TypeOf(@field(dummy, name_field));

    return struct {
        pub const OptionalObject = union(enum) {
            reserved: Name,
            object: Object
        };

        const OptionalObjects = std.ArrayListUnmanaged(OptionalObject);
        objects: OptionalObjects,

        pub fn initCapacity(allocator: std.mem.Allocator, capacity: usize) !@This() {
            return @This(){
                .objects = try OptionalObjects.initCapacity(allocator, capacity)
            };
        }

        pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
            self.objects.deinit(allocator);
        }

        pub fn get(self: *@This(), id: Id) ?*Object {
            if (id < self.objects.items.len) {
                const item = &self.objects.items[id];
                if (item.* == .object) {
                    return &item.object;
                }
            }

            return null;
        }

        pub fn remove(self: *@This(), id: Id) bool {
            if (id < self.objects.items.len) {
                const item = &self.objects.items[id];
                if (item.* == .object) {
                    item.* = .{ .reserved = @field(item.object, name_field) };
                    return true;
                }
            }

            return false;
        }

        pub fn getId(self: *@This(), name: []const u8) ?Id {
            for (0.., self.objects.items) |id, object| {
                const object_name = switch (object) {
                    .object => |obj| @field(obj, name_field).slice(),
                    .reserved => |object_name| object_name.slice(),
                };

                if (std.mem.eql(u8, name, object_name)) {
                    return id;
                }
            }

            return null;
        }

        pub fn getName(self: *@This(), id: Id) ?[]const u8 {
            if (id < self.objects.items.len) {
                return switch (self.objects.items[id]) {
                    .object => |obj| @field(obj, name_field).slice(),
                    .reserve => |name| name.slice(),
                };
            }

            return null;
        }

        pub fn getOrReserveId(self: *@This(), name: []const u8) !Id {
            if (self.getId(name)) |id| {
                return id;
            }

            if (self.objects.unusedCapacitySlice().len == 0) {
                return error.OutOfMemory;
            }

            const owned_name = try Name.fromSlice(name);
            self.objects.appendAssumeCapacity(.{ .reserved = owned_name });

            return self.objects.items.len - 1;
        }

        pub fn appendOrUpdate(self: *@This(), item: Object) !Id {
            if (self.getId(@field(item, name_field).slice())) |id| {
                self.objects.items[id] = .{ .object = item };
                return id;
            } else {
                if (self.objects.unusedCapacitySlice().len == 0) {
                    return error.OutOfMemory;
                }

                self.objects.appendAssumeCapacity(.{ .object = item });

                return self.objects.items.len - 1;
            }
        }
    };
}

const Items = Repository(Item, "code");
const Characters = Repository(Character, "name");
const Tasks = Repository(Task, "code");
const Monsters = Repository(Monster, "code");
const Resources = Repository(Resource, "code");
const Maps = std.ArrayListUnmanaged(Map);

items: Items,
characters: Characters,
tasks: Tasks,
monsters: Monsters,
resources: Resources,
images: Images,
maps: Maps,

pub fn init(allocator: std.mem.Allocator) !Store {
    const max_items = 512;
    const max_characters = 10;
    const max_tasks = 32;
    const max_monsters = 64;
    const max_resources = 32;
    const max_maps = 512;

    var items = try Items.initCapacity(allocator, max_items);
    errdefer items.deinit(allocator);

    var characters = try Characters.initCapacity(allocator, max_characters);
    errdefer characters.deinit(allocator);

    var tasks = try Tasks.initCapacity(allocator, max_tasks);
    errdefer tasks.deinit(allocator);

    var monsters = try Monsters.initCapacity(allocator, max_monsters);
    errdefer monsters.deinit(allocator);

    var resources = try Resources.initCapacity(allocator, max_resources);
    errdefer resources.deinit(allocator);

    var images = try Images.initCapacity(allocator, .{
        .max_rgba_data = 1024 * 1024 * 32,
        .max_items = max_items,
        .max_effects = 16,
        .max_resources = max_resources,
        .max_maps = max_maps,
        .max_monsters = max_monsters,
    });
    errdefer images.deinit(allocator);

    var maps = try Maps.initCapacity(allocator, max_maps);
    errdefer maps.deinit(allocator);

    return Store{
        .items = items,
        .characters = characters,
        .tasks = tasks,
        .monsters = monsters,
        .resources = resources,
        .maps = maps,
        .images = images,
    };
}

pub fn deinit(self: *Store, allocator: std.mem.Allocator) void {
    self.items.deinit(allocator);
    self.characters.deinit(allocator);
    self.tasks.deinit(allocator);
    self.monsters.deinit(allocator);
    self.resources.deinit(allocator);
    self.maps.deinit(allocator);
    self.images.deinit(allocator);
}

const SaveData = struct {
    api_version: []const u8,

    items: []Items.OptionalObject,
    characters: []Characters.OptionalObject,
    tasks: []Tasks.OptionalObject,
    monsters: []Monsters.OptionalObject,
    resources: []Resources.OptionalObject,
    maps: []Map,

    images: struct {
        rgba_data: []u8,
        images: []Image,

        // These next fields are just `category_mapping`
        character_mapping: []Id,
        item_mapping: []Id,
        monster_mapping: []Id,
        map_mapping: []Id,
        resource_mapping: []Id,
        effect_mapping: []Id,
    }
};

// TODO: Create a better serialized that doesn't write zeros or BoundedArray types
pub fn save(self: *Store, api_version: []const u8, writer: anytype) !void {
    const rgba_end_index = self.images.rgba_fba.end_index;

    const data = SaveData{
        .api_version = api_version,
        .items = self.items.objects.items,
        .characters = self.characters.objects.items,
        .tasks = self.tasks.objects.items,
        .resources = self.resources.objects.items,
        .monsters = self.monsters.objects.items,
        .maps = self.maps.items,
        .images = .{
            .rgba_data = self.images.rgba_data[0..rgba_end_index],
            .images = self.images.images.items,

            .character_mapping = self.images.category_mapping.get(.character).items,
            .item_mapping = self.images.category_mapping.get(.item).items,
            .monster_mapping = self.images.category_mapping.get(.monster).items,
            .map_mapping = self.images.category_mapping.get(.map).items,
            .resource_mapping = self.images.category_mapping.get(.resource).items,
            .effect_mapping = self.images.category_mapping.get(.effect).items,
        }
    };

    try s2s.serialize(writer, SaveData, data);
}

pub fn load(self: *Store, allocator: std.mem.Allocator, api_version: []const u8, reader: anytype) !void {
    var data: SaveData = try s2s.deserializeAlloc(reader, SaveData, allocator);
    defer s2s.free(allocator, SaveData, &data);

    // TODO: Add better version checking.
    // * Hash the layout of `SaveData` and save that hash into the file. To see if layout changed.
    // * Hash the openapi documentation file to see if the saved data is out-of-date and should be ignored.
    if (!std.mem.eql(u8, data.api_version, api_version)) {
        return error.InvalidVersion;
    }

    const repositories = .{
        .{ &self.items, data.items },
        .{ &self.characters, data.characters },
        .{ &self.tasks, data.tasks },
        .{ &self.resources, data.resources },
        .{ &self.monsters, data.monsters },
    };

    const image_category_mapping = &self.images.category_mapping;
    const image_categories = .{
        .{ image_category_mapping.getPtr(.character), data.images.character_mapping },
        .{ image_category_mapping.getPtr(.item), data.images.item_mapping },
        .{ image_category_mapping.getPtr(.monster), data.images.monster_mapping },
        .{ image_category_mapping.getPtr(.map), data.images.map_mapping },
        .{ image_category_mapping.getPtr(.resource), data.images.resource_mapping },
        .{ image_category_mapping.getPtr(.effect), data.images.effect_mapping },
    };

    // Check if there is enough space
    {
        inline for (repositories) |pair| {
            const repository = pair[0];
            const saved_objects = pair[1];
            if (saved_objects.len > repository.objects.capacity) {
                return error.OutOfMemory;
            }
        }

        if (data.maps.len > self.maps.capacity) {
            return error.OutOfMemory;
        }

        if (data.images.rgba_data.len > self.images.rgba_data.len) {
            return error.OutOfMemory;
        }

        if (data.images.images.len > self.images.images.capacity) {
            return error.OutOfMemory;
        }

        inline for (image_categories) |pair| {
            const self_ids = pair[0];
            const saved_ids = pair[1];
            if (saved_ids.len > self_ids.capacity) {
                return error.OutOfMemory;
            }
        }
    }

    // Move loaded data from file to this store
    {
        inline for (repositories) |pair| {
            const repository = pair[0];
            const saved_objects = pair[1];

            repository.objects.clearRetainingCapacity();
            repository.objects.appendSliceAssumeCapacity(saved_objects);
        }

        self.maps.clearRetainingCapacity();
        self.maps.appendSliceAssumeCapacity(data.maps);

        @memcpy(self.images.rgba_data[0..data.images.rgba_data.len], data.images.rgba_data);
        self.images.rgba_fba.end_index = data.images.rgba_data.len;

        self.images.images.clearRetainingCapacity();
        self.images.images.appendSliceAssumeCapacity(data.images.images);

        inline for (image_categories) |pair| {
            const self_ids = pair[0];
            const saved_ids = pair[1];

            self_ids.clearRetainingCapacity();
            self_ids.appendSliceAssumeCapacity(saved_ids);
        }
    }
}

pub fn appendOrUpdateMap(self: *Store, map: Map) !Position {
    if (self.getMap(map.position)) |existing_map| {
        existing_map.* = map;
    } else {
        if (self.maps.unusedCapacitySlice().len == 0) {
            return error.OutOfMemory;
        }

        self.maps.appendAssumeCapacity(map);
    }

    return map.position;
}

pub fn getMap(self: *Store, position: Position) ?*Map {
    for (self.maps.items) |*map| {
        if (map.position.eql(position)) {
            return map;
        }
    }

    return null;
}
