"""
Automatic Colour Conversion Graph
=================================

Defines the automatic colour conversion graph objects:

-   :func:`colour.describe_conversion_path`
-   :func:`colour.convert`
"""

from __future__ import annotations

import inspect
import numpy as np
import textwrap
from collections import namedtuple
from copy import copy
from functools import partial
from pprint import pformat

import colour
from colour.colorimetry import (
    CCS_ILLUMINANTS,
    MultiSpectralDistributions,
    SDS_ILLUMINANTS,
    SpectralDistribution,
    TVS_ILLUMINANTS_HUNTERLAB,
)
from colour.colorimetry import (
    colorimetric_purity,
    complementary_wavelength,
    dominant_wavelength,
    excitation_purity,
    lightness,
    luminance,
    luminous_efficacy,
    luminous_efficiency,
    luminous_flux,
    whiteness,
    yellowness,
    wavelength_to_XYZ,
)
from colour.hints import (
    Any,
    ArrayLike,
    Callable,
    Dict,
    FloatingOrArrayLike,
    FloatingOrNDArray,
    Integer,
    List,
    Literal,
    NDArray,
    Number,
    Optional,
    Union,
    cast,
)
from colour.recovery import XYZ_to_sd
from colour.models import RGB_COLOURSPACE_sRGB
from colour.models import (
    CAM02LCD_to_JMh_CIECAM02,
    CAM02SCD_to_JMh_CIECAM02,
    CAM02UCS_to_JMh_CIECAM02,
    CAM16LCD_to_JMh_CAM16,
    CAM16SCD_to_JMh_CAM16,
    CAM16UCS_to_JMh_CAM16,
    CMYK_to_CMY,
    CMY_to_CMYK,
    CMY_to_RGB,
    DIN99_to_XYZ,
    HCL_to_RGB,
    HSL_to_RGB,
    HSV_to_RGB,
    Hunter_Lab_to_XYZ,
    Hunter_Rdab_to_XYZ,
    ICaCb_to_XYZ,
    ICtCp_to_XYZ,
    IHLS_to_RGB,
    IgPgTg_to_XYZ,
    IPT_to_XYZ,
    JMh_CAM16_to_CAM16LCD,
    JMh_CAM16_to_CAM16SCD,
    JMh_CAM16_to_CAM16UCS,
    JMh_CIECAM02_to_CAM02LCD,
    JMh_CIECAM02_to_CAM02SCD,
    JMh_CIECAM02_to_CAM02UCS,
    Jzazbz_to_XYZ,
    LCHab_to_Lab,
    LCHuv_to_Luv,
    Lab_to_LCHab,
    Lab_to_XYZ,
    Luv_to_LCHuv,
    Luv_to_XYZ,
    Luv_to_uv,
    Luv_uv_to_xy,
    OSA_UCS_to_XYZ,
    Oklab_to_XYZ,
    Prismatic_to_RGB,
    ProLab_to_XYZ,
    RGB_Colourspace,
    RGB_luminance,
    RGB_to_CMY,
    RGB_to_HCL,
    RGB_to_HSL,
    RGB_to_HSV,
    RGB_to_IHLS,
    RGB_to_Prismatic,
    RGB_to_RGB,
    RGB_to_XYZ,
    RGB_to_YCbCr,
    RGB_to_YCoCg,
    RGB_to_YcCbcCrc,
    UCS_to_XYZ,
    UCS_to_uv,
    UCS_uv_to_xy,
    UVW_to_XYZ,
    XYZ_to_DIN99,
    XYZ_to_Hunter_Lab,
    XYZ_to_Hunter_Rdab,
    XYZ_to_ICaCb,
    XYZ_to_ICtCp,
    XYZ_to_IgPgTg,
    XYZ_to_IPT,
    XYZ_to_Jzazbz,
    XYZ_to_Lab,
    XYZ_to_Luv,
    XYZ_to_OSA_UCS,
    XYZ_to_Oklab,
    XYZ_to_ProLab,
    XYZ_to_RGB,
    XYZ_to_UCS,
    XYZ_to_UVW,
    XYZ_to_hdr_CIELab,
    XYZ_to_hdr_IPT,
    XYZ_to_sRGB,
    XYZ_to_xy,
    XYZ_to_xyY,
    YCbCr_to_RGB,
    YCoCg_to_RGB,
    YcCbcCrc_to_RGB,
    cctf_decoding,
    cctf_encoding,
    hdr_CIELab_to_XYZ,
    hdr_IPT_to_XYZ,
    sRGB_to_XYZ,
    uv_to_Luv,
    uv_to_UCS,
    xyY_to_XYZ,
    xyY_to_xy,
    xy_to_Luv_uv,
    xy_to_UCS_uv,
    xy_to_XYZ,
    xy_to_xyY,
)
from colour.notation import (
    HEX_to_RGB,
    RGB_to_HEX,
    munsell_value,
    munsell_colour_to_xyY,
    xyY_to_munsell_colour,
)
from colour.quality import colour_quality_scale, colour_rendering_index
from colour.appearance import (
    CAM_Specification_CAM16,
    CAM16_to_XYZ,
    CAM_Specification_CIECAM02,
    CIECAM02_to_XYZ,
    Kim2009_to_XYZ,
    XYZ_to_ATD95,
    XYZ_to_CAM16,
    XYZ_to_CIECAM02,
    XYZ_to_Hunt,
    XYZ_to_Kim2009,
    XYZ_to_LLAB,
    XYZ_to_Nayatani95,
    XYZ_to_RLAB,
    XYZ_to_ZCAM,
    ZCAM_to_XYZ,
)
from colour.appearance.ciecam02 import CAM_KWARGS_CIECAM02_sRGB
from colour.temperature import CCT_to_uv, uv_to_CCT
from colour.utilities import (
    as_float_array,
    domain_range_scale,
    filter_kwargs,
    message_box,
    optional,
    required,
    tsplit,
    tstack,
    usage_warning,
    validate_method,
)

