// SPDX-FileCopyrightText: 2025 Slatian
//
// SPDX-License-Identifier: LGPL-3.0-only

//! Combines multiple criteria.

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

pub mod builder;

#[cfg(feature = "rusqlite")]
mod rusqlite;

mod collect_as_chain;
mod direct_match;
mod list;

pub use self::collect_as_chain::CollectAsChain;
pub use self::list::CriteriumChainList;

use crate::LogicPath;

/// The CriteriumChain is a container for combining multiple criteria using boolean logic.
///
/// The `T` type is usually an enum that states the valid fields
/// and which Criterium can be used on them.
///
/// Multiple chains nested inside each other form a tree, the nodes that do not contain other chain nodes are referred to as "leafes".
///
/// Ways of constructing a criterium chain:
/// * Using the enum directly
/// * [`CriteriumChainBuilder`] for constructing the `And` and `Or` variants.
/// * [`CriteriumChainList`] (`as_and()` and `as_or()`) for `And` and `Or` variants.
/// * From an iterator using [`CollectAsChain`].
/// * Using a `CriteriumChain::Match(…).and(…)` construct.
/// * Using [`constant(…)`][Self::constant].
///
/// [`CriteriumChainBuilder`]: crate::chain::builder::CriteriumChainBuilder
/// [`CriteriumChainList`]: CriteriumChainList
/// [`CollectAsChain`]: CollectAsChain
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum CriteriumChain<T> {
	/// Chains multiple criteria together and matches when all of them match.
	/// The boolean value is the fallback for when no Criterium has been set.
	///
	/// Usually the children are `CriteriumChain::Match` and `CriteriumChain::Not`,
	/// but all other values are valid too.
	///
	/// Is called `and` in serialized from.
	And(CriteriumChainList<T>),

	/// Like `And` but matches when at least one of the criteria matches.
	///
	/// Is called `or` in serialized form.
	Or(CriteriumChainList<T>),

	/// Inverts a given chain, intended for building `Nand` and `Nor` statements.
	///
	/// Is called `not` in serialized form.
	#[cfg_attr(feature = "serde", serde(alias = "not_chain", rename = "not"))]
	NotChain(Box<Self>),

	/// Always matches, equivalent to a `true` in other languages.
	///
	/// Is called `always` in serialzed form, can also be constructed from a boolean.
	#[cfg_attr(feature = "serde", serde(rename = "always"))]
	MatchAlways,

	/// Never matches, equivalent to a `flase` in other languages.
	///
	/// Is called `never` in serialized form, can also be constructed from a boolean.
	#[cfg_attr(feature = "serde", serde(rename = "never"))]
	MatchNever,

	/// Inverts the contained Criterium.
	///
	/// Is called `not_match` in serialized form.
	#[cfg_attr(feature = "serde", serde(rename = "not_match"))]
	Not(T),

	/// Passes the contained Criterium as is.
	///
	/// Is untagged and therefore invisible in serialized form. In its place is the serialized argument directly.
	#[cfg_attr(feature = "serde", serde(untagged))]
	Match(T),

	/// Passes the contained Criterium as is.
	///
	/// Tags the criterium with how likely it is that it matches.
	///
	/// This can help optimizing query plans.
	///
	/// In serialized from this is directly represented by a table containing `likelihood` and `matcher` keys.
	#[cfg_attr(feature = "serde", serde(untagged))]
	WithLikelihood {
		/// Passed criterium
		#[cfg_attr(feature = "serde", serde(rename = "match"))]
		matcher: Box<Self>,

		/// Likelihood of `criterium` returning a `true` value.
		///
		/// Should be between `0.0` and `1.0`,
		/// but implementations using this value should not assume that it falls into this range.
		///
		/// Inverting is done by calculating `1.0 -likelihood`.
		///
		/// See also [get_known_likelihood()][Self::get_known_likelihood].
		likelihood: f32,
	},
}

impl<T> CriteriumChain<T> {
	/// Negates the Chain result.
	///
	/// Similar to a `not` or `!` in other languages.
	///
	/// This wraps the CriteriumChain in a `CriteriumChain::NotChain`
	/// or unwraps one layer if already wrapped in a `CriteriumChain::NotChain`.
	pub fn invert(self) -> Self {
		match self {
			CriteriumChain::NotChain(chain) => *chain,
			CriteriumChain::Match(c) => CriteriumChain::Not(c),
			CriteriumChain::Not(c) => CriteriumChain::Match(c),
			CriteriumChain::MatchAlways => CriteriumChain::MatchNever,
			CriteriumChain::MatchNever => CriteriumChain::MatchAlways,
			chain => CriteriumChain::NotChain(Box::new(chain)),
		}
	}

