use galactica_content::{Content, FactionHandle, GunPoint, Outfit, ShipHandle, SystemObjectHandle};
use nalgebra::{Point2, Point3};
use rand::{rngs::ThreadRng, Rng};
use rapier2d::math::Isometry;
use std::{collections::HashMap, time::Instant};

use super::{OutfitSet, ShipPersonality};

/// A ship autopilot.
/// An autopilot is a lightweight ShipController that
/// temporarily has control over a ship.
#[derive(Debug, Clone)]
pub enum ShipAutoPilot {
	/// No autopilot, use usual behavior.
	None,

	/// Automatically arrange for landing on the given object
	Landing {
		/// The body we want to land on
		target: SystemObjectHandle,
	},
}

/// Ship state machine.
/// Any ship we keep track of is in one of these states.
/// Dead ships don't exist---they removed once their collapse
/// sequence fully plays out.
#[derive(Debug, Clone)]
pub enum ShipState {
	/// This ship is dead, and should be removed from the game.
	Dead,

	/// This ship is alive and well in open space
	Flying {
		/// The autopilot we're using.
		/// Overrides ship controller.
		autopilot: ShipAutoPilot,
	},

	/// This ship has been destroyed, and is playing its collapse sequence.
	Collapsing {
		/// Total collapse sequence length, in seconds
		total: f32,

		/// How many seconds of the collapse sequence we've played
		elapsed: f32,
	},

	/// This ship is landed on a planet
	Landed {
		/// The planet this ship is landed on
		target: SystemObjectHandle,
	},

	/// This ship is landing on a planet
	/// (playing the animation)
	Landing {
		/// The planet we're landing on
		target: SystemObjectHandle,

		/// Our current z-coordinate
		current_z: f32,
	},

	/// This ship is taking off from a planet
	/// (playing the animation)
	UnLanding {
		/// The point, in world coordinates, to which we're going
		to_position: Isometry<f32>,

		/// The planet we're taking off from
		from: SystemObjectHandle,

		/// The total amount of time, in seconds, we will spend taking off
		total: f32,

		/// The amount of time we've already spent playing this unlanding sequence
		elapsed: f32,
	},
}

impl ShipState {
	/// What planet is this ship landed on?
	pub fn landed_on(&self) -> Option<SystemObjectHandle> {
		match self {
			Self::Landed { target } => Some(*target),
			_ => None,
		}
	}

	/// If this ship is collapsing, return total collapse time and remaining collapse time.
	/// Otherwise, return None
	pub fn collapse_state(&self) -> Option<(f32, f32)> {
		match self {
			Self::Collapsing {
				total,
				elapsed: remaining,
			} => Some((*total, *remaining)),
			_ => None,
		}
	}

	/// Compute position of this ship's sprite during its unlanding sequence
	pub fn unlanding_position(&self, ct: &Content) -> Option<Point3<f32>> {
		match self {
			Self::UnLanding {
				to_position,
				from,
				total,
				elapsed,
				..
			} => Some({
				let from = ct.get_system_object(*from);

				let t = Point2::new(to_position.translation.x, to_position.translation.y);
				let diff = t - Point2::new(from.pos.x, from.pos.y);
				//let diff = diff - diff.normalize() * (target.size / 2.0) * 0.8;

				// TODO: improve animation
				// TODO: fade
				// TODO: atmosphere burn
				// TODO: land at random point
				// TODO: don't jump camera
				// TODO: time by distance
				// TODO: keep momentum

				let pos = Point2::new(from.pos.x, from.pos.y) + (diff * (elapsed / total));

				Point3::new(
					pos.x,
					pos.y,
					from.pos.z + ((1.0 - from.pos.z) * (elapsed / total)),
				)
			}),
			_ => None,
		}
	}
}

/// Represents all attributes of a single ship
#[derive(Debug, Clone)]
pub struct ShipData {
	// Metadata values
	ct_handle: ShipHandle,
	faction: FactionHandle,
	outfits: OutfitSet,

