local H={}H.__index=H function H.new(V)if V~=nil and type(V)~="\116\097\098\108\101"then error("\067\111\108\108\101\099\116\105\111\110\046\110\101\119\040\041\058 \096\101\110\116\114\105\101\115\096 \109\117\115\116 \098\101 \097 \116\097\098\108\101 \119\104\101\110 \115\112\101\099\105\102\105\101\100")end local b=setmetatable({},H)b.size=function()return#b._storage end b._storage={}if V then for H,V in ipairs(V)do b:set(V[1],V[2])end end return b end function H.size(H)return#H._storage end function H.keys(H)local V={}for H,b in ipairs(H._storage)do table.insert(V,b[1])end return V end function H.values(H)local V={}for H,b in ipairs(H._storage)do table.insert(V,b[2])end return V end function H.has(V,H)if type(H)~="\115\116\114\105\110\103"then error("\067\111\108\108\101\099\116\105\111\110\046\104\097\115\040\041\058 \096\107\101\121\096 \109\117\115\116 \098\101 \097 \115\116\114\105\110\103")end for V,b in ipairs(V._storage)do if b[1]==H then return true end end return false end function H.get(V,H)if type(H)~="\115\116\114\105\110\103"then error("\067\111\108\108\101\099\116\105\111\110\046\103\101\116\040\041\058 \096\107\101\121\096 \109\117\115\116 \098\101 \097 \115\116\114\105\110\103")end for V,b in ipairs(V._storage)do if b[1]==H then return b[2]end end return nil end function H.set(b,H,V)if type(H)~="\115\116\114\105\110\103"then error("\067\111\108\108\101\099\116\105\111\110\046\115\101\116\040\041\058 \096\107\101\121\096 \109\117\115\116 \098\101 \097 \115\116\114\105\110\103")end for c,y in ipairs(b._storage)do if y[1]==H then b._storage[c]={H,V}return b end end table.insert(b._storage,{H,V})return b end function H.ensure(b,H,V)if type(H)~="\115\116\114\105\110\103"then error("\067\111\108\108\101\099\116\105\111\110\046\101\110\115\117\114\101\040\041\058 \096\107\101\121\096 \109\117\115\116 \098\101 \097 \115\116\114\105\110\103")end if type(V)~="\102\117\110\099\116\105\111\110"then error("\067\111\108\108\101\099\116\105\111\110\046\101\110\115\117\114\101\040\041\058 \096\100\101\102\097\117\108\116\086\097\108\117\101\071\101\110\101\114\097\116\111\114\096 \109\117\115\116 \098\101 \097 \102\117\110\099\116\105\111\110")end if b:has(H)then return b:get(H)end local c=V(H,b)b:set(H,c)return c end function H.delete(V,H)if type(H)~="\115\116\114\105\110\103"then error("\067\111\108\108\101\099\116\105\111\110\046\100\101\108\101\116\101\040\041\058 \096\107\101\121\096 \109\117\115\116 \098\101 \097 \115\116\114\105\110\103")end for b,c in ipairs(V._storage)do if c[1]==H then table.remove(V._storage,b)return true end end return false end function H.clear(H)H._storage={}return H end function H.forEach(V,H)if type(H)~="\102\117\110\099\116\105\111\110"then error("\067\111\108\108\101\099\116\105\111\110\046\102\111\114\069\097\099\104\040\041\058 \096\099\097\108\108\098\097\099\107\096 \109\117\115\116 \098\101 \097 \102\117\110\099\116\105\111\110")end for b,c in ipairs(V._storage)do H(c[2],c[1],V)end return V end function H.map(V,H)if type(H)~="\102\117\110\099\116\105\111\110"then error("\067\111\108\108\101\099\116\105\111\110\046\109\097\112\040\041\058 \096\109\097\112\070\117\110\099\116\105\111\110\096 \109\117\115\116 \098\101 \097 \102\117\110\099\116\105\111\110")end local b={}for c,y in ipairs(V._storage)do table.insert(b,H(y[2],y[1],V))end return b end function H.reduce(b,H,V)if type(H)~="\102\117\110\099\116\105\111\110"then error("\067\111\108\108\101\099\116\105\111\110\046\114\101\100\117\099\101\040\041\058 \096\114\101\100\117\099\101\070\117\110\099\116\105\111\110\096 \109\117\115\116 \098\101 \097 \102\117\110\099\116\105\111\110")end local c=V or nil for V,y in ipairs(b._storage)do c=c==nil and y[2]or H(c,y[2],y[1],b)end return c end function H.sort(V,H)if type(H)~="\102\117\110\099\116\105\111\110"then error("\067\111\108\108\101\099\116\105\111\110\046\115\111\114\116\040\041\058 \096\099\111\109\112\097\114\101\070\117\110\099\116\105\111\110\096 \109\117\115\116 \098\101 \097 \102\117\110\099\116\105\111\110")end table.sort(V._storage,function(b,c)return H(b[2],c[2],b[1],c[1],V)end)return V end function H.find(V,H)if type(H)~="\102\117\110\099\116\105\111\110"then error("\067\111\108\108\101\099\116\105\111\110\046\102\105\110\100\040\041\058 \096\099\097\108\108\098\097\099\107\096 \109\117\115\116 \098\101 \097 \102\117\110\099\116\105\111\110")end for b,c in ipairs(V._storage)do if H(c[2],c[1],V)then return c[2]end end return nil end function H.filter(b,V)if type(V)~="\102\117\110\099\116\105\111\110"then error("\067\111\108\108\101\099\116\105\111\110\046\102\105\108\116\101\114\040\041\058 \096\099\097\108\108\098\097\099\107\096 \109\117\115\116 \098\101 \097 \102\117\110\099\116\105\111\110")end local c=H.new()for H,y in ipairs(b._storage)do if V(y[2],y[1],b)then c:set(y[1],y[2])end end return c end function H.concat(b,V)if type(V)~="\116\097\098\108\101"then error("\067\111\108\108\101\099\116\105\111\110\046\099\111\110\099\097\116\040\041\058 \096\099\111\108\108\101\099\116\105\111\110\096 \109\117\115\116 \098\101 \097 \116\097\098\108\101")end local c=H.new()for H,V in ipairs(b._storage)do c:set(V[1],V[2])end for H,V in ipairs(V)do c:set(V[1],V[2])end return c end return H
--------------------------------------------------------------------------------------------------------------------------------
--[[                                             Copyright (C) Inertia Lighting                                             ]]--
--------------------------------------------------------------------------------------------------------------------------------

