local MAJOR, MINOR = "DiesalGUI-2.0", 1

---@alias Diesal.GUI DiesalGUI-2.0

---@class DiesalGUI-2.0
---@field ObjectBase Diesal.GUI.ObjectBase.Methods
---@field ObjectFactory Diesal.GUI.Object.Factory
---@field ObjectVersions Diesal.GUI.Object.Versions
---@field ObjectPool Diesal.GUI.Object.Pool
---@field counts Diesal.GUI.Object.Counts
local DiesalGUI, oldMinor = LibStub:NewLibrary(MAJOR, MINOR)
if not DiesalGUI then
    return
end

local DiesalGUIDebug = false

local DiesalGUIDebugRedFont = RED_FONT_COLOR or CreateColor(1, 0.12549020349979, 0.12549020349979, 1)
local DiesalGUIDebugWhiteFont = WHITE_FONT_COLOR or CreateColor(1, 1, 1, 1)

local DiesalGUIDebugPrint = function(...)
    if not DiesalGUIDebug then
        return
    end
    local args = { ... }
    local msgPrefix = DiesalGUIDebugRedFont:WrapTextInColorCode("[DiesalGUI Debug]: ")
    local str = ""
    for i = 1, #args do
        str = str .. tostring(args[i]) .. " "
    end
    print(msgPrefix .. DiesalGUIDebugWhiteFont:WrapTextInColorCode(str))
end

---@class Diesal.GUI.Padding : { [1]: left, [2]: right, [3]: top, [4]: bottom }

---@alias Diesal.Object.Constructed DiesalAccordian | DiesalAccordianSection | DiesalBar | DiesalBranch | DiesalButton | DiesalCheckBox | DiesalComboBox | DiesalComboBoxItem | DiesalDropDown | DiesalDropDownItem | DiesalInput | DiesalQuickDoc | DiesalScrollFrame | DiesalScrollingEditBox | DiesalScrollingMessageFrame | DiesalSpinner | DiesalText | DiesalTree | DiesalWindow
---@alias Diesal.Container Diesal.GUI.Object.Window | Diesal.GUI.Object.Accordian
---@alias Diesal.Container.Contents DiesalAccordian | DiesalAccordianSection | DiesalBar | DiesalBranch | DiesalButton | DiesalCheckBox | DiesalComboBox | DiesalComboBoxItem | DiesalDropDown | DiesalDropDownItem | FontString | DiesalInput | DiesalQuickDoc | DiesalScrollFrame | DiesalScrollingEditBox | DiesalScrollingMessageFrame | DiesalSpinner | DiesalTree | DiesalText | DiesalWindow | Diesal.GUI.ObjectBase

DiesalGUI.Debug = {
    Print = DiesalGUIDebugPrint,
    Enabled = DiesalGUIDebug,
    RedFont = DiesalGUIDebugRedFont,
    WhiteFont = DiesalGUIDebugWhiteFont,
}

local CallbackHandler = LibStub("CallbackHandler-1.0")

local DiesalTools = LibStub("DiesalTools-2.0")
local DiesalStyle = LibStub("DiesalStyle-2.0")

local type, select, tonumber = type, select, tonumber
local setmetatable, getmetatable, next = setmetatable, getmetatable, next
local pairs, ipairs = pairs, ipairs
local tinsert, tremove = table.insert, table.remove

local CreateFrame, UIParent = CreateFrame, UIParent

DiesalGUI.callbacks = DiesalGUI.callbacks or CallbackHandler:New(DiesalGUI)

---@class Diesal.GUI.Object.Factory : { [Diesal.Object.Type]: fun(name?: string): Diesal.Object }
DiesalGUI.ObjectFactory = DiesalGUI.ObjectFactory or {}

---@class Diesal.GUI.Object.Versions : { [Diesal.Object.Type]: number }
DiesalGUI.ObjectVersions = DiesalGUI.ObjectVersions or {}

---@class Diesal.GUI.Object.Pool : { [Diesal.Object.Type]: { [Diesal.Object]: boolean } }
DiesalGUI.ObjectPool = DiesalGUI.ObjectPool or {}

---@class Diesal.GUI.Object.Counts : { [Diesal.Object.Type]: number }
DiesalGUI.counts = DiesalGUI.counts or {}

---@class Diesal.GUI.Object.AntiPool : { [Diesal.Object.Type]: boolean }
DiesalGUI.AntiPool = DiesalGUI.AntiPool or {}

