OptionModule = OptionModule or class(ModuleBase)

--Need a better name for this
OptionModule.type_name = "Options"

function OptionModule:init(core_mod, config)
    self.super.init(self, core_mod, config)

    self.FileName = self._config.save_file or self._mod.Name .. "_Options.txt"

    self._storage = {}

    if not self._config.options then
        BeardLib:log(string.format("[ERROR] Mod: %s, must contain an options table for the OptionModule", self._mod.Name))
        return
    end

    if self._config.loaded_callback then
        self._on_load_callback = self._mod:StringToCallback(self._config.loaded_callback)
    end


    if self._config.auto_build_menu then
        self:BuildMenuHook()
    end
end

function OptionModule:post_init()
    if self._post_init_complete then
        return false
    end

    self:InitOptions(self._config.options, self._storage)

    if self._config.auto_load then
        self:Load()
    end

    self.super.post_init(self)

    return true
end

function OptionModule:Load()
    if not io.file_is_readable(self._mod.SavePath .. self.FileName) then
        --Save the Options file with the current option values
        self:Save()
        return
    end

    local file = io.open(self._mod.SavePath .. self.FileName, 'r')

    --pcall for json decoding
    local ret, data = pcall(function() return json.custom_decode(file:read("*all")) end)

    if not ret then
        BeardLib:log("[ERROR] Unable to load save file for mod, " .. self._mod.Name)
        BeardLib:log(tostring(data))

        --Save the corrupted file incase the option values should be recovered
        local corrupted_file = io.open(self._mod.SavePath .. self.FileName .. "_corrupted", "w+")
        corrupted_file:write(file:read("*all"))

        corrupted_file:close()

        --Save the Options file with the current option values
        self:Save()
        return
    end

    --Close the file handle
    file:close()

    --Merge the loaded options with the existing options
    self:ApplyValues(self._storage, data)

    if self._on_load_callback then
        self._on_load_callback()
    end
end

function OptionModule:ApplyValues(tbl, value_tbl)
    if tbl._meta == "option_set" and tbl.not_pre_generated then
        for key, value in pairs(value_tbl) do
            local new_tbl = BeardLib.Utils:RemoveAllNumberIndexes(tbl.item_parameters and deep_clone(tbl.item_parameters) or {})
            new_tbl._meta = "option"
            new_tbl.name = key
            new_tbl.value = value
            tbl[key] = new_tbl
        end
        return
    end

    for i, sub_tbl in pairs(tbl) do
        if type(sub_tbl) == "table" and sub_tbl._meta then
            if sub_tbl._meta == "option" and value_tbl[sub_tbl.name] ~= nil then
                local value = value_tbl[sub_tbl.name]
                if sub_tbl.type == "multichoice" then
                    if sub_tbl.save_value then
                        local index = table.index_of(sub_tbl.values, value)
                        value = index ~= -1 and index or sub_tbl.default_value
                    else
                        if value > #sub_tbl.values then
                            value = sub_tbl.default_value
                        end
                    end
                end
                sub_tbl.value = value
            elseif (sub_tbl._meta == "option_group" or sub_tbl._meta == "option_set") and value_tbl[sub_tbl.name] then
                self:ApplyValues(sub_tbl, value_tbl[sub_tbl.name])
            end
        end
    end
end