__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__ = [
    "Conversion_Specification",
    "sd_to_XYZ",
    "CIECAM02_to_JMh_CIECAM02",
    "JMh_CIECAM02_to_CIECAM02",
    "CAM16_to_JMh_CAM16",
    "JMh_CAM16_to_CAM16",
    "XYZ_to_luminance",
    "RGB_luminance_to_RGB",
    "CONVERSION_SPECIFICATIONS_DATA",
    "CONVERSION_GRAPH_NODE_LABELS",
    "CONVERSION_SPECIFICATIONS",
    "CONVERSION_GRAPH",
    "describe_conversion_path",
    "convert",
]


class Conversion_Specification(
    namedtuple(
        "Conversion_Specification", ("source", "target", "conversion_function")
    )
):
    """
    Conversion specification for *Colour* graph for automatic colour
    conversion describing two nodes and the edge in the graph.

    Parameters
    ----------
    source
        Source node in the graph.
    target
        Target node in the graph.
    conversion_function
        Callable converting from the ``source`` node to the ``target`` node.
    """

    def __new__(cls, source: str, target: str, conversion_function: Callable):
        """
        Return a new instance of the
        :class:`colour.graph.conversion.Conversion_Specification` class.
        """

        return super().__new__(
            cls, source.lower(), target.lower(), conversion_function
        )


def sd_to_XYZ(
    sd: Union[ArrayLike, SpectralDistribution, MultiSpectralDistributions],
    cmfs: Optional[MultiSpectralDistributions] = None,
    illuminant: Optional[SpectralDistribution] = None,
    k: Optional[Number] = None,
    method: Union[Literal["ASTM E308", "Integration"], str] = "ASTM E308",
    **kwargs: Any,
) -> NDArray:
    """
    Convert given spectral distribution to *CIE XYZ* tristimulus values using
    given colour matching functions, illuminant and method.

    This placeholder docstring is replaced with the modified
    :func:`colour.sd_to_XYZ` definition docstring.
    """

    illuminant = cast(
        SpectralDistribution,
        optional(illuminant, SDS_ILLUMINANTS[_ILLUMINANT_DEFAULT]),
    )

    return colour.sd_to_XYZ(sd, cmfs, illuminant, k, method, **kwargs)


# If-clause required for optimised python launch.
if colour.sd_to_XYZ.__doc__ is not None:
    sd_to_XYZ.__doc__ = colour.sd_to_XYZ.__doc__.replace(
        "CIE Illuminant E",
        "CIE Standard Illuminant D65",
    ).replace(
        "sd_to_XYZ(sd)",
        "sd_to_XYZ(sd)  # doctest: +SKIP",
    )


