local statsBarTop
local statsBarBottom

local statsBars = {}
local statsBarDeepInfo = {}

-- If you want to add more placements/dimensions, you'll need to add them here.
-- This is used in getStatsBarMenuOptions(), createStatsBarWidgets(), 
--                         hideAll() and destroyAllIcons() functions.
local statsBarsPlacements = {
    "Top",
    "Bottom"
}

-- This is used in constructStatsBar(), getStatsBarMenuOptions(), 
--                   reloadCurrentTab(), createStatsBarWidgets(), 
--                       hideAll() and destroyALlIcons functions.
local statsBarsDimensions = {
    Large = {
        height = 35
    },
    Default = {
        height = 35
    },
    Parallel = {
        height = 35
    },
    Compact = {
        height = 20
    }
}

local firstCall = true

local currentStats = {
    dimension = "hide",
    placement = "hide"
}

local skillsLineHeight = 20
local skillsTuples = {
    {skill = nil,               key = 'experience', icon = '/images/icons/icon_experience',  placement = 'center',   order = 0,  name = "Level"},
    {skill = nil,               key = 'magic',      icon = '/images/icons/icon_magic',       placement = 'center',   order = 1,  name = "Magic Level"},
    {skill = Skill.OneHanded,   key = 'onehanded',  icon = '/images/icons/icon_sword',       placement = 'left',     order = 1,  name = "One-Handed"},
    {skill = Skill.TwoHanded,   key = 'twohanded',  icon = '/images/icons/icon_axe',         placement = 'right',    order = 1,  name = "Two-Handed"},
    {skill = Skill.Distance,    key = 'distance',   icon = '/images/icons/icon_distance',    placement = 'left',     order = 2,  name = "Distance Fighting Skill"},
    {skill = Skill.Shielding,   key = 'shielding',  icon = '/images/icons/icon_shielding',   placement = 'right',    order = 2,  name = "Shielding Skill"},
    {skill = Skill.Fist,        key = 'fist',       icon = '/images/icons/icon_fist',        placement = 'left',     order = 3,  name = "Fist Fighting Skill"},
    {skill = Skill.Fishing,     key = 'fishing',    icon = '/images/icons/icon_fishing',     placement = 'right',    order = 3,  name = "Fishing Skill"},
}

StatsBar = {}

function getConfigurations()
    -- This method will return all the stats bar configurations.
    local configs = {}
    for _, statsBar in pairs(statsBars) do
        for _, placement in ipairs(statsBarsPlacements) do
            for dimension, _ in pairs(statsBarsDimensions) do
                local dimensionOnPlacement = tostring(dimension):lower() .. "On" .. placement
                local key = "statsBar" .. placement:gsub("^%l", string.upper)
                if statsBar[key] then
                    table.insert(configs, statsBar[key][dimensionOnPlacement])
                end
            end
        end
    end
    return configs
end

