use crate::utils::IntoTile;
use std::fmt::{Debug, Formatter};

/// The size of the grid that can be matched; equal to the length of one side of the square grid
const RULE_MAGNITUDE: usize = 7;
/// Number of array entries required to store all the data for a grid
const TILE_GRID_SIZE: usize = RULE_MAGNITUDE.pow(2);
/// The index of the center tile in the grid
const GRID_CENTER: usize = (TILE_GRID_SIZE - 1) / 2;

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct NotSquareError;
impl std::fmt::Display for NotSquareError {
	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
		write!(f, "Input is not a square grid")
	}
}
impl std::error::Error for NotSquareError {}

/// Represents how a single tile location should be matched when evaluating a rule
#[derive(Ord, PartialOrd, Eq, PartialEq, Hash, Default, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(into = "i64", from = "i64"))]
pub enum TileStatus {
	/// This tile will always match, regardless of the input tile
	#[default]
	Ignore,
	/// This tile will only match when there is no input tile (`None`)
	Nothing,
	/// This tile will always match as long as the tile exists (`Option::is_some`)
	Anything,
	/// This tile will match as long as the input tile exists and the input value is the same as this value
	Is(i32),
	/// This tile will match as long as the input tile exists and the input value is anything other than this value
	IsNot(i32),
}

impl Debug for TileStatus {
	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
		match self {
			Self::Ignore => write!(f, "Any Or No Tile"),
			Self::Nothing => write!(f, "No Tile"),
			Self::Anything => write!(f, "Any Tile"),
			Self::Is(value) => write!(f, "Must Match [{}]", value),
			Self::IsNot(value) => write!(f, "Must Not Match [{}]", value),
		}
	}
}

impl PartialEq<Option<i32>> for TileStatus {
	fn eq(&self, other: &Option<i32>) -> bool {
		match self {
			Self::Ignore => true,
			Self::Nothing => other.is_none(),
			Self::Anything => other.is_some(),
			Self::Is(value) => &Some(*value) == other,
			Self::IsNot(value) => &Some(*value) != other,
		}
	}
}

impl TileStatus {
	#[deprecated(since = "0.2.0", note = "Use `TileStatus::into` directly instead")]
	pub fn to_ldtk_value(self) -> i64 {
		self.into_tile() as i64
	}

	#[deprecated(since = "0.2.0", note = "Use `TileStatus::from` directly instead")]
	pub fn from_ldtk_value(value: i64) -> Self {
		Self::from(value)
	}
}

/// Holds a grid of raw input data, as a more ideal format for interop and storage
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from="Vec<TileStatus>", into="Vec<TileStatus>"))]
#[repr(transparent)]
#[derive(Clone, Copy)]
pub struct TileLayout(pub [Option<i32>; TILE_GRID_SIZE]);

impl TileLayout {
	/// Create a 1x1 grid of tile data
	pub fn single(value: i32) -> Self {
		let mut grid = [None; TILE_GRID_SIZE];
		grid[GRID_CENTER] = Some(value);
		TileLayout(grid)
	}

	/// Construct a filled grid of tile data
	pub fn filled(values: [impl IntoTile; TILE_GRID_SIZE]) -> Self {
		TileLayout(values.map(|val| Some(val.into_tile())))
	}

	/// Construct a filled grid of identical tile data
	pub fn spread(value: impl IntoTile) -> Self {
		TileLayout([Some(value.into_tile()); TILE_GRID_SIZE])
	}

	/// Filter the layout data so that it only contains the tiles surrounding the target tile. This
	/// means that the array index of every entry before the center point will match the original data
	/// array, but ever entry after the center point will have its index shifted down by 1
	///
	/// The main utility of this is to perform set operations on every tile _other_ than the target tile.
	///
	/// ## Examples
	///
	/// ```
	/// # use micro_autotile::TileLayout;
	/// let layout = TileLayout::single(123);
	/// let has_any_surrounding_tiles = layout.surrounding().iter().any(|tile| tile.is_some());
	///
	/// assert_eq!(has_any_surrounding_tiles, false);
	/// ```
	pub fn surrounding(&self) -> [Option<i32>; 8] {
		[
			self.0[0], self.0[1], self.0[2], self.0[3], self.0[5], self.0[6], self.0[7], self.0[8],
		]
	}
}

impl Debug for TileLayout {
	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
		if f.alternate() {
			let lines = as_lines(&self.0);
			let mut max_width = 1;

			for value in lines.iter().copied().flatten().flatten() {
				max_width = max_width.max(value.to_string().len());
			}

			for line in lines {
				for value in line {
					if let Some(value) = value {
						write!(f, "{:^max_width$?} ", value)?;
					} else {
						write!(f, "{:^max_width$} ", "#")?;
					}
				}
				writeln!(f)?;
			}
			writeln!(f)
		} else {
			writeln!(f, "{:?}", self.0)
		}
	}
}

