--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 Config = require("src/config/Config")
local Policy = require("src/model/Policy")
local Assertion = require("src/model/Assertion")
local Util = require("src/util/Util")

local Model = Policy:new()

function Model:new()
    local o = {}
    setmetatable(o, self)
    self.__index = self
    self.model = {}
    self.sectionNameMap = {
        ["r"] = "request_definition",
        ["p"] = "policy_definition",
        ["g"] = "role_definition",
        ["e"] = "policy_effect",
        ["m"] = "matchers"
    }

    self.requiredSections = {"r", "p", "e", "m"} -- Minimal required sections for a model to be valid
    self.modCount = 0   -- used by CoreEnforcer to detect changes to Model

    -- PolicyOperations: [key] = POLICY_ADD/POLICY_REMOVE and value = string(key)
    self.PolicyOperations = {
        POLICY_ADD = "POLICY_ADD",
        POLICY_REMOVE = "POLICY_REMOVE"
    }
    return o
end

function Model:getModCount()
    return self.modCount
end

function Model:loadAssertion(model, cfg, sec, key)
    local value = cfg:getString(self.sectionNameMap[sec].."::"..key)
    return model:addDef(sec, key, value)
end

--[[
    * addDef adds an assertion to the model.
    *
    * @param sec the section, "p" or "g".
    * @param key the policy type, "p", "p2", .. or "g", "g2", ..
    * @param value the policy rule, separated by ", ".
    * @return succeeds or not.
]]
function Model:addDef(sec, key, value)

    if value == "" then return false end

    if self.model[sec] == nil then
        self.model[sec] = {}
    end

    if self.model[sec][key] == nil then
        self.model[sec][key] = {}
    end

    self.model[sec][key] = Assertion:new()
    self.model[sec][key].key = key
    self.model[sec][key].value = value
    self.model[sec][key].policyMap={}
    self.model[sec][key]:initPriorityIndex()
    if sec == "r" or sec == "p" then
        self.model[sec][key].tokens = Util.splitCommaDelimited(self.model[sec][key].value)
        for k, v in pairs(self.model[sec][key].tokens) do
            self.model[sec][key].tokens[k] = key .. "_" .. self.model[sec][key].tokens[k]
        end
    else
        self.model[sec][key].value = Util.removeComments(Util.escapeAssertion(self.model[sec][key].value))
    end

    if sec == "m" and string.find(self.model[sec][key].value,"in")~=nil then
        self.model[sec][key].value = string.gsub(string.gsub(self.model[sec][key].value,"%[","("),"%]",")")
    end

    self.modCount = self.modCount + 1
    return true
end

function Model:getKeySuffix(i)
     if i == 1 then
        return ""
    end

    return ""..i
end

function Model:loadSection(model, cfg, sec)
    local i = 1
    while true do
        if not self:loadAssertion(model, cfg, sec, sec..self:getKeySuffix(i)) then
            break;
        else
            i = i + 1
        end
    end
end

--[[
    * loadModel loads the model from model CONF file.
    *
    * @param path the path of the model file.
]]
function Model:loadModel(path)
    local cfg = Config:newConfig(path)

    self:loadSection(self, cfg, "r")
    self:loadSection(self, cfg, "p")
    self:loadSection(self, cfg, "e")
    self:loadSection(self, cfg, "m")

    self:loadSection(self, cfg, "g")
end

--[[
    * loadModelFromText loads the model from the text.
    *
    * @param text the model text.
]]
function Model:loadModelFromText(text)
    local cfg = Config:newConfigFromText(text)

    self:loadSection(self, cfg, "r")
    self:loadSection(self, cfg, "p")
    self:loadSection(self, cfg, "e")
    self:loadSection(self, cfg, "m")

    self:loadSection(self, cfg, "g")
end

--[[
    * saveSectionToText saves the section to the text.
    *
    * @return the section text.
]]
function Model:saveSectionToText(sec)
    local res = "[" .. self.sectionNameMap[sec] .. "]\n"

    if not self.model[sec] then
        return ""
    end

    for key, ast in pairs(self.model[sec]) do
        local val = ast.value:gsub("%_", ".")
        local x = string.format("%s = %s\n", key, val)

        res = res .. x
    end

    return res
end