def CIECAM02_to_JMh_CIECAM02(
    specification: CAM_Specification_CIECAM02,
) -> NDArray:
    """
    Convert from *CIECAM02* specification to *CIECAM02* :math:`JMh`
    correlates.

    Parameters
    ----------
    specification
        *CIECAM02* colour appearance model specification.

    Returns
    -------
    :class:`numpy.ndarray`
        *CIECAM02* :math:`JMh` correlates.

    Examples
    --------
    >>> specification = CAM_Specification_CIECAM02(J=41.731091132513917,
    ...                                            M=0.108842175669226,
    ...                                            h=219.048432658311780)
    >>> CIECAM02_to_JMh_CIECAM02(specification)  # doctest: +ELLIPSIS
    array([  4.1731091...e+01,   1.0884217...e-01,   2.1904843...e+02])
    """

    return tstack([specification.J, specification.M, specification.h])


def JMh_CIECAM02_to_CIECAM02(JMh: ArrayLike) -> CAM_Specification_CIECAM02:
    """
    Convert from *CIECAM02* :math:`JMh` correlates to *CIECAM02*
    specification.

    Parameters
    ----------
    JMh
         *CIECAM02* :math:`JMh` correlates.

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

    Examples
    --------
    >>> import numpy as np
    >>> JMh = np.array([4.17310911e+01, 1.08842176e-01, 2.19048433e+02])
    >>> JMh_CIECAM02_to_CIECAM02(JMh)  # doctest: +ELLIPSIS
    CAM_Specification_CIECAM02(J=41.7310911..., C=None, h=219.0484329..., \
s=None, Q=None, M=0.1088421..., H=None, HC=None)
    """

    J, M, h = tsplit(JMh)

    return CAM_Specification_CIECAM02(J=J, M=M, h=h)


def CAM16_to_JMh_CAM16(specification) -> NDArray:
    """
    Convert from *CAM16* specification to *CAM16* :math:`JMh` correlates.

    Parameters
    ----------
    specification
        *CAM16* colour appearance model specification.

    Returns
    -------
    :class:`numpy.ndarray`
        *CAM16* :math:`JMh` correlates.

    Examples
    --------
    >>> specification = CAM_Specification_CAM16(J=41.731207905126638,
    ...                                         M=0.107436772335905,
    ...                                         h=217.067959767393010)
    >>> CAM16_to_JMh_CAM16(specification)  # doctest: +ELLIPSIS
    array([  4.1731207...e+01,   1.0743677...e-01,   2.1706796...e+02])
    """

    return tstack([specification.J, specification.M, specification.h])


def JMh_CAM16_to_CAM16(JMh: ArrayLike) -> CAM_Specification_CAM16:
    """
    Convert from *CAM6* :math:`JMh` correlates to *CAM6* specification.

    Parameters
    ----------
    JMh
         *CAM6* :math:`JMh` correlates.

    Returns
    -------
    :class:`colour.CAM6_Specification`
        *CAM6* colour appearance model specification.

    Examples
    --------
    >>> import numpy as np
    >>> JMh = np.array([4.17312079e+01, 1.07436772e-01, 2.17067960e+02])
    >>> JMh_CAM16_to_CAM16(JMh)  # doctest: +ELLIPSIS
    CAM_Specification_CAM16(J=41.7312079..., C=None, h=217.06796..., s=None, \
Q=None, M=0.1074367..., H=None, HC=None)
    """

    J, M, h = tsplit(JMh)

    return CAM_Specification_CAM16(J=J, M=M, h=h)


def XYZ_to_luminance(XYZ: ArrayLike) -> FloatingOrNDArray:
    """
    Convert from *CIE XYZ* tristimulus values to *luminance* :math:`Y`.

    Parameters
    ----------
    XYZ
        *CIE XYZ* tristimulus values.

    Returns
    -------
    :class:`numpy.floating` or :class:`numpy.ndarray`
        *Luminance* :math:`Y`.

    Examples
    --------
    >>> import numpy as np
    >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952])
    >>> XYZ_to_luminance(XYZ)  # doctest: +ELLIPSIS
    0.1219722...
    """

    _X, Y, _Z = tsplit(XYZ)

    return Y


def RGB_luminance_to_RGB(Y: FloatingOrArrayLike) -> NDArray:
    """
    Convert from *luminance* :math:`Y` to *RGB*.

    Parameters
    ----------
    Y
        *Luminance* :math:`Y`.

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

    Examples
    --------
    >>> RGB_luminance_to_RGB(0.123014562384318)  # doctest: +ELLIPSIS
    array([ 0.1230145...,  0.1230145...,  0.1230145...])
    """

    Y = as_float_array(Y)

    return tstack([Y, Y, Y])


