"""
Gamut Section Plotting
======================

Define the gamut section plotting objects.

-   :func:`colour.plotting.section.plot_hull_section_colours`
-   :func:`colour.plotting.section.plot_hull_section_contour`
-   :func:`colour.plotting.plot_visible_spectrum_section`
-   :func:`colour.plotting.plot_RGB_colourspace_section`
"""

from __future__ import annotations

import typing

import numpy as np

if typing.TYPE_CHECKING:
    from matplotlib.axes import Axes

from matplotlib.collections import LineCollection

if typing.TYPE_CHECKING:
    from matplotlib.figure import Figure

from matplotlib.patches import Polygon

from colour.colorimetry import (
    MultiSpectralDistributions,
    SpectralDistribution,
    SpectralShape,
    reshape_msds,
)
from colour.geometry import hull_section, primitive_cube
from colour.graph import colourspace_model_to_reference, convert

if typing.TYPE_CHECKING:
    from colour.hints import (
        Any,
        ArrayLike,
        Dict,
        Literal,
        LiteralColourspaceModel,
        LiteralRGBColourspace,
        Sequence,
        Tuple,
    )

from colour.hints import Real, cast
from colour.models import (
    COLOURSPACE_MODELS_AXIS_LABELS,
    RGB_Colourspace,
    RGB_to_XYZ,
)
from colour.notation import HEX_to_RGB
from colour.plotting import (
    CONSTANTS_COLOUR_STYLE,
    XYZ_to_plotting_colourspace,
    artist,
    colourspace_model_axis_reorder,
    filter_cmfs,
    filter_illuminants,
    filter_RGB_colourspaces,
    override_style,
    render,
)
from colour.utilities import (
    CanonicalMapping,
    as_int_array,
    first_item,
    full,
    ones,
    optional,
    required,
    suppress_warnings,
    tstack,
    validate_method,
)
from colour.volume import solid_RoschMacAdam

__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__ = [
    "MAPPING_AXIS_TO_PLANE",
    "plot_hull_section_colours",
    "plot_hull_section_contour",
    "plot_visible_spectrum_section",
    "plot_RGB_colourspace_section",
]

MAPPING_AXIS_TO_PLANE: CanonicalMapping = CanonicalMapping(
    {"+x": (1, 2), "+y": (0, 2), "+z": (0, 1)}
)
MAPPING_AXIS_TO_PLANE.__doc__ = """
Mapping from axes to their orthogonal planes.

Maps each positive axis ('+x', '+y', '+z') to the indices of the two
dimensions that form the perpendicular plane in 3D space.
"""


