--- Sphere Resource Monitor Creator
--
local ResourceMonitor = {}
local AddOnName, nsVars = ...
local ss = nsVars.SS

local setmetatable = setmetatable
local GetTime, random = GetTime, random
local tonumber, min, max, math = tonumber, min, max, math
local UnitGetTotalAbsorbs, UnitInVehicle = UnitGetTotalAbsorbs, UnitInVehicle
local UnitHealth, UnitHealthMax, UnitPower, UnitPowerMax = UnitHealth, UnitHealthMax, UnitPower, UnitPowerMax

UnitGetTotalAbsorbs = UnitGetTotalAbsorbs or function()
  return 0
end

UnitInVehicle = UnitInVehicle or function()
  return false
end

--- Resource Emulation
--
-- Controls the target value for the emulated resource.
-- Once a value is reached, it is updated to cause a fluctuation between (5-45%) and (55-95%)
-- Ensures a short duration has elapsed before changing a target.
-----------------------------------------------------------------------------------------------
local waitDuration = 10
local waitDurationSecondary = 5

--- Callback event for the resource monitor.
-- @param currVal The current resource value.
-- @param maxVal The max resource value.
-- @param height The texture height.
-- @param text The font 1 display. (optional)
-- @param filling The fill texture being updated.
-- @param prevVal The resource value being animated. (From prevVal to 0currVal)
-- @param valText The font 2 object. (optional)
-- @param textFormat The text format to use. (optional)
-- @param passed Flag to skip processing. (internal)
-- @return The animated value and display percentage. (used to continue progress)
--
local function resourceUpdate(currVal, maxVal, height, text, filling, prevVal, valText, textFormat, passed)
  if (not tonumber(maxVal)) or (maxVal == 0) then
    maxVal = 1
  end
  if (not tonumber(currVal)) then
    currVal = 1
  end

  if passed and (currVal == prevVal) then
    -- dont process the resource if it isn't changing
    return currVal
  end

  -- If the previous value is higher than the maximum reset to max
  prevVal = (prevVal <= maxVal) and prevVal or maxVal

  -- Determine animation speed if enabled.
  local speed

  if ss.global.general.smoothAnimations == "slow" then
    speed = 15
  elseif ss.global.general.smoothAnimations == "med" then
    speed = 10
  elseif ss.global.general.smoothAnimations == "fast" then
    speed = 5
  end

  -- Update the background height.
  if speed then
    local limit = 30 / GetFramerate()
    prevVal = prevVal + min((currVal - prevVal) / speed, max(currVal - prevVal, limit))

    if ((prevVal < currVal) and (prevVal / currVal) >= 0.99) or ((prevVal > currVal) and (currVal / prevVal) >= 0.99) then
      -- Ensure animation simply completes itself once steps are small enough
      prevVal = currVal
    end
  else
    prevVal = currVal
  end

  local targetTexHeight = (prevVal / maxVal * height)
  if targetTexHeight < 1 then
    targetTexHeight = 1
  end
  local newDisplayPercentage = (prevVal / maxVal)

  if newDisplayPercentage > 1 then
    newDisplayPercentage = 1
    targetTexHeight = height
  end

  if ss.global.show.gradient then
    filling:UpdateGradient(targetTexHeight, height)
  else
    filling:SetHeight(targetTexHeight)
    filling:SetTexCoord(0, 1, math.abs(newDisplayPercentage - 1), 1)
  end

  if text or valText then
    local string = math.floor(newDisplayPercentage * 100)
    if tonumber(string) == nil then
      string = 0
    end

    if text then
      text:SetText(string)
    end

    if valText then
      string = ss.Utils:valueFormat(currVal, textFormat, false)
      valText:SetText(string)
    end
  end

  return prevVal, newDisplayPercentage
end

