---@type Tinkr, Bastion

---@type Diesal.GUI
local DiesalGUI = LibStub("DiesalGUI-2.0")

---@type Diesal.Tools
local DiesalTools = LibStub("DiesalTools-2.0")

---@type Diesal.Style
local DiesalStyle = LibStub("DiesalStyle-2.0")

local Type = "DiesalObjectBase"
local Version = 1

---@class Diesal.GUI.ObjectBase.Methods
local ObjectBase = {
    --#region Diesal.GUI.Objectbase.BaseMethods

    --#region BlizzardFrameMethods

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    Hide = function(self)
        self.frame:Hide()
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    Show = function(self)
        self.frame:Show()
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@param strata FrameStrata
    SetFrameStrata = function(self, strata)
        self.frame:SetFrameStrata(strata)
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@param parent Frame
    SetParent = function(self, parent)
        local frame = self.frame
        frame:SetParent(nil)
        frame:SetParent(parent)
        self.settings.parent = parent
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    GetWidth = function(self)
        return self.frame:GetWidth()
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@param width number
    SetWidth = function(self, width)
        self.settings.width = width
        self.frame:SetWidth(width)
        self:FireEvent("OnWidthSet", width)
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    GetHeight = function(self)
        return self.frame:GetHeight()
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@param height number
    SetHeight = function(self, height)
        self.settings.height = height
        self.frame:SetHeight(height)
        self:FireEvent("OnHeightSet", height)
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@return number width, number height
    GetSize = function(self)
        return self.frame:GetSize()
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@param width number
    ---@param height number
    SetSize = function(self, width, height)
        self.settings.width = width
        self.settings.height = height

        --TODO: Transition to SetSize?
        self.frame:SetSize(width, height)
        --self.frame:SetHeight(height)
        --self.frame:SetWidth(width)

        self:FireEvent("OnWidthSet", width)
        self:FireEvent("OnHeightSet", height)
        self:FireEvent("OnSizeSet", width, height)
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    IsVisible = function(self)
        return self.frame:IsVisible()
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    IsShown = function(self)
        return self.frame:IsShown()
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    SetPoint = function(self, ...)
        self.frame:SetPoint(...)
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    SetAllPoints = function(self, ...)
        return self.frame:SetAllPoints(...)
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ClearAllPoints = function(self)
        return self.frame:ClearAllPoints()
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    GetNumPoints = function(self)
        return self.frame:GetNumPoints()
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    GetPoint = function(self, ...)
        return self.frame:GetPoint(...)
    end,

    --#endregion BlizzardFrameMethods

    ---@class Diesal.GUI.Region.FontString : FontString
    ---@field defaultFontObject Font

    ---@class Diesal.GUI.Region.EditBox : EditBox, FontableFrameMixin
    ---@field defaultFontObject Font

    ---@class Diesal.GUI.Region.ScrollingMessageFrame : ScrollingMessageFrame
    ---@field defaultFontObject Font

    ---@alias Diesal.GUI.Region.Custom
    ---|Diesal.GUI.Region.FontString
    ---|Diesal.GUI.Region.EditBox
    ---|Diesal.GUI.Region.ScrollingMessageFrame

    ---@alias Diesal.GUI.Parent.Region
    ---|Frame
    ---|Texture
    ---|Diesal.Object

    --#region DiesalObjectMethods

    ---@generic RT
    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@param regionType `RT` | "FontString" | "Texture" | "EditBox" | "ScrollingMessageFrame" | FrameType
    ---@param regionName string
    ---@param parentRegion Diesal.GUI.Parent.Region
    ---@param defaultFontObject? Font
    ---@param ... any
    ---@return RT
    CreateRegion = function(self, regionType, regionName, parentRegion, defaultFontObject, ...)
        if regionType == "FontString" then
            ---@class Diesal.GUI.Region.FontString
            local fontString = parentRegion:CreateFontString()
            fontString.defaultFontObject = defaultFontObject or _G.DiesalFontNormal
            fontString:SetFont(fontString.defaultFontObject:GetFont())
            fontString:SetTextColor(fontString.defaultFontObject:GetTextColor())
            fontString:SetSpacing(fontString.defaultFontObject:GetSpacing())

            self[regionName] = fontString
            self.fontStrings[regionName] = fontString
            return fontString
        end

        if regionType == "Texture" then
            self[regionName] = parentRegion:CreateTexture(...)
            return self[regionName]
        end

        if regionType == "EditBox" then
            local editBox = CreateFrame("EditBox", nil, parentRegion, ...)
            ---@cast editBox Diesal.GUI.Region.EditBox
            -- set Default font properties
            editBox.defaultFontObject = defaultFontObject or _G.DiesalFontNormal
            editBox:SetFont(editBox.defaultFontObject:GetFont())
            editBox:SetTextColor(editBox.defaultFontObject:GetTextColor())
            editBox:SetSpacing(editBox.defaultFontObject:GetSpacing())

            editBox:HookScript("OnEscapePressed", function(this)
                DiesalGUI:ClearFocus()
            end)
            editBox:HookScript("OnEditFocusGained", function(this)
                DiesalGUI:SetFocus(this)
                GameTooltip:Hide()
            end)

            self[regionName] = editBox
            return editBox
        end

        if regionType == "ScrollingMessageFrame" then
            local srollingMessageFrame = CreateFrame("ScrollingMessageFrame", nil, parentRegion, ...) ---@cast srollingMessageFrame Diesal.GUI.Region.ScrollingMessageFrame
            -- set Default font properties
            srollingMessageFrame.defaultFontObject = defaultFontObject or _G.DiesalFontNormal
            srollingMessageFrame:SetFont(srollingMessageFrame.defaultFontObject:GetFont())
            srollingMessageFrame:SetTextColor(srollingMessageFrame.defaultFontObject:GetTextColor())
            srollingMessageFrame:SetSpacing(srollingMessageFrame.defaultFontObject:GetSpacing())

            self[regionName] = srollingMessageFrame
            return srollingMessageFrame
        end

        self[regionName] = CreateFrame(regionType, nil, parentRegion, ...)
        return self[regionName]
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@param methods? table<string, fun(self?: self, ...): any>
    SetMethods = function(self, methods)
        if methods and type(methods) == "table" then
            for method, func in pairs(methods) do
                if type(func) == "function" then
                    self.methods[method] = func
                end
            end
        end
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ResetFonts = function(self)
        for name, fontString in pairs(self.fontStrings) do
            fontString:SetFont(fontString.defaultFontObject:GetFont())
            fontString:SetTextColor(fontString.defaultFontObject:GetTextColor())
            fontString:SetSpacing(fontString.defaultFontObject:GetSpacing())
        end
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@param object Diesal.Object
    AddChild = function(self, object)
        tinsert(self.children, object)
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    Release = function(self)
        DiesalGUI:Release(self)
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@param object Diesal.Object
    ReleaseChild = function(self, object)
        local children = self.children

        for i = 1, #children do
            if children[i] == object then
                children[i]:Release()
                tremove(children, i)
                break
            end
        end
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ReleaseChildren = function(self)
        local children = self.children
        for i = 1, #children do
            children[i]:Release()
            children[i] = nil
        end
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@param parent? Diesal.Object
    SetParentObject = function(self, parent)
        local frame = self.frame
        local settings = self.settings

        frame:SetParent(nil)
        if parent then
            frame:SetParent(parent.content)
            settings.parent = parent.content
        end

        settings.parentObject = parent
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@param settings table
    ---@param apply? boolean
    SetSettings = function(self, settings, apply)
        for key, value in pairs(settings) do
            self.settings[key] = value
        end
        if apply then
            self:ApplySettings()
        end
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@param apply? boolean
    ResetSettings = function(self, apply)
        self.settings = DiesalTools.TableCopy(self.defaults or {})
        if apply then
            self:ApplySettings()
        end
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@param event string
    FireEvent = function(self, event, ...)
        if self.eventListeners[event] then
            for _, listner in ipairs(self.eventListeners[event]) do
                listner(self, event, ...)
            end
            --return self.eventListeners[event](self, event, ...)
        end
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@param event string
    ---@param listener fun(self: T, event: string, ...: any)
    AddEventListener = function(self, event, listener)
        if type(listener) == "function" then
            --self.eventListeners[event] = listener
            if not self.eventListeners[event] then
                self.eventListeners[event] = {}
            end
            table.insert(self.eventListeners[event], listener)
        else
            error("listener is required to be a function", 2)
        end
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@param event string
    ---@param listener fun(self: T, event: string, ...: any)
    SetEventListener = function(self, event, listener)
        if type(listener) == "function" then
            --self.eventListeners[event] = listener
            if not self.eventListeners[event] then
                self.eventListeners[event] = {}
            end
            table.insert(self.eventListeners[event], listener)
        else
            error("listener is required to be a function", 2)
        end
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ResetEventListeners = function(self)
        for k in pairs(self.eventListeners) do
            self.eventListeners[k] = nil
        end
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@param name string
    ---@param style Diesal.Style.Style
    SetStyle = function(self, name, style)
        DiesalStyle:SetObjectStyle(self, name, style)
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@param name string
    ---@param style Diesal.Style.Style
    UpdateStyle = function(self, name, style)
        DiesalStyle:UpdateObjectStyle(self, name, style)
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@param Stylesheet Diesal.Style.Stylesheet
    UpdateStylesheet = function(self, Stylesheet)
        DiesalStyle:UpdateObjectStylesheet(self, Stylesheet)
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@param Stylesheet Diesal.Style.Stylesheet
    SetStylesheet = function(self, Stylesheet)
        DiesalStyle:SetObjectStylesheet(self, Stylesheet)
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ---@param name string
    ReleaseTexture = function(self, name)
        if not self.textures[name] then
            return
        end
        DiesalStyle:ReleaseTexture(self, name)
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ReleaseTextures = function(self)
        DiesalStyle:ReleaseTextures(self)
    end,

    ---@generic T : Diesal.GUI.ObjectBase
    ---@param self T | Diesal.GUI.ObjectBase
    ApplySettings = function(self) end,

    --#endregion DiesalObjectMethods

    --#endregion Diesal.GUI.Objectbase.BaseMethods
}