local Collection = {}
Collection.__index = Collection

--------------------------------------------------------------------------------------------------------------------------------

--- @param entries table
--- @return table (new collection)
--- @example
--- ```lua
--- local collection = Collection.new({ { 'nice', 69 }, { 'thing', 420 } })
--- print(collection:size()) -- 2
--- print(collection:get('nice')) -- 69
--- ```
function Collection.new(entries)
    if (entries ~= nil) and (type(entries) ~= 'table') then
        error('Collection.new(): `entries` must be a table when specified')
    end

    local self = setmetatable({}, Collection)

    self.size = function()
        return #self._storage
    end

    self._storage = {} -- { [number]: { [key: string], [value: any] } }

    if entries then
        for _, entry in ipairs(entries) do
            self:set(entry[1], entry[2])
        end
    end

    return self
end

--------------------------------------------------------------------------------------------------------------------------------

--- @return number
--- @example
--- ```lua
--- local collection = Collection.new({ { 'hello', 'world' }, { 'lorem', 'ipsum' } })
--- print(collection:size()) -- 2
--- ```
function Collection:size()
    return #self._storage
end

--- @return table keys
--- @example
--- ```lua
--- local collection = Collection.new({ { 'this', 'is' }, { 'a', 'test' }, { 'collection', 'of' }, { 'keys', 'and' }, { 'values', '!' } })
--- print(collection:keys()) -- { 'this', 'a', 'collection', 'keys', 'values' }
--- ```
function Collection:keys()
    local keys = {}

    for _, entry in ipairs(self._storage) do
        table.insert(keys, entry[1])
    end

    return keys
end

--- @return table values
--- @example
--- ```lua
--- local collection = Collection.new({ { 'this', 'is' }, { 'a', 'test' }, { 'collection', 'of' }, { 'keys', 'and' }, { 'values', '!' } })
--- print(collection:values()) -- { 'is', 'test', 'of', 'and', '!' }
--- ```
function Collection:values()
    local values = {}

    for _, entry in ipairs(self._storage) do
        table.insert(values, entry[2])
    end

    return values
end

--------------------------------------------------------------------------------------------------------------------------------

