-- @docclass
UIMoveableTabBar = extends(UIWidget, 'UIMoveableTabBar')

-- private functions
local function onTabClick(tab)
    tab.tabBar:selectTab(tab)
end

local function updateMargins(tabBar)
    if #tabBar.tabs == 0 then
        return
    end

    local currentMargin = 0
    for i = 1, #tabBar.tabs do
        tabBar.tabs[i]:setMarginLeft(currentMargin)
        currentMargin = currentMargin + tabBar.tabSpacing + tabBar.tabs[i]:getWidth()
    end
end

local function updateNavigation(tabBar)
    if tabBar.prevNavigation then
        if tabBar.prevNavigation.dynamicNavigation ~= nil and tabBar.prevNavigation.dynamicNavigation > 0 then
            local fitsOnBar = math.floor(tabBar:getWidth() / tabBar.prevNavigation.dynamicNavigation)
            if #tabBar.tabs >= fitsOnBar and (#tabBar.preTabs > 0 or table.find(tabBar.tabs, tabBar.currentTab) ~= 1) then
                tabBar.prevNavigation:enable()
            else
                tabBar.prevNavigation:disable()
            end
        else
            if #tabBar.preTabs > 0 or table.find(tabBar.tabs, tabBar.currentTab) ~= 1 then
                tabBar.prevNavigation:enable()
            else
                tabBar.prevNavigation:disable()
            end
        end
    end

    if tabBar.nextNavigation then
        if tabBar.nextNavigation.dynamicNavigation ~= nil and tabBar.nextNavigation.dynamicNavigation > 0 then
            local fitsOnBar = math.floor(tabBar:getWidth() / tabBar.nextNavigation.dynamicNavigation)
            if #tabBar.tabs >= fitsOnBar and
                (#tabBar.postTabs > 0 or table.find(tabBar.tabs, tabBar.currentTab) ~= #tabBar.tabs) then
                tabBar.nextNavigation:enable()
            else
                tabBar.nextNavigation:disable()
            end
        else
            if #tabBar.postTabs > 0 or table.find(tabBar.tabs, tabBar.currentTab) ~= #tabBar.tabs then
                tabBar.nextNavigation:enable()
            else
                tabBar.nextNavigation:disable()
            end
        end
    end
end

local function updateIndexes(tabBar, tab, xoff)
    local tabs = tabBar.tabs
    local currentMargin = 0
    local prevIndex = table.find(tabs, tab)
    local newIndex = prevIndex
    local xmid = xoff + tab:getWidth() / 2
    for i = 1, #tabs do
        local nextTab = tabs[i]
        if xmid >= currentMargin + nextTab:getWidth() / 2 then
            newIndex = table.find(tabs, nextTab)
        end
        currentMargin = currentMargin + tabBar.tabSpacing * (i - 1) + tabBar.tabs[i]:getWidth()
    end
    if newIndex ~= prevIndex then
        table.remove(tabs, table.find(tabs, tab))
        table.insert(tabs, newIndex, tab)
    end
    updateNavigation(tabBar)
end

local function getMaxMargin(tabBar, tab)
    if #tabBar.tabs == 0 then
        return 0
    end

    local maxMargin = 0
    for i = 1, #tabBar.tabs do
        if tabBar.tabs[i] ~= tab then
            maxMargin = maxMargin + tabBar.tabs[i]:getWidth()
        end
    end
    return maxMargin + tabBar.tabSpacing * (#tabBar.tabs - 1)
end

local function updateTabs(tabBar)
    if #tabBar.postTabs > 0 then
        local i = 1
        while i <= #tabBar.postTabs do
            local tab = tabBar.postTabs[i]
            if getMaxMargin(tabBar) + tab:getWidth() > tabBar:getWidth() then
                break
            end

            table.remove(tabBar.postTabs, i)
            table.insert(tabBar.tabs, tab)
            tab:setVisible(true)
        end
    end
    if #tabBar.preTabs > 0 then
        for i = #tabBar.preTabs, 1, -1 do
            local tab = tabBar.preTabs[i]
            if getMaxMargin(tabBar) + tab:getWidth() > tabBar:getWidth() then
                break
            end

            table.remove(tabBar.preTabs, i)
            table.insert(tabBar.tabs, 1, tab)
            tab:setVisible(true)
        end
    end
    updateNavigation(tabBar)
    updateMargins(tabBar)

    if not tabBar.currentTab and #tabBar.tabs > 0 then
        tabBar:selectTab(tabBar.tabs[1])
    end
end

local function hideTabs(tabBar, fromBack, toArray, width)
    while #tabBar.tabs > 0 and getMaxMargin(tabBar) + width > tabBar:getWidth() do
        local index = fromBack and #tabBar.tabs or 1
        local tab = tabBar.tabs[index]
        table.remove(tabBar.tabs, index)
        if fromBack then
            table.insert(toArray, 1, tab)
        else
            table.insert(toArray, tab)
        end
        if tabBar.currentTab == tab then
            if #tabBar.tabs > 0 then
                tabBar:selectTab(tabBar.tabs[#tabBar.tabs])
            else
                tabBar.currentTab:setChecked(false)
                tabBar.currentTab = nil
            end
        end
        tab:setVisible(false)
    end
end

local function showPreTab(tabBar)
    if #tabBar.preTabs == 0 then
        return nil
    end

    local tmpTab = tabBar.preTabs[#tabBar.preTabs]
    hideTabs(tabBar, true, tabBar.postTabs, tmpTab:getWidth())

    table.remove(tabBar.preTabs, #tabBar.preTabs)
    table.insert(tabBar.tabs, 1, tmpTab)
    tmpTab:setVisible(true)
    return tmpTab
end

local function showPostTab(tabBar)
    if #tabBar.postTabs == 0 then
        return nil
    end

    local tmpTab = tabBar.postTabs[1]
    hideTabs(tabBar, false, tabBar.preTabs, tmpTab:getWidth())

    table.remove(tabBar.postTabs, 1)
    table.insert(tabBar.tabs, tmpTab)
    tmpTab:setVisible(true)
    return tmpTab
end

local function onTabMousePress(tab, mousePos, mouseButton)
    if mouseButton == MouseRightButton then
        if tab.menuCallback then
            tab.menuCallback(tab, mousePos, mouseButton)
        end
        return true
    end
end

local function onTabDragEnter(tab, mousePos)
    tab:raise()
    tab.hotSpot = mousePos.x - tab:getMarginLeft()
    tab.tabBar.selected = tab
    return true
end

local function onTabDragLeave(tab)
    updateMargins(tab.tabBar)
    tab.tabBar.selected = nil
    return true
end

local function onTabDragMove(tab, mousePos, mouseMoved)
    if tab == tab.tabBar.selected then
        local xoff = mousePos.x - tab.hotSpot

        -- update indexes
        updateIndexes(tab.tabBar, tab, xoff)
        updateIndexes(tab.tabBar, tab, xoff)

        -- update margins
        updateMargins(tab.tabBar)
        xoff = math.max(xoff, 0)
        local maxMargin = getMaxMargin(tab.tabBar, tab)
        local parentLimit = tab.tabBar:getParent():getWidth() - tab:getWidth()
        if parentLimit > maxMargin then
            maxMargin = parentLimit
        end
        if tab.tabBar.dropTarget then
            local dt = tab.tabBar.dropTarget
            local hovered = dt:containsPoint(mousePos)
            if hovered and not tab.tabBar.dropTargetHighlighted then
                dt:setBorderColor('#FFFFFF')
                dt:setBorderWidth(1)
                tab.tabBar.dropTargetHighlighted = true
            elseif not hovered and tab.tabBar.dropTargetHighlighted then
                dt:setBorderColor('alpha')
                dt:setBorderWidth(0)
                tab.tabBar.dropTargetHighlighted = false
            end
            local dtLimit = dt:getX() - tab.tabBar:getX() + dt:getWidth()
            if dtLimit > maxMargin then
                maxMargin = dtLimit
            end
        end
        xoff = math.min(xoff, maxMargin)
        tab:setMarginLeft(xoff)
    end
end

local function tabBlink(tab, step)
    local step = step or 0
    tab:setOn(not tab:isOn())

    removeEvent(tab.blinkEvent)
    if step < 4 then
        tab.blinkEvent = scheduleEvent(function()
            tabBlink(tab, step + 1)
        end, 500)
    else
        tab:setOn(true)
        tab.blinkEvent = nil
    end
end

-- public functions
function UIMoveableTabBar.create()
    local tabbar = UIMoveableTabBar.internalCreate()
    tabbar:setFocusable(false)
    tabbar.tabs = {}
    tabbar.selected = nil -- dragged tab
    tabbar.tabSpacing = 0
    tabbar.tabsMoveable = false
    tabbar.preTabs = {}
    tabbar.postTabs = {}
    tabbar.prevNavigation = nil
    tabbar.nextNavigation = nil
    tabbar.dropTarget = nil
    tabbar.dropCallback = nil
    tabbar.dropTargetHighlighted = false
    tabbar.onGeometryChange = function()
        hideTabs(tabbar, true, tabbar.postTabs, 0)
        updateTabs(tabbar)
    end
    return tabbar
end

function UIMoveableTabBar:onDestroy()
    if self.prevNavigation then
        self.prevNavigation:disable()
    end

    if self.nextNavigation then
        self.nextNavigation:disable()
    end
    if self.dropTarget then
        self.dropTarget.onDrop = nil
    end
    self.nextNavigation = nil
    self.prevNavigation = nil
    self.dropTarget = nil
    self.dropCallback = nil
    self.dropTargetHighlighted = false
end

function UIMoveableTabBar:setContentWidget(widget)
    self.contentWidget = widget
    if #self.tabs > 0 then
        self.contentWidget:addChild(self.tabs[1].tabPanel)
    end
end

function UIMoveableTabBar:setTabSpacing(tabSpacing)
    self.tabSpacing = tabSpacing
    updateMargins(self)
end

function UIMoveableTabBar:addTab(text, panel, menuCallback)
    if panel == nil then
        panel = g_ui.createWidget(self:getStyleName() .. 'Panel')
        panel:setId('tabPanel')
    end

    local tab = g_ui.createWidget(self:getStyleName() .. 'Button', self)
    panel.isTab = true
    tab.tabPanel = panel
    tab.tabBar = self
    tab:setId('tab')
    tab:setDraggable(self.tabsMoveable)
    tab:setText(text)
    if not tab.ignoreTextResize then
        tab:setWidth(tab:getTextSize().width + tab:getPaddingLeft() + tab:getPaddingRight())
    end
    tab.menuCallback = menuCallback or nil
    tab.onClick = onTabClick
    tab.onMousePress = onTabMousePress
    tab.onDragEnter = onTabDragEnter
    tab.onDragLeave = onTabDragLeave
    tab.onDragMove = onTabDragMove
    tab.onDestroy = function()
        tab.tabPanel:destroy()
    end

    if #self.tabs == 0 then
        self:selectTab(tab)
        tab:setMarginLeft(0)
        table.insert(self.tabs, tab)
    else
        local newMargin = self.tabSpacing * #self.tabs
        for i = 1, #self.tabs do
            newMargin = newMargin + self.tabs[i]:getWidth()
        end
        tab:setMarginLeft(newMargin)

        hideTabs(self, true, self.postTabs, tab:getWidth())
        table.insert(self.tabs, tab)
        if #self.tabs == 1 then
            self:selectTab(tab)
        end
        updateMargins(self)
    end

    updateNavigation(self)
    return tab
end

-- Additional function to move the tab by lua
function UIMoveableTabBar:moveTab(tab, units)
    local index = table.find(self.tabs, tab)
    if index == nil then
        return
    end

    local focus = false
    if self.currentTab == tab then
        self:selectPrevTab()
        focus = true
    end

    table.remove(self.tabs, index)

    local newIndex = math.min(#self.tabs + 1, math.max(index + units, 1))
    table.insert(self.tabs, newIndex, tab)
    if focus then
        self:selectTab(tab)
    end
    updateMargins(self)
    return newIndex
end

function UIMoveableTabBar:onStyleApply(styleName, styleNode)
    if styleNode['movable'] then
        self.tabsMoveable = styleNode['movable']
    end
    if styleNode['tab-spacing'] then
        self:setTabSpacing(styleNode['tab-spacing'])
    end
end

function UIMoveableTabBar:clearTabs()
    while #self.tabs > 0 do
        self:removeTab(self.tabs[#self.tabs])
    end
end

function UIMoveableTabBar:removeTab(tab)
    local tabTables = {self.tabs, self.preTabs, self.postTabs}
    local index = nil
    local tabTable = nil
    for i = 1, #tabTables do
        index = table.find(tabTables[i], tab)
        if index ~= nil then
            tabTable = tabTables[i]
            break
        end
    end

    if tabTable == nil then
        return
    end

    if self.currentTab == tab then
        if #self.tabs == 1 then
            self.currentTab = nil
        elseif index == #self.tabs then
            self:selectPrevTab()
        else
            self:selectNextTab()
        end
    end
    table.remove(tabTable, index)
    if tab.blinkEvent then
        removeEvent(tab.blinkEvent)
    end
    tab:destroy()
    updateTabs(self)
end

function UIMoveableTabBar:getTab(text)
    for k, tab in pairs(self.tabs) do
        if tab:getText():lower() == text:lower() then
            return tab
        end
    end
    for k, tab in pairs(self.preTabs) do
        if tab:getText():lower() == text:lower() then
            return tab
        end
    end
    for k, tab in pairs(self.postTabs) do
        if tab:getText():lower() == text:lower() then
            return tab
        end
    end
end

function UIMoveableTabBar:selectTab(tab)
    if self.currentTab == tab then
        return
    end
    if self.contentWidget then
        local selectedWidget = self.contentWidget:getLastChild()
        if selectedWidget and selectedWidget.isTab then
            self.contentWidget:removeChild(selectedWidget)
        end
        self.contentWidget:addChild(tab.tabPanel)
        tab.tabPanel:fill('parent')
    end

    if self.currentTab then
        self.currentTab:setChecked(false)
    end
    signalcall(self.onTabChange, self, tab)
    self.currentTab = tab
    tab:setChecked(true)
    tab:setOn(false)
    tab.blinking = false

    if tab.blinkEvent then
        removeEvent(tab.blinkEvent)
        tab.blinkEvent = nil
    end

    local parent = tab:getParent()
    parent:focusChild(tab, MouseFocusReason)
    updateNavigation(self)
end

function UIMoveableTabBar:selectNextTab()
    if self.currentTab == nil then
        return
    end

    local index = table.find(self.tabs, self.currentTab)
    if index == nil then
        return
    end

    local newIndex = index + 1
    if newIndex > #self.tabs then
        if #self.postTabs > 0 then
            local widget = showPostTab(self)
            self:selectTab(widget)
        else
            if #self.preTabs > 0 then
                for i = 1, #self.preTabs do
                    showPreTab(self)
                end
            end

            self:selectTab(self.tabs[1])
        end
        updateTabs(self)
        return
    end

    local nextTab = self.tabs[newIndex]
    if not nextTab then
        return
    end

    self:selectTab(nextTab)
end

function UIMoveableTabBar:selectPrevTab()
    if self.currentTab == nil then
        return
    end

    local index = table.find(self.tabs, self.currentTab)
    if index == nil then
        return
    end

    local newIndex = index - 1
    if newIndex <= 0 then
        if #self.preTabs > 0 then
            local widget = showPreTab(self)
            self:selectTab(widget)
        else
            if #self.postTabs > 0 then
                for i = 1, #self.postTabs do
                    showPostTab(self)
                end
            end

            self:selectTab(self.tabs[#self.tabs])
        end
        updateTabs(self)
        return
    end

    local prevTab = self.tabs[newIndex]
    if not prevTab then
        return
    end

    self:selectTab(prevTab)
end

function UIMoveableTabBar:blinkTab(tab)
    if tab:isChecked() then
        return
    end
    tab.blinking = true
    tabBlink(tab)
end

function UIMoveableTabBar:getTabPanel(tab)
    return tab.tabPanel
end

function UIMoveableTabBar:getCurrentTabPanel()
    if self.currentTab then
        return self.currentTab.tabPanel
    end
end

function UIMoveableTabBar:getCurrentTab()
    return self.currentTab
end

function UIMoveableTabBar:setNavigation(prevButton, nextButton)
    self.prevNavigation = prevButton
    self.nextNavigation = nextButton

    if self.prevNavigation then
        self.prevNavigation.onClick = function()
            self:selectPrevTab()
        end
    end
    if self.nextNavigation then
        self.nextNavigation.onClick = function()
            self:selectNextTab()
        end
    end
    updateNavigation(self)
end

function UIMoveableTabBar:setDropTarget(widget, callback)
    if self.dropTarget and self.dropTarget ~= widget then
        self.dropTarget.onDrop = nil
        if self.dropTargetHighlighted then
            self.dropTarget:setBorderColor('alpha')
            self.dropTarget:setBorderWidth(0)
            self.dropTargetHighlighted = false
        end
    end
    self.dropTarget = widget
    self.dropCallback = callback
    if widget then
        widget.onDrop = function(target, draggedWidget, mousePos)
            if draggedWidget and draggedWidget.tabBar == self then
                if callback then
                    callback(self, draggedWidget)
                end
                signalcall(self.onTabDrop, self, draggedWidget)
                widget:setBorderColor('alpha')
                widget:setBorderWidth(0)
                self.dropTargetHighlighted = false
                return true
            end
        end
    end
end
