local d={}local t=game:GetService("\083\116\117\100\105\111\083\101\114\118\105\099\101")local l=require(script:FindFirstAncestor("\080\108\117\103\105\110\070\114\097\109\101\119\111\114\107"))local m local S local N local B local R local v local j local q local i local K local G="\108\111\099\097\108 \115\101\116\116\105\110\103\115 \061 \123\125"local Z local b local F local g local C=false function d.CleanSettings(t,d)for t,l in pairs(b)do local m=Z and Z[t]or nil local S=d[t]local B=R:ReturnFirstNonNil(m,l)if d[t]==nil then d[t]=B S=B end local v=typeof(l)local j=typeof(S)if v~=j then N:Warn(("\073\115\115\117\101 \119\105\116\104 \083\101\116\116\105\110\103 \037\115\058\037\115\046 \069\120\112\101\099\116\101\100 \116\121\112\101 \037\113\044 \103\111\116 \037\113\046 \078\101\119 \118\097\108\117\101\058 \037\113"):format(t,tostring(S),v,j,tostring(B)))d[t]=B end local q=F[t]local i=q and q(S)if i then N:Warn(("\073\115\115\117\101 \119\105\116\104 \083\101\116\116\105\110\103 \040\037\115\058\037\115\041\046 \037\115\046 \078\101\119 \118\097\108\117\101\058 \037\113"):format(t,tostring(S),i,tostring(B)))d[t]=B end end B:Sync(d,b)end function d.SaveSettings(l,t)d:CleanSettings(t)Z=t local N=K:Serialize(t)m:SetSetting(S.Setting,N)end function d.LoadSettings(t)local l=m:GetSetting(S.Setting)or{}local N=K:Deserialize(l)d:CleanSettings(N)Z=N return N end function d.ValidateSettings(t)local l=d:LoadSettings()d:SaveSettings(l)C=true end function d.GetSetting(l,t)while not C do task.wait()end local m=Z or d:LoadSettings()local S=m[t]if S==nil then N:Error(("\078\111 \083\101\116\116\105\110\103 \037\113"):format(t))end return S end function d.GetDefaultSettings(d)return b end function d.OpenSettings(l)local K=m:GetSetting(S.Setting)local Z=B:KeysToArray(K)table.sort(Z)local F=g.Source local C,L=F:find(G)if not(C and L)then N:Error(("\067\111\117\108\100 \110\111\116 \102\105\110\100 \112\097\116\116\101\114\110 \037\113 \105\110 \n\037\113"):format(G,F))end local h="\108\111\099\097\108 \115\101\116\116\105\110\103\115 \061 \123"for t,l in pairs(Z)do local m=d:GetSetting(l)h=("\037\115\n    \037\115 \061 \037\115\044"):format(h,l,R:ValueToSourceString(m))end h=("\037\115\n\125"):format(h)local O=F:gsub(G,h,1)local r=Instance.new("\077\111\100\117\108\101\083\099\114\105\112\116")r.Name=i:GetUserIdentifier()r.Source=O r.Parent=v.Folders.Directory;(m:GetPlugin()):OpenScript(r)local function Y()local t local l,m=pcall(function()t=require(r)end)if not l then N:Warn(("\069\114\114\111\114 \111\099\099\117\114\101\100 \119\104\101\110 \101\120\105\116\105\110\103 \115\101\116\116\105\110\103\115\044 \110\111 \099\104\097\110\103\101\115 \119\101\114\101 \097\098\108\101 \116\111 \098\101 \115\097\118\101\100 \040\037\115\041"):format(m))else d:SaveSettings(t)if d:GetSetting("\085\115\101\068\101\102\097\117\108\116\083\101\116\116\105\110\103\115")==true then d:SaveSettings(B:DeepCopy(b))end q:Refresh()N:Info("\083\101\116\116\105\110\103\115 \117\112\100\097\116\101\100\033")end end local e local u u=(t:GetPropertyChangedSignal("\065\099\116\105\118\101\083\099\114\105\112\116")):Connect(function()local d=e and e==r if d then Y()r:Destroy()u:Disconnect()end e=t.ActiveScript end)local M=j:GetRunJanitor()M:Add(function()Y()r:Destroy()u:Disconnect()end)end function d.FrameworkInit(d)m=l:Require("\080\108\117\103\105\110\072\097\110\100\108\101\114")S=l:Require("\080\108\117\103\105\110\067\111\110\115\116\097\110\116\115")N=l:Require("\076\111\103\103\101\114")B=l:Require("\084\097\098\108\101\085\116\105\108")R=l:Require("\086\097\108\117\101\085\116\105\108")v=l:Require("\083\116\117\100\105\111\072\097\110\100\108\101\114")j=l:Require("\083\111\099\107\101\116\067\111\110\116\114\111\108\108\101\114")q=l:Require("\087\105\100\103\101\116\072\097\110\100\108\101\114")i=l:Require("\083\116\117\100\105\111\085\116\105\108")K=l:Require("\083\101\116\116\105\110\103\115\085\116\105\108")end function d.FrameworkStart(d)local t=require(script.Parent.Settings.defaultSettings)b=t.defaultSettings F=t.validationFunctions g=script.Parent.Settings.settingsTemplate end return d
---
---SocketSettings
---
---@class SocketSettings
---
local SocketSettings = {}