_ILLUMINANT_DEFAULT: str = "D65"
"""Default automatic colour conversion graph illuminant name."""

_CCS_ILLUMINANT_DEFAULT: NDArray = CCS_ILLUMINANTS[
    "CIE 1931 2 Degree Standard Observer"
][_ILLUMINANT_DEFAULT]
"""
Default automatic colour conversion graph illuminant *CIE xy* chromaticity
coordinates.
"""

_TVS_ILLUMINANT_DEFAULT: NDArray = xy_to_XYZ(_CCS_ILLUMINANT_DEFAULT)
"""
Default automatic colour conversion graph illuminant *CIE XYZ* tristimulus
values.
"""

_RGB_COLOURSPACE_DEFAULT: RGB_Colourspace = RGB_COLOURSPACE_sRGB
"""Default automatic colour conversion graph *RGB* colourspace."""

_CAM_KWARGS_CIECAM02_sRGB: Dict = CAM_KWARGS_CIECAM02_sRGB.copy()
"""
Default parameter values for the *CIECAM02* colour appearance model usage in
the context of *sRGB*.

Warnings
--------
The *CIE XYZ* tristimulus values of reference white :math:`XYZ_w` is adjusted
for the domain-range scale **'1'**.
"""

_CAM_KWARGS_CIECAM02_sRGB["XYZ_w"] = _CAM_KWARGS_CIECAM02_sRGB["XYZ_w"] / 100

