use std::{cmp::Ordering, ops::Mul, rc::Rc};

use rug::Float;

use crate::environment::Environment;

use super::{
    add::Add, constant::Constant, divide::Divide, set::Set, subtract::Subtract, Error, Node, NodeEnum, Precedence
};

#[derive(Clone, Debug, PartialEq, PartialOrd)]
pub struct Multiply {
    left: Rc<NodeEnum>,
    right: Rc<NodeEnum>,
}

impl Node for Multiply {
    fn evaluate(&self, env: &mut super::Environment) -> Result<Rc<super::NodeEnum>, Error> {
        let evaluated_left = self.left.evaluate(env)?;
        let evaluated_right = self.right.evaluate(env)?;

        match (evaluated_left.as_ref(), evaluated_right.as_ref()) {
            // Zero rule
            (NodeEnum::Constant(zero), _) | (_, NodeEnum::Constant(zero))
                if zero.get_value() == &0.0 =>
            {
                Ok(Rc::new(Constant::new_from_float(0.0, &env).into()))
            }

            // Identity rule
            (x, NodeEnum::Constant(one)) | (NodeEnum::Constant(one), x)
                if one.get_value() == &1.0 =>
            {
                Ok(Rc::new(x.clone()))
            }

            // Multiply into parenthesis (add)
            (NodeEnum::Add(add), x) | (x, NodeEnum::Add(add)) => Ok(Add::new_rc(
                Multiply::new_rc(Rc::new(x.clone()), add.get_left()).evaluate(env)?,
                Multiply::new_rc(Rc::new(x.clone()), add.get_right()).evaluate(env)?,
            )),

            // Multiply into parenthesis (sub)
            (NodeEnum::Subtract(sub), x) | (x, NodeEnum::Subtract(sub)) => Ok(Subtract::new_rc(
                Multiply::new_rc(Rc::new(x.clone()), sub.get_left()).evaluate(env)?,
                Multiply::new_rc(Rc::new(x.clone()), sub.get_right()).evaluate(env)?,
            )),

            // Multiply fraction
            (x, NodeEnum::Divide(div)) | (NodeEnum::Divide(div), x) => Ok(Divide::new_rc(
                Self::new_rc(Rc::new(x.clone()), div.get_left()).evaluate(env)?,
                div.get_right(),
            )),

            // 0.5*n -> n/2
            (x, NodeEnum::Constant(c)) | (NodeEnum::Constant(c), x) if c.get_value() == &0.5 => {
                Divide::new(
                    Rc::new(x.clone()),
                    Rc::new(Constant::new_from_float(2, &env).into()),
                )
                .evaluate(env)
            }

            // Constant * Constant = Constant
            (NodeEnum::Constant(a), NodeEnum::Constant(b)) => {
                Ok(Rc::new(NodeEnum::from(Constant::new(Float::with_val_64(
                    env.get_float_precision(),
                    a.get_value() * b.get_value(),
                )))))
            }

            // Multiply into sets
            (NodeEnum::Set(s), c) | (c, NodeEnum::Set(s)) => {
                let old_values = s.get_values();
                let mut values = Vec::with_capacity(old_values.len());
                let c: Rc<NodeEnum> = Rc::new(c.clone());

                for value in old_values.iter() {
                    let new_value = Multiply::new(c.clone(), value.clone());
                    values.push(new_value.evaluate(env)?);
                }

                Ok(Set::new(values))
            }

            (NodeEnum::Symbol(s1), NodeEnum::Symbol(s2)) => {
                if s1 > s2 {
                    Ok(Self::new_rc(evaluated_left, evaluated_right))
                } else {
                    Ok(Self::new_rc(evaluated_right, evaluated_left))
                }
            }

            // At least one of the nodes is a nested multiply
            (NodeEnum::Multiply(m), o) | (o, NodeEnum::Multiply(m)) => {
                // Aggregate into a single vector for easy sorting
                let mut aggregate = vec![];
                let mut queue = vec![];
                queue.push(Rc::new(o.clone()));
                let mut c: Rc<NodeEnum> = Rc::new(m.clone().into());

                loop {
                    match c.as_ref() {
                        NodeEnum::Multiply(m) => {
                            queue.push(m.get_left());
                            queue.push(m.get_right());
                        }
                        _ => {
                            aggregate.push(c.clone());
                        }
                    }

                    if !queue.is_empty() {
                        c = queue.pop().unwrap();
                    } else {
                        break;
                    }
                }

                aggregate.sort_by(|a, b| match (a.as_ref(), b.as_ref()) {
                    (_, NodeEnum::Constant(_)) => Ordering::Less,
                    (NodeEnum::Constant(_), _) => Ordering::Greater,
                    (NodeEnum::Symbol(s1), NodeEnum::Symbol(s2)) => s1.cmp(s2),
                    _ => a.partial_cmp(b).unwrap_or(Ordering::Equal)
                });

                let mut constant = Rc::new(Constant::new_from_float(1.0, &env).into());
                let mut constanted = false;

                while let Some(agr) = aggregate.last() {
                    if let NodeEnum::Constant(_) = agr.as_ref() {
                        constanted = true;
                        constant =
                            Multiply::new_rc(constant, aggregate.pop().unwrap()).evaluate(env)?;
                    } else {
                        break;
                    }
                }

                let mut out = aggregate.pop().unwrap();

                while let Some(a) = aggregate.pop() {
                    out = Multiply::new_rc(out, a);
                }

                if constanted {
                    Ok(Multiply::new_rc(constant, out))
                } else {
                    Ok(out)
                }
            }

            (_, NodeEnum::Constant(_)) => {
                Ok(Multiply::new_rc(evaluated_right, evaluated_left))
            }

            // Default to returning with left and right evaluated
            _ => Ok(Rc::new(
                Multiply::new(evaluated_left, evaluated_right).into(),
            )),
        }
    }

    fn as_string(&self, env: Option<&super::Environment>) -> String {
        let left_string = if let NodeEnum::Multiply(_) = self.left.as_ref() {
            self.left.as_string(env)
        } else if self.left.precedence() <= self.precedence() {
            format!("({})", self.left.as_string(env))
        } else {
            self.left.as_string(env)
        };
        let right_string = if let NodeEnum::Multiply(_) = self.right.as_ref() {
            self.right.as_string(env)
        } else if self.right.precedence() <= self.precedence() {
            format!("({})", self.right.as_string(env))
        } else {
            self.right.as_string(env)
        };
        format!("{}*{}", left_string, right_string)
    }

    fn precedence(&self) -> Precedence {
        Precedence::Factor
    }
}

impl Multiply {
    pub fn symbols_match(&self, other: &Multiply) -> bool {
        if let NodeEnum::Constant(_) = self.left.as_ref() {
            if let NodeEnum::Constant(_) = other.left.as_ref() {
                return self.right.eq(&other.right);
            }
        }

        self.eq(other)
    }
}

impl Multiply {
    pub fn new(left: Rc<NodeEnum>, right: Rc<NodeEnum>) -> Self {
        Self { left, right }
    }

    pub fn new_rc(left: Rc<NodeEnum>, right: Rc<NodeEnum>) -> Rc<NodeEnum> {
        Rc::new(Self::new(left, right).into())
    }

    pub fn get_left(&self) -> Rc<NodeEnum> {
        self.left.clone()
    }

    pub fn get_right(&self) -> Rc<NodeEnum> {
        self.right.clone()
    }
}