function OptionModule:InitOptions(tbl, option_tbl)
    for i, sub_tbl in ipairs(tbl) do
        if sub_tbl._meta then
            if sub_tbl._meta == "option" then
                if sub_tbl.type == "multichoice" then
                    sub_tbl.values = sub_tbl.values_tbl and self._mod:StringToTable(sub_tbl.values_tbl) or BeardLib.Utils:RemoveNonNumberIndexes(sub_tbl.values)
                end

                if sub_tbl.value_changed then
                    sub_tbl.value_changed = self._mod:StringToCallback(sub_tbl.value_changed)
                end

                if sub_tbl.converter then
                    sub_tbl.converter = self._mod:StringToCallback(sub_tbl.converter)
                end

                if sub_tbl.enabled_callback then
                    sub_tbl.enabled_callback = self._mod:StringToCallback(sub_tbl.enabled_callback)
                end
                sub_tbl.default_value = type(sub_tbl.default_value) == "string" and BeardLib.Utils:normalize_string_value(sub_tbl.default_value) or sub_tbl.default_value
                option_tbl[sub_tbl.name] = sub_tbl
                option_tbl[sub_tbl.name].value = sub_tbl.default_value
            elseif sub_tbl._meta == "option_group" then
                option_tbl[sub_tbl.name] = BeardLib.Utils:RemoveAllSubTables(clone(sub_tbl))
                self:InitOptions(sub_tbl, option_tbl[sub_tbl.name])
            elseif sub_tbl._meta == "option_set" then
                if not sub_tbl.not_pre_generated then
                    local tbl = sub_tbl.items and BeardLib.Utils:RemoveNonNumberIndexes(sub_tbl.items)
                    if sub_tbl.items_tbl then
                        tbl = self._mod:StringToTable(sub_tbl.values_tbl)
                    elseif sub_tbl.populate_items then
                        local clbk = self._mod:StringToCallback(sub_tbl.populate_items)
                        tbl = assert(clbk)()
                    end

                    for _, item in pairs(tbl) do
                        local new_tbl = BeardLib.Utils:RemoveAllNumberIndexes(deep_clone(sub_tbl.item_parameters))
                        new_tbl._meta = "option"
                        table.insert(sub_tbl, table.merge(new_tbl, item))
                    end
                end
                option_tbl[sub_tbl.name] = BeardLib.Utils:RemoveAllSubTables(clone(sub_tbl))
                self:InitOptions(sub_tbl, option_tbl[sub_tbl.name])
            end
        end
    end
end

--Only for use by the SetValue function
function OptionModule:_SetValue(tbl, name, value, full_name)
    if tbl[name] == nil then
        BeardLib:log(string.format("[ERROR] Option of name %q does not exist in mod, %s", name, self._mod.Name))
        return
    end
    tbl[name].value = value

    if tbl[name].value_changed then
        tbl[name].value_changed(full_name, value)
    end
end

function OptionModule:SetValue(name, value)
    if string.find(name, "/") then
        local string_split = string.split(name, "/")

        local option_name = table.remove(string_split)

        local tbl = self._storage
        for _, part in pairs(string_split) do
            if tbl[part] == nil then
                BeardLib:log(string.format("[ERROR] Option Group of name %q does not exist in mod, %s", name, self._mod.Name))
                return
            end
            tbl = tbl[part]
        end

        self:_SetValue(tbl, option_name, value, name)
    else
        self:_SetValue(self._storage, name, value, name)
    end

    self:Save()
end

function OptionModule:GetOption(name)
    if string.find(name, "/") then
        local string_split = string.split(name, "/")

        local option_name = table.remove(string_split)

        local tbl = self._storage
        for _, part in pairs(string_split) do
            if tbl[part] == nil then
                BeardLib:log(string.format("[ERROR] Option of name %q does not exist in mod, %s", name, self._mod.Name))
                return
            end
            tbl = tbl[part]
        end

        return tbl[option_name]
    else
        return self._storage[name]
    end
end

function OptionModule:GetValue(name, real)
    local option = self:GetOption(name)
    if option then
        if real then
            if option.converter then
                return option.converter(option, option.value)
            elseif option.type == "multichoice" then
                if type(option.values[option.value]) == "table" then
                    return option.values[option.value].value
                else
                    return option.values[option.value]
                end
            end
        end
        return option.value
    else
        return
    end

    return option.value
end

function OptionModule:LoadDefaultValues()
    self:_LoadDefaultValues(self._storage)
end

function OptionModule:_LoadDefaultValues(option_tbl)
    for i, sub_tbl in pairs(option_tbl) do
        if sub_tbl._meta then
            if sub_tbl._meta == "option" and sub_tbl.default_value ~= nil then
                option_tbl[sub_tbl.name].value = sub_tbl.default_value
            elseif sub_tbl._meta == "option_group" or sub_tbl._meta == "option_set" then
                self:_LoadDefaultValues(option_tbl[sub_tbl.name])
            end
        end
    end
end

function OptionModule:PopulateSaveTable(tbl, save_tbl)
    for i, sub_tbl in pairs(tbl) do
        if type(sub_tbl) == "table" and sub_tbl._meta then
            if sub_tbl._meta == "option" then
                local value = sub_tbl.value
                if sub_tbl.type=="multichoice" and sub_tbl.save_value then
                    if type(sub_tbl.values[sub_tbl.value]) == "table" then
                        value = sub_tbl.values[sub_tbl.value].value
                    else
                        value = sub_tbl.values[sub_tbl.value]
                    end
                end
                save_tbl[sub_tbl.name] = value
            elseif sub_tbl._meta == "option_group" or sub_tbl._meta == "option_set" then
                save_tbl[sub_tbl.name] = {}
                self:PopulateSaveTable(sub_tbl, save_tbl[sub_tbl.name])
            end
        end
    end
