local re = re
local sdk = sdk
local d2d = d2d
local imgui = imgui
local log = log
local json = json
local draw = draw
local require = require
local tostring = tostring
local pairs = pairs
local ipairs = ipairs
local math = math
local string = string
local table = table
local type = type

local Core = require("_CatLib")
local Debug = require("_CatLib.debug")
local Draw = require("_CatLib.draw")
local CONST = require("_CatLib.const")


local mod = require("mhwilds_overlay.mod")
local OverlayData = require("mhwilds_overlay.data")
local Config = require("mhwilds_overlay.dps_chart.conf")
local DpsChartData = require("mhwilds_overlay.dps_chart.data")

local _M = {}

local WeaponImages = {}

-- 上次计算的目标index
local LastHistoryCacheIndex = 0
-- 上次计算的首个index，相当于目标index-columns count
local LastHistoryCacheFirstIndex = -Config.ChartConfig.Columns

---@class HunterHistory
---@field MaxDps number
---@field Total number
---@field Dps number
---@field HunterIndex number
---@field DpsHistory table<integer, number> -- time index ->dps
---@field TotalHistory table<integer, number> -- time index ->total dmg
---@field DisplayDpsHistory number[] -- 0-120 dps
---@field DisplayTotalHistory number[] -- 0-120 total dmg

---@type HunterHistory[]
local HunterHistoryCache = {}

local InitCount = 0
local UpdateCount = 0
local OldIteration = 0
local RecycleCount = 0
local OldStart = 0
local OldEnd = 0
local ReCalcMaxDpsCount = 0

---@class SummaryData
---@field Name string
---@field Total number
---@field Ratio number
---@field Hunter Hunter
---@field HunterIndex number
---@field AttackTime number -- attacker elasped time
---@field DPS number
---@field WeaponType number -- weapon type

---@class SortedHistory
---@field MaxDPS number
---@field Index number -- Hunter Index
---@field DPS number[]
---@field Total number[]

---@class DPSComputedCache
---@field MaxDPS number
---@field MaxTotal number

-- 用于显示的缓存数据
---@type DPSComputedCache
local ComputedCache = {
    OldMaxDPS = 0,
    MaxDPS = 0,
    MaxTotal = 0,
}

---@type SortedHistory[]
local LastComputeSortedHistory = {}
---@type SummaryData[]
local LastComputeSummary = {}

--          0-119
-- 处理当前这一秒之前的所有数据：初始化（填0、继承前一秒，删去更的一个数据（若有）并更新maxDPS
local function BuildOldHistory(hunter, hunterIdx, fromIndex, toIndex)
    OldStart = fromIndex
    OldEnd = toIndex
    if not HunterHistoryCache[hunterIdx] then
        HunterHistoryCache[hunterIdx] = {
            MaxDps = 0,
            Total = 0,
            Dps = 0,
            HunterIndex = hunterIdx,
            DpsHistory = {
                Placeholder = true,
            },
            TotalHistory = {
                Placeholder = true,
            },
            DisplayDpsHistory = {},
            DisplayTotalHistory = {},
        }
    end

    local data = DpsChartData.HunterDpsRecord[hunter]

    local cache = HunterHistoryCache[hunterIdx]
    local dpsHistory = cache.DpsHistory
    local totalHistory = cache.TotalHistory
    local maxDps = cache.MaxDps

    local prevTotal = 0
    local prevInited = false
    local startIndex = LastHistoryCacheIndex
    -- 第一个采样点没有数据，需要填0或者继承
    if fromIndex < LastHistoryCacheFirstIndex or not HunterHistoryCache[hunterIdx].TotalHistory[fromIndex] then
        -- 当且仅当所需的第一个数据不存在时，初始化它。这样后续的数据即使不存在也可以基于第一个数据计算而来
        -- 如果所需的第一个数据甚至早于最早的记录，不处理，因为会填0  reqFirst -> First -> Last
        if fromIndex >= data.FirstIndex and not data.DpsHistory[fromIndex] then
            if fromIndex > data.LastIndex then
                -- 当需要的最早的数据，都晚于现在最新的数据，取最新的数据 First -> Last -> reqFirst
                prevTotal = data.DpsHistory[data.LastIndex].Total
            else
                -- 从初始点位开始，向前回溯直到找到数据 First -> reqFirst -> Last
                for i = fromIndex, data.FirstIndex-1, -1 do
                    if data.DpsHistory[i] then
                        prevTotal = data.DpsHistory[i].Total
                        break
                    end
                end
            end

            local firstDPS = 0
            local elapsedIndex = (fromIndex - data.FirstIndex) + 1
            if elapsedIndex == 1 then
                elapsedIndex = 1.5 -- grace first hit
            end

            if prevTotal > 0 and elapsedIndex > 0 then
                firstDPS = prevTotal/(elapsedIndex * Config.SampleRate)
            end
            HunterHistoryCache[hunterIdx].DpsHistory[fromIndex] = firstDPS
            HunterHistoryCache[hunterIdx].TotalHistory[fromIndex] = prevTotal
            maxDps = math.max(maxDps, firstDPS)
            prevInited = true
        end

        -- 初始化（填0 或继承）
        startIndex = fromIndex
        InitCount = InitCount + 1
    end

    if not prevInited then
        if cache.TotalHistory[startIndex-1] then
            prevTotal = cache.TotalHistory[startIndex-1]
        end
    end

    -- OldIteration = 0
    -- mod.verbose(string.format("OldIteration %d -> %d", startIndex, toIndex))
    if startIndex ~= toIndex then
        for i = startIndex, toIndex, 1 do
            OldIteration = OldIteration + 1
            local idx = i

            if idx < data.FirstIndex then
                -- 数据量不足，填充 0
                dpsHistory[idx] = 0
                totalHistory[idx] = 0
                -- mod.verbose(string.format("dps-total[%d]=%0.1f - %0.1f", idx, 0, 0))
                goto continue
            end

            local dps = 0
            local total = 0
            local snapshot = data.DpsHistory[idx]
            if snapshot then
                dps = snapshot.DPS
                total = snapshot.Total
                prevTotal = total
            else
                total = prevTotal
                local elapsedIndex = idx - data.FirstIndex + 1
                if elapsedIndex == 1 then
                    elapsedIndex = 1.5 -- grace first hit
                end

                if total > 0 and elapsedIndex > 0 then
                    dps = total/(elapsedIndex * Config.SampleRate)
                end

                -- data.DpsHistory[index - idx] = snapshot -- will crash, why?
            end

            if dps > maxDps then
                maxDps = dps
            end

            ComputedCache.OldMaxDPS = math.max(ComputedCache.OldMaxDPS, maxDps)
            dpsHistory[idx] = dps
            totalHistory[idx] = total
            -- mod.verbose(string.format("dps-total[%d]=%0.1f - %0.1f", idx, dps, total))

            prevTotal = total
            ::continue::
        end
    end
    
    HunterHistoryCache[hunterIdx].DpsHistory = dpsHistory
    HunterHistoryCache[hunterIdx].TotalHistory = totalHistory
