use crate::output::TileOutput;
use crate::{TileLayout, TileMatcher};
use std::fmt::{Debug, Formatter};
use std::ops::Add;

/// Checks tile layouts against a matcher instance, and uses the output to produce a value
#[derive(Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AutoTileRule {
	/// The pattern that this rule will use for matching
	pub matcher: TileMatcher,
	/// The value produced when this rule gets matched
	pub output: TileOutput,
	/// When used as part of a set of rules, this value (0.0 - 1.0) determines the chance that
	/// a successful match will generate an output from this rule
	pub chance: f32,
}

impl Debug for AutoTileRule {
	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
		if f.alternate() {
			// Perform grid style formatting for matcher value
			write!(
				f,
				"AutoTileRule \n{:#?}output: {:?}, chance: {:.2}",
				self.matcher, self.output, self.chance
			)
		} else {
			write!(
				f,
				"AutoTileRule {{ matcher: {:?}, output: {:?}, chance: {:.2} }}",
				self.matcher, self.output, self.chance
			)
		}
	}
}

impl AutoTileRule {
	/// Create a rule that will always produce `output_value` when the target tile matches
	/// `input_value`
	pub const fn exact(input_value: i32, output_value: i32) -> Self {
		Self::exact_chance(input_value, output_value, 1.0)
	}

	/// Create a rule that will produce `output_value` when the target tile matches
	/// `input_value` and the selection chance is rolled under the value of `chance` (0.0 to 1.0)
	pub const fn exact_chance(input_value: i32, output_value: i32, chance: f32) -> Self {
		AutoTileRule {
			matcher: TileMatcher::single_match(input_value),
			output: TileOutput::single(output_value),
			chance,
		}
	}

	/// Create a rule that will always produce `output_value` when `matcher` evaluates to
	/// `true`
	pub const fn single_when(matcher: TileMatcher, output_value: i32) -> Self {
		AutoTileRule {
			matcher,
			output: TileOutput::single(output_value),
			chance: 1.0,
		}
	}

	/// Create a rule that will always produce one of the values contained in `output_value`
	/// when the target tile matches `input_value`
	pub const fn single_any(input_value: i32, output_value: Vec<i32>) -> Self {
		Self::single_any_chance(input_value, output_value, 1.0)
	}

	/// Create a rule that will produce one of the values contained in `output_value`
	/// when the target tile matches `input_value` and the selection chacne is rolled under the
	/// value of `chance` (0.0 to 1.0)
	pub const fn single_any_chance(input_value: i32, output_value: Vec<i32>, chance: f32) -> Self {
		AutoTileRule {
			matcher: TileMatcher::single_match(input_value),
			output: TileOutput::any(output_value),
			chance,
		}
	}

	/// Create a rule that will produce one of the values contained in `output_value`
	/// when `matcher` evaluates to `true` and the selection chance is rolled under
	/// the value of `chance` (0.0 to 1.0)
	pub const fn any_any_chance(
		input_value: TileMatcher,
		output_value: Vec<i32>,
		chance: f32,
	) -> Self {
		AutoTileRule {
			matcher: input_value,
			output: TileOutput::any(output_value),
			chance,
		}
	}

	/// Evaluate this rule and return the unresolved output value. "None" represents either no
	/// match or a match that failed its chance roll.
	///
	/// Will use a default randomly seeded RNG to evaluate the chance roll for this rule
	#[cfg(feature = "impl_fastrand")]
	pub fn get_match(&self, input: &TileLayout) -> Option<&TileOutput> {
		let chance = fastrand::f32();

		if chance <= self.chance && self.matcher.matches(input) {
			Some(&self.output)
		} else {
			None
		}
	}

	/// Evaluate this rule and return the unresolved output value. "None" represents either no
	/// match or a match that failed its chance roll.
	///
	/// Will use the provided RNG to evaluate the chance roll for this rule
	#[cfg(feature = "impl_fastrand")]
	pub fn get_match_seeded(
		&self,
		input: &TileLayout,
		seeded: &fastrand::Rng,
	) -> Option<&TileOutput> {
		let chance = seeded.f32();

		if chance <= self.chance && self.matcher.matches(input) {
			Some(&self.output)
		} else {
			None
		}
	}

	/// Evaluate this rule and produce an output, if a match is found. "None" represents either
	/// no match, a match that resolved to `TileOutput::Skip`, or a match that failed its chance
	/// roll.
	///
	/// Will use a default randomly seeded RNG to select from a list, if the output resolves to
	/// a random selection
	#[cfg(feature = "impl_fastrand")]
	pub fn resolve_match(&self, input: &TileLayout) -> Option<i32> {
		self.get_match(input).and_then(|out| out.resolve())
	}

