"""
Meng et al. (2015) - Reflectance Recovery
=========================================

Define the objects for reflectance recovery using the *Meng, Simon and
Hanika (2015)* method.

-   :func:`colour.recovery.XYZ_to_sd_Meng2015`

References
----------
-   :cite:`Meng2015c` : Meng, J., Simon, F., Hanika, J., & Dachsbacher, C.
    (2015). Physically Meaningful Rendering using Tristimulus Colours. Computer
    Graphics Forum, 34(4), 31-40. doi:10.1111/cgf.12676
"""

from __future__ import annotations

import typing

import numpy as np

from colour.colorimetry import (
    MultiSpectralDistributions,
    SpectralDistribution,
    SpectralShape,
    handle_spectral_arguments,
    sd_ones,
    sd_to_XYZ_integration,
)

if typing.TYPE_CHECKING:
    from colour.hints import DTypeFloat

from colour.hints import Domain1, NDArrayFloat  # noqa: TC001
from colour.utilities import from_range_100, required, to_domain_1

__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 Colour Developers"
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"

__all__ = [
    "SPECTRAL_SHAPE_MENG2015",
    "XYZ_to_sd_Meng2015",
]

SPECTRAL_SHAPE_MENG2015: SpectralShape = SpectralShape(360, 780, 5)
"""
Spectral shape according to *ASTM E308-15* practise shape but using an interval
of 5.
"""