local _mtEmulator = {
  maxVal = 100000,
  prevCurTargetVal = 0.1,
  prevTargetVal = 0,
  prevPVal = 100000,
  prevCurTargetValSecond = 0.1,
  prevTargetValSecond = 0,
  prevPValSecond = 100000,
  mockResourceValue = function(self)
    if (self.prevPVal >= (self.maxVal * self.prevCurTargetVal)) and (self.prevCurTargetVal > 0.5) then
      if not self.waitDuration then
        self.waitDuration = GetTime() + (random(waitDuration) / 10)
      elseif self.waitDuration and self.waitDuration < GetTime() then
        -- Set a new target value below 45%.
        self.prevCurTargetVal = random(5, 45) / 100
        self.prevTargetVal = (self.maxVal * self.prevCurTargetVal) - 0.01
        self.waitDuration = nil
      end
    elseif (self.prevPVal <= (self.maxVal * self.prevCurTargetVal)) and (self.prevCurTargetVal < 0.5) then
      if not self.waitDuration then
        self.waitDuration = GetTime() + (random(waitDuration) / 10)
      elseif self.waitDuration and self.waitDuration < GetTime() then
        -- Set a new target value above 55%.
        self.prevCurTargetVal = random(55, 95) / 100
        self.prevTargetVal = (self.maxVal * self.prevCurTargetVal) + 0.01
        self.waitDuration = nil
      end
    end

    return self.prevTargetVal
  end,
  mockResourceValueSecond = function(self)
    if (self.prevPValSecond >= (self.maxVal * self.prevCurTargetValSecond)) and (self.prevCurTargetValSecond > 0.5) then
      if not self.waitDurationSecond then
        self.waitDurationSecond = GetTime() + (random(waitDurationSecondary) / 10)
      elseif self.waitDurationSecond and self.waitDurationSecond < GetTime() then
        -- Set a new target value below 45%.
        self.prevCurTargetValSecond = random(5, 45) / 100
        self.prevTargetValSecond = (self.maxVal * self.prevCurTargetValSecond) - 0.01
        self.waitDurationSecond = nil
      end
    elseif (self.prevPValSecond <= (self.maxVal * self.prevCurTargetValSecond)) and (self.prevCurTargetValSecond < 0.5) then
      if not self.waitDurationSecond then
        self.waitDurationSecond = GetTime() + (random(waitDurationSecondary) / 10)
      elseif self.waitDurationSecond and self.waitDurationSecond < GetTime() then
        -- Set a new target value above 55%.
        self.prevCurTargetValSecond = random(55, 95) / 100
        self.prevTargetValSecond = (self.maxVal * self.prevCurTargetValSecond) + 0.01
        self.waitDurationSecond = nil
      end
    end

    return self.prevTargetValSecond
  end,
}

--- Provides the "OnUpdate" event for a resources.
-- @param display The frame reference.
--
function ResourceMonitor.monitorResource(display)
  local unit = display.config.unit

  if (display.config.unit == "vehicle") and (not UnitInVehicle("player")) then
    ss.Layout:OnPlayerExitedVehicle()
  end

  local maxResourceVal = 0
  local currentVal = 0
  local newPercent

  -- Update the micro spheres.
  display:UpdateMicroSpheres()

  -- Get first value.
  if (display.configName == "health") or (display.configName == "pet") or (display.configName == "vehicle") then
    maxResourceVal = UnitHealthMax(unit)
    currentVal = UnitHealth(unit)
  else
    maxResourceVal = UnitPowerMax(display.config.unit, display.config.unitId)
    currentVal = UnitPower(display.config.unit, display.config.unitId)
  end

  if display.config.secondaryUnitId then
    -- Update the first value.
    display.config.previousValue, newPercent = resourceUpdate(
        currentVal,
        maxResourceVal,
        display.config.realHeight,
        display.font1,
        display.fillingLeft,
        display.config.previousValue,
        nil,
        nil,
        display.config.passed)

    -- Get second value.
    if (display.configName == "pet") or (display.configName == "vehicle") then
      maxResourceVal = UnitPowerMax(display.config.unit)
      currentVal = UnitPower(display.config.unit)
    else
      maxResourceVal = UnitPowerMax(display.config.unit, display.config.secondaryUnitId)
      currentVal = UnitPower(display.config.unit, display.config.secondaryUnitId)
    end

    -- Update the second value.
    display.config.previousSecondaryValue = resourceUpdate(
        currentVal,
        maxResourceVal,
        display.config.realHeight,
        display.font2,
        display.fillingRight,
        display.config.previousSecondaryValue,
        nil,
        nil,
        display.config.passed)
  else
    -- Update the first value.
    display.config.previousValue, newPercent = resourceUpdate(
        currentVal,
        maxResourceVal,
        display.config.realHeight,
        display.font1,
        display.filling,
        display.config.previousValue,
        display.font2,
        display.config.textFormat,
        display.config.passed)

    if display.config.absorbUnitId then
      -- Update the absorb value.
      currentVal = UnitGetTotalAbsorbs(unit)
      display.config.previousAbsorbValue = resourceUpdate(
          currentVal,
          maxResourceVal,
          display.config.realHeight,
          nil,
          display.fillingAbsorb,
          display.config.previousAbsorbValue,
          nil,
          display.config.textFormat,
          display.config.passed)
    end
  end

  if newPercent then
    display.swirl1:SetAlpha(newPercent)
    display.swirl2:SetAlpha(newPercent)
    display.swirl3:SetAlpha(newPercent)

    -- TODO: Prevent animation from restarting when updating duration
    --display.swirl1:SetDurationPercent(newPercent)
    --display.swirl2:SetDurationPercent(newPercent)
    --display.swirl3:SetDurationPercent(newPercent)
  end

  -- Set the flag to start skipping redundant processing.
  display.config.passed = true
