"""
CIECAM02 Colour Appearance Model
================================

Defines the *CIECAM02* colour appearance model objects:

-   :class:`colour.appearance.InductionFactors_CIECAM02`
-   :attr:`colour.VIEWING_CONDITIONS_CIECAM02`
-   :class:`colour.CAM_Specification_CIECAM02`
-   :func:`colour.XYZ_to_CIECAM02`
-   :func:`colour.CIECAM02_to_XYZ`

References
----------
-   :cite:`Fairchild2004c` : Fairchild, M. D. (2004). CIECAM02. In Color
    Appearance Models (2nd ed., pp. 289-301). Wiley. ISBN:978-0-470-01216-1
-   :cite:`InternationalElectrotechnicalCommission1999a` : International
    Electrotechnical Commission. (1999). IEC 61966-2-1:1999 - Multimedia
    systems and equipment - Colour measurement and management - Part 2-1:
    Colour management - Default RGB colour space - sRGB (p. 51).
    https://webstore.iec.ch/publication/6169
-   :cite:`Luo2013` : Luo, Ming Ronnier, & Li, C. (2013). CIECAM02 and Its
    Recent Developments. In C. Fernandez-Maloigne (Ed.), Advanced Color Image
    Processing and Analysis (pp. 19-58). Springer New York.
    doi:10.1007/978-1-4419-6190-7
-   :cite:`Moroneya` : Moroney, N., Fairchild, M. D., Hunt, R. W. G., Li, C.,
    Luo, M. R., & Newman, T. (2002). The CIECAM02 color appearance model. Color
    and Imaging Conference, 1, 23-27.
-   :cite:`Wikipedia2007a` : Fairchild, M. D. (2004). CIECAM02. In Color
    Appearance Models (2nd ed., pp. 289-301). Wiley. ISBN:978-0-470-01216-1
"""

from __future__ import annotations

import numpy as np
from collections import namedtuple
from dataclasses import astuple, dataclass, field

from colour.algebra import matrix_dot, spow, vector_dot
from colour.adaptation import CAT_CAT02
from colour.appearance.hunt import (
    MATRIX_HPE_TO_XYZ,
    MATRIX_XYZ_TO_HPE,
    luminance_level_adaptation_factor,
)
from colour.colorimetry import CCS_ILLUMINANTS
from colour.constants import EPSILON
from colour.hints import (
    ArrayLike,
    Boolean,
    Dict,
    FloatingOrArrayLike,
    FloatingOrNDArray,
    NDArray,
    Optional,
    Tuple,
)
from colour.models import xy_to_XYZ
from colour.utilities import (
    CaseInsensitiveMapping,
    MixinDataclassArithmetic,
    as_float,
    as_float_array,
    as_int_array,
    from_range_degrees,
    from_range_100,
    has_only_nan,
    ones,
    to_domain_100,
    to_domain_degrees,
    tsplit,
    tstack,
    zeros,
)
from colour.utilities.documentation import (
    DocstringDict,
    is_documentation_building,
)

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

__all__ = [
    "CAT_INVERSE_CAT02",
    "InductionFactors_CIECAM02",
    "VIEWING_CONDITIONS_CIECAM02",
    "HUE_DATA_FOR_HUE_QUADRATURE",
    "CAM_KWARGS_CIECAM02_sRGB",
    "CAM_Specification_CIECAM02",
    "XYZ_to_CIECAM02",
    "CIECAM02_to_XYZ",
    "chromatic_induction_factors",
    "base_exponential_non_linearity",
    "viewing_condition_dependent_parameters",
    "degree_of_adaptation",
    "full_chromatic_adaptation_forward",
    "full_chromatic_adaptation_inverse",
    "RGB_to_rgb",
    "rgb_to_RGB",
    "post_adaptation_non_linear_response_compression_forward",
    "post_adaptation_non_linear_response_compression_inverse",
    "opponent_colour_dimensions_forward",
    "opponent_colour_dimensions_inverse",
    "hue_angle",
    "hue_quadrature",
    "eccentricity_factor",
    "achromatic_response_forward",
    "achromatic_response_inverse",
    "lightness_correlate",
    "brightness_correlate",
    "temporary_magnitude_quantity_forward",
    "temporary_magnitude_quantity_inverse",
    "chroma_correlate",
    "colourfulness_correlate",
    "saturation_correlate",
    "P",
    "matrix_post_adaptation_non_linear_response_compression",
]

