local AddOnName, nsVars = ...
local ss = nsVars.SS

local setmetatable = setmetatable
local rawset, rawget = rawset, rawget
local table, pairs, type, unpack, next = table, pairs, type, unpack, next
local print, math, floor, tostring = print, math, floor, tostring
local IsShiftKeyDown, GetCursorPosition = IsShiftKeyDown, GetCursorPosition

local mtUtils = {}

--- Provides a sorted itterator for a table.
-- @param t The table.
-- @param f The sorting format.
-- @return The sorted itterator.
--
function mtUtils.pairsByKeys(t, f)
  local a = {}
  
  for n in pairs(t) do
    table.insert(a, n)
  end
  
  table.sort(a, f)
  local i = 0
  
  local iter = function()
    i = i + 1
    if a[i] == nil then
      return nil
    else
      return a[i], t[a[i]]
    end
  end
  
  return iter
end

--- Set's a table and it's children's metatables.
-- @param vars The existing table.
-- @param stored The next metatable.
-- @return The configured table.
--
function mtUtils:SetMetatables(vars, stored)
  setmetatable(vars, { __index = stored })
  
  for k,v in pairs(stored) do
    if (type(v) == "table") then
      if rawget(vars, k) and type(vars[k]) ~= "table" then
        rawset(vars, k, {})
      end
      
      vars[k] = self:SetMetatables(rawget(vars, k) or {}, v)
    elseif rawget(vars, k) and type(vars[k]) == "table" then
      rawset(vars, k, nil)
    end
  end
  
  return vars
end

--- Indexes an addon component.
-- @param index The addon index.
-- @param meta The metatable.
--
function mtUtils.addModule(index, meta)
  ss[index] = setmetatable({}, { __index = meta })
end

--- Formats a string with the given values.
-- @param string The unformatted string.
-- @param ... The replacement values.
-- @return The formatted string.
-- @see string:format().
--
function mtUtils:stringf(string, ...)
  return string:format(...)
end

--- Prints a formatted string.
-- @param string The unformatted string.
-- @param ... The replacement values.
-- @see string:format().
--
function mtUtils:printf(string, ...)
  print(string:format(...))
end

--- Rounds a number by a power of 10.
-- @param num The number to round.
-- @param idp The exponential value.
-- @return The rounded value.
--
function mtUtils:round(num, idp)
  local mult = 10^(idp or 0)
  return math.floor(num * mult + 0.5) / mult
end

--- Creates or extracts values from color tables.
-- If RGB values are greater than 1, a 255 is used.
-- @param colorTable A table containing RGBA values, or the red color value when creating the color table.
-- @param g Green color value.
-- @param b Blue color value.
-- @param a Alpha value.
-- @return The color values.
--
function mtUtils:GetRGBAValues(colorTable, g, b, a)
  if not colorTable then return 1,1,1,1; end
  
  if g and b and a then
    -- Return the colors in a table.
    return { r=colorTable, g=g, b=b, a=a }
  elseif (colorTable.r and colorTable.r > 1) or (colorTable[1] and colorTable[1] > 1) then
    -- Return 255 based RGBA values.
    return (colorTable.r or colorTable[1]) / 255, (colorTable.g or colorTable[2]) / 255, (colorTable.b or colorTable[3]) / 255, (100 - (colorTable.a or colorTable[4])) / 100
  else
    -- Return RGBA values.
    return colorTable.r or colorTable[1], colorTable.g or colorTable[2], colorTable.b or colorTable[3], colorTable.a or colorTable[4]
  end
end

--- Formats a number for sphere display.
-- @param val The number to format
-- @param formatting The desired formatting. ("raw", "truncated" or "commaSeparated")
-- @param petOverride Flag to override the formatting type for pet values.
-- @return The formatted value.
--
function mtUtils:valueFormat(val, formatting, petOverride)
  local newVal = val
  if petOverride == true then
    if val > 10000 then
      newVal = (floor((val/1000)*10)/10).."k"
    end
    return newVal;
  elseif formatting == "truncated" then
    if val > 1000000 then
      newVal = (floor((val/1000000)*10)/10) .."m"
    elseif val > 999 then
      newVal = (floor((val/1000)*10)/10) .."k"
    end
    return newVal;
  elseif formatting == "commaSeparated" then
    newVal = tostring(val):reverse():gsub("%d%d%d", "%1,"):reverse():gsub("^,", "");
    return newVal;
  else
    return newVal;
  end
end

--- Makes a frame movable whike holding the selected key.
-- @param frame The target frame.
-- @param button The button to hold, defaults to "LeftButton".
-- @param listener optional callback for "OnDragStop".
--
function mtUtils:MakeFrameMovable(frame, button, listener)
  frame:SetClampedToScreen(true)
  frame:SetMovable(true)
  frame:EnableMouse(true)
  frame:RegisterForDrag(button or "LeftButton")
  frame:SetScript("OnDragStart", function(self)
    if IsShiftKeyDown() then
      self:StartMoving()
    end
  end)
  frame:SetScript("OnDragStop", function(self)
    self:StopMovingOrSizing()
    if listener then
      listener(self)
    end
  end)