@required("SciPy")
def XYZ_to_sd_Meng2015(
    XYZ: Domain1,
    cmfs: MultiSpectralDistributions | None = None,
    illuminant: SpectralDistribution | None = None,
    optimisation_kwargs: dict | None = None,
) -> SpectralDistribution:
    """
    Recover the spectral distribution from the specified *CIE XYZ* tristimulus
    values using the *Meng et al. (2015)* method.

    Parameters
    ----------
    XYZ
        *CIE XYZ* tristimulus values from which to recover the spectral
        distribution.
    cmfs
        Standard observer colour matching functions. The wavelength
        :math:`\\lambda_{i}` range interval of the colour matching functions
        directly affects the computation time. The current default interval
        of 5 provides a good compromise between precision and computation
        time. Defaults to the *CIE 1931 2 Degree Standard Observer*.
    illuminant
        Illuminant spectral distribution. Defaults to
        *CIE Standard Illuminant D65*.
    optimisation_kwargs
        Parameters for the :func:`scipy.optimize.minimize` definition.

    Returns
    -------
    :class:`colour.SpectralDistribution`
        Recovered spectral distribution.

    Raises
    ------
    RuntimeError
        If the optimisation fails.

    Notes
    -----
    +------------+-----------------------+---------------+
    | **Domain** | **Scale - Reference** | **Scale - 1** |
    +============+=======================+===============+
    | ``XYZ``    | 1                     | 1             |
    +------------+-----------------------+---------------+

    -   The definition used to convert spectrum to *CIE XYZ* tristimulus
        values is :func:`colour.colorimetry.spectral_to_XYZ_integration`
        because it processes any measurement interval, as opposed to
        :func:`colour.colorimetry.sd_to_XYZ_ASTME308` which handles only
        measurement intervals of 1, 5, 10 or 20nm.

    References
    ----------
    :cite:`Meng2015c`

    Examples
    --------
    >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS
    >>> from colour.utilities import numpy_print_options
    >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952])
    >>> cmfs = (
    ...     MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
    ...     .copy()
    ...     .align(SpectralShape(360, 780, 10))
    ... )
    >>> illuminant = SDS_ILLUMINANTS["D65"].copy().align(cmfs.shape)
    >>> sd = XYZ_to_sd_Meng2015(XYZ, cmfs, illuminant)
    >>> with numpy_print_options(suppress=True):
    ...     sd  # doctest: +SKIP
    SpectralDistribution([[ 360.        ,    0.0762005...],
                          [ 370.        ,    0.0761792...],
                          [ 380.        ,    0.0761363...],
                          [ 390.        ,    0.0761194...],
                          [ 400.        ,    0.0762539...],
                          [ 410.        ,    0.0761671...],
                          [ 420.        ,    0.0754649...],
                          [ 430.        ,    0.0731519...],
                          [ 440.        ,    0.0676701...],
                          [ 450.        ,    0.0577800...],
                          [ 460.        ,    0.0441993...],
                          [ 470.        ,    0.0285064...],
                          [ 480.        ,    0.0138728...],
                          [ 490.        ,    0.0033585...],
                          [ 500.        ,    0.       ...],
                          [ 510.        ,    0.       ...],
                          [ 520.        ,    0.       ...],
                          [ 530.        ,    0.       ...],
                          [ 540.        ,    0.0055767...],
                          [ 550.        ,    0.0317581...],
                          [ 560.        ,    0.0754491...],
                          [ 570.        ,    0.1314115...],
                          [ 580.        ,    0.1937649...],
                          [ 590.        ,    0.2559311...],
                          [ 600.        ,    0.3123173...],
                          [ 610.        ,    0.3584966...],
                          [ 620.        ,    0.3927335...],
                          [ 630.        ,    0.4159458...],
                          [ 640.        ,    0.4306660...],
                          [ 650.        ,    0.4391040...],
                          [ 660.        ,    0.4439497...],
                          [ 670.        ,    0.4463618...],
                          [ 680.        ,    0.4474625...],
                          [ 690.        ,    0.4479868...],
                          [ 700.        ,    0.4482116...],
                          [ 710.        ,    0.4482800...],
                          [ 720.        ,    0.4483472...],
                          [ 730.        ,    0.4484251...],
                          [ 740.        ,    0.4484633...],
                          [ 750.        ,    0.4485071...],
                          [ 760.        ,    0.4484969...],
                          [ 770.        ,    0.4484853...],
                          [ 780.        ,    0.4485134...]],
                         interpolator=SpragueInterpolator,
                         interpolator_kwargs={},
                         extrapolator=Extrapolator,
                         extrapolator_kwargs={...})
    >>> sd_to_XYZ_integration(sd, cmfs, illuminant) / 100  # doctest: +ELLIPSIS
    array([ 0.2065400...,  0.1219722...,  0.0513695...])
    """

    from scipy.optimize import minimize  # noqa: PLC0415

    XYZ = to_domain_1(XYZ)

    cmfs, illuminant = handle_spectral_arguments(
        cmfs, illuminant, shape_default=SPECTRAL_SHAPE_MENG2015
    )

    sd = sd_ones(cmfs.shape)

    def objective_function(a: NDArrayFloat) -> DTypeFloat:
        """Define the objective function."""

        return np.sum(np.square(np.diff(a)))

    def constraint_function(a: NDArrayFloat) -> NDArrayFloat:
        """Define the constraint function."""

        sd[:] = a

        return sd_to_XYZ_integration(sd, cmfs=cmfs, illuminant=illuminant) - XYZ

    wavelengths = sd.wavelengths
    bins = wavelengths.size

    optimisation_settings = {
        "method": "SLSQP",
        "constraints": {"type": "eq", "fun": constraint_function},
        "bounds": np.tile(np.array([0, 1000]), (bins, 1)),
        "options": {
            "ftol": 1e-10,
        },
    }
    if optimisation_kwargs is not None:
        optimisation_settings.update(optimisation_kwargs)

    result = minimize(objective_function, sd.values, **optimisation_settings)

    if not result.success:
        error = (
            f"Optimisation failed for {XYZ} after {result.nit} iterations: "
            f'"{result.message}".'
        )

        raise RuntimeError(error)

    return SpectralDistribution(
        from_range_100(result.x * 100),
        wavelengths,
        name=f"{XYZ} (XYZ) - Meng (2015)",
    )
