"""
Resolve .cube LUT Format Input / Output Utilities
=================================================

Define the *Resolve* *.cube* *LUT* format related input / output utilities
objects:

-   :func:`colour.io.read_LUT_ResolveCube`
-   :func:`colour.io.write_LUT_ResolveCube`

References
----------
-   :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
"""

from __future__ import annotations

import typing

import numpy as np

if typing.TYPE_CHECKING:
    from colour.hints import PathLike

from colour.io.luts import LUT1D, LUT3D, LUT3x1D, LUTSequence
from colour.io.luts.common import path_to_title
from colour.utilities import (
    as_float_array,
    as_int_scalar,
    attest,
    format_array_as_row,
    tstack,
)

__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__ = [
    "read_LUT_ResolveCube",
    "write_LUT_ResolveCube",
]


def read_LUT_ResolveCube(path: str | PathLike) -> LUT3x1D | LUT3D | LUTSequence:
    """
    Read the specified *Resolve* *.cube* *LUT* file.

    Read and parse a *DaVinci Resolve* *.cube* lookup table file, which may
    contain a 1D LUT, a 3D LUT, or a sequence of both. The *.cube* format
    supports configurable precision and domain ranges, making it suitable for
    colour grading and colour space transformations.

    Parameters
    ----------
    path
        Path to the *.cube* *LUT* file to read.

    Returns
    -------
    :class:`colour.LUT3x1D` or :class:`colour.LUT3D` or :class:`colour.LUTSequence`
        :class:`LUT3x1D` instance for 1D shaper LUTs, :class:`LUT3D` instance
        for 3D colour transformation LUTs, or :class:`LUTSequence` instance
        when the file contains both shaper and 3D LUT data.

    References
    ----------
    :cite:`Chamberlain2015`

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

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

    Reading a 3D *Resolve* *.cube* *LUT*:

    >>> path = os.path.join(
    ...     os.path.dirname(__file__),
    ...     "tests",
    ...     "resources",
    ...     "resolve_cube",
    ...     "Colour_Correct.cube",
    ... )
    >>> print(read_LUT_ResolveCube(path))
    LUT3D - Generated by Foundry::LUT
    ---------------------------------
    <BLANKLINE>
    Dimensions : 3
    Domain     : [[ 0.  0.  0.]
                  [ 1.  1.  1.]]
    Size       : (4, 4, 4, 3)

    Reading a 3D *Resolve* *.cube* *LUT* with comments:

    >>> path = os.path.join(
    ...     os.path.dirname(__file__),
    ...     "tests",
    ...     "resources",
    ...     "resolve_cube",
    ...     "Demo.cube",
    ... )
    >>> print(read_LUT_ResolveCube(path))
    LUT3x1D - Demo
    --------------
    <BLANKLINE>
    Dimensions : 2
    Domain     : [[ 0.  0.  0.]
                  [ 3.  3.  3.]]
    Size       : (3, 3)
    Comment 01 : Comments can't go anywhere

    Reading a 3x1D + 3D *Resolve* *.cube* *LUT*:

    >>> path = os.path.join(
    ...     os.path.dirname(__file__),
    ...     "tests",
    ...     "resources",
    ...     "resolve_cube",
    ...     "Three_Dimensional_Table_With_Shaper.cube",
    ... )
    >>> print(read_LUT_ResolveCube(path))
    LUT Sequence
    ------------
    <BLANKLINE>
    Overview
    <BLANKLINE>
        LUT3x1D --> LUT3D
    <BLANKLINE>
    Operations
    <BLANKLINE>
        LUT3x1D - LUT3D with My Shaper - Shaper
        ---------------------------------------
    <BLANKLINE>
        Dimensions : 2
        Domain     : [[-0.1 -0.1 -0.1]
                      [ 3.   3.   3. ]]
        Size       : (10, 3)
    <BLANKLINE>
        LUT3D - LUT3D with My Shaper - Cube
        -----------------------------------
    <BLANKLINE>
        Dimensions : 3
        Domain     : [[-0.1 -0.1 -0.1]
                      [ 3.   3.   3. ]]
        Size       : (3, 3, 3, 3)
        Comment 01 : A first "Shaper" comment.
        Comment 02 : A second "Shaper" comment.
        Comment 03 : A first "LUT3D" comment.
        Comment 04 : A second "LUT3D" comment.
    """

    path = str(path)

    title = path_to_title(path)
    domain_3x1D, domain_3D = None, None
    size_3x1D: int = 2
    size_3D: int = 2
    data = []
    comments = []
    has_3x1D, has_3D = False, False

    with open(path) as cube_file:
        lines = cube_file.readlines()
        for line in lines:
            line = line.strip()  # noqa: PLW2901

            if len(line) == 0:
                continue

            if line.startswith("#"):
                comments.append(line[1:].strip())
                continue

            tokens = line.split()
            if tokens[0] == "TITLE":
                title = " ".join(tokens[1:])[1:-1]
            elif tokens[0] == "LUT_1D_INPUT_RANGE":
                domain_3x1D = tstack([tokens[1:], tokens[1:], tokens[1:]])
            elif tokens[0] == "LUT_3D_INPUT_RANGE":
                domain_3D = tstack([tokens[1:], tokens[1:], tokens[1:]])
            elif tokens[0] == "LUT_1D_SIZE":
                has_3x1D = True
                size_3x1D = as_int_scalar(tokens[1])
            elif tokens[0] == "LUT_3D_SIZE":
                has_3D = True
                size_3D = as_int_scalar(tokens[1])
            else:
                data.append(tokens)

    table = as_float_array(data)

    LUT: LUT3x1D | LUT3D | LUTSequence
    if has_3x1D and has_3D:
        table_1D = table[: int(size_3x1D)]
        # The lines of table data shall be in ascending index order,
        # with the first component index (Red) changing most rapidly,
        # and the last component index (Blue) changing least rapidly.
        table_3D = np.reshape(
            table[int(size_3x1D) :], (size_3D, size_3D, size_3D, 3), order="F"
        )
        LUT = LUTSequence(
            LUT3x1D(
                table_1D,
                f"{title} - Shaper",
                domain_3x1D,
            ),
            LUT3D(
                table_3D,
                f"{title} - Cube",
                domain_3D,
                comments=comments,
            ),
        )
    elif has_3x1D:
        LUT = LUT3x1D(table, title, domain_3x1D, comments=comments)
    elif has_3D:
        # The lines of table data shall be in ascending index order,
        # with the first component index (Red) changing most rapidly,
        # and the last component index (Blue) changing least rapidly.
        table = np.reshape(table, (size_3D, size_3D, size_3D, 3), order="F")
        LUT = LUT3D(table, title, domain_3D, comments=comments)

    return LUT


