use crate::atom::{Atom, AtomView, InlineVar, Symbol};
use crate::coefficient::Coefficient;
use crate::domains::{float::Complex, integer::Integer, rational::Rational};
use crate::state::Workspace;
use numerica::domains::float::Float;

macro_rules! atom_op_views {
    ($lhs:expr, $op_ws_fn:ident, $rhs:expr) => {{
        Workspace::get_local().with(|ws| {
            let mut t = ws.new_atom();
            $lhs.$op_ws_fn(ws, $rhs, &mut t);
            t.into_inner()
        })
    }};
}

macro_rules! atom_op_num_rhs {
    ($lhs:expr, $op_ws_fn:ident, $rhs:expr) => {{
        Workspace::get_local().with(|ws| {
            let n = ws.new_num($rhs);
            let mut t = ws.new_atom();
            $lhs.$op_ws_fn(ws, n.as_view(), &mut t);
            t.into_inner()
        })
    }};
}

macro_rules! atom_op_num_lhs {
    ($lhs:expr, $op_ws_fn:ident, $rhs:expr) => {{
        Workspace::get_local().with(|ws| {
            let n = ws.new_num($lhs);
            let mut t = ws.new_atom();
            n.as_view().$op_ws_fn(ws, $rhs, &mut t);
            t.into_inner()
        })
    }};
}

impl std::ops::Neg for &Atom {
    type Output = Atom;
    fn neg(self) -> Atom {
        Workspace::get_local().with(|ws| {
            let mut t = ws.new_atom();
            self.as_view().neg_with_ws_into(ws, &mut t);
            t.into_inner()
        })
    }
}

impl std::ops::Neg for Atom {
    type Output = Atom;
    fn neg(self) -> Atom {
        -self.as_view()
    }
}

impl std::ops::Neg for AtomView<'_> {
    type Output = Atom;
    fn neg(self) -> Atom {
        Workspace::get_local().with(|ws| {
            let mut t = ws.new_atom();
            self.neg_with_ws_into(ws, &mut t);
            t.into_inner()
        })
    }
}