	/// Condidinally applies the [invert()][CriteriumChain::invert] method.
	pub fn invert_if(self, invert: bool) -> Self {
		if invert {
			self.invert()
		} else {
			self
		}
	}

	/// Apply a likelihood value to this node in the chain.
	///
	/// This is the equivalent to the [likelihood() function in SQLite](https://sqlite.org/lang_corefunc.html#likelihood).
	pub fn with_likelihood(self, likelihood: f32) -> Self {
		match self {
			// Replace an exiting likelyhood
			Self::WithLikelihood { matcher, .. } => Self::WithLikelihood {
				matcher,
				likelihood,
			},
			// Bubble up a not-chain
			Self::NotChain(chain) => Self::NotChain(chain.with_likelihood(1.0 - likelihood).into()),
			// For everything else just wrap it
			chain => Self::WithLikelihood {
				matcher: chain.into(),
				likelihood,
			},
		}
	}

	/// Shorthand for `with_likelihood(0.9375)`.
	///
	/// This is the equivalent to the [likely() function in SQLite](https://sqlite.org/lang_corefunc.html#likely).
	pub fn likely(self) -> Self {
		self.with_likelihood(0.9375)
	}

	/// Shorthand for `with_likelihood(0.0625)`.
	///
	/// This is the equivalent to the [unlikely() function in SQLite](https://sqlite.org/lang_corefunc.html#unlikely).
	pub fn unlikely(self) -> Self {
		self.with_likelihood(0.0625)
	}

	/// Returns the `likelihood` (see [WithLikelihood][Self::WithLikelihood])
	/// of this node in the chain clamped between `0.0` and `1.0`.
	///
	/// It is **not guaranteed** to resturn a value,
	/// not even when called on a `WithLikelihood`.
	/// This is because an `f32` isn't always a number.
	///
	/// * It resolves Not-chains which directly contain a mtcher with likelihood.
	/// * `MatchAlways` resolves to a likelihood of `1.0`
	/// * `MatchNever` resolves to a likelihood of `0.0`
	/// * `And` and `Or` connections always resolve to `None` as without knowlege about the dta there is no way to calculate a useful likelyhood.
	pub fn get_known_likelihood(&self) -> Option<f32> {
		match self {
			Self::WithLikelihood { likelihood, .. } => {
				if likelihood.is_nan() {
					None
				} else {
					Some(likelihood.clamp(0.0, 1.0))
				}
			}
			Self::NotChain(chain) => chain.get_known_likelihood().map(|l| 1.0 - l),
			Self::MatchAlways => Some(1.0),
			Self::MatchNever => Some(0.0),
			_ => None,
		}
	}

	/// Construct a constant criterium chain
	/// that is eiter `MatchAlways` or `MatchNever`
	/// according to the value of `value`.
	///
	/// This matches the implementation of the `From<bool>` trait.
	pub fn constant(value: bool) -> Self {
		value.into()
	}

	/// Add a criterium to this chain using an `and` operation.
	///
	/// If the chain is already an and-chain the criterium will be added to the existing chain.
	///
	/// All other kinds of chain will wrap themselves in a new and-chain that has the exiting chain and the new criterium as members and a fallback of `false`.
	pub fn and(self, criterium: T) -> Self {
		self.and_chain(Self::Match(criterium))
	}

	/// Same as [and()][Self::and], but for adding a chain.
	///
	/// If both chains are and-chains this appends the criteria of the `other_chain` to `self`, keeping the fallback value of `self`.
	///
	/// For all other cases it behaves like [and()][Self::and].
	pub fn and_chain(self, other_chain: Self) -> Self {
		match self {
			Self::And(mut list) => {
				match other_chain {
					Self::And(mut other) => {
						list.criteria.append(&mut other.criteria);
					}
					_ => {
						list.criteria.push(other_chain);
					}
				}
				Self::And(list)
			}
			_ => Self::And(CriteriumChainList {
				criteria: vec![self, other_chain],
				fallback: false,
			}),
		}
	}