--[[
    * toText saves the model to the text.
    *
    * @return the model text.
]]
function Model:toText()
    local tokenPatterns={}
    for _,ptype in pairs({"r","p"}) do
        for _,token in pairs(self.model[ptype][ptype].tokens) do
            tokenPatterns[token]=string.gsub (string.gsub (token,"^p_","p."),"^r_","r.")
        end
    end
    tokenPatterns["p_eft"] = "p.eft"

    local s=""
    local writeString=function(sec)
        local result=""
         for ptype,_ in pairs(self.model[sec]) do
            local value=self.model[sec][ptype].value
            for tokenPattern,newToken in pairs(tokenPatterns) do
                value=string.gsub(value,tokenPattern,newToken)
            end
            result=result..sec.." = "..value.."\n"
        end
        return result
    end
    s=s.."[request_definition]\n"..writeString("r").."[policy_definition]\n"..writeString("p")
    if self.model["g"] then
        s=s.."[role_definition]\n"
        for ptype,_ in pairs(self.model["g"]) do
            s=s..ptype.." = "..self.model["g"][ptype].value.."\n"
        end
    end
    s=s.."[policy_effect]\n"..writeString("e").."[matchers]\n"..writeString("m")
    return s
end

--  * printModel prints the model to the log.
function Model:printModel()
    self.logger:info("Model: \n")
    for k,v in pairs(self.model) do
        for k2, v2 in pairs(v) do
            self.logger:info("[%s.%s]:", k, k2)
            self.logger:info(v2)
        end
    end
end

local function getSubjectHierarchyMap(policies)
    local subjectHierarchyMap = { }
    --Tree structure of role
    local policyMap = { }
    for _, policy in pairs(policies) do
        if #policy < 2 then
            return nil, error("policy g expect 2 more params")
        end
        local domain=""
        if #policy~=2 then
            domain  = policy[3]
        end
        local child = domain.."::"..policy[1]
        local parent = domain.."::"..policy[2]
        if policyMap[parent]==nil then
            policyMap[parent]={}
        end
        table.insert(policyMap[parent], child)
        if subjectHierarchyMap[child]==nil then
            subjectHierarchyMap[child] = 0
        end
        if subjectHierarchyMap[parent]==nil then
            subjectHierarchyMap[parent] = 0
        end
        subjectHierarchyMap[child] = 1
    end
    local queue = {  }
    for k, v in pairs(subjectHierarchyMap) do
        if v == 0 then
            local root = k
            local lv = 0
            table.insert(queue,root)
            while #queue~=0 do
                local sz=#queue
                for i=1,sz do
                    local node=queue[1]
                    table.remove(queue,1)
                    subjectHierarchyMap[node] = lv
                    if policyMap[node]~=nil then
                        for _,child in pairs(policyMap[node]) do
                            table.insert(queue,child)
                        end
                    end
                end
                lv=lv+1
            end
        end
    end

    return subjectHierarchyMap, nil
end

function Model:sortPoliciesBySubjectHierarchy()
    if self.model["e"]["e"].value ~= "subjectPriority(p_eft) || deny" then
        return nil
    end
    local subIndex = 1
    local domainIndex = -1
    for ptype, assertion in pairs(self.model["p"]) do
        for index, token in pairs(assertion.tokens)  do
            if token == ptype.."_dom" then
            domainIndex = index
            break
            end
        end
        local subjectHierarchyMap, err = getSubjectHierarchyMap(self.model["g"]["g"].policy)
        if err ~= nil then
            return err
        end
        table.sort(assertion.policy, function(i, j)
            local domain1, domain2 = "", ""
            if domainIndex ~= -1 then
                domain1 = i[domainIndex]
                domain2 = j[domainIndex]
            end
            local name1, name2 =domain1.."::"..i[subIndex], domain2.."::".. j[subIndex]
            local p1 = subjectHierarchyMap[name1]
            local p2 = subjectHierarchyMap[name2]
            return p1 > p2
        end)
        for i, policy in pairs(assertion.policy) do
            assertion.policyMap[table.concat(policy, ",")] = i
        end
    end
    return nil
end

-- sortPoliciesByPriority sorts policies by their priorities if 'priority' token exists
function Model:sortPoliciesByPriority()
    if not self.model["p"] then return end

    for ptype, ast in pairs(self.model["p"]) do
        for inx, token in pairs(ast.tokens) do
            if token == ptype .. "_priority" then
                ast.priorityIndex = inx
                break
            end
        end
        if ast.priorityIndex == -1 then
            return
        end
        table.sort(ast.policy, function (a, b)
        return a[ast.priorityIndex] < b[ast.priorityIndex]
        end)
        for i,policy in pairs(ast.policy) do
            ast.policyMap[table.concat(policy,",")]=i
        end
     end
end

function Model:copy()
    return Util.deepCopy(self.model)
end

return Model
