--- SJTabPanel Class
--
local table, type, tostring = table, type, tostring
local CreateFrame = CreateFrame
local pairs, ipairs, tonumber = pairs, ipairs, tonumber
local GetFramerate, min, max = GetFramerate, min, max


local gameVersion = GetBuildInfo()
gameVersion = string.sub(gameVersion, 1, 2)

--- Tab navigator button logic
--------------------------------

-- Local values
local tabNavHeight = 50

--- Updates the sizing and positioning of the tab buttons according to the current width. (set via panel:SetNavWidth)
-- @param self The tab nav reference.
--
local function updateTabButtonSize(self)
  self._btns = self._btns or {}

  if type(self._btns) == "table" and table.getn(self._btns) > 0 then
    -- Calculate new tab button width.
    local numButtons = table.getn(self._btns)
    local newWidth = (self._w / numButtons)
    local xPos = 0

    for _, v in ipairs(self._btns) do
      -- Update all tab buttons.
      v:SetPoint("LEFT", xPos, 0)
      v:SetWidth(newWidth)
      v:SetHeight(tabNavHeight)
      xPos = xPos + newWidth
    end

    -- Update the slider and it's animator target.
    self._slider:Show()
    self._slider:SetWidth(newWidth * 0.8)
    self._slider:SetHeight(tabNavHeight * 0.07)
    self._slider._padX = (newWidth * 0.1)
    self._slider:ClearAllPoints()
    self._slider._curX = select(4, self._btns[self._selectedTab or 1]:GetPoint())
    self._slider._curX = self._slider._curX + self._slider._padX
  else
    self._slider:Hide()
  end
end

-- Internal pointer to keep tack of button usage.
local buttonIndex = 1

local colors = {
  tabNav = 0.15,
  btnNorm = 0.15,
  btnPushed = 0.2,
  btnSelected = 0.1,
  tabSlider = 0.4,
  tabText = { r = 0.5, g = 0.5, b = 0.5 },
  tabTextHot = { r = 0.9, g = 0.4, b = 0.05 },
}

--- Creates a new tab button.
-- @param parent The parent container.
-- @param fontNormal The normal font. (Optional)
-- @param fontSelected The selected font. (Optional)
-- @param pushTexture The pushed texture. (Optional)
-- @param selectedTexture The selected texture. (Optional)
-- @return The button.
--
local function createTabButton(parent, fontNormal, fontSelected, pushTexture, selectedTexture)
  local button = CreateFrame("FlatButton", nil, parent)
  button.NormalTexture:SetVertexColor(0, 0, 0, 0)
  button.PushedTexture:SetBlendMode("ADD")

  if pushTexture then
    button.PushedTexture:SetTexture(pushTexture)
    button.PushedTexture:SetVertexColor(0.05, 0.05, 0.05, 1)
  else
    button.PushedTexture:SetVertexColor(0.25, 0.25, 0.25, 1)
  end

  if selectedTexture then
    button.HighlightTexture:SetTexture(selectedTexture)
    button.HighlightTexture:SetVertexColor(0.05, 0.05, 0.05, 1)
    button.HighlightTexture:SetBlendMode("ADD")
  end

  if fontNormal then
    button:SetNormalFontObject(fontNormal)
  end

  if fontSelected then
    button:SetHighlightFontObject(fontSelected)
  end

  return button
end

--- Adds a button to the tab navigator.
-- @param self The tab nav reference.
-- @param text The button text.
-- @return A meaningful index for the button.
--
local function addTabNavButton(self, text)
  self._btns = self._btns or {}

  if tonumber(table.getn(self._btns)) < 4 then
    local button = createTabButton(self, self._fontNormal, self._fontSelected, self._pushTexture, self._selectedTexture)
    button:SetText(text)
    button:HookScript("OnClick", function()
      -- Call "SetSelectedTab" on the panel. (self > tabNav > panel)
      button:GetParent():GetParent():SetSelectedTab(button._ind)
    end)

    -- Assign a unique identifier and resize the tab navigator buttons.
    button._ind = "tab_" .. tostring(buttonIndex)
    table.insert(self._btns, button)
    self:_updateButtonSize()
    buttonIndex = buttonIndex + 1

    if not self._selectedTab then
      -- Set default button state.
      self:_setSelectedButton(1)
    end

    return button._ind
  end
end

--- Removes a button from the tab navigator.
-- @param self The tab nav reference.
-- @param index The button index.
-- @return A meaningful index for the removed button.
--
local function removeTabNavButton(self, index)
  -- Remove button and resize remaining frames.
  if self._btns[index] then
    local removed = table.remove(self._btns, index)
    removed:Hide()
    self:_updateButtonSize()
    return removed._ind
  end