CAT_INVERSE_CAT02: NDArray = np.linalg.inv(CAT_CAT02)
"""Inverse CAT02 chromatic adaptation transform."""


class InductionFactors_CIECAM02(
    namedtuple("InductionFactors_CIECAM02", ("F", "c", "N_c"))
):
    """
    *CIECAM02* colour appearance model induction factors.

    Parameters
    ----------
    F
        Maximum degree of adaptation :math:`F`.
    c
        Exponential non-linearity :math:`c`.
    N_c
        Chromatic induction factor :math:`N_c`.

    References
    ----------
    :cite:`Fairchild2004c`, :cite:`Luo2013`, :cite:`Moroneya`,
    :cite:`Wikipedia2007a`
    """


VIEWING_CONDITIONS_CIECAM02: CaseInsensitiveMapping = CaseInsensitiveMapping(
    {
        "Average": InductionFactors_CIECAM02(1, 0.69, 1),
        "Dim": InductionFactors_CIECAM02(0.9, 0.59, 0.9),
        "Dark": InductionFactors_CIECAM02(0.8, 0.525, 0.8),
    }
)
VIEWING_CONDITIONS_CIECAM02.__doc__ = """
Reference *CIECAM02* colour appearance model viewing conditions.

References
----------
:cite:`Fairchild2004c`, :cite:`Luo2013`, :cite:`Moroneya`,
:cite:`Wikipedia2007a`
"""

HUE_DATA_FOR_HUE_QUADRATURE: Dict = {
    "h_i": np.array([20.14, 90.00, 164.25, 237.53, 380.14]),
    "e_i": np.array([0.8, 0.7, 1.0, 1.2, 0.8]),
    "H_i": np.array([0.0, 100.0, 200.0, 300.0, 400.0]),
}

CAM_KWARGS_CIECAM02_sRGB: Dict = {
    "XYZ_w": xy_to_XYZ(
        CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["D65"]
    )
    * 100,
    "L_A": 64 / np.pi * 0.2,
    "Y_b": 20,
    "surround": VIEWING_CONDITIONS_CIECAM02["Average"],
}
if is_documentation_building():  # pragma: no cover
    CAM_KWARGS_CIECAM02_sRGB = DocstringDict(CAM_KWARGS_CIECAM02_sRGB)
    CAM_KWARGS_CIECAM02_sRGB.__doc__ = """
Default parameter values for the *CIECAM02* colour appearance model usage in
the context of *sRGB*.

References
----------
:cite:`Fairchild2004c`, :cite:`InternationalElectrotechnicalCommission1999a`,
:cite:`Luo2013`, :cite:`Moroneya`, :cite:`Wikipedia2007a`
"""


@dataclass
class CAM_Specification_CIECAM02(MixinDataclassArithmetic):
    """
    Define the *CIECAM02* colour appearance model specification.

    Parameters
    ----------
    J
        Correlate of *Lightness* :math:`J`.
    C
        Correlate of *chroma* :math:`C`.
    h
        *Hue* angle :math:`h` in degrees.
    s
        Correlate of *saturation* :math:`s`.
    Q
        Correlate of *brightness* :math:`Q`.
    M
        Correlate of *colourfulness* :math:`M`.
    H
        *Hue* :math:`h` quadrature :math:`H`.
    HC
        *Hue* :math:`h` composition :math:`H^C`.

    References
    ----------
    :cite:`Fairchild2004c`, :cite:`Luo2013`, :cite:`Moroneya`,
    :cite:`Wikipedia2007a`
    """

    J: Optional[FloatingOrNDArray] = field(default_factory=lambda: None)
    C: Optional[FloatingOrNDArray] = field(default_factory=lambda: None)
    h: Optional[FloatingOrNDArray] = field(default_factory=lambda: None)
    s: Optional[FloatingOrNDArray] = field(default_factory=lambda: None)
    Q: Optional[FloatingOrNDArray] = field(default_factory=lambda: None)
    M: Optional[FloatingOrNDArray] = field(default_factory=lambda: None)
    H: Optional[FloatingOrNDArray] = field(default_factory=lambda: None)
    HC: Optional[FloatingOrNDArray] = field(default_factory=lambda: None)


