local d={}local t=game:GetService("\082\117\110\083\101\114\118\105\099\101")local l=(settings()).Studio local m=game:GetService("\083\116\117\100\105\111\083\101\114\118\105\099\101")local S=game:GetService("\085\115\101\114\073\110\112\117\116\083\101\114\118\105\099\101")local N=require(script:FindFirstAncestor("\080\108\117\103\105\110\070\114\097\109\101\119\111\114\107"))local B local R local v local j local q local i local K local G local Z local b local F local g local C local L local h=t:IsRunning()local O local r local Y=false function d.Run(t)if Y then return end Y=true O=i:GetRoduxStore()F:ValidateSettings()C:Cleanup()d:SetupStudioActions()d:SetupMacroActions()K:Run()d:SetupKeybindHooks()if h then b:RunTransfer()b:SetupCommunication()end local l=(O:getState())[j.RoduxStoreKey.MACROS].Groups for d,t in pairs(l)do for d,t in pairs(t.Macros)do local l=t.Macro if l.BindToOpen then l.BindToOpen(l,g:GetPlugin())end end end end function d.GetRunJanitor(d)return r end function d.GetStore(d)return O end function d.GetMacro(t,d)for t,l in pairs((O:getState())[j.RoduxStoreKey.MACROS].Groups)do for t,l in pairs(l.Macros)do if t==d then return l.Macro end end end end function d.IsRunning(t)return((d:GetStore()):getState())[j.RoduxStoreKey.STUDIO].IsRunning end local function e(d)local t=d:Clone()r:Add(t)local l local m,S=pcall(function()l=require(t)end)if m then return l else q:Warn(("\069\110\099\111\117\110\116\101\114\101\100 \105\115\115\117\101 \119\104\105\108\101 \109\097\110\097\103\105\110\103 \102\105\108\101 \037\113  -  \040\037\115\041"):format(d.Name,S))end end function d.SetupMacroActions(t)local function l(t)local l=e(t)local m=l and Z:CleanMacroDefinition(t,l)local S=d:GetMacro(t)if S then S._BindToClose(S,g:GetPlugin())end if m then local d={type=j.RoduxActionType.MACROS.UPDATE_MACRO,data={macro=m,script=t}}O:dispatch(d)else local l=d:GetMacro(t)if l then l._isBroken=true local d={type=j.RoduxActionType.MACROS.UPDATE_MACRO,data={macro=l;script=t}}O:dispatch(d)end end end local function S(t)local l=d:GetMacro(t)l._BindToClose(l,g:GetPlugin())local m={type=j.RoduxActionType.MACROS.REMOVE_MACRO;data={script=t}}O:dispatch(m)end local function N(d)local t=e(d)local l=t and Z:CleanMacroDefinition(d,t)if l then local t={type=j.RoduxActionType.MACROS.ADD_MACRO;data={macro=l;script=d;isFieldsOpen=F:GetSetting("\079\112\101\110\070\105\101\108\100\115\066\121\068\101\102\097\117\108\116")}}O:dispatch(t)local m=l.AutoRun and F:GetSetting("\069\110\097\098\108\101\065\117\116\111\082\117\110")if m then Z:RunMacro(l)end end end local function B(d)local t=(O:getState())[j.RoduxStoreKey.MACROS].Groups for t,l in pairs(t)do for t,l in pairs(l.Macros)do if t==d then return true end end end return false end local R local q=v.Folders.Macros local i=L:GetOurDirectory()local function K(d)if d.Parent:IsA("\077\111\100\117\108\101\083\099\114\105\112\116")then return end N(d)r:Add(d.Changed:Connect(function(t)if t=="\083\111\117\114\099\101"and R~=d then if B(d)then l(d)else N(d)end end end))end for d,t in pairs(q:GetDescendants())do if t:IsA("\077\111\100\117\108\101\083\099\114\105\112\116")then K(t)end end for d,t in pairs(i:GetDescendants())do if t:IsA("\077\111\100\117\108\101\083\099\114\105\112\116")then K(t)end end r:Add(q.DescendantAdded:Connect(function(d)if d:IsA("\077\111\100\117\108\101\083\099\114\105\112\116")then task.wait()K(d)end end))r:Add(i.DescendantAdded:Connect(function(d)if d:IsA("\077\111\100\117\108\101\083\099\114\105\112\116")then task.wait()K(d)end end))r:Add(q.DescendantRemoving:Connect(function(d)if d:IsA("\077\111\100\117\108\101\083\099\114\105\112\116")then if B(d)then S(d)end end end))r:Add(i.DescendantRemoving:Connect(function(d)if d:IsA("\077\111\100\117\108\101\083\099\114\105\112\116")then if B(d)then S(d)end end end))r:Add((m:GetPropertyChangedSignal("\065\099\116\105\118\101\083\099\114\105\112\116")):Connect(function()local d=m.ActiveScript if R then local d=R:IsDescendantOf(v.Folders.Directory)if d then if B(R)then l(R)else K(R)end end end R=d end))end function d.SetupStudioActions(m)local function S()K:Refresh()end S()r:Add(l.ThemeChanged:Connect(S))local N={type=j.RoduxActionType.STUDIO.IS_RUNNING,data={isRunning=t:IsRunning()}};(d:GetStore()):dispatch(N)end function d.SetupKeybindHooks(d)local t={}local function l(d,l)local m=F:GetSetting("\073\103\110\111\114\101\071\097\109\101\080\114\111\099\101\115\115\101\100\075\101\121\098\105\110\100\115")t[d.KeyCode]=l local S=O:getState()local N=S[j.RoduxStoreKey.MACROS].Groups for d,l in pairs(N)do for d,l in pairs(l.Macros)do local S=l.Macro local N=#S.Keybind local B=N>0 if B then local d=0 local l=m or S.IgnoreGameProcessedKeybinds for m,S in pairs(S.Keybind)do if t[S]==false or t[S]and l then d=d+1 end end local B=N==d local R=not S.State.IsKeybindDisabled if B and R then Z:RunMacro(S)t={}return end end end end end local function m(d,l)t[d.KeyCode]=nil end r:Add(S.InputBegan:Connect(l))r:Add(S.InputEnded:Connect(m))end function d.Stop(d)if not Y then return end Y=false local t=(O:getState())[j.RoduxStoreKey.MACROS].Groups for d,t in pairs(t)do for d,t in pairs(t.Macros)do local l=t.Macro l._BindToClose(l,g:GetPlugin())end end C:Cleanup()r:Cleanup()O=nil K:Stop()end function d.FrameworkInit(d)B=N:Require("\074\097\110\105\116\111\114")R=N:Require("\082\111\100\117\120")v=N:Require("\083\116\117\100\105\111\072\097\110\100\108\101\114")j=N:Require("\083\111\099\107\101\116\067\111\110\115\116\097\110\116\115")q=N:Require("\076\111\103\103\101\114")i=N:Require("\083\111\099\107\101\116\082\111\100\117\120\083\116\111\114\101\067\111\110\116\114\111\108\108\101\114")K=N:Require("\087\105\100\103\101\116\072\097\110\100\108\101\114")G=N:Require("\077\097\099\114\111\067\111\110\115\116\097\110\116\115")Z=N:Require("\077\097\099\114\111\072\101\108\112\101\114")b=N:Require("\077\097\099\114\111\067\108\105\101\110\116\083\101\114\118\101\114")F=N:Require("\083\111\099\107\101\116\083\101\116\116\105\110\103\115")g=N:Require("\080\108\117\103\105\110\072\097\110\100\108\101\114")C=N:Require("\073\110\115\116\097\110\099\101\085\116\105\108")L=N:Require("\076\111\099\097\108\077\097\099\114\111\115")end function d.FrameworkStart(d)r=B.new()end return d
---
---The main script for running Socket (when the player has "activated" the plugin + has the widget window open)
---
---@class SocketController
---
local SocketController = {}

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

