-- Missing utility functions
local function formatMoney(value, separator)
    if not value then return "0" end
    return comma_value(tostring(value))
end

-- Enhanced number formatting function with new K, KK suffixes
local function formatLargeNumber(value)
    if not value then
        return "0"
    end
    
    -- Ensure we have a number
    local numValue = tonumber(value)
    if not numValue or numValue == 0 then
        return "0"
    end
    
    local absValue = math.abs(numValue)
    local isNegative = numValue < 0
    local prefix = isNegative and "-" or ""
    
    if absValue >= 100000000 then
        -- Values 100,000,000+ use KK notation
        -- Example: 100,700,000 = 1,007KK, 345,666,000 = 3,456KK
        local kkValue = math.floor(absValue / 100000)
        return prefix .. comma_value(tostring(kkValue)) .. "KK"
    elseif absValue >= 10000000 then
        -- Values 10,000,000 to 99,999,999 use K notation  
        -- Example: 16,667,000 = 16,667K
        local kValue = math.floor(absValue / 1000)
        return prefix .. comma_value(tostring(kValue)) .. "K"
    else
        -- Values 1 to 9,999,999 show as is
        return prefix .. comma_value(tostring(math.floor(absValue)))
    end
end

-- Add capitalize function to string library if it doesn't exist
if not string.capitalize then
    function string.capitalize(str)
        if not str or str == "" then
            return str
        end
        return str:gsub("(%l)(%w*)", function(first, rest)
            return first:upper() .. rest
        end)
    end
end

if not InputAnalyser then
	InputAnalyser = {
		launchTime = 0,
		session = 0,

		total = 0,
		maxDPS = 0,
		monsterName = '',

		inputValues = {},
		damageTicks = {},
		damageEffect = {},

		graphVisible = true,
		typesVisible = true,
		sourceVisible = true,

		-- private
		window = nil,
	}
	InputAnalyser.__index = InputAnalyser
end

local imageDir = '/game_cyclopedia/images/bestiary/icons/monster-icon-%s-resist'

local effectsFiles = {
	[0] = 'physical',
	[1] = 'fire',
	[2] = 'earth',
	[3] = 'energy',
	[4] = 'ice',
	[5] = 'holy',
	[6] = 'death',
	[7] = 'healing',
	[8] = 'drowning',
	[9] = 'lifedrain',
	[10] = 'manadrain',
	[11] = 'agony',
	[12] = 'agony',
}

local obj = {
	launchTime = 0,
	session = 0,

	total = 0,
	maxDPS = 0,
	monsterName = '',

	inputValues = {},
	damageTicks = {},
	damageEffect = {},

	-- Session data storage for 60 minutes
	sessionDamageTicks = {},
	sessionMode = false,
	sessionMinuteData = {}, -- Array to store DPS data per minute for the graph
	lastMinuteUpdate = 0, -- Track when we last updated minute data

	graphVisible = true,
	typesVisible = true,
	sourceVisible = true,

	-- private
	window = nil,
	event = nil,
	eventGraph = nil,
}

local valueInSeconds = function(t)
    local d = 0
    local time = 0
    local now = g_clock.millis()
    if #t > 0 then
		local itemsToBeRemoved = 0
        for i, v in ipairs(t) do
            if now - v.tick <= 3000 then
                if time == 0 then
                    time = v.tick
                end
                d = d + v.amount
            else
				itemsToBeRemoved = itemsToBeRemoved + 1
            end
        end

		-- items are added in order, so we can safely
		-- remove only the first items
		for i = 1, itemsToBeRemoved do
			table.remove(t, 1)
		end
    end
    local deltaTime = (now - time) / 1000
    if deltaTime <= 0 then return 0 end
    return math.ceil(d / deltaTime)
end

-- Function to handle session data for 60 minutes
local valueInSessionMode = function(t)
    local d = 0
    local time = 0
    local now = g_clock.millis()
    local sessionDuration = 60 * 60 * 1000 -- 60 minutes in milliseconds
    
    if #t > 0 then
        local itemsToBeRemoved = 0
        for i, v in ipairs(t) do
            if now - v.tick <= sessionDuration then
                if time == 0 then
                    time = v.tick
                end
                d = d + v.amount
            else
                itemsToBeRemoved = itemsToBeRemoved + 1
            end
        end

        -- Remove expired items
        for i = 1, itemsToBeRemoved do
            table.remove(t, 1)
        end
    end
    
    if time > 0 then
        local deltaTime = (now - time) / 1000
        if deltaTime <= 0 then return 0 end
        return math.ceil(d / deltaTime)
    else
        return 0
    end