def XYZ_to_CIECAM02(
    XYZ: ArrayLike,
    XYZ_w: ArrayLike,
    L_A: FloatingOrArrayLike,
    Y_b: FloatingOrArrayLike,
    surround: InductionFactors_CIECAM02 = VIEWING_CONDITIONS_CIECAM02[
        "Average"
    ],
    discount_illuminant: Boolean = False,
) -> CAM_Specification_CIECAM02:
    """
    Compute the *CIECAM02* colour appearance model correlates from given
    *CIE XYZ* tristimulus values.

    Parameters
    ----------
    XYZ
        *CIE XYZ* tristimulus values of test sample / stimulus.
    XYZ_w
        *CIE XYZ* tristimulus values of reference white.
    L_A
        Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`, (often taken
        to be 20% of the luminance of a white object in the scene).
    Y_b
        Luminous factor of background :math:`Y_b` such as
        :math:`Y_b = 100 x L_b / L_w` where :math:`L_w` is the luminance of the
        light source and :math:`L_b` is the luminance of the background. For
        viewing images, :math:`Y_b` can be the average :math:`Y` value for the
        pixels in the entire image, or frequently, a :math:`Y` value of 20,
        approximate an :math:`L^*` of 50 is used.
    surround
        Surround viewing conditions induction factors.
    discount_illuminant
        Truth value indicating if the illuminant should be discounted.

    Returns
    -------
    :class:`colour.CAM_Specification_CIECAM02`
        *CIECAM02* colour appearance model specification.

    Notes
    -----
    +------------+-----------------------+---------------+
    | **Domain** | **Scale - Reference** | **Scale - 1** |
    +============+=======================+===============+
    | ``XYZ``    | [0, 100]              | [0, 1]        |
    +------------+-----------------------+---------------+
    | ``XYZ_w``  | [0, 100]              | [0, 1]        |
    +------------+-----------------------+---------------+

    +----------------------------------+-----------------------\
+---------------+
    | **Range**                        | **Scale - Reference** \
| **Scale - 1** |
    +==================================+=======================\
+===============+
    | ``CAM_Specification_CIECAM02.J`` | [0, 100]              \
| [0, 1]        |
    +----------------------------------+-----------------------\
+---------------+
    | ``CAM_Specification_CIECAM02.C`` | [0, 100]              \
| [0, 1]        |
    +----------------------------------+-----------------------\
+---------------+
    | ``CAM_Specification_CIECAM02.h`` | [0, 360]              \
| [0, 1]        |
    +----------------------------------+-----------------------\
+---------------+
    | ``CAM_Specification_CIECAM02.s`` | [0, 100]              \
| [0, 1]        |
    +----------------------------------+-----------------------\
+---------------+
    | ``CAM_Specification_CIECAM02.Q`` | [0, 100]              \
| [0, 1]        |
    +----------------------------------+-----------------------\
+---------------+
    | ``CAM_Specification_CIECAM02.M`` | [0, 100]              \
| [0, 1]        |
    +----------------------------------+-----------------------\
+---------------+
    | ``CAM_Specification_CIECAM02.H`` | [0, 400]              \
| [0, 1]        |
    +----------------------------------+-----------------------\
+---------------+

    References
    ----------
    :cite:`Fairchild2004c`, :cite:`Luo2013`, :cite:`Moroneya`,
    :cite:`Wikipedia2007a`

    Examples
    --------
    >>> XYZ = np.array([19.01, 20.00, 21.78])
    >>> XYZ_w = np.array([95.05, 100.00, 108.88])
    >>> L_A = 318.31
    >>> Y_b = 20.0
    >>> surround = VIEWING_CONDITIONS_CIECAM02['Average']
    >>> XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround)  # doctest: +ELLIPSIS
    CAM_Specification_CIECAM02(J=41.7310911..., C=0.1047077..., \
h=219.0484326..., s=2.3603053..., Q=195.3713259..., M=0.1088421..., \
H=278.0607358..., HC=None)
    """

    XYZ = to_domain_100(XYZ)
    XYZ_w = to_domain_100(XYZ_w)
    _X_w, Y_w, _Z_w = tsplit(XYZ_w)
    L_A = as_float_array(L_A)
    Y_b = as_float_array(Y_b)

    n, F_L, N_bb, N_cb, z = viewing_condition_dependent_parameters(
        Y_b, Y_w, L_A
    )

    # Converting *CIE XYZ* tristimulus values to *CMCCAT2000* transform
    # sharpened *RGB* values.
    RGB = vector_dot(CAT_CAT02, XYZ)
    RGB_w = vector_dot(CAT_CAT02, XYZ_w)

    # Computing degree of adaptation :math:`D`.
    D = (
        degree_of_adaptation(surround.F, L_A)
        if not discount_illuminant
        else ones(L_A.shape)
    )

    # Computing full chromatic adaptation.
    RGB_c = full_chromatic_adaptation_forward(RGB, RGB_w, Y_w, D)
    RGB_wc = full_chromatic_adaptation_forward(RGB_w, RGB_w, Y_w, D)

    # Converting to *Hunt-Pointer-Estevez* colourspace.
    RGB_p = RGB_to_rgb(RGB_c)
    RGB_pw = RGB_to_rgb(RGB_wc)

    # Applying forward post-adaptation non-linear response compression.
    RGB_a = post_adaptation_non_linear_response_compression_forward(RGB_p, F_L)
    RGB_aw = post_adaptation_non_linear_response_compression_forward(
        RGB_pw, F_L
    )

    # Converting to preliminary cartesian coordinates.
    a, b = tsplit(opponent_colour_dimensions_forward(RGB_a))

    # Computing the *hue* angle :math:`h`.
    h = hue_angle(a, b)

    # Computing hue :math:`h` quadrature :math:`H`.
    H = hue_quadrature(h)
    # TODO: Compute hue composition.

    # Computing eccentricity factor *e_t*.
    e_t = eccentricity_factor(h)

    # Computing achromatic responses for the stimulus and the whitepoint.
    A = achromatic_response_forward(RGB_a, N_bb)
    A_w = achromatic_response_forward(RGB_aw, N_bb)

    # Computing the correlate of *Lightness* :math:`J`.
    J = lightness_correlate(A, A_w, surround.c, z)

    # Computing the correlate of *brightness* :math:`Q`.
    Q = brightness_correlate(surround.c, J, A_w, F_L)

    # Computing the correlate of *chroma* :math:`C`.
    C = chroma_correlate(J, n, surround.N_c, N_cb, e_t, a, b, RGB_a)

    # Computing the correlate of *colourfulness* :math:`M`.
    M = colourfulness_correlate(C, F_L)

    # Computing the correlate of *saturation* :math:`s`.
    s = saturation_correlate(M, Q)

    return CAM_Specification_CIECAM02(
        as_float(from_range_100(J)),
        as_float(from_range_100(C)),
        as_float(from_range_degrees(h)),
        as_float(from_range_100(s)),
        as_float(from_range_100(Q)),
        as_float(from_range_100(M)),
        as_float(from_range_degrees(H, 400)),
        None,
    )


