--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.

local Util = require("src/util/Util")
local Log = require("src/util/Log")

-- model's struct is map<string, map<string, Assertion>>
local Policy = {}

function Policy:new()
    local o = {}
    o.logger = Log.getLogger()
    setmetatable(o, self)
    self.__index = self
    return o
end

--[[
    * buildRoleLinks initializes the roles in RBAC.
    *
    * @param rm the role manager.
]]
function Policy:buildRoleLinks(rmMap)
    if self.model["g"] then
        for ptype, ast in pairs(self.model["g"]) do
            local rm = rmMap[ptype]
            ast:buildRoleLinks(rm)
        end
    end
end

--[[
    * printPolicy prints the policy to log.
]]
function Policy:printPolicy()
    self.logger:info("Policy: \n")
    if self.model["p"] then
        for k, ast in pairs(self.model["p"]) do
            self.logger:info("%s:   %s:", k, ast.value)
            self.logger:info(ast.policy)
        end
    end

    if self.model["g"] then
        for k, ast in pairs(self.model["g"]) do
            self.logger:info("%s:   %s:", k, ast.value)
            self.logger:info(ast.policy)
        end
    end
end

--[[
    * printPolicyMap prints the policyMap to log.
]]
function Policy:printPolicyMap()
    self.logger:info("policyMap: \n")
    if self.model["p"] then
        for k, ast in pairs(self.model["p"]) do
            self.logger:info("%s:   key,value", k )
            for i,v in pairs(ast.policyMap) do
                self.logger:info("{%s,%s}", i, v)
            end
        end
    end

    if self.model["g"] then
        for k, ast in pairs(self.model["g"]) do
            self.logger:info("%s:   key,value", k )
            for i,v in pairs(ast.policyMap) do
                self.logger:info("{%s,%s}", i, v)
            end
        end
    end
end

--[[
    * savePolicyToText saves the policy to the text.
    *
    * @return the policy text.
]]
function Policy:savePolicyToText()
    local res = ""

    if self.model["p"] then
        for key, ast in pairs(self.model["p"]) do
            for _, rule in pairs(ast.policy) do
                local x = string.format("%s, %s\n", key, table.concat(rule, ", "))
                res = res .. x
            end
        end
    end

    if self.model["g"] then
        for key, ast in pairs(self.model["g"]) do
            for _, rule in pairs(ast.policy) do
                local x = string.format("%s, %s\n", key, table.concat(rule, ", "))
                res = res .. x
            end
        end
    end

    return res
end

--[[
    * clearPolicy clears all current policy.
]]
function Policy:clearPolicy()
    if self.model["p"] then
        for _, v in pairs(self.model["p"]) do
            v.policy = {}
            v.policyMap = {}
        end
    end

    if self.model["g"] then
        for _, v in pairs(self.model["g"]) do
            v.policy = {}
            v.policyMap = {}
        end
    end
end

--[[
    * getPolicy gets all rules in a policy.
    *
    * @param sec the section, "p" or "g".
    * @param ptype the policy type, "p", "p2", .. or "g", "g2", ..
    * @return the policy rules of section sec and policy type ptype.
]]
function Policy:getPolicy(sec, ptype)
    local policy = Util.deepCopy(self.model[sec][ptype].policy)
    return policy
end

--[[
    * getFilteredPolicy gets rules based on field filters from a policy.
    *
    * @param sec the section, "p" or "g".
    * @param ptype the policy type, "p", "p2", .. or "g", "g2", ..
    * @param fieldIndex the policy rule's start index to be matched.
    * @param ... fieldValues the field values to be matched, value ""
    *                    means not to match this field.
    * @return the filtered policy rules of section sec and policy type ptype.
]]
function Policy:getFilteredPolicy(sec, ptype, fieldIndex, ...)
    local res = {}
    local fieldValues = {...}
    if type(fieldValues[1]) == "table" then
        fieldValues = fieldValues[1]
    end

    if not self.model[sec] then return res end
    if not self.model[sec][ptype] then return res end

    for _, rule in pairs(self.model[sec][ptype].policy) do
        local matched = true
        for i, v in ipairs(fieldValues) do
            if v ~= "" and rule[fieldIndex + i] ~= v then
                matched = false
                break
            end
        end
        if matched then
            table.insert(res, rule)
        end
    end

    return res
end

--[[
    * hasPolicy determines whether a model has the specified policy rule.
    *
    * @param sec the section, "p" or "g".
    * @param ptype the policy type, "p", "p2", .. or "g", "g2", ..
    * @param rule the policy rule.
    * @return whether the rule exists.
]]
function Policy:hasPolicy(sec, ptype, rule)
    if self.model[sec][ptype].policyMap[table.concat(rule,",")] == nil then
        return false
    else
        return true
    end
end

--[[
    * hasPolicies determines whether a model has any of the specified policies.
    *
    * @param sec the section, "p" or "g".
    * @param ptype the policy type, "p", "p2", .. or "g", "g2", ..
    * @param rules the policies rules.
    * @return whether one is found.
]]
function Policy:hasPolicies(sec, ptype, rules)
    for i = 1, #rules do
        if self:hasPolicy(sec, ptype, rules[i]) then
            return true
        end
    end
    return false