def write_LUT_ResolveCube(
    LUT: LUT1D | LUT3x1D | LUT3D | LUTSequence,
    path: str | PathLike,
    decimals: int = 7,
) -> bool:
    """
    Write the specified *LUT* to the specified *Resolve* *.cube* *LUT* file.

    Parameters
    ----------
    LUT
        :class:`LUT1D`, :class:`LUT3x1D` or :class:`LUT3D` or
        :class:`LUTSequence` class instance to write at the specified path.
    path
        *LUT* file path.
    decimals
        Number of decimal places for formatting numeric values.

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

    References
    ----------
    :cite:`Chamberlain2015`

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

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

    Writing a 3D *Resolve* *.cube* *LUT*:

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

    Writing a 3x1D + 3D *Resolve* *.cube* *LUT*:

    >>> from colour.models import RGB_to_HSV, HSV_to_RGB
    >>> from colour.utilities import tstack
    >>> def rotate_hue(a, angle):
    ...     H, S, V = RGB_to_HSV(a)
    ...     H += angle / 360
    ...     H[H > 1] -= 1
    ...     H[H < 0] += 1
    ...     return HSV_to_RGB([H, S, V])
    >>> domain = np.array([[-0.1, -0.1, -0.1], [3.0, 3.0, 3.0]])
    >>> shaper = LUT3x1D(
    ...     spow(LUT3x1D.linear_table(10, domain), 1 / 2.2),
    ...     "My Shaper",
    ...     domain,
    ...     comments=[
    ...         'A first "Shaper" comment.',
    ...         'A second "Shaper" comment.',
    ...     ],
    ... )
    >>> LUT = LUT3D(
    ...     rotate_hue(LUT3D.linear_table(3, domain), 10),
    ...     "LUT3D with My Shaper",
    ...     domain,
    ...     comments=['A first "LUT3D" comment.', 'A second "LUT3D" comment.'],
    ... )
    >>> LUT_sequence = LUTSequence(shaper, LUT)
    >>> write_LUT_ResolveCube(LUT_sequence, "My_LUT.cube")  # doctest: +SKIP
    """

    path = str(path)

    has_3D, has_3x1D = False, False

    if isinstance(LUT, LUTSequence):
        attest(
            len(LUT) == 2
            and isinstance(LUT[0], (LUT1D, LUT3x1D))
            and isinstance(LUT[1], LUT3D),
            "LUTSequence must be 1D + 3D or 3x1D + 3D!",
        )

        if isinstance(LUT[0], LUT1D):
            LUT[0] = LUT[0].convert(LUT3x1D)

        name = f"{LUT[0].name} - {LUT[1].name}"
        has_3x1D = True
        has_3D = True
    elif isinstance(LUT, LUT1D):
        name = LUT.name
        has_3x1D = True
        LUT = LUTSequence(LUT.convert(LUT3x1D), LUT3D())
    elif isinstance(LUT, LUT3x1D):
        name = LUT.name
        has_3x1D = True
        LUT = LUTSequence(LUT, LUT3D())
    elif isinstance(LUT, LUT3D):
        name = LUT.name
        has_3D = True
        LUT = LUTSequence(LUT3x1D(), LUT)
    else:
        error = "LUT must be 1D, 3x1D, 3D, 1D + 3D or 3x1D + 3D!"

        raise TypeError(error)

    for i in range(2):
        attest(not LUT[i].is_domain_explicit(), '"LUT" domain must be implicit!')

    attest(
        (len(np.unique(LUT[0].domain)) == 2 and len(np.unique(LUT[1].domain)) == 2),
        '"LUT" domain must be 1D!',
    )

    if has_3x1D:
        attest(
            2 <= LUT[0].size <= 65536,
            "Shaper size must be in domain [2, 65536]!",
        )
    if has_3D:
        attest(2 <= LUT[1].size <= 256, "Cube size must be in domain [2, 256]!")

    with open(path, "w") as cube_file:
        cube_file.write(f'TITLE "{name}"\n')

        if LUT[0].comments:
            cube_file.writelines(f"# {comment}\n" for comment in LUT[0].comments)

        if LUT[1].comments:
            cube_file.writelines(f"# {comment}\n" for comment in LUT[1].comments)

        default_domain = np.array([[0, 0, 0], [1, 1, 1]])

        if has_3x1D:
            cube_file.write(f"LUT_1D_SIZE {LUT[0].table.shape[0]}\n")
            if not np.array_equal(LUT[0].domain, default_domain):
                input_range = format_array_as_row(
                    [LUT[0].domain[0][0], LUT[0].domain[1][0]], decimals
                )
                cube_file.write(f"LUT_1D_INPUT_RANGE {input_range}\n")

        if has_3D:
            cube_file.write(f"LUT_3D_SIZE {LUT[1].table.shape[0]}\n")
            if not np.array_equal(LUT[1].domain, default_domain):
                input_range = format_array_as_row(
                    [LUT[1].domain[0][0], LUT[1].domain[1][0]], decimals
                )
                cube_file.write(f"LUT_3D_INPUT_RANGE {input_range}\n")

        if has_3x1D:
            table = LUT[0].table
            cube_file.writelines(
                f"{format_array_as_row(vector, decimals)}\n" for vector in table
            )
            cube_file.write("\n")

        if has_3D:
            table = np.reshape(LUT[1].table, (-1, 3), order="F")
            cube_file.writelines(
                f"{format_array_as_row(vector, decimals)}\n" for vector in table
            )

    return True
