"""
Look-Up Table (LUT) I/O
=======================

Implement support for reading and writing industry-standard Look-Up Table
(LUT) formats used in colour pipelines and digital intermediate workflows.

-   :class:`colour.LUT1D`: 1D LUT for single-channel transformations
-   :class:`colour.LUT3x1D`: Three separate 1D LUTs for per-channel operations
-   :class:`colour.LUT3D`: 3D LUT for complex colour transformations
-   :class:`colour.LUTSequence`: Sequential LUT operations
-   :class:`colour.LUTOperatorMatrix`: Matrix-based LUT operations
-   :func:`colour.io.read_LUT`: Auto-detect and read LUT files
-   :func:`colour.io.write_LUT`: Write LUT files in specified formats

References
----------
-   :cite:`AdobeSystems2013b` : Adobe Systems. (2013). Cube LUT Specification.
    https://drive.google.com/open?id=143Eh08ZYncCAMwJ1q4gWxVOqR_OSWYvs
-   :cite:`Chamberlain2015` : Chamberlain, P. (2015). LUT documentation (to
    create from another program). Retrieved August 23, 2018, from
    https://forum.blackmagicdesign.com/viewtopic.php?f=21&t=40284#p232952
-   :cite:`RisingSunResearch` : Rising Sun Research. (n.d.). cineSpace LUT
    Library. Retrieved November 30, 2018, from
    https://sourceforge.net/projects/cinespacelutlib/
"""

from __future__ import annotations

import os
import typing

if typing.TYPE_CHECKING:
    from colour.hints import Any, LiteralLUTReadMethod, LiteralLUTWriteMethod, PathLike

from colour.utilities import (
    CanonicalMapping,
    filter_kwargs,
    validate_method,
)

# isort: split

from .lut import LUT1D, LUT3D, LUT3x1D, LUT_to_LUT
from .operator import AbstractLUTSequenceOperator, LUTOperatorMatrix
from .sequence import LUTSequence

# isort: split

from .cinespace_csp import read_LUT_Cinespace, write_LUT_Cinespace
from .iridas_cube import read_LUT_IridasCube, write_LUT_IridasCube
from .resolve_cube import read_LUT_ResolveCube, write_LUT_ResolveCube
from .sony_spi1d import read_LUT_SonySPI1D, write_LUT_SonySPI1D
from .sony_spi3d import read_LUT_SonySPI3D, write_LUT_SonySPI3D
from .sony_spimtx import read_LUT_SonySPImtx, write_LUT_SonySPImtx

__all__ = [
    "LUT1D",
    "LUT3D",
    "LUT3x1D",
    "LUT_to_LUT",
]
__all__ += [
    "AbstractLUTSequenceOperator",
    "LUTOperatorMatrix",
]
__all__ += [
    "LUTSequence",
]
__all__ += [
    "read_LUT_Cinespace",
    "write_LUT_Cinespace",
]
__all__ += [
    "read_LUT_IridasCube",
    "write_LUT_IridasCube",
]
__all__ += [
    "read_LUT_ResolveCube",
    "write_LUT_ResolveCube",
]
__all__ += [
    "read_LUT_SonySPI1D",
    "write_LUT_SonySPI1D",
]
__all__ += [
    "read_LUT_SonySPI3D",
    "write_LUT_SonySPI3D",
]
__all__ += [
    "read_LUT_SonySPImtx",
    "write_LUT_SonySPImtx",
]

MAPPING_EXTENSION_TO_LUT_FORMAT: CanonicalMapping = CanonicalMapping(
    {
        ".cube": "Iridas Cube",
        ".spi1d": "Sony SPI1D",
        ".spi3d": "Sony SPI3D",
        ".spimtx": "Sony SPImtx",
        ".csp": "Cinespace",
    }
)
"""Extension to *LUT* format."""

LUT_READ_METHODS: CanonicalMapping = CanonicalMapping(
    {
        "Cinespace": read_LUT_Cinespace,
        "Iridas Cube": read_LUT_IridasCube,
        "Resolve Cube": read_LUT_ResolveCube,
        "Sony SPI1D": read_LUT_SonySPI1D,
        "Sony SPI3D": read_LUT_SonySPI3D,
        "Sony SPImtx": read_LUT_SonySPImtx,
    }
)
LUT_READ_METHODS.__doc__ = """
Supported *LUT* reading methods.

References
----------
:cite:`AdobeSystems2013b`, :cite:`Chamberlain2015`
"""


