ecs = {}

local allEntities <const> = {}

---@alias System { callback: fun(delta: number, entity: any, a: any, b: any, c: any, d: any, e: any, any), shapes: {}, keys: string[], entityCache: nil | table<any, boolean> }

---@type System[]
local systems <const> = {}

-- TODO: Add entity to any existing systems
function ecs.addEntity(entity)
    allEntities[entity] = true
    for _, system in pairs(systems) do
        if entityMatchesShapes(entity, system.shapes) then
            system.entityCache[entity] = true
        else
            system.entityCache[entity] = nil
        end
    end
end

function ecs.removeEntity(entity)
    allEntities[entity] = nil
    for _, system in pairs(systems) do
        system.entityCache[entity] = nil
    end
end

local Placeholder = {}
---@generic T
---@return T
function ecs.field()
    return Placeholder
end

function allKeysIncluded(entity, filter)
    for k, _ in pairs(filter) do
        if not entity[k] then
            return false
        end
    end
    return true
end

function entityMatchesShapes(entity, shapes)
    for _, shape in pairs(shapes) do
        if not allKeysIncluded(entity, shape) then
            return false
        end
    end
    return true
end

---@generic T
---@generic U
---@generic V
---@generic W
---@param tShape T
---@param uShape U?
---@param vShape V?
---@param wShape W?
---@return fun(callback: fun(componentT: T, componentU: U, componentV: V, componentW: W))
function ecs.entitiesHavingShapes(tShape, uShape, vShape, wShape)
    return function() end
end

-- Print contents of `tbl`, with indentation.
-- `indent` sets the initial level of indentation.
function tprint(tbl, indent)
    if not indent then
        indent = 0
    end
    for k, v in pairs(tbl) do
        formatting = string.rep("  ", indent) .. k .. ": "
        if type(v) == "table" then
            print(formatting)
            tprint(v, indent + 1)
        elseif type(v) == "boolean" then
            print(formatting .. tostring(v))
        else
            print(formatting .. v)
        end
    end
end

function addSystem(callback, keys, shapes)
    systems[#systems + 1] = {
        callback = callback,
        keys = keys,
        shapes = shapes,
        entityCache = nil,
    }
end

---@return boolean
function is(entity, shape)
    return allKeysIncluded(entity, shape)
end

---@param deltaSeconds number
function ecs.update(deltaSeconds)
    for _, system in pairs(systems) do
        if not system.entityCache then
            system.entityCache = {}
            for entity, _ in pairs(allEntities) do
                if entityMatchesShapes(entity, system.shapes) then
                    system.entityCache[entity] = true
                end
            end
        end
        local keys = system.keys
        for entity, _ in pairs(system.entityCache) do
            system.callback(
                deltaSeconds,
                entity,
                entity[keys[1]],
                entity[keys[2]],
                entity[keys[3]],
                entity[keys[4]],
                entity
            )
        end
    end
end

--- Returns a function that accepts a callback. This callback will receive one argument for each Shape provided.
---
---@generic T
---@generic TKey
---@generic U
---@generic UKey
---@generic V
---@generic VKey
---@generic W
---@generic WKey
---@param tShape { [TKey]: T }
---@param uShape { [UKey]: U } | fun(entity: any, componentT: T, any) | nil
---@param vShape { [VKey]: V } | fun(entity: any, componentT: T, componentU: U, any) | nil
---@param wShape { [WKey]: W } | fun(entity: any, componentT: T, componentU: U, componentV: V, any) | nil
---@param finalFunc fun(entity: any, componentT: T, componentU: U, componentV: V, componentW: W, any) | nil
function ecs.forEntitiesWith(tShape, uShape, vShape, wShape, finalFunc)
    local maybeShapes = { tShape, uShape, vShape, wShape, finalFunc }
    local shapes = {}
    local callback

    for _, maybeShape in pairs(maybeShapes) do
        if type(maybeShape) == "table" then
            shapes[#shapes + 1] = maybeShape
        elseif type(maybeShape) == "function" then
            callback = maybeShape
        end
    end

    local keys = {}
    for _, shape in pairs(shapes) do
        for key, _ in pairs(shape) do
            keys[#keys + 1] = key
        end
    end

    addSystem(callback, keys, shapes)
end

local f = ecs.field()
local XYPair = { x = f, y = f }
local Position = { position = XYPair }
local Target = { target = XYPair }
local Velocity = { velocity = XYPair }

function ecs.overlayOnto(entity, value)
    for key, v in pairs(value) do
        entity[key] = v
    end
    ecs.addEntity(entity)
end

local data = {
    position = { x = 0, y = 0 },
    velocity = { x = 1, y = 2 },
}

---@generic T
---@param shape T
---@param entity unknown
---@return T
function ecs.get(shape, entity)
    return entity
end

---@generic T
---@param entity unknown
---@param shape `T`
---@param value `T`
function ecs.set(entity, shape, value)
    for key, v in pairs(shape) do
        entity[key] = value[v]
    end
end

ecs.addEntity(data)

ecs.forEntitiesWith(Position, Velocity, function(delta, e, pos, vel)
    pos.x = pos.x + (delta * vel.x)
    pos.y = pos.y + (delta * vel.y)
    print("position")
    tprint(pos, 1)
    ecs.set(Target, e, {
        --target = { x = 10, y = 10}
    })
end)

ecs.forEntitiesWith(Target, function(delta, e, pos, vel)
    pos.x = pos.x + (delta * vel.x)
    pos.y = pos.y + (delta * vel.y)
    print("position")
    tprint(pos, 1)
    ecs.set(e, Target, "hallo")
end)

ecs.update(1)
