use std::{fmt::Display, panic, path::PathBuf};

use itertools::Itertools;
use rinja::Template;
use serde::{Deserialize, Deserializer};

use crate::create_file;

#[derive(Debug, Deserialize, PartialEq, Eq)]
struct Board {
	order: u32,
	spirit: String,
	routes: Vec<Route>,
	abilities: Vec<Ability>,

	#[serde(skip_deserializing)]
	total_lp: u32,
	#[serde(skip_deserializing)]
	max_level: u32,

	#[serde(skip_deserializing)]
	stats: Vec<(usize, String)>,
}

impl Board {
	pub fn init_ability_help(&mut self) {
		for abl in &mut self.abilities {
			if abl.tip.is_none() && abl.route.is_some() {
				let route_id = abl.route.unwrap();

				let mut route = None;
				for r in &self.routes {
					if r.id == route_id {
						route = Some(r);
						break;
					}
				}

				if route_id < 10 && route.is_some() {
					abl.tip = Some(format!("Unlocks with disposition {}", route.unwrap().name));
				}
			}
		}
	}

	pub fn init_routes(&mut self) {
		self.routes.iter_mut().for_each(|r| {
			r.interaction = match r.color {
				Color::Blue => Interaction::Poke,
				Color::Purple => Interaction::Poke,
				Color::Yellow => Interaction::Rub,
				Color::Green => Interaction::Rub,

				_ => Interaction::None,
			}
		});

		let routes = self
			.routes
			.clone()
			.into_iter()
			.filter(|r| r.id < 100)
			.collect_vec();

		self.routes.iter_mut().filter(|r| r.id < 100).for_each(|r| {
			r.tips.iter_mut().for_each(|t| {
				let route = routes.iter().find(|r| r.name == t.to);
				if let Some(route) = route {
					t.to_color = route.color.clone();
					t.tip = format!("{} {}", route.interaction, t.tip);
				}
			});
		});
	}