--------------------------------------------------
-- Dependencies
local RunService = game:GetService("RunService") ---@type RunService
local Studio = settings().Studio ---@type Studio
local StudioService = game:GetService("StudioService") ---@type StudioService
local UserInputService = game:GetService("UserInputService") ---@type UserInputService
local PluginFramework = require(script:FindFirstAncestor("PluginFramework")) ---@type Framework
local Janitor ---@type Janitor
local Rodux ---@type Rodux
local StudioHandler ---@type StudioHandler
local SocketConstants ---@type SocketConstants
local Logger ---@type Logger
local SocketRoduxStoreController ---@type SocketRoduxStoreController
local WidgetHandler ---@type WidgetHandler
local MacroConstants ---@type MacroConstants
local MacroHelper ---@type MacroHelper
local MacroClientServer ---@type MacroClientServer
local SocketSettings ---@type SocketSettings
local PluginHandler ---@type PluginHandler
local InstanceUtil ---@type InstanceUtil
local LocalMacros ---@type LocalMacros

--------------------------------------------------
-- Constants
local IS_RUNNING = RunService:IsRunning()

--------------------------------------------------
-- Members
local roduxStore ---@type RoduxStore
local runJanitor ---@type Janitor
local isRunning = false ---@type boolean

---
---Runs necessary routines when Socket gets activated
---
function SocketController:Run()
    -- RETURN: Already running
    if isRunning then
        return
    end
    isRunning = true

    -- Init store
    roduxStore = SocketRoduxStoreController:GetRoduxStore()

    -- Setup settings
    SocketSettings:ValidateSettings()

    -- Cleanup any instances from a bad shutdown
    InstanceUtil:Cleanup()

    -- Setup rodux store actions
    SocketController:SetupStudioActions()
    SocketController:SetupMacroActions()

    -- Tell the widget handler its go time
    WidgetHandler:Run()

    -- Setup Keybind listener
    SocketController:SetupKeybindHooks()

    -- One time server/client setup
    if IS_RUNNING then
        MacroClientServer:RunTransfer()
        MacroClientServer:SetupCommunication()
    end

    -- Bind to open on our macros
    local groups = roduxStore:getState()[SocketConstants.RoduxStoreKey.MACROS].Groups
    for _, groupInfo in pairs(groups) do
        for _, macroInfo in pairs(groupInfo.Macros) do
            local macro = macroInfo.Macro ---@type MacroDefinition
            if macro.BindToOpen then
                macro.BindToOpen(macro, PluginHandler:GetPlugin())
            end
        end
    end
