Quantcast
-- just remove global reference so it is easy to read with my ide
local pairs, tostring, type, print, string, getmetatable, table, pcall, unpack, tonumber =
pairs, tostring, type, print, string, getmetatable, table, pcall, unpack, tonumber
local HybridScrollFrame_CreateButtons, HybridScrollFrame_GetOffset, HybridScrollFrame_Update =
HybridScrollFrame_CreateButtons, HybridScrollFrame_GetOffset, HybridScrollFrame_Update

-- create global instance
ViragDevTool = {
    --static constant useed for metatable name
    METATABLE_NAME = "$metatable",
    METATABLE_NAME2 = "$metatable.__index",
    ADDON_NAME = "ViragDevTool",

    -- you can use /vdt find somestr parentname(can be in format _G.Frame.Button)
    -- for examle "/vdt find Virag" will find every variable in _G that has *Virag* pattern
    -- "/vdt find Data ViragDevTool" will find every variable that has *Data* in their name in _G.ViragDevTool object if it exists
    -- same for "startswith"
    CMD = {
        --"/vdt help"
        HELP = function()
            local a = function(txt) return "|cFF96C0CE" .. txt .. "|cFFFFFFFF" end
            local a2 = function(txt) return "|cFFBEB9B5" .. txt .. "|cFFFFFFFF" end
            local a3 = function(txt) return "|cFF3cb371" .. txt .. "|cFFFFFFFF" end


            local cFix = function(str)
                local result = "|cFFFFFFFF" .. str
                result = string.gsub(result, "name", a("name"))
                result = string.gsub(result, "eventName", a("eventName"))
                result = string.gsub(result, "tableName", a("tableName"))

                result = string.gsub(result, "parent", a2("parent"))
                result = string.gsub(result, "unit", a2("unit"))
                result = string.gsub(result, "functionName", a2("functionName"))

                result = string.gsub(result, "help", a3("help"))
                result = string.gsub(result, "find", a3("find"))
                result = string.gsub(result, "startswith", a3("startswith"))
                result = string.gsub(result, "eventadd", a3("eventadd"))
                result = string.gsub(result, "eventstop", a3("eventstop"))
                result = string.gsub(result, "logfn", a3("logfn"))
                result = string.gsub(result, "mouseover", a3("mouseover"))
                result = string.gsub(result, "vdt_reset_wnd", a3("vdt_reset_wnd"))
                return result
            end

            local help = {}
            help[cFix("01 /vdt")] = cFix("Toggle UI")
            help[cFix("02 /vdt help")] = cFix("Print help")
            help[cFix("03 /vdt name parent (optional)")] = cFix("Add _G.name or _G.parent.name to the list (ex: /vdt name A.B => _G.A.B.name")
            help[cFix("04 /vdt find name parent (optional)")] = cFix("Add name _G.*name* to the list. Adds any field name that has name part in its name")
            help[cFix("05 /vdt mouseover")] = cFix("Add hoovered frame to the list with  GetMouseFocus()")
            help[cFix("06 /vdt startswith name parent (optional)")] = cFix("Same as find but will look only for name*")
            help[cFix("07 /vdt eventadd eventName unit (optional)")] = cFix("ex: /vdt eventadd UNIT_AURA player")
            help[cFix("08 /vdt eventstop eventName")] = cFix("Stops event monitoring if active")
            help[cFix("09 /vdt logfn tableName functionName (optional)")] = cFix("Log every function call. _G.tableName.functionName")
            help[cFix("10 /vdt vdt_reset_wnd")] = cFix("Reset main frame position if you lost it for some reason")
            local sortedTable = {}
            for k, v in pairs(help) do
                table.insert(sortedTable, k)
            end

            table.sort(sortedTable)

            for _, k in pairs(sortedTable) do
                ViragDevTool:print(k .. " - " .. help[k])
            end

            return help
        end,
        -- "/vdt find Data ViragDevTool" or "/vdt find Data"
        FIND = function(msg2, msg3)
            local parent = msg3 and ViragDevTool:FromStrToObject(msg3) or _G
            return ViragDevTool:FindIn(parent, msg2, string.match)
        end,
        --"/vdt startswith Data ViragDevTool" or "/vdt startswith Data"
        STARTSWITH = function(msg2, msg3)
            local parent = msg3 and ViragDevTool:FromStrToObject(msg3) or _G
            return ViragDevTool:FindIn(parent, msg2, ViragDevTool.starts)
        end,
        --"/vdt mouseover" --m stands for mouse focus
        MOUSEOVER = function(msg2, msg3)
            local resultTable = GetMouseFocus()
            return resultTable, resultTable:GetName()
        end,
        --"/vdt eventadd ADDON_LOADED"
        EVENTADD = function(msg2, msg3)
            ViragDevTool:StartMonitorEvent(msg2, msg3)
        end,
        --"/vdt eventremove ADDON_LOADED"
        EVENTSTOP = function(msg2, msg3)
            ViragDevTool:StopMonitorEvent(msg2, msg3)
        end,
        --"/vdt log tableName fnName" tableName in global namespace and fnName in table
        LOGFN = function(msg2, msg3)
            ViragDevTool:StartLogFunctionCalls(msg2, msg3)
        end,
        VDT_RESET_WND = function(msg2, msg3)
            ViragDevToolFrame:ClearAllPoints()
            ViragDevToolFrame:SetPoint("CENTER", UIParent)
            ViragDevToolFrame:SetSize(635, 200)
        end
    },

    -- Default settings
    -- this variable will be used only on first load so it is just default init with empty values.
    -- will be replaced with ViragDevTool_Settings at 2-nd start
    default_settings = {
        -- selected list in gui. one of 3 list from settings: history or function call logs or events
        sideBarTabSelected = "history",

        -- UI saved state
        isWndOpen = true,
        isSideBarOpen = false,

        -- stores history of recent calls to /vdt
        MAX_HISTORY_SIZE = 50,
        collResizerPosition = 450,
        history = {
            -- examples
            "find LFR",
            "find SLASH",
            "find Data ViragDevTool",
            "startswith Virag",
            "ViragDevTool.settings.history",
        },
        logs = {--{
            --    fnName = "functionNameHere",
            --    parentTableName = "ViragDevTool.sometable",
            --    active = false
            --},
        },

        -- stores arguments for fcunction calls --todo implement
        tArgs = {},
        fontSize = 10, -- font size for default table
        colors = {
            white = "|cFFFFFFFF",
            gray = "|cFFBEB9B5",
            lightblue = "|cFF96C0CE",
            lightgreen = "|cFF98FB98",
            red = "|cFFFF0000",
            green = "|cFF00FF00",
            darkred = "|cFFC25B56",
            parent = "|cFFBEB9B5",
            error = "|cFFFF0000",
            ok = "|cFF00FF00",
            table = { 0.41, 0.80, 0.94, 1 },
            string = { 0.67, 0.83, 0.45, 1 },
            number = { 1, 0.96, 0.41, 1 },
            default = { 1, 1, 1, 1 },
        },

        -- events to monitor
        -- format ({event = "EVENT_NAME", unit = "player", active = true}, ...)
        -- default events inactive
        events = {
            {
                event = "CURSOR_UPDATE",
                active = false
            },
            {
                event = "UNIT_AURA",
                unit = "player",
                active = false
            },
            {
                event = "CHAT_MSG_CHANNEL",
                active = false
            }
        },
    }
}
-- just remove global reference so it is easy to read with my ide
local ViragDevTool = ViragDevTool