@required("trimesh")
@override_style()
def plot_hull_section_colours(
    hull: trimesh.Trimesh,  # pyright: ignore  # noqa: F821
    model: LiteralColourspaceModel | str = "CIE xyY",
    axis: Literal["+z", "+x", "+y"] | str = "+z",
    origin: float = 0.5,
    normalise: bool = True,
    section_colours: ArrayLike | str | None = None,
    section_opacity: float = 1,
    convert_kwargs: dict | None = None,
    samples: int = 256,
    **kwargs: Any,
) -> Tuple[Figure, Axes]:
    """
    Plot the section colours of the specified *trimesh* hull along the
    specified axis and origin.

    Parameters
    ----------
    hull
        *Trimesh* hull.
    model
        Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute
        for the list of supported colourspace models.
    axis
        Axis the hull section will be normal to.
    origin
        Coordinate along ``axis`` at which to plot the hull section.
    normalise
        Whether to normalise ``axis`` to the extent of the hull along it.
    section_colours
        Colours of the hull section, if ``section_colours`` is set to *RGB*,
        the colours will be computed using the corresponding coordinates.
    section_opacity
        Opacity of the hull section colours.
    convert_kwargs
        Keyword arguments for the :func:`colour.convert` definition.
    samples
        Sample count on one axis when computing the hull section colours.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`,
        :func:`colour.plotting.render`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    Examples
    --------
    >>> from colour.models import RGB_COLOURSPACE_sRGB
    >>> from colour.utilities import is_trimesh_installed
    >>> vertices, faces, _outline = primitive_cube(1, 1, 1, 64, 64, 64)
    >>> XYZ_vertices = RGB_to_XYZ(vertices["position"] + 0.5, RGB_COLOURSPACE_sRGB)
    >>> if is_trimesh_installed:
    ...     from trimesh import Trimesh
    ...
    ...     hull = Trimesh(XYZ_vertices, faces, process=False)
    ...     plot_hull_section_colours(hull, section_colours="RGB")
    ...     # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...Axes...>)

    .. image:: ../_static/Plotting_Plot_Hull_Section_Colours.png
        :align: center
        :alt: plot_hull_section_colours
    """

    axis = validate_method(
        axis,
        ("+z", "+x", "+y"),
        '"{0}" axis is invalid, it must be one of {1}!',
    )

    hull = hull.copy()

    settings: Dict[str, Any] = {"uniform": True}
    settings.update(kwargs)

    _figure, axes = artist(**settings)

    section_colours = optional(
        section_colours, HEX_to_RGB(CONSTANTS_COLOUR_STYLE.colour.average)
    )

    convert_kwargs = optional(convert_kwargs, {})

    # Luminance / Lightness reordered along "z" axis.
    with suppress_warnings(python_warnings=True):
        ijk_vertices = colourspace_model_axis_reorder(
            convert(hull.vertices, "CIE XYZ", model, **convert_kwargs), model
        )
        ijk_vertices = np.nan_to_num(ijk_vertices)
        ijk_vertices = colourspace_model_to_reference(ijk_vertices, model)

    hull.vertices = ijk_vertices

    if axis == "+x":
        index_origin = 0
    elif axis == "+y":
        index_origin = 1
    elif axis == "+z":
        index_origin = 2
    plane = MAPPING_AXIS_TO_PLANE[axis]

    section = hull_section(hull, axis, origin, normalise)

    padding = 0.1 * np.mean(colourspace_model_to_reference(ones(3), model))
    min_x = np.min(ijk_vertices[..., plane[0]]) - padding
    max_x = np.max(ijk_vertices[..., plane[0]]) + padding
    min_y = np.min(ijk_vertices[..., plane[1]]) - padding
    max_y = np.max(ijk_vertices[..., plane[1]]) + padding
    extent = (min_x, max_x, min_y, max_y)

    use_RGB_section_colours = str(section_colours).upper() == "RGB"
    if use_RGB_section_colours:
        ii, jj = np.meshgrid(
            np.linspace(min_x, max_x, samples),
            np.linspace(max_y, min_y, samples),
        )
        ij = tstack([ii, jj])
        ijk_section = full(
            (samples, samples, 3),
            cast("Real", np.median(section[..., index_origin])),
        )
        ijk_section[..., plane] = ij
        ijk_section = ijk_section / colourspace_model_to_reference(ones(3), model)
        XYZ_section = convert(
            colourspace_model_axis_reorder(ijk_section, model, "Inverse"),
            model,
            "CIE XYZ",
            **convert_kwargs,
        )
        RGB_section = XYZ_to_plotting_colourspace(XYZ_section)
    else:
        section_colours = np.hstack([section_colours, section_opacity])

    facecolor = "none" if use_RGB_section_colours else section_colours
    polygon = Polygon(
        section[..., plane],
        facecolor=facecolor,
        edgecolor="none",
        zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon,
    )
    axes.add_patch(polygon)

    if use_RGB_section_colours:
        image = axes.imshow(
            np.clip(RGB_section, 0, 1),
            interpolation="bilinear",
            extent=extent,  # type: ignore
            clip_path=None,
            alpha=section_opacity,
            zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon,
        )
        image.set_clip_path(polygon)

    settings = {
        "axes": axes,
        "bounding_box": extent,
    }
    settings.update(kwargs)

    return render(**settings)