end

function OptionModule:Save()
    local file = io.open(self._mod.SavePath .. self.FileName, "w+")
    local save_data = {}
    self:PopulateSaveTable(self._storage, save_data)
	file:write(json.custom_encode(save_data))
	file:close()
end

function OptionModule:GetParameter(tbl, i)
    if tbl[i] then
        if type(tbl[i]) == "function" then
            return tbl[i]()
        else
            return tbl[i]
        end
    end

    return nil
end

function OptionModule:CreateSlider(parent_node, option_tbl, option_path)
    local id = self._mod.Name .. option_tbl.name

    option_path = option_path == "" and option_tbl.name or option_path .. "/" .. option_tbl.name

    local enabled = not self:GetParameter(option_tbl, "disabled")

    if option_tbl.enabled_callback then
        enabled = option_tbl:enabled_callback()
    end

    local self_vars = {
        option_key = option_path,
        module = self
    }

    local merge_data = self:GetParameter(option_tbl, "merge_data") or {}
    merge_data = BeardLib.Utils:RemoveAllNumberIndexes(merge_data)

    MenuHelperPlus:AddSlider(table.merge({
        id = self:GetParameter(option_tbl, "name"),
        title = self:GetParameter(option_tbl, "title_id") or id .. "TitleID",
        node = parent_node,
        desc = self:GetParameter(option_tbl, "desc_id") or id .. "DescID",
        callback = "OptionModuleGeneric_ValueChanged",
        min = self:GetParameter(option_tbl, "min"),
        max = self:GetParameter(option_tbl, "max"),
        step = self:GetParameter(option_tbl, "step"),
        enabled = enabled,
        value = self:GetValue(option_path),
        show_value = self:GetParameter(option_tbl, "show_value"),
        merge_data = self_vars
    }, merge_data))
end

function OptionModule:CreateToggle(parent_node, option_tbl, option_path)
    local id = self._mod.Name .. option_tbl.name

    option_path = option_path == "" and option_tbl.name or option_path .. "/" .. option_tbl.name

    local enabled = not self:GetParameter(option_tbl, "disabled")

    if option_tbl.enabled_callback then
        enabled = option_tbl:enabled_callback()
    end

    local self_vars = {
        option_key = option_path,
        module = self
    }

    local merge_data = self:GetParameter(option_tbl, "merge_data") or {}
    merge_data = BeardLib.Utils:RemoveAllNumberIndexes(merge_data)

    MenuHelperPlus:AddToggle(table.merge({
        id = self:GetParameter(option_tbl, "name"),
        title = self:GetParameter(option_tbl, "title_id") or id .. "TitleID",
        node = parent_node,
        desc = self:GetParameter(option_tbl, "desc_id") or id .. "DescID",
        callback = "OptionModuleGeneric_ValueChanged",
        value = self:GetValue(option_path),
        enabled = enabled,
        merge_data = self_vars
    }, merge_data))
end

function OptionModule:CreateMultiChoice(parent_node, option_tbl, option_path)
    local id = self._mod.Name .. option_tbl.name

    option_path = option_path == "" and option_tbl.name or option_path .. "/" .. option_tbl.name

    local options = self:GetParameter(option_tbl, "values")

    if not options then
        BeardLib:log("[ERROR] Unable to get an option table for option " .. option_tbl.name)
    end

    local enabled = not self:GetParameter(option_tbl, "disabled")

    if option_tbl.enabled_callback then
        enabled = option_tbl:enabled_callback()
    end

    local self_vars = {
        option_key = option_path,
        module = self
    }

    local merge_data = self:GetParameter(option_tbl, "merge_data") or {}
    merge_data = BeardLib.Utils:RemoveAllNumberIndexes(merge_data)

    MenuHelperPlus:AddMultipleChoice(table.merge({
        id = self:GetParameter(option_tbl, "name"),
        title = self:GetParameter(option_tbl, "title_id") or id .. "TitleID",
        node = parent_node,
        desc = self:GetParameter(option_tbl, "desc_id") or id .. "DescID",
        callback = "OptionModuleGeneric_ValueChanged",
        value = self:GetValue(option_path),
        items = options,
        localized_items = self:GetParameter(option_tbl, "localized_items"),
        enabled = enabled,
        merge_data = self_vars
    }, merge_data))