-----------------------------------------------------------------------------------------------
-- ViragDevTool.colors additional setup
-----------------------------------------------------------------------------------------------
ViragDevTool.default_settings.colors["function"] = { 1, 0.49, 0.04, 1 }
ViragDevTool.colors = ViragDevTool.default_settings.colors --shortcut

-----------------------------------------------------------------------------------------------
-- ViragDevToolLinkedList == ViragDevTool.list
-----------------------------------------------------------------------------------------------

--- Linked List
-- @field size
-- @field first
-- @field last
--
-- Each node has:
-- @field name - string name
-- @field value - any object
-- @field next - nil/next node
-- @field padding - int expanded level( when you click on table it expands  so padding = padding + 1)
-- @field parent - parent node after it expanded
-- @field expanded - true/false/nil

local ViragDevToolLinkedList = {}

function ViragDevToolLinkedList:new(o)
    o = o or {}
    setmetatable(o, self)
    self.__index = self
    self.size = 0
    return o
end

function ViragDevToolLinkedList:GetInfoAtPosition(position)
    if self.size < position or self.first == nil then
        return nil
    end

    local node = self.first
    while position > 0 do
        node = node.next
        position = position - 1
    end

    return node
end

function ViragDevToolLinkedList:AddNodesAfter(nodeList, parentNode)
    local tempNext = parentNode.next
    local currNode = parentNode;

    for _, node in pairs(nodeList) do
        currNode.next = node
        currNode = node
        self.size = self.size + 1;
    end

    currNode.next = tempNext

    if tempNext == nil then
        self.last = currNode
    end
end

function ViragDevToolLinkedList:AddNode(data, dataName)
    local node = self:NewNode(data, dataName)

    if self.first == nil then
        self.first = node
        self.last = node
    else
        if self.last ~= nil then
            self.last.next = node
        end
        self.last = node
    end

    self.size = self.size + 1;
end

function ViragDevToolLinkedList:NewNode(data, dataName, padding, parent)
    return {
        name = dataName,
        value = data,
        next = nil,
        padding = padding == nil and 0 or padding,
        parent = parent
    }
