use super::{Angle, Quantity, SIUnit};
use crate::fmt::PrintUnit;
#[cfg(feature = "nalgebra")]
use nalgebra::{DMatrix, DVector, Dyn};
#[cfg(feature = "ndarray")]
use ndarray::{Array, Dimension};
#[cfg(feature = "ndarray")]
use numpy::IntoPyArray;
#[cfg(any(feature = "nalgebra", feature = "ndarray"))]
use numpy::PyReadonlyArray;
#[cfg(feature = "nalgebra")]
use numpy::{PyReadonlyArray1, PyReadonlyArray2, ToPyArray};
use pyo3::{exceptions::PyValueError, prelude::*};
use std::sync::LazyLock;

static SIOBJECT: LazyLock<Py<PyAny>> = LazyLock::new(|| {
    Python::attach(|py| {
        PyModule::import(py, "si_units")
            .unwrap()
            .getattr("SIObject")
            .unwrap()
            .unbind()
    })
});

impl<
    'py,
    const T: i8,
    const L: i8,
    const M: i8,
    const I: i8,
    const THETA: i8,
    const N: i8,
    const J: i8,
> IntoPyObject<'py> for Quantity<f64, SIUnit<T, L, M, I, THETA, N, J>>
{
    type Target = PyAny;
    type Output = Bound<'py, PyAny>;
    type Error = PyErr;

    fn into_pyobject(self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
        let unit = [L, M, T, I, N, THETA, J];
        SIOBJECT.bind(py).call1((self.0, unit))
    }
}

#[cfg(feature = "ndarray")]
impl<
    'py,
    const T: i8,
    const L: i8,
    const M: i8,
    const I: i8,
    const THETA: i8,
    const N: i8,
    const J: i8,
    D: Dimension,
> IntoPyObject<'py> for Quantity<Array<f64, D>, SIUnit<T, L, M, I, THETA, N, J>>
{
    type Target = PyAny;
    type Output = Bound<'py, PyAny>;
    type Error = PyErr;

    fn into_pyobject(self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
        let unit = [L, M, T, I, N, THETA, J];
        let value = self.0.into_pyarray(py).into_any();
        SIOBJECT.bind(py).call1((value, unit))
    }
}

#[cfg(feature = "nalgebra")]
impl<
    'py,
    const T: i8,
    const L: i8,
    const M: i8,
    const I: i8,
    const THETA: i8,
    const N: i8,
    const J: i8,
> IntoPyObject<'py> for Quantity<DMatrix<f64>, SIUnit<T, L, M, I, THETA, N, J>>
{
    type Target = PyAny;
    type Output = Bound<'py, PyAny>;
    type Error = PyErr;

    fn into_pyobject(self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
        let unit = [L, M, T, I, N, THETA, J];
        let value = self.0.to_pyarray(py).into_any();
        SIOBJECT.bind(py).call1((value, unit))
    }
}

#[cfg(feature = "nalgebra")]
impl<
    'py,
    const T: i8,
    const L: i8,
    const M: i8,
    const I: i8,
    const THETA: i8,
    const N: i8,
    const J: i8,
> IntoPyObject<'py> for Quantity<DVector<f64>, SIUnit<T, L, M, I, THETA, N, J>>
{
    type Target = PyAny;
    type Output = Bound<'py, PyAny>;
    type Error = PyErr;

    fn into_pyobject(self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
        let unit = [L, M, T, I, N, THETA, J];
        let value = numpy::PyArray1::from_slice(py, self.0.data.as_vec()).into_any();
        SIOBJECT.bind(py).call1((value, unit))
    }
}

impl<
    'py,
    const T: i8,
    const L: i8,
    const M: i8,
    const I: i8,
    const THETA: i8,
    const N: i8,
    const J: i8,
> FromPyObject<'_, 'py> for Quantity<f64, SIUnit<T, L, M, I, THETA, N, J>>
where
    Self: PrintUnit,
{
    type Error = PyErr;
    fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
        let Ok((value, unit_from)) = ob
            .call_method0("__getnewargs__")
            .and_then(|raw| raw.extract::<(f64, [i8; 7])>())
        else {
            return Err(PyErr::new::<PyValueError, _>(format!(
                "Missing units! Expected {}, got {}.",
                Self::UNIT,
                ob.call_method0("__repr__")?
            )));
        };
        let unit_into = [L, M, T, I, N, THETA, J];
        if unit_into == unit_from {
            Ok(Quantity::new(value))
        } else {
            Err(PyErr::new::<PyValueError, _>(format!(
                "Wrong units! Expected {}, got {}.",
                Self::UNIT,
                ob.call_method0("__repr__")?
            )))
        }
    }
}

#[cfg(feature = "ndarray")]
impl<
    'py,
    const T: i8,
    const L: i8,
    const M: i8,
    const I: i8,
    const THETA: i8,
    const N: i8,
    const J: i8,
    D: Dimension,