end

--- Emulates resource changes.
-- @param display The frame reference.
--
function ResourceMonitor.emulateResource(display)
  if display._emu.emulate == true then
    -- DEBUG: manually manage height value with the swirl color (alpha) slider
    --      local _, _, _, debugHeight = ss.ConfigPreview.widgetFrame.xmlSphereDisplayFrame2.sphere:GetSwirlColor()

    -- Begin emulating the resource.
    local newPercent

    -- Mock resource changes.
    display._emu.prevPVal, newPercent = resourceUpdate(
        display._emu:mockResourceValue(),
    --        display._emu.maxVal * debugHeight, -- DEBUG: manually manage height value, above line MUST be commented.
        display._emu.maxVal,
        display._emu.isDual and (150 * 0.95) or (150 * 1.086),
        display.font1,
        display._emu.isDual and display.fillingLeft or display.filling,
        display._emu.prevPVal,
        display._emu.isDual and nil or display.font2,
        display._emu.isDual and nil or display.textFormat,
        false)

    if display._emu.isDual then
      display._emu.prevPValSecond = resourceUpdate(
          display._emu:mockResourceValueSecond(),
      --          display._emu.maxVal * debugHeight, -- DEBUG: manually manage height value, above line MUST be commented.
          display._emu.maxVal,
          150 * 0.95,
          display.font2,
          display.fillingRight,
          display._emu.prevPValSecond,
          nil,
          nil,
          false)
    end

    if newPercent then
      display.swirl1:SetAlpha(newPercent)
      display.swirl2:SetAlpha(newPercent)
      display.swirl3:SetAlpha(newPercent)
    end
  else
    -- Begin terminating the emulation.
    local newPercent

    -- Reset the filling height to 100%.
    display._emu.prevPVal, newPercent = resourceUpdate(
        display._emu.maxVal,
        display._emu.maxVal,
        display._emu.isDual and (150 * 0.95) or (150 * 1.086),
        display.font1,
        display._emu.isDual and display.fillingLeft or display.filling,
        display._emu.prevPVal,
        display._emu.isDual and nil or display.font2,
        display._emu.isDual and nil or display.textFormat,
        false)

    if display._emu.isDual then
      display._emu.prevPValSecond = resourceUpdate(
          display._emu.maxVal,
          display._emu.maxVal,
          150 * 0.95,
          display.font2,
          display.fillingRight,
          display._emu.prevPValSecond,
          nil,
          nil,
          false)
    end

    if newPercent then
      display.swirl1:SetAlpha(newPercent)
      display.swirl2:SetAlpha(newPercent)
      display.swirl3:SetAlpha(newPercent)
    end

    if display._emu.prevPVal >= (display._emu.maxVal * 0.99999) and (display._emu.isDual and (display._emu.prevPValSecond >= (display._emu.maxVal * 0.99999)) or true) then
      -- End the emulation and remove the emulator.
      ss.Extensions:Hook(display, "OnUpdate", nil)
      display._emu = nil
    end
  end
end

--- Provides the resource monitor event handler.
-- @return the update handler.
--
function ResourceMonitor:Watch()
  return self.monitorResource
end

--- Emulates resource changes on a display.
-- @param display The display to use.
-- @param isDual Flag for dual resources.
--
function ResourceMonitor:Emulate(display, isDual)
  if display._emu then
    -- Restart the existing component.
    display._emu.isDual = isDual
    display._emu.emulate = true
  else
    -- Create and attach a new emulator to the display.
    local _emu = {
      isDual = isDual,
      emulate = true,
    }

    setmetatable(_emu, _mtEmulator)
    _mtEmulator.__index = _mtEmulator
    display._emu = _emu

    ss.Extensions:Hook(display, "OnUpdate", self.emulateResource)
  end
end

--- Completes an ongoing emulation.
-- @param display The target display.
--
function ResourceMonitor:EmulateFinish(display)
  -- Clear the emulate property so the monitor can begin completion.
  display._emu.emulate = nil
end

ss.Utils.addClass("ResourceMonitor", ResourceMonitor)
