#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
LLAB(l:c) Colour Appearance Model
=================================

Defines *LLAB(l:c)* colour appearance model objects:

-   :class:`LLAB_InductionFactors`
-   :attr:`LLAB_VIEWING_CONDITIONS`
-   :class:`LLAB_Specification`
-   :func:`XYZ_to_LLAB`

See Also
--------
`LLAB(l:c) Colour Appearance Model IPython Notebook
<http://nbviewer.ipython.org/github/colour-science/colour-ipython/blob/master/notebooks/appearance/llab.ipynb>`_  # noqa

References
----------
.. [1]  **Mark D. Fairchild**, *Color Appearance Models, 3nd Edition*,
        The Wiley-IS&T Series in Imaging Science and Technology,
        published June 2013, ASIN: B00DAYO8E2,
        locations 6019-6178.
.. [2]  **Luo, M. R., & Morovic, J.**,
        *Two Unsolved Issues in Colour Management – Colour Appearance and
        Gamut Mapping*,
        *5th International Conference on High Technology: Imaging Science and
        Technology – Evolution & Promise*
        published 1996, pp. 136–147.
.. [3]  **Luo, M. R., Lo, M. C., & Kuo, W. G.**,
        *The LLAB (l:c) colour model*,
        *Color Research & Application, Volume 21, Issue 6, pages 412–429,
        December 1996*
"""

from __future__ import division, unicode_literals

import math
import numpy as np
from collections import namedtuple

from colour.utilities import CaseInsensitiveMapping

__author__ = 'Colour Developers'
__copyright__ = 'Copyright (C) 2013 - 2014 - Colour Developers'
__license__ = 'GPL V3.0 - http://www.gnu.org/licenses/'
__maintainer__ = 'Colour Developers'
__email__ = 'colour-science@googlegroups.com'
__status__ = 'Production'

__all__ = ['LLAB_InductionFactors',
           'LLAB_VIEWING_CONDITIONS',
           'LLAB_XYZ_TO_RGB_MATRIX',
           'LLAB_RGB_TO_XYZ_MATRIX',
           'LLAB_ReferenceSpecification',
           'LLAB_Specification',
           'XYZ_to_LLAB',
           'XYZ_to_RGB_LLAB',
           'chromatic_adaptation',
           'f',
           'opponent_colour_dimensions',
           'hue_angle',
           'chroma_correlate',
           'colourfulness_correlate',
           'saturation_correlate',
           'final_opponent_signals']


class LLAB_InductionFactors(
    namedtuple('LLAB_InductionFactors',
               ('D', 'F_S', 'F_L', 'F_C'))):
    """
    *LLAB(l:c)* colour appearance model induction factors.

    Parameters
    ----------
    D : numeric
         *Discounting-the-Illuminant* factor :math:`D` in domain [0, 1].
    F_S : numeric
        Surround induction factor :math:`F_S`.
    F_L : numeric
        *Lightness* induction factor :math:`F_L`.
    F_C : numeric
        *Chroma* induction factor :math:`F_C`.
    """


LLAB_VIEWING_CONDITIONS = CaseInsensitiveMapping(
    {'Reference Samples & Images, Average Surround, Subtending > 4': (
        LLAB_InductionFactors(1, 3, 0, 1)),
     'Reference Samples & Images, Average Surround, Subtending < 4': (
         LLAB_InductionFactors(1, 3, 1, 1)),
     'Television & VDU Displays, Dim Surround': (
         LLAB_InductionFactors(0.7, 3.5, 1, 1)),
     'Cut Sheet Transparency, Dim Surround': (
         LLAB_InductionFactors(1, 5, 1, 1.1)),
     '35mm Projection Transparency, Dark Surround': (
         LLAB_InductionFactors(0.7, 4, 1, 1))})
"""
Reference *LLAB(l:c)* colour appearance model viewing conditions.

LLAB_VIEWING_CONDITIONS : dict
    ('Reference Samples & Images, Average Surround, Subtending > 4',
    'Reference Samples & Images, Average Surround, Subtending < 4',
    'Television & VDU Displays, Dim Surround',
    'Cut Sheet Transparency, Dim Surround':,
    '35mm Projection Transparency, Dark Surround')

Aliases:

-   'ref_average_4_plus':
    'Reference Samples & Images, Average Surround, Subtending > 4'
-   'ref_average_4_minus':
    'Reference Samples & Images, Average Surround, Subtending < 4'
