/**
 * @file PyCoefficient.h
 * @brief Defines pybind11 trampoline classes for mfem::Coefficient and mfem::VectorCoefficient.
 *
 * These trampoline classes allow Python classes to inherit from mfem::Coefficient
 * and mfem::VectorCoefficient, enabling Python-defined coefficients to be used
 * within the C++ MFEM library.
 */
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/functional.h> // Needed for std::function
#include <memory>

#include "mfem.hpp"

namespace py = pybind11;
using namespace mfem;

/**
 * @namespace serif::pybind
 * @brief Contains pybind11 helper classes and trampoline classes for interfacing C++ with Python.
 */
namespace serif::pybind {
    /**
     * @brief Trampoline class for mfem::Coefficient.
     *
     * This class allows Python classes to inherit from mfem::Coefficient and override
     * its virtual methods. This is essential for creating custom coefficients in Python
     * that can be used by MFEM's C++ backend.
     *
     * @see mfem::Coefficient
     *
     * @par Python Usage Example:
     * @code{.py}
     * import mfem.ser_ext as mfem
     *
     * class MyPythonCoefficient(mfem.Coefficient):
     *     def __init__(self):
     *         super().__init__() # Call the base C++ constructor
     *
     *     def Eval(self, T, ip):
     *         # T is an mfem.ElementTransformation
     *         # ip is an mfem.IntegrationPoint
     *         # Example: return a constant value
     *         return 1.0
     *
     *     def SetTime(self, t):
     *         # Optionally handle time-dependent coefficients
     *         super().SetTime(t) # Call base class method
     *         print(f"Time set to: {t}")
     *
     * # Using the Python coefficient
     * py_coeff = MyPythonCoefficient()
     * # py_coeff can now be passed to MFEM functions expecting an mfem::Coefficient
     * @endcode
     */
    class PyCoefficient : public Coefficient {
    public:
        using Coefficient::Coefficient; /**< Inherit constructors from mfem::Coefficient. */

        /**
         * @brief Evaluate the coefficient at a given IntegrationPoint in an ElementTransformation.
         *
         * This method is called by MFEM when the value of the coefficient is needed.
         * If a Python class inherits from PyCoefficient, it *must* override this method.
         *
         * @param T The element transformation.
         * @param ip The integration point.
         * @return The value of the coefficient at the given point.
         *
         * @note This method forwards the call to the Python override.
         *       PYBIND11_OVERRIDE_PURE is used in the .cpp file to handle this.
         */
        real_t Eval(ElementTransformation &T, const IntegrationPoint &ip) override;

        /**
         * @brief Set the current time for time-dependent coefficients.
         *
         * This method is called by MFEM to update the time for time-dependent coefficients.
         * Python classes inheriting from PyCoefficient can override this method to implement
         * time-dependent behavior.
         *
         * @param t The current time.
         *
         * @note This method forwards the call to the Python override if one exists.
         *       PYBIND11_OVERRIDE is used in the .cpp file to handle this.
         */
        void SetTime(real_t t) override;
    };

    /**
     * @brief Trampoline class for mfem::VectorCoefficient.
     *
     * This class allows Python classes to inherit from mfem::VectorCoefficient and override
     * its virtual methods. This is essential for creating custom vector-valued coefficients
     * in Python that can be used by MFEM's C++ backend.
     *
     * @see mfem::VectorCoefficient
     *
     * @par Python Usage Example:
     * @code{.py}
     * import mfem.ser_ext as mfem
     * import numpy as np
     *
     * class MyPythonVectorCoefficient(mfem.VectorCoefficient):
     *     def __init__(self, dim):
     *         super().__init__(dim) # Call the base C++ constructor, pass vector dimension
     *         self.dim = dim
     *
     *     def Eval(self, V, T, ip):
     *         # V is an mfem.Vector (output parameter, must be filled)
     *         # T is an mfem.ElementTransformation
     *         # ip is an mfem.IntegrationPoint
     *         # Example: return a constant vector [1.0, 2.0, ...]
     *         for i in range(self.dim):
     *             V[i] = float(i + 1)
     *
     *     def SetTime(self, t):
     *         super().SetTime(t)
     *         print(f"VectorCoefficient time set to: {t}")
     *
     * # Using the Python vector coefficient
     * vec_dim = 2
     * py_vec_coeff = MyPythonVectorCoefficient(vec_dim)
     * # py_vec_coeff can now be passed to MFEM functions expecting an mfem::VectorCoefficient
     * @endcode
     */
    class PyVectorCoefficient : public VectorCoefficient {
    public:
        using VectorCoefficient::VectorCoefficient; /**< Inherit constructors from mfem::VectorCoefficient. */

        /**
         * @brief Evaluate the vector coefficient at a given IntegrationPoint in an ElementTransformation.
         *
         * This method is called by MFEM when the value of the vector coefficient is needed.
         * If a Python class inherits from PyVectorCoefficient, it *must* override this method.
         * The result should be stored in the output Vector @p V.
         *
         * @param V Output vector to store the result. Its size should match the coefficient's dimension.
         * @param T The element transformation.
         * @param ip The integration point.
         *
         * @note This method forwards the call to the Python override.
         *       PYBIND11_OVERRIDE_PURE is used in the .cpp file to handle this.
         */
        void Eval(Vector &V, ElementTransformation &T, const IntegrationPoint &ip) override;

        /**
         * @brief Set the current time for time-dependent vector coefficients.
         *
         * This method is called by MFEM to update the time for time-dependent vector coefficients.
         * Python classes inheriting from PyVectorCoefficient can override this method to implement
         * time-dependent behavior.
         *
         * @param t The current time.
         *
         * @note This method forwards the call to the Python override if one exists.
         *       PYBIND11_OVERRIDE is used in the .cpp file to handle this.
         */
        void SetTime(real_t t) override;
    };
}