def CIECAM02_to_XYZ(
    specification: CAM_Specification_CIECAM02,
    XYZ_w: ArrayLike,
    L_A: FloatingOrArrayLike,
    Y_b: FloatingOrArrayLike,
    surround: InductionFactors_CIECAM02 = VIEWING_CONDITIONS_CIECAM02[
        "Average"
    ],
    discount_illuminant: Boolean = False,
) -> NDArray:
    """
    Convert from *CIECAM02* specification to *CIE XYZ* tristimulus values.

    Parameters
    ----------
    specification
        *CIECAM02* colour appearance model specification. Correlate of
        *Lightness* :math:`J`, correlate of *chroma* :math:`C` or correlate of
        *colourfulness* :math:`M` and *hue* angle :math:`h` in degrees must be
        specified, e.g. :math:`JCh` or :math:`JMh`.
    XYZ_w
        *CIE XYZ* tristimulus values of reference white.
    L_A
        Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`, (often taken
        to be 20% of the luminance of a white object in the scene).
    Y_b
        Luminous factor of background :math:`Y_b` such as
        :math:`Y_b = 100 x L_b / L_w` where :math:`L_w` is the luminance of the
        light source and :math:`L_b` is the luminance of the background. For
        viewing images, :math:`Y_b` can be the average :math:`Y` value for the
        pixels in the entire image, or frequently, a :math:`Y` value of 20,
        approximate an :math:`L^*` of 50 is used.
    surround
        Surround viewing conditions.
    discount_illuminant
        Discount the illuminant.

    Returns
    -------
    :class:`numpy.ndarray`
        *CIE XYZ* tristimulus values.

    Raises
    ------
    ValueError
        If neither *C* or *M* correlates have been defined in the
        ``CAM_Specification_CIECAM02`` argument.

    Notes
    -----
    +----------------------------------+-----------------------\
+---------------+
    | **Domain**                       | **Scale - Reference** \
| **Scale - 1** |
    +==================================+=======================\
+===============+
    | ``CAM_Specification_CIECAM02.J`` | [0, 100]              \
| [0, 1]        |
    +----------------------------------+-----------------------\
+---------------+
    | ``CAM_Specification_CIECAM02.C`` | [0, 100]              \
| [0, 1]        |
    +----------------------------------+-----------------------\
+---------------+
    | ``CAM_Specification_CIECAM02.h`` | [0, 360]              \
| [0, 1]        |
    +----------------------------------+-----------------------\
+---------------+
    | ``CAM_Specification_CIECAM02.s`` | [0, 100]              \
| [0, 1]        |
    +----------------------------------+-----------------------\
+---------------+
    | ``CAM_Specification_CIECAM02.Q`` | [0, 100]              \
| [0, 1]        |
    +----------------------------------+-----------------------\
+---------------+
    | ``CAM_Specification_CIECAM02.M`` | [0, 100]              \
| [0, 1]        |
    +----------------------------------+-----------------------\
+---------------+
    | ``CAM_Specification_CIECAM02.H`` | [0, 360]              \
| [0, 1]        |
    +----------------------------------+-----------------------\
+---------------+
    | ``XYZ_w``                        | [0, 100]              \
| [0, 1]        |
    +----------------------------------+-----------------------\
+---------------+

    +-----------+-----------------------+---------------+
    | **Range** | **Scale - Reference** | **Scale - 1** |
    +===========+=======================+===============+
    | ``XYZ``   | [0, 100]              | [0, 1]        |
    +-----------+-----------------------+---------------+

    References
    ----------
    :cite:`Fairchild2004c`, :cite:`Luo2013`, :cite:`Moroneya`,
    :cite:`Wikipedia2007a`

    Examples
    --------
    >>> specification = CAM_Specification_CIECAM02(J=41.731091132513917,
    ...                                            C=0.104707757171031,
    ...                                            h=219.048432658311780)
    >>> XYZ_w = np.array([95.05, 100.00, 108.88])
    >>> L_A = 318.31
    >>> Y_b = 20.0
    >>> CIECAM02_to_XYZ(specification, XYZ_w, L_A, Y_b)  # doctest: +ELLIPSIS
    array([ 19.01...,  20...  ,  21.78...])
    """

    J, C, h, _s, _Q, M, _H, _HC = astuple(specification)

    J = to_domain_100(J)
    C = to_domain_100(C)
    h = to_domain_degrees(h)
    M = to_domain_100(M)
    L_A = as_float_array(L_A)
    XYZ_w = to_domain_100(XYZ_w)
    _X_w, Y_w, _Z_w = tsplit(XYZ_w)

    n, F_L, N_bb, N_cb, z = viewing_condition_dependent_parameters(
        Y_b, Y_w, L_A
    )

    if has_only_nan(C) and not has_only_nan(M):
        C = M / spow(F_L, 0.25)
    elif has_only_nan(C):
        raise ValueError(
            'Either "C" or "M" correlate must be defined in '
            'the "CAM_Specification_CIECAM02" argument!'
        )

    # Converting *CIE XYZ* tristimulus values to *CMCCAT2000* transform
    # sharpened *RGB* values.
    RGB_w = vector_dot(CAT_CAT02, XYZ_w)

    # Computing degree of adaptation :math:`D`.
    D = (
        degree_of_adaptation(surround.F, L_A)
        if not discount_illuminant
        else ones(L_A.shape)
    )

    # Computing full chromatic adaptation.
    RGB_wc = full_chromatic_adaptation_forward(RGB_w, RGB_w, Y_w, D)

    # Converting to *Hunt-Pointer-Estevez* colourspace.
    RGB_pw = RGB_to_rgb(RGB_wc)

    # Applying post-adaptation non-linear response compression.
    RGB_aw = post_adaptation_non_linear_response_compression_forward(
        RGB_pw, F_L
    )

    # Computing achromatic response for the whitepoint.
    A_w = achromatic_response_forward(RGB_aw, N_bb)

    # Computing temporary magnitude quantity :math:`t`.
    t = temporary_magnitude_quantity_inverse(C, J, n)

    # Computing eccentricity factor *e_t*.
    e_t = eccentricity_factor(h)

    # Computing achromatic response :math:`A` for the stimulus.
    A = achromatic_response_inverse(A_w, J, surround.c, z)

    # Computing *P_1* to *P_3*.
    P_n = P(surround.N_c, N_cb, e_t, t, A, N_bb)
    _P_1, P_2, _P_3 = tsplit(P_n)

    # Computing opponent colour dimensions :math:`a` and :math:`b`.
    a, b = tsplit(opponent_colour_dimensions_inverse(P_n, h))

    # Applying post-adaptation non-linear response compression matrix.
    RGB_a = matrix_post_adaptation_non_linear_response_compression(P_2, a, b)

    # Applying inverse post-adaptation non-linear response compression.
    RGB_p = post_adaptation_non_linear_response_compression_inverse(RGB_a, F_L)

    # Converting to *Hunt-Pointer-Estevez* colourspace.
    RGB_c = rgb_to_RGB(RGB_p)

    # Applying inverse full chromatic adaptation.
    RGB = full_chromatic_adaptation_inverse(RGB_c, RGB_w, Y_w, D)

    # Converting *CMCCAT2000* transform sharpened *RGB* values to *CIE XYZ*
    # tristimulus values.
    XYZ = vector_dot(CAT_INVERSE_CAT02, RGB)

    return from_range_100(XYZ)