end

--- Calculates a position's angle relative to the center of a frame.
-- Uses cursor position if no target is provided.
-- @param frame The target frame.
-- @param x The x position to use.
-- @param y The y position to use.
-- @return The relative angle of the target.
--
function mtUtils:AngleFromFrame(frame, x, y)
  if frame.GetCenter and frame.GetEffectiveScale then
    -- Retrieve layout values.
    local fx, fy = frame:GetCenter()
    local scale = frame:GetEffectiveScale()
  
    if not (x and y) then
      -- Use cursor position for default target.
      x, y = GetCursorPosition()
    end
  
    -- Determine the angle relative to the frame.
    x, y = x / scale, y / scale
    local angle = math.deg(math.atan2(y - fy, x - fx)) % 360
  
    return  math.rad(angle)
  end
end

--- Determines the side of a frame that should be used as an anchor onto another.
-- @param frame The frame to measure.
-- @return The anchor point to use. (ex: "RIGHT", frame, "LEFT")
--
function mtUtils:GetAnchors(frame)
  local x, y = frame:GetCenter()

  if not x or not y then
    -- Default "CENTER" anchor.
    return "CENTER"
  end
  
  -- Determine a region based value.
  local vhalf = (y > UIParent:GetHeight()/2)    and "TOP"   or "BOTTOM"
  local hhalf = (x > UIParent:GetWidth() * 2/3) and "RIGHT" or
    (x < UIParent:GetWidth()/3) and "LEFT" or ""

  return vhalf .. hhalf, frame, (vhalf == "TOP" and "BOTTOM" or "TOP") .. hhalf
end

--- Provides the "OnEnter" event handler for default unit tooltips.
-- @param self The frame displaying the tooltip.
--
local function setTooltipDefault(self)
  GameTooltip_SetDefaultAnchor(GameTooltip, UIParent)
  GameTooltip:SetUnit(self.tUnit)
  GameTooltip:Show()
end

--- Provides the "OnEnter" event handler for tooltips with custom content and properties.
-- @param self The frame displaying the tooltip.
--
local function setTooltipCustom(self)
  local err
  if self.tAccess then
    -- Run the access event.
    err = self:tAccess()
  end
  
  if not err and (self.tLines or self.tUnit) then
    GameTooltip:SetOwner(self, self.tAnchor)
    GameTooltip:SetPoint(unpack(self.tPoint))
    GameTooltip:ClearLines()
  
    if self.tUnit then
      GameTooltip:SetUnit(self.tUnit)
    end
  
    if self.tLines then
      -- Add each line to the GameTooltip.
      for delta, line in next, self.tLines do
        GameTooltip:AddLine(line)
      end
    end
  
    GameTooltip:Show()
  end
end

local function hideTooltip(self)
  if GameTooltip:IsOwned(self) then
    -- Hide the tooltip if still the owner.
    GameTooltip:Hide()
  end
end

--- Sets the frame event handlers required to display a unit tooltip.
-- @param frame The frame displaying the tooltip.
-- @param unit The unit to display.
-- @see setTooltipDefault().
-- @see GameTooltip_SetDefaultAnchor().
-- @see GameTooltip.
--
function mtUtils:AddUnitTooltip(frame, unit)
  -- Set frame property.
  frame.tUnit = unit

  -- Set events to toggle the tooltip.
  frame:HookScript("OnEnter", setTooltipDefault)
  frame:HookScript("OnLeave", hideTooltip)
end

--- Sets the frame event handlers required to display a custom tooltip.
-- @param frame The frame displaying the tooltip.
-- @param anchor The tooltip anchor.
-- @param point The tooltip point.
-- @param lines The text to display.
-- @param access Access handler.
-- @param unit The unit to display.
-- @see setTooltipCustom().
-- @see GameTooltip.
--
function mtUtils:AddTooltip(frame, anchor, point, lines, access, unit)
  -- Set frame properties.
  frame.tAnchor = anchor
  frame.tPoint  = point
  frame.tLines  = lines
  frame.tAccess = access
  frame.tUnit   = unit

  -- Set events to toggle the tooltip.
  frame:HookScript("OnEnter", setTooltipCustom)
  frame:HookScript("OnLeave", hideTooltip)
end

function mtUtils:CreateSecureUnit(name, unit, anch, x, y, scale, w, h)
  local secureBase = CreateFrame("Button", ss._f..name.."secureBase", UIParent, "SecureActionButtonTemplate")
  secureBase:SetHeight(h or w)
  secureBase:SetWidth (w)
  secureBase:SetPoint (anch, x, y)
  secureBase:SetScale(scale)
  secureBase:RegisterForClicks("AnyUp")
  secureBase:SetAttribute     ("type1", "target")
  secureBase:SetAttribute     ("target", "unit")
  secureBase:SetAttribute     ("unit", unit)
  secureBase:SetAttribute     ("type2", "togglemenu")
  secureBase:SetAttribute     ("togglemenu", unit)
  
  -- Add the tooltip.
  self:AddUnitTooltip(secureBase, unit)
end

ss.Utils = setmetatable({}, { __index = mtUtils })
