util.misc = {}

--- creates a deep copy of a table
---
--- INFO: if you provide a non table value, the function will return the value unmodiefied
--- @param table table
--- @return table
function util.misc.deepCopy(table)
    -- not a table => return a val
    if type(table) ~= "table" then return table end

    -- a table => for ech value recursively call table
    local subTable = {}
    for k, v in pairs(table) do
        subTable[util.misc.deepCopy(k)] =  util.misc.deepCopy(v)
    end

    return(subTable)
end

--- shuffles an Array table
--- @param arr any[] table to be shuffled
--- @param i? number start shuffel index. Defaults to 1
--- @param j? number end shuffel index. Defaults to lenth of supplied table
--- @param inplace? boolean shuffeling should be done inplace or not. Defaults to true
--- @return any[]
function util.misc.shuffle(arr, i, j, inplace)
    if type(arr) ~= "table" then error(util.error.argErr(arr, "table", 1)) end

    i, j = i or 1, j or #arr
    inplace = inplace or true

    if not inplace then arr = util.misc.deepCopy(arr) end

    for a = i, j do
        local b = math.random(i, j)
        arr[a], arr[b] = arr[b], arr[a]
    end

    return arr
end

--- reverses a given array table
--- @param arr any[] input array
--- @param inplace? boolean array gets copied at the beginning of the function
--- @return any[]
function util.misc.reverse(arr, inplace)
    if type(arr) ~= "table" then error(util.error.argErr(arr, "table", 1)) end

    if not inplace then arr = util.misc.deepCopy(arr) end

    -- set i to end and j to beginning of array
    local i, j = #arr, 1

    -- periodically swap i and j until we reched the middle of our table
    while (i > j) do
        arr[i], arr[j] = arr[j], arr[i]
        i, j = i - 1, j + 1
    end

    return arr
end

--- combines two arrays into 1 table with arr1[i] = key and arr2[i] = val
--- @param arr1 any[]
--- @param arr2 any[]
--- @return table
function util.misc.combine(arr1, arr2)
    if type(arr1) ~= "table" then error(util.error.argErr(arr1, "table", 1)) end
    if type(arr2) ~= "table" then error(util.error.argErr(arr2, "table", 2)) end

    local com = {}

    for i, key in pairs(arr1) do com[key] = arr2[i] end

    return com
end

--- returns true if all in table meet predefined lambda condition
--- @param table table
--- @param func fun(val:any, key:any):boolean function you want to use to compare. Defaults to identety v and k can be interpreted as true
--- @return boolean
function util.misc.all(table, func)
    if type(table) ~= "table" then error(util.error.argErr(table, "table", 1)) end
    func = type(func) == "function" and func or function(v, k) return v and k end

    for k, v in pairs(table) do
        if not func(v, k) then return false end
    end

    return true
end

--- returns true if any in table meet predefined lambda condition
--- @param table table
--- @param func fun(val:any, key:any):boolean function you want to use to compare
--- @return boolean
function util.misc.any(table, func)
    if type(table) ~= "table" then error(util.error.argErr(table, "table", 1)) end
    func = type(func) == "function" and func or function(v, k) return v or k end

    for k, v in pairs(table) do
        if func(v, k) then return false end
    end

    return false
end