end

function ViragDevToolLinkedList:RemoveChildNodes(node)
    local currNode = node

    while true do

        currNode = currNode.next

        if currNode == nil then
            node.next = nil
            self.last = node
            break
        end

        if currNode.padding <= node.padding then
            node.next = currNode
            break
        end

        self.size = self.size - 1
    end
end

function ViragDevToolLinkedList:Clear()
    self.size = 0
    self.first = nil
    self.last = nil
end

-----------------------------------------------------------------------------------------------
-- ViragDevTool main
-----------------------------------------------------------------------------------------------
ViragDevTool.list = ViragDevToolLinkedList:new()

---
-- Main (and the only) function you can use in ViragDevTool API
-- Will add data to the list so you can explore its values in UI list
-- @usage
-- Lets suppose you have MyModFN function in yours addon
-- function MyModFN()
-- local var = {}
-- ViragDevTool_AddData(var, "My local var in MyModFN")
-- end
-- This will add var as new var in our list
-- @param data (any type)- is object you would like to track.
-- Default behavior is shallow copy
-- @param dataName (string or nil) - name tag to show in UI for you variable.
-- Main purpose is to give readable names to objects you want to track.
function ViragDevTool_AddData(data, dataName)
    if dataName == nil then
        dataName = tostring(data)
    end

    ViragDevTool.list:AddNode(data, tostring(dataName))
    ViragDevTool:UpdateMainTableUI()
end

function ViragDevTool:Add(data, dataName)
    ViragDevTool_AddData(data, dataName)
end

function ViragDevTool:ExecuteCMD(msg, bAddToHistory)
    if msg == "" then msg = "_G" end
    local resultTable

    local msgs = self.split(msg, " ")
    local cmd = self.CMD[string.upper(msgs[1])]

    if cmd then
        local title
        resultTable, title = cmd(msgs[2], msgs[3])

        if title then msg = title end
    else
        resultTable = self:FromStrToObject(msg)
        if not resultTable then
            self:print("_G." .. msg .. " == nil, so can't add")
        end
    end

    if resultTable then
        if bAddToHistory then
            ViragDevTool:AddToHistory(msg)
        end

        self:Add(resultTable, msg)
    end
end

function ViragDevTool:FromStrToObject(str)

    if str == "_G" then return _G end
    local vars = self.split(str, ".") or {}

    local var = _G
    for _, name in pairs(vars) do
        if var then
            var = var[name]
        end
    end

    return var
end

function ViragDevTool:ClearData()
    self.list:Clear()
    self:UpdateMainTableUI()
end

function ViragDevTool:ExpandCell(info)

    local nodeList = {}
    local padding = info.padding + 1
    local counter = 1
    local mt
    for k, v in pairs(info.value) do
        if type(v) ~= "userdata" then
            nodeList[counter] = self.list:NewNode(v, tostring(k), padding, info)
        else
            local mt = getmetatable(v)
            if mt then
                nodeList[counter] = self.list:NewNode(mt, self.METATABLE_NAME .. " for " .. tostring(k), padding, info)
            else
                if k == 0 then counter = counter - 1 else
                    nodeList[counter] = self.list:NewNode(v, self.METATABLE_NAME .. " not found for " .. tostring(k), padding, info)
                end
            end
        end

        counter = counter + 1
    end

    local mt = getmetatable(info.value)
    if mt then
        nodeList[counter] = self:NewMetatableNode(mt, padding, info)
    else
    end

    table.sort(nodeList, self:SortFnForCells(nodeList))

    self.list:AddNodesAfter(nodeList, info)

    info.expanded = true

    ViragDevTool:UpdateMainTableUI()
end

function ViragDevTool:NewMetatableNode(mt, padding, info)
    if mt then
        if self:tablelength(mt) == 1 and mt.__index then
            return self.list:NewNode(mt.__index, self.METATABLE_NAME2, padding, info)
        else
            return self.list:NewNode(mt, self.METATABLE_NAME, padding, info)
        end
    end
end

function ViragDevTool:IsMetaTableNode(info)
    return info.name == self.METATABLE_NAME or info.name == self.METATABLE_NAME2
end

function ViragDevTool:SortFnForCells(nodeList)

    local cmpFn = function(a, b)
        if a.name == "__index" then return true
        elseif b.name == "__index" then return false
        else
            return a.name < b.name
        end
    end

    if #nodeList > 20000 then --  just optimisation for _G
    cmpFn = function(a, b) return a.name < b.name end
    end
    --lets try some better sorting if we have small number of records
    --numbers will be sorted not like 1,10,2 but like 1,2,10
    if #nodeList < 300 then
        cmpFn = function(a, b)
            if a.name == "__index" then return true
            elseif b.name == "__index" then return false
            else
                if tonumber(a.name) ~= nil and tonumber(b.name) ~= nil then
                    return tonumber(a.name) < tonumber(b.name)
                else
                    return a.name < b.name
                end
            end
        end
    end

    return cmpFn