def chromatic_induction_factors(n: FloatingOrArrayLike) -> NDArray:
    """
    Return the chromatic induction factors :math:`N_{bb}` and :math:`N_{cb}`.

    Parameters
    ----------
    n
        Function of the luminance factor of the background :math:`n`.

    Returns
    -------
    :class:`numpy.ndarray`
        Chromatic induction factors :math:`N_{bb}` and :math:`N_{cb}`.

    Examples
    --------
    >>> chromatic_induction_factors(0.2)  # doctest: +ELLIPSIS
    array([ 1.000304,  1.000304])
    """

    n = as_float_array(n)

    N_bb = N_cb = as_float(0.725) * spow(1 / n, 0.2)
    N_bbcb = tstack([N_bb, N_cb])

    return N_bbcb


def base_exponential_non_linearity(
    n: FloatingOrArrayLike,
) -> FloatingOrNDArray:
    """
    Return the base exponential non-linearity :math:`n`.

    Parameters
    ----------
    n
        Function of the luminance factor of the background :math:`n`.

    Returns
    -------
    :class:`numpy.floating` or :class:`numpy.ndarray`
        Base exponential non-linearity :math:`z`.

    Examples
    --------
    >>> base_exponential_non_linearity(0.2)  # doctest: +ELLIPSIS
    1.9272135...
    """

    n = as_float_array(n)

    z = 1.48 + np.sqrt(n)

    return z