end

function InputAnalyser:create()
	InputAnalyser.window = openedWindows['damageButton']
	
	if not InputAnalyser.window then
		return
	end

	-- Hide buttons we don't want
	local toggleFilterButton = InputAnalyser.window:recursiveGetChildById('toggleFilterButton')
	if toggleFilterButton then
		toggleFilterButton:setVisible(false)
	end
	
	local newWindowButton = InputAnalyser.window:recursiveGetChildById('newWindowButton')
	if newWindowButton then
		newWindowButton:setVisible(false)
	end

	-- Position contextMenuButton where toggleFilterButton was (to the left of minimize button)
	local contextMenuButton = InputAnalyser.window:recursiveGetChildById('contextMenuButton')
	local minimizeButton = InputAnalyser.window:recursiveGetChildById('minimizeButton')
	
	if contextMenuButton and minimizeButton then
		contextMenuButton:setVisible(true)
		contextMenuButton:breakAnchors()
		contextMenuButton:addAnchor(AnchorTop, minimizeButton:getId(), AnchorTop)
		contextMenuButton:addAnchor(AnchorRight, minimizeButton:getId(), AnchorLeft)
		contextMenuButton:setMarginRight(7)  -- Same margin as toggleFilterButton had
		contextMenuButton:setMarginTop(0)
		
		-- Set up the click handler for the context menu
		contextMenuButton.onClick = function(widget, mousePosition)
			return onInputExtra(mousePosition)
		end
	end

	-- Position lockButton to the left of contextMenuButton
	local lockButton = InputAnalyser.window:recursiveGetChildById('lockButton')
	
	if lockButton and contextMenuButton then
		lockButton:setVisible(true)
		lockButton:breakAnchors()
		lockButton:addAnchor(AnchorTop, contextMenuButton:getId(), AnchorTop)
		lockButton:addAnchor(AnchorRight, contextMenuButton:getId(), AnchorLeft)
		lockButton:setMarginRight(2)  -- Same margin as in miniwindow style
		lockButton:setMarginTop(0)
	end

	InputAnalyser.launchTime = g_clock.millis()
	InputAnalyser.session = 0
	InputAnalyser.lastMinuteUpdate = g_clock.millis() -- Initialize minute tracking

	InputAnalyser.total = 0
	InputAnalyser.maxDPS = 0
	InputAnalyser.monsterName = ''
	InputAnalyser.inputValues = {}
	InputAnalyser.damageEffect = {}
	InputAnalyser.damageTicks = {}
	InputAnalyser.sessionDamageTicks = {}
	InputAnalyser.sessionMinuteData = {}
end

function InputAnalyser:reset()
	InputAnalyser.launchTime = g_clock.millis()
	InputAnalyser.session = 0

	InputAnalyser.total = 0
	InputAnalyser.maxDPS = 0
	InputAnalyser.monsterName = ''
	InputAnalyser.inputValues = {}
	InputAnalyser.damageEffect = {}
	InputAnalyser.damageTicks = {}
	InputAnalyser.sessionDamageTicks = {}
	InputAnalyser.sessionMinuteData = {}
	InputAnalyser.lastMinuteUpdate = g_clock.millis()

	InputAnalyser.window.contentsPanel.graphPanel:clear()
	
	-- Initialize graph if it doesn't exist
	if InputAnalyser.window.contentsPanel.graphPanel:getGraphsCount() == 0 then
		InputAnalyser.window.contentsPanel.graphPanel:createGraph()
		InputAnalyser.window.contentsPanel.graphPanel:setLineWidth(1, 1)
		InputAnalyser.window.contentsPanel.graphPanel:setLineColor(1, "#f75f5f")
	end
	
	InputAnalyser.window.contentsPanel.graphPanel:addValue(1, 0)

	InputAnalyser:toggleDamageSource(false)
	InputAnalyser:updateWindow()

	-- Damage types
	local contentsPanel = InputAnalyser.window.contentsPanel
	contentsPanel.dmgTypes:destroyChildren()
	local widget = g_ui.createWidget('NoDataLabel', contentsPanel.dmgTypes)
	widget:setId("nodata")
	contentsPanel.dmgTypes:setHeight(15)

	-- Damage sources
	contentsPanel.dmgSrc:destroyChildren()
	widget = g_ui.createWidget('NoDataLabel', contentsPanel.dmgSrc)
	widget:setId("nodata")
	contentsPanel.dmgSrc:setHeight(15)
