// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
use std::mem;

use glam::{UVec2, Vec2, Mat4};
use strum::EnumCount;
use strum_macros::EnumCount as EnumCountMacro;

use super::ResetType;

use crate::app::config::action::{Action, ActionEvent, ActionState};
use crate::utils::maths::{self, decay::Decay};

#[derive(EnumCountMacro)]
enum ZoomAction { In, Out }

#[derive(Default)]
pub struct Camera {
	pos: Vec2,
	target_pos: Vec2,
	target_vel: Vec2,
	zoom: f32,
	zoom_rendered: f32,
	size: Vec2,
	winsize: Vec2,
	matrix: Mat4,
	zoom_actions: [bool; ZoomAction::COUNT],
	jolted: bool,
}

pub enum DecayMode {
	Player,
	Free, // Freely moving (spectating and not following anyone)
}

const ZOOM_MIN: f32 = 4.0;
const ZOOM_MAX: f32 = 100.0;
const ZOOM_MAX_LEVEL: f32 = 20.0;
const INIT_ZOOM: f32 = ZOOM_MAX_LEVEL / 2.0;

impl Camera {
	pub fn new(winsize: UVec2) -> Camera {
		let zoom = INIT_ZOOM;
		Camera {
			zoom,
			zoom_rendered: zoom,
			winsize: winsize.as_vec2(),
			.. Camera::default()
		}
	}

	pub fn reset(&mut self, target_pos: Vec2, target_vel: Vec2, winsize: UVec2, typ: ResetType) {
		if typ == ResetType::Full {
			self.zoom = INIT_ZOOM;
			self.zoom_rendered = INIT_ZOOM;
		}

		self.pos = target_pos;
		self.target_pos = target_pos;
		self.target_vel = target_vel;
		self.winsize = winsize.as_vec2();
		self.jolted = true;
		self.update(DecayMode::Player, 0.0); // Forgot why I needed to call this
	}

	pub fn get_pos(&self) -> Vec2 { self.pos }
	pub fn get_size(&self) -> Vec2 { self.size }
	pub fn get_matrix(&self) -> &Mat4 { &self.matrix }

	pub fn resize_event(&mut self, winsize: UVec2) {
		self.winsize = winsize.as_vec2();
	}

	pub fn mouse_scroll_event(&mut self, dy: f32) {
		self.change_zoom(-dy);
	}

	pub fn get_window_size(&self) -> UVec2 {
		self.winsize.as_uvec2()
	}

	fn change_zoom(&mut self, diff: f32) {
		self.zoom = (self.zoom + diff).clamp(0.0, ZOOM_MAX_LEVEL);
	}

	pub fn action(&mut self, action: ActionEvent) {
		match action {
			ActionEvent::Action(Action::ZoomReset, ActionState::Pressed) => self.zoom = INIT_ZOOM,
			ActionEvent::Action(Action::ZoomIn, state) => self.zoom_actions[ZoomAction::In as usize] = state.pressed(),
			ActionEvent::Action(Action::ZoomOut, state) => self.zoom_actions[ZoomAction::Out as usize] = state.pressed(),
			ActionEvent::Action(..) => (),
			ActionEvent::AllReleased => self.zoom_actions = Default::default(),
		}
	}

	pub fn move_by(&mut self, dpos: Vec2) {
		self.pos += dpos;
		self.jolted = true;
	}

	pub fn move_to(&mut self, pos: Vec2) {
		let dpos = self.pos - self.target_pos;
		self.target_pos = pos;
		self.pos = self.target_pos + dpos;
		self.target_vel = Vec2::ZERO;
		self.jolted = true;
	}

	/**
	 * Input is in physical pixel coordinates.
	 */
	pub fn pixel_to_abs_world_coords(&self, pos: Vec2) -> Vec2 {
		self.pos + self.size * Vec2::new(
			pos.x / self.winsize.x - 0.5,
			0.5 - pos.y / self.winsize.y,
		)
	}

	/**
	 * Input is in physical pixel coordinates.
	 */
	pub fn rel_pixel_to_world_coords(&self, mut dpos: Vec2) -> Vec2 {
		dpos.y *= -1.0;
		dpos / self.winsize * self.size
	}

	pub fn rel_world_to_pixel_coords(&self, dpos: f32) -> f32 {
		dpos * self.winsize.x / self.size.x
	}

	/**
	 * Allows using logical pixel coordinates.
	 */
	pub fn world_to_pixel_coords_with_size(&self, pos: Vec2, winsize: Vec2) -> Vec2 {
		let half_ndc = (pos - self.pos) / self.size; // Both x and y between [-0.5, 0.5]
		Vec2::new(
			(half_ndc.x + 0.5) * winsize.x,
			(0.5 - half_ndc.y) * winsize.y,
		)
	}

	pub fn set_target(&mut self, pos: Vec2, vel: Vec2) {
		self.target_pos = pos;
		self.target_vel = vel;
	}

	pub fn move_target_dir(&mut self, dir: Vec2, dt: f32) {
		self.target_vel = dir * Camera::calc_zoom(self.zoom_rendered) * 3.0;
		self.target_pos += self.target_vel * dt;
	}

	pub fn move_target_dpos(&mut self, dpos: Vec2, dt: f32) {
		self.target_vel = dpos / maths::limit_dt(dt);
		self.target_pos += dpos;
	}

	pub fn update(&mut self, decay_mode: DecayMode, dt: f32) -> Vec2 {
		let mut dir = 0;
		if self.zoom_actions[ZoomAction::Out as usize] { dir += 1; }
		if self.zoom_actions[ZoomAction::In as usize] { dir -= 1; }
		self.change_zoom(dir as f32 * dt * 12.0);

		let diff_centre = (self.target_pos - self.pos) / Camera::calc_zoom(self.zoom_rendered);

		// Exponentially decays towards the real zoom, making it smooth
		self.zoom_rendered.decay_to(self.zoom, 12.0, dt);

		let zoom = Camera::calc_zoom(self.zoom_rendered);
		self.pos = self.target_pos - diff_centre * zoom;

		let rate = match decay_mode {
			DecayMode::Player => 128.0 / zoom,
			DecayMode::Free => 12.0,
		};

		let prev_pos = self.pos;
		self.pos.decay_to_moving(self.target_pos, self.target_vel, rate, dt); // Need to use this precise method to reduce jitter

		self.size = if self.winsize.x > self.winsize.y {
			Vec2::new(zoom * self.winsize.x / self.winsize.y, zoom)
		} else {
			Vec2::new(zoom, zoom * self.winsize.y / self.winsize.x)
		};

		self.matrix = Mat4::orthographic_rh_gl(
			self.pos.x - self.size.x / 2.0, self.pos.x + self.size.x / 2.0,
			self.pos.y - self.size.y / 2.0, self.pos.y + self.size.y / 2.0,
			-1.0, 1.0,
		);

		self.pos - prev_pos
	}

	fn calc_zoom(rendered: f32) -> f32 {
		ZOOM_MIN * (ZOOM_MAX / ZOOM_MIN).powf(rendered / ZOOM_MAX_LEVEL)
	}

	pub fn jolted(&mut self) -> bool {
		mem::take(&mut self.jolted)
	}
}