def viewing_condition_dependent_parameters(
    Y_b: FloatingOrArrayLike,
    Y_w: FloatingOrArrayLike,
    L_A: FloatingOrArrayLike,
) -> Tuple[
    FloatingOrNDArray,
    FloatingOrNDArray,
    FloatingOrNDArray,
    FloatingOrNDArray,
    FloatingOrNDArray,
]:
    """
    Return the viewing condition dependent parameters.

    Parameters
    ----------
    Y_b
        Adapting field *Y* tristimulus value :math:`Y_b`.
    Y_w
        Whitepoint *Y* tristimulus value :math:`Y_w`.
    L_A
        Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`.

    Returns
    -------
    :class:`tuple`
        Viewing condition dependent parameters.

    Examples
    --------
    >>> viewing_condition_dependent_parameters(20.0, 100.0, 318.31)
    ... # doctest: +ELLIPSIS
    (0.2000000..., 1.1675444..., 1.0003040..., 1.0003040..., 1.9272135...)
    """

    Y_b = as_float_array(Y_b)
    Y_w = as_float_array(Y_w)

    n = Y_b / Y_w

    F_L = luminance_level_adaptation_factor(L_A)
    N_bb, N_cb = tsplit(chromatic_induction_factors(n))
    z = base_exponential_non_linearity(n)

    return n, F_L, N_bb, N_cb, z