end

function InputAnalyser:updateWindow(ignoreVisible)
	if not InputAnalyser.window then
		return
	end

	if not InputAnalyser.window:isVisible() and not ignoreVisible then
		return
	end
	InputAnalyser:checkAnchos()
	local contentsPanel = InputAnalyser.window.contentsPanel

	local dps = tonumber(InputAnalyser.maxDPS) or 1
	contentsPanel.rcvDmg:setText(formatMoney(InputAnalyser.total, ","))
	contentsPanel.maxDps:setText(formatMoney(dps, ","))

	-------------- DAMAGE TYPE
	local count = 1
	local widgets = {}

	for effect, damage in pairs(InputAnalyser.damageEffect) do
		local widget = contentsPanel.dmgTypes:recursiveGetChildById(tostring(effect))
		if not widget then
			widget = g_ui.createWidget('DamagePanel', contentsPanel.dmgTypes)
		end
	
		local percent = (damage * 100) / InputAnalyser.total
		widget:setId(effect)
		widget.icon:setImageSource(string.format(imageDir, effectsFiles[effect]))
		widget.icon:setTooltip(getCombatName(effect))
		widget.desc:setText(formatMoney(damage, ",") .. " (" .. string.format("%.1f", percent) .. "%)")
	
		count = count + 1
		table.insert(widgets, {widget = widget, percent = percent})
	end

	table.sort(widgets, function(a, b)
		return a.percent > b.percent
	end)

	for index, item in ipairs(widgets) do
		contentsPanel.dmgTypes:moveChildToIndex(item.widget, index)
	end

	contentsPanel.dmgTypes:setHeight(15 * count)
	if count > 1 then
		local noData = contentsPanel.dmgTypes:recursiveGetChildById("nodata")
		if noData then
			noData:destroy()
		end
	end

	-------------- DAMAGE SOURCE
	count = 1
	widgets = {}
	for monsterName, damageInfo in pairs(InputAnalyser.inputValues) do
		local damageMonster = 0
		for effect, damage in pairs(damageInfo) do
			damageMonster = damageMonster + damage
		end

		local widget = contentsPanel.dmgSrc:recursiveGetChildById(monsterName)
		if not widget then
			widget = g_ui.createWidget('DamageSourcePanel', contentsPanel.dmgSrc)
		end

		count = count + 1
		widget:setId(monsterName)
		widget.name:setText(short_text(string.capitalize(monsterName), 15))
		widget:setTooltip(string.capitalize(monsterName))
		local percent = (damageMonster * 100) / InputAnalyser.total
		widget.desc:setText(string.format("%.1f", percent) .. "%")
		widget.onClick = function()
			if InputAnalyser.monsterName == monsterName then
				InputAnalyser.monsterName = ''
				InputAnalyser:toggleDamageSource(false)
			else
				InputAnalyser.monsterName = monsterName
				InputAnalyser:toggleDamageSource(true)
			end
		end

		table.insert(widgets, {widget = widget, percent = percent})
	end

	table.sort(widgets, function(a, b)
		return a.percent > b.percent
	end)

	for index, item in ipairs(widgets) do
		contentsPanel.dmgSrc:moveChildToIndex(item.widget, index)
	end

	contentsPanel.dmgSrc:setHeight(15 + (10 * count))
	if count > 1 then
		local noData = contentsPanel.dmgSrc:recursiveGetChildById("nodata")
		if noData then
			noData:destroy()
		end
	end

	----------------- MONSTER LABEL
	contentsPanel.dmgSourceTypes:destroyChildren()
	if InputAnalyser.inputValues[InputAnalyser.monsterName] then
		local count = 1
		for effect, damage in pairs(InputAnalyser.inputValues[InputAnalyser.monsterName]) do
			local widget = g_ui.createWidget('DamagePanel', contentsPanel.dmgSourceTypes)
			count = count + 1
			widget.icon:setImageSource(string.format(imageDir, effectsFiles[effect]))
			widget.icon:setTooltip(getCombatName(effect))
			local percent = (damage * 100) / InputAnalyser.total
			widget.desc:setText(formatMoney(damage, ",") .. " (" .. string.format("%.1f", percent) .. "%)")
		end
		contentsPanel.dmgSourceTypes:setHeight(15 * count)
	elseif table.empty(InputAnalyser.inputValues) then
		local widget = g_ui.createWidget('NoDataLabel', contentsPanel.dmgSourceTypes)
		contentsPanel.dmgSourceTypes:setHeight(15)
	end