@required("trimesh")
@override_style()
def plot_hull_section_contour(
    hull: trimesh.Trimesh,  # pyright: ignore  # noqa: F821
    model: LiteralColourspaceModel | str = "CIE xyY",
    axis: Literal["+z", "+x", "+y"] | str = "+z",
    origin: float = 0.5,
    normalise: bool = True,
    contour_colours: ArrayLike | str | None = None,
    contour_opacity: float = 1,
    convert_kwargs: dict | None = None,
    **kwargs: Any,
) -> Tuple[Figure, Axes]:
    """
    Plot the section contour of the specified *trimesh* hull along the
    specified axis and origin.

    Parameters
    ----------
    hull
        *Trimesh* hull.
    model
        Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute
        for the list of supported colourspace models.
    axis
        Axis the hull section will be normal to.
    origin
        Coordinate along ``axis`` at which to plot the hull section.
    normalise
        Whether to normalise ``axis`` to the extent of the hull along it.
    contour_colours
        Colours of the hull section contour, if ``contour_colours`` is set
        to *RGB*, the colours will be computed using the corresponding
        coordinates.
    contour_opacity
        Opacity of the hull section contour.
    convert_kwargs
        Keyword arguments for the :func:`colour.convert` definition.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`,
        :func:`colour.plotting.render`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    Examples
    --------
    >>> from colour.models import RGB_COLOURSPACE_sRGB
    >>> from colour.utilities import is_trimesh_installed
    >>> vertices, faces, _outline = primitive_cube(1, 1, 1, 64, 64, 64)
    >>> XYZ_vertices = RGB_to_XYZ(vertices["position"] + 0.5, RGB_COLOURSPACE_sRGB)
    >>> if is_trimesh_installed:
    ...     from trimesh import Trimesh
    ...
    ...     hull = Trimesh(XYZ_vertices, faces, process=False)
    ...     plot_hull_section_contour(hull, contour_colours="RGB")
    ...     # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...Axes...>)

    .. image:: ../_static/Plotting_Plot_Hull_Section_Contour.png
        :align: center
        :alt: plot_hull_section_contour
    """

    hull = hull.copy()

    contour_colours = optional(contour_colours, CONSTANTS_COLOUR_STYLE.colour.dark)

    settings: Dict[str, Any] = {"uniform": True}
    settings.update(kwargs)

    _figure, axes = artist(**settings)

    convert_kwargs = optional(convert_kwargs, {})

    # Luminance / Lightness is re-ordered along "z-up" axis.
    with suppress_warnings(python_warnings=True):
        ijk_vertices = colourspace_model_axis_reorder(
            convert(hull.vertices, "CIE XYZ", model, **convert_kwargs), model
        )
        ijk_vertices = np.nan_to_num(ijk_vertices)
        ijk_vertices = colourspace_model_to_reference(ijk_vertices, model)

    hull.vertices = ijk_vertices

    plane = MAPPING_AXIS_TO_PLANE[axis]

    padding = 0.1 * np.mean(colourspace_model_to_reference(np.ones(3), model))
    min_x = np.min(ijk_vertices[..., plane[0]]) - padding
    max_x = np.max(ijk_vertices[..., plane[0]]) + padding
    min_y = np.min(ijk_vertices[..., plane[1]]) - padding
    max_y = np.max(ijk_vertices[..., plane[1]]) + padding
    extent = (min_x, max_x, min_y, max_y)

    use_RGB_contour_colours = str(contour_colours).upper() == "RGB"
    section = hull_section(hull, axis, origin, normalise)
    if use_RGB_contour_colours:
        ijk_section = section / colourspace_model_to_reference(np.ones(3), model)
        XYZ_section = convert(
            colourspace_model_axis_reorder(ijk_section, model, "Inverse"),
            model,
            "CIE XYZ",
            **convert_kwargs,
        )
        contour_colours = np.clip(XYZ_to_plotting_colourspace(XYZ_section), 0, 1)

    section = np.reshape(section[..., plane], (-1, 1, 2))
    line_collection = LineCollection(
        np.concatenate([section[:-1], section[1:]], axis=1),  # pyright: ignore
        colors=contour_colours,
        alpha=contour_opacity,
        zorder=CONSTANTS_COLOUR_STYLE.zorder.background_line,
    )
    axes.add_collection(line_collection)

    settings = {
        "axes": axes,
        "bounding_box": extent,
    }
    settings.update(kwargs)

    return render(**settings)


