--- Provides Uitility Functions
--
local Utils = {}
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 GetCursorPosition = GetCursorPosition
local error, pcall = error, pcall
local CreateFrame = ss.CreateFrame

-- Provides Frame:OnUpdate callbacks without recreating frames.
local onUpdateListeners = {}
local eventFrame = CreateFrame("Frame")

eventFrame:HookScript("OnUpdate", function()
  for k,v in pairs(onUpdateListeners) do
    v()
  end
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 access = true
  if self.tAccess then
    -- Run the access event.
    access = self:tAccess()
  end

  if access 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

--- Provides the "OnHide" event handler for tooltips.
-- @param self The frame displaying the tooltip.
--
local function hideTooltip(self)
  if GameTooltip:IsOwned(self) then
    -- Hide the tooltip if still the owner.
    GameTooltip:Hide()
  end
end

--- Executes a function without causing errors.
-- The first error encountered will be passed,
-- to the error handler.
--
-- The given table must be structured as follows:
--   {
--     ["_func"] = __function__,
--     __params__,
--     ["catch"] = __function__,
--   }
--
-- @Usage
--  local vals = {param1, param2, param3,}
--
--  vals._func = function(param1, param2, param3)
--    error("nope")
--    return "completed", param1, param2, param3
--  end
--
--  vals._catch = function(err)
--    print(err)
--    return "found error"
--  end
--
--  try(args)
--
-- @param vals A table containing:
--          the unsafe function, (Required index: [1])
--          the parameters to use,
--          the error handler. (Required index: ["catch"])
--
-- @return The result of the function
--           or the result of the error handler.
--
local function try(vals)
  local func, catch = vals._func, vals._catch
  vals._catch, vals._func = nil, nil

  local result = {
    pcall(function()
      return { func(unpack(vals)) }
    end)
  }

  if not result[1] then
    return catch(result[2])
  end

  return unpack(result[2])
end

--- Generates an error message for invalid arguments.
-- Checks args for given and expected arguments,
-- adding an entry for each mismatch.
-- Usage:
--   ErrorInvalidArgs("MyObjectType", "myFunctionName",
--     {"param1", "string", param1},
--     {"param2", "number", param2}
--   )
-- @param obj The type of object generating the error. (Optional - default: "lua")
-- @param func The function being called.
-- @param ... Parameter list.
--
local function ErrorInvalidArgs(obj, func, ...)
  local args = {...}
  local err = ss.Utils:stringf("%s:%s() - Invalid Arguments: ", (obj or "lua"), (func or "ErrorInvalidArgs"))

  for index, arg in pairs(args) do
    if type(arg[3]) ~= arg[2] then
      err = err .. ss.Utils:stringf("\n\t[%d]%s: %s given, %s expected.", index, arg[1], type(arg[3]), arg[2])
    end
  end

  return err
end

--- Provides a wrapped Error function.
-- @param name The error name.
-- @param handler The error function to wrap.
-- @param paramTypes The input parameter types.
-- @return The wrapped function.
--
local function Error(name, handler, paramTypes)
  return function(...)
    local vals = {...}
    local requiredParams = {}

    for key, val in pairs(paramTypes or {}) do
      table.insert(requiredParams, { "", val, vals[key], })
    end

    vals._catch = function(err)
      Utils.throwAtLevel(ErrorInvalidArgs, 6, "Error", name, unpack(requiredParams))
      --      error(err, 6) -- THIS IS THE ACTUAL ERROR
    end
    
    vals._func = function(...)
      return handler(...)
    end

    return try(vals)
  end
end

Utils.Errors = {
  ErrorInvalidArgs = Error("ErrorInvalidArgs", ErrorInvalidArgs, {"string", "string",}),
}

Utils.errInvalidArgs = ErrorInvalidArgs
Utils.try = try
Utils.Error = Error

Utils._tmp = {}

--- Provides a sorted itterator for a table.
-- @param t The table.
-- @param f The sorting format.
-- @return The sorted itterator.
--
function Utils.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 Utils: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 Utils.addModule(index, meta)
  ss[index] = setmetatable({}, { __index = meta })
end

--- Indexes an addon class.
-- @param index The addon index.
-- @param meta The metatable.
--
function Utils.addClass(index, meta)
  ss[index] = {}

  ss[index].New = function()
    local instance = setmetatable({}, meta)
    meta.__index = meta

    return instance
  end
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 Utils:stringf(string, ...)
  return ... and string:format(...) or string
end

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

--- copies a table and all sub tables.
-- @param t The table to copy.
-- @param deep Flag to copy child tables.
-- @param seen The list of processed tables. (Internal)
--
function Utils.tcopy(t, deep, seen)
  seen = seen or {}
  if t == nil then return nil end
  if seen[t] then return seen[t] end

  local nt = {}
  for k, v in pairs(t) do
    if deep and type(v) == 'table' then
      nt[k] = ss.Utils.tcopy(v, deep, seen)
    else
      nt[k] = v
    end
  end
  setmetatable(nt, ss.Utils.tcopy(getmetatable(t), deep, seen))
  seen[t] = nt
  return nt
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 Utils:round(num, idp)
  local mult = 10^(idp or 0)
  return math.floor(num * mult + 0.5) / mult
end

--- Resets the current profile and reloads the UI.
--
function Utils:ResetSpheres()
  Seamore[Spheres.profileName] = {}
  ReloadUI()
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 Utils: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 Utils: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

--- Removes temp data once it is no longer used.
--
function Utils:ClearTemp()
  local time = GetTime()
  local completed = {};

  for k,v in pairs(self._tmp) do
    if v.created + 1800 < time then
      -- Clear whatever is in here.
      table.insert(completed, k)
    end
  end

  for k,v in pairs(completed) do
    self._tmp[k] = nil
  end

  -- Recreate the container if all objects have been removed.
  self._tmp = self._tmp or {}
end

--- Storea temporary values.
-- @param name
-- @param temp
--
function Utils:NewTemp(name, temp)
  self:ClearTemp()
  self._tmp[name] = {
    temp = temp,
    created = GetTime(),
  }
end

--- Retrieves a temporary value.
-- @param name The table name. (index)
-- @return The stored value.
-- @see Utils:NewTemp()
--
function Utils:GetTemp(name)
  return self._tmp[name] and self._tmp[name].temp or nil
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 Utils: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 Utils: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

--- 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 Utils:AddUnitTooltip(frame, unit)
  if frame.tUnit then
    frame.tUnit = unit
  else
    -- Set frame property.
    frame.tUnit = unit
  
    -- Set events to toggle the tooltip.
    frame:HookScript("OnEnter", setTooltipDefault)
    frame:HookScript("OnLeave", hideTooltip)
  end
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 Utils: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

--- Creates a unit frame using the "SecureActionButtonTemplate"
-- @param name The name of the frame.
-- @param unit The unit to display.
-- @param anch The anchor
-- @param x The x position.
-- @param y The y position.
-- @param scale The scale
-- @param w The width.
-- @param h The height.
--
function Utils: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)

  return secureBase