	/// Add a criterium to this chain using an `or` operation.
	///
	/// If the chain is already an or-chain the criterium will be added to the existing chain.
	///
	/// All other kinds of chain will wrap themselves in a new or-chain that has the exiting chain or the new criterium as members or a fallback of `false`.
	pub fn or(self, criterium: T) -> Self {
		self.or_chain(Self::Match(criterium))
	}

	/// Same as [or()][Self::or], but for adding a chain.
	///
	/// If both chains are or-chains this appends the criteria of the `other_chain` to `self`, keeping the fallback value of `self`.
	///
	/// For all other cases it behaves like [or()][Self::or].
	pub fn or_chain(self, other_chain: Self) -> Self {
		match self {
			Self::Or(mut list) => {
				match other_chain {
					Self::Or(mut other) => {
						list.criteria.append(&mut other.criteria);
					}
					_ => {
						list.criteria.push(other_chain);
					}
				}
				Self::Or(list)
			}
			_ => Self::Or(CriteriumChainList {
				criteria: vec![self, other_chain],
				fallback: false,
			}),
		}
	}

	/// Translates the criteria in one criterium chain into
	/// another using the provided translation function.
	///
	/// This will **not** allow you to alter the structure of the chain itself though.
	///
	/// Note: if there is already an `From` implemented for the desired
	/// target use the [chain_into()][ChainInto::chain_into] function.
	pub fn translate<F, O>(self, func: &F) -> CriteriumChain<O>
	where
		F: Fn(T) -> O,
	{
		match self {
			Self::And(list) => CriteriumChain::And(list.map(|c| c.translate(func))),
			Self::Or(list) => CriteriumChain::Or(list.map(|c| c.translate(func))),
			Self::NotChain(chain) => CriteriumChain::NotChain(Box::new(chain.translate(func))),
			Self::WithLikelihood {
				matcher,
				likelihood,
			} => CriteriumChain::WithLikelihood {
				matcher: Box::new(matcher.translate(func)),
				likelihood,
			},
			Self::Not(c) => CriteriumChain::Not(func(c)),
			Self::Match(c) => CriteriumChain::Match(func(c)),
			Self::MatchAlways => CriteriumChain::MatchAlways,
			Self::MatchNever => CriteriumChain::MatchNever,
		}
	}

	/// Version of [translate()][Self::translate()] that also tracks the [LogicPath] taken to the leaf of a chain tree.
	pub fn translate_with_structure<F, O>(
		self,
		func: &F,
		path: Option<LogicPath>,
	) -> CriteriumChain<O>
	where
		F: Fn(T, LogicPath) -> O,
	{
		let path = path.unwrap_or_else(LogicPath::new_root).traverse(&self);
		match self {
			Self::And(list) => {
				CriteriumChain::And(list.map(|c| c.translate_with_structure(func, Some(path))))
			}
			Self::Or(list) => {
				CriteriumChain::Or(list.map(|c| c.translate_with_structure(func, Some(path))))
			}
			Self::NotChain(chain) => {
				CriteriumChain::NotChain(Box::new(chain.translate_with_structure(func, Some(path))))
			}
			Self::WithLikelihood {
				matcher,
				likelihood,
			} => CriteriumChain::WithLikelihood {
				matcher: Box::new(matcher.translate_with_structure(func, Some(path))),
				likelihood,
			},
			Self::Not(c) => CriteriumChain::Not(func(c, path)),
			Self::Match(c) => CriteriumChain::Match(func(c, path)),
			Self::MatchAlways => CriteriumChain::MatchAlways,
			Self::MatchNever => CriteriumChain::MatchNever,
		}
	}