macro_rules! impl_binary_ops {
    ($op_trait:ident, $op_method:ident, $op_ws_fn:ident) => {
        impl std::ops::$op_trait<Atom> for Atom {
            type Output = Atom;
            fn $op_method(self, rhs: Atom) -> Atom {
                atom_op_views!(self.as_view(), $op_ws_fn, rhs.as_view())
            }
        }

        impl std::ops::$op_trait<Atom> for &Atom {
            type Output = Atom;
            fn $op_method(self, rhs: Atom) -> Atom {
                atom_op_views!(self.as_view(), $op_ws_fn, rhs.as_view())
            }
        }

        impl std::ops::$op_trait<&Atom> for Atom {
            type Output = Atom;
            fn $op_method(self, rhs: &Atom) -> Atom {
                atom_op_views!(self.as_view(), $op_ws_fn, rhs.as_view())
            }
        }

        impl std::ops::$op_trait<&Atom> for &Atom {
            type Output = Atom;
            fn $op_method(self, rhs: &Atom) -> Atom {
                atom_op_views!(self.as_view(), $op_ws_fn, rhs.as_view())
            }
        }

        impl<'a, 'b> std::ops::$op_trait<AtomView<'b>> for AtomView<'a> {
            type Output = Atom;
            fn $op_method(self, rhs: AtomView<'b>) -> Atom {
                atom_op_views!(self, $op_ws_fn, rhs)
            }
        }

        impl<'a> std::ops::$op_trait<AtomView<'a>> for Atom {
            type Output = Atom;
            fn $op_method(self, rhs: AtomView<'a>) -> Atom {
                atom_op_views!(self.as_view(), $op_ws_fn, rhs)
            }
        }

        impl std::ops::$op_trait<Symbol> for Atom {
            type Output = Atom;
            fn $op_method(self, rhs: Symbol) -> Atom {
                let rhs = InlineVar::new(rhs);
                atom_op_views!(self.as_view(), $op_ws_fn, rhs.as_view())
            }
        }

        impl std::ops::$op_trait<Symbol> for &Atom {
            type Output = Atom;
            fn $op_method(self, rhs: Symbol) -> Atom {
                let rhs = InlineVar::new(rhs);
                atom_op_views!(self.as_view(), $op_ws_fn, rhs.as_view())
            }
        }

        impl std::ops::$op_trait<Symbol> for AtomView<'_> {
            type Output = Atom;
            fn $op_method(self, rhs: Symbol) -> Atom {
                let rhs = InlineVar::new(rhs);
                atom_op_views!(self, $op_ws_fn, rhs.as_view())
            }
        }

        impl std::ops::$op_trait<Symbol> for Symbol {
            type Output = Atom;
            fn $op_method(self, rhs: Symbol) -> Atom {
                let lhs = InlineVar::new(self);
                let rhs = InlineVar::new(rhs);
                atom_op_views!(lhs.as_view(), $op_ws_fn, rhs.as_view())
            }
        }

        impl std::ops::$op_trait<Atom> for Symbol {
            type Output = Atom;
            fn $op_method(self, rhs: Atom) -> Atom {
                let lhs = InlineVar::new(self);
                atom_op_views!(lhs.as_view(), $op_ws_fn, rhs.as_view())
            }
        }

        impl std::ops::$op_trait<&Atom> for Symbol {
            type Output = Atom;
            fn $op_method(self, rhs: &Atom) -> Atom {
                let lhs = InlineVar::new(self);
                atom_op_views!(lhs.as_view(), $op_ws_fn, rhs.as_view())
            }
        }

        impl std::ops::$op_trait<AtomView<'_>> for Symbol {
            type Output = Atom;
            fn $op_method(self, rhs: AtomView<'_>) -> Atom {
                let lhs = InlineVar::new(self);
                atom_op_views!(lhs.as_view(), $op_ws_fn, rhs)
            }
        }

        impl<T: Into<Coefficient>> std::ops::$op_trait<T> for Symbol {
            type Output = Atom;
            fn $op_method(self, rhs: T) -> Atom {
                let lhs = InlineVar::new(self);
                atom_op_num_rhs!(lhs.as_view(), $op_ws_fn, rhs)
            }
        }

        impl<T: Into<Coefficient>> std::ops::$op_trait<T> for Atom {
            type Output = Atom;
            fn $op_method(self, rhs: T) -> Atom {
                atom_op_num_rhs!(self.as_view(), $op_ws_fn, rhs)
            }
        }

        impl<T: Into<Coefficient>> std::ops::$op_trait<T> for &Atom {
            type Output = Atom;
            fn $op_method(self, rhs: T) -> Atom {
                atom_op_num_rhs!(self.as_view(), $op_ws_fn, rhs)
            }
        }
    };
}

macro_rules! impl_numeric_lhs_symbol_ops {
    ($op_trait:ident, $op_method:ident, $op_ws_fn:ident, $($ty:ty),+ $(,)?) => {
        $(
            impl std::ops::$op_trait<Symbol> for $ty {
                type Output = Atom;
                fn $op_method(self, rhs: Symbol) -> Atom {
                    let rhs = InlineVar::new(rhs);
                    atom_op_num_lhs!(self, $op_ws_fn, rhs.as_view())
                }
            }
        )+
    };
}

macro_rules! impl_numeric_lhs_atom_ops {
    ($op_trait:ident, $op_method:ident, $op_ws_fn:ident, $($ty:ty),+ $(,)?) => {
        $(
            impl std::ops::$op_trait<Atom> for $ty {
                type Output = Atom;
                fn $op_method(self, rhs: Atom) -> Atom {
                    atom_op_num_lhs!(self, $op_ws_fn, rhs.as_view())
                }
            }

            impl std::ops::$op_trait<&Atom> for $ty {
                type Output = Atom;
                fn $op_method(self, rhs: &Atom) -> Atom {
                    atom_op_num_lhs!(self, $op_ws_fn, rhs.as_view())
                }
            }

            impl std::ops::$op_trait<AtomView<'_>> for $ty {
                type Output = Atom;
                fn $op_method(self, rhs: AtomView<'_>) -> Atom {
                    atom_op_num_lhs!(self, $op_ws_fn, rhs)
                }
            }
        )+
    };
}

macro_rules! impl_domain_lhs_atom_ops {
    ($op_trait:ident, $op_method:ident, $op_ws_fn:ident, $($ty:ty),+ $(,)?) => {
        $(
            impl std::ops::$op_trait<Atom> for $ty {
                type Output = Atom;
                fn $op_method(self, rhs: Atom) -> Atom {
                    atom_op_num_lhs!(self, $op_ws_fn, rhs.as_view())
                }
            }

            impl std::ops::$op_trait<AtomView<'_>> for $ty {
                type Output = Atom;
                fn $op_method(self, rhs: AtomView<'_>) -> Atom {
                    atom_op_num_lhs!(self, $op_ws_fn, rhs)
                }
            }
        )+
    };
}

