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

/// 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 std::fmt::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)]
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 {
		let matched = 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,
		};

		matched
	}
}

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
#[repr(transparent)]
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: [i32; TILE_GRID_SIZE]) -> Self {
		TileLayout(values.map(Some))
	}

	/// Construct a filled grid of identical tile data
	pub fn spread(value: i32) -> Self {
		TileLayout([Some(value); 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 line in lines.iter() {
            	for value in line.iter() {
					if let Some(value) = value {
						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$} ", "#")?;
					}
                }
                write!(f, "\n")?;
			}
			write!(f, "\n")
		} else {
			writeln!(f, "{:?}", self.0)
		}
	}
}

impl <T> TryFrom<&[T]> for TileLayout where T: IntoTile + Copy + Default + Debug {
	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)
		}
	}
}

/// Holds parsed tile rules that are used to evaluate a layout
#[derive(Clone, Copy)]
#[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)?;
				}
				write!(f, "\n")?;
			}
			writeln!(f, "\n")
		} else {
			write!(f, "TileMatcher({:?})", self.0)
		}
	}
}

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

impl TileMatcher {
	pub const fn single(value: TileStatus) -> Self {
		let mut rules = [TileStatus::Ignore; TILE_GRID_SIZE];
		rules[GRID_CENTER] = value;
		TileMatcher(rules)
	}


	/// Create a 1x1 matcher, with any rule for the target tile
	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`
	pub fn from_ldtk_array(value: Vec<i64>) -> Option<Self> {
		if is_square(value.len()) {
			Some(Self(transpose(value.as_slice()).map(TileStatus::from)))
		} else {
			None
		}
	}
}

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)
        }
	}
}

/// 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));
	}
}