end

function ViragDevTool:ColapseCell(info)
    self.list:RemoveChildNodes(info)
    info.expanded = nil
    self:UpdateMainTableUI()
end

-----------------------------------------------------------------------------------------------
-- UI
-----------------------------------------------------------------------------------------------
function ViragDevTool:UpdateUI()
    self:UpdateMainTableUI()
    self:UpdateSideBarUI()
end

function ViragDevTool:ToggleUI()
    self:Toggle(self.wndRef)
    self.settings.isWndOpen = self.wndRef:IsVisible()
end

function ViragDevTool:Toggle(view)
    self:SetVisible(view, not view:IsVisible())
end

function ViragDevTool:SetVisible(view, isVisible)
    if not view then return end

    if isVisible then
        view:Show()
    else
        view:Hide()
    end
end

-- i ddo manual resizing and not the defalt
-- self:GetParent():StartSizing("BOTTOMRIGHT");
-- self:GetParent():StopMovingOrSizing();
-- BEACUSE i don't like default behaviur.
function ViragDevTool:ResizeMainFrame(dragFrame)
    local parentFrame = dragFrame:GetParent()

    local left = dragFrame:GetParent():GetLeft()
    local top = dragFrame:GetParent():GetTop()

    local x, y = GetCursorPosition()
    local s = parentFrame:GetEffectiveScale()
    x = x / s
    y = y / s

    local maxX, maxY = parentFrame:GetMaxResize()
    local minX, minY = parentFrame:GetMinResize()


    parentFrame:SetSize(self:CalculatePosition(x - left, minX, maxX),
        self:CalculatePosition(top - y, minY, maxY))
end


function ViragDevTool:DragResizeColumn(dragFrame, ignoreMousePosition)
    local parentFrame = dragFrame:GetParent()

    -- 150 and 50 are just const values. safe to change
    local minX = 150
    local maxX = parentFrame:GetWidth() - 50

    local pos = dragFrame:GetLeft() - parentFrame:GetLeft()
    pos = self:CalculatePosition(pos, minX, maxX)


    if not ignoreMousePosition then
        local x, y = GetCursorPosition()
        local s = parentFrame:GetEffectiveScale()
        x = x / s
        y = y / s
        if x <= (minX + parentFrame:GetLeft()) then pos = minX end
        if x >= (maxX + parentFrame:GetLeft()) then pos = maxX end
    end

    dragFrame:ClearAllPoints()
    dragFrame:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", pos, -30) -- 30 is offset from above (top buttons)

    -- save pos so we can restore it on reloda ui or logout
    self.settings.collResizerPosition = pos
end

function ViragDevTool:CalculatePosition(pos, min, max)
    if pos < min then pos = min end
    if pos > max then pos = max end
    return pos
end

-----------------------------------------------------------------------------------------------
-- Main table UI
-----------------------------------------------------------------------------------------------
function ViragDevTool:ForceUpdateMainTableUI()
    self:UpdateMainTableUI(true)
end


function ViragDevTool:UpdateMainTableUI(force)

    if not force then
        self:UpdateMainTableUIOptimized()
        return
    end

    local scrollFrame = self.wndRef.scrollFrame
    self:ScrollBar_AddChildren(scrollFrame, "ViragDevToolEntryTemplate")

    local buttons = scrollFrame.buttons;

    local offset = HybridScrollFrame_GetOffset(scrollFrame)
    self:UpdateScrollFrameRowSize(scrollFrame)
    local totalRowsCount = self.list.size
    local lineplusoffset;

    local nodeInfo = self.list:GetInfoAtPosition(offset)
    for k, view in pairs(buttons) do
        lineplusoffset = k + offset;
        if lineplusoffset <= totalRowsCount then
            self:UIUpdateMainTableButton(view, nodeInfo, lineplusoffset)
            nodeInfo = nodeInfo.next
            view:Show();
        else
            view:Hide();
        end
    end

    HybridScrollFrame_Update(scrollFrame, totalRowsCount * buttons[1]:GetHeight(), scrollFrame:GetHeight());

    scrollFrame.scrollChild:SetWidth(scrollFrame:GetWidth())
end

function ViragDevTool:UpdateScrollFrameRowSize(scrollFrame)
    local currentFont = self.settings and self.settings.fontSize or 10

    local buttons = scrollFrame.buttons;
    local cellHeight = currentFont + currentFont * 0.2
    cellHeight = cellHeight %2 == 0 and cellHeight or cellHeight + 1
    for _, button in pairs(buttons) do
        button:SetHeight(cellHeight)
        local font = button.nameButton:GetFontString():GetFont()
        button.nameButton:GetFontString():SetFont(font, currentFont)
        button.rowNumberButton:GetFontString():SetFont(font, currentFont)
        button.valueButton:GetFontString():SetFont(font, currentFont)
    end

    scrollFrame.buttonHeight = cellHeight


