
local detailsFramework = _G["DetailsFramework"]
if (not detailsFramework or not DetailsFrameworkCanLoad) then
	return
end

---@cast detailsFramework detailsframework

local CreateFrame = CreateFrame
local unpack = unpack
local wipe = table.wipe
local _

--[=[
    file description: this file has the code for the object editor
    the object editor itself is a frame and has a scrollframe as canvas showing another frame where there's the options for the editing object

--]=]


--the editor doesn't know which key in the profileTable holds the current value for an attribute, so it uses a map table to find it.
--the mapTable is a table with the attribute name as a key, and the value is the profile key. For example, {["size"] = "text_size"} means profileTable["text_size"] = 10.




---@class df_editor : frame, df_optionsmixin, df_editormixin
---@field options table
---@field registeredObjects df_editor_objectinfo[]
---@field registeredObjectsByID table<any, df_editor_objectinfo>
---@field editingObject uiobject
---@field editingProfileTable table
---@field editingProfileMap table
---@field editingOptions df_editobjectoptions
---@field currentObjectNinePoints df_ninepoints
---@field editingExtraOptions table
---@field registeredObjectInfo df_editor_objectinfo
---@field moverGuideLines table<string, texture>
---@field onEditCallback function
---@field optionsFrame frame
---@field overTheTopFrame frame
---@field objectSelector df_scrollbox
---@field moverFrames df_editor_mover[]
---@field canvasScrollBox df_canvasscrollbox
---@field AnchorFrames df_editor_anchorframes

---@class df_editor_attribute
---@field key string?
---@field label string?
---@field widget string
---@field default any?
---@field minvalue number?
---@field maxvalue number?
---@field step number?
---@field usedecimals boolean?
---@field subkey string?

---@class df_editor_objectinfo : table
---@field object uiobject
---@field label string
---@field id any
---@field profiletable table
---@field profilekeymap table
---@field subtablepath string?
---@field extraoptions table
---@field callback function?
---@field options df_editobjectoptions
---@field selectButton button
---@field refFrame frame usually the parent of the object registered

---@class df_editor_mover_movinginfo : table
---@field startX number
---@field startY number
---@field restingX number
---@field restingY number

---@class df_editor_mover : frame
---@field MovingInfo df_editor_mover_movinginfo
---@field ObjectBackgroundTexture texture
---@field MoverIcon texture
---@field bIsMoving boolean

--which object attributes are used to build the editor menu for each object type
local attributes = {
    ---@type df_editor_attribute[]
    Texture = {
        {
            key = "texture",
            label = "Texture",
            widget = "textentry",
            default = "",
            setter = function(widget, value) widget:SetTexture(value) end,
        },
        {
            key = "width",
            label = "Width",
            widget = "range",
            minvalue = 5,
            maxvalue = 120,
            setter = function(widget, value) widget:SetWidth(value) end
        },
        {
            key = "height",
            label = "Height",
            widget = "range",
            minvalue = 5,
            maxvalue = 120,
            setter = function(widget, value) widget:SetHeight(value) end
        },
        {widget = "blank"},
        {
            key = "anchor",
            label = "Anchor",
            widget = "anchordropdown",
            setter = function(widget, value) detailsFramework:SetAnchor(widget, value, widget:GetParent()) end
        },
        {
            key = "anchoroffsetx",
            label = "Anchor X Offset",
            widget = "range",
            minvalue = -120,
            maxvalue = 120,
            setter = function(widget, value) detailsFramework:SetAnchor(widget, value, widget:GetParent()) end
        },
        {
            key = "anchoroffsety",
            label = "Anchor Y Offset",
            widget = "range",
            minvalue = -120,
            maxvalue = 120,
            setter = function(widget, value) detailsFramework:SetAnchor(widget, value, widget:GetParent()) end
        },
    },

    FontString = {
        {
            key = "text",
            label = "Text",
            widget = "textentry",
            default = "font string text",
            setter = function(widget, value) widget:SetText(value) end,
        },
        {
            key = "size",
            label = "Size",
            widget = "range",
            minvalue = 5,
            maxvalue = 120,
            setter = function(widget, value) widget:SetFont(widget:GetFont(), value, select(3, widget:GetFont())) end
        },
        {
            key = "font",
            label = "Font",
            widget = "fontdropdown",
            setter = function(widget, value)
                local font = LibStub:GetLibrary("LibSharedMedia-3.0"):Fetch("font", value)
                widget:SetFont(font, select(2, widget:GetFont()))
            end
        },
        {
            key = "color",
            label = "Color",
            widget = "color",
            setter = function(widget, value) widget:SetTextColor(unpack(value)) end
        },
        {
            key = "alpha",
            label = "Alpha",
            widget = "range",
            setter = function(widget, value) widget:SetAlpha(value) end
        },
        {widget = "blank"},
        {
            key = "shadow",
            label = "Draw Shadow",
            widget = "toggle",
            setter = function(widget, value) widget:SetShadowColor(widget:GetShadowColor(), select(2, widget:GetShadowColor()), select(3, widget:GetShadowColor()), value and 0.5 or 0) end
        },
        {
            key = "shadowcolor",
            label = "Shadow Color",
            widget = "color",
            setter = function(widget, value) widget:SetShadowColor(unpack(value)) end
        },
        {
            key = "shadowoffsetx",
            label = "Shadow X Offset",
            widget = "range",
            minvalue = -10,
            maxvalue = 10,
            setter = function(widget, value) widget:SetShadowOffset(value, select(2, widget:GetShadowOffset())) end
        },
        {
            key = "shadowoffsety",
            label = "Shadow Y Offset",
            widget = "range",
            minvalue = -10,
            maxvalue = 10,
            setter = function(widget, value) widget:SetShadowOffset(widget:GetShadowOffset(), value) end
        },
        {
            key = "outline",
            label = "Outline",
            widget = "outlinedropdown",
            setter = function(widget, value) widget:SetFont(widget:GetFont(), select(2, widget:GetFont()), value) end
        },
        {widget = "blank"},
        {
            key = "anchor",
            label = "Anchor",
            widget = "anchordropdown",
            setter = function(widget, value) detailsFramework:SetAnchor(widget, value, widget:GetParent()) end
        },
        {
            key = "anchoroffsetx",
            label = "Anchor X Offset",
            widget = "range",
            minvalue = -120,
            maxvalue = 120,
            setter = function(widget, value) detailsFramework:SetAnchor(widget, value, widget:GetParent()) end
        },
        {
            key = "anchoroffsety",
            label = "Anchor Y Offset",
            widget = "range",
            minvalue = -120,
            maxvalue = 120,
            setter = function(widget, value) detailsFramework:SetAnchor(widget, value, widget:GetParent()) end
        },
        {
            key = "rotation",
            label = "Rotation",
            widget = "range",
            usedecimals = true,
            minvalue = 0,
            maxvalue = math.pi*2,
            setter = function(widget, value) widget:SetRotation(value) end
        },
        {
            key = "scale",
            label = "Scale",
            widget = "range",
            usedecimals = true,
            minvalue = 0.65,
            maxvalue = 2.5,
            setter = function(widget, value) widget:SetScale(value) end
        },
    }
}

