// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
use serde::{Serialize, Deserialize};
use glam::Vec2;
#[cfg(feature = "client")] use strum::EnumCount;
#[cfg(feature = "client")] use strum_macros::EnumCount as EnumCountMacro;

#[cfg(feature = "client")] use super::direction::{Direction, BITS as DIRECTION_BITS};

/**
 * Represents the player input state.
 *
 * The input state is represented as a `u8` in the following format, where 'm'
 * represents the movement, 's' stands for shooting and 'd' stands for dropping
 * a flag:
 *
 * __dsmmmm
 *
 * 's' and 'd' are 1 when the shooting/drop flag actions are down respectively.
 * and "mmmm" is a value between 0 − 8 inclusive that represents which of the
 * eight directions (0 = top-left, 1 top, 2 = top-right, ...) and not moving at
 * all.
 *
 * Bits labelled '_' can be anything without changing the behaviour.
 */
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct InputState(u8);

const MOVEMENT_MASK: u8 = 0xf;
const SHOOTING_MASK: u8 = 0x10;
const DROPPING_FLAG_MASK: u8 = 0x20;

#[cfg(feature = "client")] const SHOOTING_POS: u8 = SHOOTING_MASK.ilog2() as u8;
#[cfg(feature = "client")] const DROPPING_FLAG_POS: u8 = DROPPING_FLAG_MASK.ilog2() as u8;

impl InputState {
	#[cfg(feature = "client")]
	pub fn new(movement: InputMovement, shooting: bool, dropping_flag: bool) -> InputState {
		InputState(
			movement.0 |
			((shooting as u8) << SHOOTING_POS) |
			((dropping_flag as u8) << DROPPING_FLAG_POS)
		)
	}

	#[cfg(any(feature = "client", test))]
	pub(super) fn to_bits(self) -> u8 {
		self.0
	}

	pub(super) fn from_bits(data: u8) -> InputState {
		/*
		 * Don't need to worry about invalid bits as those don't influence
		 * behaviour unlike direction which the code relies on being correct.
		 */
		InputState(data)
	}

	pub fn get_move_dir(self) -> Vec2 {
		use std::f32::consts::FRAC_1_SQRT_2 as F1S2;

		// Left to right, top to bottom
		Vec2::from_array(match self.0 & MOVEMENT_MASK {
			0 => [-F1S2,  F1S2],
			1 => [  0.0,   1.0],
			2 => [ F1S2,  F1S2],
			3 => [ -1.0,   0.0],
			4 => [  0.0,   0.0],
			5 => [  1.0,   0.0],
			6 => [-F1S2, -F1S2],
			7 => [  0.0,  -1.0],
			_ => [ F1S2, -F1S2],
		})
	}

	/**
	 * Returns if the player is attempting to shoot.
	 */
	pub fn shooting(self) -> bool {
		self.0 & SHOOTING_MASK != 0
	}

	/**
	 * Returns if the player is attempting to drop a flag they might have
	 * captured.
	 */
	pub fn dropping_flag(self) -> bool {
		self.0 & DROPPING_FLAG_MASK != 0
	}

	/**
	 * Returns if the player is attempting to move.
	 */
	#[cfg(feature = "client")]
	pub fn thrusting(self) -> bool {
		(self.0 & MOVEMENT_MASK) != 4
	}
}

impl Default for InputState {
	fn default() -> InputState {
		InputState(4) // Not moving and not shooting
	}
}

// INVARIANT: Must be between 0 and 8 inclusive
#[cfg(feature = "client")]
pub struct InputMovement(u8);

#[derive(EnumCountMacro)]
#[cfg(feature = "client")]
pub enum Movement { Left, Right, Down, Up }

#[cfg(feature = "client")]
impl From<[bool; Movement::COUNT]> for InputMovement {
	fn from(movement_states: [bool; Movement::COUNT]) -> InputMovement {
		let (mut dx, mut dy) = (0, 0);
		if movement_states[Movement::Left as usize] { dx -= 1; }
		if movement_states[Movement::Right as usize] { dx += 1; }
		if movement_states[Movement::Up as usize] { dy -= 1; }
		if movement_states[Movement::Down as usize] { dy += 1; }
		InputMovement(((dx + 1) + (dy + 1) * 3) as u8) // Invariant obviously met
	}
}

#[cfg(feature = "client")]
impl From<Direction> for InputMovement {
	fn from(dir: Direction) -> InputMovement {
		InputMovement(match dir.to_bits() >> (DIRECTION_BITS - 4) {
			0 | 15 => 5,
			1 | 2 => 2,
			3 | 4 => 1,
			5 | 6 => 0,
			7 | 8 => 3,
			9 | 10 => 6,
			11 | 12 => 7,
			_ => 8,
		}) // Invariant met, all values are in range
	}
}

#[cfg(feature = "client")]
impl Default for InputMovement {
	fn default() -> InputMovement {
		InputMovement(4) // Not moving
	}
}