--------------------------------------------------
-- Types
-- ...

--------------------------------------------------
-- Dependencies
local StudioService = game:GetService("StudioService") ---@type StudioService
local PluginFramework = require(script:FindFirstAncestor("PluginFramework")) ---@type Framework
local PluginHandler ---@type PluginHandler
local PluginConstants ---@type PluginConstants
local Logger ---@type Logger
local TableUtil ---@type TableUtil
local ValueUtil ---@type ValueUtil
local StudioHandler ---@type StudioHandler
local SocketController ---@type SocketController
local WidgetHandler ---@type WidgetHandler
local StudioUtil ---@type StudioUtil
local SettingsUtil ---@type SettingsUtil

--------------------------------------------------
-- Constants
local TEMPLATE_SOURCE_SETTINGS_LINE = "local settings = {}"

--------------------------------------------------
-- Members
local cachedLoadedSettings ---@type SocketSettings
local defaultSettings ---@type SocketSettings
local validationFunctions ---@type table
local settingsTemplateFile ---@type ModuleScript
local isValidated = false

---
---Cleans the table of passed settings; syncs with default values, ensures correct types and runs any validation functions.
---If any issues, uses the currently stored value or default value (whichever exists first)
---@param settings SocketSettings
---
function SocketSettings:CleanSettings(settings)
    -- Validate settings
    for settingName, defaultValue in pairs(defaultSettings) do
        -- Get values
        local storedValue = cachedLoadedSettings and cachedLoadedSettings[settingName] or nil
        local setValue = settings[settingName]
        local overrideValue = ValueUtil:ReturnFirstNonNil(storedValue, defaultValue)

        -- SILENT: Doesn't exist
        if settings[settingName] == nil then
            settings[settingName] = overrideValue
            setValue = overrideValue
        end

        -- ISSUE: Bad type
        local defaultType = typeof(defaultValue)
        local setType = typeof(setValue)
        if defaultType ~= setType then
            Logger:Warn(
                ("Issue with Setting %s:%s. Expected type %q, got %q. New value: %q"):format(
                    settingName,
                    tostring(setValue),
                    defaultType,
                    setType,
                    tostring(overrideValue)
                )
            )
            settings[settingName] = overrideValue
        end

        -- ISSUE: Bad validation
        local validationFunction = validationFunctions[settingName]
        local validationError = validationFunction and validationFunction(setValue)
        if validationError then
            Logger:Warn(
                ("Issue with Setting (%s:%s). %s. New value: %q"):format(
                    settingName,
                    tostring(setValue),
                    validationError,
                    tostring(overrideValue)
                )
            )
            settings[settingName] = overrideValue
        end
    end

    -- Sync
    TableUtil:Sync(settings, defaultSettings)
end

---
---Stores the passed settings into our plugin
---
function SocketSettings:SaveSettings(settings)
    -- Clean
    SocketSettings:CleanSettings(settings)

    -- Cache
    cachedLoadedSettings = settings

    -- Serialize
    local serializedSettings = SettingsUtil:Serialize(settings)

    -- Store
    PluginHandler:SetSetting(PluginConstants.Setting, serializedSettings)
end

---
---@return table
---
function SocketSettings:LoadSettings()
    -- Deserialize
    local serializedSettings = PluginHandler:GetSetting(PluginConstants.Setting) or {}
    local deserializedSettings = SettingsUtil:Deserialize(serializedSettings)

    -- Clean
    SocketSettings:CleanSettings(deserializedSettings)

    -- Cache
    cachedLoadedSettings = deserializedSettings

    return deserializedSettings
end

---
---Will ensure the stored settings on the plugin is in line with our template.
---Usually called on startup.
---
function SocketSettings:ValidateSettings()
    -- Load, then save incase anything happened during cleaning
    local loadedSettings = SocketSettings:LoadSettings()
    SocketSettings:SaveSettings(loadedSettings)

    -- Cache Action
    isValidated = true
end

---
---Returns the currently set value for this settingName
---@param settingName string
---@return any
---
function SocketSettings:GetSetting(settingName)
    -- Ensure validated
    while not isValidated do
        task.wait()
    end

    local settings = cachedLoadedSettings or SocketSettings:LoadSettings()
    local settingValue = settings[settingName]

    -- ERROR: Bad settingName
    if settingValue == nil then
        Logger:Error(("No Setting %q"):format(settingName))
    end

    return settingValue
