//! This module provides an implementation of an Entity Component System
const std = @import("std");
const component = @import("component.zig");
const ComponentType = component.ComponentType;

/// EntityComponentSystem keeps track of components in a MultiArrayList of Components.
/// Each Entity is simply an index into that MultiArrayList of Components.
/// To save on memory and avoid resizing, ids are recycled upon Entities being deleted.
pub const EntityComponentSystem = struct {
    /// An allocator for use by the EntityComponentSystem to manage memory
    allocator: std.mem.Allocator,

    /// The mapping of Components to Entities is in this MultiArrayList
    components: std.MultiArrayList(component.Components),

    /// Keeps track of ids available for reuse
    available_ids: std.ArrayList(usize),

    fn nullEntity(self: *EntityComponentSystem, entity: usize) void {
        inline for (std.enums.valuesFromFields(ComponentType, std.meta.fields(ComponentType))) |comp| {
            self.componentRemoveEntity(comp, entity);
        }
    }

    pub fn init(allocator: std.mem.Allocator) EntityComponentSystem {
        return EntityComponentSystem{
            .allocator = allocator,
            .components = .{},
            .available_ids = std.ArrayList(usize).init(allocator),
        };
    }

    pub fn deinit(self: *EntityComponentSystem) void {
        self.available_ids.deinit();
        self.components.deinit(self.allocator);
    }

    /// Create a new entity (or recycle an old one) and return the id.
    /// May error if the Allocator runs out of memory.
    pub fn createEntity(self: *EntityComponentSystem) !usize {
        const id = self.available_ids.popOrNull() orelse noroom: {
            const id = self.components.len;
            try self.components.append(self.allocator, undefined);
            break :noroom id;
        };
        self.nullEntity(id);
        return id;
    }

    /// Add an entity to a component.
    /// Takes a ComponentType and an entity id along with any arguments needed to initialize the component.
    pub fn componentAddEntity(
        self: *EntityComponentSystem,
        comp: component.ComponentType,
        entity: usize,
        args: anytype,
    ) void {
        switch (comp) {
            ComponentType.component_stub => self.components.items(.component_stub)[entity] = component.ComponentStub.init(args),
        }
    }

    /// Delete an entity and make it ready for recycling by nulling all components.
    /// Takes an entity id.
    /// May error if the Allocator runs out of memory.
    pub fn deleteEntity(self: *EntityComponentSystem, entity: usize) !void {
        self.nullEntity(entity);
        try self.available_ids.append(entity);
    }

    fn componentRemoveEntityGeneric(comp: anytype, entity: usize) void {
        var fixed_comp = comp[entity] orelse return;
        fixed_comp.deinit();
        comp[entity] = null;
    }

    /// Removes an entity from a component.
    /// Takes a ComponentType and an entity id.
    pub fn componentRemoveEntity(
        self: *EntityComponentSystem,
        comp: component.ComponentType,
        entity: usize,
    ) void {
        var components = self.components;
        switch (comp) {
            ComponentType.component_stub => componentRemoveEntityGeneric(components.items(.component_stub), entity),
        }
    }
};

test "add entities to components" {
    var ecs = EntityComponentSystem.init(std.testing.allocator);
    defer ecs.deinit();

    for (0..100) |i| {
        const entity = try ecs.createEntity();
        try std.testing.expectEqual(i, entity);
        ecs.componentAddEntity(ComponentType.component_stub, entity, .{});
        try std.testing.expect(ecs.components.items(.component_stub)[entity] != null);
    }
}

test "remove entities from components" {
    var ecs = EntityComponentSystem.init(std.testing.allocator);
    defer ecs.deinit();

    const entity = try ecs.createEntity();
    ecs.componentAddEntity(ComponentType.component_stub, entity, .{});
    const entity2 = try ecs.createEntity();
    ecs.componentAddEntity(ComponentType.component_stub, entity2, .{});

    try std.testing.expect(ecs.components.items(.component_stub)[entity2] != null);
    ecs.componentRemoveEntity(ComponentType.component_stub, entity2);
    try std.testing.expect(ecs.components.items(.component_stub)[entity2] == null);
}

test "delete entities" {
    var ecs = EntityComponentSystem.init(std.testing.allocator);
    defer ecs.deinit();

    const entity = try ecs.createEntity();
    ecs.componentAddEntity(ComponentType.component_stub, entity, .{});
    const entity2 = try ecs.createEntity();
    ecs.componentAddEntity(ComponentType.component_stub, entity2, .{});

    try std.testing.expect(ecs.components.items(.component_stub)[entity2] != null);
    try ecs.deleteEntity(entity2);
    try std.testing.expect(ecs.components.items(.component_stub)[entity2] == null);
    try std.testing.expectEqual(1, ecs.available_ids.items.len);
}

test "recycle entities" {
    var ecs = EntityComponentSystem.init(std.testing.allocator);
    defer ecs.deinit();

    const entity = try ecs.createEntity();
    ecs.componentAddEntity(ComponentType.component_stub, entity, .{});
    const entity2 = try ecs.createEntity();
    ecs.componentAddEntity(ComponentType.component_stub, entity2, .{});

    try ecs.deleteEntity(entity2);
    try std.testing.expectEqual(1, ecs.available_ids.items.len);

    const entity3 = try ecs.createEntity();
    try std.testing.expectEqual(1, entity3);
    try std.testing.expectEqual(0, ecs.available_ids.items.len);

    const entity4 = try ecs.createEntity();
    try std.testing.expectEqual(2, entity4);

    try ecs.deleteEntity(entity);
    try std.testing.expectEqual(1, ecs.available_ids.items.len);

    const entity5 = try ecs.createEntity();
    try std.testing.expectEqual(0, entity5);
    try std.testing.expectEqual(0, ecs.available_ids.items.len);

    try ecs.deleteEntity(entity5);
    try ecs.deleteEntity(entity4);
    try ecs.deleteEntity(entity3);
    try std.testing.expectEqual(3, ecs.available_ids.items.len);
}