end

function ViragDevTool:UpdateMainTableUIOptimized()

    if (self.waitFrame == nil) then
        self.waitFrame = CreateFrame("Frame", "ViragDevToolWaitFrame", UIParent);
        self.waitFrame.lastUpdateTime = 0
        self.waitFrame:SetScript("onUpdate", function(self, elapse)

            if self.updateNeeded then
                self.lastUpdateTime = self.lastUpdateTime + elapse
                if self.lastUpdateTime > 0.1 then
                    --preform update
                    ViragDevTool:ForceUpdateMainTableUI()
                    self.updateNeeded = false
                    self.lastUpdateTime = 0
                end
            end
        end);
    end

    self.waitFrame.updateNeeded = true
end

function ViragDevTool:ScrollBar_AddChildren(scrollFrame, strTemplate)
    if scrollFrame.ScrollBarHeight == nil or scrollFrame:GetHeight() > scrollFrame.ScrollBarHeight then
        scrollFrame.ScrollBarHeight = scrollFrame:GetHeight()

        local scrollBarValue = scrollFrame.scrollBar:GetValue()
        HybridScrollFrame_CreateButtons(scrollFrame, strTemplate, 0, -2)
        scrollFrame.scrollBar:SetValue(scrollBarValue);

    end
end

function ViragDevTool:UIUpdateMainTableButton(node, info, id)
    local color = self.colors[type(info.value)]
    if not color then color = self.colors.default end
    if type(info.value) == "table" and self:IsMetaTableNode(info) then
        color = self.colors.default
    end

    node.nameButton:SetPoint("LEFT", node.rowNumberButton, "RIGHT", 10 * info.padding - 10, 0)

    node.valueButton:SetText(self:ToUIString(info.value, info.name, true))
    node.nameButton:SetText(tostring(info.name))
    node.rowNumberButton:SetText(tostring(id))

    node.nameButton:GetFontString():SetTextColor(unpack(color))
    node.valueButton:GetFontString():SetTextColor(unpack(color))
    node.rowNumberButton:GetFontString():SetTextColor(unpack(color))

    self:SetMainTableButtonScript(node.nameButton, info)
    self:SetMainTableButtonScript(node.valueButton, info)
end

function ViragDevTool:ToUIString(value, name, withoutLineBrakes)
    local result
    local valueType = type(value)

    if valueType == "table" then
        result = self:GetObjectInfoFromWoWAPI(name, value) or tostring(value)
        result = "(" .. self:tablelength(value) .. ") " .. result
    else
        result = tostring(value)
    end

    if withoutLineBrakes then
        result = string.gsub(string.gsub(tostring(result), "|n", ""), "\n", "")
    end

    return result
end

function ViragDevTool:GetObjectInfoFromWoWAPI(helperText, value)
    local resultStr
    local ok, objectType = self:TryCallAPIFn(value.GetObjectType, value)

    -- try to get frame name
    if ok then
        local concat = function(str, before, after)
            before = before or ""
            after = after or ""
            if str then
                return resultStr .. " " .. before .. str .. after
            end
            return resultStr
        end

        local _, name = self:TryCallAPIFn(value.GetName, value)
        local _, texture = self:TryCallAPIFn(value.GetTexture, value)
        local _, text = self:TryCallAPIFn(value.GetText, value)

        local hasSize, left, bottom, width, height = self:TryCallAPIFn(value.GetBoundsRect, value)


        resultStr = objectType or ""
        if hasSize then
            resultStr = concat(self.colors.white .. "[" ..
                    tostring(self:round(left)) .. ", " ..
                    tostring(self:round(bottom)) .. ", " ..
                    tostring(self:round(width)) .. ", " ..
                    tostring(self:round(height)) .. "]",
                self.colors.lightblue)
        end


        if helperText ~= name then
            resultStr = concat(name, self.colors.gray .. "<", ">" .. self.colors.white)
        end

        resultStr = concat(texture, self.colors.white, self.colors.white)
        resultStr = concat(text, self.colors.white .. "'", "'")
        resultStr = concat(tostring(value), self.colors.lightblue)
    end

    return resultStr
end

function ViragDevTool:TryCallAPIFn(fn, value)
    -- this function is helper fn to get table type from wow api.
    -- if there is GetObjectType then we will return it.
    -- returns Button, Frame or something like this

    -- VALIDATION
    if type(value) ~= "table" then return
    end

    -- VALIDATION FIX if __index is function we dont want to execute it
    -- Example in ACP.L
    local mt = getmetatable(value)
    if mt and type(mt.__index) == "function" then return
    end

    -- VALIDATION is forbidden from wow api
    if value.IsForbidden then
        local ok, forbidden = pcall(value.IsForbidden, value)
        if not ok or (ok and forbidden) then return
        end
    end
    -- VALIDATION has WoW API
    if not fn or type(fn) ~= "function" then return
    end

    -- MAIN PART:
    return pcall(fn, value)
