// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
#[cfg(feature = "client")] use std::fmt::Write;

use serde::{Serialize, Deserialize};

#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Timer {
	now: f64,
	limit: f64,
	counting: Counting,
	#[serde(default)] paused: bool,
	#[serde(default)] format: Format,
	#[serde(default)] precision: Precision,
}

#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
enum Counting {
	Up,
	Down,
}

#[derive(Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
enum Format {
	#[default] Hmmss,
	Mss,
	S,
}

#[derive(Default, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
enum Precision {
	Integer,
	Deci,
	#[default] Centi,
	Milli,
}

impl Timer {
	pub fn count_down(time: f64, paused: bool) -> Timer {
		Timer {
			now: time,
			limit: 0.0,
			counting: Counting::Down,
			paused,
			format: Format::default(),
			precision: Precision::default(),
		}
	}

	pub fn update(&mut self, dt: f32) {
		if self.paused { return; }

		let dt = dt as f64;

		match self.counting {
			Counting::Up => self.now += dt,
			Counting::Down => self.now -= dt,
		}
	}

	#[cfg(feature = "client")]
	pub fn format_string(&self, time: f64) -> String {
		if time.is_nan() { return String::from("NaN"); }

		let mut s = String::new();
		if time < 0.0 {
			s.push('-');
		}

		if time.is_infinite() {
			s.push('∞');
			return s;
		}

		let subsec_scale = match self.precision {
			Precision::Integer => 1,
			Precision::Deci => 10,
			Precision::Centi => 100,
			Precision::Milli => 1000,
		};

		/*
		 * Using a different rounding strategy depending on the direction of the
		 * timer.
		 *
		 * The purpose of this is to avoid prematurely displaying the limit
		 * result. For example, a clock counting down starting at five seconds
		 * should go "5, 4, 3, 2, 1, 0 (briefly)", not "5 (half a second), 4, 3,
		 * 2, 1, 0 (half a second)" or "5 (briefly), 4, 3, 2, 1, 0". This is
		 * mainly personal preference.
		 */
		let round = match self.counting {
			Counting::Up => f64::floor,
			Counting::Down => f64::ceil,
		};

		let int_time = round(time * subsec_scale as f64).abs() as u64;
		let seconds = int_time / subsec_scale;

		if self.format == Format::S || seconds < 60 {
			let _ = write!(s, "{seconds}");
		} else if self.format == Format::Mss || seconds < 3600 {
			let _ = write!(s, "{}:{:02}", seconds / 60, seconds % 60);
		} else {
			let _ = write!(s, "{}:{:02}:{:02}", seconds / 3600, (seconds / 60) % 60, seconds % 60);
		}

		let frac_seconds = int_time % subsec_scale;
		match self.precision {
			Precision::Integer => (),
			Precision::Deci => _ = write!(s, ".{frac_seconds}"),
			Precision::Centi => _ = write!(s, ".{frac_seconds:02}"),
			Precision::Milli => _ = write!(s, ".{frac_seconds:03}"),
		}

		s
	}

	pub fn raw_seconds(&self) -> f64 {
		self.now
	}

	pub fn displayed_seconds(&self, time: f64, shift: f64) -> f64 {
		match self.counting {
			_ if self.paused => time,
			Counting::Up => (time + shift).min(self.limit),
			Counting::Down => (time - shift).max(self.limit),
		}
	}

	/**
	 * Returns the time `a` or `b` that is in the future. If the timer is
	 * counting up, that is taking the maximum and if the timer is counting down
	 * that is taking the minimum.
	 */
	#[cfg(feature = "client")]
	pub fn most_ahead(&self, a: f64, b: f64) -> f64 {
		match self.counting {
			Counting::Up => a.max(b),
			Counting::Down => a.min(b),
		}
	}
}