end

---
---@return Janitor
---
function SocketController:GetRunJanitor()
    return runJanitor
end

---
---@return RoduxStore
---
function SocketController:GetStore()
    return roduxStore
end

---
---Gets the currently stored MacroDefiniton from the passed macroScript
---@param macroScript ModuleScript
---@return MacroDefinition
---
function SocketController:GetMacro(macroScript)
    for _, groupInfo in pairs(roduxStore:getState()[SocketConstants.RoduxStoreKey.MACROS].Groups) do
        for someMacroScript, someMacroInfo in pairs(groupInfo.Macros) do
            if someMacroScript == macroScript then
                return someMacroInfo.Macro
            end
        end
    end
end

---
---@return boolean
---
function SocketController:IsRunning()
    return SocketController:GetStore():getState()[SocketConstants.RoduxStoreKey.STUDIO].IsRunning
end

---Will try require the current state of the passed moduleScript, by using a clone.
---@param moduleScript ModuleScript
---@return table|nil
local function tryCloneRequire(moduleScript)
    -- Get clone + try require
    local clone = moduleScript:Clone()
    runJanitor:Add(clone)

    local requiredClone ---@type table
    local requireSuccess, err = pcall(function()
        requiredClone = require(clone)
    end)

    -- Response
    if requireSuccess then
        return requiredClone
    else
        Logger:Warn(("Encountered issue while managing file %q  -  (%s)"):format(moduleScript.Name, err))
    end