	/// Like [translate_with_structure()][Self::translate_with_structure], but allows the callback `func` to return a whole criterium chain that the leaf will be replaced with.
	///
	/// * The existing structure of the chain itself is preserved.
	/// * `Match` leaves will be replaced by the callback result.
	/// * `Not` leaves will be replaced by the callback result inverted (using [invert()][Self::invert])
	/// * Always returning a `CriteriumChain::Match(…)` is equivalent to using [translate_with_structure()][Self::translate_with_structure]
	pub fn translate_to_chain<F, O>(self, func: &F, path: Option<LogicPath>) -> CriteriumChain<O>
	where
		F: Fn(T, LogicPath) -> CriteriumChain<O>,
	{
		let path = path.unwrap_or_else(LogicPath::new_root).traverse(&self);
		match self {
			Self::And(list) => {
				CriteriumChain::And(list.map(|c| c.translate_to_chain(func, Some(path))))
			}
			Self::Or(list) => {
				CriteriumChain::Or(list.map(|c| c.translate_to_chain(func, Some(path))))
			}
			Self::NotChain(chain) => {
				CriteriumChain::NotChain(Box::new(chain.translate_to_chain(func, Some(path))))
			}
			Self::WithLikelihood {
				matcher,
				likelihood,
			} => CriteriumChain::WithLikelihood {
				matcher: Box::new(matcher.translate_to_chain(func, Some(path))),
				likelihood,
			},
			Self::Not(c) => func(c, path).invert(),
			Self::Match(c) => func(c, path),
			Self::MatchAlways => CriteriumChain::MatchAlways,
			Self::MatchNever => CriteriumChain::MatchNever,
		}
	}

	/// Iterates over all leaves of this tree and calls `func` with the leaf data and the [LogicPath] leading to the leaf as argument.
	///
	/// If no initial logic path is given the function assumes it was called on the root node.
	pub fn for_each_leaf<F>(&self, func: &mut F, path: Option<LogicPath>)
	where
		F: FnMut(&T, LogicPath),
	{
		let path = path.unwrap_or_else(LogicPath::new_root).traverse(self);
		match self {
			Self::And(list) => list.for_each(|c| c.for_each_leaf(func, Some(path))),
			Self::Or(list) => list.for_each(|c| c.for_each_leaf(func, Some(path))),
			Self::NotChain(chain) => chain.for_each_leaf(func, Some(path)),
			Self::WithLikelihood { matcher, .. } => matcher.for_each_leaf(func, Some(path)),
			Self::Not(c) | Self::Match(c) => func(c, path),
			Self::MatchAlways | Self::MatchNever => { /* noop */ }
		}
	}

	/// If the chain result can be known without comparing against anything this will retzrn the constant value it would evaluate to.
	///
	/// If it has at least one `Match` or `Not` node it returns `None` as these can't be evaluated for being constant yet.
	pub fn evaluate_constant(&self) -> Option<bool> {
		match self {
			Self::And(list) => {
				if list.is_empty() {
					return Some(list.fallback);
				}
				for chain in &list.criteria {
					match chain.evaluate_constant() {
						None => return None,
						Some(true) => { /* Not significant */ }
						Some(false) => return Some(false),
					}
				}
				Some(true)
			}
			Self::Or(list) => {
				if list.is_empty() {
					return Some(list.fallback);
				}
				for chain in &list.criteria {
					match chain.evaluate_constant() {
						None => return None,
						Some(true) => return Some(true),
						Some(false) => { /* Not significant */ }
					}
				}
				return Some(false);
			}
			Self::NotChain(chain) => chain.evaluate_constant().map(|b| !b),
			Self::WithLikelihood { matcher, .. } => matcher.evaluate_constant(),
			Self::MatchAlways => Some(true),
			Self::MatchNever => Some(false),
			// Can't evaluate leaves
			Self::Not(_) | Self::Match(_) => None,
		}
	}

	/// Returns whether this chain would evaliate to a constant value.
	pub fn is_constant(&self) -> bool {
		self.evaluate_constant().is_some()
	}

	/// If the chain is an `and` or `or` chain it retrns wheter the list of criteria is empty.
	///
	/// Variants that contain exactly one other chain (`NotChain`, `WithLikelihood`) will pass on the return value of our inner chain.
	///
	/// Other variants will always return `false`.
	pub fn is_empty(&self) -> bool {
		match self {
			Self::And(list) | Self::Or(list) => list.criteria.is_empty(),
			Self::Not(_) | Self::Match(_) => false,
			Self::NotChain(chain) => chain.is_empty(),
			Self::WithLikelihood { matcher, .. } => matcher.is_empty(),
			Self::MatchAlways | Self::MatchNever => false,
		}
	}
}

impl<T> From<Option<CriteriumChain<T>>> for CriteriumChain<T> {
	fn from(chain_opt: Option<CriteriumChain<T>>) -> Self {
		match chain_opt {
			Some(cc) => cc,
			None => CriteriumChain::MatchAlways,
		}
	}
}

