--Copyright 2021 The casbin Authors. All Rights Reserved.
--
--Licensed under the Apache License, Version 2.0 (the "License");
--you may not use this file except in compliance with the License.
--You may obtain a copy of the License at
--
--    http://www.apache.org/licenses/LICENSE-2.0
--
--Unless required by applicable law or agreed to in writing, software
--distributed under the License is distributed on an "AS IS" BASIS,
--WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
--See the License for the specific language governing permissions and
--limitations under the License.

-- Utility Functions for lua-casbin
local Util = {}

-- arrayToString convert table of strings to one string
function Util.arrayToString(rule)
    local str = ""
    if #rule>0 then str = rule[1] end
    for i = 2, #rule do
        str = str .. ", " .. rule[i]
    end
    return str
end 

--[[
    * splitCommaDelimited splits a comma-delimited string into a string array. It assumes that any
    * number of whitespace might exist before or after the comma and that tokens do not include
    * whitespace as part of their value.
    *
    * @param str the comma-delimited string.
    * @return the array with the string tokens.
]]
function Util.splitCommaDelimited(str)
    str = str .. ","
    local t ={}
    for word in str:gmatch("([^,]+),%s*") do
         table.insert(t,Util.trim(word))
    end
    return t
end

--Escapes the dots in the assertion, because the expression evaluation doesn't support such variable names.
function Util.escapeAssertion(str)
    if string.sub(str, 1, 1) == "r" or string.sub(str, 1, 1) == "p" then
        str = str:gsub("%.","_", 1)
    end
    str = str:gsub("[^%w]+r%.", function (a)
        return string.sub(a, 1, -3).."r_"
    end)
    str = str:gsub("[^%w]+p%.", function (a)
        return string.sub(a, 1, -3).."p_"
    end)
    return str
end

--Removes the comments starting with # in the text.
function Util.removeComments(str)
    local i, _ = string.find(str, "#")
    if i then str = str:sub(1,i-1) end

    return Util.trim(str)
end

function Util.arrayEquals(a, b)
    if #a ~= #b then
        return false
    end
    for i = 1, #a do
        if a[i] ~= b[i] then
            return false
        end
    end
    return true
end

function Util.array2DEquals(a, b)
    if #a ~= #b then
        return false
    end
    for i = 1, #a do
        if not Util.arrayEquals(a[i], b[i]) then
            return false
        end
    end
    return true
end

function Util.arrayRemoveDuplications(s)
    local hash = {}
    local res = {}
    for _, v in pairs(s) do
        if not hash[v] then
            table.insert(res, v)
            hash[v] = true
        end
    end
    return res
end

-- Trims the leading and trailing whitespaces from a string
function Util.trim(s)
    return (s:gsub("^%s*(.-)%s*$", "%1"))
end

--[[
    * Splits string "str" with any "delimiter" and returns a table
    * (optional) 'x' is the maximum no. of times the string should be split
]]
function Util.split(str, delimiter, x)
    local result = {}
    local from  = 1
    local delim_from, delim_to = string.find(str, delimiter, from)
    while delim_from do
        table.insert(result, Util.trim(string.sub(str, from, delim_from-1)))
        from = delim_to + 1
        delim_from, delim_to = string.find(str, delimiter, from)
        if x~=nil then x = x - 1 end
        if x == 0 then break end
    end
    table.insert(result, Util.trim(string.sub(str, from)))
    return result
end

--[[
    * isInstance checks if o has parent as it's parent tables(metatables) recursively
    * @param base table, parent table
    * @returns true/false
]]
function Util.isInstance(o, parent)
    while o do
        o = getmetatable(o)
        if parent == o then return true end
    end
    return false
end

-- Searches if all values in a table are present in the other table regardless of order
function Util.areTablesSame(a, b)
    local c = {}
    for _, v in pairs(a) do
        if c[v] then
            c[v] = c[v] + 1
        else
            c[v] = 1
        end
    end

    for _, v in pairs(b) do
        if c[v] then
            c[v] = c[v] - 1
            if c[v] == 0 then
                c[v] = nil
            end
        else
            return false
        end
    end
    for _, v in pairs(c) do
        if v then
            return false
        end
    end
    return true