end

function OptionModule:CreateMatrix(parent_node, option_tbl, option_path, components)
    local id = self._mod.Name .. option_tbl.name

    option_path = option_path == "" and option_tbl.name or option_path .. "/" .. option_tbl.name

    local enabled = not self:GetParameter(option_tbl, "disabled")

    if option_tbl.enabled_callback then
        enabled = option_tbl:enabled_callback()
    end
    local scale_factor = self:GetParameter(option_tbl, "scale_factor") or 1

    local self_vars = {
        option_key = option_path,
        module = self,
        scale_factor = scale_factor,
        opt_type = option_tbl.type
    }

    local merge_data = self:GetParameter(option_tbl, "merge_data") or {}
    merge_data = BeardLib.Utils:RemoveAllNumberIndexes(merge_data)
    local base_params = table.merge({
        id = self:GetParameter(option_tbl, "name"),
        title = managers.localization:text(self:GetParameter(option_tbl, "title_id") or id .. "TitleID"),
        node = parent_node,
        desc = managers.localization:text(self:GetParameter(option_tbl, "desc_id") or id .. "DescID"),
        callback = "OptionModuleVector_ValueChanged",
        min = self:GetParameter(option_tbl, "min") or 0,
        max = self:GetParameter(option_tbl, "max") or scale_factor,
        step = self:GetParameter(option_tbl, "step") or (scale_factor > 1 and 1 or 0.01),
        enabled = enabled,
        show_value = self:GetParameter(option_tbl, "show_value"),
        localized = false,
        localized_help = false,
        merge_data = self_vars
    }, merge_data)
    local value = self:GetValue(option_path)
    local function GetComponentValue(val, component)
        return type(val[component]) == "function" and val[component](val) or val[component]
    end

    for _, vec in pairs(components) do
        local params = clone(base_params)
        params.id = params.id .. "-" .. vec.id
        params.title = params.title .. " - " .. vec.title
        params.desc = params.desc .. " - " .. vec.title
        params.merge_data.component = vec.id
        if vec.max then
            params.max = vec.max
        end
        params.value = GetComponentValue(value, vec.id) * scale_factor
        MenuHelperPlus:AddSlider(params)
    end
end

function OptionModule:CreateColour(parent_node, option_tbl, option_path)
    local alpha = not not self:GetParameter(option_tbl, "alpha")
    self:CreateMatrix(parent_node, option_tbl, option_path, { [1] =alpha and {id="a", title="A"} or nil, {id="r", title="R"}, {id="g", title="G"}, {id="b", title="B"} })
end

function OptionModule:CreateVector(parent_node, option_tbl, option_path)
    self:CreateMatrix(parent_node, option_tbl, option_path, { {id="x", title="X"}, {id="y", title="Y"}, {id="z", title="Z"} })
end

function OptionModule:CreateRotation(parent_node, option_tbl, option_path)
    self:CreateMatrix(parent_node, option_tbl, option_path, { {id="yaw", title="YAW"}, {id="pitch", title="PITCH"}, {id="roll", title="ROLL", max=90} })
end

function OptionModule:CreateOption(parent_node, option_tbl, option_path)
    if option_tbl.type == "number" then
        self:CreateSlider(parent_node, option_tbl, option_path)
    elseif option_tbl.type == "bool" or option_tbl.type == "boolean" then
        self:CreateToggle(parent_node, option_tbl, option_path)
    elseif option_tbl.type == "multichoice" then
        self:CreateMultiChoice(parent_node, option_tbl, option_path)
    elseif option_tbl.type == "colour" or option_tbl.type == "color" then
        self:CreateColour(parent_node, option_tbl, option_path)
    elseif option_tbl.type == "vector" then
        self:CreateVector(parent_node, option_tbl, option_path)
    elseif option_tbl.type == "rotation" then
        self:CreateRotation(parent_node, option_tbl, option_path)
    else
        BeardLib:log("[ERROR] No supported type for option " .. tostring(option_tbl.name) .. " in mod " .. self._mod.Name)
    end
end