impl<T> TryFrom<&[T]> for TileLayout
where
	T: IntoTile + Copy + Default,
{
	type Error = NotSquareError;
	fn try_from(value: &[T]) -> Result<Self, Self::Error> {
		if is_square(value.len()) {
			let formatted = transpose(value);
			Ok(Self(formatted.map(|t| {
				if t.into_tile() == 0 {
					None
				} else {
					Some(t.into_tile())
				}
			})))
		} else {
			Err(NotSquareError)
		}
	}
}

impl TryFrom<Vec<TileStatus>> for TileLayout {
    type Error = NotSquareError;

    fn try_from(value: Vec<TileStatus>) -> Result<Self, Self::Error> {
        if is_square(value.len()) {
            let formatted = transpose(value.as_slice());
            Ok(Self(formatted.map(|t| {
                if t.into_tile() == 0 {
                    None
                } else {
                    Some(t.into_tile())
                }
            })))
        } else {
            Err(NotSquareError)
        }    }
}

impl From<TileLayout> for Vec<TileStatus> {
    fn from(value: TileLayout) -> Self {
        value.0.iter().map(|val| (*val).into()).collect()
    }
}

/// Holds parsed tile rules that are used to evaluate a layout
#[derive(Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from="Vec<TileStatus>", into="Vec<TileStatus>"))]
#[repr(transparent)]
pub struct TileMatcher(pub [TileStatus; TILE_GRID_SIZE]);
impl Debug for TileMatcher {
	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
		if f.alternate() {
			let lines = as_lines(&self.0);
			writeln!(f, "Tile Matcher")?;
			for line in lines {
				for value in line {
					write!(f, "{:?} ", value)?;
				}
				writeln!(f)?;
			}
			writeln!(f)
		} else {
			write!(f, "TileMatcher({:?})", self.0)
		}
	}
}

impl Default for TileMatcher {
	fn default() -> Self {
		TileMatcher([TileStatus::default(); TILE_GRID_SIZE])
	}
}

impl TileMatcher {
	/// Create a matcher that only evaluates one rule against the central target tile
	///
	/// ## Examples
	///
	/// ```
	/// # use micro_autotile::{TileLayout, TileMatcher, TileStatus};
	/// let rule = TileStatus::IsNot(123);
	/// let matcher = TileMatcher::single(rule);
	///
	/// assert!(matcher.matches(&TileLayout::single(456)));
	/// assert!(matcher.matches(&TileLayout::spread(1)));
	/// ```
	pub const fn single(value: TileStatus) -> Self {
		let mut rules = [TileStatus::Ignore; TILE_GRID_SIZE];
		rules[GRID_CENTER] = value;
		TileMatcher(rules)
	}

	/// Create a matcher that only checks if the target tile matches the given value
	///
	/// ## Examples
	///
	/// ```
	/// # use micro_autotile::{TileLayout, TileMatcher};
	/// let matcher = TileMatcher::single_match(123);
	/// assert!(matcher.matches(&TileLayout::single(123)));
	/// ```
	pub const fn single_match(value: i32) -> Self {
		Self::single(TileStatus::Is(value))
	}

	/// Check if the given input layout of tile data conforms to this matcher
	pub fn matches(&self, layout: &TileLayout) -> bool {
		self.0
			.iter()
			.zip(layout.0.iter())
			.all(|(status, reality)| *status == *reality)
	}

	/// Load data from an LDTK JSON file. Supports arbitrary sized matchers for any square grid.
	/// Other sizes of matcher will result in `None`
	///
	/// ## Examples
	///
	/// A simple 1x1 LDTK Array
	///
	/// ```
	/// # use micro_autotile::{TileMatcher, TileLayout};
	/// let matcher = TileMatcher::from_ldtk_array(vec![1]).expect("Invalid input");
	/// assert!(matcher.matches(&TileLayout::single(1)));
	/// ```
	///
	/// A 3x3 matcher with more complex rules
	///
	/// ```
	/// # use micro_autotile::{TileMatcher, TileLayout};
	/// let rule_input: Vec<i64> = vec![
	///     100, -100, 100,
	///     0, 5, 0,
	///     0, 0, 0
	/// ];
	///
	/// let tile_section: [i32; 9] = [
	///     100, 5, 100,
	///     24, 5, 293,
	///     34, 21, 22
	/// ];
	///
	/// let matcher = TileMatcher::from_ldtk_array(rule_input).expect("Invalid input");
	/// let map_layout = TileLayout::try_from(tile_section.as_slice()).expect("Invalid input");
	/// assert!(matcher.matches(&map_layout));
	/// ```
	pub fn from_ldtk_array(value: Vec<i64>) -> Option<Self> {
		Self::try_from(value.as_slice()).ok()
	}
}

