local A={}function A.starts_with(A,m)return A:sub(1,#m)==m end function A.ends_with(A,m)return m==""or A:sub(-(#m))==m end function A.fname(A)return A:match("\040\091\094\047\093\043\041\036")end function A.is_whitespace_or_nil(A)if not A then return true end return A:match("\094\037\115\042\036")end function A.path_combine(...)local m={...}local Y=m[1]for x=2,#m,1 do local a=m[x]local s=A.ends_with(Y,"\047")local t=A.starts_with(a,"\047")if s and t then a=a:sub(2)elseif not s and not t then a="\047"..a end Y=Y..a end return Y end function A.file_exists(A)local m,Y=io.open(A,"\114")if m then m:close()return true else return false end end function A.file_read(A)local m,Y=io.open(A,"\114")if m then local A=m:read("\097")m:close()return A,nil else return nil,Y end end function A.file_write(A,m)local Y,x=io.open(A,"\119")assert(Y,string.format("\070\097\105\108\101\100 \116\111 \119\114\105\116\101 \116\111 \102\105\108\101 \037\115\044 \101\114\114\111\114\058 \037\115",A,x))Y:write(m)Y:close()end function A.split(A,m)local Y={}local x=1 local a,s=A:find(m,x)while a do table.insert(Y,A:sub(x,a-1))x=s+1 a,s=A:find(m,x)end table.insert(Y,A:sub(x))return Y end function A.find_in_table_str(A,m)for A,Y in ipairs(A)do if Y:find(m)then return A end end return nil end function A.tbl_extend(A,m,Y,x)local a=x or#m for Y=Y or 1,a,1 do table.insert(A,m[Y])end end function A.tbl_contains(A,m)for A,Y in pairs(A)do if Y==m then return true end end return false end function A.tbl_keys_contains(A,m)for A,Y in pairs(A)do if A==m then return true end end return false end function A.get_script_dir()local A=(debug.getinfo(2,"\083")).source:sub(2)return A:match("\040\046\042\047\041")end function A.spec_path()return A.path_combine(A.get_script_dir(),"\046\046","\115\112\101\099\115")end function A.transform(A,m)local Y={}for A,x in pairs(A)do Y[A]=m(A,x)end return Y end function A.matches_any(A,m)if not m then return false end for m,Y in ipairs(m)do if A:find(Y)then return true,Y end end return false,nil end function A.remove_match(A,m)local Y=A local x,a=Y:find(m)if x and a then Y=Y:sub(1,x-1)..Y:sub(a+1,#Y)end return Y,#Y~=#A end return A
local M = {}

--- @param str string
--- @param start string
--- @return boolean
function M.starts_with(str, start) return str:sub(1, #start) == start end

--- @param str string
--- @param ending string
--- @return boolean
function M.ends_with(str, ending) return ending == "" or str:sub(-#ending) == ending end

--- @param path string
--- @return string
function M.fname(path) return path:match("([^/]+)$") end

--- @param str string
--- @return boolean
function M.is_whitespace_or_nil(str)
    if not str then
        return true
    end

    return str:match("^%s*$")
end

--- @param ... string
--- @return string
function M.path_combine(...)
    local args = { ... }
    local res = args[1]
    for i = 2, #args do
        local segment = args[i]
        local rew = M.ends_with(res, "/")
        local ssw = M.starts_with(segment, "/")

        if rew and ssw then
            segment = segment:sub(2)
        elseif not rew and not ssw then
            segment = "/" .. segment
        end

        res = res .. segment
    end

    return res
end

--- @param path string
--- @return boolean
function M.file_exists(path)
    local fd, _ = io.open(path, "r")

    if fd then
        fd:close()
        return true
    else
        return false
    end
end

--- @param path string
--- @return string|nil, string|nil
function M.file_read(path)
    local fd, err = io.open(path, "r")

    if fd then
        local ret = fd:read("a")
        fd:close()
        return ret, nil
    else
        return nil, err
    end
end

---
--- @param path string
--- @param contents string|number
function M.file_write(path, contents)
    local fd, err = io.open(path, "w")

    assert(fd, string.format("Failed to write to file %s, error: %s", path, err))

    fd:write(contents)
    fd:close()
end

--- @param str string
--- @param delimiter string
--- @return string[]
function M.split(str, delimiter)
    local result = {}
    local from = 1
    local delim_from, delim_to = str:find(delimiter, from)
    while delim_from do
        table.insert(result, str:sub(from, delim_from - 1))
        from = delim_to + 1
        delim_from, delim_to = str:find(delimiter, from)
    end
    table.insert(result, str:sub(from))
    return result
end

--- @param tbl string[]
--- @param pattern string
--- @return number|nil
function M.find_in_table_str(tbl, pattern)
    for i, v in ipairs(tbl) do
        if v:find(pattern) then
            return i
        end
    end

    return nil
end

function M.tbl_extend(tbl, tbl_2, s, e)
    local finish = e or #tbl_2

    for i = (s or 1), finish do
        table.insert(tbl, tbl_2[i])
    end
end

--- @param tbl table
--- @param value any
--- @return boolean
function M.tbl_contains(tbl, value)
    for _, v in pairs(tbl) do
        if v == value then
            return true
        end
    end

    return false
end

--- @param tbl table
--- @param key any
--- @return boolean
function M.tbl_keys_contains(tbl, key)
    for k, _ in pairs(tbl) do
        if k == key then
            return true
        end
    end

    return false
end

function M.get_script_dir()
    local str = debug.getinfo(2, "S").source:sub(2)
    return str:match("(.*/)")
end

function M.spec_path() return M.path_combine(M.get_script_dir(), "..", "specs") end

--- Applies `func` to each key-value pair in the table, providing the key and
--- value as arguments
--- @param tbl table
--- @param func function
--- @return table
function M.transform(tbl, func)
    local ret = {}

    for k, v in pairs(tbl) do
        ret[k] = func(k, v)
    end

    return ret
end

--- @param segment string
--- @param matches string[]
--- @return boolean, string|nil
function M.matches_any(segment, matches)
    if not matches then
        return false
    end

    for _, match in ipairs(matches) do
        if segment:find(match) then
            return true, match
        end
    end

    return false, nil
end

--- @param line string
--- @param match string
--- @return string, boolean
function M.remove_match(line, match)
    local ret = line
    local vs, ve = ret:find(match)
    if vs and ve then
        ret = ret:sub(1, vs - 1) .. ret:sub(ve + 1, #ret)
    end

    return ret, #ret ~= #line
end

return M
