--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 Role = require("src/rbac/Role")
local Log = require("src/util/Log")

local DefaultRoleManager = {
    maxHierarchyLevel = 0
}

--[[
     * In order to use a specific role name matching function, set explicitly the role manager on
     * the Enforcer and rebuild role links (you can optimize by using minimal enforcer constructor).
     *
     * <pre>
     * final Enforcer e = new Enforcer("model.conf");
     * e.setAdapter(new FileAdapter("policies.csv"));
     * e.setRoleManager(new DefaultRoleManager(10, BuiltInFunctions::domainMatch));
     * e.loadPolicy();
     * </pre>
     *
     *
     * @param maxHierarchyLevel the maximized allowed RBAC hierarchy level.
     * @param matchingFunc a matcher for supporting pattern in g
     * @param domainMatchingFunc a matcher for supporting domain pattern in g
]]
function DefaultRoleManager:new(maxHierarchyLevel, matchingFunc, domainMatchingFunc)
    local o = {}
    setmetatable(o, self)
    self.__index = self
    o.logger = Log.getLogger()
    o.allRoles = {}
    o.maxHierarchyLevel = maxHierarchyLevel
    o.matchingFunc = matchingFunc
    o.domainMatchingFunc = domainMatchingFunc
    return o
end

--[[
    * addMatchingFunc adds a Matching Function to the RM
    * if nil is passed in, it removes the function
]]
function DefaultRoleManager:addMatchingFunc(matchingFunc)
    self.matchingFunc = matchingFunc
end

--[[
    * addDomainMatchingFunc adds a Domain Matching Function to the RM
    * if nil is passed in, it removes the function
]]
function DefaultRoleManager:addDomainMatchingFunc(domainMatchingFunc)
    self.domainMatchingFunc = domainMatchingFunc
end

function DefaultRoleManager:hasRole(role)
    
    if not self.matchingFunc and not self.domainMatchingFunc then
        return self.allRoles[role:getKey()]
    end

    for _, r in pairs(self.allRoles) do
        local flag = true
        if self.matchingFunc then
            if not self.matchingFunc(role.name, r.name) then
                flag = false
            end
        else
            if role.name ~= r.name then
                flag = false
            end
        end

        if self.domainMatchingFunc then
            if not self.domainMatchingFunc(role.domain, r.domain) then
                flag = false
            end
        else
            if role.domain ~= r.domain then
                flag = false
            end
        end
        
        if flag then
            return true
        end
    end
end

function DefaultRoleManager:createRole(name, domain)
    local role = Role:new(name, domain)
    local key
    if domain and domain ~= "" then
        key = domain .. "::" .. name
    else
        key = name
    end
    
    if not self.allRoles[key] then
        self.allRoles[key] = role
    end

    return self.allRoles[key]
end

--  * clear clears all stored data and resets the role manager to the initial state.
function DefaultRoleManager:clear()
    self.allRoles = {}
end

--[[
     * addLink adds the inheritance link between role: name1 and role: name2. aka role: name1
     * inherits role: name2. domain is a prefix to the roles.
]]
function DefaultRoleManager:addLink(name1, name2, ...)
    local domain = {...}
    if #domain > 1 then
        error("domain should be only 1 parameter")
    elseif #domain == 1 then
        domain = domain[1]
    else
        domain = ""
    end

    local role1 = self:createRole(name1, domain)
    local role2 = self:createRole(name2, domain)
    role1:addRole(role2)

    if self.matchingFunc then
        for _, role in pairs(self.allRoles) do
            local flag = true
            if self.domainMatchingFunc then
                if not self.domainMatchingFunc(domain, role.domain) then
                    flag = false
                end
            else
                if domain ~= role.domain then
                    flag = false
                end
            end
            
            if flag then
                if self.matchingFunc(role.name, role1.name) and role1.name ~= role.name then
                    self.allRoles[role:getKey()]:addRole(role1)
                end
                if self.matchingFunc(role.name, role2.name) and role2.name ~= role.name then
                    self.allRoles[role2:getKey()]:addRole(role)
                end
                if self.matchingFunc(role1.name, role.name) and role1.name ~= role.name then
                    self.allRoles[role:getKey()]:addRole(role1)
                end
                if self.matchingFunc(role2.name, role.name) and role2.name ~= role.name then
                    self.allRoles[role2:getKey()]:addRole(role)
                end
            end
        end
    end