def read_LUT(
    path: str | PathLike,
    method: LiteralLUTReadMethod | str | None = None,
    **kwargs: Any,
) -> LUT1D | LUT3x1D | LUT3D | LUTSequence | LUTOperatorMatrix:
    """
    Read the specified *LUT* file.

    Parameters
    ----------
    path
        *LUT* path.
    method
        Reading method, if *None*, the method will be auto-detected
        according to extension.

    Returns
    -------
    :class:`colour.LUT1D` or :class:`colour.LUT3x1D` or \
:class:`colour.LUT3D` or :class:`colour.LUTSequence` or \
:class:`colour.LUTOperatorMatrix`
        :class:`colour.LUT1D` or :class:`colour.LUT3x1D` or
        :class:`colour.LUT3D` or :class:`colour.LUTSequence` or
        :class:`colour.LUTOperatorMatrix` class instance.

    Raises
    ------
    ValueError
        If the *LUT* file format is not supported or if reading fails.

    References
    ----------
    :cite:`AdobeSystems2013b`, :cite:`Chamberlain2015`,
    :cite:`RisingSunResearch`

    Examples
    --------
    Reading a 3x1D *Iridas* *.cube* *LUT*:

    >>> path = os.path.join(
    ...     os.path.dirname(__file__),
    ...     "tests",
    ...     "resources",
    ...     "iridas_cube",
    ...     "ACES_Proxy_10_to_ACES.cube",
    ... )
    >>> print(read_LUT(path))
    LUT3x1D - ACES Proxy 10 to ACES
    -------------------------------
    <BLANKLINE>
    Dimensions : 2
    Domain     : [[ 0.  0.  0.]
                  [ 1.  1.  1.]]
    Size       : (32, 3)

    Reading a 1D *Sony* *.spi1d* *LUT*:

    >>> path = os.path.join(
    ...     os.path.dirname(__file__),
    ...     "tests",
    ...     "resources",
    ...     "sony_spi1d",
    ...     "eotf_sRGB_1D.spi1d",
    ... )
    >>> print(read_LUT(path))
    LUT1D - eotf sRGB 1D
    --------------------
    <BLANKLINE>
    Dimensions : 1
    Domain     : [-0.1  1.5]
    Size       : (16,)
    Comment 01 : Generated by "Colour 0.3.11".
    Comment 02 : "colour.models.eotf_sRGB".

    Reading a 3D *Sony* *.spi3d* *LUT*:

    >>> path = os.path.join(
    ...     os.path.dirname(__file__),
    ...     "tests",
    ...     "resources",
    ...     "sony_spi3d",
    ...     "Colour_Correct.spi3d",
    ... )
    >>> print(read_LUT(path))
    LUT3D - Colour Correct
    ----------------------
    <BLANKLINE>
    Dimensions : 3
    Domain     : [[ 0.  0.  0.]
                  [ 1.  1.  1.]]
    Size       : (4, 4, 4, 3)
    Comment 01 : Adapted from a LUT generated by Foundry::LUT.

    Reading a *Sony* *.spimtx* *LUT*:

    >>> path = os.path.join(
    ...     os.path.dirname(__file__),
    ...     "tests",
    ...     "resources",
    ...     "sony_spimtx",
    ...     "dt.spimtx",
    ... )
    >>> print(read_LUT(path))
    LUTOperatorMatrix - dt
    ----------------------
    <BLANKLINE>
    Matrix     : [[ 0.864274  0.        0.        0.      ]
                  [ 0.        0.864274  0.        0.      ]
                  [ 0.        0.        0.864274  0.      ]
                  [ 0.        0.        0.        1.      ]]
    Offset     : [ 0.  0.  0.  0.]
    """

    path = str(path)

    method = (
        MAPPING_EXTENSION_TO_LUT_FORMAT[os.path.splitext(path)[-1]].lower()
        if method is None
        else validate_method(method, tuple(LUT_WRITE_METHODS))
    )

    function = LUT_READ_METHODS[method]

    try:
        return function(path, **filter_kwargs(function, **kwargs))
    except ValueError as error:
        # Case where a "Resolve Cube" with "LUT3x1D" shaper was read as an
        # "Iridas Cube" "LUT".
        if method == "iridas cube":
            function = LUT_READ_METHODS["Resolve Cube"]
            return function(path, **filter_kwargs(function, **kwargs))

        raise ValueError from error


