use std::{cmp::Ordering, str::FromStr};

use anyhow::bail;
use serde_with::DeserializeFromStr;

#[derive(Debug, PartialEq, Eq)]
pub enum ConditionOp {
	Or,
	And,
	Not,

	// Only used in `build()`
	Space,
	LeftParen,
	RightParen,
	Unknown,

	// Only used in `eval()`
	Term(String),
}

impl ConditionOp {
	pub fn new(c: char) -> Self {
		match c {
			'|' => Self::Or,
			'&' => Self::And,
			'!' => Self::Not,

			'(' => Self::LeftParen,
			')' => Self::RightParen,
			_ if c.is_ascii_whitespace() => Self::Space,
			_ => Self::Unknown,
		}
	}

	fn prec(&self) -> u8 {
		match self {
			Self::Or => 1,
			Self::And => 2,
			Self::Not => 3,
			_ => 0,
		}
	}
}

impl PartialOrd for ConditionOp {
	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
		use Ordering::*;

		match self.prec().cmp(&other.prec()) {
			// Keep repeated `!` right-associative by making `! >= !` false.
			Equal if matches!((self, other), (Self::Not, Self::Not)) => None,
			ordering => Some(ordering),
		}
	}
}

#[derive(Debug, DeserializeFromStr)]
pub struct Condition {
	ops: Vec<ConditionOp>,
}

impl FromStr for Condition {
	type Err = anyhow::Error;

	fn from_str(expr: &str) -> Result<Self, Self::Err> {
		let cond = Self::build(expr);
		if cond.eval(|_| true).is_none() {
			bail!("Invalid condition: {expr}");
		}

		Ok(cond)
	}
}

impl Condition {
	fn build(expr: &str) -> Self {
		let mut stack: Vec<ConditionOp> = vec![];
		let mut output: Vec<ConditionOp> = vec![];

		let mut chars = expr.chars().peekable();
		while let Some(token) = chars.next() {
			let op = ConditionOp::new(token);
			match op {
				ConditionOp::Or | ConditionOp::And | ConditionOp::Not => {
					while matches!(stack.last(), Some(last) if last >= &op) {
						output.push(stack.pop().unwrap());
					}
					stack.push(op);
				}
				ConditionOp::Space => continue,
				ConditionOp::LeftParen => stack.push(op),
				ConditionOp::RightParen => {
					while matches!(stack.last(), Some(last) if last != &ConditionOp::LeftParen) {
						output.push(stack.pop().unwrap());
					}
					stack.pop();
				}
				ConditionOp::Unknown => {
					let mut s = String::from(token);
					while matches!(chars.peek(), Some(&c) if ConditionOp::new(c) == op) {
						s.push(chars.next().unwrap());
					}
					output.push(ConditionOp::Term(s));
				}
				ConditionOp::Term(_) => unreachable!(),
			}
		}

		while let Some(op) = stack.pop() {
			output.push(op);
		}

		Self { ops: output }
	}

	pub fn eval(&self, f: impl Fn(&str) -> bool) -> Option<bool> {
		let mut stack: Vec<bool> = Vec::with_capacity(self.ops.len());
		for op in &self.ops {
			match op {
				ConditionOp::Or => {
					let b = stack.pop()? | stack.pop()?;
					stack.push(b);
				}
				ConditionOp::And => {
					let b = stack.pop()? & stack.pop()?;
					stack.push(b);
				}
				ConditionOp::Not => {
					let b = !stack.pop()?;
					stack.push(b);
				}
				ConditionOp::Term(s) => {
					stack.push(f(s));
				}
				_ => return None,
			}
		}

		if stack.len() == 1 { Some(stack[0]) } else { None }
	}
}

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

	#[test]
	fn test_condition_not() -> anyhow::Result<()> {
		let cond: Condition = "!dir".parse()?;
		assert!(!cond.eval(|s| s == "dir").unwrap());
		assert!(cond.eval(|_| false).unwrap());

		let cond: Condition = "!!dir".parse()?;
		assert!(cond.eval(|s| s == "dir").unwrap());
		assert!(!cond.eval(|_| false).unwrap());

		Ok(())
	}
}