CONVERSION_SPECIFICATIONS_DATA: List = [
    # Colorimetry
    ("Spectral Distribution", "CIE XYZ", sd_to_XYZ),
    ("CIE XYZ", "Spectral Distribution", XYZ_to_sd),
    ("Spectral Distribution", "Luminous Flux", luminous_flux),
    ("Spectral Distribution", "Luminous Efficiency", luminous_efficiency),
    ("Spectral Distribution", "Luminous Efficacy", luminous_efficacy),
    ("CIE XYZ", "Luminance", XYZ_to_luminance),
    ("Luminance", "Lightness", lightness),
    ("Lightness", "Luminance", luminance),
    (
        "CIE XYZ",
        "Whiteness",
        partial(whiteness, XYZ_0=_TVS_ILLUMINANT_DEFAULT),
    ),
    ("CIE XYZ", "Yellowness", yellowness),
    (
        "CIE xy",
        "Colorimetric Purity",
        partial(colorimetric_purity, xy_n=_CCS_ILLUMINANT_DEFAULT),
    ),
    (
        "CIE xy",
        "Complementary Wavelength",
        partial(complementary_wavelength, xy_n=_CCS_ILLUMINANT_DEFAULT),
    ),
    (
        "CIE xy",
        "Dominant Wavelength",
        partial(dominant_wavelength, xy_n=_CCS_ILLUMINANT_DEFAULT),
    ),
    (
        "CIE xy",
        "Excitation Purity",
        partial(excitation_purity, xy_n=_CCS_ILLUMINANT_DEFAULT),
    ),
    ("Wavelength", "CIE XYZ", wavelength_to_XYZ),
    # Colour Models
    ("CIE XYZ", "CIE xyY", XYZ_to_xyY),
    ("CIE xyY", "CIE XYZ", xyY_to_XYZ),
    ("CIE xyY", "CIE xy", xyY_to_xy),
    ("CIE xy", "CIE xyY", xy_to_xyY),
    ("CIE XYZ", "CIE xy", XYZ_to_xy),
    ("CIE xy", "CIE XYZ", xy_to_XYZ),
    ("CIE XYZ", "CIE Lab", XYZ_to_Lab),
    ("CIE Lab", "CIE XYZ", Lab_to_XYZ),
    ("CIE Lab", "CIE LCHab", Lab_to_LCHab),
    ("CIE LCHab", "CIE Lab", LCHab_to_Lab),
    ("CIE XYZ", "CIE Luv", XYZ_to_Luv),
    ("CIE Luv", "CIE XYZ", Luv_to_XYZ),
    ("CIE Luv", "CIE Luv uv", Luv_to_uv),
    ("CIE Luv uv", "CIE Luv", uv_to_Luv),
    ("CIE Luv uv", "CIE xy", Luv_uv_to_xy),
    ("CIE xy", "CIE Luv uv", xy_to_Luv_uv),
    ("CIE Luv", "CIE LCHuv", Luv_to_LCHuv),
    ("CIE LCHuv", "CIE Luv", LCHuv_to_Luv),
    ("CIE XYZ", "CIE UCS", XYZ_to_UCS),
    ("CIE UCS", "CIE XYZ", UCS_to_XYZ),
    ("CIE UCS", "CIE UCS uv", UCS_to_uv),
    ("CIE UCS uv", "CIE UCS", uv_to_UCS),
    ("CIE UCS uv", "CIE xy", UCS_uv_to_xy),
    ("CIE xy", "CIE UCS uv", xy_to_UCS_uv),
    ("CIE XYZ", "CIE UVW", XYZ_to_UVW),
    ("CIE UVW", "CIE XYZ", UVW_to_XYZ),
    ("CIE XYZ", "DIN99", XYZ_to_DIN99),
    ("DIN99", "CIE XYZ", DIN99_to_XYZ),
    ("CIE XYZ", "hdr-CIELAB", XYZ_to_hdr_CIELab),
    ("hdr-CIELAB", "CIE XYZ", hdr_CIELab_to_XYZ),
    (
        "CIE XYZ",
        "Hunter Lab",
        partial(
            XYZ_to_Hunter_Lab,
            XYZ_n=TVS_ILLUMINANTS_HUNTERLAB[
                "CIE 1931 2 Degree Standard Observer"
            ]["D65"].XYZ_n
            / 100,
        ),
    ),
    (
        "Hunter Lab",
        "CIE XYZ",
        partial(
            Hunter_Lab_to_XYZ,
            XYZ_n=TVS_ILLUMINANTS_HUNTERLAB[
                "CIE 1931 2 Degree Standard Observer"
            ]["D65"].XYZ_n
            / 100,
        ),
    ),
    (
        "CIE XYZ",
        "Hunter Rdab",
        partial(
            XYZ_to_Hunter_Rdab,
            XYZ_n=TVS_ILLUMINANTS_HUNTERLAB[
                "CIE 1931 2 Degree Standard Observer"
            ]["D65"].XYZ_n
            / 100,
        ),
    ),
    (
        "Hunter Rdab",
        "CIE XYZ",
        partial(
            Hunter_Rdab_to_XYZ,
            XYZ_n=TVS_ILLUMINANTS_HUNTERLAB[
                "CIE 1931 2 Degree Standard Observer"
            ]["D65"].XYZ_n
            / 100,
        ),
    ),
    ("CIE XYZ", "ICaCb", XYZ_to_ICaCb),
    ("ICaCb", "CIE XYZ", ICaCb_to_XYZ),
    ("CIE XYZ", "ICtCp", XYZ_to_ICtCp),
    ("ICtCp", "CIE XYZ", ICtCp_to_XYZ),
    ("CIE XYZ", "IgPgTg", XYZ_to_IgPgTg),
    ("IgPgTg", "CIE XYZ", IgPgTg_to_XYZ),
    ("CIE XYZ", "IPT", XYZ_to_IPT),
    ("IPT", "CIE XYZ", IPT_to_XYZ),
    ("CIE XYZ", "Jzazbz", XYZ_to_Jzazbz),
    ("Jzazbz", "CIE XYZ", Jzazbz_to_XYZ),
    ("CIE XYZ", "hdr-IPT", XYZ_to_hdr_IPT),
    ("hdr-IPT", "CIE XYZ", hdr_IPT_to_XYZ),
    ("CIE XYZ", "OSA UCS", XYZ_to_OSA_UCS),
    ("OSA UCS", "CIE XYZ", OSA_UCS_to_XYZ),
    ("CIE XYZ", "Oklab", XYZ_to_Oklab),
    ("Oklab", "CIE XYZ", Oklab_to_XYZ),
    ("CIE XYZ", "ProLab", XYZ_to_ProLab),
    ("ProLab", "CIE XYZ", ProLab_to_XYZ),
    # RGB Colour Models
    (
        "CIE XYZ",
        "RGB",
        partial(
            XYZ_to_RGB,
            illuminant_XYZ=_RGB_COLOURSPACE_DEFAULT.whitepoint,
            illuminant_RGB=_RGB_COLOURSPACE_DEFAULT.whitepoint,
            matrix_XYZ_to_RGB=_RGB_COLOURSPACE_DEFAULT.matrix_XYZ_to_RGB,
        ),
    ),
    (
        "RGB",
        "CIE XYZ",
        partial(
            RGB_to_XYZ,
            illuminant_RGB=_RGB_COLOURSPACE_DEFAULT.whitepoint,
            illuminant_XYZ=_RGB_COLOURSPACE_DEFAULT.whitepoint,
            matrix_RGB_to_XYZ=_RGB_COLOURSPACE_DEFAULT.matrix_RGB_to_XYZ,
        ),
    ),
    (
        "RGB",
        "Scene-Referred RGB",
        partial(
            RGB_to_RGB,
            input_colourspace=_RGB_COLOURSPACE_DEFAULT,
            output_colourspace=_RGB_COLOURSPACE_DEFAULT,
        ),
    ),
    (
        "Scene-Referred RGB",
        "RGB",
        partial(
            RGB_to_RGB,
            input_colourspace=_RGB_COLOURSPACE_DEFAULT,
            output_colourspace=_RGB_COLOURSPACE_DEFAULT,
        ),
    ),
    ("RGB", "HSV", RGB_to_HSV),
    ("HSV", "RGB", HSV_to_RGB),
    ("RGB", "HSL", RGB_to_HSL),
    ("HSL", "RGB", HSL_to_RGB),
    ("RGB", "HCL", RGB_to_HCL),
    ("HCL", "RGB", HCL_to_RGB),
    ("RGB", "IHLS", RGB_to_IHLS),
    ("IHLS", "RGB", IHLS_to_RGB),
    ("CMY", "RGB", CMY_to_RGB),
    ("RGB", "CMY", RGB_to_CMY),
    ("CMY", "CMYK", CMY_to_CMYK),
    ("CMYK", "CMY", CMYK_to_CMY),
    (
        "RGB",
        "RGB Luminance",
        partial(
            RGB_luminance,
            primaries=_RGB_COLOURSPACE_DEFAULT.primaries,
            whitepoint=_RGB_COLOURSPACE_DEFAULT.whitepoint,
        ),
    ),
    ("RGB Luminance", "RGB", RGB_luminance_to_RGB),
    ("RGB", "Prismatic", RGB_to_Prismatic),
    ("Prismatic", "RGB", Prismatic_to_RGB),
    ("Output-Referred RGB", "YCbCr", RGB_to_YCbCr),
    ("YCbCr", "Output-Referred RGB", YCbCr_to_RGB),
    ("RGB", "YcCbcCrc", RGB_to_YcCbcCrc),
    ("YcCbcCrc", "RGB", YcCbcCrc_to_RGB),
    ("Output-Referred RGB", "YCoCg", RGB_to_YCoCg),
    ("YCoCg", "Output-Referred RGB", YCoCg_to_RGB),
    ("RGB", "Output-Referred RGB", cctf_encoding),
    ("Output-Referred RGB", "RGB", cctf_decoding),
    ("Scene-Referred RGB", "Output-Referred RGB", cctf_encoding),
    ("Output-Referred RGB", "Scene-Referred RGB", cctf_decoding),
    ("CIE XYZ", "sRGB", XYZ_to_sRGB),
    ("sRGB", "CIE XYZ", sRGB_to_XYZ),
    # Colour Notation Systems
    ("Output-Referred RGB", "Hexadecimal", RGB_to_HEX),
    ("Hexadecimal", "Output-Referred RGB", HEX_to_RGB),
    ("CIE xyY", "Munsell Colour", xyY_to_munsell_colour),
    ("Munsell Colour", "CIE xyY", munsell_colour_to_xyY),
    ("Luminance", "Munsell Value", munsell_value),
    ("Munsell Value", "Luminance", partial(luminance, method="ASTM D1535")),
    # Colour Quality
    ("Spectral Distribution", "CRI", colour_rendering_index),
    ("Spectral Distribution", "CQS", colour_quality_scale),
    # Colour Temperature
    ("CCT", "CIE UCS uv", CCT_to_uv),
    ("CIE UCS uv", "CCT", uv_to_CCT),
    # Advanced Colorimetry
    (
        "CIE XYZ",
        "ATD95",
        partial(
            XYZ_to_ATD95,
            XYZ_0=_TVS_ILLUMINANT_DEFAULT,
            Y_0=80 * 0.2,
            k_1=0,
            k_2=(15 + 50) / 2,
        ),
    ),
    (
        "CIE XYZ",
        "CIECAM02",
        partial(XYZ_to_CIECAM02, **_CAM_KWARGS_CIECAM02_sRGB),
    ),
    (
        "CIECAM02",
        "CIE XYZ",
        partial(CIECAM02_to_XYZ, **_CAM_KWARGS_CIECAM02_sRGB),
    ),
    ("CIECAM02", "CIECAM02 JMh", CIECAM02_to_JMh_CIECAM02),
    ("CIECAM02 JMh", "CIECAM02", JMh_CIECAM02_to_CIECAM02),
    ("CIE XYZ", "CAM16", partial(XYZ_to_CAM16, **_CAM_KWARGS_CIECAM02_sRGB)),
    ("CAM16", "CIE XYZ", partial(CAM16_to_XYZ, **_CAM_KWARGS_CIECAM02_sRGB)),
    ("CAM16", "CAM16 JMh", CAM16_to_JMh_CAM16),
    ("CAM16 JMh", "CAM16", JMh_CAM16_to_CAM16),
    (
        "CIE XYZ",
        "Kim 2009",
        partial(XYZ_to_Kim2009, XYZ_w=_TVS_ILLUMINANT_DEFAULT, L_A=80 * 0.2),
    ),
    (
        "Kim 2009",
        "CIE XYZ",
        partial(Kim2009_to_XYZ, XYZ_w=_TVS_ILLUMINANT_DEFAULT, L_A=80 * 0.2),
    ),
    (
        "CIE XYZ",
        "Hunt",
        partial(
            XYZ_to_Hunt,
            XYZ_w=_TVS_ILLUMINANT_DEFAULT,
            XYZ_b=_TVS_ILLUMINANT_DEFAULT,
            L_A=80 * 0.2,
            CCT_w=6504,
        ),
    ),
    (
        "CIE XYZ",
        "LLAB",
        partial(
            XYZ_to_LLAB, XYZ_0=_TVS_ILLUMINANT_DEFAULT, Y_b=80 * 0.2, L=80
        ),
    ),
    (
        "CIE XYZ",
        "Nayatani95",
        partial(
            XYZ_to_Nayatani95,
            XYZ_n=_TVS_ILLUMINANT_DEFAULT,
            Y_o=0.2,
            E_o=1000,
            E_or=1000,
        ),
    ),
    (
        "CIE XYZ",
        "RLAB",
        partial(XYZ_to_RLAB, XYZ_n=_TVS_ILLUMINANT_DEFAULT, Y_n=20),
    ),
    (
        "CIE XYZ",
        "ZCAM",
        partial(
            XYZ_to_ZCAM,
            XYZ_w=_TVS_ILLUMINANT_DEFAULT,
            L_A=64 / np.pi * 0.2,
            Y_b=20,
        ),
    ),
    (
        "ZCAM",
        "CIE XYZ",
        partial(
            ZCAM_to_XYZ,
            XYZ_w=_TVS_ILLUMINANT_DEFAULT,
            L_A=64 / np.pi * 0.2,
            Y_b=20,
        ),
    ),
    ("CIECAM02 JMh", "CAM02LCD", JMh_CIECAM02_to_CAM02LCD),
    ("CAM02LCD", "CIECAM02 JMh", CAM02LCD_to_JMh_CIECAM02),
    ("CIECAM02 JMh", "CAM02SCD", JMh_CIECAM02_to_CAM02SCD),
    ("CAM02SCD", "CIECAM02 JMh", CAM02SCD_to_JMh_CIECAM02),
    ("CIECAM02 JMh", "CAM02UCS", JMh_CIECAM02_to_CAM02UCS),
    ("CAM02UCS", "CIECAM02 JMh", CAM02UCS_to_JMh_CIECAM02),
    ("CAM16 JMh", "CAM16LCD", JMh_CAM16_to_CAM16LCD),
    ("CAM16LCD", "CAM16 JMh", CAM16LCD_to_JMh_CAM16),
    ("CAM16 JMh", "CAM16SCD", JMh_CAM16_to_CAM16SCD),
    ("CAM16SCD", "CAM16 JMh", CAM16SCD_to_JMh_CAM16),
    ("CAM16 JMh", "CAM16UCS", JMh_CAM16_to_CAM16UCS),
    ("CAM16UCS", "CAM16 JMh", CAM16UCS_to_JMh_CAM16),
]
"""
Automatic colour conversion graph specifications data describing two nodes and
the edge in the graph.
"""