---@class df_editormixin : table
---@field GetAllRegisteredObjects fun(self:df_editor):df_editor_objectinfo[]
---@field GetEditingObject fun(self:df_editor):uiobject
---@field GetEditingObjectIndex fun(self:df_editor):number?
---@field GetEditingOptions fun(self:df_editor):df_editobjectoptions
---@field GetExtraOptions fun(self:df_editor):table
---@field GetEditingProfile fun(self:df_editor):table, table
---@field GetOnEditCallback fun(self:df_editor):function
---@field GetOptionsFrame fun(self:df_editor):df_menu
---@field GetCanvasScrollBox fun(self:df_editor):df_canvasscrollbox
---@field GetObjectSelector fun(self:df_editor):df_scrollbox
---@field EditObject fun(self:df_editor, object:uiobject, profileTable:table?, profileKeyMap:table?, extraOptions:table?, callback:function?, options:df_editobjectoptions?)
---@field PrepareObjectForEditing fun(self:df_editor)
---@field CreateMoverGuideLines fun(self:df_editor)
---@field GetOverTheTopFrame fun(self:df_editor):frame
---@field CreateMoverFrames fun(self:df_editor):df_editor_mover[]
---@field GetMoverFrames fun(self:df_editor):df_editor_move
---@field StartObjectMovement fun(self:df_editor, anchorSettings:df_anchor)
---@field StopObjectMovement fun(self:df_editor)
---@field RegisterObject fun(self:df_editor, object:uiobject, localizedLabel:string, id:any, profileTable:table, subTablePath:string, profileKeyMap:table, extraOptions:table?, callback:function?, options:df_editobjectoptions?, refFrame:frame):df_editor_objectinfo
---@field UnregisterObject fun(self:df_editor, object:uiobject)
---@field EditObjectById fun(self:df_editor, id:any)
---@field EditObjectByIndex fun(self:df_editor, index:number)
---@field UpdateGuideLinesAnchors fun(self:df_editor)
---@field GetObjectByRef fun(self:df_editor, object:uiobject):df_editor_objectinfo
---@field GetObjectByIndex fun(self:df_editor, index:number):df_editor_objectinfo
---@field GetObjectById fun(self:df_editor, id:any):df_editor_objectinfo
---@field GetEditingRegisteredObject fun(self:df_editor):df_editor_objectinfo
---@field CreateObjectSelectionList fun(self:df_editor, scroll_width:number, scroll_height:number, scroll_lines:number, scroll_line_height:number):df_scrollbox
---@field OnHide fun(self:df_editor)
---@field OnShow fun(self:df_editor)
---@field UpdateProfileTableOnAllRegisteredObjects fun(self:df_editor, profileTable:table)
---@field GetProfileTableFromObject fun(self:df_editor, object:df_editor_objectinfo):table

---@class df_editobjectoptions : table
---@field use_colon boolean if true a colon is shown after the option name
---@field can_move boolean if true the object can be moved
---@field use_guide_lines boolean if true guide lines are shown when the object is being moved
---@field text_template table

---@type df_editobjectoptions
local editObjectDefaultOptions = {
    use_colon = false,
    can_move = true,
    use_guide_lines = true,
    text_template = detailsFramework:GetTemplate("font", "OPTIONS_FONT_TEMPLATE"),
}


local getParentTable = function(profileTable, profileKey)
    local parentPath
    if (profileKey:match("%]$")) then
        parentPath = profileKey:gsub("%s*%[.*%]%s*$", "")
    else
        parentPath = profileKey:gsub("%.[^.]*$", "")
    end

    local parentTable = detailsFramework.table.getfrompath(profileTable, parentPath)
    return parentTable
end