end

---
---Runs the logic for manipulating our RoduxStore from the Macro files in studio.
---Called once per Run().
---
function SocketController:SetupMacroActions()
    ---Applies changes to our store after a macro has been changed
    ---@param moduleScript ModuleScript
    local function changedMacro(moduleScript)
        local requiredClone = tryCloneRequire(moduleScript)
        local macroDefinition = requiredClone and MacroHelper:CleanMacroDefinition(moduleScript, requiredClone)

        -- Stop it running if needed
        local currentMacroDefinition = SocketController:GetMacro(moduleScript)
        if currentMacroDefinition then
            currentMacroDefinition._BindToClose(currentMacroDefinition, PluginHandler:GetPlugin())
        end

        -- Update Store
        if macroDefinition then
            -- Update RoduxStore
            ---@type RoduxAction
            local action = {
                type = SocketConstants.RoduxActionType.MACROS.UPDATE_MACRO,
                data = {
                    macro = macroDefinition,
                    script = moduleScript,
                },
            }
            roduxStore:dispatch(action)
        else
            -- Had an issue; the user has likely done something wrong
            local currentMacroDefinition = SocketController:GetMacro(moduleScript)
            if currentMacroDefinition then
                currentMacroDefinition._isBroken = true

                -- Update RoduxStore
                ---@type RoduxAction
                local action = {
                    type = SocketConstants.RoduxActionType.MACROS.UPDATE_MACRO,
                    data = {
                        macro = currentMacroDefinition,
                        script = moduleScript,
                    },
                }
                roduxStore:dispatch(action)
            end
        end
    end

    ---Applies changes to our store after a macro has been removed
    ---@param moduleScript ModuleScript
    local function removedMacro(moduleScript)
        -- Run Bind to close
        local macro = SocketController:GetMacro(moduleScript)
        macro._BindToClose(macro, PluginHandler:GetPlugin())

        -- Update RoduxStore
        ---@type RoduxAction
        local action = {
            type = SocketConstants.RoduxActionType.MACROS.REMOVE_MACRO,
            data = {
                script = moduleScript,
            },
        }
        roduxStore:dispatch(action)
    end

    ---Applies changes to our store after a macro has been added
    ---@param moduleScript ModuleScript
    local function newMacro(moduleScript)
        local requiredClone = tryCloneRequire(moduleScript)
        local macroDefinition = requiredClone and MacroHelper:CleanMacroDefinition(moduleScript, requiredClone)
        if macroDefinition then
            -- Update RoduxStore
            ---@type RoduxAction
            local action = {
                type = SocketConstants.RoduxActionType.MACROS.ADD_MACRO,
                data = {
                    macro = macroDefinition,
                    script = moduleScript,
                    isFieldsOpen = SocketSettings:GetSetting("OpenFieldsByDefault"),
                },
            }
            roduxStore:dispatch(action)

            -- Auto run?
            local doRun = macroDefinition.AutoRun and SocketSettings:GetSetting("EnableAutoRun")
            if doRun then
                MacroHelper:RunMacro(macroDefinition)
            end
        end
    end

    ---Returns true if this macroScript is added to our store
    ---@param macroScript ModuleScript
    ---@return boolean
    local function isMacroScriptAdded(macroScript)
        local groups = roduxStore:getState()[SocketConstants.RoduxStoreKey.MACROS].Groups
        for _, groupInfo in pairs(groups) do
            for someMacroScript, _ in pairs(groupInfo.Macros) do
                if someMacroScript == macroScript then
                    return true
                end
            end
        end

        return false
    end

    -- Variables for tracking macro scripts
    local cachedActiveScript ---@type Instance
    local macrosFolder = StudioHandler.Folders.Macros
    local ourMacrosFolder = LocalMacros:GetOurDirectory()

    ---Had a new module script added that is likely a macro
    ---@param macroScript ModuleScript
    local function newMacroScript(macroScript)
        -- RETURN: Parent is a module script
        if macroScript.Parent:IsA("ModuleScript") then
            return
        end

        newMacro(macroScript)

        -- Listen for source changes to update macro
        runJanitor:Add(macroScript.Changed:Connect(function(property)
            if property == "Source" and cachedActiveScript ~= macroScript then
                if isMacroScriptAdded(macroScript) then
                    -- Changed
                    changedMacro(macroScript)
                else
                    -- Wasn't in memory
                    newMacro(macroScript)
                end
            end
        end))
    end

    -- Grab the macros already sitting there
    for _, descendant in pairs(macrosFolder:GetDescendants()) do
        if descendant:IsA("ModuleScript") then
            newMacroScript(descendant)
        end
    end
    for _, descendant in pairs(ourMacrosFolder:GetDescendants()) do
        if descendant:IsA("ModuleScript") then
            newMacroScript(descendant)
        end
    end

    -- Hook up listener events for macro files being added/removed
    runJanitor:Add(macrosFolder.DescendantAdded:Connect(function(descendant)
        if descendant:IsA("ModuleScript") then
            task.wait() -- May have been added through a Rojo Sync
            newMacroScript(descendant)
        end
    end))
    runJanitor:Add(ourMacrosFolder.DescendantAdded:Connect(function(descendant)
        if descendant:IsA("ModuleScript") then
            task.wait() -- May have been added through a Rojo Sync
            newMacroScript(descendant)
        end
    end))

    runJanitor:Add(macrosFolder.DescendantRemoving:Connect(function(descendant)
        if descendant:IsA("ModuleScript") then
            if isMacroScriptAdded(descendant) then
                removedMacro(descendant)
            end
        end
    end))
    runJanitor:Add(ourMacrosFolder.DescendantRemoving:Connect(function(descendant)
        if descendant:IsA("ModuleScript") then
            if isMacroScriptAdded(descendant) then
                removedMacro(descendant)
            end
        end
    end))

    -- Listener to the user viewing different scripts; used to trigger "macro changed" events
    runJanitor:Add(StudioService:GetPropertyChangedSignal("ActiveScript"):Connect(function()
        local activeScript = StudioService.ActiveScript

        -- Have we just exited a script file?
        if cachedActiveScript then
            local isMacroScript = cachedActiveScript:IsDescendantOf(StudioHandler.Folders.Directory)
            if isMacroScript then
                if isMacroScriptAdded(cachedActiveScript) then
                    -- Changed
                    changedMacro(cachedActiveScript)
                else
                    -- Wasn't in memory
                    newMacroScript(cachedActiveScript)
                end
            end
        end

        -- Update cache
        cachedActiveScript = activeScript
    end))