end

--- Sets the selected tab navigator button by index.
-- @param self The tab nav reference.
-- @param index The button index.
--
local function setSelectedTabNavButton(self, index)
  self._btns[self._selectedTab or 1]:UnlockHighlight()
  self._btns[index]:LockHighlight()
  self._selectedTab = index

  -- Update the slider's animator target.
  local newX = select(4, self._btns[self._selectedTab or 1]:GetPoint())

  if self._slider._curX - self._slider._padX ~= newX then
    self._slider:ClearAllPoints()
    self._slider._curX = newX
    self._slider._curX = self._slider._curX + self._slider._padX
  end
end

--- Sets the selected tab navigator button by identifier.
-- @param self The tab nav reference.
-- @param btnIndex The button identifier.
-- @return The selected button's identifier.
--
local function selectTabNavButton(self, btnIndex)
  for k, v in ipairs(self._btns) do
    if v._ind == btnIndex then
      -- Update button states.
      if k ~= self._selectedTab then
        self:_setSelectedButton(k)
      end

      return btnIndex
    end
  end
end


--- Tab nav animation logic
-----------------------------

--- Updates the value used by the animator.
-- @param self The animator reference.
-- @param currVal The current x value.
-- @param dimen The tab navifator's width.
-- @return The updated value.
--
local function animateVal(self, currVal, dimen)
  currVal = currVal or 0.01

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

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

  self.prevVal = self.prevVal < 0 and 0 or self.prevVal
  self.prevVal = self.prevVal > dimen and dimen or self.prevVal

  return self.prevVal
end


--- Tab navigator logic
-------------------------

--- Selects a tab by index.
-- @param self The tab nav reference.
-- @param index The tab button index.
-- @return The selected tab button's identifier.
--
local function selectTabNavIndex(self, index)
  if self._btns[index] then
    -- Update button states.
    if index ~= self._selectedTab then
      self:_setSelectedButton(index)
    end

    return self._btns[index]._ind
  end
end

--- Highlights a tab button.
-- @param self The tab nav reference.
-- @param index The button index.
-- @param hot The highlight color.
--
local function setHotTabNavButton(self, index, hot)
  if self._btns[index] then
    -- Update button states.
    local col = hot and colors.tabTextHot or colors.tabText
    self._btns[index].Text:SetTextColor(col.r, col.g, col.b, 1)
  end
end

-- Adds the tab navigator to the panel.
--- Creates a tab navigator.
-- @param panel The parent tab panel.
-- @param name The tab nav name.
-- @param fontNormal The normal button font. (Optional)
-- @param fontSelected The selected button font. (Optional)
-- @param pushTexture The pushed button texture. (Optional)
-- @param selectedTexture The selected button texture. (Optional)
--
local function addTabNav(panel, name, fontNormal, fontSelected, pushTexture, selectedTexture)
  -- Create the tab navigation frame.
  panel._tabNav = CreateFrame("Frame", name, panel)
  panel._tabNav:SetPoint("TOP", 0, 0)
  panel._tabNav:SetHeight(tabNavHeight)

  -- Add internal properties.
  panel._tabNav._updateButtonSize = updateTabButtonSize
  panel._tabNav._addButton = addTabNavButton
  panel._tabNav._removeButton = removeTabNavButton
  panel._tabNav._setSelectedButton = setSelectedTabNavButton
  panel._tabNav._selectByButton = selectTabNavButton
  panel._tabNav._selectByIndex = selectTabNavIndex
  panel._tabNav._setHotButton = setHotTabNavButton
  panel._tabNav._fontNormal = fontNormal
  panel._tabNav._fontSelected = fontSelected
  panel._tabNav._pushTexture = pushTexture
  panel._tabNav._selectedTexture = selectedTexture
  panel._tabNav._btns = {}

  -- Add a background texture.
  local texture = panel._tabNav:CreateTexture(nil, "BACKGROUND")
  texture:SetColorTexture(1, 1, 1, 1)
  texture:SetVertexColor(colors.tabNav, colors.tabNav, colors.tabNav, 1)
  texture:SetAllPoints(panel._tabNav)

  -- Add the texture for the selected item
  panel._tabNav._slider = panel._tabNav:CreateTexture(nil, "OVERLAY")
  panel._tabNav._slider:SetColorTexture(1, 1, 1, 1)
  panel._tabNav._slider:SetVertexColor(colors.tabSlider, colors.tabSlider, colors.tabSlider, 1)
  panel._tabNav._slider:SetAllPoints()

  -- Add the animator.
  panel._tabNav._animator = {
    prevVal = 0,
    animate = animateVal,
  }

  panel._tabNav:HookScript("OnUpdate", function(self)
    -- Move the index marker using the animated vanlue.
    if self._slider._curX ~= self._animator.prevVal then
      self._slider:SetPoint("LEFT", self, "LEFT",
          self._animator:animate(self._slider._curX, self._w),
          (-tabNavHeight * 0.25))
    end
  end)
