--------------------------------------------------------------------------------
-- ecr.luau
-- v0.9.0
--------------------------------------------------------------------------------

local ID_SIZE = 4
local MAX_ENTITIES = 0x0000_FFFF

--------------------------------------------------------------------------------
-- types
--------------------------------------------------------------------------------

export type entity = number

type Array<T> = { [number]: T }
type Map<T, U> = { [T]: U }
type Listener<T> = (id: entity, value: T) -> ()
type CType = unknown

export type Signal<T...> = {
    connect: (self: Signal<T...>, listener: (T...) -> ()) -> Connection,
}

export type Connection = {
    disconnect: (self: Connection) -> (),
    reconnect: (self: Connection) -> ()
}

type Pool<T> = {
    -- sparse
    map_max: number, -- largest key that can fit
    map: buffer, -- maps keys to internal indexes

    -- dense
    capacity: number, -- allocated amount
    size: number, -- entities in pool
    entities: buffer, -- all entities (0-indexed)
    values: Array<T>, -- all values (1-indexed)

    on_add: Array<Listener<T>> | false,
    on_change: Array<Listener<T>> | false,
    on_remove: Array<Listener<nil>> | false,
    on_clear: Array<() -> ()> | false,
    after_clear: Array<() -> ()> | false,

    group: GroupData<T> | false,

    reserve: (self: Pool<T>, size: number) -> ()
}

type EntityList = {
    data: buffer,
    capacity: number,
    free: number,
}

type GroupData<T = unknown> = {
    size: number, -- entities in group
    added: boolean, -- flag to detect iter invalidation
    connections: Array<Connection>, -- listeners to add and remove from group
    [number]: Pool<T> -- all pools in group
}

export type Handle = typeof(setmetatable(
    {} :: { registry: Registry, entity: entity },
    {} :: HandleMT
))

type HandleMT = {
    __index: HandleMT,
    destroy: (self: Handle) -> (),
    has_none: (self: Handle) -> boolean,
    add: <T...>(self: Handle, T...) -> (),
    set: <T>(self: Handle, ctype: T, value: T) -> Handle,
    insert: <T>(self: Handle, ctype: Array<T>, value: T) -> Handle,
    patch: <T>(self: Handle, ctype: T, fn: ((T) -> T)?) -> T,
    has: <T...>(self: Handle, T...) -> boolean,
    get: <T...>(self: Handle, T...) -> T...,
    try_get: <T>(self: Handle, T) -> T?,
    remove: <T...>(self: Handle, T...) -> (),
}

export type View<T...> = typeof(setmetatable({} :: {
    exclude: <U...>(self: View<T...>, U...) -> View<T...>,
    of: (self: View<T...>, list: Array<entity>) -> View<T...>,
    patch: (self: View<T...>, fn: (T...) -> T...) -> (),
    iter: (self: View<T...>) -> () -> (entity, T...)
}, {} :: {
    __len: (self: View<T...>) -> number ,
    __iter: (self: View<T...>) -> () -> (entity, T...)
}))

export type Observer<T...> = typeof(setmetatable({} :: {
    exclude: <U...>(self: Observer<T...>, U...) -> Observer<T...>,
    disconnect: (self: Observer<T...>) -> Observer<T...>,
    reconnect: (self: Observer<T...>) -> Observer<T...>,
    clear: (self: Observer<T...>) -> Observer<T...>,
    iter: (self: Observer<T...>) -> () -> (entity, T...)
}, {} :: {
    __len: (self: Observer<T...>) -> number,
    __iter: (self: Observer<T...>) -> () -> (entity, T...)
}))

export type Group<T...> = typeof(setmetatable({} :: {
    iter: (self: Group<T...>) -> () -> (entity, T...)
}, {} :: {
    __len: (self: Group<T...>) -> number,
    __iter: (self: Group<T...>) -> () -> (entity, T...)
}))

export type Registry = {
    create: ((self: Registry, id: entity) -> entity) & ((self: Registry) -> entity),
    release: (self: Registry, id: entity) -> (),
    destroy: (self: Registry, id: entity) -> (),
    contains: (self: Registry, id: entity) -> boolean,

    has_none: (self: Registry, id: entity) -> boolean,
    add: <T...>(self: Registry, id: entity, T...) -> (),
    set: <T>(self: Registry, id: entity, ctype: T, value: T) -> (),
    insert: <T>(self: Registry, id: entity, ctype: Array<T>, value: T) -> (),
    patch: <T>(self: Registry, id: entity, ctype: T, fn: ((T) -> T)?) -> T,
    has: <T...>(self: Registry, id: entity, T...) -> boolean,
    get: <T...>(self: Registry, id: entity, T...) -> T...,
    try_get: <T>(self: Registry, id: entity, T) -> T?,
    remove: <T...>(self: Registry, id: entity, T...) -> (),
    find: <T>(self: Registry, ctype: T, value: T) -> entity?,
    copy: <T>(self: Registry, a: T, b: T) -> (),

    view: <T...>(self: Registry, T...) -> View<T...>,
    track: <T...>(self: Registry, T...) -> Observer<T...>,
    group: <T...>(self: Registry, T...) -> Group<T...>,

    clear: <T...>(self: Registry, T...) -> (),
    storage: (<T>(self: Registry, ctype: T) -> Pool<T>) & (<T>(self: Registry) -> () -> (unknown, Pool<unknown>)),

    on_add: <T>(self: Registry, ctype: T) -> Signal<entity, T>,
    on_change: <T>(self: Registry, ctype: T) -> Signal<entity, T>,
    on_remove: <T>(self: Registry, ctype: T) -> Signal<entity, nil>,
    on_clear: <T>(self: Registry, ctype: T) -> Signal<>,
    after_clear: <T>(self: Registry, ctype: T) -> Signal<>,

    handle: ((self: Registry, id: entity) -> Handle) & ((self: Registry) -> Handle),
    context: (self: Registry) -> Handle
}

export type Queue<T...> = typeof(setmetatable({} :: {
    add: (self: Queue<T...>, T...) -> (),
    clear: (self: Queue<T...>) -> (),
    iter: (self: Queue<T...>) -> () -> (T...)
}, {} :: {
    __len: (self: Queue<T...>) -> number,
    __iter: (self: Queue<T...>) -> () -> (T...)
}))

local NIL = nil :: any

-- error but stack trace always starts at first callsite outside of this file
local function throw(msg: string)
    local s = 1
    repeat s += 1 until debug.info(s, "s") ~= debug.info(1, "s")
    error(msg, s)
end

local ASSERT = function<T>(v: T, msg: string): T
    if v then return v end
    return throw(msg)
end :: typeof(assert)

--------------------------------------------------------------------------------
-- entity id
--------------------------------------------------------------------------------

local RESERVED_BITS = 0

local ID_SIZE_BITS = ID_SIZE * 8
local ID_MAX = 2^(ID_SIZE_BITS - RESERVED_BITS) - 1

local ID_MASK_RES = 0xFFFF_FFFF - ID_MAX
local ID_MASK_KEY = MAX_ENTITIES
local ID_MASK_VER = ID_MAX - ID_MASK_KEY
local ID_MASK_VER_RES = ID_MASK_VER + ID_MASK_RES

local ID_LSHIFT = ID_MASK_KEY + 1
local ID_RSHIFT = 1/ID_LSHIFT

local ID_NULL_KEY = ID_MASK_KEY
local ID_CTX_KEY = 0

local ID_NULL_VER = 0
local ID_MIN_VER = 1 * ID_LSHIFT

local ID_MAX_VALID_KEY = ID_MASK_KEY - 1

assert(ID_SIZE <= 4 and ID_SIZE >= 1)
assert(MAX_ENTITIES <= ID_MAX/2 and MAX_ENTITIES > 0)
assert(bit32.band(MAX_ENTITIES + 1, MAX_ENTITIES) == 0)

local function ID_CREATE(key: number, ver: number): number
    return ver + key
end

local function ID_KEY(id: number): number
    return bit32.band(id, ID_MASK_KEY)
end

local function ID_VER(id: number): number
    return bit32.band(id, ID_MASK_VER)
end

local function ID_REPLACE(id: number, new_key: number): number
    return ID_VER(id) + new_key
end

local function ID_VERSION_NOT_EQUAL(id1: number, id2: number)
    return ID_VER(id1) ~= ID_VER(id2)
end

local function ID_ASSERT_VERSION_EQUAL(id1: number, id2: number)
    if ID_VERSION_NOT_EQUAL(id1, id2) then
        throw("invalid entity")
    end
end

local ID_NULL = ID_CREATE(ID_NULL_KEY, ID_NULL_VER)
local ID_CTX = ID_CREATE(ID_CTX_KEY, ID_MIN_VER)

--------------------------------------------------------------------------------
-- component type
--------------------------------------------------------------------------------

-- a ctor set to `true` indicates component is a tag
local ctype_ctors: Map<CType, (boolean | () -> unknown)> = {}
local ctype_names: Map<CType, string?> = {}
local ctype_n = 0

local function ctype_create<T>(ctor: boolean | () -> unknown): CType
    ctype_n += 1
    ctype_ctors[ctype_n] = ctor
    return ctype_n
end

local function ctype_is_tag(ctype: CType): boolean
    return ctype_ctors[ctype] == true
end

local function ctype_set_name(ctype: CType, name: unknown)
    ctype_names[ctype] = tostring(name)
end

--[[NO INLINE]] local ctype_debug: (ctype: CType?, idx: number?) -> string = (function()
    return function(ctype: CType?, idx: number?)
        local name = ctype_names[ctype]
        return
            if name then `component "{name}"`
            elseif idx then `component (arg #{idx})`
            else "component (unknown)"
    end
end)()

local function CTYPE_VALID(v: unknown): boolean
    return type(v) == "number" and math.floor(v) == v and v > 0 and v <= ctype_n
end