end

--[[
     * deleteLink deletes the inheritance link between role: name1 and role: name2. aka role: name1
     * does not inherit role: name2 any more. domain is a prefix to the roles.
]]
function DefaultRoleManager:deleteLink(name1, name2, ...)
    local domain = {...}
    if #domain > 1 then
        error("domain should be only 1 parameter")
    elseif #domain == 1 then
        domain = domain[1]
    else
        domain = ""
    end

    local role1, role2 = self:twoRoleDomainWrapper(name1, name2, domain)

    if not (self:hasRole(role1)) or not (self:hasRole(role2)) then
        error("name1 or name2 does not exist")
    end

    role1:deleteRole(role2)
end

-- hasLink determines whether role: name1 inherits role: name2. domain is a prefix to the roles.
function DefaultRoleManager:hasLink(name1, name2, ...)
    local domain = {...}
    if #domain > 1 then
        error("domain should be only 1 parameter")
    elseif #domain == 1 then
        domain = domain[1]
    else
        domain = ""
    end

    local role1, role2 = self:twoRoleDomainWrapper(name1, name2, domain)

    if role1.name == role2.name then
        return true
    end

    if not (self:hasRole(role1)) or not (self:hasRole(role2)) then
        return false
    end

    if not self.matchingFunc and not self.domainMatchingFunc then
        return role1:hasRole(role2, self.maxHierarchyLevel)
    end

    for _, role in pairs(self.allRoles) do
        local flag = true
        if self.domainMatchingFunc then
            if not self.domainMatchingFunc(domain, role.domain) then
                flag = false
            end
        else
            if domain ~= role.domain then
                flag = false
            end
        end

        if flag then
            if self.matchingFunc then
                if self.matchingFunc(role1.name, role.name) and role:hasRole(role2, self.maxHierarchyLevel, self.matchingFunc, self.domainMatchingFunc) then
                    return true
                end
            else
                if role1.name == role.name and role:hasRole(role2, self.maxHierarchyLevel, self.matchingFunc, self.domainMatchingFunc) then
                    return true
                end
            end
        end
    end
    
    return false
end

--  * getRoles gets the roles that a subject inherits. domain is a prefix to the roles.
function DefaultRoleManager:getRoles(name, ...)
    local domain = {...}
    if #domain > 1 then
        error("domain should be only 1 parameter")
    elseif #domain == 1 then
        domain = domain[1]
    else
        domain = ""
    end

    local role = self:roleDomainWrapper(name, domain)

    if not self:hasRole(role) then
        return {}
    end

    local roles = self:createRole(name, domain):getRoles()

    return roles
end

-- getUsers gets the users that inherits a subject.
function DefaultRoleManager:getUsers(name, ...)
    local domain = {...}
    if #domain > 1 then
        error("domain should be only 1 parameter")
    elseif #domain == 1 then
        domain = domain[1]
    else
        domain = ""
    end

    local targetRole = self:roleDomainWrapper(name, domain)

    local names = {}
    for _, role in pairs(self.allRoles) do
        if role:hasDirectRole(targetRole) then
            table.insert(names, role.name)
        end
    end

    return names
end

-- printRoles prints all the roles to log.
function DefaultRoleManager:printRoles()

    self.logger:info("Roles: ")
    for _, role in pairs(self.allRoles) do
        local text = role:toString()
        if text then 
            self.logger:info(text)
        end
    end

end

function DefaultRoleManager:roleDomainWrapper(name, domain)
    if type(domain) ~= "string" then
        if not domain or #domain==0 then
            domain = ""
        elseif #domain == 1 then
            domain = domain[1]
        else
            error("domain should be only 1 parameter")
        end
    end

    local role = Role:new(name, domain)
    if not self:hasRole(role) then
        return role
    end
    return self:createRole(name, domain)
end

function DefaultRoleManager:twoRoleDomainWrapper(name1, name2, domain)
    return self:roleDomainWrapper(name1, domain), self:roleDomainWrapper(name2, domain)
end

return DefaultRoleManager
