--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 Config = {
    DEFAULT_SECTION = "default",
    DEFAULT_COMMENT = "#",
    DEFAULT_COMMENT_SEM = ";",
    DEFAULT_MULTI_LINE_SEPARATOR = "\\\\", -- This is equivalent to "\\" in the text since "\" is a special character in Lua
}

--[[
    * newConfig create an empty configuration representation from file.
    *
    * @param confName the path of the model file.
    * @return the constructor of Config.
]]
function Config:newConfig(confName)
    local c = {}
    setmetatable(c,self)
    self.__index = self
    c.data = {}
    c:parse(confName)
    return c
end

--[[
    * newConfigFromText create an empty configuration representation from text.
    *
    * @param text the model text.
    * @return the constructor of Config.
]]
function Config:newConfigFromText(text)
   	local c = {}
	setmetatable(c,self)
	self.__index = self
	c.data = {}
	local lines = {}
	string.gsub(text, '[^'.."\r\n"..']+', function(w) table.insert(lines, w) end )
	c:parseBuffer(lines)
	return c
end

-- addConfig adds a new section->key:value to the configuration.
function Config:addConfig(section, option, value)
    if section == "" then section = self.DEFAULT_SECTION end

    if self.data[section] == nil then
        self.data[section] = {}
    end

    self.data[section][option] = value
end

function Config:parse(fname)
    local lines = {}
    local f = assert(io.open(fname,"r"))

    if f then
        for line in f:lines() do
            table.insert(lines, line)
        end
    end

    f:close()
    self:parseBuffer(lines)
end

function Config:parseBuffer(lines)
    local section = ""
    local buf = {}
    local lineNum = 0
    local canWrite = false
	local line

    while true do
        if canWrite then
            if self:write(section, lineNum, buf) == true then
                buf = {}
            end
            canWrite = false
        end
        lineNum = lineNum + 1

        line = lines[lineNum]

        if lineNum>#lines then
            if #buf>0 then
                self:write(section, lineNum, buf)
            end
            break
        end

        line = Util.trim(line)

        if line == "" or self.DEFAULT_COMMENT == string.sub(line, 1, 1) or self.DEFAULT_COMMENT_SEM == string.sub(line, 1, 1) then
            canWrite = true
        elseif "[" == string.sub(line, 1, 1) and "]" == string.sub(line, -1, -1) then
            if #buf>0 then
                if self:write(section, lineNum, buf) == true then
                    buf = {}
                end
            end

            section = string.sub(line, 2, -2)
        else
            local p

            if self.DEFAULT_MULTI_LINE_SEPARATOR == string.sub(line, -2, -1) then
                p = Util.trim(string.sub(line, 1, -3))
                p = p .. " "
            else
                p = line
                canWrite = true
            end
        table.insert(buf, p)
        end
    end
end

function Config:write(section, lineNum, b)
    local buf = ""
    for _, v in pairs(b) do
        buf = buf .. v
    end

    if buf == "" then return end

    local optionVal = Util.split(buf, "=", 1)

    if #optionVal~=2 then
        error("parse the content error : line "..lineNum.." , "..optionVal[1].." = ?")
    end

    local option = Util.trim(optionVal[1])
    local value = Util.trim(optionVal[2])

    self:addConfig(section, option, value)
    return true
end

-- getBool lookups up the value using the provided key and converts the value to a bool
function Config:getBool(key)
    local s = self:get(key)
    if string.upper(s) == "TRUE" then
        return true
    elseif string.upper(s) == "FALSE" then
        return false
    else
        error("Not a boolean value")
    end
end

-- getNum lookups up the value using the provided key and converts the value to a number
function Config:getNum(key)
    local s = self:get(key)
    if tonumber(s) then
        return tonumber(s)
    else
        error("Not a num value")
    end
end

-- getString lookups up the value using the provided key and converts the value to a string
function Config:getString(key)
    return self:get(key)
end

--[[
    Strings lookups up the value using the provided key and converts the value to an array
    of string by splitting the string by comma.
]]
function Config:getStrings(key)
    local s = self:get(key)
    if s == "" then return end
    local v = Util.split(s, ",")

    return v
end

-- Set sets the value for the specific key in the Config
function Config:set(key, value)
    if #key == 0 then
        error("key is empty")
    end

    local section = ""
    local option

    local keys = Util.split(string.lower(key), "::")
    if #keys >= 2 then
        section = keys[1]
        option = keys[2]
    else
        option = keys[1]
    end

    self:addConfig(section, option, value)
end

-- section.key or key
function Config:get(key)
    local section = self.DEFAULT_SECTION
    local option

    local keys = Util.split(string.lower(key), "::")
    if #keys >= 2 then
        section = keys[1]
        option = keys[2]
    else
        option = keys[1]
    end

    if self.data[section] == nil then
        return ""
    elseif self.data[section][option] == nil then
        return ""
    else
        return self.data[section][option]
    end
end

return Config