CONVERSION_SPECIFICATIONS: List = [
    Conversion_Specification(*specification)
    for specification in CONVERSION_SPECIFICATIONS_DATA
]
"""
Automatic colour conversion graph specifications describing two nodes and
the edge in the graph.
"""

CONVERSION_GRAPH_NODE_LABELS: Dict = {
    specification[0].lower(): specification[0]
    for specification in CONVERSION_SPECIFICATIONS_DATA
}
"""Automatic colour conversion graph node labels."""

CONVERSION_GRAPH_NODE_LABELS.update(
    {
        specification[1].lower(): specification[1]
        for specification in CONVERSION_SPECIFICATIONS_DATA
    }
)


@required("NetworkX")
def _build_graph() -> networkx.DiGraph:  # type: ignore[name-defined]  # noqa
    """
    Build the automatic colour conversion graph.

    Returns
    -------
    :class:`networkx.DiGraph`
         Automatic colour conversion graph.
    """

    import networkx as nx

    graph = nx.DiGraph()

    for specification in CONVERSION_SPECIFICATIONS:
        graph.add_edge(
            specification.source,
            specification.target,
            conversion_function=specification.conversion_function,
        )

    return graph


CONVERSION_GRAPH: (  # type: ignore[name-defined]
    Optional[networkx.DiGraph]  # noqa
) = None
"""Automatic colour conversion graph."""


