use std::rc::Rc;

use rug::Float;

use super::{constant::Constant, set::Set, Error, Node, NodeEnum, Precedence};

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

impl Node for Divide {
    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()) {
            // Error if dividing by zero
            (_, NodeEnum::Constant(zero)) if zero.get_value() == &0.0 => {
                Err(Error::DivideByZero)
            }

            (_, _) if evaluated_left == evaluated_right => {
                Ok(Rc::new(Constant::new_from_float(0.0, &env).into()))
            }

            // Zero rule
            (NodeEnum::Constant(zero), _) if zero.get_value() == &0.0 => Ok(evaluated_left),

            // Cancel out symbols and constants
            (NodeEnum::Multiply(m1), NodeEnum::Multiply(m2)) => {
                match (
                    (m1.get_left().as_ref(), m1.get_right().as_ref()),
                    (m2.get_left().as_ref(), m2.get_right().as_ref()),
                ) {
                    ((NodeEnum::Symbol(s1), o1), (NodeEnum::Symbol(s2), o2))
                    | ((o1, NodeEnum::Symbol(s1)), (NodeEnum::Symbol(s2), o2))
                    | ((NodeEnum::Symbol(s1), o1), (o2, NodeEnum::Symbol(s2)))
                    | ((o1, NodeEnum::Symbol(s1)), (o2, NodeEnum::Symbol(s2))) => {
                        if s1 == s2 {
                            Divide::new(Rc::new(o1.clone()), Rc::new(o2.clone())).evaluate(env)
                        } else {
                            Ok(Divide::new_rc(evaluated_left, evaluated_right))
                        }
                    }

                    _ => Ok(Rc::new(Divide::new(evaluated_left, evaluated_right).into())),
                }
            }

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

            // Divide a set with a constant
            (NodeEnum::Set(s), NodeEnum::Constant(c)) => {
                let old_values = s.get_values();
                let mut values = Vec::with_capacity(old_values.len());
                let c: Rc<NodeEnum> = Rc::new(c.clone().into());

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

                Ok(Set::new(values))
            }
            // Divide a set with a symbol
            (NodeEnum::Set(s), NodeEnum::Symbol(c)) => {
                let old_values = s.get_values();
                let mut values = Vec::with_capacity(old_values.len());
                let c: Rc<NodeEnum> = Rc::new(c.clone().into());

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

                Ok(Set::new(values))
            }
            _ => Ok(Rc::new(Divide::new(evaluated_left, evaluated_right).into())),
        }
    }

    fn as_string(&self, env: Option<&super::Environment>) -> String {
        let left_string = if self.left.precedence() <= self.precedence() {
            format!("({})", self.left.as_string(env))
        } else {
            self.left.as_string(env)
        };
        let right_string = 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 Divide {
    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()
    }
}
