use std::{fmt::Display, rc::Rc};

use add::Add;
use assign::Assign;
use call::Call;
use closure::Closure;
use comparison::{Greater, GreaterEquals, Less, LessEquals};
use constant::Constant;
use divide::Divide;
use empty::Empty;
use enum_dispatch::enum_dispatch;
use equals::Equals;
use exponent::Exponent;
use function::Function;
use if_else::{Bool, IfElse};
use r#loop::{Break, Loop};
use multiply::Multiply;
use range::Range;
use set::Set;
use string_node::StringNode;
use subtract::Subtract;
use symbol::Symbol;

use crate::environment::Environment;

pub mod add;
pub mod assign;
pub mod call;
pub mod closure;
pub mod comparison;
pub mod constant;
pub mod divide;
pub mod empty;
pub mod equals;
pub mod exponent;
pub mod function;
pub mod if_else;
pub mod r#loop;
pub mod range;
pub mod multiply;
pub mod node_ref;
pub mod set;
pub mod string_node;
pub mod subtract;
pub mod symbol;

#[enum_dispatch]
#[enum_dispatch(Debug, Clone, PartialEq, PartialOrd, ToString)]
#[derive(Debug, Clone, PartialEq)]
pub enum NodeEnum {
    Constant,
    StringNode,

    // Operators
    Add,
    Subtract,
    Multiply,
    Divide,
    Exponent,

    Symbol,
    Assign,
    Empty,
    Function,
    Closure,
    Call,

    Loop,
    Break,

    Bool,
    IfElse,

    Set,
    Range,

    Equals,
    Greater,
    GreaterEquals,
    Less,
    LessEquals,
    // Logical operators
    // In,
    // Where,
    // Not,
    // Or,
    // And
}

#[derive(PartialEq, Eq, PartialOrd, Ord)]
pub enum Precedence {
    Empty,
    Assign,
    Compare,
    Equality,
    Term,
    Factor,
    Unary,
    Exponent,
    Call,
    Primary,
}

pub enum Error {
    Break,
    StackOverflow,
    StackUnderflow,
    /// Expected, Got
    ArgumentCount(usize, usize),
    MismatchedType(String),

    ConversionError(String),

    IllegalAssignment(String),
    IllegalRhsAssignment(String),
    EmptySetAssignment,
    /// Expected, Got
    SetAssignmentCountMismatch(usize, usize),

    DivideByZero,
    /// Lenght, Index
    IndexOutOfBounds(usize, usize),

    Other(String),
}

impl ToString for Error {
    fn to_string(&self) -> String {
        match self {
            Error::Break => "Invalid break statement".to_owned(),
            Error::StackOverflow => "Stack Overflow".to_owned(),
            Error::StackUnderflow => "Stack Underflow".to_owned(),
            Error::ArgumentCount(exp, got) => {
                format!("Expected {} arguments, but got {}", exp, got)
            }
            Error::EmptySetAssignment => "Cannot assign to an empty Set".to_owned(),
            Error::SetAssignmentCountMismatch(exp, got) => {
                format!("Cannot define {} symbols from a Set of {} values", exp, got)
            }
            Error::DivideByZero => "Division by Zero!".to_owned(),
            Error::IndexOutOfBounds(exp, got) => {
                format!("Index {} out of bounds for Set of size {}", got, exp)
            }

            Error::MismatchedType(s)
            | Error::ConversionError(s)
            | Error::IllegalAssignment(s)
            | Error::IllegalRhsAssignment(s)
            | Error::Other(s) => s.to_owned(),
        }
    }
}

#[enum_dispatch(NodeEnum)]
pub trait Node {
    fn evaluate(&self, _: &mut Environment) -> Result<Rc<NodeEnum>, Error>;
    fn as_string(&self, _: Option<&Environment>) -> String;
    fn precedence(&self) -> Precedence;
}

impl Display for NodeEnum {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", Node::as_string(self, None))
    }
}

impl PartialOrd for NodeEnum {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        match (self, other) {
            (NodeEnum::Constant(constant1), NodeEnum::Constant(constant2)) => {
                constant1.partial_cmp(constant2)
            }