@required("NetworkX")
def _conversion_path(source: str, target: str) -> List[Callable]:
    """
    Return the conversion path from the source node to the target node in the
    automatic colour conversion graph.

    Parameters
    ----------
    source
        Source node.
    target
        Target node.

    Returns
    -------
    :class:`list`
        Conversion path from the source node to the target node, i.e. a list of
        conversion function callables.

    Examples
    --------
    >>> _conversion_path('cie lab', 'cct')
    ... # doctest: +ELLIPSIS
    [<function Lab_to_XYZ at 0x...>, <function XYZ_to_xy at 0x...>, \
<function xy_to_UCS_uv at 0x...>, <function uv_to_CCT at 0x...>]
    """

    import networkx as nx

    global CONVERSION_GRAPH

    if CONVERSION_GRAPH is None:
        # Updating the :attr:`CONVERSION_GRAPH` attributes.
        colour.graph.CONVERSION_GRAPH = CONVERSION_GRAPH = _build_graph()

    path = nx.shortest_path(CONVERSION_GRAPH, source, target)

    return [
        CONVERSION_GRAPH.get_edge_data(a, b)["conversion_function"]
        for a, b in zip(path[:-1], path[1:])
    ]


def _lower_order_function(callable_: Callable) -> Callable:
    """
    Return the lower order function associated with given callable, i.e.
    the function wrapped by a partial object.

    Parameters
    ----------
    callable_
        Callable to return the lower order function.

    Returns
    -------
    Callable
        Lower order function or given callable if no lower order function
        exists.
    """

    return callable_.func if isinstance(callable_, partial) else callable_


def describe_conversion_path(
    source: str,
    target: str,
    mode: Union[Literal["Short", "Long", "Extended"], str] = "Short",
    width: Integer = 79,
    padding: Integer = 3,
    print_callable: Callable = print,
    **kwargs: Any,
):
    """
    Describe the conversion path from source colour representation to target
    colour representation using the automatic colour conversion graph.

    Parameters
    ----------
    source
        Source colour representation, i.e. the source node in the automatic
        colour conversion graph.
    target
        Target colour representation, i.e. the target node in the automatic
        colour conversion graph.
    mode
        Verbose mode: *Short* describes the conversion path, *Long* provides
        details about the arguments, definitions signatures and output values,
        *Extended* appends the definitions' documentation.
    width
        Message box width.
    padding
        Padding on each side of the message.
    print_callable
        Callable used to print the message box.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.convert`},
        See the documentation of the previously listed definition.

    Examples
    --------
    >>> describe_conversion_path('Spectral Distribution', 'sRGB', width=75)
    ===========================================================================
    *                                                                         *
    *   [ Conversion Path ]                                                   *
    *                                                                         *
    *   "sd_to_XYZ" --> "XYZ_to_sRGB"                                         *