impl<T> From<bool> for CriteriumChain<T> {
	fn from(value: bool) -> Self {
		match value {
			true => CriteriumChain::MatchAlways,
			false => CriteriumChain::MatchNever,
		}
	}
}

/// Makes a Chains convertible from one Criterium type to another
pub trait ChainInto<T, F>
where
	T: From<F>,
{
	/// Converts one type of chain into another
	fn chain_into(self) -> CriteriumChain<T>;
}

impl<T, F> ChainInto<T, F> for CriteriumChain<F>
where
	T: From<F>,
{
	fn chain_into(self) -> CriteriumChain<T> {
		self.translate(&Into::into)
	}
}

#[cfg(all(test, feature = "serde"))]
mod test_serde {
	use super::*;
	use crate::direct_match::DirectMatch;
	use crate::string::StringCriterium;
	use crate::CriteriumChainBuilder;
	use serde_json;

	#[test]
	fn deserialize() {
		let data = r#"{
			"and": [
				{ "not": { "equals": "foobar" } },
				{ "not": { "equals": "foo" } },
				{ "has_prefix": "foo" },
				{ "likelihood": 0.3, "match": { "not": { "equals": "bar" } } },
				"always",
				{ "or": ["always"] }
			]
		}"#;
		let res: CriteriumChain<StringCriterium> = serde_json::from_str(data).unwrap();

		// Be lazy, string compare against debug formatter output …
		assert_eq!(
			format!("{:?}",res),
			"And(CriteriumChainList { criteria: [NotChain(Match(Equals(\"foobar\"))), NotChain(Match(Equals(\"foo\"))), Match(HasPrefix(\"foo\")), WithLikelihood { matcher: NotChain(Match(Equals(\"bar\"))), likelihood: 0.3 }, MatchAlways, Or(CriteriumChainList { criteria: [MatchAlways], fallback: false })], fallback: false })".to_string()
		);

		// Test if the criterium is functional using direct match checks:
		assert_eq!(res.evaluate_constant(), None);
		assert!(res.criterium_match("foo_bar"));
		assert!(res.criterium_match("foothing"));
		assert!(res.criterium_match("foobaz"));
		assert!(!res.criterium_match("foobar"));
		assert!(!res.criterium_match("foo"));
		assert!(!res.criterium_match("fo"));
	}

	#[test]
	fn serialize() {
		let mut builder = CriteriumChainBuilder::and(false);
		builder.add_criterium_inverted(StringCriterium::Equals("foo".to_string()));
		builder.add_criterium(StringCriterium::HasPrefix("foo".to_string()));
		builder.add_chain(CriteriumChain::MatchAlways);
		builder.add_chain(CriteriumChain::Or(CriteriumChainList::empty(true)));
		assert_eq!(
			serde_json::to_string(&builder.to_chain()).unwrap(),
			r#"{"and":[{"not_match":{"equals":"foo"}},{"has_prefix":"foo"},"always",{"or":["always"]}]}"#.to_string()
		);
	}

	/// This tests if the translate methods behave like NoOps if they are configured as such.
	#[test]
	fn translate_preserve() {
		let data = r#"{
			"and": [
				{ "not": { "equals": "foobar" } },
				{ "not": { "equals": "foo" } },
				{ "has_prefix": "foo" },
				{ "likelihood": 0.3, "match": { "not": { "equals": "bar" } } },
				"always",
				{ "or": ["always"] }
			]
		}"#;
		let res: CriteriumChain<StringCriterium> = serde_json::from_str(data).unwrap();

		let debugged = "And(CriteriumChainList { criteria: [NotChain(Match(Equals(\"foobar\"))), NotChain(Match(Equals(\"foo\"))), Match(HasPrefix(\"foo\")), WithLikelihood { matcher: NotChain(Match(Equals(\"bar\"))), likelihood: 0.3 }, MatchAlways, Or(CriteriumChainList { criteria: [MatchAlways], fallback: false })], fallback: false })".to_string();

		assert_eq!(format!("{:?}", res), debugged);

		assert_eq!(format!("{:?}", res.clone().translate(&|t| t)), debugged);

		assert_eq!(
			format!(
				"{:?}",
				res.clone().translate_with_structure(&|t, _| t, None)
			),
			debugged
		);

		assert_eq!(
			format!(
				"{:?}",
				res.translate_to_chain(&|t, _| CriteriumChain::Match(t), None)
			),
			debugged
		);
	}
}