-   'tv_dim': 'Television & VDU Displays, Dim Surround'
-   'sheet_dim': 'Cut Sheet Transparency, Dim Surround'
-   'projected_dark': '35mm Projection Transparency, Dark Surround'
"""
LLAB_VIEWING_CONDITIONS['ref_average_4_plus'] = (
    LLAB_VIEWING_CONDITIONS[
        'Reference Samples & Images, Average Surround, Subtending > 4'])
LLAB_VIEWING_CONDITIONS['ref_average_4_minus'] = (
    LLAB_VIEWING_CONDITIONS[
        'Reference Samples & Images, Average Surround, Subtending < 4'])
LLAB_VIEWING_CONDITIONS['tv_dim'] = (
    LLAB_VIEWING_CONDITIONS[
        'Television & VDU Displays, Dim Surround'])
LLAB_VIEWING_CONDITIONS['sheet_dim'] = (
    LLAB_VIEWING_CONDITIONS[
        'Cut Sheet Transparency, Dim Surround'])
LLAB_VIEWING_CONDITIONS['projected_dark'] = (
    LLAB_VIEWING_CONDITIONS[
        '35mm Projection Transparency, Dark Surround'])

LLAB_XYZ_TO_RGB_MATRIX = np.array(
    [[0.8951, 0.2664, -0.1614],
     [-0.7502, 1.7135, 0.0367],
     [0.0389, -0.0685, 1.0296]])
"""
*LLAB(l:c)* colour appearance model *CIE XYZ* colourspace matrix to normalised
cone responses matrix.

LLAB_XYZ_TO_RGB_MATRIX : array_like, (3, 3)
"""

LLAB_RGB_TO_XYZ_MATRIX = np.around(
    np.linalg.inv(LLAB_XYZ_TO_RGB_MATRIX),
    decimals=4)
"""
*LLAB(l:c)* colour appearance model normalised cone responses to *CIE XYZ*
colourspace matrix.

Notes
-----
-   This matrix has been rounded on purpose to 4 decimals so that we keep
    consistency with **Mark D. Fairchild** implementation results.