            (NodeEnum::Add(add1), NodeEnum::Add(add2)) => add1.partial_cmp(add2),
            (NodeEnum::Subtract(subtract1), NodeEnum::Subtract(subtract2)) => {
                subtract1.partial_cmp(subtract2)
            }
            (NodeEnum::Multiply(multiply1), NodeEnum::Multiply(multiply2)) => {
                multiply1.partial_cmp(multiply2)
            }
            (NodeEnum::Divide(divide1), NodeEnum::Divide(divide2)) => divide1.partial_cmp(divide2),
            (NodeEnum::Exponent(exponent1), NodeEnum::Exponent(exponent2)) => {
                exponent1.partial_cmp(exponent2)
            }

            (NodeEnum::Symbol(symbol1), NodeEnum::Symbol(symbol2)) => symbol1.partial_cmp(symbol2),
            (NodeEnum::Assign(assign1), NodeEnum::Assign(assign2)) => assign1.partial_cmp(assign2),
            (NodeEnum::Empty(empty1), NodeEnum::Empty(empty2)) => empty1.partial_cmp(empty2),
            (NodeEnum::Function(function1), NodeEnum::Function(function2)) => {
                function1.partial_cmp(function2)
            }
            (NodeEnum::Call(call1), NodeEnum::Call(call2)) => call1.partial_cmp(call2),

            (NodeEnum::Bool(bool1), NodeEnum::Bool(bool2)) => bool1.partial_cmp(bool2),
            (NodeEnum::IfElse(if_else1), NodeEnum::IfElse(if_else2)) => {
                if_else1.partial_cmp(if_else2)
            }

            (NodeEnum::Set(set1), NodeEnum::Set(set2)) => set1.partial_cmp(set2),

            (NodeEnum::Equals(equals1), NodeEnum::Equals(equals2)) => equals1.partial_cmp(equals2),
            (NodeEnum::Greater(greater1), NodeEnum::Greater(greater2)) => {
                greater1.partial_cmp(greater2)
            }
            (
                NodeEnum::GreaterEquals(greater_equals1),
                NodeEnum::GreaterEquals(greater_equals2),
            ) => greater_equals1.partial_cmp(greater_equals2),
            (NodeEnum::Less(less1), NodeEnum::Less(less2)) => less1.partial_cmp(less2),
            (NodeEnum::LessEquals(less_equals1), NodeEnum::LessEquals(less_equals2)) => {
                less_equals1.partial_cmp(less_equals2)
            }

            // (a,b) => a.partial_cmp(b)
            _ => None,
        }
    }
}

impl NodeEnum {
    pub fn type_str(&self) -> String {
        match self {
            NodeEnum::Constant(_) => "Constant",
            NodeEnum::StringNode(_) => "String",
            NodeEnum::Add(_) => "Add",
            NodeEnum::Subtract(_) => "Subtract",
            NodeEnum::Multiply(_) => "Multiply",
            NodeEnum::Divide(_) => "Divide",
            NodeEnum::Exponent(_) => "Exponent",
            NodeEnum::Symbol(_) => "Symbol",
            NodeEnum::Assign(_) => "Assign",
            NodeEnum::Empty(_) => "Empty",
            NodeEnum::Function(_) => "Function",
            NodeEnum::Closure(_) => "Closure",
            NodeEnum::Call(_) => "Call",
            NodeEnum::Bool(_) => "Bool",
            NodeEnum::IfElse(_) => "If",
            NodeEnum::Set(_) => "Set",
            NodeEnum::Equals(_) => "Equals",
            NodeEnum::Greater(_) => "Greater",
            NodeEnum::GreaterEquals(_) => "Greater Equals",
            NodeEnum::Less(_) => "Less",
            NodeEnum::LessEquals(_) => "Less Equals",
            NodeEnum::Loop(_) => "Loop",
            NodeEnum::Break(_) => "Break",
            NodeEnum::Range(_) => "Range",
        }
        .to_owned()
    }
}