end

function Utils:Class(obj)
  return {
    New = function(self, vals, extending)
      local instance = {}

      for k,v in pairs(obj) do
        instance[k] = v
      end

      if vals then
        for k,v in pairs(vals) do
          if instance[k] and type(instance[k]) ~= "function" then
            instance[k] = v
          end
        end
      end

      if instance.OnCreate and not extending then
        instance:OnCreate()
      end

      return instance
    end,
    Extends = function(self, ext)
      local instance = ext:New(nil, true)

      for k,v in pairs(instance) do
        if not obj[k] then
          obj[k] = v
        end
      end

      return self
    end,
  }
end

--ss.Utils.throw(ss.Utils.Errors.ErrorInvalidArgs, "OBJECT NAME", "FUNCTION NAME",
--  {"menu", "UIDropDownMenu", menu,} -- Param 1
--)

--- Throws an error blaming the calling function.
-- @param t The error message or function.
-- @param ... Params to use with the error function. (Optional)
--
function Utils.throw(t, ...)
  local msg = (type(t) == "function") and t(...) or t
  error(msg or t, 3)
end

--- Throws an error blaming the calling function.
-- @param t The error message or function.
-- @param l The level to throw the exception.
-- @param ... Params to use with the error function. (Optional)
--
function Utils.throwAtLevel(t, l, ...)
  local msg = (type(t) == "function") and t(...) or t
  l = msg and 3 or 1

  error(msg or "and unexpected error occured.", l or 3)
end

--- Adds a callback function for Frame:OnUpdate().
-- @param i The index to store the function.
-- @param f The callback function.
--
function Utils.addOnUpdateListener(i, f)
  onUpdateListeners[i] = f
end

--- Removes a callback function for Frame:OnUpdate().
-- @param i The index to store the function.
--
function Utils.removeOnUpdateListener(i)
  onUpdateListeners[i] = nil
end

--- Provides the addon's memory usage in mb.
function Utils.getMemUsage()
  return GetAddOnMemoryUsage(AddOnName) / 1024
end

Utils.addModule("Utils", Utils)


