/*!
Conversions between [`DolfinxPyFunction_f64`] and `dolfinx.fem.Function` in Python.
*/

use crate::dolfinx_access::ffi;
use crate::dolfinx_access::DolfinxPyFunction_f64;
use pyo3::conversion::FromPyObject;
use pyo3::exceptions::PyException;
use pyo3::prelude::*;

macro_rules! py_bail {
    ($msg:literal $(,)?) => {
        return Err(PyException::new_err(format!($msg)))
    };
    ($err:expr $(,)?) => {
        return Err(PyException::new_err(format!($err)))
    };
    ($fmt:expr, $($arg:tt)*) => {
        return Err(PyException::new_err(format!($fmt, $($arg)*)))
    };
}

impl<'a, 'py, const N: u32, const O: u32, const D: u32> FromPyObject<'a, 'py>
    for DolfinxPyFunction_f64<'py, N, O, D>
{
    type Error = PyErr;

    fn extract(u_: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
        // We maintain our reference-counted copy
        let u = u_.to_owned();
        // The "_cpp_object" attribute of a Python Dolfinx Function points to the C++ instance
        let u_cpp = u.getattr("_cpp_object")?;
        let cxx = unsafe { ffi::cast_mut_Function_f64(u_cpp.as_ptr() as *mut ffi::PyObject) }
            .or_else(|err| py_bail!("CXX cast error: {}", err))?;
        let info = unsafe { ffi::info_Function_f64(cxx) };
        if !info.triangular_mesh {
            py_bail!("Triangular mesh required")
        }
        if info.order < O {
            py_bail!("Insufficient order")
        }
        if info.codomain_dim != D {
            py_bail!("Codomain of invalid size")
        }
        if info.domain_dim != N {
            py_bail!("Domain of invalid size")
        }
        DolfinxPyFunction_f64::new_prechecked(u, cxx)
    }
}

// TODO: should probably use internal mutability to avoid just supporting mut references.
impl<'a, 'py, const N: u32, const O: u32, const D: u32> IntoPyObject<'py>
    for &'a mut DolfinxPyFunction_f64<'py, N, O, D>
where
    'py: 'a,
{
    type Target = PyAny;
    type Error = PyErr;
    type Output = pyo3::Borrowed<'a, 'py, Self::Target>;

    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
        self._into_py_ref(py)
    }
}

impl<'py, const N: u32, const O: u32, const D: u32> IntoPyObject<'py>
    for DolfinxPyFunction_f64<'py, N, O, D>
{
    type Target = PyAny;
    type Error = PyErr;
    type Output = pyo3::Bound<'py, Self::Target>;

    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
        self._into_py(py)
    }
}