end

-- checks if the matcher string has eval(...)
function Util.hasEval(str)
    return string.find(str, "eval%((.-)%)")
end

-- finds if string has eval and replaces eval(...) with its value so that it can be evaluated by luaxp
function Util.findAndReplaceEval(str, context)
    local m = string.gsub(str, "eval%((.-)%)", function (s)
        return Util.escapeAssertion(context[Util.trim(s)])
    end)
    return m
end

-- replaces [obj] in (obj1, obj2, ...) to obj == obj1 || obj == obj2 || ...
function Util.replaceInOfMatcher(str)
    local s = string.gsub(str, "([^%s]+)(%s+)in(%s+)%((.*)%)", function (a, _, _ , b)
        local t = ""
        local vals = Util.splitCommaDelimited(b)
        for k, v in pairs(vals) do
            t = t .. a .. " == " .. v
            if k<#vals then
                t = t .. " || "
            else
                t = t .. " "
            end
        end
        return t
    end)
    return s
end

-- returns deep (recursive) copy of a table (values only)
function Util.deepCopy(org)
    if type(org) ~= "table" then
        return org
    end
    local copy = {}
    for k, v in pairs(org) do
        copy[k] = Util.deepCopy(v)
    end
    setmetatable(copy,getmetatable(org))
    return copy
end

-- setSubtract returns the elements in `a` that aren't in `b`.
function Util.setSubtract(a, b)
    local res = {}
    for _, v in pairs(a) do
        res[v] = true
    end
    for _, v in pairs(b) do
        if res[v] then
            res[v] = nil
        else
            res[v] = true
        end
    end
    local diff = {}
    for v, chk in pairs(res) do
        if chk == true then
            table.insert(diff, v)
        end
    end

    return diff
end

-- printTable recursively prints a table for logging
function Util.printTable(t)
    local s = "{ "
    for k, v in pairs(t) do
        if type(v) == "table" then
            s = s .. Util.printTable(v) .. ", "
        else
            if type(k)=="string" then
                s = s .. k .. " = " .. v .. ", "
            else
                s = s .. v .. ", "
            end
        end
    end
    s = string.sub(s, 1, -3)
    s = s .. " }"
    return s
end

function Util.isOnlyWhitespaces(str)
    return str:match("^%s*$") ~= nil
end

function Util.splitEnhanced(str, delim, trimFields)
    local result = {}
    local i = 1
    local quotedField
    local escaping = false
    local field = ""

    if delim == nil then delim = ',' end
    if trimFields == nil then trimFields = true end
  
    -- Loop over the characters of the string
    while i <= #str do
        local char = str:sub(i, i)

        -- Check if it's the first character and it's a double quote.
        if Util.isOnlyWhitespaces(field) and char == '"' then
            -- Then this field is a quoted field
            quotedField = true
        else
            if quotedField then
                if escaping then
                    -- ", End of quoted field
                    if char == delim then
                        if trimFields then
                            field = Util.trim(field)
                        end
    
                        table.insert(result, field)
                        field = ""
                        quotedField = false
                    -- "" Escapes the double quote character
                    elseif char == '"' then
                        field = field .. char
                        escaping = false
                    -- " followed by some other character (not allowed)
                    else
                        error("Quoted fields cannot have extra characters outside double quotes.")
                    end
                else -- Not escaping
                    if char == '"' then
                        escaping = true
                    else
                        field = field .. char
                    end
                end

            else -- Not quotedField
                if char == delim then
                    if trimFields then
                        field = Util.trim(field)
                    end

                    table.insert(result, field)
                    field = ""
                else
                    field = field .. char
                end
            end
        end

        i = i + 1
    end

    -- Throw error if there are quotes left open
    if quotedField and not escaping then
        error("Unmatched quotes.")
    end
  
    -- Add the last field (since it won't have the delimiter after it)
    if trimFields then
        field = Util.trim(field)
    end
    table.insert(result, field)
  
    return result
end

return Util