impl<T> TryFrom<&[T]> for TileMatcher
where
	T: IntoTile + Copy + Default,
{
	type Error = NotSquareError;
	fn try_from(value: &[T]) -> Result<Self, Self::Error> {
		if is_square(value.len()) {
			let formatted = transpose(value);
			Ok(Self(formatted.map(TileStatus::from)))
		} else {
			Err(NotSquareError)
		}
	}
}
impl TryFrom<&[TileStatus]> for TileMatcher {
	type Error = NotSquareError;
	fn try_from(value: &[TileStatus]) -> Result<Self, Self::Error> {
		if is_square(value.len()) {
			Ok(Self(transpose(value)))
		} else {
			Err(NotSquareError)
		}
	}
}

impl TryFrom<Vec<TileStatus>> for TileMatcher {
    type Error = NotSquareError;
    fn try_from(value: Vec<TileStatus>) -> Result<Self, Self::Error> {
        TileMatcher::try_from(value.as_slice())
    }
}

impl From<TileMatcher> for Vec<TileStatus> {
    fn from(value: TileMatcher) -> Self {
        value.0.to_vec()
    }
}

/// Convert a square grid of arbitrary size into one of the expected autotiler grid size (7x7). Odd
/// numbered grids will be centered in the output, while even numbered grids will be offset by 1 to
/// the top and left of the output.
///
/// This method will panic if the provided list of values is not a square grid
///
/// ## Example
///
/// For a 2x2 input grid:
///
/// ```text
/// 2 2
/// 2 2
/// ```
///
/// Applying this function will result in the following output grid:
///
/// ```text
/// 1 1 1 1 1 1 1
/// 1 1 1 1 1 1 1
/// 1 1 2 2 1 1 1
/// 1 1 2 2 1 1 1
/// 1 1 1 1 1 1 1
/// 1 1 1 1 1 1 1
/// 1 1 1 1 1 1 1
/// ```
///
/// ## Example
///
/// For a 3x3 input grid:
///
/// ```text
/// 3 3 3
/// 3 3 3
/// 3 3 3
/// ```
///
/// Applying this function will result in the following output grid:
///
/// ```text
/// 1 1 1 1 1 1 1
/// 1 1 1 1 1 1 1
/// 1 1 3 3 3 1 1
/// 1 1 3 3 3 1 1
/// 1 1 3 3 3 1 1
/// 1 1 1 1 1 1 1
/// 1 1 1 1 1 1 1
/// ```
fn transpose<Value>(input: &[Value]) -> [Value; TILE_GRID_SIZE]
where
	Value: Default + Copy,
{
	if input.len() == TILE_GRID_SIZE {
		match input.try_into() {
			Ok(output) => return output,
			Err(_) => {
				// This should never happen since we've already checked the length - just in case,
				// we want it to fall through and run the rest of the algorithm
			}
		}
	}

	if !is_square(input.len()) {
		// Length isn't square == it does not represent a square grid
		panic!("Input must be a square grid");
	}

	let input_size = (input.len() as f64).sqrt() as usize;
	let mut output = [Value::default(); TILE_GRID_SIZE];

	let output_start = if input_size < RULE_MAGNITUDE {
		// Even padding requires our initial x coord to be half the size difference from the edge.
		// Even sized squares will be offset to by one to the left and top, which is preferable to
		// rejecting even squares altogether
		(RULE_MAGNITUDE - input_size) / 2
	} else {
		0
	};

	let input_start = if input_size > RULE_MAGNITUDE {
		(input_size - RULE_MAGNITUDE) / 2
	} else {
		0
	};

	for x in 0..input_size {
		for y in 0..input_size {
			let adjusted_input_x = x + input_start;
			let adjusted_input_y = y + input_start;
			let input_idx = adjusted_input_y * input_size + adjusted_input_x;

			let adjusted_output_x = x + output_start;
			let adjusted_output_y = y + output_start;
			let output_idx = adjusted_output_y * RULE_MAGNITUDE + adjusted_output_x;

			output[output_idx] = input[input_idx];
		}
	}

	output
}

fn is_square(n: usize) -> bool {
	let sqrt_n = (n as f64).sqrt() as usize;
	n == sqrt_n.pow(2)
}

fn as_lines<T>(input: &[T]) -> Vec<&[T]> {
	input.chunks(RULE_MAGNITUDE).collect()
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn it_transposes_odd_grids() {
		let input = [2, 2, 2, 2, 2, 2, 2, 2, 2];
		let expected = [
			0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0,
			0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		];

		assert_eq!(expected, transpose(&input));
	}
}