DiesalGUI.ObjectBase = ObjectBase

---@class Diesal.GUI.ObjectBase.Settings
---@field parent Frame
---@field parentObject Diesal.Object?
---@field height number
---@field width number

---@class Diesal.GUI.ObjectBase.Methods
---@field OnAcquire fun()
---@field OnRelease fun()
---@field ApplySettings fun(self)

---@alias DiesalObjectBase Diesal.GUI.ObjectBase

-- Returns an Object Base
---@param objectType string | Diesal.Object.Type
local Constructor = function(objectType)
    ---@class Diesal.GUI.ObjectBase : Diesal.GUI.ObjectBase.Methods
    ---@field frame Frame
    ---@field isContainer boolean
    ---@field type Diesal.Object.Type
    ---@field fontStrings { [string]: Diesal.GUI.Region.FontString }
    ---@field textures { [string]: Diesal.Style.Texture }
    ---@field children { [number]: Diesal.GUI.ObjectBase }
    ---@field eventListeners { [string]: fun(self: self, event: string, ...)[] }
    ---@field settings Diesal.GUI.ObjectBase.Settings
    ---@field defaults Diesal.GUI.ObjectBase.Settings
    local object = {
        isContainer = false,
        type = objectType,
        fontStrings = {},
        textures = {},
        children = {},
        eventListeners = {},
        methods = {},
    }

    for k, v in pairs(ObjectBase) do
        object.methods[k] = v
    end

    setmetatable(object, {
        ---@param self Diesal.GUI.ObjectBase
        ---@param k string
        __index = function(self, k)
            -- Look for replaced methods.
            local response = self.methods[k]
            if response ~= nil then
                return response
            end

            return rawget(self, k)
        end,
    })
    return object
end

DiesalGUI:RegisterObjectConstructor(Type, Constructor, Version)

--[[ function DiesalGUI:CreateObjectBase(objectType)
    local object = {
        isContainer = false,
        type = objectType,
        fontStrings = {},
        textures = {},
        children = {},
        eventListeners = {},
    }
    setmetatable(object, { __index = ObjectBase })
    return object
end ]]