	/// Evaluate this rule and produce an output, if a match is found. "None" represents either
	/// no match, a match that resolved to `TileOutput::Skip`, or a match that failed its chance
	/// roll.
	///
	/// Will use the provided RNG to select from a list, if the output resolves to
	/// a random selection
	#[cfg(feature = "impl_fastrand")]
	pub fn resolve_match_seeded(&self, input: &TileLayout, seeded: &fastrand::Rng) -> Option<i32> {
		self.get_match_seeded(input, seeded)
			.and_then(|out| out.resolve_with(seeded))
	}
}

/// Holds a list of rules, for efficiently evaluating a tile layout against multiple exclusive rules.
/// Rules will be evaluated in the order they are added to the set, and will stop evaluating when
/// a match is found
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AutoRuleSet(pub Vec<AutoTileRule>);

impl Add<AutoRuleSet> for AutoRuleSet {
	type Output = AutoRuleSet;

	/// Combine two AutoRuleSet values, where the rules in the right hand side
	/// will be appended to the end of the set represented by the left hand
	/// side
	fn add(self, rhs: AutoRuleSet) -> Self::Output {
		AutoRuleSet([self.0.as_slice(), rhs.0.as_slice()].concat())
	}
}

impl From<AutoTileRule> for AutoRuleSet {
	/// Create a rule set from a single rule
	///
	/// ```rust
	/// # use micro_autotile::AutoRuleSet;
	/// use micro_autotile::AutoTileRule;
	///
	/// fn main() {
	/// use micro_autotile::TileLayout;
	/// let rule_set: AutoRuleSet = AutoTileRule::exact(1, 2).into();
	///
	/// assert_eq!(rule_set.resolve_match(&TileLayout::single(1)), Some(2));
	/// # }
	/// ```
	fn from(value: AutoTileRule) -> Self {
		Self(vec![value])
	}
}

impl From<Vec<AutoTileRule>> for AutoRuleSet {
	/// Convert a set of rules into a rule set
	///
	/// ```rust
	/// # use micro_autotile::AutoRuleSet;
	/// use micro_autotile::AutoTileRule;
	///
	/// fn main() {
	/// use micro_autotile::TileLayout;
	/// let rule_set: AutoRuleSet = vec![
	///   AutoTileRule::exact(1, 2),
	///   AutoTileRule::exact(5123, 231)
	/// ].into();
	///
	/// assert_eq!(rule_set.resolve_match(&TileLayout::single(1)), Some(2));
	/// # }
	/// ```
	fn from(value: Vec<AutoTileRule>) -> Self {
		Self(value)
	}
}

impl AutoRuleSet {
	/// Evaluate this set of rules and return the unresolved output value from the first match.
	/// A return value of `None` means that no rules have matched.
	///
	/// Will use a default randomly seeded RNG to evaluate the chance roll for each matching rule
	#[cfg(feature = "impl_fastrand")]
	pub fn get_match(&self, input: &TileLayout) -> Option<&TileOutput> {
		for rule in self.0.iter() {
			let result = rule.get_match(input);
			if result.is_some() {
				return result;
			}
		}
		None
	}

	/// Evaluate this set of rules and return the unresolved output value from the first match.
	/// A return value of `None` means that no rules have matched, or all matching results failed
	/// their chance roll or resolved to `TileOutput::Skip`.
	///
	/// Will use the provided RNG to evaluate the chance roll for each matching rule
	#[cfg(feature = "impl_fastrand")]
	pub fn get_match_seeded(
		&self,
		input: &TileLayout,
		seeded: &fastrand::Rng,
	) -> Option<&TileOutput> {
		for rule in self.0.iter() {
			let result = rule.get_match_seeded(input, seeded);
			if result.is_some() {
				return result;
			}
		}
		None
	}

	/// Evaluate this set of rules and produce an output, if a match is found.
	/// A return value of `None` means that no rules have matched, or all matching results failed
	/// their chance roll or resolved to `TileOutput::Skip`.
	///
	/// Will use a default randomly seeded RNG to select from a list, if the output resolves to
	/// a random selection
	#[cfg(feature = "impl_fastrand")]
	pub fn resolve_match(&self, input: &TileLayout) -> Option<i32> {
		self.get_match(input).and_then(|out| out.resolve())
	}

	/// Evaluate this set of rules and produce an output, if a match is found.
	/// A return value of `None` means that no rules have matched, or all matching results failed
	/// their chance roll or resolved to `TileOutput::Skip`.
	///
	/// Will use the provided RNG to select from a list, if the output resolves to
	/// a random selection
	#[cfg(feature = "impl_fastrand")]
	pub fn resolve_match_seeded(&self, input: &TileLayout, seeded: &fastrand::Rng) -> Option<i32> {
		self.get_match_seeded(input, seeded)
			.and_then(|out| out.resolve_with(seeded))
	}
}