macro_rules! impl_numeric_lhs_symbol_ops_for_all_ops {
    ($($ty:ty),+ $(,)?) => {
        impl_numeric_lhs_symbol_ops!(Add, add, add_with_ws_into, $($ty),+);
        impl_numeric_lhs_symbol_ops!(Sub, sub, sub_with_ws_into, $($ty),+);
        impl_numeric_lhs_symbol_ops!(Mul, mul, mul_with_ws_into, $($ty),+);
        impl_numeric_lhs_symbol_ops!(Div, div, div_with_ws_into, $($ty),+);
    };
}

macro_rules! impl_numeric_lhs_atom_ops_for_all_ops {
    ($($ty:ty),+ $(,)?) => {
        impl_numeric_lhs_atom_ops!(Add, add, add_with_ws_into, $($ty),+);
        impl_numeric_lhs_atom_ops!(Sub, sub, sub_with_ws_into, $($ty),+);
        impl_numeric_lhs_atom_ops!(Mul, mul, mul_with_ws_into, $($ty),+);
        impl_numeric_lhs_atom_ops!(Div, div, div_with_ws_into, $($ty),+);
    };
}

macro_rules! impl_domain_lhs_atom_ops_for_all_ops {
    ($($ty:ty),+ $(,)?) => {
        impl_domain_lhs_atom_ops!(Add, add, add_with_ws_into, $($ty),+);
        impl_domain_lhs_atom_ops!(Sub, sub, sub_with_ws_into, $($ty),+);
        impl_domain_lhs_atom_ops!(Mul, mul, mul_with_ws_into, $($ty),+);
        impl_domain_lhs_atom_ops!(Div, div, div_with_ws_into, $($ty),+);
    };
}

impl_binary_ops!(Add, add, add_with_ws_into);
impl_binary_ops!(Sub, sub, sub_with_ws_into);
impl_binary_ops!(Mul, mul, mul_with_ws_into);
impl_binary_ops!(Div, div, div_with_ws_into);

impl_numeric_lhs_symbol_ops_for_all_ops!(
    Coefficient,
    Rational,
    Float,
    Complex<Rational>,
    Complex<Float>,
    i8,
    i16,
    i32,
    i64,
    i128,
    isize,
    u8,
    u16,
    u32,
    u64,
    u128,
    usize,
    f64,
);

impl_numeric_lhs_atom_ops_for_all_ops!(
    i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f64,
);

impl_domain_lhs_atom_ops_for_all_ops!(Integer, Rational);

macro_rules! impl_assign_ops {
    ($assign_trait:ident, $assign_method:ident, $op_trait:ident, $op_method:ident) => {
        impl std::ops::$assign_trait<Atom> for Atom {
            fn $assign_method(&mut self, rhs: Atom) {
                *self = std::ops::$op_trait::$op_method(std::mem::take(self), rhs);
            }
        }

        impl std::ops::$assign_trait<&Atom> for Atom {
            fn $assign_method(&mut self, rhs: &Atom) {
                *self = std::ops::$op_trait::$op_method(std::mem::take(self), rhs);
            }
        }

        impl<'a> std::ops::$assign_trait<AtomView<'a>> for Atom {
            fn $assign_method(&mut self, rhs: AtomView<'a>) {
                *self = std::ops::$op_trait::$op_method(std::mem::take(self), rhs);
            }
        }

        impl std::ops::$assign_trait<Symbol> for Atom {
            fn $assign_method(&mut self, rhs: Symbol) {
                *self = std::ops::$op_trait::$op_method(std::mem::take(self), rhs);
            }
        }

        impl<T: Into<Coefficient>> std::ops::$assign_trait<T> for Atom {
            fn $assign_method(&mut self, rhs: T) {
                *self = std::ops::$op_trait::$op_method(std::mem::take(self), rhs);
            }
        }
    };
}

impl_assign_ops!(AddAssign, add_assign, Add, add);
impl_assign_ops!(SubAssign, sub_assign, Sub, sub);
impl_assign_ops!(MulAssign, mul_assign, Mul, mul);
impl_assign_ops!(DivAssign, div_assign, Div, div);
