--- Config module
-- @module Config
-- luacheck: ignore 143 options reservedCharacters value

require 'stdlib/string'
require 'stdlib/table'

-----------------------------------------------------------------------
--Setup repeated code for use in sub functions here
-----------------------------------------------------------------------
local reservedCharacters = '`~!@#$%^&*+=|;:/\\\'",?()[]{}<>'
local testReservedCharacters = function(path)
    local reservedCharacters = reservedCharacters
    for c in reservedCharacters:gmatch('.') do
        if path:find(c, 1, true) then
            return c
        end
    end
    return nil
end

Config = {}

--- Creates a new Config object
-- to ease the management of a config table.
-- @param config_table [required] The table to be managed.
-- @return the Config instance for managing config_table
--
-- @usage --[Use a global table for config that persists across game save/loads]
--CONFIG = Config.new(global.testtable)
--
-- @usage --[You can also create a temporary scratch pad config]
--CONFIG = Config.new({}) -- Temporary scratch pad
--
-- @usage --[Setting data in Config]
--CONFIG = Config.new(global.testtable)
--CONFIG.set("your.path.here", "myvalue")
--
-- @usage --[Getting data out of Config]
--CONFIG = Config.new(global.testtable)
--my_data = CONFIG.get("your.path.here")
--
-- @usage --[Getting data out of Config with a default to use if path is not found in Config]
--CONFIG = Config.new(global.testtable)
--my_data = CONFIG.get("your.path.here", "Your Default here")
--
-- @usage --[Deleting a path from Config]
--CONFIG = Config.new(global.testtable)
--CONFIG.delete("your.path.here")
--
-- @usage --[Checking if a path exists in Config]
--CONFIG = Config.new(global.testtable)
--CONFIG.is_set("your.path.here")
function Config.new(config_table)
    if not config_table then
        error("config_table is a required parameter.", 2)
    elseif type(config_table) ~= "table" then
        error("config_table must be a table. Was given [" .. type(options) .. "]", 2)
    elseif type(config_table.get) == "function" then
        error("Config can't manage another Config object", 2)
    end

    -----------------------------------------------------------------------
    --Setup the Config object
    -----------------------------------------------------------------------
    local Config = {}

    --- Get a stored config value.
    -- @param path [required] a string representing the variable to retrieve
    -- @param default (optional) value to be used if path is nil
    -- @return value at path or nil if not found and no default given
    function Config.get(path, default)
        if type(path) ~= "string" or path:is_empty() then error("path is invalid", 2) end

        local config = config_table

        local c = testReservedCharacters(path)
        if c ~= nil then error("path '" .. path .. "' contains the reserved character '" .. c .. "'", 2) end

        local pathParts = path:split('.')
        local part = config;
        local value = nil;

        for key = 1, #pathParts, 1 do
            local partKey = pathParts[key]
            if (type(part) ~= "table") then
                value = nil;
                break;
            end

            value = part[partKey];
            part = part[partKey];
        end

        if (type(value) == "table") then
            --Force break references.
            return table.deepcopy(value);
        elseif (value ~= nil) then
            return value;
        else
            return default;
        end
    end

    --- Set a stored config value.
    -- @param path [required] a string, config path to set
    -- @param data (optional) Value to set path to. If nil it behaves identical to Config.delete()
    -- @return number 0 on failure; number of affected paths on success
    function Config.set(path, data)
        if type(path) ~= "string" or path:is_empty() then error("path is invalid", 2) end

        local config = config_table

        local c = testReservedCharacters(path)
        if c ~= nil then error("path contains the reserved character '" .. c .. "'", 2) end

        local pathParts = path:split('.')
        local part = config;
        local value = nil;

        for key = 1, #pathParts - 1, 1 do
            local partKey = pathParts[key]
            if (type(part[partKey]) ~= "table") then
                part[partKey] = {};
            end

            value = part[partKey];
            part = part[partKey];
        end

        part[pathParts[#pathParts]] = data;

        return 1;
    end

    --- Delete a stored config value.
    -- @param path a string, config path to delete
    -- @return number 0 on failure; number of affected paths on success
    function Config.delete(path)
        if type(path) ~= "string" or path:is_empty() then error("path is invalid", 2) end

        local config = config_table

        local c = testReservedCharacters(path)
        if c ~= nil then error("path contains the reserved character '" .. c .. "'", 2) end

        local pathParts = path:split('.')
        local part = config
        local value = nil

        for key = 1, #pathParts - 1, 1 do
            local partKey = pathParts[key]
            if (type(part[partKey]) ~= "table") then
                return 0
            end

            part = part[partKey]
        end

        if part[pathParts[#pathParts]] == nil then
            return 0
        else
            part[pathParts[#pathParts]] = nil
            return 1
        end
    end

    --- Test the existence of a stored config value.
    -- @param path a string, config path to test
    -- @return boolean, true on success, false otherwise
    function Config.is_set(path)
        if type(path) ~= "string" or path:is_empty() then error("path is invalid", 2) end

        return Config.get(path) ~= nil
    end

    return Config
end

return Config
