// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! Runtime performance guardrails.
//!
//! The idle/redraw model is efficient only if the app cooperates — a stuck
//! [`App::is_animating`](crate::App::is_animating), an aggressive
//! [`poll_interval`](crate::App::poll_interval), or continuous animation on
//! the software backend all quietly burn CPU/battery. The docs warn about
//! these, but the runtime can also **detect** them (opt-in dev diagnostics,
//! `LTK_PERF_WARN=1`) and **mitigate** one of them (cap animation to ~30 Hz
//! on the software renderer, opt-out via
//! [`App::cap_software_animation`](crate::App::cap_software_animation)).

use std::sync::OnceLock;
use std::time::{ Duration, Instant };

/// A settled animation should return `false` well before this; staying `true`
/// this long is almost always a forgotten `is_animating = false`.
const STUCK_THRESHOLD: Duration = Duration::from_secs( 10 );
/// Continuous software-rendered animation past this is worth flagging on
/// mobile, where it means sustained CPU with no GPU offload.
const SW_ANIM_THRESHOLD: Duration = Duration::from_secs( 3 );
/// Minimum gap between software-backend animation re-rasters (≈ 30 Hz).
const SOFTWARE_ANIM_MIN_INTERVAL: Duration = Duration::from_millis( 33 );
/// A `poll_interval` shorter than this defeats the event-driven idle model.
const POLL_WARN_THRESHOLD: Duration = Duration::from_millis( 100 );

/// `true` when `LTK_PERF_WARN` is set to a non-empty, non-`0` value. Cached.
fn perf_warn_enabled() -> bool
{
	static ENABLED: OnceLock<bool> = OnceLock::new();
	*ENABLED.get_or_init( ||
		std::env::var( "LTK_PERF_WARN" )
			.map( |v| !v.is_empty() && v != "0" )
			.unwrap_or( false )
	)
}

/// Per-runtime performance-guard state. Lives on `AppData`.
pub struct PerfState
{
	/// When the current continuous animation began (`None` while idle).
	animating_since:      Option<Instant>,
	/// Last software-backend animation re-raster, for the 30 Hz cap.
	last_anim_draw:       Instant,
	warned_stuck:         bool,
	warned_software_anim: bool,
	warned_poll:          bool,
}

impl PerfState
{
	pub fn new() -> Self
	{
		Self
		{
			animating_since:      None,
			// Start in the past so the first animated frame is never throttled.
			// `checked_sub` guards against underflow shortly after boot.
			last_anim_draw:       Instant::now().checked_sub( SOFTWARE_ANIM_MIN_INTERVAL ).unwrap_or_else( Instant::now ),
			warned_stuck:         false,
			warned_software_anim: false,
			warned_poll:          false,
		}
	}

	/// Reset when the app stops animating, so the next animation starts fresh
	/// (and can warn again if it, too, gets stuck).
	pub fn animation_stopped( &mut self )
	{
		self.animating_since      = None;
		self.warned_stuck         = false;
		self.warned_software_anim = false;
	}

	/// Called on every animated main-surface frame callback. Runs the opt-in
	/// diagnostics and applies the software animation-rate cap. Returns `true`
	/// if this frame should actually re-raster, `false` to throttle-skip it
	/// (the caller keeps the vsync cadence with a bare frame callback).
	pub fn animated_frame( &mut self, software: bool, cap_software: bool ) -> bool
	{
		let since = *self.animating_since.get_or_insert_with( Instant::now );

		if perf_warn_enabled()
		{
			if !self.warned_stuck && since.elapsed() >= STUCK_THRESHOLD
			{
				eprintln!(
					"[ltk][perf] App::is_animating() has stayed true for {}s — a settled \
					 animation must return false, or the loop redraws at the display rate \
					 forever (battery drain).",
					STUCK_THRESHOLD.as_secs(),
				);
				self.warned_stuck = true;
			}
			if software && !self.warned_software_anim && since.elapsed() >= SW_ANIM_THRESHOLD
			{
				eprintln!(
					"[ltk][perf] continuous animation on the software renderer — sustained \
					 CPU with no GPU offload (costly on mobile). Prefer event-driven redraws, \
					 or a GLES-capable compositor.",
				);
				self.warned_software_anim = true;
			}
		}

		if software && cap_software && self.last_anim_draw.elapsed() < SOFTWARE_ANIM_MIN_INTERVAL
		{
			return false;
		}
		self.last_anim_draw = Instant::now();
		true
	}

	/// Warn once (under `LTK_PERF_WARN`) if the app's `poll_interval` is short
	/// enough to defeat the idle model.
	pub fn warn_poll_interval( &mut self, dur: Duration )
	{
		if perf_warn_enabled() && !self.warned_poll && dur < POLL_WARN_THRESHOLD
		{
			eprintln!(
				"[ltk][perf] poll_interval() of {}ms defeats the event-driven idle model — \
				 wake the loop from your worker with set_channel_sender instead of polling.",
				dur.as_millis(),
			);
			self.warned_poll = true;
		}
	}
}

#[ cfg( test ) ]
mod tests
{
	use super::*;

	#[ test ]
	fn gles_animation_is_never_capped()
	{
		let mut p = PerfState::new();
		// Two back-to-back frames on the GPU backend both re-raster.
		assert!( p.animated_frame( false, true ) );
		assert!( p.animated_frame( false, true ) );
	}

	#[ test ]
	fn software_animation_is_capped_between_rerasters()
	{
		let mut p = PerfState::new();
		// First software frame draws; an immediate second is throttled
		// (< 33 ms since the last re-raster).
		assert!(  p.animated_frame( true, true ) );
		assert!( !p.animated_frame( true, true ) );
	}

	#[ test ]
	fn software_cap_opt_out_keeps_full_rate()
	{
		let mut p = PerfState::new();
		assert!( p.animated_frame( true, false ) );
		assert!( p.animated_frame( true, false ) );
	}

	#[ test ]
	fn stopping_resets_stuck_warning_state()
	{
		let mut p = PerfState::new();
		let _ = p.animated_frame( false, true );
		assert!( p.animating_since.is_some() );
		p.animation_stopped();
		assert!( p.animating_since.is_none() );
	}
}