end


--- Tab content container logic
---------------------------------

--- Adds a new tab content container.
-- @param self The tab panel reference.
-- @param tabIndex The tab index.
--
local function addTabContentContainer(self, tabIndex, name)
  local contentFrame = CreateFrame("Frame", name, self)
  contentFrame:SetPoint("TOP", 0, -tabNavHeight)
  contentFrame:SetWidth(self._tabNav._w)
  contentFrame:SetHeight(self._tabNav._h - tabNavHeight)

  self._tabs[tabIndex] = contentFrame
end

--- Updates the content container sizes.
-- @param self The tab panel reference.
--
local function updateTabContentContainerSizes(self)
  for _, v in pairs(self._tabs) do
    v:SetPoint("TOP", 0, -tabNavHeight)
    v:SetWidth(self._tabNav._w or 600)
    v:SetHeight((self._tabNav._h or 600) - tabNavHeight)
  end
end


--- Tab panel logic
---------------------

-- Internal pointer to keep tack of panel usage.
local panelCount = 1

--- Sets the tab navigator width.
-- @param self The tab panel reference.
-- @param width The new width.
--
local function setNavWidth(self, width)
  if width then
    self._tabNav._w = width
    self._tabNav:SetPoint("TOP", 0, 0)
    self._tabNav:SetWidth(width)
    self._tabNav:_updateButtonSize()
    self:_updateContainerSizes()
  end
end

--- Sets the tab navigator height.
-- @param self The tab panel reference.
-- @param height The new height.
--
local function setNavHeight(self, height)
  if height then
    self._tabNav._h = height
    self._tabNav:_updateButtonSize()
    self:_updateContainerSizes()
  end
end

--- Adds a new tab to the tab panel.
-- @param self The tab panel reference.
-- @param label The tab's text.
-- @param onShow A callback for when the tab is displayed. (Optional)
-- @return The created tab.
--
local function addTab(self, name, label, onShow)
  local tabIndex = self._tabNav:_addButton(label)
  self:_addContentContainer(tabIndex, name)
  self._numTabs = self._numTabs + 1

  if not self._selectedTab then
    self._selectedTab = tabIndex
    self._tabs[self._selectedTab]:Show()
  else
    self._tabs[tabIndex]:Hide()
  end

  self._tabs[tabIndex]._onShow = onShow

  return self._tabs[tabIndex]
end

--- Removes a tab from the tab panel.
-- @param self The tab panel reference.
-- @param index The index of the tab.
--
local function removeTab(self, index)
  if (index > 0) and (index <= self._numTabs) and (self._numTabs > 1) then
    local tabIndex = self._tabNav:_removeButton(index)

    if tabIndex == self._selectedTab then
      -- Set tab to first index since button has already been removed.
      self:SetSelectedIndex(1)
    end

    if tabIndex and self._tabs[tabIndex] then
      -- Hide and forget the tab.
      self._tabs[tabIndex]:Hide()
      self._tabs[tabIndex] = nil
      self._numTabs = self._numTabs - 1
    end
  end
end

--- Updates the tab visibilities.
-- @param self The tab panel reference.
-- @param tabIndex The tab index to show.
--
local function setVisibleTab(self, tabIndex)
  if tabIndex and self._tabs[tabIndex] then
    self._tabs[self._selectedTab]:Hide()
    self._selectedTab = tabIndex
    self._tabs[self._selectedTab]:Show()

    if self._tabs[self._selectedTab]._onShow then
      self._tabs[self._selectedTab]._onShow()
    end
  end
end

--- Sets the selected tab by identifier.
-- @param self The tab panel reference.
-- @param tabIndex The tab identifier.
--
local function setSelectedTab(self, tabIndex)
  tabIndex = self._tabNav:_selectByButton(tabIndex)
  setVisibleTab(self, tabIndex)
end

--- Sets the selected tab by index.
-- @param self The tab panel reference.
-- @param index The tab index.
--
local function setSelectedIndex(self, index)
  local tabIndex = self._tabNav:_selectByIndex(index)
  setVisibleTab(self, tabIndex)
end

--- Returns the selected tab imdex.
-- @param self The tab panel reference.
-- @return The selected index.
--
local function getSelectedIndex(self)
  return self._tabNav._selectedTab