--- returns a filterd version of input array. All values where lambda equates to true will be includedsy
--- @param table table
--- @param func fun(val:any, key:any):boolean function you want to use to compare
--- @param isTable boolean 
--- @return boolean|nil, nil|string
function util.misc.filter(table, func, isTable)
    if type(table) ~= "table" then error(util.error.argErr(table, "table", 1)) end
    func = type(func) == "function" and func or function(v, k) return true end

    local filterd = {}

    for k,v in pairs(table) do
        if func(v, k) then filterd[isTable and k or (#filterd+1)] = v end
    end

    return filterd
end

--- mappes each value / key of a given table to a value / key retuned by the provided lambda funciton
--- @param table table input table
--- @param func fun(val:any, key:any):any,any assignment lambda function
--- @return table
function util.misc.map(table, func)
    if type(table) ~= "table" then error(util.error.argErr(table, "table", 1)) end
    func = type(func) == "function" and func or function(v, k) return true end

    local mapped, i = {}, 1

    for k, v in pairs(table) do
        local vnew, knew = func(v, k, i)
        mapped[knew or k], i = vnew, i+1
    end

    return mapped
end

--- flattens a table down. The supplied function controls the new key name
--- @param table table input table (does not get modified)
--- @param func function function used to generate new key value in form 'function(parentKey, childKey) return end'
--- @param depth integer flatting depth. If this param is set to any negative value, the table get flattened down until no further sub tables exist.
--- @param map table adding a table poiter as key and setting value to true will exlude this subtable from flatting algorithm. Defaults to table param to prevent cyclic references
--- @return table flattened
function util.misc.flat(table, func, depth, map)
    -- add original table to already visited
    map = map or { [table] = true }

    -- return if depth reached
    if depth == 0 then return(table) end

    local buffer, flat = {}, true

    -- go through all values in table
    for k, v in pairs(table) do
        if type(v) ~= "table" then
            buffer[k] = v

        -- flatten out by 1 level if the table wasnt visited yet
        elseif not map[tostring(v)] then
            -- mark table as visited and set not yet flat flag
            map[tostring(v)] = true
            flat = false

            -- insert into flattened buffer table with new key value (see func)
            for ik, iv in pairs(v) do
                buffer[func(k, ik)] = iv
            end
        end
    end

    -- generate new depth and flatten down further
    local newDepth = depth < 0 and depth or depth-1
    return(util.misc.flat(buffer, func, flat and 0 or newDepth, map))
end

--- appends a2 to the end of a1
--- @param a1 any[] original array (modified inplace)
--- @param a2 any[]
--- @param isTable boolean if true the function treats arrays as tables. This means instead of incementing indecees it just joins the tables.
--- WARNING: identical keys in a1 and a2 will replace each other. In this case a2 overwrites a1
--- @return any[] (Pointer) to a1
function util.misc.append(a1, a2, isTable)
    if type(a1) ~= "table" then error(util.error.argErr(a1, "table", 1)) end
    if type(a2) ~= "table" then error(util.error.argErr(a2, "table", 2)) end

    -- individually appended every val of t2 to t1
    local iterate = isTable and pairs or ipairs

    for key, val in iterate(a2) do
        a1[isTable and key or #a1+1] = val
    end

    return a1
end

--- genearates a keyset out of a table
--- @param table table
--- @return any[] @key array
function util.misc.getKeys(table)
    if type(table) ~= "table" then error(util.error.argErr(table, "table", 1)) end
    local keys = {}

    for k, _v in pairs(table) do keys[#keys+1] = k end

    return keys
end

--- genearates a valueset out of a table
--- @param table table
--- @return any[] @value array
function util.misc.getValues(table)
    if type(table) ~= "table" then error(util.error.argErr(table, "table", 1)) end

    local vals = {}

    for _k, v in pairs(table) do vals[#vals+1] = v end

    return vals
end

--- attempts to find the first key, with matching value inside a table
--- @param table table
--- @param val any (has to implement == operator)
--- @return any|nil @found value or nil if nothing was found
function util.misc.find(table, val)
    if type(table) ~= "table" then error(util.error.argErr(table, "table", 1)) end

    for k, v in pairs(table) do
        if v == val then return k end
    end

    return nil
end

--- attempts to find the first key + path, with matching value inside a nested table. This implementation is also known as Depth-first search
---
--- WARNING: this function works via recursion. Be carefull with greater depth values (dont really trust lua with tail recursion)
--- @param table table nested input table. Does not get modified
--- @param val any search value. The value type as well as its counterpart (in table) have to implement the == operator
--- @param key any search key. val must be the value of this key otherwise the result is invalid (used to search key value pairs). If not supplied every key is possible
--- @param depth number maximum search depth. If not provided --> risk of infinit loops
--- @return any[]|nil@path to the value. Because of the recusive nature the path table will be reversed
function util.misc.recFind(table, val, key, depth)
    depth = depth or 100

    -- we reached a compareabel value / max depth => check if value is searched one
    if type(table) ~= "table" or depth <= 0 then
        if table == val then

            -- we have to return a table here, so we can append the path further down the stack
            return ({ table })
        end

        return nil
    end

    -- for each table go furthe in the attempt to find again
    for k, v in pairs(table) do
        -- go further in the depth (Depth-first search)
        local path = (key == nil or k == key or type(v) == "table") and util.misc.recFind(v, val, key, depth - 1) or nil

        -- if the path is not nil, our search value was found further down => append to path and pop function from stack
        if path ~= nil then
            path[#path+1] = k
            return(path)
        end
    end

    return nil
end

--- attempts to find a specified value (unique) and memorizes all paths to prefent cyclic references
---
--- INFO: this function is intended to search the _G table for function names but could theoretically used for different purposes
--- @param table table nested input table. Does not get modified
--- @param val any search value. The value type as well as its counterpart (in table) have to implement the == operator
--- @param map table array of already visited branches. Defaults to _G since this is the intended use of this function
--- @return any[]|nil@path to the value. Because of the recusive nature the path table will be reversed
function util.misc.memFind(table, val, map)
    map = map or { [tostring(table)] = true }

    -- we reached a compareabel value / max depth => check if value is searched one
    if type(table) ~= "table" then
        if table == val then   
            -- we have to return a table here, so we can append the path further down the stack
            return ({ table })
        end

        return nil
    end

    -- for each table go furthe in the attempt to find again
    for k, v in pairs(table) do
        local path = nil

        if (not map[tostring(v)]) then
            -- save branch as visited and recursively call
            map[tostring(v)] = true
            path = util.misc.memFind(v, val, map)
        end

        -- if the path is not nil, our search value was found further down => append to path and pop function from stack
        if path ~= nil then
            path[#path+1] = k
            return(path)
        end
    end

    return nil
end

--- splits a string by the given delimiter. All delimiters are removed from the results
---
--- INFO: this implementation returns an empty string if fead with one
--- @param str string string you want to split
--- @param del string delimiter
--- @return string[] @{ [i] = string ... }
function util.misc.split(str, del)
    if type(str) ~= "string" then error(util.error.argErr(str, "string", 1)) end
    if type(del) ~= "string" then error(util.error.argErr(del, "string", 2)) end

    local arr = {}
    local i, j = 1, str:find(del) -- i = start delimiter, j = stop delimiter

    -- check if first character is a del then skip
    if i == j then i, j = j + 1, str:find(del, j + 1) end

    while j ~= nil and j ~= #str do
        arr[#arr + 1] = str:sub(i, j - 1) -- split string from start to stop
        i, j = j + 1, str:find(del, j + 1) -- set start to prev stop and find new stop (+1 to not include del)
    end

    -- check if search abortet because nil (no del found) or string end (last char is del)
    if j == #str then
        arr[#arr + 1] = str:sub(i, #str - 1)
    elseif j == nil then
        arr[#arr + 1] = str:sub(i, #str)
    end

    return arr
end

--- joins all elements in array into one string delimited by given delimiter
--- @param array any[] input array (should implement tostring())
--- @param del any delimiter (should implement tostring())
--- @return string
function util.misc.join(array, del)
    if type(array) ~= "table" then error(util.error.argErr(array, "table", 1)) end
    del = type(del) == "string" and del or ","
    local str = ""

    -- only append delimiter if index > 1 (not beginning)
    for i, val in ipairs(array) do
        str = str .. (i > 1 and tostring(del) or "") .. tostring(val)
    end

    return str
end

--- fills a given array with values.
--- If the given table already contains values this function appends the filled values at the end and does not modify any other content
---
--- INFO: this function works inplace, but generates a empty tabel if no value is supplied so assignment is adviced
--- @param table? any[] input array. If nil defaults to {}
--- @param value? any fill value
--- @param amount number number of items to be filled
--- @return table @either new table (if input nil) otherwise pointer to original table.
function util.misc.fill(table, value, amount)
    if not tonumber(amount) then error(util.error.argErr(amount, "number", 1)) end

    -- better save then sorry
    table = type(table) == "table" and  table or {}

    if type(value == "function") then
        -- fill function generated (changing) values
        for i=1, amount do
            table[#table+1] = value(i)
        end
    else
        -- fill with values
        for i=1, amount do
            table[#table+1] = value
        end
    end

    return table
end

--- used to generate a sub array between i and j form original
--- @param array any[] input array
--- @param i integer start index
--- @param j integer stop index
--- @return any[] #sub array between i and j
function util.misc.sub(array, i, j)
    if type(array) ~= "table" then error(util.error.argErr(array, "table", 1)) end
    i, j = i or 1, j or #array

    local buff = {}

    for a=i, j do
        buff[#buff+1] = array[a]
    end

    return buff
end

--- returns a index, value tuple of the minimum value inside the array
--- @param arr any[] input arr
--- @param i? number start index
--- @param j? number stop index
--- @return number, number @index, value
function util.misc.getMinIndex(arr, i, j)
    if type(arr) ~= "table" then error(util.error.argErr(arr, "table", 1)) end

    i, j = i or 1, j or #arr
    local minIndex = i

    for currIndex=i, j do
        minIndex = (arr[minIndex] < arr[currIndex]) and minIndex or currIndex
    end

    return minIndex, arr[minIndex]
end

--- converts an array of strings to lowercase
--- @param arr string[] input array
--- @param inplace? boolean if false the array gets deepcopied before convertion. Defauts to true
--- @return string[]
function util.misc.arrLower(arr, inplace)
    if type(arr) ~= "table" then error(util.error.argErr(arr, "table", 1)) end

    -- deepcopy if needed
    arr = not inplace and util.misc.deepCopy(arr) or arr

    for i in ipairs(arr) do
        arr[i] = tostring(arr[i]):lower()
    end

    return arr

end