def degree_of_adaptation(
    F: FloatingOrArrayLike, L_A: FloatingOrArrayLike
) -> FloatingOrNDArray:
    """
    Return the degree of adaptation :math:`D` from given surround maximum
    degree of adaptation :math:`F` and adapting field *luminance* :math:`L_A`
    in :math:`cd/m^2`.

    Parameters
    ----------
    F
        Surround maximum degree of adaptation :math:`F`.
    L_A
        Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`.

    Returns
    -------
    :class:`numpy.floating` or :class:`numpy.ndarray`
        Degree of adaptation :math:`D`.

    Examples
    --------
    >>> degree_of_adaptation(1.0, 318.31)  # doctest: +ELLIPSIS
    0.9944687...
    """

    F = as_float_array(F)
    L_A = as_float_array(L_A)

    D = F * (1 - (1 / 3.6) * np.exp((-L_A - 42) / 92))

    return D


def full_chromatic_adaptation_forward(
    RGB: ArrayLike,
    RGB_w: ArrayLike,
    Y_w: FloatingOrArrayLike,
    D: FloatingOrArrayLike,
) -> NDArray:
    """
    Apply full chromatic adaptation to given *CMCCAT2000* transform sharpened
    *RGB* array using given *CMCCAT2000* transform sharpened whitepoint
    *RGB_w* array.

    Parameters
    ----------
    RGB
        *CMCCAT2000* transform sharpened *RGB* array.
    RGB_w
        *CMCCAT2000* transform sharpened whitepoint *RGB_w* array.
    Y_w
        Whitepoint *Y* tristimulus value :math:`Y_w`.
    D
        Degree of adaptation :math:`D`.

    Returns
    -------
    :class:`numpy.ndarray`
        Adapted *RGB* array.

    Examples
    --------
    >>> RGB = np.array([18.985456, 20.707422, 21.747482])
    >>> RGB_w = np.array([94.930528, 103.536988, 108.717742])
    >>> Y_w = 100.0
    >>> D = 0.994468780088
    >>> full_chromatic_adaptation_forward(RGB, RGB_w, Y_w, D)
    ... # doctest: +ELLIPSIS
    array([ 19.9937078...,  20.0039363...,  20.0132638...])
    """

    RGB = as_float_array(RGB)
    RGB_w = as_float_array(RGB_w)
    Y_w = as_float_array(Y_w)
    D = as_float_array(D)

    RGB_c = (
        (Y_w[..., np.newaxis] * D[..., np.newaxis] / RGB_w)
        + 1
        - D[..., np.newaxis]
    ) * RGB

    return RGB_c


def full_chromatic_adaptation_inverse(
    RGB: ArrayLike,
    RGB_w: ArrayLike,
    Y_w: FloatingOrArrayLike,
    D: FloatingOrArrayLike,
) -> NDArray:
    """
    Revert full chromatic adaptation of given *CMCCAT2000* transform sharpened
    *RGB* array using given *CMCCAT2000* transform sharpened whitepoint
    *RGB_w* array.

    Parameters
    ----------
    RGB
        *CMCCAT2000* transform sharpened *RGB* array.
    RGB_w
        *CMCCAT2000* transform sharpened whitepoint *RGB_w* array.
    Y_w
        Whitepoint *Y* tristimulus value :math:`Y_w`.
    D
        Degree of adaptation :math:`D`.

    Returns
    -------
    :class:`numpy.ndarray`
        Adapted *RGB* array.

    Examples
    --------
    >>> RGB = np.array([19.99370783, 20.00393634, 20.01326387])
    >>> RGB_w = np.array([94.930528, 103.536988, 108.717742])
    >>> Y_w = 100.0
    >>> D = 0.994468780088
    >>> full_chromatic_adaptation_inverse(RGB, RGB_w, Y_w, D)
    array([ 18.985456,  20.707422,  21.747482])
    """

    RGB = as_float_array(RGB)
    RGB_w = as_float_array(RGB_w)
    Y_w = as_float_array(Y_w)
    D = as_float_array(D)

    RGB_c = RGB / (
        Y_w[..., np.newaxis] * (D[..., np.newaxis] / RGB_w)
        + 1
        - D[..., np.newaxis]
    )

    return RGB_c