end

-----------------------------------------------------------------------------------------------
-- Sidebar UI
-----------------------------------------------------------------------------------------------
function ViragDevTool:ToggleSidebar()
    self:Toggle(self.wndRef.sideFrame)
    self.settings.isSideBarOpen = self.wndRef.sideFrame:IsVisible()
    self:UpdateSideBarUI()
end

function ViragDevTool:SubmitEditBoxSidebar()
    local edditBox = self.wndRef.sideFrame.editbox
    local msg = edditBox:GetText()
    local selectedTab = self.settings.sideBarTabSelected
    local cmd = msg

    if selectedTab == "logs" then
        cmd = "logfn " .. msg
    elseif selectedTab == "events" then
        cmd = "eventadd " .. msg
    end

    self:ExecuteCMD(cmd, true)
    self:UpdateSideBarUI()
end

function ViragDevTool:EnableSideBarTab(tabStrName)
    --Update ui
    local sidebar = self.wndRef.sideFrame
    sidebar.history:SetChecked(false)
    sidebar.events:SetChecked(false)
    sidebar.logs:SetChecked(false)
    sidebar[tabStrName]:SetChecked(true)

    -- update selected tab  and function to update cell items
    self.settings.sideBarTabSelected = tabStrName

    -- refresh ui
    self:UpdateSideBarUI()
end

function ViragDevTool:UpdateSideBarUI()
    local scrollFrame = self.wndRef.sideFrame.sideScrollFrame

    self:ScrollBar_AddChildren(scrollFrame, "ViragDevToolSideBarRowTemplate")

    local buttons = scrollFrame.buttons;

    local offset = HybridScrollFrame_GetOffset(scrollFrame)
    local data = self.settings and self.settings[self.settings.sideBarTabSelected] or {}
    local totalRowsCount = self:tablelength(data)

    for k, frame in pairs(buttons) do
        local lineplusoffset = k + offset;

        if lineplusoffset > totalRowsCount then
            frame:Hide();
        else
            self:UpdateSideBarRow(frame.mainButton, data, lineplusoffset)

            --setup remove button for every row
            frame.actionButton:SetScript("OnMouseUp", function()
                table.remove(data, lineplusoffset)
                self:UpdateSideBarUI()
            end)

            frame:Show();
        end
    end

    HybridScrollFrame_Update(scrollFrame, totalRowsCount * buttons[1]:GetHeight(), scrollFrame:GetHeight());
end

function ViragDevTool:UpdateSideBarRow(view, data, lineplusoffset)
    local selectedTab = self.settings.sideBarTabSelected

    local currItem = data[lineplusoffset]

    local colorForState = function(isActive)
        return isActive and ViragDevTool.colors.white or ViragDevTool.colors.gray
    end

    if selectedTab == "history" then
        -- history update
        local name = tostring(currItem)
        view:SetText(name)
        view:SetScript("OnMouseUp", function()
            ViragDevTool:ExecuteCMD(name)
            --move to top
            table.remove(data, lineplusoffset)
            table.insert(data, 1, currItem)
            ViragDevTool:UpdateSideBarUI()
        end)

    elseif selectedTab == "logs" then
        local text = self:LogFunctionCallText(currItem)

        -- logs update
        view:SetText(colorForState(currItem.active) .. text)
        view:SetScript("OnMouseUp", function()
            ViragDevTool:ToggleFnLogger(currItem)
            view:SetText(colorForState(currItem.active) .. text)
        end)

    elseif selectedTab == "events" then
        -- events  update
        view:SetText(colorForState(currItem.active) .. currItem.event)
        view:SetScript("OnMouseUp", function()
            ViragDevTool:ToggleMonitorEvent(currItem)
            view:SetText(colorForState(currItem.active) .. currItem.event)
        end)
    end
end

-----------------------------------------------------------------------------------------------
-- Main table row button clicks setup
-----------------------------------------------------------------------------------------------
function ViragDevTool:SetMainTableButtonScript(button, info)
    --todo add left click = copy to chat
    local valueType = type(info.value)
    local leftClickFn = function()
    end

    if valueType == "table" then
        leftClickFn = function(this, button, down)
            if info.expanded then
                self:ColapseCell(info)
            else
                self:ExpandCell(info)
            end
        end
    elseif valueType == "function" then
        leftClickFn = function(this, button, down)
            self:TryCallFunction(info)
        end
    end

    button:SetScript("OnMouseUp", function(this, mouseButton, down)
        if mouseButton == "RightButton" then
            local nameButton = this:GetParent().nameButton
            local valueButton = this:GetParent().valueButton
            ViragDevTool:print(nameButton:GetText() .. " - " .. valueButton:GetText())
        else
            leftClickFn(this, mouseButton, down)
        end
    end)
