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

--- 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 updateTabButtonSize = function(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 k, 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 createTabButton = function(parent, fontNormal, fontSelected, pushTexture, selectedTexture)
  local button = CreateFrame("Button", nil, parent)
  
  -- Crreate the standard texture.
  button._tNorm = button:CreateTexture(nil, "BACKGROUND")
  button._tNorm:SetColorTexture(1,1,1,1)
  button._tNorm:SetAllPoints   (button)
  button._tNorm:SetVertexColor (colors.btnNorm, colors.btnNorm, colors.btnNorm, 0)
  button:SetNormalTexture(button._tNorm)
  
  -- Crreate the pushed texture.
  button._tPushed = button:CreateTexture(nil, "BACKGROUND")
  if pushTexture then button._tPushed:SetTexture(pushTexture);
    else button._tPushed:SetColorTexture(1,1,1,1); end
  
  button._tPushed:SetVertexColor (colors.btnPushed, colors.btnPushed, colors.btnPushed, 1)
  button._tPushed:SetAllPoints   (button)
  button:SetPushedTexture(button._tPushed)
  
  -- Crreate the highlighted texture.
  button._tHighlight = button:CreateTexture(nil, "BACKGROUND")
  if selectedTexture then button._tHighlight:SetTexture(selectedTexture);
    else button._tHighlight:SetColorTexture(1,1,1,1); end
  
  button._tHighlight:SetVertexColor (colors.btnSelected, colors.btnSelected, colors.btnSelected, 1)
  button._tHighlight:SetAllPoints   (button)
  button:SetHighlightTexture(button._tHighlight)
  
  -- Crreate the font string.
  button._fontStr = button:CreateFontString()
  button._fontStr:SetAllPoints(button)
  button._fontStr:SetTextColor(colors.tabText.r, colors.tabText.g, colors.tabText.b, 1)
  button:SetFontString(button._fontStr)
  
  -- Set the fonts.
  button:SetNormalFontObject   (fontNormal or "GameFontNormal")
  button:SetHighlightFontObject(fontSelected or "GameFontHighlight")
  
  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 addTabNavButton = function(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:SetScript("OnClick", function(self)
      -- Call "SetSelectedTab" on the panel. (self > tabNav > panel)
      self:GetParent():GetParent():SetSelectedTab(self._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 removeTabNavButton = function(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 setSelectedTabNavButton = function(self, index)
  self._btns[self._selectedTab or 1]:UnlockHighlight()
  self._btns[index]:LockHighlight()
  self._selectedTab = index
  
  -- Update the slider's animator target.
  self._slider:ClearAllPoints()
  self._slider._curX = select(4, self._btns[self._selectedTab or 1]:GetPoint())
  self._slider._curX = self._slider._curX + self._slider._padX
end

--- Sets the selected tab navigator button by identifier.
-- @param self The tab nav reference.
-- @param buttonIndex The button indentifier.
-- @return The selected button's identifier.
--
local selectTabNavButton = function(self, buttonIndex)
  for k, v in ipairs(self._btns) do
    if v._ind == buttonIndex then
      -- Update button states.
      self:_setSelectedButton(k)
      return buttonIndex
    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 animateVal = function(self, currVal, dimen)
  currVal = currVal or 0
  
  -- 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 selectTabNavIndex = function(self, index)
  if self._btns[index] then
    -- Update button states.
    self:_setSelectedButton(index)
    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 setHotTabNavButton = function(self, index, hot)
  if self._btns[index] then
    -- Update button states.
    local col = hot and colors.tabTextHot or colors.tabText
    self._btns[index]._fontStr: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 addTabNav = function(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:SetScript("OnUpdate", function(self)
    -- Move the index marker using the animated vanlue.
    self._slider:SetPoint("LEFT", self, "LEFT",
      self._animator:animate(self._slider._curX, self._w),
      (-tabNavHeight * 0.25))
  end)
end


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

--- Adds a new tab content container.
-- @param self The tab panel reference.
-- @param tabIndex The tab index.
--
local addTabContentContainer = function(self, tabIndex)
  local contentFrame = CreateFrame("Frame", nil, 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 updateTabContentContainerSizes = function(self)
  for k, 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 setNavWidth = function(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 setNavHeight = function(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 addTab = function(self, label, onShow)
  local tabIndex = self._tabNav:_addButton(label)
  self:_addContentContainer(tabIndex)
  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 removeTab = function(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 setVisibleTab = function(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 setSelectedTab = function(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 setSelectedIndex = function(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 getSelectedIndex = function(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 setHotTab = function(self, index, hot)
  local tabIndex = 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 createTabPanel = function(parent, name, fontNormal, fontSelected, pushTexture, selectedTexture)
  -- Increment panel counter.
  name = name.."_tab_panel_"..tostring(panelCount)
  panelCount = panelCount + 1
  
  -- Create the panel container.
  local panel = CreateFrame("Frame", name, parent)
  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:SetScript("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