def RGB_to_rgb(RGB: ArrayLike) -> NDArray:
    """
    Convert given *RGB* array to *Hunt-Pointer-Estevez*
    :math:`\\rho\\gamma\\beta` colourspace.

    Parameters
    ----------
    RGB
        *RGB* array.

    Returns
    -------
    :class:`numpy.ndarray`
        *Hunt-Pointer-Estevez* :math:`\\rho\\gamma\\beta` colourspace array.

    Examples
    --------
    >>> RGB = np.array([19.99370783, 20.00393634, 20.01326387])
    >>> RGB_to_rgb(RGB)  # doctest: +ELLIPSIS
    array([ 19.9969397...,  20.0018612...,  20.0135053...])
    """

    rgb = vector_dot(matrix_dot(MATRIX_XYZ_TO_HPE, CAT_INVERSE_CAT02), RGB)

    return rgb


def rgb_to_RGB(rgb: ArrayLike) -> NDArray:
    """
    Convert given *Hunt-Pointer-Estevez* :math:`\\rho\\gamma\\beta`
    colourspace array to *RGB* array.

    Parameters
    ----------
    rgb
        *Hunt-Pointer-Estevez* :math:`\\rho\\gamma\\beta` colourspace array.

    Returns
    -------
    :class:`numpy.ndarray`
        *RGB* array.

    Examples
    --------
    >>> rgb = np.array([19.99693975, 20.00186123, 20.01350530])
    >>> rgb_to_RGB(rgb)  # doctest: +ELLIPSIS
    array([ 19.9937078...,  20.0039363...,  20.0132638...])
    """

    RGB = vector_dot(matrix_dot(CAT_CAT02, MATRIX_HPE_TO_XYZ), rgb)

    return RGB


def post_adaptation_non_linear_response_compression_forward(
    RGB: ArrayLike, F_L: FloatingOrArrayLike
) -> NDArray:
    """
    Return given *CMCCAT2000* transform sharpened *RGB* array with post
    adaptation non-linear response compression.

    Parameters
    ----------
    RGB
        *CMCCAT2000* transform sharpened *RGB* array.
    F_L
        *Luminance* level adaptation factor :math:`F_L`.

    Returns
    -------
    :class:`numpy.ndarray`
        Compressed *CMCCAT2000* transform sharpened *RGB* array.

    Notes
    -----
    -   This definition implements negative values handling as per
        :cite:`Luo2013`.

    Examples
    --------
    >>> RGB = np.array([19.99693975, 20.00186123, 20.01350530])
    >>> F_L = 1.16754446415
    >>> post_adaptation_non_linear_response_compression_forward(RGB, F_L)
    ... # doctest: +ELLIPSIS
    array([ 7.9463202...,  7.9471152...,  7.9489959...])
    """

    RGB = as_float_array(RGB)
    F_L = as_float_array(F_L)

    F_L_RGB = spow(F_L[..., np.newaxis] * np.absolute(RGB) / 100, 0.42)
    RGB_c = (400 * np.sign(RGB) * F_L_RGB) / (27.13 + F_L_RGB) + 0.1

    return RGB_c


def post_adaptation_non_linear_response_compression_inverse(
    RGB: ArrayLike, F_L: FloatingOrArrayLike
) -> NDArray:
    """
    Return given *CMCCAT2000* transform sharpened *RGB* array without post
    adaptation non-linear response compression.

    Parameters
    ----------
    RGB
        *CMCCAT2000* transform sharpened *RGB* array.
    F_L
        *Luminance* level adaptation factor :math:`F_L`.

    Returns
    -------
    :class:`numpy.ndarray`
        Uncompressed *CMCCAT2000* transform sharpened *RGB* array.

    Examples
    --------
    >>> RGB = np.array([7.94632020, 7.94711528, 7.94899595])
    >>> F_L = 1.16754446415
    >>> post_adaptation_non_linear_response_compression_inverse(RGB, F_L)
    ... # doctest: +ELLIPSIS
    array([ 19.9969397...,  20.0018612...,  20.0135052...])
    """

    RGB = as_float_array(RGB)
    F_L = as_float_array(F_L)

    RGB_p = (
        np.sign(RGB - 0.1)
        * (100 / F_L[..., np.newaxis])
        * spow(
            (27.13 * np.absolute(RGB - 0.1)) / (400 - np.absolute(RGB - 0.1)),
            1 / 0.42,
        )
    )