end

function InputAnalyser:checkDPS()
	local curDPS = 0
	if InputAnalyser.sessionMode then
		curDPS = valueInSessionMode(InputAnalyser.sessionDamageTicks)
	else
		curDPS = valueInSeconds(InputAnalyser.damageTicks)
	end
	
	if not curDPS or not tonumber(curDPS) then curDPS = 0 end
	InputAnalyser.curDPS = curDPS
	local lastDps = tonumber(InputAnalyser.maxDPS) or 1
	InputAnalyser.maxDPS = InputAnalyser.maxDPS > curDPS and InputAnalyser.maxDPS or curDPS
	if not tonumber(InputAnalyser.maxDPS) then
		InputAnalyser.maxDPS = lastDps
	end

	InputAnalyser.window.contentsPanel.maxDps:setText(formatMoney(InputAnalyser.maxDPS, ","))
	
	-- Ensure graph exists before adding value
	if InputAnalyser.window.contentsPanel.graphPanel:getGraphsCount() == 0 then
		InputAnalyser.window.contentsPanel.graphPanel:createGraph()
		InputAnalyser.window.contentsPanel.graphPanel:setLineWidth(1, 1)
		InputAnalyser.window.contentsPanel.graphPanel:setLineColor(1, "#f75f5f")
	end
	
	-- Only add current DPS to graph if in normal mode
	-- Session mode will rebuild the entire graph when switched
	if not InputAnalyser.sessionMode then
		InputAnalyser.window.contentsPanel.graphPanel:addValue(1, InputAnalyser.curDPS)
	end
	
	-- Update minute data for session tracking
	InputAnalyser:updateMinuteData()
end

function InputAnalyser:updateMinuteData()
	local now = g_clock.millis()
	local minuteInMs = 60 * 1000
	
	-- Initialize if first time
	if InputAnalyser.lastMinuteUpdate == 0 then
		InputAnalyser.lastMinuteUpdate = now
		return
	end
	
	-- Check if a minute has passed
	if now - InputAnalyser.lastMinuteUpdate >= minuteInMs then
		-- Calculate DPS for the past minute using session data
		local minuteDPS = 0
		local minuteStart = InputAnalyser.lastMinuteUpdate
		local totalDamage = 0
		
		for _, tick in ipairs(InputAnalyser.sessionDamageTicks) do
			if tick.tick >= minuteStart and tick.tick < now then
				totalDamage = totalDamage + tick.amount
			end
		end
		
		minuteDPS = totalDamage / 60 -- Damage per second for that minute
		
		-- Add to minute data array
		table.insert(InputAnalyser.sessionMinuteData, {
			timestamp = now,
			dps = minuteDPS
		})
		
		-- Keep only last 60 minutes of data
		while #InputAnalyser.sessionMinuteData > 60 do
			table.remove(InputAnalyser.sessionMinuteData, 1)
		end
		
		-- Update the graph if we're in session mode
		if InputAnalyser.sessionMode then
			InputAnalyser.window.contentsPanel.graphPanel:addValue(1, minuteDPS)
		end
		
		InputAnalyser.lastMinuteUpdate = now
	end
end