function OptionModule:CreateDivider(parent_node, tbl)
    local merge_data = self:GetParameter(tbl, "merge_data") or {}
    merge_data = BeardLib.Utils:RemoveAllNumberIndexes(merge_data)
    MenuHelperPlus:AddDivider(table.merge({
        id = self:GetParameter(tbl, "name"),
        node = parent_node,
        size = self:GetParameter(tbl, "size")
    }, merge_data))
end

function OptionModule:CreateSubMenu(parent_node, option_tbl, option_path)
    option_path = option_path or ""
    local name = self:GetParameter(option_tbl, "name")
    local base_name = name and self._mod.Name .. name .. self._name or self._mod.Name .. self._name
    local menu_name = self:GetParameter(option_tbl, "node_name") or  base_name .. "Node"

    local merge_data = self:GetParameter(option_tbl, "merge_data") or {}
    merge_data = BeardLib.Utils:RemoveAllNumberIndexes(merge_data)
    local main_node = MenuHelperPlus:NewNode(nil, table.merge({
        name = menu_name
    }, merge_data))

    if option_tbl.build_items == nil or option_tbl.build_items then
        self:InitializeNode(main_node, option_tbl, name and (option_path == "" and name or option_path .. "/" .. name) or "")
    end

    MenuHelperPlus:AddButton({
        id = base_name .. "Button",
        title = self:GetParameter(option_tbl, "title_id") or base_name .. "ButtonTitleID",
        desc = self:GetParameter(option_tbl, "desc_id") or base_name .. "ButtonDescID",
        node = parent_node,
        next_node = menu_name
    })

    managers.menu:add_back_button(main_node)
end

function OptionModule:InitializeNode(node, option_tbl, option_path)
    option_tbl = option_tbl or self._config.options
    option_path = option_path or ""
    for i, sub_tbl in ipairs(option_tbl) do
        if sub_tbl._meta then
            if sub_tbl._meta == "option" and not sub_tbl.hidden then
                self:CreateOption(node, sub_tbl, option_path)
            elseif sub_tbl._meta == "divider" then
                self:CreateDivider(node, sub_tbl)
            elseif sub_tbl._meta == "option_group" or sub_tbl._meta == "option_set" and (sub_tbl.build_menu == nil or sub_tbl.build_menu) then
                self:CreateSubMenu(node, sub_tbl, option_path)
            end
        end
    end
end

function OptionModule:BuildMenuHook()
    Hooks:Add("MenuManagerSetupCustomMenus", self._mod.Name .. "Build" .. self._name .. "Menu", function(self_menu, nodes)
        self:BuildMenu(nodes[LuaModManager.Constants._lua_mod_options_menu_id])
    end)
end

function OptionModule:BuildMenu(node)
    self:CreateSubMenu(node, self._config.options)
end

--Create MenuCallbackHandler callbacks
Hooks:Add("BeardLibCreateCustomNodesAndButtons", "BeardLibOptionModuleCreateCallbacks", function(self_menu)
    MenuCallbackHandler.OptionModuleGeneric_ValueChanged = function(this, item)
        local value = item:value()
        if item.TYPE == "toggle" then value = value == "on" end
        OptionModule.SetValue(item._parameters.module, item._parameters.option_key, value)
    end

    MenuCallbackHandler.OptionModuleVector_ValueChanged = function(this, item)
        local cur_val =  OptionModule.GetValue(item._parameters.module, item._parameters.option_key)
        local new_value = item:value() / item._parameters.scale_factor
        if item._parameters.opt_type == "colour" or item._parameters.opt_type == "color" then
            cur_val[item._parameters.component] = new_value
        elseif item._parameters.opt_type == "vector" then
            if item._parameters.component == "x" then
                mvector3.set_x(cur_val, new_value)
            elseif item._parameters.component == "y" then
                mvector3.set_y(cur_val, new_value)
            elseif item._parameters.component == "z" then
                mvector3.set_z(cur_val, new_value)
            end
        elseif item._parameters.opt_type == "rotation" then
            local comp = item._parameters.component
            log("1" .. tostring(cur_val))
            mrotation.set_yaw_pitch_roll(cur_val, comp == "yaw" and new_value or cur_val:yaw(), comp == "pitch" and new_value or cur_val:pitch(), comp == "roll" and new_value or cur_val:roll())
            log("2" .. tostring(cur_val))
        end

        OptionModule.SetValue(item._parameters.module, item._parameters.option_key, cur_val)
    end
end)

BeardLib:RegisterModule(OptionModule.type_name, OptionModule)