	personality: ShipPersonality,

	/// Ship state machine. Keeps track of all possible ship state.
	/// TODO: document this, draw a graph
	state: ShipState,

	// State values
	// TODO: unified ship stats struct, like outfit space
	hull: f32,
	shields: f32,
	gun_cooldowns: HashMap<GunPoint, f32>,
	rng: ThreadRng,

	// Utility values
	/// The last time this ship was damaged
	last_hit: Instant,
}

impl ShipData {
	/// Create a new ShipData
	pub(crate) fn new(
		ct: &Content,
		ct_handle: ShipHandle,
		faction: FactionHandle,
		personality: ShipPersonality,
	) -> Self {
		let s = ct.get_ship(ct_handle);
		ShipData {
			ct_handle,
			faction,
			outfits: OutfitSet::new(s.space, &s.guns),
			personality,
			last_hit: Instant::now(),
			rng: rand::thread_rng(),

			// TODO: ships must always start landed on planets
			state: ShipState::Flying {
				autopilot: ShipAutoPilot::None,
			},

			// Initial stats
			hull: s.hull,
			shields: 0.0,
			gun_cooldowns: s.guns.iter().map(|x| (x.clone(), 0.0)).collect(),
		}
	}

	/// Set this ship's autopilot.
	/// Panics if we're not flying.
	pub fn set_autopilot(&mut self, autopilot: ShipAutoPilot) {
		match self.state {
			ShipState::Flying {
				autopilot: ref mut pilot,
			} => {
				*pilot = autopilot;
			}
			_ => {
				unreachable!("Called `set_autopilot` on a ship that isn't flying!")
			}
		};
	}

	/// Land this ship on `target`
	/// This does NO checks (speed, range, etc).
	/// That is the simulation's responsiblity.
	///
	/// Will panic if we're not flying.
	pub fn start_land_on(&mut self, target_handle: SystemObjectHandle) {
		match self.state {
			ShipState::Flying { .. } => {
				self.state = ShipState::Landing {
					target: target_handle,
					current_z: 1.0,
				};
			}
			_ => {
				unreachable!("Called `start_land_on` on a ship that isn't flying!")
			}
		};
	}

	/// When landing, update z position.
	/// Will panic if we're not landing
	pub fn set_landing_z(&mut self, z: f32) {
		match &mut self.state {
			ShipState::Landing {
				ref mut current_z, ..
			} => *current_z = z,
			_ => unreachable!("Called `set_landing_z` on a ship that isn't landing!"),
		}
	}

	/// Finish landing sequence
	/// Will panic if we're not landing
	pub fn finish_land_on(&mut self) {
		match self.state {
			ShipState::Landing { target, .. } => {
				self.state = ShipState::Landed { target };
			}
			_ => {
				unreachable!("Called `finish_land_on` on a ship that isn't flying!")
			}
		};
	}

	/// Take off from `target`
	pub fn unland(&mut self, to_position: Isometry<f32>) {
		match self.state {
			ShipState::Landed { target } => {
				self.state = ShipState::UnLanding {
					to_position,
					from: target,
					total: 2.0,
					elapsed: 0.0,
				};
			}
			_ => {
				unreachable!("Called `unland` on a ship that isn't landed!")
			}
		};
	}

	/// Add an outfit to this ship
	pub fn add_outfit(&mut self, o: &Outfit) -> super::OutfitAddResult {
		let r = self.outfits.add(o);
		self.shields = self.outfits.get_shield_strength();
		return r;
	}

	/// Remove an outfit from this ship
	pub fn remove_outfit(&mut self, o: &Outfit) -> super::OutfitRemoveResult {
		self.outfits.remove(o)
	}