function InputAnalyser:addInputDamage(amount, effect, target)
	if not InputAnalyser.inputValues[target] then
		InputAnalyser.inputValues[target] = {}
	end
	if not InputAnalyser.inputValues[target][effect] then
		InputAnalyser.inputValues[target][effect] = 0
	end

	InputAnalyser.inputValues[target][effect] = InputAnalyser.inputValues[target][effect] + amount
	InputAnalyser.total = InputAnalyser.total + amount

	local currentTime = g_clock.millis()
	InputAnalyser.damageTicks[#InputAnalyser.damageTicks + 1] = {amount = amount, tick = currentTime}
	InputAnalyser.sessionDamageTicks[#InputAnalyser.sessionDamageTicks + 1] = {amount = amount, tick = currentTime}

	if not InputAnalyser.damageEffect[effect] then
		InputAnalyser.damageEffect[effect] = 0
	end

	InputAnalyser.damageEffect[effect] = InputAnalyser.damageEffect[effect] + amount
end

function InputAnalyser:toggleDamageSource(bool)
	InputAnalyser.window.contentsPanel.separatorDmgSrc:setVisible(bool)
	InputAnalyser.window.contentsPanel.damageSourceName:setVisible(bool)
	InputAnalyser.window.contentsPanel.dmgSourceTypes:setVisible(bool)

	InputAnalyser.window.contentsPanel.damageSourceName:setText(string.capitalize(InputAnalyser.monsterName))
	InputAnalyser.window.contentsPanel.dmgSourceTypes:destroyChildren()
	if InputAnalyser.inputValues[InputAnalyser.monsterName] then
		local count = 1
		for effect, damage in pairs(InputAnalyser.inputValues[InputAnalyser.monsterName]) do
			local widget = g_ui.createWidget('DamagePanel', InputAnalyser.window.contentsPanel.dmgSourceTypes)
			count = count + 1
			widget.icon:setImageSource(string.format(imageDir, effectsFiles[effect]))
			widget.icon:setTooltip(getCombatName(effect))
			local percent = (damage * 100) / InputAnalyser.total
			widget.desc:setText(formatMoney(damage, ",") .. " (" .. string.format("%.1f", percent) .. "%)")
		end
		InputAnalyser.window.contentsPanel.dmgSourceTypes:setHeight(15 * count)
	elseif table.empty(InputAnalyser.inputValues) then
		local widget = g_ui.createWidget('NoDataLabel', InputAnalyser.window.contentsPanel.dmgSourceTypes)
		InputAnalyser.window.contentsPanel.dmgSourceTypes:setHeight(15)
	end
end

function onInputExtra(mousePosition)
  if cancelNextRelease then
    cancelNextRelease = false
    return false
  end

  local graphVisible = InputAnalyser.window.contentsPanel.dpsGraphBG:isVisible()
  local typesVisible = InputAnalyser.window.contentsPanel.damageTypeLabel:isVisible()
  local sourceVisible = InputAnalyser.window.contentsPanel.damageSource:isVisible()

	local menu = g_ui.createWidget('PopupMenu')
	menu:setGameMenu(true)
	menu:addOption(tr('Reset Data'), function() InputAnalyser:reset() return end)
	
	-- Toggle between "Show Session Values" and "Show Current Values" based on current mode
	local sessionOptionText = InputAnalyser.sessionMode and tr('Show Current Values') or tr('Show Session Values')
	menu:addOption(sessionOptionText, function()
		InputAnalyser:toggleSessionMode()
	end)
	
	menu:addSeparator()
	menu:addCheckBox(tr('Show Damage Graph'), graphVisible, function()
		InputAnalyser:setDamageGraph(not graphVisible, true)
	end)
	menu:addCheckBox(tr('Show Damage Types'), typesVisible, function()
		InputAnalyser:setDamageTypes(not typesVisible, true)
	end)
	menu:addCheckBox(tr('Show Damage Sources'), sourceVisible, function()
		InputAnalyser:setDamageSource(not sourceVisible, true)
	end)
	menu:addSeparator()
	menu:addOption(tr('Copy to Clipboard'), function() InputAnalyser:clipboardData() return end)
	menu:display(mousePosition)
  return true
end

function InputAnalyser:checkAnchos()
	if InputAnalyser.window.contentsPanel.dpsGraphBG:isVisible() then
		InputAnalyser.window.contentsPanel.damageTypeLabel:addAnchor(AnchorTop, 'separatorGraph', AnchorBottom)
	else
		InputAnalyser.window.contentsPanel.damageTypeLabel:addAnchor(AnchorTop, 'separatorMaxDps', AnchorBottom)
	end

	if InputAnalyser.window.contentsPanel.damageTypeLabel:isVisible() then
		InputAnalyser.window.contentsPanel.damageSource:addAnchor(AnchorTop, 'separatorDmgType', AnchorBottom)
	elseif InputAnalyser.window.contentsPanel.dpsGraphBG:isVisible() then
		InputAnalyser.window.contentsPanel.damageSource:addAnchor(AnchorTop, 'separatorGraph', AnchorBottom)
	else
		InputAnalyser.window.contentsPanel.damageSource:addAnchor(AnchorTop, 'separatorMaxDps', AnchorBottom)
	end
end

function InputAnalyser:setDamageGraph(value, check)
	InputAnalyser.window.contentsPanel.dpsGraphBG:setVisible(value)
	InputAnalyser.window.contentsPanel.graphPanel:setVisible(value)
	InputAnalyser.window.contentsPanel.horizontalGraph:setVisible(value)
	InputAnalyser.window.contentsPanel.separatorGraph:setVisible(value)

	InputAnalyser.graphVisible = value

	if check then
		InputAnalyser:checkAnchos()
	end
end

function InputAnalyser:setDamageTypes(value, check)
	InputAnalyser.window.contentsPanel.damageTypeLabel:setVisible(value)
	InputAnalyser.window.contentsPanel.dmgTypes:setVisible(value)
	InputAnalyser.window.contentsPanel.separatorDmgType:setVisible(value)

	InputAnalyser.typesVisible = value

	if check then
		InputAnalyser:checkAnchos()
	end
end


function InputAnalyser:setDamageSource(value, check)
	InputAnalyser.window.contentsPanel.damageSource:setVisible(value)
	InputAnalyser.window.contentsPanel.dmgSrc:setVisible(value)

	InputAnalyser.sourceVisible = value

	InputAnalyser:toggleDamageSource(value)

	if check then
		InputAnalyser:checkAnchos()
	end
end

function InputAnalyser:toggleSessionMode()
	InputAnalyser.sessionMode = not InputAnalyser.sessionMode
	
	local horizontalGraph = InputAnalyser.window.contentsPanel.horizontalGraph
	
	if InputAnalyser.sessionMode then
		-- Switch to session mode: change image and show minute-by-minute data
		horizontalGraph:setImageSource('/images/game/analyzer/graphHorizontal')
		
		-- Clear and rebuild graph with session data
		InputAnalyser.window.contentsPanel.graphPanel:clear()
		if InputAnalyser.window.contentsPanel.graphPanel:getGraphsCount() == 0 then
			InputAnalyser.window.contentsPanel.graphPanel:createGraph()
			InputAnalyser.window.contentsPanel.graphPanel:setLineWidth(1, 1)
			InputAnalyser.window.contentsPanel.graphPanel:setLineColor(1, "#f75f5f")
		end
		
		InputAnalyser.window.contentsPanel.graphPanel:setCapacity(3600) -- 60 minutes worth of data points
		
		-- Add all historical minute data to graph
		for _, minuteData in ipairs(InputAnalyser.sessionMinuteData) do
			InputAnalyser.window.contentsPanel.graphPanel:addValue(1, minuteData.dps)
		end
		
		-- If no session data exists yet, add current DPS to start the graph properly
		if #InputAnalyser.sessionMinuteData == 0 then
			local currentDPS = valueInSeconds(InputAnalyser.damageTicks) or 0
			InputAnalyser.window.contentsPanel.graphPanel:addValue(1, currentDPS)
		end
	else
		-- Switch back to normal mode: restore original image and continue with existing graph
		horizontalGraph:setImageSource('/images/game/analyzer/graphDpsHorizontal')
		InputAnalyser.window.contentsPanel.graphPanel:setCapacity(400) -- Default capacity
		
		-- Don't clear the graph - let it continue from where it was before session mode
		-- The checkDPS() function will continue adding values automatically
	end
end

function InputAnalyser:clipboardData()

	local text = "Received Damage"
	text = text .. "\nTotal: " .. formatMoney(InputAnalyser.total, ",")
	text = text .. "\nMax-DPS: " .. formatMoney(InputAnalyser.maxDPS, ",")
	text = text .. "\nDamage Types"
	if table.empty(InputAnalyser.inputValues) then
		text = text .. "\n\tNo Data"
	else
		local count = 1
		for effect, damage in pairs(InputAnalyser.damageEffect) do
			local percent = (damage * 100) / InputAnalyser.total
			text = text .. "\n\t" .. getCombatName(effect) .. " " .. formatMoney(damage, ",") .. " (".. string.format("%.1f", percent) .."%)"
		end
	end
	text = text .. "\nDamage Sources"
	if table.empty(InputAnalyser.inputValues) then
		text = text .. "\n\tNo Data"
	else
		for monsterName, damageInfo in pairs(InputAnalyser.inputValues) do
			local damageMonster = 0
			for effect, damage in pairs(damageInfo) do
				damageMonster = damageMonster + damage
			end

			local percent = (damageMonster * 100) / InputAnalyser.total
			text = text .. "\n\t" .. string.capitalize(monsterName) .. " " .. formatMoney(damageMonster, ",") .. " (" .. string.format("%.1f", percent) .. "%)"
		end
	end
	if InputAnalyser.inputValues[InputAnalyser.monsterName] and InputAnalyser.window.contentsPanel.separatorDmgSrc:isVisible() then
		text = text .. "\n" .. string.capitalize(InputAnalyser.monsterName)
		for effect, damage in pairs(InputAnalyser.inputValues[InputAnalyser.monsterName]) do
			local percent = (damage * 100) / InputAnalyser.total
			text = text .. "\n\t" .. getCombatName(effect) .. " " .. formatMoney(damage, ",") .. " (".. string.format("%.1f", percent) .."%)"
		end
	end
	g_window.setClipboardText(text)
end

function InputAnalyser:damageGraphIsVisible() return InputAnalyser.graphVisible end
function InputAnalyser:damageTypesIsVisible() return InputAnalyser.typesVisible end
function InputAnalyser:damageSourceIsVisible() return InputAnalyser.sourceVisible end

function InputAnalyser:loadConfigJson()
	local config = {
		showDamageGraph = true,
		showDamageSources = true,
		showDamageTypes = true,
		showSessionValues = false,
	}
	local player = g_game.getLocalPlayer()
	if not player then return end
	local file = "/characterdata/" .. player:getId() .. "/damageinputanalyser.json"
	if g_resources.fileExists(file) then
		local status, result = pcall(function()
			return json.decode(g_resources.readFileContents(file))
		end)

		if not status then
			return g_logger.error("Error while reading characterdata file. Details: " .. result)
		end

		config = result
	end

	InputAnalyser:setDamageGraph(config.showDamageGraph, false)
	InputAnalyser:setDamageSource(config.showDamageSources, false)
	InputAnalyser:setDamageTypes(config.showDamageTypes, false)
	
	-- Load session mode state
	if config.showSessionValues then
		InputAnalyser.sessionMode = false -- Start false so toggle works correctly
		InputAnalyser:toggleSessionMode()
	end

	InputAnalyser:checkAnchos()
end

function InputAnalyser:saveConfigJson()
	local player = g_game.getLocalPlayer()
	if not player then return end
	
	-- Ensure the characterdata directory exists
	local characterDir = "/characterdata/" .. player:getId()
	pcall(function() g_resources.makeDir("/characterdata") end)
	pcall(function() g_resources.makeDir(characterDir) end)
	
	local config = {
		showDamageGraph = InputAnalyser:damageGraphIsVisible(),
		showDamageSources = InputAnalyser:damageSourceIsVisible(),
		showDamageTypes = InputAnalyser:damageTypesIsVisible(),
		showSessionValues = InputAnalyser.sessionMode,
	}

	local file = "/characterdata/" .. player:getId() .. "/damageinputanalyser.json"
	local status, result = pcall(function() return json.encode(config, 2) end)
	if not status then
		return g_logger.error("Error while saving profile itemsData. Data won't be saved. Details: " .. result)
	end

	if result:len() > 100 * 1024 * 1024 then
		return g_logger.error("Something went wrong, file is above 100MB, won't be saved")
	end
	
	-- Safely attempt to write the file, ignoring errors during logout
	local writeStatus, writeError = pcall(function()
		return g_resources.writeFileContents(file, result)
	end)
	
	if not writeStatus then
		-- Log the error but don't spam the console during normal logout
		g_logger.debug("Could not save InputAnalyser config during logout: " .. tostring(writeError))
	end
end