	pub fn init_stats(&mut self) {
		let v = self
			.abilities
			.iter()
			.filter(|&ability| ability.r#type == AbilityType::Stat)
			.sorted_by(|a, b| Ord::cmp(&a.name, &b.name))
			.collect_vec();

		let mut stats: Vec<(usize, String)> = vec![];
		for (key, chunk) in &v.into_iter().chunk_by(|k| k.name.clone()) {
			let grouped = chunk.collect_vec();
			stats.push((grouped.len(), key));
		}

		self.stats = stats;
	}

	pub fn init_total_lp(&mut self) {
		self.total_lp = self
			.abilities
			.iter()
			.filter(|&ability| ability.price.contains("LP"))
			.map(|ability| {
				let mut split = ability.price.split_whitespace();

				split.next().unwrap_or("0").parse::<u32>().unwrap_or(0)
			})
			.sum::<u32>();
	}

	pub fn init_max_level(&mut self) {
		self.max_level = self
			.abilities
			.iter()
			.filter(|&ability| ability.price.starts_with("Level"))
			.map(|ability| {
				let mut split = ability.price.split_whitespace();

				split.nth(1).unwrap_or("0").parse::<u32>().unwrap_or(0)
			})
			.sorted()
			.last()
			.unwrap_or(0);
	}

	pub fn get_size(&self) -> BoardPosition {
		let mut x = 1;
		let mut y = 1;
		for ability in &self.abilities {
			if ability.pos.0 > x {
				x = ability.pos.0;
			}

			if ability.pos.1 > y {
				y = ability.pos.1;
			}
		}

		(x, y)
	}

	pub fn get_ability_at(&self, x: &u32, y: &u32) -> Option<&Ability> {
		self.abilities
			.iter()
			.find(|&ability| ability.pos == (*x, *y))
	}

	pub fn get_char(&self, i: &u32) -> char {
		char::from_u32(64 + *i).unwrap_or('0')
	}

	pub fn get_dispositions(&self) -> Vec<&Route> {
		self.routes.iter().filter(|r| r.id < 100).collect_vec()
	}

	pub fn get_supports(&self) -> Vec<&Ability> {
		self.abilities
			.iter()
			.filter(|&ability| ability.r#type == AbilityType::Support)
			.collect_vec()
	}

	pub fn get_commands(&self) -> Vec<&Ability> {
		self.abilities
			.iter()
			.filter(|&ability| {
				ability.r#type == AbilityType::Magic
					|| ability.r#type == AbilityType::Attack
					|| ability.r#type == AbilityType::Reprisal
					|| ability.r#type == AbilityType::Defense
			})
			.collect_vec()
	}

	pub fn get_spirits(&self) -> Vec<&Ability> {
		self.abilities
			.iter()
			.filter(|&ability| ability.r#type == AbilityType::Spirit)
			.collect_vec()
	}
}

#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
struct Route {
	id: u32,
	name: String,
	color: Color,
	tips: Vec<Tip>,
	#[serde(skip_deserializing)]
	interaction: Interaction,
}

#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
enum Color {
	#[serde(alias = "blue")]
	Blue,
	#[serde(alias = "purple")]
	Purple,
	#[serde(alias = "yellow")]
	Yellow,
	#[serde(alias = "green")]
	Green,

	#[serde(alias = "secret1")]
	SecretGreen,
	#[serde(alias = "secret2")]
	SecretRed,
}

impl Display for Color {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match self {
			Color::Blue => f.write_str("blue"),
			Color::Purple => f.write_str("purple"),
			Color::Yellow => f.write_str("yellow"),
			Color::Green => f.write_str("green"),

			Color::SecretGreen => f.write_str("secret1"),
			Color::SecretRed => f.write_str("secret2"),
		}
	}
}

#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
struct Tip {
	to: String,
	#[serde(skip_deserializing, default = "default_disposition_color")]
	to_color: Color,
	tip: String,
}

fn default_disposition_color() -> Color {
	Color::Blue
}

#[derive(Debug, Deserialize, PartialEq, Eq)]
enum AbilityType {
	Start,
	Checkpoint,
	Secret,
	Stat,
	Spirit,
	Support,
	Attack,
	Magic,
	Reprisal,
	Defense,
}

type BoardPosition = (u32, u32);

#[derive(Debug, Deserialize, PartialEq, Eq)]
struct ParseBoardPositionError;

fn deserialize_position<'de, D>(deserializer: D) -> Result<BoardPosition, D::Error>
where
	D: Deserializer<'de>,
{
	let str = match String::deserialize(deserializer) {
		Ok(v) => v,
		Err(e) => {
			panic!("Tried deserializing a non-string type\nerror: {}", e);
		}
	};
	let str = str.to_uppercase();

	let mut chars = str.chars();

	let a = chars.next().unwrap_or('A');
	let b = chars.next().unwrap_or('1');

	let a = (a.to_ascii_uppercase() as u32).saturating_sub(64);
	if a == 0 {
		panic!("Second position parameter {} is 0 or lower!", str);
	}

	let b = b.to_digit(10).unwrap_or(0);

	Ok((a, b))
}

#[derive(Debug, Deserialize, PartialEq, Eq)]
struct Ability {
	name: String,
	#[serde(deserialize_with = "deserialize_position")]
	pos: BoardPosition,
	r#type: AbilityType,
	price: String,
	route: Option<u32>,
	path: Vec<Direction>,
	tip: Option<String>,
}

impl Ability {
	pub fn tip(&self) -> String {
		self.tip.clone().unwrap_or_default()
	}

	pub fn get_slot_details(&self, board: &Board) -> String {
		let mut details = String::new();
		if let Some(route) = self.route {
			for broute in &board.routes {
				if broute.id == route {
					details += &format!("{} ", broute.color);
					break;
				}
			}
		}

		for path in &self.path {
			match path {
				Direction::North => details += "north ",
				Direction::South => details += "south ",
				Direction::East => details += "east ",
				Direction::West => details += "west ",
			}
		}

		if self.r#type == AbilityType::Start {
			details += "start ";
		}

		details
	}
}

#[derive(Debug, Deserialize, PartialEq, Eq)]
enum Direction {
	#[serde(alias = "N")]
	North,
	#[serde(alias = "S")]
	South,
	#[serde(alias = "E")]
	East,
	#[serde(alias = "W")]
	West,
}

#[derive(Debug, Default, Deserialize, PartialEq, Eq, Clone)]
enum Interaction {
	#[default]
	None,
	Poke,
	Rub,
}

impl Display for Interaction {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match self {
			Interaction::Poke => f.write_str("Poke"),
			Interaction::Rub => f.write_str("Rub"),
			_ => f.write_str(""),
		}
	}
}

#[derive(Template)]
#[template(path = "pages/ddd/boards.html")]
struct AbilitiesTemplate {
	pub boards: Vec<Board>,
}

const ABILITIES_PATH: &str = "./input/ddd/abilities";

pub fn init() {
	tracing::info!("Loading ability boards data from {}", ABILITIES_PATH);
	let mut boards: Vec<Board> = vec![];
	// Loading multiple files into one vector due to the size of each board
	let paths = std::fs::read_dir(ABILITIES_PATH)
		.unwrap()
		.filter_map(|f| f.ok())
		.map(|f| f.path())
		.filter_map(|p| match p.extension().map_or(false, |e| e == "toml") {
			true => Some(p),
			false => None,
		})
		.collect::<Vec<PathBuf>>();

	for path in paths {
		let board_str = std::fs::read_to_string(path).unwrap();
		let mut board = toml::from_str::<Board>(&board_str).unwrap();

		board.init_routes();
		board.init_total_lp();
		board.init_max_level();
		board.init_stats();
		board.init_ability_help();

		// dbg!(&board);
		boards.push(board);
	}
	boards.sort_by(|a, b| a.order.cmp(&b.order));

	tracing::info!("Generating the DDD ability boards template");
	let boards_template = AbilitiesTemplate { boards };

	create_file("./out/ddd", "boards", boards_template.render().unwrap()).unwrap();
}