@required("trimesh")
@override_style()
def plot_visible_spectrum_section(
    cmfs: (
        MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str]
    ) = "CIE 1931 2 Degree Standard Observer",
    illuminant: SpectralDistribution | str = "D65",
    model: LiteralColourspaceModel | str = "CIE xyY",
    axis: Literal["+z", "+x", "+y"] | str = "+z",
    origin: float = 0.5,
    normalise: bool = True,
    show_section_colours: bool = True,
    show_section_contour: bool = True,
    **kwargs: Any,
) -> Tuple[Figure, Axes]:
    """
    Plot the visible spectrum volume section colours along the specified axis
    and origin.

    The visible spectrum volume represents the *Rösch-MacAdam* colour solid.

    Parameters
    ----------
    cmfs
        Standard observer colour matching functions, default to the
        *CIE 1931 2 Degree Standard Observer*.  ``cmfs`` can be of any type
        or form supported by the :func:`colour.plotting.common.filter_cmfs`
        definition.
    illuminant
        Illuminant spectral distribution, default to *CIE Illuminant D65*.
        ``illuminant`` can be of any type or form supported by the
        :func:`colour.plotting.common.filter_illuminants` definition.
    model
        Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute
        for the list of supported colourspace models.
    axis
        Axis the hull section will be normal to.
    origin
        Coordinate along ``axis`` at which to plot the hull section.
    normalise
        Whether to normalise ``axis`` to the extent of the hull along it.
    show_section_colours
        Whether to show the hull section colours.
    show_section_contour
        Whether to show the hull section contour.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`,
        :func:`colour.plotting.render`,
        :func:`colour.plotting.section.plot_hull_section_colours`
        :func:`colour.plotting.section.plot_hull_section_contour`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    Examples
    --------
    >>> from colour.utilities import is_trimesh_installed
    >>> if is_trimesh_installed:
    ...     plot_visible_spectrum_section(section_colours="RGB", section_opacity=0.15)
    ...     # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...Axes...>)

    .. image:: ../_static/Plotting_Plot_Visible_Spectrum_Section.png
        :align: center
        :alt: plot_visible_spectrum_section
    """

    import trimesh.convex  # noqa: PLC0415
    from trimesh import Trimesh  # noqa: PLC0415

    settings: Dict[str, Any] = {"uniform": True}
    settings.update(kwargs)

    _figure, axes = artist(**settings)

    cmfs = cast(
        "MultiSpectralDistributions",
        reshape_msds(
            first_item(filter_cmfs(cmfs).values()),
            SpectralShape(360, 780, 1),
            copy=False,
        ),
    )
    illuminant = cast(
        "SpectralDistribution",
        first_item(filter_illuminants(illuminant).values()),
    )

    vertices = solid_RoschMacAdam(
        cmfs,
        illuminant,
        point_order="Pulse Wave Width",
        filter_jagged_points=True,
    )
    mesh = Trimesh(vertices)
    hull = trimesh.convex.convex_hull(mesh)

    if show_section_colours:
        settings = {"axes": axes}
        settings.update(kwargs)
        settings["show"] = False

        plot_hull_section_colours(hull, model, axis, origin, normalise, **settings)

    if show_section_contour:
        settings = {"axes": axes}
        settings.update(kwargs)
        settings["show"] = False

        plot_hull_section_contour(hull, model, axis, origin, normalise, **settings)

    title = (
        f"Visible Spectrum Section - "
        f"{f'{origin * 100}%' if normalise else origin} - "
        f"{model} - "
        f"{cmfs.display_name}"
    )

    plane = MAPPING_AXIS_TO_PLANE[axis]

    labels = np.array(COLOURSPACE_MODELS_AXIS_LABELS[model])[
        as_int_array(colourspace_model_axis_reorder([0, 1, 2], model))
    ]
    x_label, y_label = labels[plane[0]], labels[plane[1]]

    settings.update(
        {
            "axes": axes,
            "show": True,
            "title": title,
            "x_label": x_label,
            "y_label": y_label,
        }
    )
    settings.update(kwargs)

    return render(**settings)