end

-- returns dps[], total[]
---@param hunter Hunter
---@return number[], number[]
local function BuildHunterHistory(hunter, arrayIdx)
    local data = DpsChartData.HunterDpsRecord[hunter]
    if not data or not data.FirstIndex then
        return
    end
    if not OverlayData.HunterInfo[hunter] then
        return
    end

    UpdateCount = UpdateCount + 1
    local hunterIdx = OverlayData.HunterInfo[hunter].HitIndex

    local cols = Config.ChartConfig.Columns
    local index = DpsChartData.CurrentSampleIndex
    local reqFirstIndex = index - cols

    -- if LastHistoryCacheFirstIndex == nil then
    --     LastHistoryCacheIndex = reqFirstIndex - 1
    --     LastHistoryCacheFirstIndex = reqFirstIndex - 1
    -- end

    local isNewIndex = index > LastHistoryCacheIndex or not HunterHistoryCache[hunterIdx]

    if isNewIndex then
        BuildOldHistory(hunter, hunterIdx, reqFirstIndex, index-1)
    end
    
    local cache = HunterHistoryCache[hunterIdx]
    local dpsHistory = cache.DpsHistory
    local totalHistory = cache.TotalHistory
    local maxDps = cache.MaxDps

    -- 实时更新当前这一秒的数据
    local dps = 0
    local total = 0
    local snapshot = data.DpsHistory[index]
    if snapshot then
        dps = snapshot.DPS
        total = snapshot.Total
        -- mod.verbose(string.format("dps-total[%d]=%0.1f - %0.1f [real-time]", index, dps, total))
    else
        total = totalHistory[index-1]
        local elapsedIndex = index - data.FirstIndex + 1
        if elapsedIndex == 1 then
            elapsedIndex = 1.5 -- grace first hit
        end

        if total > 0 and elapsedIndex > 0 then
            dps = total/(elapsedIndex * Config.SampleRate)
        end
        -- mod.verbose(string.format("dps-total[%d]=%0.1f - %0.1f [old]", index, dps, total))
    end

    if dps > maxDps then
        maxDps = dps
    end

    dpsHistory[index] = dps
    totalHistory[index] = total

    local reCalcMaxDps = false
    if isNewIndex then
        local indexDiff = index - LastHistoryCacheIndex
        if indexDiff > 0 then
            for i = 1, indexDiff, 1 do
                -- gc outdated data
                local deletedDps = dpsHistory[LastHistoryCacheFirstIndex-i]
                if not deletedDps then
                    goto continue
                end
                if deletedDps >= maxDps then
                    reCalcMaxDps = true
                end
                -- dpsHistory[LastHistoryCacheFirstIndex-i] = nil
                -- totalHistory[LastHistoryCacheFirstIndex-i] = nil
                RecycleCount = RecycleCount+1
                ::continue::
            end
        end
    end
    HunterHistoryCache[hunterIdx].DpsHistory = dpsHistory
    HunterHistoryCache[hunterIdx].TotalHistory = totalHistory
    HunterHistoryCache[hunterIdx].MaxDps = maxDps
    HunterHistoryCache[hunterIdx].Total = total
    HunterHistoryCache[hunterIdx].Dps = dps

    local dpsData = cache.DisplayDpsHistory
    local totalData = cache.DisplayTotalHistory
    if isNewIndex then
        dpsData = {table.unpack(dpsHistory, reqFirstIndex, index)}
        totalData = {table.unpack(totalHistory, reqFirstIndex, index)}
    
        if reCalcMaxDps then
            ReCalcMaxDpsCount = ReCalcMaxDpsCount + 1
            maxDps = math.max(table.unpack(dpsData))
        end
    else
        dpsData[#dpsData] = dps
        totalData[#dpsData] = total
    end
    HunterHistoryCache[hunterIdx].DisplayDpsHistory = dpsData
    HunterHistoryCache[hunterIdx].DisplayTotalHistory = totalData
    HunterHistoryCache[hunterIdx].MaxDps = maxDps

    ComputedCache.MaxDPS = math.max(ComputedCache.MaxDPS, maxDps)
    ComputedCache.MaxTotal = math.max(ComputedCache.MaxTotal, total)
    if not LastComputeSortedHistory[arrayIdx] then
        LastComputeSortedHistory[arrayIdx] = {}
    end
    LastComputeSortedHistory[arrayIdx].MaxDPS = maxDps
    LastComputeSortedHistory[arrayIdx].Index = hunterIdx
    LastComputeSortedHistory[arrayIdx].DPS = dpsData
    LastComputeSortedHistory[arrayIdx].Total = totalData

    --- 更新统计信息
    local attackTime = OverlayData.QuestStats.ElapsedTime - OverlayData.HunterInfo[hunter].FirstHitTime
    if attackTime < Config.SampleRate then
        attackTime = Config.SampleRate*1.5 -- grace first hit
    end

    if not LastComputeSummary[arrayIdx] then
        LastComputeSummary[arrayIdx] = {}        
    end
    
    LastComputeSummary[arrayIdx].Name = OverlayData.HunterInfo[hunter].Name
    LastComputeSummary[arrayIdx].Hunter = hunter
    LastComputeSummary[arrayIdx].HunterIndex = hunterIdx
    LastComputeSummary[arrayIdx].Total = total
    LastComputeSummary[arrayIdx].Ratio = total / OverlayData.QuestStats.Total
    LastComputeSummary[arrayIdx].AttackTime = attackTime
    LastComputeSummary[arrayIdx].DPS = total / attackTime
    LastComputeSummary[arrayIdx].WeaponType = OverlayData.HunterInfo[hunter].WeaponType
end

local function GCOutdatedData(hunter)
    if true then
        -- 是否值得用算力换一点内存？可能不值得。
        return
    end
    -- TODO FIXME
    local data = DpsChartData.HunterDpsRecord[hunter]
    if not data or not data.FirstIndex then
        return
    end
    local cols = Config.ChartConfig.Columns
    local index = DpsChartData.CurrentSampleIndex
    local firstDisplayIndex = index - cols

    local endIndex = math.min(data.LastIndex, firstDisplayIndex)
    local startIndex = data.FirstIndex + 1

    if endIndex > startIndex then
        local dataTransfered = data[firstDisplayIndex] ~= nil
        for i = endIndex, startIndex, -1 do
            local d = data.DpsHistory[i]
            if not dataTransfered and d then
                -- 将数据搬运到 FirstDisplayIndex 上
                data[firstDisplayIndex] = {
                    Total = d.Total,
                    DPS = 0,
                }
                local elapsedIndex = (firstDisplayIndex - data.FirstIndex + 1)
                if elapsedIndex == 1 then
                    elapsedIndex = 1.5 -- grace first hit
                end
                if d.Total > 0 and elapsedIndex > 0 then
                    data[firstDisplayIndex].DPS = data[firstDisplayIndex].Total / (elapsedIndex * Config.SampleRate)
                end

                dataTransfered = true
            end

            DpsChartData.HunterDpsRecord[hunter][i] = nil
        end
    end
end

local function BuildDpsChartData()
    local index = DpsChartData.CurrentSampleIndex
    if index == 0 and DpsChartData.LastBuildCacheIndex == -1 then
        return
    end

    if index <= DpsChartData.LastBuildCacheIndex then
        return
    end
    index = index -1

    local cols = Config.ChartConfig.Columns
    local reqFirstIndex = index - cols

    ComputedCache.MaxDPS = 0 -- ComputedCache.OldMaxDPS
    ComputedCache.MaxTotal = 0
    -- LastComputeSortedHistory = {}
    -- LastComputeSummary = {}
    -- hunter:get_StableQuestMemberIndex()
    -- get_StableMemberIndex() and get_StableQuestMemberIndex() return -1 in NPC, wtf
    local arrayIdx = 1
    for hunter, data in pairs(DpsChartData.HunterDpsRecord) do
        if not OverlayData.HunterInfo[hunter] or not OverlayData.HunterInfo[hunter].IsPlayer then
            goto continue
        end
        BuildHunterHistory(hunter, arrayIdx)

        -- GCOutdatedData(hunter)
        arrayIdx = arrayIdx + 1
        ::continue::
    end
    LastHistoryCacheIndex = index
    LastHistoryCacheFirstIndex = reqFirstIndex


    table.sort(LastComputeSortedHistory, function (left, right)
        return left.MaxDPS > right.MaxDPS
    end)

    table.sort(LastComputeSummary, function (left, right)
        return left.Total > right.Total
    end)

    -- do no update current sec
    DpsChartData.LastBuildCacheIndex = index
end

local AutoThresholdArrayBig = {50000, 20000, 10000, 5000, 2000, 1000, 500, 200}
local AutoThresholdArray = {100, 50, 20, 10, 5}

local DpsLabelConfig = {
    Threshold = {
        threshold = function (max)
            max = max * 0.8 -- we don't want the chart be filled
            if Config.ChartConfig.ChartTypeTotalDamage or Config.ChartConfig.AutoDpsMeter then
                if max > 200 then
                    for _, threshold in pairs(AutoThresholdArrayBig) do
                        if max > threshold then
                            return threshold
                        end
                    end
                end

                for _, threshold in pairs(AutoThresholdArray) do
                    if max > threshold then
                        return threshold
                    end
                end
                return 2.5
            else
                return Config.ChartConfig.DpsMeterInterval
            end
        end,
        format = function (num)
            if Config.ChartConfig.ChartTypeTotalDamage then
                return string.format("%0.0f", num)
            else
                return string.format("%0.1f/s", num)
            end
        end,
    },
    SampleRate = {
        format = function (i)
            local delta = 0
            if DpsChartData.CurrentSampleIndex then
                delta = DpsChartData.CurrentSampleIndex - Config.ChartConfig.Columns
            end
            i = i + delta
            local curSecs = i * Config.SampleRate
            local min = math.floor(curSecs / 60)
            local secs = math.floor(curSecs % 60)
            return string.format("%d:%02.0f", min, secs)
        end,
        show = function (i)
            local delta = 0
            if DpsChartData.CurrentSampleIndex then
                delta = DpsChartData.CurrentSampleIndex - Config.ChartConfig.Columns
            end
            i = i + delta
            local curSecs = i * Config.SampleRate
            return curSecs > 0 and curSecs % Config.ChartConfig.QuestTimeInterval == 0
        end
    }
}

local function DrawDpsChart()
    if not (Config.ChartConfig.Enable or Config.QuestInfoConfig.Enable or Config.PlayerInfoConfig.Enable) then
        return
    end

    local summary = LastComputeSummary
    if not summary or #summary == 0 then
        return
    end

    -- if mod.Config.Debug and summary[1] then
    --     if not summary[2] then
    --         summary[2] = summary[1]
    --     end
    --     if not summary[3] then
    --         summary[3] = summary[1]
    --     end
    --     if not summary[4] then
    --         summary[4] = summary[1]
    --     end
    -- end

    local bgColor = Config.BackgroundColor
    local x = Config.PosX
    local y = Config.PosY

    local dpsFont = Draw.LoadD2dFontWithConfig(Config.ChartConfig.DpsMeterFontConfig)
    local timeFont = Draw.LoadD2dFontWithConfig(Config.ChartConfig.TimeMeterFontConfig)
    local thresholdFmt = DpsLabelConfig.Threshold.format
    local sampleRateFmt = DpsLabelConfig.SampleRate.format
    -------------------------------
    -- [calculate background width]
    -- Width: PADDING(Left) + Vertical Label MaxWidth + LabelMargin +
    --          ThresholdExtend + DPS Chart Width + Max(ThresholdExtend, Horizontal Label MaxWidth) + PADDING(Right)
    -- [calculate background height]
    -- height: PADDING(Top) + Vertical Label MaxHeight/2 +
    --              DPS Chart Height +
    --        Max(Vertical Label MaxHeight/2, Horizontal Label MaxHeight) +PADDING(Top)
    -------------------------------
    local maxDisp = 100.1
    if Config.ChartConfig.ChartTypeTotalDamage then
        maxDisp = 50000
    end
    local VerticalLabelMaxWidth, VerticalLabelMaxHeight = dpsFont:measure(thresholdFmt(maxDisp))
    local HorizontalLabelMaxWidth, HorizontalLabelMaxHeight = timeFont:measure(sampleRateFmt(40*60.0))

    -- 仅曲线部分 h/w
    local DpsCurveHeight = Config.ChartConfig.Height
    local DpsCurveWidth = Config.ChartConfig.ColumnWidth * Config.ChartConfig.Columns


    local DpsLabelPaddingLeft = 0 -- 时间标记导致的 DPS 标尺向左延长
    if Config.ChartConfig.EnableDpsMeter or Config.ChartConfig.EnableQuestTimeMeter then
        DpsLabelPaddingLeft = HorizontalLabelMaxWidth / 2
    end

    local DpsLabelPaddingRight = 0
    if Config.ChartConfig.EnableDpsMeter then
        DpsLabelPaddingRight = math.floor(HorizontalLabelMaxWidth / 2)
    end

    local DpsLabelWidth = 0
    if Config.ChartConfig.EnableDpsMeter then
        DpsLabelWidth = VerticalLabelMaxWidth + Config.ChartConfig.DpsMeterPadding
    end
    local DpsLabelHeight = 0
    local DpsLabelHeightTop = 0
    local DpsLabelHeightDown = 0
    if Config.ChartConfig.EnableDpsMeter then
        DpsLabelHeightTop = math.floor(VerticalLabelMaxHeight/2)
        DpsLabelHeightDown = DpsLabelHeightTop
        DpsLabelHeight = DpsLabelHeightTop + DpsLabelHeightDown
    end
    if Config.ChartConfig.EnableQuestTimeMeter then
        DpsLabelHeightDown = math.max(DpsLabelHeightTop, HorizontalLabelMaxHeight)
        DpsLabelHeight = DpsLabelHeightTop + DpsLabelHeightDown
    end

    local DpsWidth = Config.ChartConfig.PaddingLeft + DpsLabelWidth + DpsLabelPaddingLeft + DpsCurveWidth + DpsLabelPaddingRight + Config.ChartConfig.PaddingRight
    local DpsHeight = Config.ChartConfig.PaddingTop + DpsLabelHeight + DpsCurveHeight + Config.ChartConfig.PaddingDown

    -------------------------------
    -- SUMMARY Calculation
    -------------------------------
    local enableQuestInfo = Config.QuestInfoConfig.Enable and Core.IsActiveQuest()
    local QuestHeight = 0
    if enableQuestInfo then
        QuestHeight = Config.QuestInfoConfig.Height + Config.QuestInfoConfig.OffsetY
    end

    local SummaryHeight = 0
    if Config.PlayerInfoConfig.Enable then
        SummaryHeight = (#summary) * Config.PlayerInfoConfig.Height + Config.PlayerInfoConfig.OffsetY
    end

    local WIDTH = math.max(Config.MinWidth, DpsWidth)
    local HEIGHT = QuestHeight + SummaryHeight + DpsHeight

    local QuestYStart = y + Config.QuestInfoConfig.OffsetY
    local SummaryYStart = y + QuestHeight + Config.PlayerInfoConfig.OffsetY
    local DpsYStart = y + QuestHeight + SummaryHeight + Config.ChartConfig.OffsetY

    -------------------------------
    -- Background
    -------------------------------
    if bgColor then
        local bgX = x
        local bgY = y
        local bgWidth = WIDTH
        local bgHeight = 0
        if Config.ChartConfig.Enable then
            bgHeight = bgHeight + DpsHeight
        end
        if Config.QuestInfoConfig.Enable then
            bgHeight = bgHeight + QuestHeight
        end
        if Config.PlayerInfoConfig.Enable then
            bgHeight = bgHeight + SummaryHeight
        end

        d2d.fill_rect(bgX, bgY, bgWidth, bgHeight, bgColor)
    end

    -------------------------------
    -- SUMMARY
    -------------------------------
    -- Quest Data
    if enableQuestInfo then
        local posX = x + Config.QuestInfoConfig.OffsetX
        local posY = QuestYStart
        local width = WIDTH
        local height = Config.QuestInfoConfig.Height

        local elapsed = OverlayData.QuestStats.ElapsedTime
        local elapsedMin = math.floor(elapsed / 60.0)
        local elapsedSecs = elapsed % 60.0
        local limit = math.floor(OverlayData.QuestStats.LimitTime)

        local timeMsg = string.format("%02d:%02.0f/%d:00", elapsedMin, elapsedSecs, limit)

        Draw.SmartText(posX, posY, width, height, Config.QuestInfoConfig.QuestTime, timeMsg)
    end

    -- PlayerData
    if Config.PlayerInfoConfig.Enable then
        local highestDPS = 0
        if Config.PlayerInfoConfig.HighestDPSLineFill then
            for i, data in pairs(summary) do
                if data.DPS > highestDPS then
                    highestDPS = data.DPS
                end
            end
        end
        for i, data in pairs(summary) do -- i from 1
            local posX = x + Config.PlayerInfoConfig.OffsetX + Config.PlayerInfoConfig.PaddingLeft
            local posY = SummaryYStart + (i-1)*Config.PlayerInfoConfig.Height
            local width = WIDTH - Config.PlayerInfoConfig.OffsetX - Config.PlayerInfoConfig.PaddingLeft
            local height = Config.PlayerInfoConfig.Height
            local endY = posY + height

            local color = mod.Runtime.Colors[data.HunterIndex]
            local solidColor = mod.Runtime.SolidColors[data.HunterIndex]
            if not color then
                color = Draw.RandomColor(0x44)
                mod.Runtime.Colors[data.HunterIndex] = color
            end
            if not solidColor then
                solidColor = Draw.SetAlpha(color, 0xAA)
                mod.Runtime.SolidColors[data.HunterIndex] = solidColor
            end
            local dpsWidth = width
            if Config.PlayerInfoConfig.HighestDPSLineFill and highestDPS > 0 then
                dpsWidth = width * (data.DPS / highestDPS)
            else
                dpsWidth = width * data.Ratio
            end

            if Config.PlayerInfoConfig.ShowDPSLineBackground then
                Draw.DimmedRect(posX, endY, dpsWidth, Config.PlayerInfoConfig.Height, color)
            end
            if Config.PlayerInfoConfig.ShowDPSLine then
                local thickness = Config.PlayerInfoConfig.DPSLineHeight
                local yOffset = math.floor(thickness/2)
                d2d.line(posX, endY - yOffset, posX + dpsWidth, endY - yOffset, thickness, solidColor)
            end

            -- weapon icon
            local weaponIconOffsetX
            if Config.PlayerInfoConfig.ShowWeaponIcon then
                local size = Config.PlayerInfoConfig.WeaponIconSize
                local imgY = posY + Config.PlayerInfoConfig.WeaponIconOffsetY
                if size < height then
                    imgY = imgY + math.floor((height-size)/2)
                end
                
                weaponIconOffsetX = Config.PlayerInfoConfig.WeaponIconOffsetX
                if size < Config.PlayerInfoConfig.DataOffsetX then
                    weaponIconOffsetX = weaponIconOffsetX + math.floor((Config.PlayerInfoConfig.DataOffsetX-size)/2)
                end
                local imgX = posX + weaponIconOffsetX

                if WeaponImages[data.WeaponType] then
                    d2d.image(WeaponImages[data.WeaponType], imgX, imgY, size, size)
                end
            end

            -- HR
            if OverlayData.HunterInfo[data.Hunter] and OverlayData.HunterInfo[data.Hunter].HR ~= nil then
                local hrMsg = tostring(OverlayData.HunterInfo[data.Hunter].HR)
                -- if mod.Config.Debug then
                --     hrMsg = "999"
                -- end
                local offsetX = 0
                if Config.PlayerInfoConfig.ShowWeaponIcon and Config.PlayerInfoConfig.HRLabelAlignWithWeaponIcon then
                    offsetX = weaponIconOffsetX
                    local w = Draw.LoadD2dFontWithConfig(Config.PlayerInfoConfig.HR):measure(hrMsg)
                    offsetX = offsetX + math.floor((Config.PlayerInfoConfig.WeaponIconSize-w)/2)
                end
                Draw.SmartText(posX+offsetX, posY, width, height, Config.PlayerInfoConfig.HR, hrMsg)
            end

            posX = posX + Config.PlayerInfoConfig.DataOffsetX
            local textWidth = width - Config.PlayerInfoConfig.DataOffsetX - Config.PlayerInfoConfig.PaddingRight
            local name = data.Name
            if mod.Config.Debug then
                name = name .. "#" .. tostring(i-1)
            end
            Draw.SmartText(posX, posY, textWidth, height, Config.PlayerInfoConfig.Name, name)

            local dpsMsg = string.format("%0.1f/s", data.DPS)
            Draw.SmartText(posX, posY, textWidth, height, Config.PlayerInfoConfig.DPS, dpsMsg)

            local totalMsg = tostring(math.floor(data.Total))
            Draw.SmartText(posX, posY, textWidth, height, Config.PlayerInfoConfig.TotalDamage, totalMsg)

            local ratio = string.format("%0.1f%%", data.Ratio * 100)
            Draw.SmartText(posX, posY, textWidth, height, Config.PlayerInfoConfig.Percentage, ratio)
        end
    end

    -------------------------------
    -- DPS Chart
    -------------------------------
    if Config.ChartConfig.Enable then
        local colW = Config.ChartConfig.ColumnWidth
        local len = Config.ChartConfig.Columns

        local posX = x + Config.ChartConfig.OffsetX + Config.ChartConfig.PaddingLeft
        local posY = DpsYStart + Config.ChartConfig.PaddingTop

        local max = ComputedCache.MaxDPS
        if Config.ChartConfig.ChartTypeTotalDamage then
            max = ComputedCache.MaxTotal
        end
        -------------------------------
        -- vertical labels (dps) (100.0/s)
        -------------------------------
        if Config.ChartConfig.EnableDpsMeter then
            local threshold = DpsLabelConfig.Threshold.threshold(max)
            local levels = math.max(math.floor(max / threshold), 1) + 1
            max = levels * threshold
            if len > 1 then
                local dpsLabelXStart = posX
                local dpsMeterX = dpsLabelXStart + DpsLabelWidth
                local dpsMeterXEnd = dpsMeterX + DpsLabelPaddingLeft + DpsCurveWidth + DpsLabelPaddingRight
                local dpsMeterY = posY + DpsLabelHeightTop

                for i = 0, levels, 1 do
                    local ratio = i * threshold / max
                    local offsetY = math.floor((1 - ratio) * DpsCurveHeight)
                    local label = thresholdFmt(i * threshold)
                    local labelWidth = dpsFont:measure(label)

                    local labelX = posX + (VerticalLabelMaxWidth - labelWidth)
                    local labelY = posY + offsetY
                    Draw.TextWithConfig(labelX, labelY, label, Config.ChartConfig.DpsMeterFontConfig)

                    local meterY = dpsMeterY + offsetY
                    d2d.line(dpsMeterX, meterY, dpsMeterXEnd, meterY, 1, 0xFFFFFFFF)
                end
            end
        end

        -------------------------------
        -- horizontal labels (7:00) quest time
        -------------------------------
        local dpsCurveX = posX + DpsLabelWidth + DpsLabelPaddingLeft
        local dpsCurveY = posY + DpsLabelHeightTop + DpsCurveHeight

        if Config.ChartConfig.EnableQuestTimeMeter then
            if len > 2 then
                local meterYTop = posY + DpsLabelHeightTop
                if Config.ChartConfig.TimeMeterHeight >= 0 then
                    meterYTop = dpsCurveY - Config.ChartConfig.TimeMeterHeight
                end
                -- we don't want left or right close line
                for i = 1, len-1, 1 do
                    if DpsLabelConfig.SampleRate.show(i) then
                        local label = sampleRateFmt(i)
                        local labelWidth, _ = timeFont:measure(label)
                        local labelX = dpsCurveX + colW * i

                        Draw.TextWithConfig(labelX - labelWidth / 2, dpsCurveY, label, Config.ChartConfig.TimeMeterFontConfig)
                        d2d.line(labelX, meterYTop, labelX, dpsCurveY, 1, 0xFFFFFFFF)
                    end
                end
            end
        end

        if Config.ChartConfig.ChartTypeTotalDamage then
            local dataArr = {}
            local colors = {}
            local solidColors = {}
            for i=1, #LastComputeSortedHistory, 1 do
                local data = LastComputeSortedHistory[i]
                table.insert(dataArr, data.Total)
                table.insert(colors, mod.Runtime.Colors[data.Index])
                table.insert(solidColors, mod.Runtime.SolidColors[data.Index])
            end

            Draw.FilledLinePlots(dpsCurveX, dpsCurveY, DpsCurveHeight, Config.ChartConfig.ColumnWidth, colors, solidColors, dataArr, max, Config.ChartConfig.Fill)
        else
            local dataArr = {}
            local colors = {}
            local solidColors = {}
            for i=1, #LastComputeSortedHistory, 1 do
                local data = LastComputeSortedHistory[i]
                table.insert(dataArr, data.DPS)
                table.insert(colors, mod.Runtime.Colors[data.Index])
                table.insert(solidColors, mod.Runtime.SolidColors[data.Index])
            end

            Draw.FilledLinePlots(dpsCurveX, dpsCurveY, DpsCurveHeight, Config.ChartConfig.ColumnWidth, colors, solidColors, dataArr, max, Config.ChartConfig.Fill)
        end
    end
end

local LastBuildTime = 0
mod.OnUpdateBehavior(function()
    -- 不要放到d2d reg里执行，否则会有bug
    if OverlayData.QuestStats.ElapsedTime - LastBuildTime > 0.1 then
        if Core.IsActiveQuest() then
            BuildDpsChartData()
        end
        LastBuildTime = OverlayData.QuestStats.ElapsedTime
    end
end)

mod.D2dRegister(function ()
    Draw.LoadD2dFontWithConfig(Config.PlayerInfoConfig.HR)
    Draw.LoadD2dFontWithConfig(Config.PlayerInfoConfig.Name)
    Draw.LoadD2dFontWithConfig(Config.PlayerInfoConfig.DPS)
    Draw.LoadD2dFontWithConfig(Config.PlayerInfoConfig.TotalDamage)
    Draw.LoadD2dFontWithConfig(Config.PlayerInfoConfig.Percentage)
    Draw.LoadD2dFontWithConfig(Config.QuestInfoConfig.QuestTime)
    Draw.LoadD2dFontWithConfig(Config.ChartConfig.DpsMeterFontConfig)
    Draw.LoadD2dFontWithConfig(Config.ChartConfig.TimeMeterFontConfig)

    if Config.PlayerInfoConfig.ShowWeaponIcon then
        WeaponImages[CONST.WeaponType.GreatSword] = mod.LoadImage("greatsword.png")
        WeaponImages[CONST.WeaponType.SwordShield] = mod.LoadImage("swordshield.png")
        WeaponImages[CONST.WeaponType.DualBlades] = mod.LoadImage("dualblades.png")
        WeaponImages[CONST.WeaponType.LongSword] = mod.LoadImage("longsword.png")
        WeaponImages[CONST.WeaponType.Hammer] = mod.LoadImage("hammer.png")
        WeaponImages[CONST.WeaponType.HuntingHorn] = mod.LoadImage("huntinghorn.png")
        WeaponImages[CONST.WeaponType.Lance] = mod.LoadImage("lance.png")
        WeaponImages[CONST.WeaponType.Gunlance] = mod.LoadImage("gunlance.png")
        WeaponImages[CONST.WeaponType.SwitchAxe] = mod.LoadImage("switchaxe.png")
        WeaponImages[CONST.WeaponType.ChargeBlade] = mod.LoadImage("chargeblade.png")
        WeaponImages[CONST.WeaponType.InsectGlaive] = mod.LoadImage("insectglaive.png")
        WeaponImages[CONST.WeaponType.Bow] = mod.LoadImage("bow.png")
        WeaponImages[CONST.WeaponType.HeavyBowgun] = mod.LoadImage("heavybowgun.png")
        WeaponImages[CONST.WeaponType.LightBowgun] = mod.LoadImage("lightbowgun.png")
    end
end, function ()
    DrawDpsChart()
end, "DpsChart")

function _M.ClearData()
    LastHistoryCacheIndex = 0
    LastHistoryCacheFirstIndex = -Config.ChartConfig.Columns

    HunterHistoryCache = {}
    InitCount = 0
    UpdateCount = 0
    OldIteration = 0
    RecycleCount = 0
    OldStart = 0
    OldEnd = 0
    ReCalcMaxDpsCount = 0
    ComputedCache = {
        OldMaxDPS = 0,
        MaxDPS = 0,
        MaxTotal = 0,
    }

    LastComputeSortedHistory = {}
    LastComputeSummary = {}

    LastBuildTime = 0
end

mod.OnDebugFrame(function ()
    local hunter = Core.GetPlayerCharacter()
    if not hunter then return end

    imgui.text(string.format("RecycleCount: %d, OldIteration: %d (%d->%d), InitCount: %d, UpdateCount: %d, ReCalcMaxDpsCount: %d",
        RecycleCount, OldIteration, OldStart, OldEnd, InitCount, UpdateCount, ReCalcMaxDpsCount))
    if false and OverlayData.HunterInfo[hunter] then
        -- debug internal data
        local curIdx = DpsChartData.CurrentSampleIndex
        local firstIdx = DpsChartData.CurrentSampleIndex - Config.ChartConfig.Columns
        local hunterIdx = OverlayData.HunterInfo[hunter].HitIndex

        -- BuildHunterHistory(hunter)
        imgui.text(string.format("%d -> %d [Request]", firstIdx, curIdx))
        imgui.text(string.format("%d -> %d [Cached]", LastHistoryCacheFirstIndex, LastHistoryCacheIndex))
        local data = HunterHistoryCache[hunterIdx]
        if data then
            local maxDPS = data.MaxDps
            local dpsData = {table.unpack(data.DpsHistory, firstIdx, curIdx)}
            local totalData = {table.unpack(data.TotalHistory, firstIdx, curIdx)}
            
            imgui.text("data: " .. tostring(data))
            imgui.text("maxDPS: " .. tostring(maxDPS))
            imgui.text("DpsHistory: " .. tostring(data.DpsHistory) .. ", size: " .. tostring(Core.GetTableSize(data.DpsHistory)))
            imgui.text("TotalHistory: " .. tostring(data.TotalHistory) .. ", size: " .. tostring(Core.GetTableSize(data.TotalHistory)))
            imgui.text("dpsData: " .. tostring(dpsData))
            imgui.text("totalData: " .. tostring(totalData))
        end
    end
    if OverlayData.HunterInfo[hunter] then
        -- debug computed data
        local current = DpsChartData.CurrentSampleIndex 
        local firstIdx = DpsChartData.CurrentSampleIndex - Config.ChartConfig.Columns
        local hunterIdx = OverlayData.HunterInfo[hunter].HitIndex
        
        imgui.text("Time: " .. tostring(OverlayData.QuestStats.ElapsedTime))
        imgui.text(string.format("%d  [Current]", current))
        imgui.text(string.format("%d -> %d [Request]", firstIdx, current))
        imgui.text(string.format("%d -> %d [Cached]", LastHistoryCacheFirstIndex or -1, LastHistoryCacheIndex or -1))
        imgui.text(string.format("%d[%d]", Core.GetTableSize(HunterHistoryCache), hunterIdx))
        -- BuildHunterHistory(hunter)
        local data = HunterHistoryCache[hunterIdx]
        if data then
            imgui.text("== Cached History Data ==")
            imgui.text("data: " .. tostring(data))
            imgui.text("maxDPS: " .. tostring(data.MaxDps))
            imgui.text("DPS: " .. tostring(data.Dps))
            imgui.text("Total: " .. tostring(data.Total))
            imgui.text("hunterIdx: " .. tostring(data.HunterIndex))
            imgui.text("DpsHistory: " .. tostring(data.DpsHistory) .. ", size: " .. tostring(Core.GetTableSize(data.DpsHistory)))
            -- Debug.DebugTable(data.DpsHistory)
            imgui.text("TotalHistory: " .. tostring(data.TotalHistory) .. ", size: " .. tostring(Core.GetTableSize(data.TotalHistory)))

            local dpsData = data.DpsHistory
            local totalData = data.TotalHistory

            if dpsData[current] then
                local msg = "("
                local dpsMsg = "("
                local totalMsg = "("
                for i = firstIdx, current, 1 do
                    local idx = tostring(i)
                    local dps = Core.FloatFixed1(dpsData[i])
                    local total = Core.FloatFixed1(totalData[i])
                    local max = math.max(string.len(idx), string.len(dps)-1, string.len(total)-1)

                    msg = msg .. "." .. string.rep("0", max-string.len(idx)) .. idx .. ", "
                    dpsMsg = dpsMsg .. string.rep("0", max-string.len(dps)+1) .. dps .. ", "
                    totalMsg = totalMsg .. string.rep("0", max-string.len(total)+1) .. total .. ", "
                end

                imgui.text(msg .. ")")
                imgui.text(dpsMsg .. ")")
                imgui.text(totalMsg .. ")")
            end
            
        else
            imgui.text("ERROR: nil cache")
        end

        local history = LastComputeSortedHistory[1]
        if history then
            local maxDPS = history.MaxDPS
            local dpsData = history.DPS
            local totalData = history.Total
            
            imgui.text("== Sorted Data ==")
            imgui.text("maxDPS: " .. tostring(maxDPS))
            imgui.text("dpsData: " .. tostring(dpsData) .. ", size: " .. tostring(#(dpsData)))
            imgui.text("totalData: " .. tostring(totalData) .. ", size: " .. tostring(#(dpsData)))
            
            local dpsData = LastComputeSortedHistory[1].DPS
            local totalData = LastComputeSortedHistory[1].Total

            local dpsMsg = "("
            local totalMsg = "("
            for i = 1, #dpsData, 1 do
                local dps = Core.FloatFixed1(dpsData[i])
                local total = Core.FloatFixed1(totalData[i])
                dpsMsg = dpsMsg .. dps .. ", "
                totalMsg = totalMsg .. total .. ", "
            end

            imgui.text(dpsMsg .. ")")
            imgui.text(totalMsg .. ")")
        else
            imgui.text("ERROR: nil sorted history")
        end

        local summary = LastComputeSummary[1]
        if summary then
            for i = 1, #LastComputeSummary, 1 do
                summary = LastComputeSummary[i]
                imgui.text("maxDPS: " .. tostring(summary.Name))
                imgui.text("HunterIndex: " .. tostring(summary.HunterIndex))
                imgui.text("WeaponType: " .. tostring(summary.WeaponType))
            end
        else
            imgui.text("ERROR: nil sorted history")
        end
    end


    imgui.text("QuestMemberIndex: " .. tostring(hunter:get_StableQuestMemberIndex()))

    if OverlayData.HunterDamageRecords[hunter] then
        Debug.DebugTable(OverlayData.HunterDamageRecords[hunter])
    end

    imgui.text("MemberCount: " .. tostring(#LastComputeSortedHistory))

end)

return _M