end


--[[
    * addPolicy adds a policy rule to the model.
    *
    * @param sec the section, "p" or "g".
    * @param ptype the policy type, "p", "p2", .. or "g", "g2", ..
    * @param rule the policy rule.
    * @return succeeds or not.
]]
function Policy:addPolicy(sec, ptype, rule)
    if not self:hasPolicy(sec, ptype, rule) then
        table.insert(self.model[sec][ptype].policy, rule)
        self.model[sec][ptype].policyMap[table.concat(rule,",")] = #self.model[sec][ptype].policy
        if sec == "p" and self.model[sec][ptype].priorityIndex > 0 then
            local idxInsert=tonumber(rule[self.model[sec][ptype].priorityIndex])
            if rule[self.model[sec][ptype].priorityIndex]~= nil then
                local i = #self.model[sec][ptype].policy-1
                for j = i, 1, -1 do
                    local idx=tonumber(self.model[sec][ptype].policy[i+1][self.model[sec][ptype].priorityIndex])
                    if idx < idxInsert then
                        self.model[sec][ptype].policy[i+1] = self.model[sec][ptype].policy[i]
                        self.model[sec][ptype].policyMap[table.concat(self.model[sec][ptype].policy[i+1], ",")] = i+1
                    else
                        i = j
                        break
                    end
                    i = j
                end
                self.model[sec][ptype].policy[i] = rule
                self.model[sec][ptype].policyMap[table.concat(rule,",")] = i
            end
        end
        return true
    end
    return false
end

--[[
    * addPolicies adds policy rules to the model.
    * @param sec the section, "p" or "g".
    * @param ptype the policy type, "p", "p2", .. or "g", "g2", ..
    * @param rules the policy rules.
    * @return succeeds or not.
]]
function Policy:addPolicies(sec, ptype, rules)
    return self:addPoliciesWithAffected(sec, ptype, rules) ~= 0
end

--[[
    * addPoliciesWithAffected adds policy rules to the model.
    * @param sec the section, "p" or "g".
    * @param ptype the policy type, "p", "p2", .. or "g", "g2", ..
    * @param rules the policy rules.
    * @return effected.
]]
function Policy:addPoliciesWithAffected(sec, ptype, rules)
    local effected = {}
    for _, rule in pairs(rules) do
        if self:addPolicy(sec, ptype, rule) then
            table.insert(effected, rule)

        end
    end
    return effected
end

--[[
    * UpdatePolicy updates a policy rule from the model.
    *
    * @param sec the section, "p" or "g".
    * @param ptype the policy type, "p", "p2", .. or "g", "g2", ..
    * @param oldRule the old rule.
    * @param newRule the new rule.
    * @return succeeds or not.
]]
function Policy:updatePolicy(sec, ptype, oldRule, newRule)
    if not self:hasPolicy(sec, ptype, oldRule) then return false end
    local key = table.concat(oldRule,",")
    local index = self.model[sec][ptype].policyMap[key]
    self.model[sec][ptype].policy[index] = newRule
    self.model[sec][ptype].policyMap[key] = nil
    local tempKey = table.concat(newRule,",")
    self.model[sec][ptype].policyMap[tempKey] = index
    return true
end

-- Updates multiple policy rules from the model.
function Policy:updatePolicies(sec, ptype, oldRules, newRules)
    local rollbackFlag = false
    local modifiedRuleIndex = {}

    local newIndex = 1
    for oldIndex, oldRule in pairs(oldRules) do
        local oldPolicy = table.concat(oldRule, ",")
        if self.model[sec][ptype].policyMap[oldPolicy] == nil then
            rollbackFlag = true
            break
        end
        local index = self.model[sec][ptype].policyMap[oldPolicy]

        self.model[sec][ptype].policy[index] = newRules[newIndex]
        self.model[sec][ptype].policyMap[oldPolicy] = nil
        self.model[sec][ptype].policyMap[table.concat(newRules[newIndex], ",")] = index
        modifiedRuleIndex[index] = {oldIndex, newIndex}
        newIndex = newIndex+1
    end

    if rollbackFlag then
        for index, oldNewIndex in pairs(modifiedRuleIndex)  do
            self.model[sec][ptype].policy[index] = oldRules[oldNewIndex[1]]
            local oldPolicy = table.concat(oldRules[oldNewIndex[1]], ",")
            local newPolicy = table.concat(newRules[oldNewIndex[2]], ",")
            self.model[sec][ptype].policyMap[newPolicy] = nil
            self.model[sec][ptype].policyMap[oldPolicy] = index
        end
        return false
    end
    return true
end

