
local MAJOR, MINOR = 'LibClass-2.0', 10
---@class LibClass-2.0
local Class = LibStub:NewLibrary(MAJOR, MINOR)
if not Class then
    return
end

Class._UIBaseClass = Class._UIBaseClass or {}
Class._Classes = Class._Classes or {}

Class.Object = Class.Object or {}

---- Lua APIS

local assert, error, pcall, type = assert, error, pcall, type
local wipe, pairs, rawget, rawset = wipe, pairs, rawget, rawset
local setmetatable, hooksecurefunc = setmetatable, hooksecurefunc
local tconcat, loadstring, xpcall = table.concat, loadstring, xpcall
local geterrorhandler = geterrorhandler
local format, wipe, select = string.format, wipe, select

---- WOW APIS

local CreateFrame = CreateFrame

--[[
	 xpcall safecall implementation
]]
local xpcall = xpcall

local function errorhandler(err)
    return geterrorhandler()(err)
end

local function safecall(func, ...)
    if type(func) == 'function' then
        return xpcall(func, errorhandler, ...)
    end
end

local function safereturn(success, ...)
    if success then
        return ...
    end
end

-----------------------------
--                     Object
-----------------------------

---@class Object
---@field _Meta table
local Object = wipe(Class.Object)

local function Constructor(class, object, ...)
    if not class then
        return
    end
    Constructor(class:GetSuper(), object, ...)

    local ctor = rawget(class, 'Constructor')
    if type(ctor) == 'function' then
        ctor(object, ...)
    end
    return object
end

local function Create(_Meta)
    local object

    if not _Meta.__ui then
        object = {}
    elseif not _Meta.__uiname then
        object = CreateFrame(_Meta.__ui, nil, nil, _Meta.__inherit)
    else
        _Meta.__uiindex = (_Meta.__uiindex or 0) + 1

        object = CreateFrame(_Meta.__ui, _Meta.__uiname .. _Meta.__uiindex, nil, _Meta.__inherit)
    end
    return setmetatable(object, _Meta)
end