	/// Try to fire a gun.
	/// Will panic if `which` isn't a point on this ship.
	/// Returns `true` if this gun was fired,
	/// and `false` if it is on cooldown or empty.
	pub(crate) fn fire_gun(&mut self, ct: &Content, which: &GunPoint) -> bool {
		let c = self.gun_cooldowns.get_mut(which).unwrap();

		if *c > 0.0 {
			return false;
		}

		let g = self.outfits.get_gun(which);
		if g.is_some() {
			let g = ct.get_outfit(g.unwrap());
			let gun = g.gun.as_ref().unwrap();
			*c = 0f32.max(gun.rate + self.rng.gen_range(-gun.rate_rng..=gun.rate_rng));
			return true;
		} else {
			return false;
		}
	}

	/// Hit this ship with the given amount of damage
	pub(crate) fn apply_damage(&mut self, ct: &Content, mut d: f32) {
		match self.state {
			ShipState::Flying { .. } => {}
			_ => {
				unreachable!("Cannot apply damage to a ship that is not flying!")
			}
		}

		if self.shields >= d {
			self.shields -= d
		} else {
			d -= self.shields;
			self.shields = 0.0;
			self.hull = 0f32.max(self.hull - d);
		}
		self.last_hit = Instant::now();

		if self.hull <= 0.0 {
			// This ship has been destroyed, update state
			self.state = ShipState::Collapsing {
				total: ct.get_ship(self.ct_handle).collapse.length,
				elapsed: 0.0,
			}
		}
	}

	/// Update this ship's state by `t` seconds
	pub(crate) fn step(&mut self, t: f32) {
		match self.state {
			ShipState::Landing { .. } => {}

			ShipState::UnLanding {
				ref mut elapsed,
				total,
				..
			} => {
				*elapsed += t;
				if *elapsed >= total {
					self.state = ShipState::Flying {
						autopilot: ShipAutoPilot::None,
					};
				}
			}

			ShipState::Landed { .. } => {
				// Cooldown guns
				for (_, c) in &mut self.gun_cooldowns {
					if *c > 0.0 {
						*c = 0.0;
					}
				}

				// Regenerate shields
				if self.shields != self.outfits.get_shield_strength() {
					self.shields = self.outfits.get_shield_strength();
				}
			}

			ShipState::Flying { .. } => {
				// Cooldown guns
				for (_, c) in &mut self.gun_cooldowns {
					if *c > 0.0 {
						*c -= t;
					}
				}

				// Regenerate shields
				let time_since = self.last_hit.elapsed().as_secs_f32();
				if self.shields != self.outfits.get_shield_strength() {
					for g in self.outfits.iter_shield_generators() {
						if time_since >= g.delay {
							self.shields += g.generation * t;
							if self.shields > self.outfits.get_shield_strength() {
								self.shields = self.outfits.get_shield_strength();
								break;
							}
						}
					}
				}
			}

			ShipState::Collapsing {
				ref mut elapsed,
				total,
			} => {
				*elapsed += t;
				if *elapsed >= total {
					self.state = ShipState::Dead
				}
			}

			ShipState::Dead => {}
		}
	}
}

// Misc getters, so internal state is untouchable
impl ShipData {
	/// Get this ship's state
	pub fn get_state(&self) -> &ShipState {
		&self.state
	}

	/// Get a handle to this ship's content
	pub fn get_content(&self) -> ShipHandle {
		self.ct_handle
	}

	/// Get this ship's current hull.
	/// Use content handle to get maximum hull
	pub fn get_hull(&self) -> f32 {
		self.hull
	}

	/// Get this ship's current shields.
	/// Use get_outfits() for maximum shields
	pub fn get_shields(&self) -> f32 {
		self.shields
	}

	/// Get all outfits on this ship
	pub fn get_outfits(&self) -> &OutfitSet {
		&self.outfits
	}

	/// Get this ship's personality
	pub fn get_personality(&self) -> ShipPersonality {
		self.personality
	}

	/// Get this ship's faction
	pub fn get_faction(&self) -> FactionHandle {
		self.faction
	}

	/// Get this ship's content handle
	pub fn get_ship(&self) -> ShipHandle {
		self.ct_handle
	}
}