--- @param key string
--- @return boolean exists
--- @example
--- ```lua
--- local collection = Collection.new({ { 'iron', 'FE' }, { 'gold', 'AU' } })
--- print(collection:has('iron')) -- true
--- print(collection:has('platinum')) -- false
--- ```
function Collection:has(key)
    if type(key) ~= 'string' then
        error('Collection.has(): `key` must be a string')
    end

    for _, entry in ipairs(self._storage) do
        if entry[1] == key then
            return true
        end
    end

    return false
end

--- @param key string
--- @return any value (defaults to `nil` if not found)
--- @example
--- ```lua
--- local collection = Collection.new({ { 'apple', 'red' }, { 'banana', 'yellow' }, { 'blueberry', 'blue' } })
--- print(collection:get('apple')) -- red
--- print(collection:get('banana')) -- yellow
--- print(collection:get('blueberry')) -- blue
--- print(collection:get('orange')) -- nil
--- ```
function Collection:get(key)
    if type(key) ~= 'string' then
        error('Collection.get(): `key` must be a string')
    end

    for _, entry in ipairs(self._storage) do
        if entry[1] == key then
            return entry[2]
        end
    end

    return nil
end

--- @param key string
--- @param value any
--- @return table self
--- @example
--- ```lua
--- local collection = Collection.new()
--- print(collection:get('nice')) -- nil
--- collection:set('nice', 69)
--- print(collection:get('nice')) -- 69
--- ```
function Collection:set(key, value)
    if type(key) ~= 'string' then
        error('Collection.set(): `key` must be a string')
    end

    for index, entry in ipairs(self._storage) do
        if entry[1] == key then
            self._storage[index] = { key, value }

            return self
        end
    end

    table.insert(self._storage, { key, value })

    return self
end

--- Ensures that the collection has a value for the specified key.
--- @param key string
--- @param defaultValueGenerator function `(key, collection) => defaultValue`
--- @return any value
--- @example
--- ```lua
--- local collection = Collection.new()
--- collection:ensure('nice', function(key, collection)
---     return 69
--- end)
--- print(collection:get('nice')) -- 69
--- ```
function Collection:ensure(key, defaultValueGenerator)
    if type(key) ~= 'string' then
        error('Collection.ensure(): `key` must be a string')
    end

    if type(defaultValueGenerator) ~= 'function' then
        error('Collection.ensure(): `defaultValueGenerator` must be a function')
    end

    if self:has(key) then
        return self:get(key)
    end

    local defaultValue = defaultValueGenerator(key, self)
    self:set(key, defaultValue)
    return defaultValue
end

--- @param key string
--- @return boolean deleted if the key was found and removed
--- @example
--- ```lua
--- local collection = Collection.new({ { 'nice', 69 }, { 'thing', 420 } })
--- print(collection:size()) -- 2
--- print(collection:delete('nice')) -- true
--- print(collection:size()) -- 1
--- print(collection:delete('nice')) -- false
--- ```
function Collection:delete(key)
    if type(key) ~= 'string' then
        error('Collection.delete(): `key` must be a string')
    end

    for index, entry in ipairs(self._storage) do
        if entry[1] == key then
            table.remove(self._storage, index)

            return true
        end
    end

    return false
end