end

--- Highlights a tab.
-- @param self The tab panel reference.
-- @param index The tab index.
-- @param hot The highlight color.
--
local function setHotTab(self, index, hot)
  self._tabNav:_setHotButton(index, hot)
end

--- Creates a new tab panel.
-- @param parent The parent container.
-- @param name The tab panel name.
-- @param fontNormal The normal font. (Optional)
-- @param fontSelected The selected font. (Optional)
-- @param pushTexture The pushed texture. (Optional)
-- @param selectedTexture The selected texture. (Optional)
-- @return The created tab panel.
--
local function createTabPanel(parent, name, fontNormal, fontSelected, pushTexture, selectedTexture)
  -- Increment panel counter.
  name = name .. "_tab_panel_" .. tostring(panelCount)
  panelCount = panelCount + 1

  -- Create the panel container.
  local panel

  if tonumber(gameVersion) >= 2 then
    panel = CreateFrame("Frame", name, parent)
  else
    panel = CreateFrame("Button", name, parent)
  end

  panel:SetAllPoints(parent)
  panel._name = name
  panel._numTabs = 0
  panel._tabs = {}

  -- Add a background to the panel.
  local texture = panel:CreateTexture(nil, "BACKGROUND")
  texture:SetColorTexture(1, 1, 1, 1)
  texture:SetVertexColor(0.1, 0.1, 0.1, 1)
  texture:SetAllPoints(panel)

  -- Declare internal functions.
  panel._addContentContainer = addTabContentContainer
  panel._updateContainerSizes = updateTabContentContainerSizes

  -- Create the tab navigation frame.
  addTabNav(panel, name .. "_tab_nav", fontNormal, fontSelected, pushTexture, selectedTexture)

  -- Add public functions.
  panel.SetNavWidth = setNavWidth
  panel.SetNavHeight = setNavHeight
  panel.AddTab = addTab
  panel.RemoveTab = removeTab
  panel.SetSelectedTab = setSelectedTab
  panel.SetSelectedIndex = setSelectedIndex
  panel.GetSelectedIndex = getSelectedIndex
  panel.SetHotTab = setHotTab

  function panel:IsVisible()
    -- Return parent visibility
    return parent:IsVisible()
  end

  panel:HookScript("OnSizeChanged", function(self)
    --  Adjust component dimensions.
    self:SetNavWidth(self:GetWidth())
    self:SetNavHeight(self:GetHeight())
  end)

  return panel
end


--- Class provider logic
--------------------------

local _SJTabPanel = {
  _v = "1.0",
}

--- Creates a new instance of SJTabPanel.
-- @param parent The parent container.
-- @param name The tab panel name.
-- @param fontNormal The normal font. (Optional)
-- @param fontSelected The selected font. (Optional)
-- @param pushTexture The pushed texture. (Optional)
-- @param selectedTexture The selected texture. (Optional)
-- @return The created tab panel.
--
function _SJTabPanel:New(parent, name, fontNormal, fontSelected, pushTexture, selectedTexture)
  -- Instantiate a new tab panel.
  local panel = createTabPanel(parent, name, fontNormal, fontSelected, pushTexture, selectedTexture)

  -- Set size if given otherwise 600x600 default.
  panel:SetNavWidth(600)
  panel:SetNavHeight(600)

  -- Create secure instance.
  return panel
end

--- Creates a new instance of SJTabPanel as an "InterfaceOptions".
-- @param parent The parent container.
-- @param name The tab panel name.
-- @param refresh The interface option refresh function.
-- @param fontNormal The normal font. (Optional)
-- @param fontSelected The selected font. (Optional)
-- @param pushTexture The pushed texture. (Optional)
-- @param selectedTexture The selected texture. (Optional)
-- @return The created tab panel.
-- @see InterfaceOptions_AddCategory().
--
function _SJTabPanel:NewInterfaceOption(name, parent, refresh, fontNormal, fontSelected, pushTexture, selectedTexture)
  -- Create the interface option frame.
  local tabPanel = CreateFrame("Frame", name .. "_tabPanel")
  tabPanel.parent = parent and parent.name or nil
  tabPanel.name = name
  tabPanel.refresh = refresh
  InterfaceOptions_AddCategory(tabPanel)

  -- Create the tab panel.
  tabPanel.tabNav = self:New(tabPanel, name, fontNormal, fontSelected, pushTexture, selectedTexture)
  tabPanel:Hide()

  return tabPanel.tabNav
end

if not SJTabPanel or (SJTabPanel._v < _SJTabPanel._v) then
  -- Use the latest version.
  rawset(_G, "SJTabPanel", _SJTabPanel)
end