LLAB_RGB_TO_XYZ_MATRIX : array_like, (3, 3)
"""


class LLAB_ReferenceSpecification(
    namedtuple('LLAB_ReferenceSpecification',
               ('L_L', 'Ch_L', 'h_L', 's_L', 'C_L', 'HC', 'A_L', 'B_L'))):
    """
    Defines the *LLAB(l:c)* colour appearance model reference specification.

    This specification has field names consistent with **Mark D. Fairchild**
    reference.

    Parameters
    ----------
    L_L : numeric
        Correlate of *Lightness* :math:`L_L`.
    Ch_L : numeric
        Correlate of *chroma* :math:`Ch_L`.
    h_L : numeric
        *Hue* angle :math:`h_L` in degrees.
    s_L : numeric
        Correlate of *saturation* :math:`s_L`.
    C_L : numeric
        Correlate of *colourfulness* :math:`C_L`.
    HC : numeric
        *Hue* :math:`h` composition :math:`H^C`.
    A_L : numeric
        Opponent signal :math:`A_L`.
    B_L : numeric
        Opponent signal :math:`B_L`.
    """


class LLAB_Specification(
    namedtuple('LLAB_Specification',
               ('J', 'C', 'h', 's', 'M', 'HC', 'a', 'b'))):
    """
    Defines the *LLAB(l:c)* colour appearance model specification.

    This specification has field names consistent with the remaining colour
    appearance models in :mod:`colour.appearance` but diverge from
    **Mark D. Fairchild** reference.

    Parameters
    ----------
    J : numeric
        Correlate of *Lightness* :math:`L_L`.
    C : numeric
        Correlate of *chroma* :math:`Ch_L`.
    h : numeric
        *Hue* angle :math:`h_L` in degrees.
    s : numeric
        Correlate of *saturation* :math:`s_L`.
    M : numeric
        Correlate of *colourfulness* :math:`C_L`.
    HC : numeric
        *Hue* :math:`h` composition :math:`H^C`.
    a : numeric
        Opponent signal :math:`A_L`.
    b : numeric
        Opponent signal :math:`B_L`.
    """


def XYZ_to_LLAB(
        XYZ,
        XYZ_0,
        Y_b,
        L,
        surround=LLAB_VIEWING_CONDITIONS.get(
            'Reference Samples & Images, Average Surround, Subtending < 4')):
    """
    Computes the *LLAB(L:c)* colour appearance model correlates.

    Parameters
    ----------
    XYZ : array_like, (3,)
        *CIE XYZ* colourspace matrix of test sample / stimulus in domain
        [0, 100].
    XYZ_0 : array_like, (3,)
        *CIE XYZ* colourspace matrix of reference white in domain [0, 100].
    Y_b : numeric
        Luminance factor of the background in :math:`cd/m^2`.
    L : numeric
        Absolute luminance :math:`L` of reference white in :math:`cd/m^2`.
    surround : LLAB_InductionFactors, optional
         Surround viewing conditions induction factors.

    Returns
    -------
    LLAB_Specification
        *LLAB(L:c)* colour appearance model specification.

    Warning
    -------
    The output domain of that definition is non standard!

    Notes
    -----
    -   Input *CIE XYZ* colourspace matrix is in domain [0, 100].
    -   Input *CIE XYZ_0* colourspace matrix is in domain [0, 100].

    Examples
    --------
    >>> XYZ = np.array([19.01, 20, 21.78])
    >>> XYZ_0 = np.array([95.05, 100, 108.88])
    >>> Y_b = 20.0
    >>> L = 318.31
    >>> surround = LLAB_VIEWING_CONDITIONS['ref_average_4_minus']
    >>> XYZ_to_LLAB(XYZ, XYZ_0, Y_b, L, surround)  # doctest: +ELLIPSIS
    LLAB_Specification(J=37.3680474..., C=0.0086506..., h=229.4635727..., s=0.0002314..., M=0.0183832..., HC=None, a=-0.0119478..., b=-0.0139711...)
    """

    X, Y, Z = np.ravel(XYZ)
    RGB = XYZ_to_RGB_LLAB(XYZ)
    RGB_0 = XYZ_to_RGB_LLAB(XYZ_0)

    # Reference illuminant *CIE Standard Illuminant D Series* *D65*.
    XYZ_0r = np.array([95.05, 100, 108.88])
    RGB_0r = XYZ_to_RGB_LLAB(XYZ_0r)

    # Computing chromatic adaptation.
    XYZ_r = chromatic_adaptation(RGB, RGB_0, RGB_0r, Y, surround.D)

    # -------------------------------------------------------------------------
    # Computing the correlate of *Lightness* :math:`L_L`.
    # -------------------------------------------------------------------------
    # Computing opponent colour dimensions.
    L_L, a, b = opponent_colour_dimensions(XYZ_r,
                                           Y_b,
                                           surround.F_S,
                                           surround.F_L)

    # Computing perceptual correlates.
    # -------------------------------------------------------------------------
    # Computing the correlate of *chroma* :math:`Ch_L`.
    # -------------------------------------------------------------------------
    Ch_L = chroma_correlate(a, b)

    # -------------------------------------------------------------------------
    # Computing the correlate of *colourfulness* :math:`C_L`.
    # -------------------------------------------------------------------------
    C_L = colourfulness_correlate(L, L_L, Ch_L, surround.F_C)

    # -------------------------------------------------------------------------
    # Computing the correlate of *saturation* :math:`s_L`.
    # -------------------------------------------------------------------------
    s_L = saturation_correlate(Ch_L, L_L)

    # -------------------------------------------------------------------------
    # Computing the *hue* angle :math:`h_L`.
    # -------------------------------------------------------------------------
    h_L = hue_angle(a, b)
    h_Lr = math.radians(h_L)
    # TODO: Implement hue composition computation.

    # -------------------------------------------------------------------------
    # Computing final opponent signals.
    # -------------------------------------------------------------------------
    A_L, B_L = final_opponent_signals(C_L, h_Lr)

    return LLAB_Specification(L_L, Ch_L, h_L, s_L, C_L, None, A_L, B_L)


def XYZ_to_RGB_LLAB(XYZ):
    """
    Converts from *CIE XYZ* colourspace to normalised cone responses.

    Parameters
    ----------
    XYZ : array_like, (3,)
        *CIE XYZ* colourspace matrix.

    Returns
    -------
    ndarray, (3,)
        Normalised cone responses.

    Examples
    --------
    >>> XYZ = np.array([19.01, 20, 21.78])
    >>> XYZ_to_RGB_LLAB(XYZ)  # doctest: +ELLIPSIS
    array([ 0.9414279...,  1.0404012...,  1.0897088...])
    """

    return LLAB_XYZ_TO_RGB_MATRIX.dot(XYZ / XYZ[1])


def chromatic_adaptation(RGB, RGB_0, RGB_0r, Y, D=1):
    """
    Applies chromatic adaptation to given *RGB* normalised cone responses
    matrix.

    Parameters
    ----------
    RGB : array_like, (3,)
        *RGB* normalised cone responses matrix of test sample / stimulus.
    RGB_0 : array_like, (3,)
        *RGB* normalised cone responses matrix of reference white.
    RGB_0r : array_like, (3,)
        *RGB* normalised cone responses matrix of reference illuminant
        *CIE Standard Illuminant D Series* *D65*.
    Y : numeric
        Tristimulus values :math:`Y` of the stimulus.
    D : numeric, optional
         *Discounting-the-Illuminant* factor in domain [0, 1].

    Returns
    -------
    ndarray, (3,)
        Adapted *CIE XYZ* colourspace matrix.

    Examples
    --------
    >>> RGB = np.array([0.94142795, 1.0404012, 1.08970885])
    >>> RGB_0 = np.array([0.94146023, 1.04039386, 1.08950293])
    >>> RGB_0r = np.array([0.94146023, 1.04039386, 1.08950293])
    >>> Y = 20.0
    >>> chromatic_adaptation(RGB, RGB_0, RGB_0r, Y)  # doctest: +ELLIPSIS
    array([ 19.0099957...,  20.0009186...,  21.7799386...])
    """

    R, G, B = np.ravel(RGB)
    R_0, G_0, B_0 = np.ravel(RGB_0)
    R_0r, G_0r, B_0r = np.ravel(RGB_0r)

    beta = (B_0 / B_0r) ** 0.0834

    R_r = (D * (R_0r / R_0) + 1 - D) * R
    G_r = (D * (G_0r / G_0) + 1 - D) * G
    B_r = (D * (B_0r / (B_0 ** beta)) + 1 - D) * (abs(B) ** beta)

    RGB_r = np.array([R_r, G_r, B_r])

    X_r, Y_r, Z_r = LLAB_RGB_TO_XYZ_MATRIX.dot(RGB_r * Y)

    return np.array([X_r, Y_r, Z_r])


def f(x, F_S):
    """
    Defines the nonlinear response function of the *LLAB(L:c)* colour
    appearance model used to model the nonlinear behavior of various visual
    responses.

    Parameters
    ----------
    x : numeric or array_like
        Visual response variable :math:`x`.
    F_S : numeric
        Surround induction factor :math:`F_S`.

    Returns
    -------
    numeric or array_like
        Modeled visual response variable :math:`x`.

    Examples
    --------
    >>> x = np.array([0.23350512, 0.23351103, 0.23355179])
    >>> f(0.20000918623399996, 3)  # doctest: +ELLIPSIS
    array(0.5848125...)
    """

    x_m = np.where(x > 0.008856,
                   x ** (1 / F_S),
                   ((((0.008856 ** (1 / F_S)) -
                      (16 / 116)) / 0.008856) * x + (16 / 116)))
    return x_m


def opponent_colour_dimensions(XYZ, Y_b, F_S, F_L):
    """
    Returns opponent colour dimensions from given adapted *CIE XYZ* colourspace
    matrix.

    The opponent colour dimensions are based on a modified *CIE Lab*
    colourspace formulae.

    Parameters
    ----------
    XYZ : array_like, (3,)
        Adapted *CIE XYZ* colourspace matrix.
    Y_b : numeric
        Luminance factor of the background in :math:`cd/m^2`.
    F_S : numeric
        Surround induction factor :math:`F_S`.
    F_L : numeric
        Lightness induction factor :math:`F_L`.

    Returns
    -------
    ndarray, (3,)
        Opponent colour dimensions.

    Examples
    --------
    >>> XYZ = np.array([19.00999572, 20.00091862, 21.77993863])
    >>> Y_b = 20.0
    >>> F_S = 3.0
    >>> F_L = 1.0
    >>> opponent_colour_dimensions(XYZ, Y_b, F_S, F_L)  # doctest: +ELLIPSIS
    array([  3.7368047...e+01,  -4.4986443...e-03,  -5.2604647...e-03])
    """

    X, Y, Z = np.ravel(XYZ)

    # Account for background lightness contrast.
    z = 1 + F_L * ((Y_b / 100) ** 0.5)

    # Computing modified *CIE Lab* colourspace matrix.
    L = 116 * (f(Y / 100, F_S) ** z) - 16
    a = 500 * (f(X / 95.05, F_S) - f(Y / 100, F_S))
    b = 200 * (f(Y / 100, F_S) - f(Z / 108.88, F_S))

    return np.array([L, a, b])


def hue_angle(a, b):
    """
    Returns the *hue* angle :math:`h_L` in degrees.

    Parameters
    ----------
    a : numeric
        Opponent colour dimension :math:`a`.
    b : numeric
        Opponent colour dimension :math:`b`.

    Returns
    -------
    numeric
        *Hue* angle :math:`h_L` in degrees.

    Examples
    --------
    >>> hue_angle(-4.49864756e-03, -5.26046353e-03)  # doctest: +ELLIPSIS
    229.4635727...
    """

    h_L = math.degrees(np.arctan2(b, a)) % 360
    return h_L


def chroma_correlate(a, b):
    """
    Returns the correlate of *chroma* :math:`Ch_L`.

    Parameters
    ----------
    a : numeric
        Opponent colour dimension :math:`a`.
    b : numeric
        Opponent colour dimension :math:`b`.

    Returns
    -------
    numeric
        Correlate of *chroma* :math:`Ch_L`.

    Examples
    --------
    >>> a = -4.49864756e-03
    >>> b = -5.26046353e-03
    >>> chroma_correlate(a, b)  # doctest: +ELLIPSIS
    0.0086506...
    """

    c = (a ** 2 + b ** 2) ** 0.5
    Ch_L = 25 * np.log(1 + 0.05 * c)
    return Ch_L


def colourfulness_correlate(L, L_L, Ch_L, F_C):
    """
    Returns the correlate of *colourfulness* :math:`C_L`.

    Parameters
    ----------
    L : numeric
        Absolute luminance :math:`L` of reference white in :math:`cd/m^2`.
    L_L : numeric
        Correlate of *Lightness* :math:`L_L`.
    Ch_L : numeric
        Correlate of *chroma* :math:`Ch_L`.
    F_C : numeric
        Chroma induction factor :math:`F_C`.

    Returns
    -------
    numeric
        Correlate of *colourfulness* :math:`C_L`.

    Examples
    --------
    >>> L = 318.31
    >>> L_L = 37.368047493928195
    >>> Ch_L = 0.0086506620517144972
    >>> F_C = 1.0
    >>> colourfulness_correlate(L, L_L, Ch_L, F_C)  # doctest: +ELLIPSIS
    0.0183832...
    """

    S_C = 1 + 0.47 * np.log10(L) - 0.057 * np.log10(L) ** 2
    S_M = 0.7 + 0.02 * L_L - 0.0002 * L_L ** 2
    C_L = Ch_L * S_M * S_C * F_C

    return C_L


def saturation_correlate(Ch_L, L_L):
    """
    Returns the correlate of *saturation* :math:`S_L`.

    Parameters
    ----------
    Ch_L : numeric
        Correlate of *chroma* :math:`Ch_L`.
    L_L : numeric
        Correlate of *Lightness* :math:`L_L`.

    Returns
    -------
    numeric
        Correlate of *saturation* :math:`S_L`.

    Examples
    --------
    >>> Ch_L = 0.0086506620517144972
    >>> L_L = 37.368047493928195
    >>> saturation_correlate(Ch_L, L_L)  # doctest: +ELLIPSIS
    0.0002314...
    """

    S_L = Ch_L / L_L

    return S_L


def final_opponent_signals(C_L, h_L):
    """
    Returns the final opponent signals :math:`A_L` and :math:`B_L`.

    Parameters
    ----------
    C_L : numeric
        Correlate of *colourfulness* :math:`C_L`.
    h_L : numeric
        Correlate of *hue* :math:`h_L` in radians.

    Returns
    -------
    tuple
        Final opponent signals :math:`A_L` and :math:`B_L`.

    Examples
    --------
    >>> C_L = 0.0183832899143
    >>> h_L = 4.004894857014253
    >>> final_opponent_signals(C_L, h_L)  # doctest: +ELLIPSIS
    (-0.0119478..., -0.0139711...)
    """

    A_L = C_L * np.cos(h_L)
    B_L = C_L * np.sin(h_L)

    return A_L, B_L