@required("trimesh")
@override_style()
def plot_RGB_colourspace_section(
    colourspace: (
        RGB_Colourspace
        | LiteralRGBColourspace
        | str
        | Sequence[RGB_Colourspace | LiteralRGBColourspace | str]
    ),
    model: LiteralColourspaceModel | str = "CIE xyY",
    axis: Literal["+z", "+x", "+y"] | str = "+z",
    origin: float = 0.5,
    normalise: bool = True,
    size: float = 1.0,
    show_section_colours: bool = True,
    show_section_contour: bool = True,
    segments: int = 64,
    **kwargs: Any,
) -> Tuple[Figure, Axes]:
    """
    Plot the specified *RGB* colourspace section colours along the
    specified axis and origin.

    Parameters
    ----------
    colourspace
        *RGB* colourspace of the *RGB* array. ``colourspace`` can be of
        any type or form supported by the
        :func:`colour.plotting.common.filter_RGB_colourspaces`
        definition.
    model
        Colourspace model, see :attr:`colour.COLOURSPACE_MODELS`
        attribute for the list of supported colourspace models.
    axis
        Axis the hull section will be normal to.
    origin
        Coordinate along ``axis`` at which to plot the hull section.
    normalise
        Whether to normalise ``axis`` to the extent of the hull along
        it.
    size:
        Size of the underlying *RGB* colourspace cube; used for plotting
        HDR related sections.
    show_section_colours
        Whether to show the hull section colours.
    show_section_contour
        Whether to show the hull section contour.
    segments
        Edge segments count for the *RGB* colourspace cube.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`,
        :func:`colour.plotting.render`,
        :func:`colour.plotting.section.plot_hull_section_colours`
        :func:`colour.plotting.section.plot_hull_section_contour`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    Examples
    --------
    >>> from colour.utilities import is_trimesh_installed
    >>> if is_trimesh_installed:
    ...     plot_RGB_colourspace_section(
    ...         "sRGB", section_colours="RGB", section_opacity=0.15
    ...     )
    ...     # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...Axes...>)

    .. image:: ../_static/Plotting_Plot_RGB_Colourspace_Section.png
        :align: center
        :alt: plot_RGB_colourspace_section
    """

    from trimesh import Trimesh  # noqa: PLC0415

    settings: Dict[str, Any] = {"uniform": True}
    settings.update(kwargs)

    _figure, axes = artist(**settings)

    colourspace = cast(
        "RGB_Colourspace",
        first_item(filter_RGB_colourspaces(colourspace).values()),
    )

    vertices, faces, _outline = primitive_cube(1, 1, 1, segments, segments, segments)
    XYZ_vertices = RGB_to_XYZ((vertices["position"] + 0.5) * size, colourspace)
    hull = Trimesh(XYZ_vertices, faces, process=False)

    if show_section_colours:
        settings = {"axes": axes}
        settings.update(kwargs)
        settings["show"] = False

        plot_hull_section_colours(hull, model, axis, origin, normalise, **settings)

    if show_section_contour:
        settings = {"axes": axes}
        settings.update(kwargs)
        settings["show"] = False

        plot_hull_section_contour(hull, model, axis, origin, normalise, **settings)

    title = (
        f"{colourspace.name} Section - "
        f"{f'{origin * 100}%' if normalise else origin} - "
        f"{model}"
    )

    plane = MAPPING_AXIS_TO_PLANE[axis]

    labels = np.array(COLOURSPACE_MODELS_AXIS_LABELS[model])[
        as_int_array(colourspace_model_axis_reorder([0, 1, 2], model))
    ]
    x_label, y_label = labels[plane[0]], labels[plane[1]]

    settings.update(
        {
            "axes": axes,
            "show": True,
            "title": title,
            "x_label": x_label,
            "y_label": y_label,
        }
    )
    settings.update(kwargs)

    return render(**settings)