> FromPyObject<'_, 'py> for Quantity<Array<f64, D>, SIUnit<T, L, M, I, THETA, N, J>>
where
    Self: PrintUnit,
{
    type Error = PyErr;
    fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
        let Ok((value, unit_from)) = ob
            .call_method0("__getnewargs__")
            .and_then(|raw| raw.extract::<(PyReadonlyArray<f64, D>, [i8; 7])>())
        else {
            return Err(PyErr::new::<PyValueError, _>(format!(
                "Missing units! Expected {}, got {}.",
                Self::UNIT,
                ob.call_method0("__repr__")?
            )));
        };
        let value = value.as_array().to_owned();
        let unit_into = [L, M, T, I, N, THETA, J];
        if unit_into == unit_from {
            Ok(Quantity::new(value))
        } else {
            Err(PyErr::new::<PyValueError, _>(format!(
                "Wrong units! Expected {}, got {}.",
                Self::UNIT,
                ob.call_method0("__repr__")?
            )))
        }
    }
}

#[cfg(feature = "nalgebra")]
impl<
    'py,
    const T: i8,
    const L: i8,
    const M: i8,
    const I: i8,
    const THETA: i8,
    const N: i8,
    const J: i8,
> FromPyObject<'_, 'py> for Quantity<DVector<f64>, SIUnit<T, L, M, I, THETA, N, J>>
where
    Self: PrintUnit,
{
    type Error = PyErr;
    fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
        let Ok((value, unit_from)) = ob.call_method0("__getnewargs__").and_then(|raw| {
            raw.extract::<(PyReadonlyArray1<f64>, [i8; 7])>()
                .map(|(m, u)| {
                    let m: nalgebra::DVectorView<f64, Dyn, Dyn> = m.try_as_matrix().unwrap();
                    (m.clone_owned(), u)
                })
        }) else {
            return Err(PyErr::new::<PyValueError, _>(format!(
                "Missing units! Expected {}, got {}.",
                Self::UNIT,
                ob.call_method0("__repr__")?
            )));
        };
        let unit_into = [L, M, T, I, N, THETA, J];
        if unit_into == unit_from {
            Ok(Quantity::new(value))
        } else {
            Err(PyErr::new::<PyValueError, _>(format!(
                "Wrong units! Expected {}, got {}.",
                Self::UNIT,
                ob.call_method0("__repr__")?
            )))
        }
    }
}

#[cfg(feature = "nalgebra")]
impl<
    'py,
    const T: i8,
    const L: i8,
    const M: i8,
    const I: i8,
    const THETA: i8,
    const N: i8,
    const J: i8,
> FromPyObject<'_, 'py> for Quantity<DMatrix<f64>, SIUnit<T, L, M, I, THETA, N, J>>
where
    Self: PrintUnit,
{
    type Error = PyErr;
    fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
        let Ok((value, unit_from)) = ob.call_method0("__getnewargs__").and_then(|raw| {
            raw.extract::<(PyReadonlyArray2<f64>, [i8; 7])>()
                .map(|(m, u)| {
                    let m: nalgebra::DMatrixView<f64, Dyn, Dyn> = m.try_as_matrix().unwrap();
                    (m.clone_owned(), u)
                })
        }) else {
            return Err(PyErr::new::<PyValueError, _>(format!(
                "Missing units! Expected {}, got {}.",
                Self::UNIT,
                ob.call_method0("__repr__")?
            )));
        };
        let unit_into = [L, M, T, I, N, THETA, J];
        if unit_into == unit_from {
            Ok(Quantity::new(value))
        } else {
            Err(PyErr::new::<PyValueError, _>(format!(
                "Wrong units! Expected {}, got {}.",
                Self::UNIT,
                ob.call_method0("__repr__")?
            )))
        }
    }
}

static ANGLE: LazyLock<Py<PyAny>> = LazyLock::new(|| {
    Python::attach(|py| {
        PyModule::import(py, "si_units")
            .unwrap()
            .getattr("Angle")
            .unwrap()
            .unbind()
    })
});

impl<'py> IntoPyObject<'py> for Angle {
    type Target = PyAny;
    type Output = Bound<'py, PyAny>;
    type Error = PyErr;
    fn into_pyobject(self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
        ANGLE.bind(py).call1((self.0,))
    }
}

impl<'py> FromPyObject<'_, 'py> for Angle {
    type Error = PyErr;

    fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
        let Ok(value) = ob
            .call_method0("__getnewargs__")
            .and_then(|raw| raw.extract::<f64>())
        else {
            return Err(PyErr::new::<PyValueError, _>(format!(
                "Missing units! Expected angle, got {}.",
                ob.call_method0("__repr__")?
            )));
        };
        Ok(Quantity::new(value))
    }
}