local function reloadSkillsTab(skills, parent)
    -- This method might need some refactoring if you want to add side stats bars.
    local player = g_game.getLocalPlayer()
    if not player then
        return
    end

    local tuples = {}
    for i = 1, #skillsTuples do
        local skillTuple = skillsTuples[i]
        if skillTuple and g_settings.getBoolean('top_statsbar_' .. skillTuple.key) then
            table.insert(tuples, skillTuple)
        end
    end

    local statsBar = StatsBar.getCurrentStatsBar()
    if not statsBar then
        return
    end

    statsBar:setHeight(statsBar:getHeight() - skills:getHeight())

    parent:setHeight(parent:getHeight() - (40 + skills:getHeight()))
    skills:setHeight(0)
    skills:destroyChildren()
    local lines = 0
    local lastPlacement = 'left'
    for i = 1, #tuples do
        local skillTuple = tuples[i]
        local widget = g_ui.createWidget('TopStatsSkillElement', skills)
        widget:setId('statsbar_skill_' .. skillTuple.key)
        widget:addAnchor(AnchorTop, 'parent', AnchorTop)
        if lastPlacement == 'left' then
            widget:setMarginTop(lines * skillsLineHeight)
        else
            widget:setMarginTop((lines - 1) * skillsLineHeight)
        end
        widget.level = widget:getChildById('level')
        widget.icon = widget:getChildById('icon')
        widget.bar = widget:getChildById('bar')

        widget.icon:setImageSource(skillTuple.icon)
        widget.icon:setTooltip(skillTuple.name)

        widget.bar.statsGrade = 4
        widget.bar.statsGradeColor = '#070707ff'
        widget.bar:reloadBorder()

        widget.bar.showText = false
        if skillTuple.key == 'experience' then
            widget.bar.statsType = 'experience'
        else
            widget.bar.statsType = 'skill'
        end

        if skillTuple.placement == 'center' or (i == #tuples and lastPlacement == 'left') then
            widget:addAnchor(AnchorLeft, 'parent', AnchorLeft)
            widget:addAnchor(AnchorRight, 'parent', AnchorRight)
            lines = lines + 1
        elseif lastPlacement == 'left' then
            widget:addAnchor(AnchorLeft, 'parent', AnchorLeft)
            widget:addAnchor(AnchorRight, 'parent', AnchorHorizontalCenter)
            lines = lines + 1
            lastPlacement = 'right'
        elseif lastPlacement == 'right' then
            widget:addAnchor(AnchorRight, 'parent', AnchorRight)
            widget:addAnchor(AnchorLeft, 'parent', AnchorHorizontalCenter)
            lastPlacement = 'left'
        end

        if skillTuple.key == 'experience' then
            widget.level:setText(player:getLevel())
            widget.bar:setValue(player:getLevelPercent(), 100)
        elseif skillTuple.key == 'magic' then
            widget.level:setText(player:getMagicLevel())
            widget.bar:setValue(player:getMagicLevelPercent(), 100)
        else
            widget.level:setText(player:getSkillLevel(skillTuple.skill))
            widget.bar:setValue(player:getSkillLevelPercent(skillTuple.skill), 100)
        end

    end

    skills:setHeight((lines * skillsLineHeight) + 5)
    parent:setHeight(40 + skills:getHeight())
    statsBar:setHeight(statsBar:getHeight() + skills:getHeight())
end

function StatsBar.getAllStatsBarWithPosition()
    -- This method will return all the stats bars based on their placement and dimension.
    -- i.e statsBarTop.largeOnTop, statsBarTop.parallelOnTop, statsBarTop.defaultOnTop, statsBarTop.compactOnTop [..]
    local statsBarsWithPosition = {}
    for _, statsBar in pairs(statsBars) do
        for _, placement in ipairs(statsBarsPlacements) do
            for dimension, _ in pairs(statsBarsDimensions) do
                local dimensionOnPlacement = tostring(dimension):lower() .. "On" .. placement
                if statsBar[dimensionOnPlacement] then
                    statsBarsWithPosition[#statsBarsWithPosition+1] = statsBar[dimensionOnPlacement]
                end
            end
        end
    end

    return statsBarsWithPosition
end

function StatsBar.getCurrentStatsBarWithPosition()
    -- This method will return the statsbar based on its placement and dimension.
    -- i.e "largeOnTop" will return statsBarTop.largeOnTop.
    -- It's made this way so we can call the current stats bar without having to use a switch statement.
    -- And if it's necessary to add more stats bars like "largeOnLeft" it will be easier to add them
    -- Without changing this code.
    if currentStats.dimension == 'hide' or currentStats.placement == 'hide' then
        return nil
    end
    -- -- Get full position as a single string. 
    -- -- i.e. largeOnTop // parallelOnBottom
    local placement = currentStats.placement:gsub("^%l", string.upper)
    local fullPosition = currentStats.dimension .. "On" .. placement
    local statsBar = StatsBar.getCurrentStatsBar()
    if not statsBar then
        return nil
    end

    if statsBar[fullPosition] then
    -- Return the stats bar based on the full position.
    -- i.e. statsBarTop.largeOnTop
        return statsBar[fullPosition]
    else
        print("No stats bar with position found for:", statsBar)
    end

    return nil
end

function StatsBar.getCurrentStatsBar()
    -- This method will return the statsbar based on its placement.
    -- i.e statsBarTop // statsBarBottom
    if currentStats.dimension == 'hide' or currentStats.placement == 'hide' then
        return nil
    end
    -- -- Get full placement. 
    -- -- i.e. Top // Bottom
    local placement = currentStats.placement:gsub("^%l", string.upper)

    -- -- Get the stats bar based on the placement.
    -- -- i.e. statsBarTop // statsBarBottom
    local statsBar = "statsBar" .. placement

    if statsBars[statsBar] then
        return statsBars[statsBar]
    else
        print("No stats bar found for:", statsBar)
    end

    return nil
end

function StatsBar.reloadCurrentStatsBarQuickInfo()
    local player = g_game.getLocalPlayer()
    if not player then
        return
    end

    local bar = StatsBar.getCurrentStatsBarWithPosition()
    if not bar then
        return
    end

    bar.health:setValue(player:getHealth(), player:getMaxHealth())
    bar.mana:setValue(player:getMana(), player:getMaxMana())
end

local function loadIcon(bitChanged, content, topmenu)
    local icon = g_ui.createWidget('ConditionWidget', content)
    icon:setId(Icons[bitChanged].id)
    icon:setImageSource("/images/game/states/player-state-flags")
    icon:setImageClip(((Icons[bitChanged].clip - 1) * 9) .. ' 0 9 9')
    local tooltip = Icons[bitChanged].tooltip
    if tooltip == "You are GoshnarTaint" then
        tooltip = "Goshnar's Lairs Penalties:\n" ..
                  "- 10% chance of creature teleportation to you\n" ..
                  "- 0.5% chance of new creature spawn when hitting another\n" ..
                  "- 15% increased damage received\n" ..
                  "- 10% chance of creature full heal instead of dying\n" ..
                  "- Lose 10% of current HP and mana every 10 seconds"
    end
    icon:setTooltip(tooltip)
    icon:setImageSize(tosize("9 9"))
    icon:setMarginRight(-1)
    if topmenu then
        icon:setMarginTop(5)
        icon:setMarginLeft(2)
        icon:setMarginRight(-2)
    end
    return icon
end

local function getStatsBarsIconContent()
    local iconContents = {}
    local statsBars = StatsBar.getAllStatsBarWithPosition()

    for _, statsBar in ipairs(statsBars) do
        iconContents[#iconContents+1] = { content = statsBar.icons, loadIconTransparent = true }
    end

    iconContents[#iconContents+1] = { content = modules.game_inventory.getIconsPanelOn(), loadIconTransparent = false }
    iconContents[#iconContents+1] = { content = modules.game_inventory.getIconsPanelOff(), loadIconTransparent = false }

    return iconContents
end

local function toggleIcon(bitChanged)
    local contents = getStatsBarsIconContent()

    local iconId = Icons[bitChanged]
    if not iconId then
        g_logger.warning(string.format("No icon ID %s (%s)  found. Check Icons array in modules/gamelib/player.lua.", tostring(bitChanged), tostring(math.log(bitChanged) / math.log(2))))
        return
    end
    for _, contentData in ipairs(contents) do
        local icon = contentData.content:getChildById(iconId.id)
        if icon then
            icon:destroy()
        else
            icon = loadIcon(bitChanged, contentData.content, contentData.loadIconTransparent)
            icon:setParent(contentData.content)
        end
    end
end

function processIcon(id, action, createIfMissing)
    -- game_rewardwall
    for _, contentData in ipairs(getStatsBarsIconContent()) do
        local icon = contentData.content:getChildById(id)
        if icon then
            action(icon)
        elseif createIfMissing then
            icon = loadIcon(id, contentData.content, contentData.loadIconTransparent)
            icon:setParent(contentData.content)
            action(icon)
        end
    end
end

function StatsBar.reloadCurrentStatsBarQuickInfo_state(localPlayer, now, old)
    local player = g_game.getLocalPlayer()
    if not player then
        return
    end

    if now == old then
        return
    end
    local bitsChanged = bit.bxor(now, old)
    for i = 1, 32 do
        local pow = math.pow(2, i - 1)
        if pow > bitsChanged then
            break
        end
        local bitChanged = bit.band(bitsChanged, pow)
        if bitChanged ~= 0 then
            toggleIcon(bitChanged)
        end
    end
end

function StatsBar.reloadCurrentStatsBarDeepInfo()
    local player = g_game.getLocalPlayer()
    if not player then
        return
    end

    local bar = StatsBar.getCurrentStatsBarWithPosition()
    if not bar then
        return
    end

    for _, skillTuple in ipairs(skillsTuples) do
        local widget = bar:recursiveGetChildById('statsbar_skill_' .. skillTuple.key)
        if widget then
            if skillTuple.key == 'experience' then
                widget.level:setText(player:getLevel())
                widget.bar:setValue(player:getLevelPercent(), 100)
            elseif skillTuple.key == 'magic' then
                widget.level:setText(player:getMagicLevel())
                widget.bar:setValue(player:getMagicLevelPercent(), 100)
            else
                widget.level:setText(player:getSkillLevel(skillTuple.skill))
                widget.bar:setValue(player:getSkillLevelPercent(skillTuple.skill), 100)
            end
        end
    end
end

function constructStatsBar(dimension, placement)
    local dimensionString = dimension:gsub("^%u", string.lower)
    StatsBar.updateCurrentStats(dimensionString, placement)

    local dimensionOnPlacement = dimensionString:gsub("^%u", string.lower) .. "On" .. placement:gsub("^%l", string.upper)
    local statsBar = statsBars["statsBar" .. placement:gsub("^%l", string.upper)]

    if statsBar[dimensionOnPlacement] then
        statsBar:setHeight(statsBarsDimensions[dimension].height)
        statsBar[dimensionOnPlacement]:setHeight(statsBarsDimensions[dimension].height)
        statsBar[dimensionOnPlacement]:show()
        statsBar[dimensionOnPlacement]:setPhantom(false)
        statsBar[dimensionOnPlacement].health = statsBar[dimensionOnPlacement]:getChildById('health')
        statsBar[dimensionOnPlacement].mana = statsBar[dimensionOnPlacement]:getChildById('mana')
        statsBar[dimensionOnPlacement].skills = statsBar[dimensionOnPlacement]:getChildById('skills')

    reloadSkillsTab(statsBar[dimensionOnPlacement].skills, statsBar[dimensionOnPlacement])
    StatsBar.reloadCurrentStatsBarQuickInfo()

    modules.game_healthcircle.setStatsBarOption()
    else 
        print("No stats bar found for:", dimensionOnPlacement .. " on constructStatsBar()")
    end
end

function StatsBar.updateCurrentStats(dimension, placement)
    currentStats = {
        dimension = dimension,
        placement = placement
    }
end

local function openDropMenu(mousePos)
    local menu = g_ui.createWidget('PopupMenu')
    menu:setGameMenu(true)

    local current = StatsBar.getCurrentStatsBarWithPosition()

    local menuOptions = getStatsBarMenuOptions(current)

    -- Add options to the menu based on the current stats bar
    for _, option in ipairs(menuOptions) do
        menu:addOption(tr(option.label), function()
            StatsBar.hideAll()
            constructStatsBar(option.dimension, option.placement)
        end)
    end

    menu:addSeparator()

    local current = StatsBar.getCurrentStatsBarWithPosition()
    if current and current.skills then
        for _, skillTuple in ipairs(skillsTuples) do
            if not g_settings.getBoolean('top_statsbar_' .. skillTuple.key) then
                menu:addOption(tr('Show') .. ' ' .. tr(skillTuple.name), function()
                    g_settings.set('top_statsbar_' .. skillTuple.key, true)
                    reloadSkillsTab(current.skills, current)
                end)
            else
                menu:addOption(tr('Hide') .. ' ' .. tr(skillTuple.name), function()
                    g_settings.set('top_statsbar_' .. skillTuple.key, false)
                    reloadSkillsTab(current.skills, current)
                end)
            end
        end
    end

    menu:addSeparator()
    menu:addOption(tr('Hide Customisable Status Bars'), function()
        StatsBar.hideAll()
        modules.game_healthcircle.setStatsBarOption("hide")
    end)

    menu:display(mousePos)
end

function shouldAddStatsBarOption(current, placement, style)
    local id = current:getId()
    return string.find(id, placement) and id ~= style
end

function getStatsBarMenuOptions(current)
    local optionsMenu = {}

    -- Create options available based on statsBarsPlacements and statsBarsDimensions tables.
    for _, placement in ipairs(statsBarsPlacements) do
        for dimension, _ in pairs(statsBarsDimensions) do
            local style = tostring(dimension):gsub("^%u", string.lower) .. 'On' .. placement
            if shouldAddStatsBarOption(current, placement, style) then
                optionsMenu[#optionsMenu+1] = { 
                    label = 'Switch to ' .. tostring(dimension) .. ' Style',
                    dimension = dimension,
                    placement = placement:gsub("^%u", string.lower),
                    style = style
                }
            end
        end
    end
    
    -- Create options available to switch placements keeping the same style
    -- i.e. from bottom to top // from top to bottom
    for _, placement in ipairs(statsBarsPlacements) do
        if not string.find(current:getId(), placement) then
            optionsMenu[#optionsMenu+1] = {
                label = 'Switch to ' .. placement .. ' Style',
                dimension = currentStats.dimension:gsub("^%l", string.upper),
                placement = placement:gsub("^%u", string.lower),
                construct = constructStatsBar,
                style = current:getId()
            }
        end
    end

    return optionsMenu
end

local function onStatsMousePress(tab, mousePos, mouseButton)
    if mouseButton == MouseRightButton then
        openDropMenu(mousePos)
        return true
    end
end

function StatsBar.reloadCurrentTab()
    if currentStats.dimension == "hide" then
        return
    end

    local dimension = currentStats.dimension:gsub("^%l", string.upper)

    if statsBarsDimensions[dimension] then
        return constructStatsBar(dimension, currentStats.placement)
    else 
        print("No stats bars dimensions found: ", dimension, " on reloadCurrentTab()")
        return
    end
end

function StatsBar.updateStatsBarOption(dimension)
    StatsBar.hideAll()
    StatsBar.firstLoadSettings()

    if currentStats.dimension ~= "hide" and dimension ~= "hide" then
        StatsBar.reloadCurrentTab()
    end
end

local function getSettingOrDefault(setting, default)
  local value = g_settings.getString(setting)
  return value ~= "" and value or default
end

local function setSetting(setting, value)
  g_settings.set(setting, value)
end

function StatsBar.loadSettings()
    currentStats = {
        dimension = getSettingOrDefault('statsbar_dimension', "hide"),
        placement = getSettingOrDefault('statsbar_placement', "top")
    }
end

function StatsBar.saveSettings()
    setSetting('statsbar_dimension', currentStats.dimension)
    setSetting('statsbar_placement', currentStats.placement)
end

function StatsBar.firstLoadSettings()
    if firstCall then
        currentStats.dimension = getSettingOrDefault("statsbar_dimension", "hide")
        currentStats.placement = getSettingOrDefault("statsbar_placement", "top")

        firstCall = false
    end

    StatsBar.saveSettings()
    StatsBar.loadSettings()
end

function StatsBar.OnGameEnd()
    StatsBar.saveSettings()
    StatsBar.hideAll()

    modules.game_inventory.getIconsPanelOn():destroyChildren()
    modules.game_inventory.getIconsPanelOff():destroyChildren()

    StatsBar.destroyAllIcons()
end

function StatsBar.OnGameStart()
    StatsBar.loadSettings()
    StatsBar.reloadCurrentTab()
    modules.game_healthcircle.setStatsBarOption()
end

function createStatsBarWidgets(statsBar)
    -- This method will create the widgets based on the statsBar, statsBarsPlacements and statsBarsDimensions tables.
    local widget = statsBar
    for _, placement in ipairs(statsBarsPlacements) do
      for dimension, _ in pairs(statsBarsDimensions) do
        local elementName = tostring(dimension):gsub("^%u", string.lower) .. "On" .. placement
        widget[elementName] = statsBar:getChildById(elementName)
      end
    end
    widget.onMousePress = onStatsMousePress
    return widget
end

function StatsBar.init()
    statsBarTop = modules.game_interface.getGameTopStatsBar()
    statsBarBottom = modules.game_interface.getGameBottomStatsBar()

    statsBars = {
        statsBarTop = statsBarTop,
        statsBarBottom = statsBarBottom
    }

    if not statsBarTop then
        return
    end

    if not statsBarBottom then
        return
    end

    -- Create widgets based on statsBars table.
    for _, statBar in pairs(statsBars) do
        statBar = createStatsBarWidgets(statBar)
    end

    statsBarDeepInfo = {
        onExperienceChange = StatsBar.reloadCurrentStatsBarDeepInfo,
        onLevelChange = StatsBar.reloadCurrentStatsBarDeepInfo,
        onHealthChange = StatsBar.reloadCurrentStatsBarQuickInfo,
        onManaChange = StatsBar.reloadCurrentStatsBarQuickInfo,
        onMagicLevelChange = StatsBar.reloadCurrentStatsBarDeepInfo,
        onBaseMagicLevelChange = StatsBar.reloadCurrentStatsBarDeepInfo,
        onSkillChange = StatsBar.reloadCurrentStatsBarDeepInfo,
        onBaseSkillChange = StatsBar.reloadCurrentStatsBarDeepInfo,
        onStatesChange = StatsBar.reloadCurrentStatsBarQuickInfo_state
    }

    StatsBar.hideAll()
    connect(LocalPlayer, statsBarDeepInfo)
    connect(g_game, {
        onGameStart = StatsBar.OnGameStart,
        onGameEnd = StatsBar.OnGameEnd
    })
end

function StatsBar.hideAll()
    -- This iterates between the tables: statsBar -> statsBarsPlacements -> statsBarsDimensions
    -- And hides all the stats bars based on these tables.
    for _, bar in pairs(statsBars) do
        for _, placement in pairs(statsBarsPlacements) do
            for dimension, _ in pairs(statsBarsDimensions) do
                local key = tostring(dimension):lower() .. "On" .. placement
                if bar[key] and bar[key].skills then
                    bar[key].skills:destroyChildren()
                    bar[key].skills:setHeight(0)
                    bar[key]:setHeight(0)
                    bar[key]:hide()
                end
            end
        end
        bar:setHeight(0)
    end
end

function StatsBar.destroyAllIcons()
    -- This iterates between the tables: statsBar -> statsBarsPlacements -> statsBarsDimensions
    -- And destroy all icons based on these tables.
    for _, bar in pairs(statsBars) do
        for _, placement in pairs(statsBarsPlacements) do
            for dimension, _ in pairs(statsBarsDimensions) do
                local key = tostring(dimension):lower() .. "On" .. placement
                if bar[key] and bar[key].skills then
                    bar[key].icons:destroyChildren()
                end
            end
        end
        bar:setHeight(0)
    end
end

function StatsBar.destroyAllBars()
    -- This iterates between the tables: statsBars
    -- And destroy all bars based on these tables.
    for _, bar in pairs(statsBars) do
        bar:destroy()
    end
end

function StatsBar.terminate()
    StatsBar.saveSettings()
    
    disconnect(LocalPlayer, statsBarDeepInfo)
    disconnect(g_game, {
        onGameStart = StatsBar.OnGameStart,
        OnGameEnd = StatsBar.OnGameEnd
    })
    
    StatsBar.destroyAllBars()
end

function StatsBar.onHungryChange(regenerationTime, alert)
    local contents = getStatsBarsIconContent()
    local info = Icons[PlayerStates.Hungry]
    if regenerationTime <= alert then
        for _, contentData in ipairs(contents) do
            local icon = contentData.content:getChildById(info.id)
            if not icon then
                icon = g_ui.createWidget('ConditionWidget', contentData.content)
                icon:setId(info.id)
                icon:setImageSource("/images/game/states/player-state-flags")
                icon:setImageClip(((info.clip - 1) * 9) .. ' 0 9 9')
                icon:setTooltip(info.tooltip)
                icon:setImageSize(tosize("9 9"))
                if contentData.loadIconTransparent then
                    icon:setMarginTop(5)
                end
            end
        end
    else
        for _, contentData in ipairs(contents) do
            local icon = contentData.content:getChildById(info.id)
            if icon then
                icon:destroy()
                icon = nil
            end
        end
    end
end