end

---
---Runs the logic for manipulating our RoduxStore, reading from the Studio service
---Called once per Run().
---
function SocketController:SetupStudioActions()
    -- Theme
    local function setTheme()
        -- Used to update the store.. now just refresh the widget
        WidgetHandler:Refresh()
    end

    setTheme()
    runJanitor:Add(Studio.ThemeChanged:Connect(setTheme))

    -- Is Running
    -- Update RoduxStore
    ---@type RoduxAction
    local action = {
        type = SocketConstants.RoduxActionType.STUDIO.IS_RUNNING,
        data = {
            isRunning = RunService:IsRunning(),
        },
    }
    SocketController:GetStore():dispatch(action)
end

---
---Listens to users input to detect whether to run macros via keybind
---
function SocketController:SetupKeybindHooks()
    -- Track keys being held down
    local heldKeys = {} ---@type table<Enum.KeyCode, boolean>

    ---@param inputObject InputObject
    ---@param gameProcessedEvent boolean
    local function inputBegan(inputObject, gameProcessedEvent)
        -- Grab setting
        local doIgnoreGameProcessedKeybinds = SocketSettings:GetSetting("IgnoreGameProcessedKeybinds")

        -- Record keypress
        heldKeys[inputObject.KeyCode] = gameProcessedEvent

        -- Does this equate a keybind?
        local state = roduxStore:getState()
        local groups = state[SocketConstants.RoduxStoreKey.MACROS].Groups
        for _, groupInfo in pairs(groups) do
            for _, macroInfo in pairs(groupInfo.Macros) do
                local macro = macroInfo.Macro ---@type MacroDefinition

                -- See if we have all held keys in this macro
                local totalKeyCodes = #macro.Keybind
                local hasKeybind = totalKeyCodes > 0
                if hasKeybind then
                    local matchingKeyCodes = 0
                    local allowGameProcessedEventKeyCodes = doIgnoreGameProcessedKeybinds or macro.IgnoreGameProcessedKeybinds
                    for _, someKeyCode in pairs(macro.Keybind) do
                        if heldKeys[someKeyCode] == false or heldKeys[someKeyCode] and allowGameProcessedEventKeyCodes then
                            matchingKeyCodes = matchingKeyCodes + 1
                        end
                    end

                    -- Huzzah!
                    local isMatchingKeybind = totalKeyCodes == matchingKeyCodes
                    local hasKeybindEnabled = not macro.State.IsKeybindDisabled
                    if isMatchingKeybind and hasKeybindEnabled then
                        -- Run Macro Function
                        MacroHelper:RunMacro(macro)

                        -- Clear heldKeys
                        heldKeys = {}

                        -- Stop
                        return
                    end
                end
            end
        end
    end

    ---@param inputObject InputObject
    ---@param gameProcessedEvent boolean
    local function inputEnded(inputObject, gameProcessedEvent)
        heldKeys[inputObject.KeyCode] = nil
    end

    runJanitor:Add(UserInputService.InputBegan:Connect(inputBegan))
    runJanitor:Add(UserInputService.InputEnded:Connect(inputEnded))