function Policy:updateFilteredPolicies(sec, ptype, fieldIndex, fieldValues,newRules)
    local tmp = {}
    local res = false
    local effects = {}
    local newRulesIndex=1

    if not self.model[sec] then return res end
    if not self.model[sec][ptype] then return res end
    self.model[sec][ptype].policyMap = {}
    for _, rule in pairs(self.model[sec][ptype].policy) do
        local matched = true
        for i, value in pairs(fieldValues) do
            if value ~= "" and rule[fieldIndex+i] ~= value then
                matched = false
                break
            end
        end

        if matched then
            table.insert(effects, rule)
            table.insert(tmp, newRules[newRulesIndex])
            local tempKey1 = table.concat(newRules[newRulesIndex],",")
            self.model[sec][ptype].policyMap[tempKey1] = #tmp
            newRulesIndex = newRulesIndex+1
            res = true
        else
            table.insert(tmp, rule)
            local tempKey = table.concat(rule,",")
            self.model[sec][ptype].policyMap[tempKey] = #tmp
        end
    end

    if res then
        self.model[sec][ptype].policy = tmp
    end
    return res, effects
end

--[[
    * removePolicy removes a policy rule from the model.
    *
    * @param sec the section, "p" or "g".
    * @param ptype the policy type, "p", "p2", .. or "g", "g2", ..
    * @param rule the policy rule.
    * @return succeeds or not.
]]
function Policy:removePolicy(sec, ptype, rule)
    if self:hasPolicy(sec,ptype,rule) then
        local key = table.concat(rule,",")
        local index = self.model[sec][ptype].policyMap[key]
        table.remove(self.model[sec][ptype].policy, index)
        self.model[sec][ptype].policyMap[key] = nil
        local length = #self.model[sec][ptype].policy
        for i=index, length, 1 do
            local tempKey = table.concat(self.model[sec][ptype].policy[i],",")
            self.model[sec][ptype].policyMap[tempKey] = i
        end
        return true
    end
    return false
end

--[[
    * removePolicies removes rules from the current policy.
    * @param sec the section, "p" or "g".
    * @param ptype the policy type, "p", "p2", .. or "g", "g2", ..
    * @param rules the policy rules.
    * @return succeeds or not.
]]
function Policy:removePolicies(sec, ptype, rules)
    return #self:removePoliciesWithEffected(sec, ptype, rules) ~= 0
end

--[[
    * removePoliciesWithEffected removes policy rules from the model, and returns effected rules.
    *
    * @param sec the section, "p" or "g".
    * @param ptype the policy type, "p", "p2", .. or "g", "g2", ..
    * @param rules the policy rules.
    *
    * @return effected.
]]
function Policy:removePoliciesWithEffected(sec, ptype, rules)
    local effected={}
    for _,rule in pairs(rules) do
        if self:removePolicy(sec,ptype,rule) then
            table.insert(effected,rule)
        end
    end
    return effected
end

--[[
    * removeFilteredPolicy removes policy rules based on field filters from the model.
    *
    * @param sec the section, "p" or "g".
    * @param ptype the policy type, "p", "p2", .. or "g", "g2", ..
    * @param fieldIndex the policy rule's start index to be matched.
    * @param ... fieldValues the field values to be matched, value ""
    *                    means not to match this field.
    * @return succeeds or not.
]]
function Policy:removeFilteredPolicy(sec, ptype, fieldIndex, fieldValues)
    local tmp = {}
    local res = false
    local effects = {}

    if not self.model[sec] then return res end
    if not self.model[sec][ptype] then return res end
    self.model[sec][ptype].policyMap = {}
    for _, rule in pairs(self.model[sec][ptype].policy) do
        local matched = true
        for i, value in pairs(fieldValues) do
            if value ~= "" and rule[fieldIndex+i] ~= value then
                matched = false
                break
            end
        end

        if matched then
            table.insert(effects, rule)
            res = true
        else
            table.insert(tmp, rule)
            local tempKey = table.concat(rule,",")
            self.model[sec][ptype].policyMap[tempKey] = #tmp
        end
    end
    if #tmp ~= #self.model[sec][ptype].policy then
        self.model[sec][ptype].policy = tmp
    end
    return res, effects
end

--[[
    * getValuesForFieldInPolicy gets all values for a field for all rules in a policy, duplicated values are removed.
    *
    * @param sec the section, "p" or "g".
    * @param ptype the policy type, "p", "p2", .. or "g", "g2", ..
    * @param fieldIndex the policy rule's index.
    * @return the field values specified by fieldIndex.
]]
function Policy:getValuesForFieldInPolicy(sec, ptype, fieldIndex)
    local values = {}

    for _, rule in pairs(self.model[sec][ptype].policy) do
        values[#values + 1] = rule[fieldIndex]
    end

    values = Util.arrayRemoveDuplications(values)
    return values
end

function Policy:getValuesForFieldInPolicyAllTypes(sec, fieldIndex)
    local values = {}

    for ptype, _ in pairs(self.model[sec]) do
        local tvalues = self:getValuesForFieldInPolicy(sec, ptype, fieldIndex)
        for _, v in pairs(tvalues) do
            table.insert(values, v)
        end
    end

    return values
end

function Policy:buildIncrementalRoleLinks(rm, op, sec, ptype, rules)
    if sec == "g" then
        self.model[sec][ptype]:buildIncrementalRoleLinks(rm, op, rules)
    end
end

return Policy