detailsFramework.EditorMixin = {
    ---@param self df_editor
    GetEditingObject = function(self)
        return self.editingObject
    end,

    ---@param self df_editor
    GetEditingObjectIndex = function(self)
        local object = self:GetEditingObject()
        local registeredObjects = self:GetAllRegisteredObjects()
        for i = 1, #registeredObjects do
            local objectRegistered = registeredObjects[i]
            if (objectRegistered.object == object) then
                return i
            end
        end
    end,

    ---@param self df_editor
    ---@return df_editobjectoptions
    GetEditingOptions = function(self)
        return self.editingOptions
    end,

    ---@param self df_editor
    ---@return table
    GetExtraOptions = function(self)
        return self.editingExtraOptions
    end,

    ---@param self df_editor
    ---@return table, table
    GetEditingProfile = function(self)
        return self.editingProfileTable, self.editingProfileMap
    end,

    ---@param self df_editor
    ---@return function
    GetOnEditCallback = function(self)
        return self.onEditCallback
    end,

    GetOptionsFrame = function(self)
        return self.optionsFrame
    end,

    GetOverTheTopFrame = function(self)
        return self.overTheTopFrame
    end,

    GetMoverFrames = function(self)
        return self.moverFrames
    end,

    GetCanvasScrollBox = function(self)
        return self.canvasScrollBox
    end,

    GetObjectSelector = function(self)
        return self.objectSelector
    end,

    ---@param self df_editor
    CreateMoverGuideLines = function(self)
        local overTheTopFrame = self:GetOverTheTopFrame()

        self.moverGuideLines = {
            left = overTheTopFrame:CreateTexture(nil, "overlay"),
            right = overTheTopFrame:CreateTexture(nil, "overlay"),
            top = overTheTopFrame:CreateTexture(nil, "overlay"),
            bottom = overTheTopFrame:CreateTexture(nil, "overlay"),
        }

        for side, texture in pairs(self.moverGuideLines) do
            texture:SetColorTexture(.8, .8, .8, 0.1)
            texture:SetSize(1, 1)
            texture:SetDrawLayer("overlay", 7)
            texture:Hide()

            if (side == "left" or side == "right") then
                texture:SetHeight(1)
                texture:SetWidth(GetScreenWidth())
            else
                texture:SetWidth(1)
                texture:SetHeight(GetScreenHeight())
            end
        end
    end,

    UpdateGuideLinesAnchors = function(self)
        local object = self:GetEditingObject()

        for side, texture in pairs(self.moverGuideLines) do
            texture:ClearAllPoints()
            if (side == "left" or side == "right") then
                if (side == "left") then
                    texture:SetPoint("right", object, "left", -2, 0)
                else
                    texture:SetPoint("left", object, "right", 2, 0)
                end
            else
                if (side == "top") then
                    texture:SetPoint("bottom", object, "top", 0, 2)
                else
                    texture:SetPoint("top", object, "bottom", 0, -2)
                end
            end
        end
    end,

---@class df_editor_anchorframes : table
---@field anchorFrames table
---@field DisableAllAnchors fun(self:df_editor_anchorframes)
---@field SetupAnchorsForObject fun(self:df_editor_anchorframes, anchorTable:table)
---@field SelectAnchorPoint fun(self:df_editor_anchorframes)
---@field GetAnchorFrame fun(self:df_editor_anchorframes, index:number):frame
---@field SetNotInUseForAllAnchors fun(self:df_editor_anchorframes)
---@field SetNotInUse fun(self:df_editor_anchorframes, anchorFrame:frame)
---@field SetInUse fun(self:df_editor_anchorframes, anchorFrame:frame)
---@field CreateNineAnchors fun(self:df_editor_anchorframes)

    --screen position offset is the XY screen position offset from the bottom left of the screen, they are always positive, they go from 0 to screen width or height
    CreateAnchorFrames = function(editorFrame)
        --table containing 9 buttons, each one in a different position of the object, indexes one to nine in this order: topleft, left, bottomleft, bottom, bottomright, right, topright, top, center
        editorFrame.AnchorFrames = {
            anchorFrames = {},

            DisableAllAnchors = function(self)
                for i = 1, 9 do
                    local anchorFrame = self:GetAnchorFrame(i)
                    self:SetNotInUse(anchorFrame)
                    anchorFrame:Hide()
                end
            end,

            SetupAnchorsForObject = function(self, anchorTable)
                editorFrame.AnchorFrames:DisableAllAnchors()

                local registeredObject = editorFrame:GetEditingRegisteredObject()

                if (registeredObject.refFrame) then
                    for i = 1, 9 do
                        local anchorFrame = self.anchorFrames[i]
                        local anchorName = detailsFramework.AnchorPointsByIndex[i]
                        anchorFrame:ClearAllPoints()
                        anchorFrame:SetPoint(anchorName, registeredObject.refFrame, anchorName, 0, 0)
                        anchorFrame:Show()
                    end

                    local sideSelected = anchorTable.side
                    local anchorFrameSelected = self:GetAnchorFrame(detailsFramework.InsidePointsToAnchor[sideSelected] or sideSelected)
                    self:SetInUse(anchorFrameSelected)

                    self.anchorTable = anchorTable
                end
            end,

            --when the user click on one of the anchor points, change the anchor side setting and recalculate the xy offset of the new point related to the same point in the object
            SelectAnchorPoint = function(anchorFrame)
                editorFrame.AnchorFrames:SetNotInUseForAllAnchors()

                --change the color of the anchor point to show it's selected
                anchorFrame.Texture:SetColorTexture(1, 0, 0, 0.5)

                --get the object being edited in the editor
                local object = editorFrame:GetEditingObject()

                --get the xy of the nine points of the object
                local ninePoints = detailsFramework.Math.GetNinePoints(object)

                --get the coordinates of the anchorIndex within the ninePoints table
                --the xy point in here is the XY screen position offset from the bottom left of the screen
                local screenPoint = ninePoints[anchorFrame.anchorIndex]
                local objectScreenPosX = screenPoint.x
                local objectScreenPosY = screenPoint.y

                --get the screen position offset of the anchor
                local anchorScreenPosX, anchorScreenPosY = anchorFrame:GetCenter()

                --calculate the xy offset of the anchor point related to the object
                local offsetX = objectScreenPosX - anchorScreenPosX
                local offsetY = objectScreenPosY - anchorScreenPosY

                --get the anchor settings table
                local anchorTable = editorFrame.AnchorFrames.anchorTable

                --set the anchor settings
                anchorTable.x = offsetX
                anchorTable.y = offsetY
                anchorTable.side = detailsFramework.AnchorPointsToInside[anchorFrame.anchorIndex]

                C_Timer.After(0, function()
                    editorFrame:PrepareObjectForEditing()
                end)
            end,

            GetAnchorFrame = function(self, anchorIndex)
                return self.anchorFrames[anchorIndex]
            end,

            SetNotInUseForAllAnchors = function(self)
                for i = 1, 9 do
                    local anchorFrame = self:GetAnchorFrame(i)
                    self:SetNotInUse(anchorFrame)
                end
            end,

            SetNotInUse = function(self, anchorFrame)
                anchorFrame.Texture:SetColorTexture(1, 1, 1, 0.5)
            end,

            SetInUse = function(self, anchorFrame)
                anchorFrame.Texture:SetColorTexture(1, 0, 0, 0.5)
            end,

            CreateNineAnchors = function(self)
                local overTheTopFrame = editorFrame:GetOverTheTopFrame()

                for i = 1, 9 do
                    local anchorFrame = CreateFrame("button", "$parentAnchorFrame" .. i, overTheTopFrame, "BackdropTemplate")
                    anchorFrame:SetSize(8, 8)
                    anchorFrame:SetBackdrop({bgFile = [[Interface\Tooltips\UI-Tooltip-Background]], tileSize = 64, tile = true})
                    anchorFrame:SetBackdropColor(1, 0, 0, 0.5)
                    anchorFrame:SetFrameStrata("TOOLTIP")
                    anchorFrame:SetFrameLevel(10)
                    anchorFrame.anchorIndex = i
                    anchorFrame:Hide()

                    anchorFrame:SetScript("OnClick", editorFrame.AnchorFrames.SelectAnchorPoint)

                    self.anchorFrames[i] = anchorFrame

                    anchorFrame.Texture = anchorFrame:CreateTexture("$parentTexture", "border")
                    anchorFrame.Texture:SetColorTexture(1, 1, 1, 0.5)
                    anchorFrame.Texture:SetAllPoints(anchorFrame)
                end
            end,
        }

        editorFrame.AnchorFrames:CreateNineAnchors()
        return editorFrame.AnchorFrames
    end,

    ---create a frame to move the object, the frame is attached into the bottom right of the selected object
    ---@param editorFrame df_editor
    ---@return df_editor_movermain
    CreateMoverFrames = function(editorFrame)
        local amountOfMovers = 1

        --frame that is used to move the object
        ---@type df_editor_movermain
        local movers = {
            anchorNames = {"bottomright", "bottomleft", "topright", "topleft"},

            Hide = function(self)
                for i = 1, amountOfMovers do
                    self[i]:Hide()
                end
            end,

            Stop = function(self)
                self.bIsMoving = false
                for i = 1, amountOfMovers do
                    local moverFrame = self[i]
                    moverFrame:StopMovingOrSizing()
                    moverFrame:SetScript("OnUpdate", nil)
                end
            end,

            UpdatePosition = function(self, moverFrame)
                for i = 1, amountOfMovers do
                    local thisMoverFrame = self[i]
                    if (thisMoverFrame ~= moverFrame) then
                        thisMoverFrame.OnTickNotMoving(thisMoverFrame, 0)
                    end
                end
            end,

            Setup = function(self, object, registeredObject, onTickWhileMoving, onTickNotMoving)
                for i = 1, amountOfMovers do
                    local moverFrame = self[i]
                    moverFrame:Show()
                    moverFrame:EnableMouse(true)
                    moverFrame.OnTickWhileMoving = onTickWhileMoving
                    moverFrame.OnTickNotMoving = onTickNotMoving
                    moverFrame.anchorName = self.anchorNames[i]

                    moverFrame:SetScript("OnMouseDown", function()
                        --save the current position of the object
                        local startX, startY = moverFrame:GetCenter()
                        moverFrame.MovingInfo.startX = startX
                        moverFrame.MovingInfo.startY = startY

                        editorFrame.currentObjectNinePoints = detailsFramework.Math.GetNinePoints(registeredObject.refFrame)

                        --start moving
                        moverFrame:SetScript("OnUpdate", onTickWhileMoving)
                        moverFrame.bIsMoving = true
                        moverFrame:StartMoving()
                    end)

                    moverFrame:SetScript("OnMouseUp", function()
                        self:Stop()
                        moverFrame:EnableMouse(true)

                        --save the current position of the object selected
                        local x, y = object:GetCenter()
                        moverFrame.MovingInfo.restingX = x
                        moverFrame.MovingInfo.restingY = y
                        moverFrame:SetScript("OnUpdate", onTickNotMoving)

                        local currentNinePoints = editorFrame.currentObjectNinePoints
                        local startX, startY = moverFrame:GetCenter()
                        local closestPoint = editorFrame.currentObjectNinePoints:GetClosestPoint(CreateVector2D(startX, startY))
                        --if (closestPoint ~= parentTable.side) then
                            --print("side is different", closestPoint, parentTable.side)
                        --end
                    end)

                    moverFrame:ClearAllPoints()

                    if (i == 1) then
                        if (amountOfMovers == 1) then
                            moverFrame:SetPoint("topleft", object, "topleft", 0, 0)
                            moverFrame:SetPoint("bottomright", object, "bottomright", 0, 0)
                        else
                            moverFrame:SetPoint("center", object, "bottomright", 0, 0)
                        end
                    elseif (i == 2) then
                        moverFrame:SetPoint("center", object, "bottomleft", 0, 0)
                    elseif (i == 3) then
                        moverFrame:SetPoint("center", object, "topright", 0, 0)
                    elseif (i == 4) then
                        moverFrame:SetPoint("center", object, "topleft", 0, 0)
                        moverFrame:SetSize(object:GetWidth(), object:GetHeight())
                    end

                    local x, y = moverFrame:GetCenter()
                    --moverFrame:SetPoint("center", UIParent, "bottomleft", x, y)

                    --current position of object selected
                    local x, y = object:GetCenter()
                    moverFrame.MovingInfo.restingX = x
                    moverFrame.MovingInfo.restingY = y
                    moverFrame:SetScript("OnUpdate", onTickNotMoving)

                    --problem, I don't remember why caused the issue of the hide on click on others tabs, I think the mover was anchored to parent frame all points
                    if (not moverFrame.moved) then
                        moverFrame:GetScript("OnMouseDown")(moverFrame)
                        moverFrame:GetScript("OnMouseUp")(moverFrame)
                        --moverFrame.moved = true
                    end
                end

                self.ObjectBackgroundTexture:SetPoint("topleft", object, "topleft", 0, 0)
                self.ObjectBackgroundTexture:SetSize(object:GetWidth(), object:GetHeight())
            end,
        }

        for i = 1, amountOfMovers do
            ---@type df_editor_mover
            local moverFrame = CreateFrame("button", "$parentMover" .. i, UIParent, "BackdropTemplate")
            moverFrame:SetFrameStrata("TOOLTIP")
            moverFrame:SetSize(16, 16)
            moverFrame:SetClampedToScreen(true)
            moverFrame:EnableMouse(true)
            moverFrame:SetMovable(true)
            moverFrame:SetFrameLevel(math.abs(i-5))
            moverFrame.MovingInfo = {
                startX = 0,
                startY = 0,
                restingX = 0,
                restingY = 0,
            }

            movers[i] = moverFrame

            moverFrame.MoverIcon = moverFrame:CreateTexture("$parentMoverIcon", "overlay")
            moverFrame.MoverIcon:SetTexture([[Interface\CHATFRAME\CHATFRAMEBACKGROUND]])
            moverFrame.MoverIcon:SetSize(6, 6)
            moverFrame.MoverIcon:SetPoint("center", moverFrame, "center", 0, 0)
        end

        movers.ObjectBackgroundTexture = movers[1]:CreateTexture("$parentMoverObjectBackground", "artwork")
        movers.ObjectBackgroundTexture:SetColorTexture(1, 1, 1, 0.25)

        return movers
    end,

    EditObjectById = function(self, id)
        ---@type df_editor_objectinfo
        local objectRegistered = self:GetObjectById(id)
        assert(type(objectRegistered) == "table", "EditObjectById() object not found.")
        self:EditObject(objectRegistered)
        self.objectSelector:RefreshMe()
    end,

    EditObjectByIndex = function(self, index)
        ---@type df_editor_objectinfo
        local objectRegistered = self:GetObjectByIndex(index)
        assert(type(objectRegistered) == "table", "EditObjectById() object not found.")
        self:EditObject(objectRegistered)
        self.objectSelector:RefreshMe()
    end,

    ---@param self df_editor
    ---@param registeredObject df_editor_objectinfo
    EditObject = function(self, registeredObject)
        --clear previous values
        self.editingObject = nil
        self.editingProfileMap = nil
        self.editingProfileTable = nil
        self.editingOptions = nil
        self.editingExtraOptions = nil
        self.onEditCallback = nil

        local object = registeredObject.object
        local profileKeyMap = registeredObject.profilekeymap
        local extraOptions = registeredObject.extraoptions
        local callback = registeredObject.callback
        local options = registeredObject.options

        local profileTable = self:GetProfileTableFromObject(registeredObject)
        assert(type(profileTable) == "table", "EditObject() profileTable is invalid.")

        --as there's no other place which this members are set, there is no need to create setter functions
        self.registeredObjectInfo = registeredObject
        self.editingObject = object
        self.editingProfileMap = profileKeyMap
        self.editingProfileTable = profileTable
        self.editingOptions = options
        self.editingExtraOptions = extraOptions

        if (type(callback) == "function") then
            self.onEditCallback = callback
        end

        self:PrepareObjectForEditing()
    end,

    PrepareObjectForEditing = function(self) --~edit
        --get the object and its profile table with the current values
        local object = self:GetEditingObject()
        local profileTable, profileMap = self:GetEditingProfile()
        profileMap = profileMap or {}

        self.AnchorFrames:DisableAllAnchors()

        local conditionalKeys = profileMap.enable_if or {}

        if (not object or not profileTable) then
            return
        end

        --get the object type
        local objectType = object:GetObjectType()
        local attributeList

        --get options and extra options
        local editingOptions = self:GetEditingOptions()
        local extraOptions = self:GetExtraOptions()

        --get the attribute list for the object type
        if (objectType == "FontString") then
            ---@cast object fontstring
            attributeList = attributes[objectType]

        elseif (objectType == "Texture") then
            ---@cast object texture
            attributeList = attributes[objectType]
        end

        --if there's extra options, add the attributeList to a new table and right after the extra options
        if (extraOptions and #extraOptions > 0) then
            local attributeListWithExtraOptions = {}

            for i = 1, #attributeList do
                attributeListWithExtraOptions[#attributeListWithExtraOptions+1] = attributeList[i]
            end

            attributeListWithExtraOptions[#attributeListWithExtraOptions+1] = {widget = "blank", default = true}

            for i = 1, #extraOptions do
                attributeListWithExtraOptions[#attributeListWithExtraOptions+1] = extraOptions[i]
            end

            attributeList = attributeListWithExtraOptions
        end

        local anchorSettings

        --table to use on DF:BuildMenuVolatile()
        local menuOptions = {}
        for i = 1, #attributeList do
            local option = attributeList[i]

            if (option.widget == "blank") then
                menuOptions[#menuOptions+1] = {type = "blank"}
            else
                --get the key to be used on profile table
                local profileKey = profileMap[option.key]
                local value

                --if the key contains a dot or a bracket, it means it's a table path, example: "text_settings[1].width"
                if (profileKey and (profileKey:match("%.") or profileKey:match("%["))) then
                    value = detailsFramework.table.getfrompath(profileTable, profileKey)
                else
                    value = profileTable[profileKey]
                end

                --if no value is found, attempt to get a default
                if (type(value) == "nil") then
                    value = option.default
                end

                local bHasValue = type(value) ~= "nil"

                local minValue = option.minvalue
                local maxValue = option.maxvalue

                if (option.key == "anchoroffsetx") then
                    minValue = -math.floor(object:GetParent():GetWidth())
                    maxValue = math.floor(object:GetParent():GetWidth())
                elseif (option.key == "anchoroffsety") then
                    minValue = -math.floor(object:GetParent():GetHeight())
                    maxValue = math.floor(object:GetParent():GetHeight())
                end

                if (bHasValue) then
                    local parentTable = getParentTable(profileTable, profileKey)

                    if (option.key == "anchor" or option.key == "anchoroffsetx" or option.key == "anchoroffsety") then
                        anchorSettings = parentTable
                    end

                    local optionTable = {
                        type = option.widget,
                        name = option.label,
                        get = function() return value end,
                        set = function(widget, fixedValue, newValue, ...)
                            --color is a table with 4 indexes for each color plus alpha
                            if (option.widget == "range" or option.widget == "slider") then
                                if (not option.usedecimals) then
                                    newValue = math.floor(newValue)
                                end

                            elseif (option.widget == "color") then
                                --calor callback sends the red color in the fixedParameter slot
                                local r, g, b, alpha = fixedValue, newValue, ...
                                --need to use the same table from the profile table
                                parentTable[1] = r
                                parentTable[2] = g
                                parentTable[3] = b
                                parentTable[4] = alpha

                                newValue = parentTable
                            end

                            detailsFramework.table.setfrompath(profileTable, profileKey, newValue)

                            if (self:GetOnEditCallback()) then
                                self:GetOnEditCallback()(object, option.key, newValue, profileTable, profileKey)
                            end

                            --update the widget visual
                            --anchoring uses SetAnchor() which require the anchorTable to be passed
                            if (option.key == "anchor" or option.key == "anchoroffsetx" or option.key == "anchoroffsety") then
                                anchorSettings = parentTable

                                if (option.key == "anchor") then
                                    anchorSettings.x = 0
                                    anchorSettings.y = 0
                                end

                                self:StopObjectMovement()

                                option.setter(object, parentTable)

                                if (editingOptions.can_move) then
                                    self:StartObjectMovement(anchorSettings)
                                end
                            else
                                option.setter(object, newValue)
                            end
                        end,
                        min = minValue,
                        max = maxValue,
                        step = option.step,
                        usedecimals = option.usedecimals,
                        id = option.key,
                    }

                    if (conditionalKeys[option.key]) then
                        local bIsEnabled = conditionalKeys[option.key](object, profileTable, profileKey)
                        if (not bIsEnabled) then
                            optionTable.disabled = true
                        end
                    end

                    menuOptions[#menuOptions+1] = optionTable
                end
            end
        end

        if (anchorSettings) then
            self.AnchorFrames:SetupAnchorsForObject(anchorSettings)
        end

        --at this point, the optionsTable is ready to be used on DF:BuildMenuVolatile()
        menuOptions.align_as_pairs = true
        menuOptions.align_as_pairs_length = 150
        menuOptions.widget_width = 180
        menuOptions.slider_buttons_to_left = true

        local optionsFrame = self:GetOptionsFrame()
        local canvasScrollBox = self:GetCanvasScrollBox()

        local bUseColon = editingOptions.use_colon

        local bSwitchIsCheckbox = true
        local maxHeight = 5000

        local amountOfOptions = #menuOptions
        local optionsFrameHeight = amountOfOptions * 20
        optionsFrame:SetHeight(optionsFrameHeight)

        --templates
        local options_text_template = self.options.text_template or detailsFramework:GetTemplate("font", "OPTIONS_FONT_TEMPLATE")
        local options_dropdown_template = detailsFramework:GetTemplate("dropdown", "OPTIONS_DROPDOWN_TEMPLATE")
        local options_switch_template = detailsFramework:GetTemplate("switch", "OPTIONS_CHECKBOX_TEMPLATE")
        local options_slider_template = detailsFramework:GetTemplate("slider", "OPTIONS_SLIDER_TEMPLATE")
        local options_button_template = detailsFramework:GetTemplate("button", "OPTIONS_BUTTON_TEMPLATE")

        --~build ~menu ~volatile
        detailsFramework:BuildMenuVolatile(optionsFrame, menuOptions, 2, -2, maxHeight, bUseColon, options_text_template, options_dropdown_template, options_switch_template, bSwitchIsCheckbox, options_slider_template, options_button_template)

        if (editingOptions.can_move) then
            self:StartObjectMovement(anchorSettings)
        end
    end,

    ---@param self df_editor
    ---@param anchorSettings df_anchor
    StartObjectMovement = function(self, anchorSettings)
        local object = self:GetEditingObject()

        --update guidelines
        if (self:GetEditingOptions().use_guide_lines) then
            --self:UpdateGuideLinesAnchors()
            --show all four guidelines
            for side, texture in pairs(self.moverGuideLines) do
                texture:Show()
            end
        end

        local registeredObject = self:GetEditingRegisteredObject()
        local optionsFrame = self:GetOptionsFrame()
        local objectParent = object:GetParent()
        local moverFrames = self:GetMoverFrames()

        local onTickWhileMoving = function(moverFrame, deltaTime)
            local startX, startY = moverFrame:GetCenter()
            local xOffset = startX - moverFrame.MovingInfo.startX
            local yOffset = startY - moverFrame.MovingInfo.startY

            if (xOffset ~= 0 or yOffset ~= 0) then
                moverFrame.MovingInfo.startX = startX
                moverFrame.MovingInfo.startY = startY
                anchorSettings.x = anchorSettings.x + xOffset
                anchorSettings.y = anchorSettings.y + yOffset
                detailsFramework:SetAnchor(object, anchorSettings, objectParent)

                --update the slider offset in the options frame
                local anchorXSlider = optionsFrame:GetWidgetById("anchoroffsetx")
                anchorXSlider:SetValueNoCallback(anchorSettings.x)
                local anchorYSlider = optionsFrame:GetWidgetById("anchoroffsety")
                anchorYSlider:SetValueNoCallback(anchorSettings.y)

                --save the new position
                local profileTable, profileMap = self:GetEditingProfile()
                local profileKey = profileMap.anchor
                local parentTable = getParentTable(profileTable, profileKey)
                parentTable.x = anchorSettings.x
                parentTable.y = anchorSettings.y

                local closestPoint = self.currentObjectNinePoints:GetClosestPoint(CreateVector2D(startX, startY))
                if (closestPoint ~= parentTable.side) then
                    --print("side is different", closestPoint, parentTable.side)
                end

                if (self:GetOnEditCallback()) then
                    self:GetOnEditCallback()(object, "x", anchorSettings.x, profileTable, profileKey)
                    self:GetOnEditCallback()(object, "y", anchorSettings.y, profileTable, profileKey)
                end

                moverFrames:UpdatePosition(moverFrame)
            end
        end

        local onTickNotMoving = function(moverFrame, deltaTime)
            --current position of the selected object
            local objectX, objectY = object:GetCenter()
            --did the object move?
            if (objectX ~= moverFrame.MovingInfo.restingX or objectY ~= moverFrame.MovingInfo.restingY) then
                moverFrame:SetPoint("center", object, moverFrame.anchorName, 0, 0)
                local x, y = moverFrame:GetCenter()
                --moverFrame:SetPoint("center", UIParent, "bottomleft", x, y)
                moverFrame.MovingInfo.restingX = objectX
                moverFrame.MovingInfo.restingY = objectY
            end
        end

        moverFrames:Setup(object, registeredObject, onTickWhileMoving, onTickNotMoving)
    end,

    ---@param self df_editor
    StopObjectMovement = function(self)
        local moverFrame = self:GetMoverFrames()
        moverFrame:Stop()

        --hide all four guidelines
        for side, texture in pairs(self.moverGuideLines) do
            texture:Hide()
        end
    end,

    ---@param self df_editor
    ---@param object df_editor_objectinfo
    GetProfileTableFromObject = function(self, object)
        local profileTable = object.profiletable
        local subTablePath = object.subtablepath

        if (type(subTablePath) == "string" and subTablePath ~= "") then
            local subTable = detailsFramework.table.getfrompath(profileTable, subTablePath)
            assert(type(subTable) == "table", "GetProfileTableFromObject() subTablePath is invalid.")
            return subTable
        end

        return profileTable
    end,

    UpdateProfileTableOnAllRegisteredObjects = function(self, profileTable)
        assert(type(profileTable) == "table", "UpdateProfileTableOnAllRegisteredObjects() expects a table on #1 parameter.")

        local registeredObjects = self:GetAllRegisteredObjects()

        for i = 1, #registeredObjects do
            local objectRegistered = registeredObjects[i]
            objectRegistered.profiletable = profileTable
        end

        local objectSelector = self:GetObjectSelector()
        objectSelector:RefreshMe()
    end,

    RegisterObject = function(self, object, localizedLabel, id, profileTable, subTablePath, profileKeyMap, extraOptions, callback, options, refFrame)
        assert(type(object) == "table", "RegisterObjectToEdit() expects an UIObject on #1 parameter.")
        assert(object.GetObjectType, "RegisterObjectToEdit() expects an UIObject on #1 parameter.")
        assert(type(profileTable) == "table", "RegisterObjectToEdit() expects a table on #4 parameter.")
        assert(type(id) ~= "nil" and type(id) ~= "boolean", "RegisterObjectToEdit() expects an ID on parameter #3.")
        assert(type(callback) == "function" or callback == nil, "RegisterObjectToEdit() expects a function or nil as the #7 parameter.")

        local registeredObjects = self:GetAllRegisteredObjects()

        --is object already registered?
        for i = 1, #registeredObjects do
            local objectRegistered = registeredObjects[i]
            if (objectRegistered.object == object) then
                error("RegisterObjectToEdit() object already registered.")
            end
        end

        --deploy the options table
        options = type(options) == "table" and options or {}
        detailsFramework.table.deploy(options, editObjectDefaultOptions)

        localizedLabel = type(localizedLabel) == "string" and localizedLabel or "invalid label"

        --a button to select the widget
        local selectButton = CreateFrame("button", "$parentSelectButton" .. id, object:GetParent())
        selectButton:SetAllPoints(object)

        ---@type df_editor_objectinfo
        local objectRegistered = {
            object = object,
            label = localizedLabel,
            id = id,
            profiletable = profileTable,
            subtablepath = subTablePath,
            profilekeymap = profileKeyMap,
            extraoptions = extraOptions or {},
            callback = callback,
            options = options,
            selectButton = selectButton,
            refFrame = refFrame,
        }

        registeredObjects[#registeredObjects+1] = objectRegistered
        self.registeredObjectsByID[id] = objectRegistered

        local objectSelector = self:GetObjectSelector()
        objectSelector:RefreshMe()

        selectButton:SetScript("OnClick", function()
            self:EditObject(objectRegistered)
            objectSelector:RefreshMe()
        end)

        --what to do after an object is registered?
        return objectRegistered
    end,

    UnregisterObject = function(self, object)
        local registeredObjects = self:GetAllRegisteredObjects()

        for i = 1, #registeredObjects do
            local objectRegistered = registeredObjects[i]
            if (objectRegistered.object == object) then
                self.registeredObjectsByID[objectRegistered.id] = nil
                table.remove(registeredObjects, i)
                break
            end
        end

        local objectSelector = self:GetObjectSelector()
        objectSelector:RefreshMe()

        --stop editing the object
    end,

    ---@param self df_editor
    ---@return df_editor_objectinfo[]
    GetAllRegisteredObjects = function(self)
        return self.registeredObjects
    end,

    ---@param self df_editor
    ---@return df_editor_objectinfo?
    GetObjectByRef = function(self, object)
        local registeredObjects = self:GetAllRegisteredObjects()
        for i = 1, #registeredObjects do
            local objectRegistered = registeredObjects[i]
            if (objectRegistered.object == object) then
                return objectRegistered
            end
        end
    end,


    ---@param self df_editor
    ---@return df_editor_objectinfo
    GetEditingRegisteredObject = function(self)
        return self.registeredObjectInfo
    end,

    GetObjectByIndex = function(self, index)
        local registeredObjects = self:GetAllRegisteredObjects()
        return registeredObjects[index]
    end,

    GetObjectById = function(self, id)
        return self.registeredObjectsByID[id]
    end,

    CreateObjectSelectionList = function(self, scroll_width, scroll_height, scroll_lines, scroll_line_height)
        local editorFrame = self

        local refreshFunc = function(self, data, offset, totalLines) --~refresh
            self.SelectionTexture:Hide()
            self.SelectionTexture:ClearAllPoints()
            local objectCurrentBeingEdited = editorFrame:GetEditingObject()

			for i = 1, totalLines do
				local index = i + offset
                ---@type df_editor_objectinfo
				local objectRegistered = data[index]

				if (objectRegistered) then
                    local line = self:GetLine(i)
                    line.index = index

                    if (objectRegistered.object:GetObjectType() == "Texture") then
                        line.Icon:SetAtlas("AnimCreate_Icon_Texture")

                    elseif (objectRegistered.object:GetObjectType() == "FontString") then
                        line.Icon:SetAtlas("AnimCreate_Icon_Text")
                    end

                    line.Label:SetText(objectRegistered.label)

                    if (objectRegistered.object == objectCurrentBeingEdited) then
                        self.SelectionTexture:SetAllPoints(line)
                        self.SelectionTexture:Show()
                    end
                end
            end
        end

		local createLineFunc = function(self, index) -- ~createline --~line
			local line = CreateFrame("button", "$parentLine" .. index, self, "BackdropTemplate")
			line:SetPoint("topleft", self, "topleft", 1, -((index-1)*(scroll_line_height+1)) - 1)
			line:SetSize(scroll_width - 2, scroll_line_height)

			line:SetBackdrop({bgFile = [[Interface\Tooltips\UI-Tooltip-Background]], tileSize = 64, tile = true})
            if (index % 2 == 0) then
                line:SetBackdropColor(.1, .1, .1, .1)
            else
                line:SetBackdropColor(.1, .1, .1, .4)
            end

            detailsFramework:CreateHighlightTexture(line, "HighlightTexture")
    		detailsFramework:Mixin(line, detailsFramework.HeaderFunctions)

            line:SetScript("OnClick", function(self)
                local objectRegistered = editorFrame:GetObjectByIndex(self.index)
                editorFrame:EditObject(objectRegistered)
                editorFrame.objectSelector:RefreshMe()
            end)

			--icon
			local objectIcon = line:CreateTexture("$parentIcon", "overlay")
			objectIcon:SetSize(scroll_line_height - 2, scroll_line_height - 2)

			--object label
			local objectLabel = line:CreateFontString("$parentLabel", "overlay", "GameFontNormal")

            objectIcon:SetPoint("left", line, "left", 2, 0)
            objectLabel:SetPoint("left", objectIcon, "right", 2, 0)

			line.Icon = objectIcon
			line.Label = objectLabel

			return line
		end

        local selectObjectScrollBox = detailsFramework:CreateScrollBox(self:GetParent(), "$parentSelectObjectScrollBox", refreshFunc, editorFrame:GetAllRegisteredObjects(), scroll_width, scroll_height, scroll_lines, scroll_line_height)
        detailsFramework:ReskinSlider(selectObjectScrollBox)

        local selectionTexture = selectObjectScrollBox:CreateTexture(nil, "overlay")
        selectionTexture:SetColorTexture(1, 1, 0, 0.2)
        selectObjectScrollBox.SelectionTexture = selectionTexture

		function selectObjectScrollBox:RefreshMe()
			selectObjectScrollBox:SetData(editorFrame:GetAllRegisteredObjects())
		    selectObjectScrollBox:Refresh()
		end

		--create lines
		for i = 1, scroll_lines do
			selectObjectScrollBox:CreateLine(createLineFunc)
		end

        return selectObjectScrollBox
    end,

    OnHide = function(self)
        self:StopObjectMovement()
        local moverFrames = self:GetMoverFrames()
        moverFrames:Hide()
        self.AnchorFrames:DisableAllAnchors()
    end,

    ---@param self df_editor
    OnShow = function(self)
        --get the editing object
        local objectIndex = self:GetEditingObjectIndex()
        if (objectIndex) then
            self:EditObjectByIndex(objectIndex)
            self.objectSelector:RefreshMe()
        end
    end,
}

---@class df_editor_defaultoptions : table
---@field width number
---@field height number
---@field options_width number
---@field create_object_list boolean
---@field object_list_width number
---@field object_list_height number
---@field object_list_lines number
---@field object_list_line_height number
---@field text_template table

--editorFrame.options.text_template

---@type df_editor_defaultoptions
local editorDefaultOptions = {
    width = 400,
    height = 548,
    options_width = 340,
    create_object_list = true,
    object_list_width = 200,
    object_list_height = 420,
    object_list_lines = 20,
    object_list_line_height = 20,
    text_template = detailsFramework:GetTemplate("font", "OPTIONS_FONT_TEMPLATE"),
}

function detailsFramework:CreateEditor(parent, name, options)
    name = name or ("DetailsFrameworkEditor" .. math.random(100000, 10000000))
    local editorFrame = CreateFrame("frame", name, parent, "BackdropTemplate")

    detailsFramework:Mixin(editorFrame, detailsFramework.EditorMixin)
    detailsFramework:Mixin(editorFrame, detailsFramework.OptionsFunctions)

    editorFrame:SetScript("OnShow", editorFrame.OnShow)
    editorFrame:SetScript("OnHide", editorFrame.OnHide)

    editorFrame.registeredObjects = {}
    editorFrame.registeredObjectsByID = {}

    editorFrame:BuildOptionsTable(editorDefaultOptions, options)

    editorFrame:SetSize(editorFrame.options.width, editorFrame.options.height)

    --The options frame holds the options for the object being edited. It is used as the parent frame for the BuildMenuVolatile() function.
    ---@type df_menu
    local optionsFrame = CreateFrame("frame", name .. "OptionsFrame", editorFrame, "BackdropTemplate")
    optionsFrame:SetSize(editorFrame.options.options_width, 5000)

    local canvasScrollBoxOptions = {
        width = editorFrame.options.options_width,
        height = 400,
        reskin_slider = true,
    }
    local canvasFrame = detailsFramework:CreateCanvasScrollBox(editorFrame, optionsFrame, name .. "CanvasScrollBox", canvasScrollBoxOptions)

    if (editorFrame.options.create_object_list) then
        local scrollWidth = editorFrame.options.object_list_width
        local scrollHeight = editorFrame.options.object_list_height
        local scrollLinesAmount = editorFrame.options.object_list_lines
        local scrollLineHeight = editorFrame.options.object_list_line_height

        local objectSelector = editorFrame:CreateObjectSelectionList(scrollWidth, scrollHeight, scrollLinesAmount, scrollLineHeight)
        objectSelector:SetPoint("topleft", editorFrame, "topleft", 0, -2)
        objectSelector:SetBackdropBorderColor(0, 0, 0, 0)
        editorFrame.objectSelector = objectSelector
        objectSelector:RefreshMe()

        local nScrollBarWidth = 30
        canvasFrame:SetPoint("topleft", objectSelector, "topright", nScrollBarWidth, 0)
        canvasFrame:SetPoint("bottomleft", objectSelector, "bottomright", -nScrollBarWidth, 0)
    else
        canvasFrame:SetPoint("topleft", editorFrame, "topleft", 2, -2)
        canvasFrame:SetPoint("bottomleft", editorFrame, "bottomleft", 2, 0)
    end

    --over the top frame is a frame that is always on top of everything else
    local OTTFrame = CreateFrame("frame", "$parentOTTFrame", UIParent)
    OTTFrame:SetFrameStrata("TOOLTIP")
    editorFrame.overTheTopFrame = OTTFrame

    editorFrame:CreateAnchorFrames()

    editorFrame.moverFrames = editorFrame:CreateMoverFrames()
    editorFrame:CreateMoverGuideLines()

    editorFrame.optionsFrame = optionsFrame
    editorFrame.canvasScrollBox = canvasFrame

    return editorFrame
end