---@generic T
---@param self T
---@return T
function Object:New(...)
    if not Class:IsClass(self) then
        error([[bad argument #self to 'New' (class expected)]], 2)
    end
    return Constructor(self, Create(self._Meta), ...)
end

---@generic T
---@param self T
---@return T
function Object:Bind(object, ...)
    if not Class:IsClass(self) then
        error([[bad argument #self to 'Bind' (class expected)]], 2)
    end
    if Class:IsWidget(object) then
        return Constructor(self, setmetatable(object, self._Meta), object:GetParent(), ...)
    else
        return Constructor(self, setmetatable(object, self._Meta), ...)
    end
end

---@return Object
function Object:GetSuper()
    return self._Meta.__super
end

---@return Object
function Object:GetType()
    return self._Meta.__type
end

---@return string?
function Object:GetInherit()
    return self._Meta.__inherit
end

---@param class Object
---@return boolean
function Object:IsType(class)
    if not self.GetType then
        return false
    end
    if not Class:IsClass(class) then
        return false
    end
    if self:GetType() == class then
        return true
    end
    local super = self:GetSuper()
    return super and super:IsType(class) or false
end

---@param object Object
---@return boolean
function Object:IsInstance(object)
    return Class:IsObject(object) and object:IsType(self._Meta.__type)
end

---@param name string
---@param func function
function Object:SetCallback(name, func)
    if type(func) == 'function' then
        self.events = self.events or {}
        self.events[name] = func
    end
end

---@param name string
---@vararg any[]
function Object:Fire(name, ...)
    if self.events and self.events[name] then
        return safereturn(safecall(self.events[name], self, ...))
    end
end

---@param method string
---@vararg any[]
function Object:SuperCall(method, ...)
    local super = self:GetSuper()
    if not super then
        error([[this class not have a super class]], 2)
    end
    if type(method) ~= 'string' then
        error(format([[bad argument #1 to 'SuperCall' (string expected, got %s)]], type(method)), 2)
    end
    if type(super[method]) ~= 'function' then
        error(format([[attempt to call global '%s' (a nil value)]], method), 2)
    end

    while super do
        if super[method] == self[method] then
            super = super:GetSuper()
        else
            return super[method](self, ...)
        end
    end
end

-----------------------------
--                      Class
-----------------------------

local function DefaultConstructor(object, parent)
    if Class:IsWidget(parent) and object:GetParent() ~= parent then
        object:SetParent(parent)
    end
end

local _UIBaseClass = setmetatable(Class._UIBaseClass, {
    __index = function(t, k)
        local ok, class = pcall(CreateFrame, k)
        if ok then
            class._Meta = {__index = class, __type = class, __ui = k}
            class:Hide()
            class.Constructor = DefaultConstructor

            for k, v in pairs(Object) do
                class[k] = v
            end

            t[k] = class
        else
            t[k] = false
        end
        return t[k]
    end,
})

function Class:SuperHelper(super)
    if type(super) == 'string' then
        local objType, inherit = super:match('^(%a+)%.?([%w_]*)$')
        local baseSuper = _UIBaseClass[objType:lower()]
        if not baseSuper then
            error(format([['%s' isnot blizzard widget object.]], super), 4)
        else
            return baseSuper, inherit ~= '' and inherit or nil
        end
    elseif Class:IsClass(super) then
        return super, super._Meta.__inherit
    elseif not super then
        return
    else
        error([[bad argument #1 to 'New' (string/class/widget expected)]], 4)
    end
end

local function Inherit(class, super)
    if not super then
        return
    end
    Inherit(class, super:GetSuper())

    local inherit = rawget(super, 'Inherit')
    if type(inherit) == 'function' then
        inherit(class)
    end
end

local function CloneMeta(super)
    if not super then
        return {}
    else
        local _Meta = {}
        for k, v in pairs(super._Meta) do
            _Meta[k] = v
        end
        return _Meta
    end
end

function Class:New(super)
    if self ~= Class then
        error([[Usage: Class:New(name[, super])]], 2)
    end

    local super, inherit = self:SuperHelper(super)
    local class = {}

    class._Meta = CloneMeta(super)
    class._Meta.__index = class
    class._Meta.__super = super
    class._Meta.__type = class

    if super then
        class._Meta.__inherit = inherit

        setmetatable(class, super._Meta)

        Inherit(class, super)
    end

    for k, v in pairs(Object) do
        class[k] = v
    end
    return class
end

function Class:IsClass(value)
    if self ~= Class then
        error([[Usage: Class:IsClass(value)]], 2)
    end
    if type(value) ~= 'table' then
        return false
    end
    local _Meta = rawget(value, '_Meta')
    if not _Meta then
        return false
    end
    return _Meta.__type == value
end

function Class:IsObject(value)
    if self ~= Class then
        error([[Usage: Class:IsObject(value)]], 2)
    end
    if type(value) ~= 'table' then
        return false
    end
    if rawget(value, '_Meta') then
        return false
    end
    local _Meta = value._Meta
    if not _Meta then
        return false
    end
    return Class:IsClass(_Meta.__type)
end

function Class:IsWidget(value)
    if self ~= Class then
        error([[Usage: Class:IsWidget(value)]], 2)
    end
    if type(value) ~= 'table' then
        return false
    end
    return value.GetObjectType and type(rawget(value, 0)) == 'userdata' or false
end

function Class:IsUIClass(value)
    if self ~= Class then
        error([[Usage: Class:IsClass(value)]], 2)
    end
    if type(value) ~= 'table' then
        return false
    end
    local _Meta = rawget(value, '_Meta')
    if not _Meta then
        return false
    end
    return _Meta.__ui
end

-----------------------------
--                    Library
-----------------------------

local _Classes = setmetatable(Class._Classes, {
    __index = function(t, k)
        t[k] = {}
        return t[k]
    end,
})

function Class:NewClass(name, ...)
    if _Classes[self][name] then
        return
    end
    local class = Class:New(...)
    _Classes[self][name] = class
    safecall(self.OnClassCreated, self, class, name)
    return class
end

function Class:GetClass(name)
    return _Classes[self][name]
end

function Class:Embed(target)
    target.NewClass = Class.NewClass
    target.GetClass = Class.GetClass
end