end

---
---Runs necessary routines when Socket gets deactivated
---
function SocketController:Stop()
    -- RETURN: Not running
    if not isRunning then
        return
    end
    isRunning = false

    -- Bind to close on our macros
    local groups = roduxStore:getState()[SocketConstants.RoduxStoreKey.MACROS].Groups
    for _, groupInfo in pairs(groups) do
        for _, macroInfo in pairs(groupInfo.Macros) do
            local macro = macroInfo.Macro ---@type MacroDefinition
            macro._BindToClose(macro, PluginHandler:GetPlugin())
        end
    end

    -- Cleanup Instances
    InstanceUtil:Cleanup()

    -- Clear Janitor
    runJanitor:Cleanup()

    -- Clear cache
    roduxStore = nil

    -- Widget handler bye
    WidgetHandler:Stop()
end

---@private
function SocketController:FrameworkInit()
    Janitor = PluginFramework:Require("Janitor")
    Rodux = PluginFramework:Require("Rodux")
    StudioHandler = PluginFramework:Require("StudioHandler")
    SocketConstants = PluginFramework:Require("SocketConstants")
    Logger = PluginFramework:Require("Logger")
    SocketRoduxStoreController = PluginFramework:Require("SocketRoduxStoreController")
    WidgetHandler = PluginFramework:Require("WidgetHandler")
    MacroConstants = PluginFramework:Require("MacroConstants")
    MacroHelper = PluginFramework:Require("MacroHelper")
    MacroClientServer = PluginFramework:Require("MacroClientServer")
    SocketSettings = PluginFramework:Require("SocketSettings")
    PluginHandler = PluginFramework:Require("PluginHandler")
    InstanceUtil = PluginFramework:Require("InstanceUtil")
    LocalMacros = PluginFramework:Require("LocalMacros")
end

---@private
function SocketController:FrameworkStart()
    runJanitor = Janitor.new()
end

return SocketController
