use std::rc::Rc;

use super::{Environment, Error, Node, NodeEnum, Precedence, empty::Empty};

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

impl Node for Assign {
    fn evaluate(&self, env: &mut Environment) -> Result<Rc<NodeEnum>, Error> {
        match self.left.as_ref() {
            NodeEnum::Symbol(symbol) => {
                let name = env
                    .id_to_str(&symbol.get_value())
                    .expect("Unknown symbol")
                    .clone();
                let right = self.right.evaluate(env)?;

                env.insert(symbol.get_value(), right.clone(), !self.shadow);
                Ok(Empty::new(format!(
                    "{name} {} {}",
                    if self.shadow { ":=" } else { "<-" },
                    right.as_string(Some(env))
                )))
            }

            NodeEnum::Set(set) => {
                // Check if every element of the set is a symbol
                let values = set.get_values();

                if values.len() == 0 {
                    return Err(Error::EmptySetAssignment);
                }

                let mut symbols = Vec::with_capacity(values.len());
                for node in values {
                    if let NodeEnum::Symbol(symbol) = node.as_ref() {
                        symbols.push(symbol);
                    } else {
                        return Err(Error::IllegalAssignment(format!(
                            "Cannot assign to a {}",
                            node.type_str()
                        )));
                    }
                }

                // Evaluate right hand side and check that it is a set of the same size
                let right = self.right.evaluate(env)?;

                if let NodeEnum::Set(set) = right.as_ref() {
                    let values = set.get_values();
                    if values.len() != symbols.len() {
                        return Err(Error::SetAssignmentCountMismatch(
                            symbols.len(),
                            values.len(),
                        ));
                    }

                    for (symbol, value) in symbols.iter().zip(values.into_iter()) {
                        env.insert(symbol.get_value(), value.clone(), !self.shadow);
                    }

                    Ok(Empty::new(format!(
                        "[{}] {} {}",
                        if self.shadow { ":=" } else { "<-" },
                        symbols
                            .iter()
                            .map(|x| env.id_to_str(&x.get_value()).unwrap().clone())
                            .reduce(|x, acc| format!("{x}, {acc}"))
                            .unwrap(),
                        right.as_string(Some(env))
                    )))
                } else {
                    return Err(Error::IllegalRhsAssignment(format!(
                        "Cannot unpack {} for assignment",
                        right.type_str()
                    )));
                }
            }

            _ => Err(Error::IllegalAssignment(format!(
                "Cannot assign to a {}",
                self.left.type_str()
            ))),
        }
    }

    fn as_string(&self, env: Option<&Environment>) -> String {
        format!(
            "{} {} {}",
            self.left.as_string(env),
            if self.shadow { ":=" } else { "<-" },
            self.right.as_string(env)
        )
    }

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

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

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

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