--core/fps_sampler.lua, samples the framerate and produces rolling averages (backend).
--the monitor's OnUpdate feeds one frame in at a time; this module accumulates and, every
--200ms, closes a fast-average bucket (the fast chart). every 10 of those it also closes a
--2-second-average bucket (the smooth chart). it owns no frame of its own --
--it only runs while the monitor (its single driver) is shown, so it costs nothing when the
--chart is closed.

local _ = nil
local addonName, private = ...

private.Core = private.Core or {}

--constants
--each primary (fast) bar is a 200ms average; each secondary (smooth) bar is a 2s average,
--so one secondary bucket holds 2 / 0.2 = 10 primary buckets (rounded for float safety).
--each chart keeps primaryCapacity / secondaryCapacity bars (set by the monitor via SetCapacities).
local PRIMARY_INTERVAL = 0.2
local SECONDARY_INTERVAL = 2
local PRIMARY_PER_SECONDARY = math.floor(SECONDARY_INTERVAL / PRIMARY_INTERVAL + 0.5)
--default number of bars each chart keeps; the monitor overrides this via SetCapacities so the bar
--count -- and therefore the time span (bars x interval) -- follows the chart's width.
local DEFAULT_CAPACITY = 140

---@class FPSPP_FPSSampler
local Sampler = {}
private.Core.FPSSampler = Sampler

--rolling history of 200ms average framerates (oldest first, newest last).
Sampler.primary = {}
--rolling history of 2-second average framerates.
Sampler.secondary = {}
--the 200ms values collected so far for the in-progress 2-second bucket.
Sampler.block = {}
--accumulators for the in-progress 200ms bucket.
Sampler.sum = 0
Sampler.frames = 0
Sampler.elapsed = 0
--the most recent 200ms average (shown as the big number).
Sampler.current = 0
--how many bars each chart keeps; the monitor sets these from the chart width (see SetCapacities).
Sampler.primaryCapacity = DEFAULT_CAPACITY
Sampler.secondaryCapacity = DEFAULT_CAPACITY

--creates an independent sampler with its own histories and accumulators, sharing this table's
--logic as the metatable. the embedded /fps chart uses the module singleton directly; the detached
--window makes one of these. two live drivers feeding ONE sampler would each call Update() per frame
--and double-count (elapsed and frames advance twice as fast), so each viewer owns its own.
---@return FPSPP_FPSSampler
function Sampler.New()
	local instance = setmetatable({}, {__index = Sampler})
	instance.primary = {}
	instance.secondary = {}
	instance.block = {}
	instance.sum = 0
	instance.frames = 0
	instance.elapsed = 0
	instance.current = 0
	instance.primaryCapacity = DEFAULT_CAPACITY
	instance.secondaryCapacity = DEFAULT_CAPACITY
	return instance
end

--appends value to a history list and trims it to capacity by dropping the oldest entry.
local pushCapped = function(history, value, capacity)
	history[#history + 1] = value
	if #history > capacity then
		table.remove(history, 1)
	end
end

--clears every accumulator and history; called when the monitor opens so each viewing
--session starts from a clean slate.
function Sampler:Reset()
	wipe(self.primary)
	wipe(self.secondary)
	wipe(self.block)
	self.sum = 0
	self.frames = 0
	self.elapsed = 0
	self.current = 0
end

--feeds one frame in: accumulates the current framerate, and when the primary interval has
--elapsed, closes a 200ms bucket (and, every 10, a 2-second bucket). returns true when a new
--primary value was produced, i.e. the charts should be redrawn.
---@param elapsed number seconds since the last frame
---@return boolean
function Sampler:Update(elapsed)
	--hot path: keep this minimal, it runs once per rendered frame.
	self.sum = self.sum + GetFramerate()
	self.frames = self.frames + 1
	self.elapsed = self.elapsed + elapsed
	if self.elapsed < PRIMARY_INTERVAL then
		return false
	end

	--close the primary bucket: mean of the per-frame framerates sampled this interval.
	local average = self.sum / self.frames
	self.current = average
	self.sum = 0
	self.frames = 0
	self.elapsed = 0
	pushCapped(self.primary, average, self.primaryCapacity)

	--feed the in-progress 2-second bucket; close it once it holds enough primary values.
	self.block[#self.block + 1] = average
	if #self.block >= PRIMARY_PER_SECONDARY then
		local blockSum = 0
		for index, value in ipairs(self.block) do
			blockSum = blockSum + value
		end

		pushCapped(self.secondary, blockSum / #self.block, self.secondaryCapacity)
		wipe(self.block)
	end

	return true
end

--returns the rolling 200ms-average history (oldest first).
---@return number[]
function Sampler:GetPrimaryHistory()
	return self.primary
end

--returns the rolling 2-second-average history (oldest first).
---@return number[]
function Sampler:GetSecondaryHistory()
	return self.secondary
end

--returns the most recent 200ms average framerate.
---@return number
function Sampler:GetCurrent()
	return self.current
end

--sets how many bars each chart keeps. the monitor derives this from the chart width so a wider
--chart holds more bars and therefore spans more time (bars x interval). call before sampling.
---@param primaryCapacity number
---@param secondaryCapacity number
function Sampler:SetCapacities(primaryCapacity, secondaryCapacity)
	self.primaryCapacity = primaryCapacity
	self.secondaryCapacity = secondaryCapacity
end

--returns how many bars each chart holds, so the monitor builds the matching texture count.
---@return number primaryCapacity
---@return number secondaryCapacity
function Sampler:GetCapacities()
	return self.primaryCapacity, self.secondaryCapacity
end

--returns the seconds-per-bar of each chart, so the monitor can label the time span it covers
--(bar count x interval).
---@return number primaryInterval
---@return number secondaryInterval
function Sampler:GetIntervals()
	return PRIMARY_INTERVAL, SECONDARY_INTERVAL
end