end

function ViragDevTool:TryCallFunction(info)
    -- info.value is just our function to call
    local parent, ok
    local fn = info.value
    local args = { unpack(self.settings.tArgs) }
    for k, v in pairs(args) do
        if type(v) == "string" and self.starts(v, "t=") then

            local obj = self:FromStrToObject(string.sub(v, 3))
            if obj then
                args[k] = obj
            end
        end
    end

    -- lets try safe call first
    local ok, results = self:TryCallFunctionWithArgs(fn, args)

    if not ok then
        -- if safe call failed we probably could try to find self and call self:fn()
        parent = self:GetParentTable(info)

        if parent then
            args = { parent.value, unpack(args) } --shallow copy and add parent table
            ok, results = self:TryCallFunctionWithArgs(fn, args)
        end
    end

    self:ProcessCallFunctionData(ok, info, parent, args, results)
end

function ViragDevTool:GetParentTable(info)
    local parent = info.parent
    if parent and parent.value == _G then
        -- this fn is in global namespace so no parent
        parent = nil
    end

    if parent then
        if self:IsMetaTableNode(parent) then
            -- metatable has real object 1 level higher
            parent = parent.parent
        end
    end

    return parent
end

function ViragDevTool:TryCallFunctionWithArgs(fn, args)
    local results = { pcall(fn, unpack(args, 1, 10)) }
    local ok = results[1]
    table.remove(results, 1)
    return ok, results
end

-- this function is kinda hard to read but it just adds new items to list and prints log in chat.
-- will add 1 row for call result(ok or error) and 1 row for each return value
function ViragDevTool:ProcessCallFunctionData(ok, info, parent, args, results)
    local nodes = {}

    self:ColapseCell(info) -- if we already called this fn remove old results

    local C = self.colors
    local list = self.list
    local padding = info.padding + 1

    local stateStr = function(state)
        if state then return C.ok .. "OK"
        end
        return C.error .. "ERROR"
    end

    --constract collored full function call name
    local fnNameWithArgs = C.white .. info.name .. C.lightblue .. "(" .. self:argstostring(args) .. ")" .. C.white
    fnNameWithArgs = parent and C.gray .. parent.name .. ":" .. fnNameWithArgs or fnNameWithArgs

    local returnFormatedStr = ""

    -- itterate backwords because we want to include every meaningfull nil result
    -- and with default itteration like pairs() we will just skip them so
    -- for example 1, 2, nil, 4 should return only this 4 values nothing more, nothing less.
    local found = false
    for i = 10, 1, -1 do
        if results[i] ~= nil then found = true
        end

        if found or i == 1 then -- if found some return or if return is nil
        nodes[i] = list:NewNode(results[i], string.format("  return: %d", i), padding)

        returnFormatedStr = string.format(" %s%s %s(%s)%s", C.white, tostring(results[i]),
            C.lightblue, type(results[i]), returnFormatedStr)
        end
    end

    -- create fist node of result info no need for now. will use debug
    table.insert(nodes, 1, list:NewNode(string.format("%s - %s", stateStr(ok), fnNameWithArgs), -- node value
        C.white .. date("%X") .. " function call results:", padding))


    -- adds call result to our UI list
    list:AddNodesAfter(nodes, info)
    self:UpdateMainTableUI()

    --print info to chat
    self:print(stateStr(ok) .. " " .. fnNameWithArgs .. C.gray .. " returns:" .. returnFormatedStr)
end

-----------------------------------------------------------------------------------------------
-- BOTTOM PANEL Fn Arguments button  and arguments input eddit box
-----------------------------------------------------------------------------------------------
function ViragDevTool:SetArgForFunctionCallFromString(argStr)
    local args = self.split(argStr, ",") or {}

    local trim = function(s)
        return (s:gsub("^%s*(.-)%s*$", "%1"))
    end

    for k, arg in pairs(args) do

        arg = trim(arg)
        if tonumber(arg) then
            args[k] = tonumber(arg)
        elseif arg == "nil" then
            args[k] = nil
        elseif arg == "true" then
            args[k] = true
        elseif arg == "false" then
            args[k] = false
        end
    end

    self.settings.tArgs = args
    self:Add(args, "New Args for function calls")
end