end

---
---@return SocketSettings
---
function SocketSettings:GetDefaultSettings()
    return defaultSettings
end

---
---Opens up a settings file, with the users settings loaded on.
---When the user closes the file, we read their settings.
---
function SocketSettings:OpenSettings()
    -- Get Settings Names
    local storedSettings = PluginHandler:GetSetting(PluginConstants.Setting)
    local settingsNames = TableUtil:KeysToArray(storedSettings)
    table.sort(settingsNames) -- Alphabetical order

    -- Create source
    local templateSource = settingsTemplateFile.Source ---@type string
    local pos0, pos1 = templateSource:find(TEMPLATE_SOURCE_SETTINGS_LINE)
    if not (pos0 and pos1) then
        Logger:Error(("Could not find pattern %q in \n%q"):format(TEMPLATE_SOURCE_SETTINGS_LINE, templateSource))
    end

    -- Replace settings = {} with a populated version
    local newSettingsLine = "local settings = {"
    for _, settingName in pairs(settingsNames) do
        local settingValue = SocketSettings:GetSetting(settingName)
        newSettingsLine = ("%s\n    %s = %s,"):format(newSettingsLine, settingName, ValueUtil:ValueToSourceString(settingValue))
    end
    newSettingsLine = ("%s\n}"):format(newSettingsLine)

    -- Swap out with our populated version
    local newSource = templateSource:gsub(TEMPLATE_SOURCE_SETTINGS_LINE, newSettingsLine, 1)

    -- Create module script to host the settings, with the new source
    local settingsScript = Instance.new("ModuleScript") ---@type ModuleScript
    settingsScript.Name = StudioUtil:GetUserIdentifier()
    settingsScript.Source = newSource
    settingsScript.Parent = StudioHandler.Folders.Directory

    -- Open her up
    PluginHandler:GetPlugin():OpenScript(settingsScript)

    --------------------------------------------------
    local function saveSettingsOnClose()
        -- Get Settings
        local newSettings ---@type SocketSettings
        local success, err = pcall(function()
            newSettings = require(settingsScript)
        end)
        if not success then
            Logger:Warn(("Error occured when exiting settings, no changes were able to be saved (%s)"):format(err))
        else
            -- Save
            SocketSettings:SaveSettings(newSettings)

            -- OVERRIDE: Has default settings been enabled?
            if SocketSettings:GetSetting("UseDefaultSettings") == true then
                SocketSettings:SaveSettings(TableUtil:DeepCopy(defaultSettings))
            end

            -- Refresh widget
            WidgetHandler:Refresh()

            Logger:Info("Settings updated!")
        end
    end

    -- Listener to the user viewing different scripts; used to know when to read + write the settingsScript
    local cachedActiveScript ---@type Instance
    local activeScriptConnection ---@type RBXScriptConnection
    activeScriptConnection = StudioService:GetPropertyChangedSignal("ActiveScript"):Connect(function()
        local justClosedSettings = cachedActiveScript and cachedActiveScript == settingsScript
        if justClosedSettings then
            saveSettingsOnClose()

            -- Close up shop!
            settingsScript:Destroy()
            activeScriptConnection:Disconnect()
        end

        -- Update cache
        cachedActiveScript = StudioService.ActiveScript
    end)

    -- Cleanup incase plugin closes
    local runJanitor = SocketController:GetRunJanitor()
    runJanitor:Add(function()
        saveSettingsOnClose()
        settingsScript:Destroy()
        activeScriptConnection:Disconnect()
    end)
end

---
---@private
---
---Asynchronously called with all other FrameworkInit()
---
function SocketSettings:FrameworkInit()
    PluginHandler = PluginFramework:Require("PluginHandler")
    PluginConstants = PluginFramework:Require("PluginConstants")
    Logger = PluginFramework:Require("Logger")
    TableUtil = PluginFramework:Require("TableUtil")
    ValueUtil = PluginFramework:Require("ValueUtil")
    StudioHandler = PluginFramework:Require("StudioHandler")
    SocketController = PluginFramework:Require("SocketController")
    WidgetHandler = PluginFramework:Require("WidgetHandler")
    StudioUtil = PluginFramework:Require("StudioUtil")
    SettingsUtil = PluginFramework:Require("SettingsUtil")
end

---
---@private
---
---Synchronously called, one after the other, with all other FrameworkStart()
---
function SocketSettings:FrameworkStart()
    local defaultSettingsTable = require(script.Parent.Settings.defaultSettings)
    defaultSettings = defaultSettingsTable.defaultSettings
    validationFunctions = defaultSettingsTable.validationFunctions

    settingsTemplateFile = script.Parent.Settings.settingsTemplate
end

return SocketSettings