--- @return table self
--- @example
--- ```lua
--- local collection = Collection.new({ { 'nice', 69 }, { 'thing', 420 } })
--- print(collection:size()) -- 2
--- collection:clear() -- Collection
--- print(collection:size()) -- 0
function Collection:clear()
    self._storage = {}

    return self
end

--- @param callback function `(value, key, tbl) => void`
--- @return table self
--- @example
--- ```lua
--- local collection = Collection.new({ { 'nice', 69 }, { 'thing', 420 } })
--- collection:forEach(function(value, key, tbl)
---     print(key, value)
--- end)
--- -- nice 69
--- -- thing 420
--- ```
function Collection:forEach(callback)
    if type(callback) ~= 'function' then
        error('Collection.forEach(): `callback` must be a function')
    end

    for _, entry in ipairs(self._storage) do
        callback(entry[2], entry[1], self)
    end

    return self
end

--- @param mapFunction function `(value, key, collection) => newValue`
--- @return table mapped contains the results of the map function
--- @example
--- ```lua
--- local collection = Collection.new({ { 'd1', '231' }, { 'd2', '123' }, { 'd3', '456' } })
--- local mapped = collection:map(function(value)
---     return string.format('%s: %s', 'data-sample-', value)
--- end)
--- print(mapped) -- { 'data-sample-231', 'data-sample-123', 'data-sample-456' }
--- ```
function Collection:map(mapFunction)
    if type(mapFunction) ~= 'function' then
        error('Collection.map(): `mapFunction` must be a function')
    end

    local mapped = {}

    for _, entry in ipairs(self._storage) do
        table.insert(mapped, mapFunction(entry[2], entry[1], self))
    end

    return mapped
end

--- @param reduceFunction function `(accumulator, value, key, collection) => modifiedAccumulator`
--- @param initialValue any (optional, defaults to `nil`)
--- @example
--- ```lua
--- local collection = Collection.new({ { 'nice', 69 }, { 'thing', 420 } })
--- local sum = collection:reduce(function(accumulator, value)
---     return accumulator + value
--- end, 0)
--- print(sum) -- 489
--- ```
function Collection:reduce(reduceFunction, initialValue)
    if type(reduceFunction) ~= 'function' then
        error('Collection.reduce(): `reduceFunction` must be a function')
    end

    local accumulator = initialValue or nil

    for _, entry in ipairs(self._storage) do
        accumulator = (accumulator == nil) and entry[2] or reduceFunction(accumulator, entry[2], entry[1], self)
    end

    return accumulator
end

--- @param compareFunction function `(firstValue, secondValue, firstKey, secondKey, collection) => boolean`
--- @example
--- ```lua
--- local collection = Collection.new({ { 'a', 4 }, { 'b', 2 }, { 'c', 5 }, { 'd', 1 }, { 'e', 3 } })
--- collection:sort(function(firstValue, secondValue)
---     return firstValue < secondValue
--- end)
--- ```
function Collection:sort(compareFunction)
    if type(compareFunction) ~= 'function' then
        error('Collection.sort(): `compareFunction` must be a function')
    end

    table.sort(self._storage, function(a, b)
        return compareFunction(a[2], b[2], a[1], b[1], self)
    end)

    return self
end

--- @param callback function `(value, key, collection) => any`
--- @return any value
--- @example
--- ```lua
--- local collection = Collection.new({
---     { '13123123', { name = 'John', age = 23 } },
---     { '12312312', { name = 'Jane', age = 21 } },
---     { '12312312', { name = 'Jack', age = 25 } },
--- })
--- local personNamedJohn = collection:find(function(value)
---     return value.name == 'John'
--- end)
--- print(personNamedJohn.age) -- 23
--- ```
function Collection:find(callback)
    if type(callback) ~= 'function' then
        error('Collection.find(): `callback` must be a function')
    end

    for _, entry in ipairs(self._storage) do
        if callback(entry[2], entry[1], self) then
            return entry[2]
        end
    end

    return nil
end

--- @param callback function `(value, key, collection) => new Collection`
--- @return table filtered collection contains the results of the filter function
--- @example
--- ```lua
--- local collection = Collection.new({ { 'nice', 69 }, { 'thing', 420 } })
--- local filtered = collection:filter(function(value)
---     return value > 100
--- end)
--- print(filtered) -- { { 'thing', 420 } }
--- ```
function Collection:filter(callback)
    if type(callback) ~= 'function' then
        error('Collection.filter(): `callback` must be a function')
    end

    local filtered = Collection.new()

    for _, entry in ipairs(self._storage) do
        if callback(entry[2], entry[1], self) then
            filtered:set(entry[1], entry[2])
        end
    end

    return filtered
end

--- @param collection table
--- @return table concatenatedCollection
--- @example
--- ```lua
--- local collection = Collection.new({ { 'nice', 69 }, { 'thing', 420 } })
--- local otherCollection = Collection.new({ { 'hello', 'world' }, { 'goodbye', 'moon' } })
--- local concatenatedCollection = collection:concat(otherCollection)
--- print(concatenatedCollection:size()) -- 4
--- ```
function Collection:concat(collection)
    if type(collection) ~= 'table' then
        error('Collection.concat(): `collection` must be a table')
    end

    local concatenatedCollection = Collection.new()

    for _, entry in ipairs(self._storage) do
        concatenatedCollection:set(entry[1], entry[2])
    end

    for _, entry in ipairs(collection) do
        concatenatedCollection:set(entry[1], entry[2])
    end

    return concatenatedCollection
end

--------------------------------------------------------------------------------------------------------------------------------

return Collection