-----------------------------------------------------------------------------------------------
-- LIFECICLE
-----------------------------------------------------------------------------------------------
function ViragDevTool:OnLoad(mainFrame)
    self.wndRef = mainFrame

    self.wndRef:RegisterEvent("ADDON_LOADED")
    self.wndRef:SetScript("OnEvent", function(this, event, addonName, ...)
        if event == "ADDON_LOADED" and addonName == self.ADDON_NAME then
            self:OnAddonSettingsLoaded()
        end
    end);

    --register update scrollFrame
    self.wndRef.scrollFrame.update = function()
        self:ForceUpdateMainTableUI()
    end

    self.wndRef.sideFrame.sideScrollFrame.update = function()
        self:UpdateSideBarUI()
    end

    -- register slash cmd
    SLASH_VIRAGDEVTOOLS1 = '/vdt';
    function SlashCmdList.VIRAGDEVTOOLS(msg, editbox)
        if msg == "" or msg == nil then
            self:ToggleUI()
        else
            self:ExecuteCMD(msg, true)
        end
    end

    self:UpdateUI()
end

function ViragDevTool:OnAddonSettingsLoaded()
    local s = ViragDevTool_Settings

    if s == nil then
        s = self.default_settings
        ViragDevTool_Settings = s
    else
        -- validating current settings and updating if version changed

        for k, defaultValue in pairs(self.default_settings) do
            local savedValue = s[k] -- saved value from "newSettings"

            -- if setting is a table of size 0 or if value is nil set it to default
            -- for now we have only arrays in settings so its fine to use #table
            if (type(savedValue) == "table" and self:tablelength(savedValue) == 0)
                    or savedValue == nil then

                s[k] = defaultValue
            end
        end
    end

    --save to local var, so it is easy to use
    self.settings = s

    -- refresh gui

    -- setup open o closed main wnd
    self:SetVisible(self.wndRef, s.isWndOpen)

    -- setup open or closed sidebar
    self:SetVisible(self.wndRef.sideFrame, s.isSideBarOpen)

    -- setup selected sidebar tab history/events/logs
    self:EnableSideBarTab(s.sideBarTabSelected)

    -- setup logs. Just disable all of them for now on startup
    for _, tLog in pairs(self.settings.logs) do
        tLog.active = false
    end

    -- setup events part 1 register listeners
    for _, tEvent in pairs(self.settings.events) do
        if tEvent.active then
            self:StartMonitorEvent(tEvent.event, tEvent.unit)
        end
    end

    -- show in UI fn saved args if you have them
    local args = ""
    local delim = ""
    for _, arg in pairs(s.tArgs) do
        args = tostring(arg) .. delim .. args
        delim = ", "
    end

    self.wndRef.editbox:SetText(args)

    -- setup events part 2 set scripts on frame to listen registered events
    self:SetMonitorEventScript()


    --we store colors not in saved settings for now
    if s.colors then self.colors = s.colors
    end
    s.colors = self.colors

    self:LoadInterfaceOptions()

    self.wndRef.columnResizer:SetPoint("TOPLEFT", self.wndRef, "TOPLEFT", s.collResizerPosition, -30)

    self:UpdateUI()
end

-----------------------------------------------------------------------------------------------
-- UTILS
-----------------------------------------------------------------------------------------------
function ViragDevTool:print(strText)
    print(self.colors.darkred .. "[Virag's DT]: " .. self.colors.white .. strText)
end

function ViragDevTool:split(sep)
    local sep, fields = sep or ".", {}
    local pattern = string.format("([^%s]+)", sep)
    self:gsub(pattern, function(c) fields[#fields + 1] = c
    end)
    return fields
end

function ViragDevTool.starts(String, Start)
    return string.sub(String, 1, string.len(Start)) == Start
end

function ViragDevTool.ends(String, End)
    return End == '' or string.sub(String, -string.len(End)) == End
end

function ViragDevTool:tablelength(T)
    local count = 0
    for _ in pairs(T) do count = count + 1
    end
    return count
end

function ViragDevTool:argstostring(args)
    local strArgs = ""
    local found = false
    local delimiter = ""
    for i = 10, 1, -1 do
        if args[i] ~= nil then found = true
        end

        if found then
            strArgs = tostring(args[i]) .. delimiter .. strArgs
            delimiter = ", "
        end
    end
    return strArgs
end

function ViragDevTool:round(num, idp)
    if num == nil then return nil end
    local mult = 10 ^ (idp or 0)
    return math.floor(num * mult + 0.5) / mult
end

function ViragDevTool:RGBPercToHex(r, g, b, a)
    r = r <= 1 and r >= 0 and r or 0
    g = g <= 1 and g >= 0 and g or 0
    b = b <= 1 and b >= 0 and b or 0
    a = a <= 1 and a >= 0 and a or 0
    return string.format("%02x%02x%02x%02x", a * 255, r * 255, g * 255, b * 255)
end

local function HexToRGBPerc(hex)
    local rhex, ghex, bhex = string.sub(hex, 1, 2), string.sub(hex, 3, 4), string.sub(hex, 5, 6)
    return tonumber(rhex, 16) / 255, tonumber(ghex, 16) / 255, tonumber(bhex, 16) / 255
end