LUT_WRITE_METHODS = CanonicalMapping(
    {
        "Cinespace": write_LUT_Cinespace,
        "Iridas Cube": write_LUT_IridasCube,
        "Resolve Cube": write_LUT_ResolveCube,
        "Sony SPI1D": write_LUT_SonySPI1D,
        "Sony SPI3D": write_LUT_SonySPI3D,
        "Sony SPImtx": write_LUT_SonySPImtx,
    }
)
LUT_WRITE_METHODS.__doc__ = """
Supported *LUT* writing methods.

References
----------
:cite:`AdobeSystems2013b`, :cite:`Chamberlain2015`
"""


def write_LUT(
    LUT: LUT1D | LUT3x1D | LUT3D | LUTSequence | LUTOperatorMatrix,
    path: str | PathLike,
    decimals: int = 7,
    method: LiteralLUTWriteMethod | str | None = None,
    **kwargs: Any,
) -> bool:
    """
    Write the specified *LUT* to the specified file.

    Parameters
    ----------
    LUT
        :class:`colour.LUT1D` or :class:`colour.LUT3x1D` or
        :class:`colour.LUT3D` or :class:`colour.LUTSequence` or
        :class:`colour.LUTOperatorMatrix` class instance to write at the
        specified path.
    path
        *LUT* file path.
    decimals
        Number of decimal places for formatting numeric values.
    method
        Writing method, if *None*, the method will be auto-detected
        according to the file extension.

    Returns
    -------
    :class:`bool`
        Definition success.

    References
    ----------
    :cite:`AdobeSystems2013b`, :cite:`Chamberlain2015`,
    :cite:`RisingSunResearch`

    Examples
    --------
    Writing a 3x1D *Iridas* *.cube* *LUT*:

    >>> import numpy as np
    >>> from colour.algebra import spow
    >>> domain = np.array([[-0.1, -0.2, -0.4], [1.5, 3.0, 6.0]])
    >>> LUT = LUT3x1D(
    ...     spow(LUT3x1D.linear_table(16, domain), 1 / 2.2),
    ...     "My LUT",
    ...     domain,
    ...     comments=["A first comment.", "A second comment."],
    ... )
    >>> write_LUT(LUT, "My_LUT.cube")  # doctest: +SKIP

    Writing a 1D *Sony* *.spi1d* *LUT*:

    >>> domain = np.array([-0.1, 1.5])
    >>> LUT = LUT1D(
    ...     spow(LUT1D.linear_table(16, domain), 1 / 2.2),
    ...     "My LUT",
    ...     domain,
    ...     comments=["A first comment.", "A second comment."],
    ... )
    >>> write_LUT(LUT, "My_LUT.spi1d")  # doctest: +SKIP

    Writing a 3D *Sony* *.spi3d* *LUT*:

    >>> LUT = LUT3D(
    ...     LUT3D.linear_table(16) ** (1 / 2.2),
    ...     "My LUT",
    ...     np.array([[0, 0, 0], [1, 1, 1]]),
    ...     comments=["A first comment.", "A second comment."],
    ... )
    >>> write_LUT(LUT, "My_LUT.cube")  # doctest: +SKIP
    """

    path = str(path)

    method = (
        MAPPING_EXTENSION_TO_LUT_FORMAT[os.path.splitext(path)[-1]].lower()
        if method is None
        else validate_method(method, tuple(LUT_WRITE_METHODS))
    )

    if method == "iridas cube" and isinstance(LUT, LUTSequence):
        method = "resolve cube"

    function = LUT_WRITE_METHODS[method]

    return function(LUT, path, decimals, **filter_kwargs(function, **kwargs))


__all__ += [
    "LUT_READ_METHODS",
    "read_LUT",
    "LUT_WRITE_METHODS",
    "write_LUT",
]