local ObjectFactory = DiesalGUI.ObjectFactory
local ObjectVersions = DiesalGUI.ObjectVersions
local ObjectPool = DiesalGUI.ObjectPool

local function OnMouse(frame, button)
    DiesalGUI:ClearFocus()
end

-- Capture mouse clicks on the WorldFrame
---@param frame Frame
---@param button Button
local function WorldFrameOnMouse(frame, button)
    OnMouse(frame, button)
end

---@alias Diesal.Object
---|DiesalAccordian
---|DiesalAccordianSection
---|DiesalBar
---|DiesalBranch
---|DiesalButton
---|DGUIBuilder.Container
---|DiesalCheckBox
---|DiesalComboBox
---|DiesalComboBoxItem
---|DiesalDropDown
---|DiesalDropDownItem
---|DiesalInput
---|DiesalObjectBase
---|DiesalQuickDoc
---|DiesalScrollFrame
---|DiesalScrollingEditBox
---|DiesalScrollingMessageFrame
---|DiesalSpinner
---|DiesalText
---|DiesalTree
---|DiesalWindow

WorldFrame:HookScript("OnMouseDown", WorldFrameOnMouse)

-- Objects (widgets) that are defined for use in DiesalGUI
---@alias Diesal.Object.Type
---|'"DiesalAccordian"'
---|'"DiesalAccordianSection"'
---|'"DiesalBar"'
---|'"DiesalBranch"'
---|'"DiesalButton"'
---|'"DiesalCheckBox"'
---|'"DiesalComboBox"'
---|'"DiesalComboBoxItem"'
---|'"DiesalDropDown"'
---|'"DiesalDropDownItem"'
---|'"DiesalInput"'
---|'"DiesalObjectBase"'
---|'"DiesalQuickDoc"'
---|'"DiesalScrollFrame"'
---|'"DiesalScrollingEditBox"'
---|'"DiesalScrollingMessageFrame"'
---|'"DiesalSpinner"'
---|'"DiesalText"'
---|'"DiesalTree"'
---|'"DiesalWindow"'
---|'"Container"'
---|'"VerticalLayout"'

-- Returns a new object
---@generic T
---@param objectType `T` | Diesal.Object.Type
---@param base? boolean
---@return T newObj
local function newObject(objectType, base)
    if not ObjectFactory[objectType] then
        error("Attempt to construct unknown Object type", 2)
    end

    if base then
        return ObjectFactory.DiesalObjectBase(objectType)
    end

    ObjectPool[objectType] = ObjectPool[objectType] or {}

    local newObj = next(ObjectPool[objectType])
    if not newObj then
        newObj = ObjectFactory[objectType]()
    else
        ObjectPool[objectType][newObj] = nil
    end

    return newObj
end

-- Releases an object into ReleasedObjects
---@param obj Diesal.Object
---@param objectType Diesal.Object.Type
local function releaseObject(obj, objectType)
    ObjectPool[objectType] = ObjectPool[objectType] or {}

    if ObjectPool[objectType][obj] then
        error("Attempt to Release Object that is already released", 2)
    end
    ObjectPool[objectType][obj] = true
end

-- Registers an Object constructor in the ObjectFactory
---@generic T : Diesal.Object.Type
---@param Type `T`
---@param constructor fun(name: string): T
---@param version number
function DiesalGUI:RegisterObjectConstructor(Type, constructor, version)
    assert(type(constructor) == "function")
    assert(type(version) == "number")

    local oldVersion = ObjectVersions[Type]
    if oldVersion and oldVersion >= version then
        return
    end

    ObjectVersions[Type] = version
    ObjectFactory[Type] = constructor
end

--[[ -- Registers an Object constructor in the ObjectFactory
---@generic T : Diesal.Object.Type
---@param Type `T`
---@param constructor fun(self: Diesal.GUI, type: string | Diesal.Object.Type): Diesal.GUI.ObjectBase
---@param version number
function DiesalGUI:RegisterObjectBaseConstructor(Type, constructor, version)
    assert(type(constructor) == "function")
    assert(type(version) == "number")

    local oldVersion = ObjectVersions[Type]
    if oldVersion and oldVersion >= version then
        return
    end

    ObjectVersions[Type] = version
    ObjectFactory[Type] = constructor

    self.CreateObjectBase = constructor
end ]]