local function ASSERT_CTYPE_VALID(ctype: unknown, idx: number?)
    if not CTYPE_VALID(ctype) then
        throw(`invalid component {idx and `arg #{idx}` or ""}`)
    end
end

local ctype_entity = ctype_create(false)
ctype_set_name(ctype_entity, "entity")

--------------------------------------------------------------------------------
-- pool
--------------------------------------------------------------------------------

local function next_pow_of_2(x: number)
    assert(x > 0 and x <= 2^32)
    x -= 1
    x = bit32.bor(x, bit32.rshift(x, 1))
    x = bit32.bor(x, bit32.rshift(x, 2))
    x = bit32.bor(x, bit32.rshift(x, 4))
    x = bit32.bor(x, bit32.rshift(x, 8))
    x = bit32.bor(x, bit32.rshift(x, 16))
    return x + 1
end

local function fire<T>(listeners: Array<Listener<T>>, id: entity, value: T)
    for i = #listeners, 1, -1 do
        listeners[i](id, value)
    end
end

local function BUFFER_CREATE(size: number): buffer
    return buffer.create(size * ID_SIZE)
end

local function BUFFER_GET(b: buffer, i: number): number
    return buffer.readu32(b, i * ID_SIZE)
end

local function BUFFER_SET(b: buffer, i: number, id: entity)
    buffer.writeu32(b, i * ID_SIZE, id)
end

local function BUFFER_FILL(b: buffer, first: number, last: number, v: number)
    assert(buffer.len(b) >= last * ID_SIZE)
    for i = first * ID_SIZE, last * ID_SIZE, ID_SIZE do
        buffer.writeu32(b, i, v)
    end
end

local function BUFFER_RESIZE(b: buffer, size: number): buffer
    local b_new = buffer.create(size * ID_SIZE)
    local n_old = buffer.len(b)

    buffer.copy(b_new, 0, b)

    for i = n_old, size * ID_SIZE - 1, ID_SIZE do
        buffer.writeu32(b_new, i, ID_NULL)
    end

    return b_new
end

-- local function BUFFER_CLONE(b: buffer): buffer
--     local b_new = buffer.create(buffer.len(b))
--     buffer.copy(b_new, 0, b)
--     return b_new
-- end

-- wrappers to treat arrays as 0-indexed
local function ARRAY_SET<T>(t: Array<T>, i: number, v: T)
    t[i + 1] = v
end

local function ARRAY_GET<T>(t: Array<T>, i: number): T
    return t[i + 1]
end

local function ARRAY_SWAP<T>(t: Array<T>, a: number, b: number)
    a += 1
    b += 1
    t[a], t[b] = t[b], t[a]
end

local function ARRAY_SWAP_REMOVE<T>(t: Array<T>, remove: number, swap: number)
    swap += 1
    t[remove + 1] = t[swap]
    t[swap] = nil
end

local function POOL_HAS<T>(pool: Pool<T>, key: number): boolean
    return pool.map_max >= key and BUFFER_GET(pool.map, key) ~= ID_NULL
end

local function POOL_NOT_HAS<T>(pool: Pool<T>, key: number): boolean
    return pool.map_max < key or BUFFER_GET(pool.map, key) == ID_NULL
end

local function POOL_FIND(pool: Pool<any>, key: number): number
    if key > pool.map_max then return ID_NULL end
    return BUFFER_GET(pool.map, key)
end

local function pool_resize_entities<T>(self: Pool<T>, size: number)
    if self.capacity >= size then return end
    local new_capacity = math.ceil(size * 1.5)
    self.entities = BUFFER_RESIZE(self.entities, new_capacity)
    self.capacity = new_capacity
end

local function pool_resize_map<T>(self: Pool<T>, size: number)
    if self.map_max + 1 >= size then return end
    local new_map_capacity = math.ceil(size * 1.5)
    self.map = BUFFER_RESIZE(self.map, new_map_capacity)
    self.map_max = new_map_capacity - 1
end

local function POOL_RESIZE_MAP_IF_NEEDED<T>(self: Pool<T>, key: number)
    if self.map_max < key then
        pool_resize_map(self, key + 1)
    end
end

local function POOL_SWAP<T>(
    self: Pool<T>,
    idx: number,
    idx_swap: number
)
    local map = self.map
    local entities = self.entities
    local values = self.values

    local id = BUFFER_GET(entities, idx)
    local id_swap = BUFFER_GET(entities, idx_swap)

    BUFFER_SET(entities, idx_swap, id)
    BUFFER_SET(entities, idx, id_swap)

    BUFFER_SET(map, ID_KEY(id), ID_REPLACE(id, idx_swap))
    BUFFER_SET(map, ID_KEY(id_swap), ID_REPLACE(id_swap, idx))

    ARRAY_SWAP(values, idx_swap, idx)
end

local function GROUP_ADD<T>(group: GroupData<T>, key: number)
    group.added = true -- used to test for invalidation
    local n = group.size
    group.size = n + 1
    for _, pool in ipairs(group) do
        POOL_SWAP(pool, ID_KEY(BUFFER_GET(pool.map, key)), n)
    end
end

local function GROUP_CAN_ADD<T>(group: GroupData<T>, key: number): boolean
    for _, pool in ipairs(group) do
        if POOL_NOT_HAS(pool, key) then
            return false
        end
    end
    return true
end

local function GROUP_REMOVE<T>(group: GroupData<T>, idx: number)
    local n = group.size - 1
    group.size = n
    for _, pool in ipairs(group) do
        POOL_SWAP(pool, idx, n)
    end
end

local function POOL_TRY_GROUP<T>(pool: Pool<T>, id: number)
    local group = pool.group :: GroupData<T>
    local key = ID_KEY(id)
    if GROUP_CAN_ADD(group, key) then
        GROUP_ADD(group, key)
    end
end

local function POOL_TRY_UNGROUP<T>(pool: Pool<T>, id: entity)
    local group = pool.group :: GroupData<T>
    local idx = ID_KEY(BUFFER_GET(pool.map, ID_KEY(id)))
    if idx < group.size then
        GROUP_REMOVE(group, idx)
    end
end

local function POOL_RESIZE_ENTITIES_IF_NEEDED<T>(self: Pool<T>, size: number)
    if size >= self.capacity then
        pool_resize_entities(self, size + 1)
    end
end

-- separated to encourage compiler to inline
local function _POOL_ADD(self, i, id, v)
    BUFFER_SET(self.map, ID_KEY(id), ID_REPLACE(id, i))
    BUFFER_SET(self.entities, i, id)
    ARRAY_SET(self.values, i, v)
end
local function POOL_ADD<T>(self: Pool<T>, id: entity, v: T)
    local n = self.size
    POOL_RESIZE_ENTITIES_IF_NEEDED(self, n)
    self.size = n + 1
    _POOL_ADD(self, n, id, v)
    if self.on_add then fire(self.on_add, id, v) end
end

local function _POOL_ADD_ID(self, i, id)
    BUFFER_SET(self.map, ID_KEY(id), ID_REPLACE(id, i))
    BUFFER_SET(self.entities, i, id)
end
local function POOL_ADD_ID<T>(self: Pool<T>, id: entity)
    local n = self.size
    POOL_RESIZE_ENTITIES_IF_NEEDED(self, n)
    self.size = n + 1
    _POOL_ADD_ID(self, n, id)
    if self.on_add then fire(self.on_add, id, NIL) end
end

local function POOL_REMOVE<T>(self: Pool<T>, key, id)
    local map = self.map

    if self.on_remove then fire(self.on_remove, id) end

    local idx_ver = BUFFER_GET(map, key)
    local idx = ID_KEY(idx_ver)

    if ID_VERSION_NOT_EQUAL(idx_ver, id) then return end

    local n = self.size - 1
    self.size = n
    local entities = self.entities
    local values = self.values

    local id_last = BUFFER_GET(entities, n)

    BUFFER_SET(map, ID_KEY(id_last), ID_REPLACE(id_last, idx))
    BUFFER_SET(map, key, ID_NULL)

    BUFFER_SET(entities, idx, id_last)
    --BUFFER_SET(entities, n, ID_NULL)

    ARRAY_SWAP_REMOVE(values, idx, n)
end
local function POOL_TRY_REMOVE<T>(self: Pool<T>, id: entity)
    local key = ID_KEY(id)
    if POOL_HAS(self, key) then
        POOL_REMOVE(self, key, id)
    end
end

local function POOL_COPY<T>(self: Pool<T>, into: Pool<T>)
    if into.on_add or into.on_change or into.on_remove or into.on_clear then throw("cannot paste into component with signals") end

    POOL_RESIZE_MAP_IF_NEEDED(into, self.map_max)
    POOL_RESIZE_ENTITIES_IF_NEEDED(into, self.capacity)

    into.size = self.size

    buffer.copy(into.map, 0, self.map, 0)
    buffer.copy(into.entities, 0, self.entities, 0)
    table.move(self.values, 1, math.max(self.size, into.size), 1, into.values)
end

local function pool_clear<T>(self: Pool<T>)
    local entities = self.entities

    local on_clear = self.on_clear :: Array<() -> ()>
    if on_clear then
        for _, listener in on_clear do listener() end
    end

    local on_remove = self.on_remove :: Array<Listener<nil>>
    if on_remove then
        for i = 0, self.size - 1 do
            fire(on_remove, BUFFER_GET(entities, i))
        end
    end

    local after_clear = self.after_clear :: Array<() -> ()>
    if after_clear then
        for _, listener in after_clear do listener() end
    end

    self.size = 0
    BUFFER_FILL(self.map, 0, self.map_max, ID_NULL)
    table.clear(self.values)
end

local function pool_create<T>(size: number?): Pool<T>
    local n = size or 1

    local map = BUFFER_CREATE(n)
    BUFFER_FILL(map, 0, n - 1, ID_NULL)

    return {
        map_max = n - 1,
        capacity = n,
        size = 0,
        map = map,
        entities = BUFFER_CREATE(n),
        values = table.create(n),

        on_add = false,
        on_change = false,
        on_remove = false,
        on_clear = false,
        after_clear = false,
        group = false,

        reserve = function(self: Pool<T>, size: number)
            pool_resize_map(self, size)
            pool_resize_entities(self, size)

            -- todo: why does this cause innacurate buffer alloc readings?
            do -- force array reallocation
                local values = self.values
                local n = next_pow_of_2(size)

                for i = self.size + 1, n do
                    values[i] = true :: any
                end

                for i = n, self.size + 1, -1 do
                    values[i] = nil
                end
            end
        end
    }
end

--------------------------------------------------------------------------------
-- entity_list
--------------------------------------------------------------------------------

local function entity_list_create(): EntityList
    return {
        data = buffer.create(0),
        capacity = 0,
        free = ID_NULL_KEY
    }
end

local function entity_list_get_prev_key(self: EntityList, key: number): number
    local prev = self.free
    local i = 0; repeat i += 1
        local next = ID_KEY(BUFFER_GET(self.data, prev))
        if next == key then break end
        prev = next
    until i == ID_MAX_VALID_KEY
    assert(i < ID_MAX_VALID_KEY, "key not found") -- todo: is this a valid case?
    return prev
end

local function entity_list_resize(self: EntityList, new_capacity: number, partition_start: number, partition_stop: number)
    ASSERT(new_capacity > self.capacity, "new capacity must be greater than current capacity")
    local new_max = new_capacity - 1
    ASSERT(new_max <= ID_MAX_VALID_KEY)

    local old_capacity = self.capacity
    local old_max = old_capacity - 1
    local old_data = self.data

    local new_data = BUFFER_RESIZE(old_data, new_capacity)

    if old_max < partition_stop and new_max >= partition_start then
        local start = old_max < partition_start and partition_start or old_max + 1 -- todo: verify
        local stop = new_max > partition_stop and partition_stop or new_max

        if self.free == ID_NULL_KEY then
            self.free = start
        else
            local tail = entity_list_get_prev_key(self, ID_NULL_KEY)
            BUFFER_SET(new_data, tail, ID_CREATE(start, ID_VER(BUFFER_GET(new_data, tail))))
        end

        for i = start, stop - 1 do
            BUFFER_SET(new_data, i, ID_CREATE(i + 1, ID_MIN_VER))
        end
        BUFFER_SET(new_data, stop, ID_CREATE(ID_NULL_KEY, ID_MIN_VER))
    end

    self.data = new_data
    self.capacity = new_capacity
end

local function entity_list_remove(self: EntityList, id: entity, partition_start: number, partition_stop: number)
    local key = ID_KEY(id)
    local ver = ID_VER(id)

    if key >= partition_start and key <= partition_stop then
        BUFFER_SET(self.data, key, ID_CREATE(self.free, ver < ID_MASK_VER and ver + 1 * ID_LSHIFT or ID_MIN_VER))
        self.free = key
    else
        BUFFER_SET(self.data, key, ID_NULL)
    end
end

local function entity_list_clear(self: EntityList, partition_start: number, partition_stop: number)
    for i = self.capacity - 1, 0, -1 do
        local key_ver = BUFFER_GET(self.data, i)
        if ID_KEY(key_ver) == i then -- is in use
            entity_list_remove(self, key_ver, partition_start, partition_stop)
        end
    end
end

local function ENTITY_LIST_ID_IN_USE(self: EntityList, id: entity): boolean
    local key = ID_KEY(id)
    return key < self.capacity and BUFFER_GET(self.data, ID_KEY(id)) == id
end

--------------------------------------------------------------------------------
-- view
--------------------------------------------------------------------------------

local view_create, observer_create, group_create do

type IRegistry = {
    storage: <T>(self: IRegistry, ctype: T) -> Pool<T>,
    on_add: <T>(self: IRegistry, ctype: T) -> Signal<entity, T>,
    on_change: <T>(self: IRegistry, ctype: T) -> Signal<entity, T>,
    on_remove: <T>(self: IRegistry, ctype: T) -> Signal<entity, nil>,
    on_clear: <T>(self: IRegistry, ctype: T) -> Signal<>,
    after_clear: <T>(self: IRegistry, ctype: T) -> Signal<>,
}

local function HAS_ANY(pools: Array<Pool<unknown>>, key: number): boolean
    for _, pool in next, pools do
        if POOL_HAS(pool, key) then return true end
    end
    return false
end

local function get_smallest(pools: Array<Pool<unknown>>): (Pool<unknown>, number)
    local pool: Pool<unknown>?
    local pos = 0

    for i, p in next, pools do
        if pool == nil or p.size < pool.size then
            pool = p
            pos = i
        end
    end

    return assert(pool, "no pools given"), pos
end

local get_smallest_first_tuple = {}
local function get_smallest_first(pools: Array<Pool<unknown>>): (number, ...Pool<unknown>)
    table.clear(get_smallest_first_tuple)
    local s, s_pos = get_smallest(pools)
    for i, p in next, pools do
        if p ~= s then
            table.insert(get_smallest_first_tuple, p)
        end
    end
    return s_pos, s, unpack(get_smallest_first_tuple)
end

local function get_pools(registry: IRegistry, ctypes: Array<CType>): Array<Pool<unknown>>
    local pools = table.create(#ctypes)
    for i, ctype in next, ctypes do
        pools[i] = registry:storage(ctype)
    end
    return pools
end

local function disconnect_all(connections: Array<Connection>)
    for _, connection in next, connections do
        connection:disconnect()
    end
end

local function clear_invalidation_flag<T>(pool: Pool<T>)
    if pool.group then pool.group.added = false end
end

local function ASSERT_INVALIDATION<T>(pool: Pool<T>): ...any
    if pool.group and pool.group.added then
        throw("group reordered during iteration")
    end
end

local function view_iter_1<A>(a: Pool<A>): () -> (entity, A)
    local n = a.size
    local entities = a.entities
    local values = a.values

    clear_invalidation_flag(a)

    return function()
        local i = n - 1; n = i

        if i < 0 then
            ASSERT_INVALIDATION(a)
            return NIL, NIL
        end

        return BUFFER_GET(entities, i), ARRAY_GET(values, i)
    end
end

local function view_iter_1_excl_1<A>(a: Pool<A>, b: Pool<unknown>): () -> (entity, A)
    local n = a.size
    local entities = a.entities
    local values = a.values

    clear_invalidation_flag(a)

    return function()
        for i = n - 1, 0, -1 do
            local id = BUFFER_GET(entities, i)

            if POOL_FIND(b, ID_KEY(id)) ~= ID_NULL then continue end
            n = i

            return id, ARRAY_GET(values, i)
        end

        ASSERT_INVALIDATION(a)
        return NIL, NIL
    end
end

local function view_iter_2<A, B>(a: Pool<A>, b: Pool<B>): () -> (entity, A, B)
    local na, nb = a.size, b.size

    if na <= nb then
        local n = na
        local entities = a.entities
        local values = a.values

        clear_invalidation_flag(a)

        return function()
            for i = n - 1, 0, -1 do
                local id = BUFFER_GET(entities, i)
                local idx_ver = POOL_FIND(b, ID_KEY(id))
                if idx_ver == ID_NULL then continue end
                n = i
                return id, ARRAY_GET(values, i), ARRAY_GET(b.values, ID_KEY(idx_ver))
            end

            ASSERT_INVALIDATION(a)
            return NIL, NIL, NIL
        end
    else
        local n = nb
        local entities = b.entities
        local values = b.values

        clear_invalidation_flag(b)

        return function()
            for i = n - 1, 0, -1 do
                local id = BUFFER_GET(entities, i)
                local idx_ver = POOL_FIND(a, ID_KEY(id))
                if idx_ver == ID_NULL then continue end
                n = i
                return id, ARRAY_GET(a.values, ID_KEY(idx_ver)), ARRAY_GET(values, i)
            end

            ASSERT_INVALIDATION(b)
            return NIL, NIL, NIL
        end
    end
end

local function view_iter_2_excl_1<A, B>(a: Pool<A>, b: Pool<B>, c: Pool<unknown>): () -> (entity, A, B)
    local na, nb = a.size, b.size

    if na <= nb then
        local n = na
        local entities = a.entities
        local values = a.values

        clear_invalidation_flag(a)

        return function()
            for i = n - 1, 0, -1 do
                local id = BUFFER_GET(entities, i)
                local key = ID_KEY(id)
                if POOL_FIND(c, key) ~= ID_NULL then continue end
                local idx_ver = POOL_FIND(b, key)
                if idx_ver == ID_NULL then continue end
                n = i
                return id, ARRAY_GET(values, i), ARRAY_GET(b.values, ID_KEY(idx_ver))
            end

            ASSERT_INVALIDATION(a)
            return NIL, NIL, NIL
        end
    else
        local n = nb
        local entities = b.entities
        local values = b.values

        clear_invalidation_flag(b)

        return function()
            for i = n - 1, 0, -1 do
                local id = BUFFER_GET(entities, i)
                local key = ID_KEY(id)
                if POOL_FIND(c, key) ~= ID_NULL then continue end
                local idx_ver = POOL_FIND(a, key)
                if idx_ver == ID_NULL then continue end
                n = i
                return id, ARRAY_GET(a.values, ID_KEY(idx_ver)), ARRAY_GET(values, i)
            end

            ASSERT_INVALIDATION(b)
            return NIL, NIL, NIL
        end
    end
end

local function view_iter_3(includes: Array<Pool<unknown>>): () -> (entity, ...any)
    local s_pos, s, a, b = get_smallest_first(includes)

    local n = s.size
    local entities = s.entities
    local values = s.values

    clear_invalidation_flag(s)

    return function()
        for i = n - 1, 0, -1 do
            local id = BUFFER_GET(entities, i)
            local key = ID_KEY(id)

            local a_idx_ver = POOL_FIND(a, key)
            if a_idx_ver == ID_NULL then continue end
            local va = ARRAY_GET(a.values, ID_KEY(a_idx_ver))

            local b_idx_ver = POOL_FIND(b, key)
            if b_idx_ver == ID_NULL then continue end
            local vb = ARRAY_GET(b.values, ID_KEY(b_idx_ver))

            local sv = ARRAY_GET(values, i)

            n = i
            if s_pos == 1 then
                return id, sv, va, vb
            elseif s_pos == 2 then
                return id, va, sv, vb
            else
                return id, va, vb, sv
            end
        end

        ASSERT_INVALIDATION(s)
        return NIL
    end
end

local function view_iter_4(includes: Array<Pool<unknown>>): () -> (entity, ...any)
    local s_pos, s, a, b, c = get_smallest_first(includes)

    local n = s.size
    local entities = s.entities
    local values = s.values

    clear_invalidation_flag(s)

    return function()
        for i = n - 1, 0, -1 do
            local id = BUFFER_GET(entities, i)
            local key = ID_KEY(id)

            local a_idx_ver = POOL_FIND(a, key)
            if a_idx_ver == ID_NULL then continue end
            local va = ARRAY_GET(a.values, ID_KEY(a_idx_ver))

            local b_idx_ver = POOL_FIND(b, key)
            if b_idx_ver == ID_NULL then continue end
            local vb = ARRAY_GET(b.values, ID_KEY(b_idx_ver))

            local c_idx_ver = POOL_FIND(c, key)
            if c_idx_ver == ID_NULL then continue end
            local vc = ARRAY_GET(c.values, ID_KEY(c_idx_ver))

            local sv = ARRAY_GET(values, i)

            n = i
            if s_pos == 1 then
                return id, sv, va, vb, vc
            elseif s_pos == 2 then
                return id, va, sv, vb, vc
            elseif s_pos == 3 then
                return id, va, vb, sv, vc
            else
                return id, va, vb, vc, sv
            end
        end

        ASSERT_INVALIDATION(s)
        return NIL
    end
end

local function view_iter_general(
    includes: Array<Pool<unknown>>,
    excludes: Array<Pool<unknown>>?,
    lead: Pool<unknown>?
): () -> (entity, ...any)
    local source = lead or get_smallest(includes)
    local source_pos: number?
    for i, pool in next, includes do
        if pool == source then
            source_pos = i
            break
        end
    end
    assert(source_pos)

    local n = source.size
    local entities = source.entities
    local values = source.values

    local last_pos = #includes
    local tuple = table.create(last_pos)

    clear_invalidation_flag(source)

    return function()
        for i = n - 1, 0, -1 do
            local id = BUFFER_GET(entities, i)
            local key = ID_KEY(id)

            if excludes and HAS_ANY(excludes, key) then continue end

            local has_all = true

            for pos = 1, source_pos - 1 do
                local pool = includes[pos]
                local idx_ver = POOL_FIND(pool, key)
                if idx_ver == ID_NULL then has_all = false; break end
                tuple[pos] = ARRAY_GET(pool.values, ID_KEY(idx_ver))
            end

            if has_all == false then continue end

            for pos = source_pos + 1, last_pos do
                local pool = includes[pos]
                local idx_ver = POOL_FIND(pool, key)
                if idx_ver == ID_NULL then has_all = false; break end
                tuple[pos] = ARRAY_GET(pool.values, ID_KEY(idx_ver))
            end

            if has_all == false then continue end

            tuple[source_pos] = ARRAY_GET(values, i)
            n = i
            return id, unpack(tuple)
        end

        ASSERT_INVALIDATION(source)
        return NIL
    end
end

local function view_iter_list_general(
    list: Array<entity>,
    includes: Array<Pool<unknown>>,
    excludes: Array<Pool<unknown>>?
): () -> (entity, ...any)
    local n = #list

    local last_pos = #includes
    local tuple = table.create(last_pos)

    return function()
        for i = n, 1, -1 do
            local id = list[i]
            local key = ID_KEY(id)

            if excludes and HAS_ANY(excludes, key) then continue end

            local has_all = true

            for pos = 1, last_pos do
                local pool = includes[pos]
                local idx_ver = POOL_FIND(pool, key)
                if idx_ver == ID_NULL then has_all = false; break end
                tuple[pos] = ARRAY_GET(pool.values, ID_KEY(idx_ver))
            end

            if has_all == false then continue end

            n = i - 1
            return id, unpack(tuple)
        end

        return NIL
    end
end

local function view_patch_1<A>(a: Pool<A>, fn: (A) -> A)
    local entities = a.entities
    local values = a.values

    local on_change = a.on_change :: Array<Listener<A>>
    if on_change then
        for i, v in values do
            local v_new = fn(v)
            fire(on_change, BUFFER_GET(entities, i - 1), v_new)
            values[i] = v_new
        end
    else
        for i, v in values do
            values[i] = fn(v)
        end
    end
end

local function view_patch_2<A, B>(a: Pool<A>, b: Pool<B>, fn: (A, B) -> (A, B))
    local na, nb = a.size, b.size

    local a_on_change = a.on_change :: Array<Listener<A>>
    local b_on_change = b.on_change :: Array<Listener<B>>

    if na <= nb then
        local n = na
        local entities = a.entities
        local values = a.values

        for i = 0, n - 1 do
            local id = BUFFER_GET(entities, i)
            local idx_ver = POOL_FIND(b, ID_KEY(id))
            if idx_ver == ID_NULL then continue end
            local idx = ID_KEY(idx_ver)

            local va_new, vb_new = fn(ARRAY_GET(values, i), ARRAY_GET(b.values, idx))

            if a_on_change then fire(a_on_change, id, va_new) end
            if b_on_change then fire(b_on_change, id, vb_new) end

            ARRAY_SET(values, i, va_new)
            ARRAY_SET(b.values, idx, vb_new)
        end

    else
        local n = nb
        local entities = b.entities
        local values = b.values

        for i = 0, n - 1 do
            local id = BUFFER_GET(entities, i)
            local idx_ver = POOL_FIND(a, ID_KEY(id))
            if idx_ver == ID_NULL then continue end
            local idx = ID_KEY(idx_ver)

            local va_new, vb_new = fn(ARRAY_GET(a.values, idx), ARRAY_GET(values, i))

            if a_on_change then fire(a_on_change, id, va_new) end
            if b_on_change then fire(b_on_change, id, vb_new) end

            ARRAY_SET(a.values, idx, va_new)
            ARRAY_SET(values, i, vb_new)
        end
    end
end

local function view_patch_general(
    includes: Array<Pool<unknown>>,
    excludes: Array<Pool<unknown>>?,
    fn: (...unknown) -> ...unknown
)
    ASSERT(#includes <= 4, "cannot patch a view with more than 4 component types")
    local source = get_smallest(includes)
    local source_pos: number?
    for i, pool in next, includes do
        if pool == source then
            source_pos = i
            break
        end
    end
    assert(source_pos)

    local n = source.size
    local entities = source.entities
    local values = source.values

    local last_pos = #includes
    local tuple = table.create(last_pos)
    local idxs = table.create(last_pos)

    for i = 0, n - 1 do
        local id = BUFFER_GET(entities, i)
        local key = ID_KEY(id)

        if excludes and HAS_ANY(excludes, key) then continue end

        local has_all = true

        for pos = 1, source_pos - 1 do
            local pool = includes[pos]
            local idx_ver = POOL_FIND(pool, key)
            if idx_ver == ID_NULL then has_all = false; break end
            local idx = ID_KEY(idx_ver)
            tuple[pos] = ARRAY_GET(pool.values, idx)
            idxs[pos] = idx
        end

        if has_all == false then continue end

        for pos = source_pos + 1, last_pos do
            local pool = includes[pos]
            local idx_ver = POOL_FIND(pool, key)
            if idx_ver == ID_NULL then has_all = false; break end
            local idx = ID_KEY(idx_ver)
            tuple[pos] = ARRAY_GET(pool.values, idx)
            idxs[pos] = idx
        end

        if has_all == false then continue end

        tuple[source_pos] = ARRAY_GET(values, i)
        idxs[source_pos] = i

        tuple[1], tuple[2], tuple[3], tuple[4] =
            fn(tuple[1], tuple[2], tuple[3], tuple[4])

        for pos, pool in includes do
            local on_change = pool.on_change :: Array<Listener<unknown>>
            if on_change then fire(on_change, id, tuple[pos]) end
            ARRAY_SET(pool.values, idxs[pos], tuple[pos])
        end
    end

    return NIL
end

type _View<T...> = typeof(setmetatable({} :: {
    registry: IRegistry,
    includes: Array<CType>,
    excludes: Array<CType>?,
    use_list: Array<entity>?,

    exclude: <U...>(_View<T...>, U...) -> _View<T...>,
    of: (_View<T...>, list: Array<entity>) -> _View<T...>,
    patch: (_View<T...>, fn: (T...) -> T...) -> (),
    iter: (_View<T...>) -> () -> (entity, T...)
}, {} :: {
    __len: (_View<T...>) -> number,
    __iter: (_View<T...>) -> () -> (entity, T...)
}))

function view_create<T...>(reg: IRegistry, ...: T...): View<T...>
    local includes = {}

    for i = 1, select("#", ...) do
        local ctype = select(i, ...)
        ASSERT_CTYPE_VALID(ctype, i)
        table.insert(includes, ctype)
    end

    local function iter(self: _View<T...>): () -> (entity, T...)
        local includes = get_pools(self.registry, self.includes)
        local excludes = self.excludes and get_pools(self.registry, self.excludes)

        return if self.use_list then
                view_iter_list_general(self.use_list, includes, excludes)
            elseif #includes == 1 and not excludes then
                view_iter_1(includes[1])
            elseif #includes == 1 and excludes and #excludes == 1 then
                view_iter_1_excl_1(includes[1], excludes[1])
            elseif #includes == 2 and not excludes then
                view_iter_2(includes[1], includes[2])
            elseif #includes == 2 and excludes and #excludes == 1 then
                view_iter_2_excl_1(includes[1], includes[2], excludes[1])
            elseif #includes == 3 and not excludes then
                view_iter_3(includes)
            elseif #includes == 4 and not excludes then
                view_iter_4(includes)
            else
                view_iter_general(includes, excludes)
    end

    local self = {
        registry = reg,
        includes = includes,
        excludes = nil,

        exclude = function<E...>(self: _View<T...>, ...: E...): _View<T...>
            local includes = self.includes
            local excludes = self.excludes or (function()
                local t = {}
                self.excludes = t
                return t
            end)()

            for i = 1, select("#", ...) do
                local ctype = select(i, ...)
                ASSERT_CTYPE_VALID(ctype, i)
                ASSERT(not table.find(includes, ctype), `cannot exclude {ctype_debug(ctype, i)}, component is a part of the view`)
                if table.find(excludes, ctype) then continue end
                table.insert(excludes, ctype)
            end

            return self
        end,

        of = function(self: _View<T...>, list: Array<entity>?): _View<T...>
            self.use_list = list or {}
            return self
        end,

        patch = function(self: _View<T...>, fn: (T...) -> T...)
            local includes = get_pools(self.registry, self.includes)
            local excludes = self.excludes and get_pools(self.registry, self.excludes)

            if #includes == 1 and not excludes then
                view_patch_1(includes[1], fn :: any)
            elseif #includes == 2 and not excludes then
                view_patch_2(includes[1], includes[2], fn :: any)
            else
                view_patch_general(includes, excludes, fn :: any)
            end
        end,

        iter = iter,

        __len = function(self: _View<T...>): number
            return get_smallest(get_pools(self.registry, self.includes)).size
        end,

        __iter = iter
    }

    setmetatable(self, self)

    return self :: _View<T...>
end

type _Observer<T...> = typeof(setmetatable({} :: {
    registry: IRegistry,
    pool: Pool<unknown>,
    includes: Array<CType>,
    excludes: Array<CType>?,
    connections: Array<Connection>?,

    exclude: <U...>(_Observer<T...>, U...) -> _Observer<T...>,
    disconnect: (_Observer<T...>) -> _Observer<T...>,
    reconnect: (_Observer<T...>) -> _Observer<T...>,

    clear: (_Observer<T...>) -> _Observer<T...>,
    iter: (_Observer<T...>) -> () -> (entity, T...)
}, {} :: {
    __len: (_Observer<T...>) -> number,
    __iter: (_Observer<T...>) -> () -> (entity, T...)
}))

function observer_create<T...>(reg: IRegistry, ...: T...): Observer<T...>
    local includes = {}

    for i = 1, select("#", ...) do
        local ctype = select(i, ...)
        ASSERT_CTYPE_VALID(ctype, i)
        table.insert(includes, ctype)
    end

    local function iter(self: _Observer<T...>): () -> (entity, T...)
        local pool = self.pool
        local reg = self.registry
        local includes = get_pools(reg, self.includes)
        local excludes = self.excludes and get_pools(reg, self.excludes)

        local n = pool.size
        local entities = pool.entities

        local reg_pool = includes[1]
        local reg_map = reg_pool.map
        local reg_values = reg_pool.values

        local tuple = table.create(#includes) :: Array<any>

        return if #includes == 1 and not excludes then
                function()
                    local i = n - 1
                    n = i

                    if i < 0 then
                        self:clear()
                        return NIL
                    end

                    local id = BUFFER_GET(entities, i)

                    return id, ARRAY_GET(
                        reg_values,
                        ID_KEY(BUFFER_GET(reg_map, ID_KEY(id)))
                    ) :: any
                end
            else
                function()
                    for i = n - 1, 0, -1 do
                        local id = BUFFER_GET(entities, i)
                        local key = ID_KEY(id)

                        if excludes and HAS_ANY(excludes, key) then continue end

                        local has_all = true
                        for pos, pool in next, includes do
                            local idx_ver = POOL_FIND(pool, key) -- todo: guarantee
                            if idx_ver == ID_NULL then has_all = false; break end
                            tuple[pos] = ARRAY_GET(pool.values, ID_KEY(idx_ver))
                        end

                        if has_all == false then continue end

                        n = i
                        return id, unpack(tuple)
                    end

                    self:clear()
                return nil :: any, nil :: any
            end
    end

    local self = {
        registry = reg,
        pool = pool_create(), -- treat all initial ids as changed
        includes = includes,
        excludes = nil,
        connections = nil,

        exclude = function<E...>(self: _Observer<T...>, ...: E...): _Observer<T...>
            local excludes = self.excludes or (function()
                local t = {}
                self.excludes = t
                return t
            end)()

            for i = 1, select("#", ...) do
                local ctype = select(i, ...)
                ASSERT_CTYPE_VALID(ctype, i)
                ASSERT(not table.find(self.includes, ctype), `cannot exclude {ctype_debug(ctype, i)}, component is being tracked`)
                if table.find(excludes, ctype) then continue end
                table.insert(excludes, ctype)
            end

            return self
        end,

        disconnect = function(self: _Observer<T...>): _Observer<T...>
            ASSERT(self.pool.size == 0, "attempt to disconnect a non-empty observer")
            if not self.connections then return self end
            disconnect_all(self.connections)
            self.connections = nil
            return self
        end,

        reconnect = function(self: _Observer<T...>): _Observer<T...>
            if self.connections then return self end

            local reg = self.registry
            local pool = self.pool
            local includes = self.includes

            local all_connections = {}
            local remove_connections = {}

            for i, ctype in includes do
                local function added_or_changed_listener(id)
                    local key = ID_KEY(id)
                    POOL_RESIZE_MAP_IF_NEEDED(pool, key)
                    local idx_ver = BUFFER_GET(pool.map, key)

                    if idx_ver == ID_NULL then
                        local n = pool.size
                        POOL_RESIZE_ENTITIES_IF_NEEDED(pool, n)
                        pool.size = n + 1
                        BUFFER_SET(pool.map, key, ID_REPLACE(id, n))
                        BUFFER_SET(pool.entities, n, id)
                    end
                end

                local function removed_listener(id: number)
                    local map = pool.map
                    local entities = pool.entities

                    local key = ID_KEY(id)
                    local idx_ver = POOL_FIND(pool, key)

                    if idx_ver ~= ID_NULL then
                        local n = pool.size - 1
                        pool.size = n
                        local idx = ID_KEY(idx_ver)
                        local id_last = BUFFER_GET(entities, n)

                        BUFFER_SET(map, ID_KEY(id_last), ID_REPLACE(id_last, idx))
                        BUFFER_SET(map, key, ID_NULL)

                        BUFFER_SET(entities, idx, id_last)
                        --BUFFER_SET(entities, n, ID_NULL)
                    end
                end

                table.insert(all_connections, reg:on_add(ctype):connect(added_or_changed_listener))
                table.insert(all_connections, reg:on_change(ctype):connect(added_or_changed_listener))

                local remove_connection = reg:on_remove(ctype):connect(removed_listener)
                table.insert(all_connections, remove_connection)
                table.insert(remove_connections, remove_connection)

                table.insert(all_connections, reg:on_clear(ctype):connect(function()
                    for _, cn in remove_connections do cn:disconnect() end
                end))

                table.insert(all_connections, reg:after_clear(ctype):connect(function()
                    pool_clear(self.pool)
                    for _, cn in remove_connections do cn:reconnect() end
                end))
            end

            self.connections = all_connections

            return self
        end,

        clear = function(self: _Observer<T...>): _Observer<T...>
            pool_clear(self.pool)
            return self
        end,

        iter = function(self) return iter(self) end,

        __len = function(self: _Observer<T...>): number
            return self.pool.size
        end,

        __iter = function(self) return iter(self) end
    }

    setmetatable(self, self)

    return self:reconnect()  :: _Observer<T...>
end

type _Group<T...> = typeof(setmetatable({} :: {
    data: GroupData,
    pools: Array<Pool<unknown>>,

    iter: (self: _Group<T...>) -> () -> (entity, T...)
}, {} :: {
    __len: (self: _Group<T...>) -> number,
    __iter: (self: _Group<T...>) -> () -> (entity, T...)
}))

function group_create<T...>(reg: Registry, data: GroupData, ...: T...): Group<T...>
    local pools = {}

    for i = 1, select("#", ...) do
        local ctype = select(i, ...)
        local pool = reg:storage(ctype)
        assert(table.find(data, pool), "component type is not in group")
        pools[i] = pool
    end

    local function iter(self: _Group<T...>): () -> (entity, T...)
        local pools = self.pools
        local n = self.data.size
        local entities = pools[1].entities

        local values: Array<Array<unknown>> = table.create(#pools)
        for i, pool in next, pools do
            values[i] = pool.values
        end

        if #pools == 1 then
            local a = unpack(values)
            return function()
                local ia = n
                if ia == 0 then return NIL end
                local ib = ia - 1
                n = ib
                return BUFFER_GET(entities, ib), a[ia]
            end
        elseif #pools == 2 then
            local a, b = unpack(values)
            return function()
                local ia = n
                if ia == 0 then return NIL end
                local ib = ia - 1
                n = ib
                return BUFFER_GET(entities, ib), a[ia], b[ia]
            end
        elseif #pools == 3 then
            local a, b, c = unpack(values)
            return function()
                local ia = n
                if ia == 0 then return NIL end
                local ib = ia - 1
                n = ib
                return BUFFER_GET(entities, ib), a[ia], b[ia], c[ia]
            end
        elseif #pools == 4 then
            local a, b, c, d = unpack(values)
            return function()
                local ia = n
                if ia == 0 then return NIL end
                local ib = ia - 1
                n = ib
                return BUFFER_GET(entities, ib), a[ia], b[ia], c[ia], d[ia]
            end
        else
            local tuple = table.create(#values) :: Array<any>
            return function()
                local ia = n
                if ia == 0 then return NIL end
                local ib = ia - 1
                n = ib
                for pos, v in next, values do
                    tuple[pos] = v[ia]
                end
                return BUFFER_GET(entities, ib), unpack(tuple)
            end
        end
    end

    local self = {
        data = data,
        pools = pools,

        iter = iter,

        __len = function(self: _Group<T...>): number
            return self.data.size
        end,

        __iter = iter
    }

    setmetatable(self, self)

    return self :: _Group<T...>
end

end

--------------------------------------------------------------------------------
-- signal
--------------------------------------------------------------------------------

local signal_create: <T...>() -> (Signal<T...>, Array<(T...) -> ()>) do
    type _Signal<T...> = {
        listeners: Array<(T...) -> ()>,

        connect: (_Signal<T...>, (T...) -> ()) -> _Connection<T...>,
        on_empty: () -> ()?,
        on_not_empty: () -> ()?
    }

    type _Connection<T... = ...unknown> = {
        signal: _Signal<T...>,
        listener: (T...) -> (),
        connected: boolean,

        disconnect: (_Connection<T...>) -> (),
        reconnect: (_Connection<T...>) -> ()
    }

    local Connection = {}

    function Connection.new<T...>(signal: _Signal<T...>, fn: (T...) -> ()): _Connection<T...>
        return {
            signal = signal,
            listener = fn,
            connected = true,

            disconnect = function(self: _Connection<T...>)
                if self.connected then
                    local listeners = self.signal.listeners
                    local n = #listeners
                    local last = listeners[n]
                    local idx = table.find(listeners, fn)
                    assert(idx, "cannot find listener")

                    listeners[idx] = last
                    listeners[n] = nil

                    self.connected = false

                    if n == 1 and self.signal.on_empty then
                        self.signal.on_empty()
                    end

                end
            end,

            reconnect = function(self: _Connection<T...>)
                if not self.connected then
                    local new = self.signal:connect(self.listener)
                    self.idx = new.idx
                    self.connected = true
                end
            end
        }
    end

    local Signal = {}

    function Signal.new<T...>(): _Signal<T...>
        local self: _Signal<T...> = {
            listeners = {},

            on_empty = nil,
            on_not_empty = nil,

            connect = function(self: _Signal<T...>, listener: (T...) -> ()): _Connection<T...>
                local n = #self.listeners

                if n == 0 and self.on_not_empty then
                    self.on_not_empty()
                end

                self.listeners[n + 1] = listener

                return Connection.new(self, listener)
            end
        }

        return self
    end

    function signal_create<T...>(): Signal<T...>
        local signal = Signal.new()
        return signal :: any -- todo
    end
end

--------------------------------------------------------------------------------
-- handle
--------------------------------------------------------------------------------

local Handle = (function(): HandleMT
    local Handle = {}
    Handle.__index = Handle

    function Handle.destroy(self: Handle)
        self.registry:destroy(self.entity)
    end

    function Handle.has_none(self: Handle): boolean
        return self.registry:has_none(self.entity)
    end

    function Handle.add<T...>(self: Handle, ...: T...)
        self.registry:add(self.entity, ...)
    end

    function Handle.set<T>(self: Handle, ctype: T, value: T): Handle
        self.registry:set(self.entity, ctype, value)
        return self
    end

    function Handle.insert<T>(self: Handle, ctype: Array<T>, value: T): Handle
        self.registry:insert(self.entity, ctype, value)
        return self
    end

    function Handle.patch<T>(self: Handle, ctype: T, fn: ((T) -> T)?): T
        return self.registry:patch(self.entity, ctype, fn)
    end

    function Handle.has<T...>(self: Handle, ...: T...): boolean
        return self.registry:has(self.entity, ...)
    end

    function Handle.get<T...>(self: Handle, ...: T...): T...
        return self.registry:get(self.entity, ...)
    end

    function Handle.try_get<T>(self: Handle, ctype: T): T?
        return self.registry:try_get(self.entity, ctype)
    end

    function Handle.remove<T...>(self: Handle, ...: T...)
        self.registry:remove(self.entity, ...)
    end

    return Handle
end)()

--------------------------------------------------------------------------------
-- registry
--------------------------------------------------------------------------------

local function registry_create(range_start: number?, range_stop: number?): Registry
    local MAX_CTYPE = ctype_n

    if range_start then
        assert(range_stop)
        ASSERT(range_start > 0, "start of partition must be at least 1")
        ASSERT(range_stop >= range_start, "end of partition must be greater then start")
        ASSERT(range_stop <= ID_MAX_VALID_KEY, `end of partition cannot be greater than {ID_MAX_VALID_KEY}`)
    end

    local partition_start = range_start and range_start or 1
    local partition_stop = range_stop and range_stop or ID_MAX_VALID_KEY

    local registry = {}

    local entity_list: EntityList = entity_list_create()
    local entity_pool = pool_create()

    local signals = {
        on_add = {} :: Map<CType, Signal<entity, unknown>>,
        on_change = {} :: Map<CType, Signal<entity, unknown>>,
        on_remove = {} :: Map<CType, Signal<entity, nil>>,
        on_clear = {} :: Map<CType, Signal<>>,
        after_clear = {} :: Map<CType, Signal<>>
    }

    local handle_cache = {} :: Map<entity?, Handle?>
    setmetatable(handle_cache :: any, { __mode = "v" })

    local ctype_pools: Map<CType, Pool<unknown>> = table.create(MAX_CTYPE)
    setmetatable(ctype_pools :: any, {
        __index = function(self, ctype: CType): Pool<unknown>
            ASSERT_CTYPE_VALID(ctype)
            ASSERT((ctype :: number) <= MAX_CTYPE, `cannot use {ctype_debug(ctype)}, component must be created before registry creation`)
            local pool = pool_create(1)
            self[ctype] = pool
            return pool
        end
    })
    ctype_pools[ctype_entity] = entity_pool

    local function STORAGE<T>(ctype: T): Pool<T>
        return ctype_pools[ctype] :: Pool<any>
    end

    local function group_init(...: any): GroupData
        local group = { size = 0, added = false, connections = {} }

        for i = 1, select("#", ...) do
            local ctype = select(i, ...)
            local pool = STORAGE(ctype)
            group[i] = pool
            pool.group = group

            table.insert(group.connections, registry:on_add(ctype):connect(function(id)
                POOL_TRY_GROUP(pool, id)
            end))

            table.insert(group.connections, registry:on_remove(ctype):connect(function(id)
                POOL_TRY_UNGROUP(pool, id)
            end))

            registry:on_clear(ctype):connect(function()
                for _, cn in group.connections do cn:disconnect() end
            end)

            registry:after_clear(ctype):connect(function()
                group.size = 0
                for _, cn in group.connections do cn:reconnect() end
            end)
        end

        local pool = STORAGE((...))
        for i = 0, pool.size - 1 do
            POOL_TRY_GROUP(pool, BUFFER_GET(pool.entities, i))
        end

        return group
    end

    local function ASSERT_VALID_ENTITY(id: entity)
        ASSERT(ENTITY_LIST_ID_IN_USE(entity_list, id), "invalid entity")
    end

    function registry.create(self: Registry, desired_id: entity?): entity
        if not desired_id then
            if entity_list.free == ID_NULL_KEY then
                local old_capacity = entity_list.capacity
                local old_max = old_capacity - 1
                ASSERT(old_max < partition_stop, "cannot create entity; registry is at max entities")

                local new_capacity = math.ceil((old_capacity + 1) * 1.5)
                local new_max = new_capacity - 1
                if new_max > partition_stop then
                    new_capacity = partition_stop + 1
                elseif new_max < partition_start then
                    new_capacity = partition_start + 1
                end

                entity_list_resize(entity_list, new_capacity, partition_start, partition_stop)
            end

            assert(entity_list.free ~= ID_NULL_KEY)

            local new_key = entity_list.free
            local next_key_cur_ver = BUFFER_GET(entity_list.data, new_key)
            local new_id = ID_CREATE(new_key, ID_VER(next_key_cur_ver))

            BUFFER_SET(entity_list.data, new_key, new_id)
            entity_list.free = ID_KEY(next_key_cur_ver)

            POOL_RESIZE_MAP_IF_NEEDED(entity_pool, new_key)
            POOL_ADD_ID(entity_pool, new_id)

            return new_id
        else
            local desired_key = ID_KEY(desired_id)
            local desired_ver = ID_VER(desired_id)

            ASSERT(
                desired_id < ID_MAX and
                desired_key ~= ID_NULL_KEY and desired_key <= ID_MAX_VALID_KEY and
                desired_ver ~= ID_NULL_VER and desired_ver <= ID_MASK_VER,
                "malformed id"
            )

            if desired_key > entity_list.capacity - 1 then
                local new_capacity = math.ceil((desired_key + 1) * 1.5)
                if new_capacity - 1 > ID_MAX_VALID_KEY then new_capacity = ID_MAX_VALID_KEY + 1 end
                entity_list_resize(entity_list, new_capacity, partition_start, partition_stop)
            end

            ASSERT(ID_KEY(BUFFER_GET(entity_list.data, desired_key)) ~= desired_key, "unable to create entity; key is already in use")

            if entity_list.free == desired_key then
                entity_list.free = ID_KEY(BUFFER_GET(entity_list.data, desired_key))
            elseif BUFFER_GET(entity_list.data, desired_key) ~= ID_NULL then -- is somewhere along linked list
                local prev = entity_list_get_prev_key(entity_list, desired_key)
                BUFFER_SET(entity_list.data, prev, ID_CREATE( -- a -> b -> c, remove b, link a -> c
                    ID_KEY(BUFFER_GET(entity_list.data, desired_key)),
                    ID_VER(BUFFER_GET(entity_list.data, prev))
                ))
            end

            BUFFER_SET(entity_list.data, desired_key, desired_id)

            POOL_RESIZE_MAP_IF_NEEDED(entity_pool, desired_key)
            POOL_ADD_ID(entity_pool, desired_id)

            return desired_id
        end
    end

    function registry.release(self: Registry, id: entity)
        ASSERT_VALID_ENTITY(id)
        if entity_pool.on_remove then fire(entity_pool.on_remove, id) end
        entity_list_remove(entity_list, id, partition_start, partition_stop)
        POOL_REMOVE(entity_pool, ID_KEY(id), id)
    end

    function registry.destroy(self: Registry, id: entity)
        ASSERT_VALID_ENTITY(id)
        local key = ID_KEY(id)

        for _, pool in ctype_pools do
            if POOL_HAS(pool, key) then
                POOL_REMOVE(pool, key, id)
            end
        end

        if entity_pool.on_remove then fire(entity_pool.on_remove, id) end
        entity_list_remove(entity_list, id, partition_start, partition_stop)
        POOL_REMOVE(entity_pool, ID_KEY(id), id)
    end

    function registry.contains(self: Registry, id: entity): boolean
        return ENTITY_LIST_ID_IN_USE(entity_list, id)
    end

    function registry.has_none(self: Registry, id: entity): boolean
        ASSERT_VALID_ENTITY(id)
        local key = ID_KEY(id)

        for _, pool in next, ctype_pools, 1 do -- k0=1 to skip entity pool
            if POOL_HAS(pool, key) then return false end
        end

        return true
    end

    function registry.add<T...>(self: Registry, id: entity, ...: T...)
        ASSERT_VALID_ENTITY(id)
        local key = ID_KEY(id)

        for i = 1, select("#", ...) do
            local ctype = select(i, ...)
            local pool = STORAGE(ctype)

            POOL_RESIZE_MAP_IF_NEEDED(pool, key)
            if BUFFER_GET(pool.map, key) ~= ID_NULL then continue end

            local ctor = ctype_ctors[ctype]

            if ctor == true then
                POOL_ADD_ID(pool, id)
            elseif ctor == false then
                throw(`no constructor defined for {ctype_debug(ctype, i)}`)
            else
                local value = (ctor :: () -> unknown)()
                if value == nil then throw(`{ctype_debug(ctype, i)} constructor did not return a value`) end
                POOL_ADD(pool, id, value)
            end
        end
    end

    function registry.set<T>(self: Registry, id: entity, ctype: T, value: T)
        local pool = STORAGE(ctype)

        local key = ID_KEY(id)
        POOL_RESIZE_MAP_IF_NEEDED(pool, key)
        local idx_ver = BUFFER_GET(pool.map, key)

        if value ~= nil then -- valued component
            if idx_ver ~= ID_NULL then -- already added, change value
                ID_ASSERT_VERSION_EQUAL(idx_ver, id)
                if pool.on_change then fire(pool.on_change, id, value) end
                ARRAY_SET(pool.values, ID_KEY(idx_ver), value)
            else -- not added, add value
                ASSERT_VALID_ENTITY(id)
                POOL_ADD(pool, id, value)
            end
        else -- tag component
            ASSERT_VALID_ENTITY(id)
            ASSERT(ctype_is_tag(ctype), "cannot set component value to nil")
            if idx_ver ~= ID_NULL then return end
            POOL_ADD_ID(pool, id)
        end
    end

    function registry.insert<T>(self: Registry, id: entity, ctype: Array<T>, value: T)
        local pool = STORAGE(ctype)

        local key = ID_KEY(id)
        POOL_RESIZE_MAP_IF_NEEDED(pool, key)
        local idx_ver = BUFFER_GET(pool.map, key)

        if idx_ver ~= ID_NULL then
            ID_ASSERT_VERSION_EQUAL(idx_ver, id)
            local idx = ID_KEY(idx_ver)
            local t = ARRAY_GET(pool.values, idx)
            table.insert(t, value)
            if pool.on_change then fire(pool.on_change, id, t) end
        else
            ASSERT_VALID_ENTITY(id)
            local t = {value}
            POOL_ADD(pool, id, t)
        end
    end

    function registry.patch<T>(self: Registry, id: entity, ctype: T, fn: ((T) -> T)?): T
        local pool = STORAGE(ctype)

        local key = ID_KEY(id)
        POOL_RESIZE_MAP_IF_NEEDED(pool, key)
        local idx_ver = BUFFER_GET(pool.map, key)

        if idx_ver ~= ID_NULL then
            ID_ASSERT_VERSION_EQUAL(idx_ver, id)
            local idx = ID_KEY(idx_ver)
            if fn then
                local value = fn(ARRAY_GET(pool.values, idx))
                ASSERT(value ~= nil, "function cannot return nil")
                if pool.on_change then fire(pool.on_change, id, value) end
                ARRAY_SET(pool.values, idx, value)
                return value
            else
                local value = ARRAY_GET(pool.values, idx)
                if pool.on_change then fire(pool.on_change, id, value) end
                return value
            end
        else
            ASSERT_VALID_ENTITY(id)
            local ctor = ctype_ctors[ctype]
            if ctor == false or ctor == true then throw(`entity does not have component and no constructor for {ctype_debug(ctype)}`) end
            local value = (ctor :: () -> T)()
            if fn then value = fn(value) end
            ASSERT(value ~= nil, "function cannot return nil")
            POOL_ADD(pool, id, value)
            return value
        end
    end

    registry.has = (function(self: Registry, id: entity, a, b, c, d, e): boolean
        local key = ID_KEY(id)
        local idx_ver = POOL_FIND(STORAGE(a), key)
        return
            idx_ver ~= ID_NULL and ID_VER(idx_ver) == ID_VER(id) and
            (b == nil or POOL_FIND(STORAGE(b), key) ~= ID_NULL) and
            (c == nil or POOL_FIND(STORAGE(c), key) ~= ID_NULL) and
            (d == nil or POOL_FIND(STORAGE(d), key) ~= ID_NULL) and
            (e == nil or throw("args exceeded") :: never)
    end :: any) :: <T...>(self: Registry, id: entity, T...) -> boolean

    local function UNSAFE_GET(ctype: CType, key: number): unknown
        local pool = STORAGE(ctype)
        local idx_ver = POOL_FIND(pool, key)
        if idx_ver == ID_NULL then throw("entity does not have " .. ctype_debug(ctype)) end
        return ARRAY_GET(pool.values, ID_KEY(idx_ver))
    end

    registry.get = (function(self: Registry, id: entity, a, b, c, d, e): ...unknown
        local pool = STORAGE(a)

        local key = ID_KEY(id)
        local idx_ver = POOL_FIND(pool, key)
        if idx_ver == ID_NULL then throw(`entity does not have {ctype_debug(a, 1)}`) end

        ID_ASSERT_VERSION_EQUAL(idx_ver, id)
        local va = ARRAY_GET(pool.values, ID_KEY(idx_ver))

        if b == nil then
            return va
        elseif c == nil then
            return va, UNSAFE_GET(b, key)
        elseif d == nil then
            return va, UNSAFE_GET(b, key), UNSAFE_GET(c, key)
        elseif e == nil then
            return va, UNSAFE_GET(b, key), UNSAFE_GET(c, key), UNSAFE_GET(d, key)
        else throw("args exceeded") end
    end :: any) :: <T...>(self: Registry, id: entity, T...) -> T...

    function registry.try_get<T>(self: Registry, id: entity, ctype: T): T?
        local pool = STORAGE(ctype)
        local idx_ver = POOL_FIND(pool, ID_KEY(id))
        if idx_ver == ID_NULL or ID_VERSION_NOT_EQUAL(idx_ver, id) then return nil end
        return ARRAY_GET(pool.values, ID_KEY(idx_ver))
    end

    function registry.remove<T...>(self: Registry, id: entity, ...: T...)
        for i = 1, select("#", ...) do
            local pool = STORAGE(select(i, ...))
            POOL_TRY_REMOVE(pool, id)
        end
    end

    function registry.clear<T...>(self: Registry, ...: T...)
        local argn = select("#", ...)
        if argn > 0 then
            for i = 1, argn do pool_clear(STORAGE(select(i, ...))) end
        else
            for _, pool in next, ctype_pools do pool_clear(pool) end

            pool_clear(entity_pool)
            entity_list_clear(entity_list, partition_start, partition_stop)
        end
    end

    function registry.find<T>(self: Registry, ctype: T, value: T): entity?
        local pool = STORAGE(ctype)

        if value == nil then
            if pool.size == 0 then return nil end
            return BUFFER_GET(pool.entities, 0)
        else
            local arr_idx = table.find(pool.values, value)
            return arr_idx and BUFFER_GET(pool.entities, arr_idx - 1)
        end
    end

    function registry.copy<T>(self: Registry, copy: T, paste: T)
        local pool_a = STORAGE(copy)
        local pool_b = STORAGE(paste)

        POOL_COPY(pool_a, pool_b)
    end

    function registry.view<T...>(self: Registry, ...: T...): View<T...>
        ASSERT(select("#", ...) > 0, "view must contain at least 1 component")
        return view_create(registry, ...)
    end

    function registry.track<T...>(self: Registry, ...: T...): Observer<T...>
        ASSERT(select("#", ...) > 0, "observer must contain at least 1 component")
        return observer_create(registry, ...)
    end

    registry.group = nil :: any -- todo: why?
    function registry.group<T...>(self: Registry, ...: T...): Group<T...>
        local argn = select("#", ...)
        ASSERT(argn > 1, "group must contain at least 2 components")
        local group = STORAGE((select(1, ...))).group :: GroupData<unknown>

        for i = 1, argn do
            local ctype = select(i, ...)
            ASSERT_CTYPE_VALID(ctype, i)
            ASSERT(STORAGE(ctype).group == group, `cannot create group; {ctype_debug(ctype, i)} is not owned by the same group as previous args`)
        end

        return group_create(registry, group or group_init(...), ...)
    end

    function registry.storage<T>(self: Registry, ctype: T?): any
        if ctype == "list" then
            return entity_list
        elseif ctype then
            return STORAGE(ctype)
        else
            return coroutine.wrap(function()
                for ctype, pool in next, ctype_pools do
                    coroutine.yield(ctype, pool)
                end
            end)
        end
    end

    function registry.on_add<T>(self: Registry, ctype: T): Signal<entity, T>
        return (signals.on_add[ctype] or (function()
            local signal = signal_create() :: any
            signals.on_add[ctype] = signal
            function signal.on_empty() STORAGE(ctype).on_add = false end
            function signal.on_not_empty() STORAGE(ctype).on_add = signal.listeners end
            return signal
        end)()) :: Signal<entity, T>
    end

    function registry.on_change<T>(self: Registry, ctype: T): Signal<entity, T>
        return (signals.on_change[ctype] or (function()
            local signal = signal_create() :: any
            signals.on_change[ctype] = signal
            function signal.on_empty() STORAGE(ctype).on_change = false end
            function signal.on_not_empty() STORAGE(ctype).on_change = signal.listeners end
            return signal
        end)()) :: Signal<entity, T>
    end

    function registry.on_remove<T>(self: Registry, ctype: T): Signal<entity, nil>
        return signals.on_remove[ctype] or (function()
            local signal = signal_create() :: any
            signals.on_remove[ctype] = signal
            function signal.on_empty() STORAGE(ctype).on_remove = false end
            function signal.on_not_empty() STORAGE(ctype).on_remove = signal.listeners end
            return signal
        end)() :: Signal<entity, nil>
    end

    function registry.on_clear<T>(self: Registry, ctype: T): Signal<>
        return signals.on_clear[ctype] or (function()
            local signal = signal_create() :: any
            signals.on_clear[ctype] = signal
            function signal.on_empty() STORAGE(ctype).on_clear = false end
            function signal.on_not_empty() STORAGE(ctype).on_clear = signal.listeners end
            return signal
        end)() :: Signal<>
    end

    function registry.after_clear<T>(self: Registry, ctype: T): Signal<>
        return signals.after_clear[ctype] or (function()
            local signal = signal_create() :: any
            signals.after_clear[ctype] = signal
            function signal.on_empty() STORAGE(ctype).after_clear = false end
            function signal.on_not_empty() STORAGE(ctype).after_clear = signal.listeners end
            return signal
        end)() :: Signal<>
    end

    function registry.handle(self: Registry, id: entity?): Handle
        id = id or registry:create()

        local handle = handle_cache[id]

        if not handle then
            handle = table.freeze(setmetatable({
                registry = registry,
                entity = id :: entity
            }, Handle))

            handle_cache[id] = handle
        end

        return assert(handle)
    end

    function registry.context(self: Registry): Handle
        if not ENTITY_LIST_ID_IN_USE(entity_list, ID_CTX) then registry:create(ID_CTX) end
        return registry:handle(ID_CTX)
    end

    return table.freeze(registry)
end

--------------------------------------------------------------------------------
-- queue
--------------------------------------------------------------------------------

local Queue = {} do
    Queue.__index = Queue

    type _Queue = Queue<...any> & {
        size: number,
        columns: Array<Array<unknown>>
    }

    function Queue.new<T...>(): Queue<T...>
        local self: _Queue = setmetatable({
            size = 0,
            columns = {}
        }, Queue) :: any

        setmetatable(self.columns, {
            __index = function(columns: Array<Array<unknown>>, idx: number)
                columns[idx] = {}
                return columns[idx]
            end
        })

        return self :: Queue<T...>
    end

    function Queue.add(self: _Queue, ...: unknown)
        -- iteration will stop if first value is `nil`
        ASSERT((...) ~= nil, "first argument cannot be nil")

        local columns = self.columns
        local n = self.size + 1
        self.size = n

        for i = 1, select("#", ...) do
            columns[i][n] = select(i, ...)
        end
    end

    function Queue.clear(self: _Queue)
        self.size = 0
        for _, column in next, self.columns do
            table.clear(column)
        end
    end

    local function iter(self: _Queue)
        local columns = self.columns
        local n = self.size
        local i = 0

        if #columns <= 1 then
            local column = columns[1]
            return function()
                i += 1
                local value = column[i]
                if i == n then self:clear() end
                return value
            end
        else
            local tuple = table.create(#columns)
            return function()
                i += 1
                for ci, column in next, columns do
                    tuple[ci] = column[i]
                end
                if i == n then self:clear() end
                return unpack(tuple)
            end
        end
    end

    Queue.iter = function(self) return iter(self) end
    Queue.__iter = function(self) return iter(self) end

    function Queue.__len(self: _Queue)
        return self.size
    end
end

type ISignal<T...> = {
    connect: (self: any, listener: (T...) -> ()) -> ()
} | {
    Connect: (self: any, listener: (T...) -> ()) -> ()
} | (listener: (T...) -> ()) -> ()

local queue_create = function<T...>(signal: ISignal<T...>?): Queue<T...>
    local queue = Queue.new()

    if signal then
        local function listener(...: T...) queue:add(...) end

        if type(signal) == "function" then
            signal(listener)
        else
            local connector = (signal :: any).connect or (signal :: any).Connect
            ASSERT(connector, "signal has no member `connect()`")
            connector(signal, listener)
        end
    end

    return queue
end :: ( <T...>() -> Queue<T...> ) & ( <T...>(signal: ISignal<T...>) -> Queue<T...> )

--------------------------------------------------------------------------------
-- buffer util
--------------------------------------------------------------------------------

local function buffer_to_array(buf: buffer, size: number, arr: Array<entity>?): Array<entity>
    ASSERT(size * ID_SIZE <= buffer.len(buf), "buffer is smaller than given size")

    arr = arr or table.create(size); assert(arr)

    for i = 0, size - 1 do
        arr[i + 1] = BUFFER_GET(buf, i)
    end

    return arr
end

local function array_to_buffer(arr: Array<entity>, size: number, buf: buffer?): buffer
    buf = buf or buffer.create(size * ID_SIZE); assert(buf)

    ASSERT(size * ID_SIZE <= buffer.len(buf), "size is larger than buffer size")

    for i = 1, size do
        BUFFER_SET(buf, (i - 1), arr[i])
    end

    return buf
end

local function buffer_to_buffer(buf_src: buffer, size: number, buf_tar: buffer?): buffer
    buf_tar = buf_tar or buffer.create(size * ID_SIZE); assert(buf_tar)

    ASSERT(size * ID_SIZE <= buffer.len(buf_tar), "size is larger than buffer size")

    buffer.copy(buf_tar, 0, buf_src, 0, size * ID_SIZE)

    return buf_tar
end

--------------------------------------------------------------------------------
-- export
--------------------------------------------------------------------------------

local ecr = {
    registry = registry_create,

    component = function(constructor: () -> ()?)
        ASSERT(constructor == nil or type(constructor) == "function", "constructor must be a function")
        return ctype_create(constructor or false)
    end :: (() -> unknown) & (<T>(constructor: () -> T) -> T),

    tag = function() return ctype_create(true) end :: () -> nil,

    is_tag = function<T>(ctype: T): boolean
        ASSERT_CTYPE_VALID(ctype)
        return ctype_is_tag(ctype)
    end,

    name = function<T>(v: T): T | string?
        if type(v) == "table" then
            for name, ctype in next, v do
                ASSERT(CTYPE_VALID(ctype), `{name} refers to an invalid component`)
                ctype_set_name(ctype, name)
            end
            return v
        else
            ASSERT_CTYPE_VALID(v)
            return ctype_names[v]
        end
    end :: (<T>(names: T & {}) -> T) & (<T>(ctype: T) -> string?),

    queue = queue_create,

    array_to_buffer = array_to_buffer,
    buffer_to_array = buffer_to_array,
    buffer_to_buffer = buffer_to_buffer,

    entity = ctype_entity,
    null = ID_NULL,
    context = ID_CTX,
    id_size = ID_SIZE,

    inspect = function(id: entity): (number, number)
        local key, ver = ID_KEY(id), ID_VER(id)
        return key, ver * ID_RSHIFT
    end,

    _test = {
        ver_shift = ID_LSHIFT,
        max_ver = ID_MASK_VER * ID_RSHIFT,
        max_creatable = ID_MASK_KEY - 1,

        create_id = function(key: number, ver: number)
            return ID_CREATE(key, ver * ID_LSHIFT)
        end,

        set_key_version = function(reg: Registry, key: number, ver: number)
            local list = reg:storage("list") :: any
            local key_ver = BUFFER_GET(list.data, key)
            if ENTITY_LIST_ID_IN_USE(list, key_ver) then ASSERT(false, "attempt to set version of in-use entity") end
            BUFFER_SET(list.data, key, ID_CREATE(ID_KEY(key_ver), ver * ID_LSHIFT))
        end,

        get_key_version = function(reg: Registry, key: number): number
            local list = reg:storage("list") :: any
            ASSERT(list.capacity > key, "not contained")
            return ID_VER(BUFFER_GET(list.data, key)) * ID_RSHIFT
        end
    }
}

return table.freeze(ecr)


return function(rt: any, memory_at_0: any, cfns: any, INDIRECT_FUNCTIONS: any, utils: typeof(require("./utils")()))
    local module = {}

    function module.luau_setflag(name: string, state: boolean)
        --// Flag name
        local flagNamePtr = utils.cstr(name)

        --// Set the flag
        cfns.set_flag(flagNamePtr, if (state) then 1 else 0)

        --// Cleanup
        cfns.free(flagNamePtr)
        return
    end

    function module.luau_setallflags(state: boolean)
        cfns.set_all_flags(if (state) then 1 else 0)
        return
    end

    function module.luau_resetflags()
        cfns.reset_flags()
        return
    end

    return module
end


local task = task or (require)("@lune/task")

return function(utils: typeof(require("./utils")()))
    local rt, memory_at_0, cfns, INDIRECT_FUNCTIONS
    local polyfill = {}

    local FILE_MAP = { {}, {} }
    local LONGJMP_THROW_SIGN = newproxy(false)
    local __TIMERS = {}
    local __EXIT_STATUS = nil
    local __ABORT_WASM = false
    local __NO_EXIT_RUNTIME = true
    local __RUNTIME_KEEPALIVE_COUNTER = 0

    local function fd_flush(file: {string})
        local final = table.concat(file)
        local last = 1
        for index, data in string.gmatch(final, '()([^\n]*)\n') do
            last = index + #data
            print(data)
        end
        table.clear(file)
        file[1] = string.sub(final, last + 1)
    end

    local function invokeHandler(index: number)
        return function(fnIndex: number, ...)
            local stack_ptr = cfns.emscripten_stack_get_current()
            local err, stacktrace = "N/A", "N/A"
            local success = xpcall(
                INDIRECT_FUNCTIONS.data[fnIndex],
                function(_e)
                    err,stacktrace = _e,debug.traceback()
                end,
                ...
            )
            if success then
                return
            end

            cfns._emscripten_stack_restore(stack_ptr)
            if err ~= LONGJMP_THROW_SIGN then
                error(`{err}\n\ninvoke stacktrace:\n{stacktrace}`, 2)
            end

            cfns.setThrew(1, 0)
        end
    end

    polyfill.invoke_vii = invokeHandler(1)
    polyfill._tzset_js = utils.stub("_tzset_js", 0)

    function polyfill.__cxa_throw(ptr, type, destructor)
        local info = utils.newCException(type)
        error(`exception occured in c: {info:_UndecoratedName()} ({info:vfptr()})`, 0)
    end

    function polyfill._abort_js(reason: any)
        error(`Aborted({reason})`, 0)
    end

    function polyfill._emscripten_throw_longjmp()
        return error(LONGJMP_THROW_SIGN, 0)
    end

    function polyfill._emscripten_runtime_keepalive_clear()
        __NO_EXIT_RUNTIME, __RUNTIME_KEEPALIVE_COUNTER = false, 0
    end

    function polyfill.emscripten_resize_heap(_requestedSize: number)
        error("OOM", 0)
    end

    function polyfill._setitimer_js(which, timeoutMs)
        if (not task) then
            print("program is using _setitimer_js but task library not found, timers will NOT work.")
            return 0
        end

        if (__TIMERS[which]) then
            task.cancel(__TIMERS[which].thread)
            __TIMERS[which] = nil
        end
        if (not timeoutMs or timeoutMs == 0) then
            return 0
        end

        local thread = task.delay(function()
            __TIMERS[which] = nil
            if (__ABORT_WASM) then
                return
            end

            local success, result = pcall(function()
                cfns._emscripten_timeout(which, os.clock())
                if (__NO_EXIT_RUNTIME or __RUNTIME_KEEPALIVE_COUNTER > 0) then
                    return
                end

                local exit_success, exit_result = pcall(polyfill.proc_exit, __EXIT_STATUS)
                if exit_success then
                    return
                end

                if (string.match(exit_result, "ExitStatus:") ~= nil or exit_result == "unwind") then
                    return __EXIT_STATUS
                end

                error(exit_result, 0)
            end)

            if (not success) then
                if (string.match(result, "ExitStatus:") ~= nil or result == "unwind") then
                    return
                end

                error(result, 0)
            end
        end, timeoutMs / 1000)

        __TIMERS[which] = { thread = thread, timeoutMs = timeoutMs }
        return 0
    end

    function polyfill.fd_write(fd, list, count, ret_pointer)
        local file = FILE_MAP[fd]
        if not file then return 8 end
        local total = 0
        for pointer = list, list + (count - 1) * 8, 8 do
            local start = rt.load.i32(memory_at_0, pointer)
            local len = rt.load.i32(memory_at_0, pointer + 4)
            local read = rt.load.string(memory_at_0, start, len)
            table.insert(file, read)
            total = total + #read
        end
        rt.store.i32(memory_at_0, ret_pointer, total)
        fd_flush(file)
        return 0
    end

    function polyfill.proc_exit(code: number)
        __EXIT_STATUS = code
        if (not (__NO_EXIT_RUNTIME or __RUNTIME_KEEPALIVE_COUNTER > 0)) then
            __ABORT_WASM = true
        end

        error(`ExitStatus: Program exited with code ${code}`, 0)
    end

    return polyfill, function(_rt: any, _memory_at_0: any, _cfns: any, _INDIRECT_FUNCTIONS: any)
        rt, memory_at_0, cfns, INDIRECT_FUNCTIONS = _rt, _memory_at_0, _cfns, _INDIRECT_FUNCTIONS
    end
end


local runtime_initialize = (require)("@self/runtime")
local utils_create = require("./utils")

return function(polyfill_create: typeof(require("./polyfill")))
    local utils, utils_ready = utils_create()
    local polyfill, polyfill_ready = polyfill_create(utils)

    local common_imports = { func_list = polyfill }
    local RuntimeInstance = runtime_initialize({
        env = common_imports,
        wasi_snapshot_preview1 = common_imports,
    })

    local rt, memory_at_0, cfns, INDIRECT_FUNCTIONS =
        RuntimeInstance.rt,
        RuntimeInstance.memory_list.memory,
        RuntimeInstance.func_list,
        RuntimeInstance.table_list.__indirect_function_table

    polyfill_ready(rt, memory_at_0, cfns, INDIRECT_FUNCTIONS)
    utils_ready(rt, memory_at_0, cfns, INDIRECT_FUNCTIONS)
    return rt, memory_at_0, cfns, INDIRECT_FUNCTIONS, utils, polyfill
end


return function()
    local rt, memory_at_0, cfns, INDIRECT_FUNCTIONS
    local module = {}

    function module.stub(name: string, ret: any)
        return function(...)
            if (STUB_WARN) then
                print(`[WARN]: stub function '{name}' called`)
            end

            return ret
        end
    end

    function module.insertCFunction(func: (...any) -> (...any)): number
        local ptr = #INDIRECT_FUNCTIONS.data + 1
        INDIRECT_FUNCTIONS.data[ptr] = func

        return ptr
    end

    function module.cstr(value: string): number
        local str_len = #value
        local str_ptr = cfns.malloc(str_len + 1)
        rt.store.string(memory_at_0, str_ptr, value)
        rt.store.i32_n8(memory_at_0, str_ptr + str_len, 0)

        return str_ptr
    end

    function module.cstr_array(array: {string}): number
        local array_len = #array
        local array_ptr = cfns.malloc((array_len + 1) * 4)

        for idx = 0, (array_len - 1) do
            local str_val = array[idx + 1]
            local str_len = #str_val
            local str_ptr = cfns.malloc(str_len + 1)

            rt.store.string(memory_at_0, str_ptr, str_val)
            rt.store.i32_n8(memory_at_0, str_ptr + str_len, 0)
            rt.store.i32(memory_at_0, array_ptr + (idx * 4), str_ptr)
        end

        rt.store.i32(memory_at_0, array_ptr + (array_len * 4), 0)
        return array_ptr
    end

    function module.free_array(array_ptr: number)
        local position = 0

        while true do
            local str_ptr = rt.load.i32(array_ptr + position)
            if str_ptr == 0 then
                break
            end

            cfns.free(str_ptr)
            position += 4
        end
        cfns.free(array_ptr)
    end

    function module.newCException(excPtr: number)
        local CException = {}
        CException.ptr = excPtr

        function CException._UndecoratedName(self)
            local name_ptr = rt.load.i32(memory_at_0, self.ptr + 4)
            return rt.load.string(memory_at_0, name_ptr, cfns.strlen(name_ptr))
        end

        function CException.vfptr(self)
            return rt.load.i32(memory_at_0, self.ptr)
        end
        return CException
    end

    return module, function(_rt: any, _memory_at_0: any, _cfns: any, _INDIRECT_FUNCTIONS: any)
        rt, memory_at_0, cfns, INDIRECT_FUNCTIONS = _rt, _memory_at_0, _cfns, _INDIRECT_FUNCTIONS
    end
end


--// Vanilla Luau Vector3 implementation
--// Provides Vector3 lib if no existing implementation is available
--// Missing: xAxis, yAxis, zAxis, Magnitude, Unit, and all namecall functions
--// Last checked: 11 December 2024
local type = type
local typeof = typeof
local Vector3 = Vector3 or (if vector then { new = vector.create } else nil)
if not Vector3 then
    local Vector3Internal = {}
    Vector3Internal.__index = Vector3Internal
    Vector3Internal.__type = "Vector3"
    Vector3Internal.__metatable = "This metatable is locked"

    Vector3 = {}
    Vector3.new = function(x, y, z)
        return setmetatable({X = x or 0, Y = y or 0, Z = z or 0}, Vector3Internal)
    end
    Vector3.zero = Vector3.new(0,0,0)
    Vector3.one = Vector3.new(1,1,1)
    table.freeze(Vector3)

    Vector3Internal.__concat = function()
        assert(false, "attempt to concatenate Vector3 with Vector3")
        return
    end
    Vector3Internal.__unm = function(self)
        return Vector3.new(-self.X, -self.Y, -self.Z)
    end
    Vector3Internal.__add = function(self, v3_2)
        v3_2 = if (typeof(v3_2) == "number") then Vector3.new(v3_2, v3_2, v3_2) else v3_2
        return Vector3.new(self.X + v3_2.X, self.Y + v3_2.Y, self.Z + v3_2.Z)
    end
    Vector3Internal.__sub = function(self, v3_2)
        v3_2 = if (typeof(v3_2) == "number") then Vector3.new(v3_2, v3_2, v3_2) else v3_2
        return Vector3.new(self.X - v3_2.X, self.Y - v3_2.Y, self.Z - v3_2.Z)
    end
    Vector3Internal.__mul = function(self, v3_2)
        v3_2 = if (typeof(v3_2) == "number") then Vector3.new(v3_2, v3_2, v3_2) else v3_2
        return Vector3.new(self.X * v3_2.X, self.Y * v3_2.Y, self.Z * v3_2.Z)
    end
    Vector3Internal.__div = function(self, v3_2)
        v3_2 = if (typeof(v3_2) == "number") then Vector3.new(v3_2, v3_2, v3_2) else v3_2
        return Vector3.new(self.X / v3_2.X, self.Y / v3_2.Y, self.Z / v3_2.Z)
    end
    Vector3Internal.__mod = function(_, t)
        if (typeof(t) == "table") and (t["__type"] == "Vector3") then
            assert(false, "attempt to perform arithmetic (mod) on Vector3")
        else
            assert(false, "attempt to perform arithmetic (mod) on Vector3 and "..typeof(t))
        end
    end
    Vector3Internal.__pow = function(_, t)
        if (typeof(t) == "table") and (t["__type"] == "Vector3") then
            assert(false, "attempt to perform arithmetic (pow) on Vector3")
        else
            assert(false, "attempt to perform arithmetic (pow) on Vector3 and "..typeof(t))
        end
    end
    Vector3Internal.__tostring = function(self)
        return self.X..", "..self.Y..", "..self.Z
    end
    Vector3Internal.__eq = function(self, v3_2)
        if (typeof(v3_2) ~= "Vector3") then return false end
        return (self.X == v3_2.X) and (self.Y == v3_2.Y) and (self.Z == v3_2.Z)
    end
    Vector3Internal.__lt = function(self, v3_2)
        if (typeof(v3_2) ~= "Vector3") then return false end
        return (self.X < v3_2.X) and (self.Y < v3_2.Y) and (self.Z < v3_2.Z)
    end
    Vector3Internal.__le = function(self, v3_2)
        if (typeof(v3_2) ~= "Vector3") then return false end
        return (self.X <= v3_2.X) and (self.Y <= v3_2.Y) and (self.Z <= v3_2.Z)
    end
    Vector3Internal.__len = function()
        assert(false, "attempt to get length of a Vector3 value")
        return
    end

    local oldtype = type
    type = function(v)
        return if (oldtype(v) == "table") and v["__type"] then v["__type"] else oldtype(v)
    end

    local oldtypeof = typeof
    typeof = function(v)
        return if (oldtypeof(v) == "table") and v["__type"] then v["__type"] else oldtypeof(v)
    end
end


export type LuauCompileOptions = {
    OptimizationLevel: number,
    DebugLevel: number,
    TypeInfoLevel: number,
    CoverageLevel: number,

    VectorLibName: string?,
    VectorLibConstructor: string?,
    VectorType: string?,

    MutableGlobals: {string}?,
    DisabledBuiltins: {string}?,
}

local DEFAULT_LuauCompileOptions: LuauCompileOptions = {
    OptimizationLevel = 1,
    DebugLevel = 1,
    TypeInfoLevel = 0,
    CoverageLevel = 0,
}

return function(rt: any, memory_at_0: any, cfns: any, INDIRECT_FUNCTIONS: any, utils: typeof(require("../Common/utils")()))
    local module = {}

    local function construct_luau_CompileOptions(options: LuauCompileOptions): (number, () -> ())
        local compileOptions_ptr = cfns.malloc(52)
        local __freed = false

        local vectorLibName_ptr = if options.VectorLibName then utils.cstr(options.VectorLibName) else 0
        local vectorLibConstructor_ptr = if options.VectorLibConstructor then utils.cstr(options.VectorLibConstructor) else 0
        local vectorType_ptr = if options.VectorType then utils.cstr(options.VectorType) else 0

        local mutableGlobals_ptr = if options.MutableGlobals then utils.cstr_array(options.MutableGlobals) else 0
        local disabledBuiltins_ptr = if options.DisabledBuiltins then utils.cstr_array(options.DisabledBuiltins) else 0

        -- struct lua_CompileOptions (see luau/Compiler/include/luacode.h)
        rt.store.i32(memory_at_0, compileOptions_ptr + 0, options.OptimizationLevel) -- int optimizationLevel
        rt.store.i32(memory_at_0, compileOptions_ptr + 4, options.DebugLevel) -- int debugLevel
        rt.store.i32(memory_at_0, compileOptions_ptr + 8, options.TypeInfoLevel) -- int typeInfoLevel
        rt.store.i32(memory_at_0, compileOptions_ptr + 12, options.CoverageLevel) -- int coverageLevel
        rt.store.i32(memory_at_0, compileOptions_ptr + 16, vectorLibName_ptr) -- const char* vectorLib
        rt.store.i32(memory_at_0, compileOptions_ptr + 20, vectorLibConstructor_ptr) -- const char* vectorCtor
        rt.store.i32(memory_at_0, compileOptions_ptr + 24, vectorType_ptr) -- const char* vectorType
        rt.store.i32(memory_at_0, compileOptions_ptr + 28, mutableGlobals_ptr) -- const char* const* mutableGlobals
        rt.store.i32(memory_at_0, compileOptions_ptr + 32, 0) -- const char* const* userdataTypes
        rt.store.i32(memory_at_0, compileOptions_ptr + 36, 0) -- const char* const* librariesWithKnownMembers
        rt.store.i32(memory_at_0, compileOptions_ptr + 40, 0) -- lua_LibraryMemberTypeCallback libraryMemberTypeCb
        rt.store.i32(memory_at_0, compileOptions_ptr + 44, 0) -- lua_LibraryMemberConstantCallback libraryMemberConstantCb
        rt.store.i32(memory_at_0, compileOptions_ptr + 48, disabledBuiltins_ptr) -- const char* const* disabledBuiltins

        return compileOptions_ptr, function()
            if __freed then
                error("Already freed!")
            end
            __freed = true

            cfns.free(compileOptions_ptr)
            if vectorLibName_ptr ~= 0 then cfns.free(vectorLibName_ptr) end
            if vectorLibConstructor_ptr ~= 0 then cfns.free(vectorLibConstructor_ptr) end
            if vectorType_ptr ~= 0 then cfns.free(vectorType_ptr) end
            if disabledBuiltins_ptr ~= 0 then cfns.free(disabledBuiltins_ptr) end
            if mutableGlobals_ptr ~= 0 then cfns.free(mutableGlobals_ptr) end
        end
    end

    function module.luau_compile(source: string, options: LuauCompileOptions?): (string, number)
        -- load source and luau_CompileOptions
        local luau_CompileOptions_ptr, compileOptions_free = construct_luau_CompileOptions(options or DEFAULT_LuauCompileOptions)
        local source_ptr = utils.cstr(source)

        -- allocate for getting bytecode size
        local bytecodeSize_ptr = cfns.malloc(8)

        -- compile
        local bytecode_ptr = cfns.luau_compile(source_ptr, #source, luau_CompileOptions_ptr, bytecodeSize_ptr)
        local bytecodeSize = rt.load.i32(memory_at_0, bytecodeSize_ptr)
        local bytecode = rt.load.string(memory_at_0, bytecode_ptr, bytecodeSize)

        -- Cleanup
        compileOptions_free()
        cfns.free(source_ptr)
        cfns.free(bytecode_ptr)
        cfns.free(bytecodeSize_ptr)
        return bytecode, bytecodeSize
    end
    return module
end


local api_flags_create = require("../Common/api_flags")
local runtime_create = require("../Common/runtime_create")
local compiler_common_init = require("../Compiler/common")
export type LuauCompileOptions = compiler_common_init.LuauCompileOptions

local rt, memory_at_0, cfns, INDIRECT_FUNCTIONS, utils = runtime_create(require("../Common/polyfill"))
local api_flags = api_flags_create(rt, memory_at_0, cfns, INDIRECT_FUNCTIONS, utils)
local compiler_common = compiler_common_init(rt, memory_at_0, cfns, INDIRECT_FUNCTIONS, utils)

return {
    luau_compile = compiler_common.luau_compile,
    luau_setflag = api_flags.luau_setflag,
    luau_setallflags = api_flags.luau_setallflags,
    luau_resetflags = api_flags.luau_resetflags,
    wasm = {
        rt = rt,
        cfns = cfns,
        indirect_function_table = INDIRECT_FUNCTIONS,
        memory = memory_at_0,
        insertCFunction = utils.insertCFunction,
        cstr = utils.cstr,
    }
}


local api_flags_create = require("../Common/api_flags")
local runtime_create = require("../Common/runtime_create")
local compiler_common_init = require("../Compiler/common")
local vm_common_init = require("../VM/common")
export type LuauCompileOptions = compiler_common_init.LuauCompileOptions

local rt, memory_at_0, cfns, INDIRECT_FUNCTIONS, utils = runtime_create(require("../VM/polyfill"))
local api_flags = api_flags_create(rt, memory_at_0, cfns, INDIRECT_FUNCTIONS, utils)
local compiler_common = compiler_common_init(rt, memory_at_0, cfns, INDIRECT_FUNCTIONS, utils)
local vm_common = vm_common_init(rt, memory_at_0, cfns, INDIRECT_FUNCTIONS, utils)

return {
    luau_compile = compiler_common.luau_compile,
    luau_run = vm_common.luau_run,
    luau_load = vm_common.luau_load,
    luau_setflag = api_flags.luau_setflag,
    luau_setallflags = api_flags.luau_setallflags,
    luau_resetflags = api_flags.luau_resetflags,
    macros = require("../VM/luau_macros")(rt, memory_at_0, cfns, INDIRECT_FUNCTIONS, utils),
    wasm = {
        rt = rt,
        cfns = cfns,
        indirect_function_table = INDIRECT_FUNCTIONS,
        memory = memory_at_0,
        insertCFunction = utils.insertCFunction,
        cstr = utils.cstr,
    }
}


return function(rt: any, memory_at_0: any, cfns: any, INDIRECT_FUNCTIONS: any, utils: typeof(require("../Common/utils")()))
    local module = {}

    function module.luau_run(
        bytecode: string,
        chunkName: string?,
        safeEnv: boolean?
    ): boolean
        -- load bytecode and chunkName to memory
        local bytecode_ptr = utils.cstr(bytecode)
        local chunkName_ptr = utils.cstr(chunkName or "LuauCeptionChunk")

        -- setup Luau state
        local L = cfns.luaL_newstate()
        cfns.luaL_openlibs(L);
        if safeEnv then
            cfns.luaL_sandbox(L)
        end

        -- load bytecode
        local load_result = cfns.luau_load(L, chunkName_ptr, bytecode_ptr, #bytecode, 0)

        -- cleanup unused stuff
        cfns.free(bytecode_ptr)
        cfns.free(chunkName_ptr)

        -- check if load succeded
        if (load_result ~= 0) then
            cfns.lua_close(L)
        end
        assert(load_result == 0, "failed to load luau bytecode")

        -- call function
        local clRes = cfns.lua_pcall(L, 0, 0, 0)

        -- cleanup state
        cfns.lua_close(L)
        return clRes == 0
    end

    function module.luau_load(
        bytecode: string,
        chunkName: string?,
        safeEnv: boolean?
    ): () -> (boolean)
        return function()
            return module.luau_run(bytecode, chunkName, safeEnv)
        end
    end

    return module
end