-- Create a new Object
---@generic T
---@param objectType `T` | Diesal.Object.Type
---@param name? string | boolean
---@param methods? table<string, fun(self?: self, ...): any>
---@return T object
function DiesalGUI:Create(objectType, name, methods)
    local base = (name ~= nil and type(name) == "boolean") and name or nil
    local name = (name ~= nil and type(name) == "string") and name or nil
    if ObjectFactory[objectType] then
        local object
        if name then -- needs a specific name, bypass the objectPool and create a new object
            object = ObjectFactory[objectType](name)
        else
            object = newObject(objectType, base)
        end
        if type(methods) == "table" then
            object:SetMethods(methods)
        end
        object:ResetSettings()
        -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        if object.OnAcquire then
            object:OnAcquire()
        end
        return object
        ---@diagnostic disable-next-line: missing-return
    end
end

---@param duration number
---@param callback fun(...)
function DiesalGUI:CreateThrottle(duration, callback)
    assert(callback and type(callback) == "function", "callback has to be a function ")
    assert(duration and type(duration) == "number", "duration has to be a number ")

    ---@class Throttle : AnimationGroup, Object, FrameScriptObject
    local throttle = CreateFrame("Frame", nil, UIParent):CreateAnimationGroup()
    throttle.anim = throttle:CreateAnimation("Animation")
    throttle.args = {}

    local mt = getmetatable(throttle)
    ---@cast mt Throttle, metatable

    mt.__index.SetCallback = function(this, callback)
        assert(callback and type(callback) == "function", "callback required to be a function ")
        this:SetScript("OnFinished", function()
            callback(unpack(this.args))
        end)
    end

    mt.__index.AddCallback = function(this, callback)
        assert(callback and type(callback) == "function", "callback required to be a function ")
        this:HookScript("OnFinished", function()
            callback(unpack(this.args))
        end)
    end

    mt.__index.SetDuration = function(this, callback)
        assert(callback and type(callback) == "number", "duration has to be a number ")
        this.anim:SetDuration(callback)
    end

    mt.__call = function(this, ...)
        this.args = { ... }
        this:Stop()
        this:Play()
    end

    throttle = setmetatable(throttle, mt)

    throttle:SetScript("OnFinished", function()
        callback(unpack(throttle.args))
    end)
    throttle:SetDuration(duration)

    return throttle
end

---@param object Diesal.Object
function DiesalGUI:Release(object)
    if object.OnRelease then
        object:OnRelease()
    end

    object:FireEvent("OnRelease")

    object:ReleaseChildren()
    object:ReleaseTextures()
    object:ResetFonts()
    object:ResetEventListeners()

    object.frame:ClearAllPoints()
    object.frame:Hide()
    object.frame:SetParent(UIParent)
    releaseObject(object, object.type)
end

---@alias Diesal.GUI.Object.Focusable
---|Diesal.GUI.Object.ComboBox
---|Diesal.GUI.Object.DropDown

-- Set FocusedObject: Menu, Dropdown, editBox etc....
---@param object Diesal.GUI.Object.Focusable
function DiesalGUI:SetFocus(object)
    if self.FocusedObject and self.FocusedObject ~= object then
        DiesalGUI:ClearFocus()
    end
    self.FocusedObject = object
end

-- Clear focus from the FocusedObject
function DiesalGUI:ClearFocus()
    local FocusedObject = self.FocusedObject
    if FocusedObject then
        if FocusedObject.ClearFocus then -- FocusedObject is Focusable Frame
            FocusedObject:ClearFocus()
        end
        self.FocusedObject = nil
    end
end

-- Mouse Input capture for any DiesalGUI interactive region
---@param frame Frame
---@param button MouseButton | "MouseWheel"
function DiesalGUI:OnMouse(frame, button)
    -- print(button)
    OnMouse(frame, button)
    DiesalGUI.callbacks:Fire("DiesalGUI_OnMouse", frame, button)
end

--- A type-based counter to count the number of widgets created.
---@param objectType Diesal.Object.Type
function DiesalGUI:GetNextObjectNum(objectType)
    if not self.counts[objectType] then
        self.counts[objectType] = 0
    end
    self.counts[objectType] = self.counts[objectType] + 1
    return self.counts[objectType]
end

--- Return the number of created widgets for this type.
---@param objectType Diesal.Object.Type
function DiesalGUI:GetObjectCount(objectType)
    return self.counts[objectType] or 0
end
