# -*- coding: utf-8 -*-
"""
CIE 2017 Colour Fidelity Index
==============================

Defines the *CIE 2017 Colour Fidelity Index* (CFI) computation objects:

- :class:`colour.quality.ColourRendering_Specification_CIE2017`
- :func:`colour.quality.colour_fidelity_index_CIE2017`

References
----------
-   :cite:`CIETC1-902017` : CIE TC 1-90. (2017). CIE 2017 colour fidelity index
    for accurate scientific use. CIE Central Bureau. ISBN:978-3-902842-61-9
"""

from __future__ import division, unicode_literals

import numpy as np
import os
from collections import namedtuple

from colour.algebra import euclidean_distance, Extrapolator
from colour.appearance import XYZ_to_CIECAM02, VIEWING_CONDITIONS_CIECAM02
from colour.colorimetry import (
    SpectralShape, SpectralDistribution, MultiSpectralDistributions, sd_to_XYZ,
    sd_blackbody, MSDS_CMFS, sd_ones, sd_CIE_illuminant_D_series)
from colour.models import XYZ_to_UCS, UCS_to_uv, JMh_CIECAM02_to_CAM02UCS
from colour.temperature import uv_to_CCT_Ohno2013, CCT_to_xy_CIE_D
from colour.utilities import as_int, lerp, usage_warning

__author__ = 'Colour Developers'
__copyright__ = 'Copyright (C) 2013-2020 - 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__ = [
    'SPECTRAL_SHAPE_CIE2017', 'RESOURCES_DIRECTORY_CIE2017',
    'TCS_ColorimetryData_CIE2017', 'ColourRendering_Specification_CIE2017',
    'colour_fidelity_index_CIE2017', 'load_TCS_CIE2017',
    'CCT_reference_illuminant', 'sd_reference_illuminant',
    'tcs_colorimetry_data', 'delta_E_to_R_f'
]

SPECTRAL_SHAPE_CIE2017 = SpectralShape(380, 780, 1)
"""
Spectral shape for *CIE 2017 Colour Fidelity Index* (CFI)
standard.

SPECTRAL_SHAPE_CIE2017 : SpectralShape
"""

RESOURCES_DIRECTORY_CIE2017 = os.path.join(
    os.path.dirname(__file__), 'datasets')
"""
*CIE 2017 Colour Fidelity Index* resources directory.

RESOURCES_DIRECTORY_CIE2017 : unicode
"""

_CACHE_TCS_CIE2017 = {}


class TCS_ColorimetryData_CIE2017(
        namedtuple('TCS_ColorimetryData_CIE2017',
                   ('name', 'XYZ', 'CAM', 'JMh', 'Jpapbp'))):
    """
    Defines the the class storing *test colour samples* colorimetry data.
    """


class ColourRendering_Specification_CIE2017(
        namedtuple('ColourRendering_Specification_CIE2017',
                   ('name', 'sd_reference', 'R_f', 'R_s', 'CCT', 'D_uv',
                    'colorimetry_data', 'delta_E_s'))):
    """
    Defines the *CIE 2017 Colour Fidelity Index* (CFI) colour quality
    specification.

    Parameters
    ----------
    name : unicode
        Name of the test spectral distribution.
    sd_reference : SpectralDistribution
        Spectral distribution of the reference illuminant.
    R_f : numeric
        *CIE 2017 Colour Fidelity Index* (CFI) :math:`R_f`.
    R_s : array_like
        Individual *colour fidelity indexes* data for each sample.
    CCT : numeric
        Correlated colour temperature :math:`T_{cp}`.
    D_uv : numeric
        Distance from the Planckian locus :math:`\\Delta_{uv}`.
    colorimetry_data : tuple
        Colorimetry data for the test and reference computations.
    delta_E_s : ndarray, (16,)
        Colour shifts of samples.
    """


def colour_fidelity_index_CIE2017(sd_test, additional_data=False):
    """
    Returns the *CIE 2017 Colour Fidelity Index* (CFI) :math:`R_f` of given
    spectral distribution.

    Parameters
    ----------
    sd_test : SpectralDistribution
        Test spectral distribution.
    additional_data : bool, optional
        Whether to output additional data.

    Returns
    -------
    numeric or ColourRendering_Specification_CIE2017
        *CIE 2017 Colour Fidelity Index* (CFI) :math:`R_f`.

    References
    ----------
    :cite:`CIETC1-902017`

    Examples
    --------
    >>> from colour.colorimetry import SDS_ILLUMINANTS
    >>> sd = SDS_ILLUMINANTS['FL2']
    >>> colour_fidelity_index_CIE2017(sd)  # doctest: +ELLIPSIS
    70.1208254...
    """

    if sd_test.shape.start > 380 or sd_test.shape.end < 780:
        usage_warning('Test spectral distribution shape does not span the'
                      'recommended 380-780nm range, missing values will be'
                      'filled with zeros!')

        # NOTE: "CIE 2017 Colour Fidelity Index" standard recommends filling
        # missing values with zeros.
        sd_test = sd_test.copy()
        sd_test.extrapolator = Extrapolator
        sd_test.extrapolator_kwargs = {
            'method': 'constant',
            'left': 0,
            'right': 0
        }

    if sd_test.shape.interval > 5:
        raise ValueError('Test spectral distribution interval is greater than'
                         '5nm which is the maximum recommended value '
                         'for computing the "CIE 2017 Colour Fidelity Index"!')

    shape = SpectralShape(SPECTRAL_SHAPE_CIE2017.start,
                          SPECTRAL_SHAPE_CIE2017.end, sd_test.shape.interval)

    CCT, D_uv = CCT_reference_illuminant(sd_test)
    sd_reference = sd_reference_illuminant(CCT, shape)

    # NOTE: All computations except CCT calculation use the
    # "CIE 1964 10 Degree Standard Observer".
    cmfs_10 = MSDS_CMFS['CIE 1964 10 Degree Standard Observer'].copy().align(
        shape)

    sds_tcs = load_TCS_CIE2017(shape).align(shape)

    test_tcs_colorimetry_data = tcs_colorimetry_data(sd_test, sds_tcs, cmfs_10)
    reference_tcs_colorimetry_data = tcs_colorimetry_data(
        sd_reference, sds_tcs, cmfs_10)

    delta_E_s = np.empty(len(sds_tcs.labels))
    for i, _delta_E in enumerate(delta_E_s):
        delta_E_s[i] = euclidean_distance(
            test_tcs_colorimetry_data[i].Jpapbp,
            reference_tcs_colorimetry_data[i].Jpapbp)

    R_s = delta_E_to_R_f(delta_E_s)
    R_f = delta_E_to_R_f(np.average(delta_E_s))

    if additional_data:
        return ColourRendering_Specification_CIE2017(
            sd_test.name, sd_reference, R_f, R_s, CCT, D_uv,
            (test_tcs_colorimetry_data, reference_tcs_colorimetry_data),
            delta_E_s)
    else:
        return R_f


def load_TCS_CIE2017(shape):
    """
    Loads the *CIE 2017 Test Colour Samples* dataset appropriate for the given
    spectral shape.

    The datasets are cached and won't be loaded again on subsequent calls to
    this definition.

    Parameters
    ----------
    shape : SpectralShape
        Spectral shape of the tested illuminant.

    Returns
    -------
    MultiSpectralDistributions
        *CIE 2017 Test Colour Samples* dataset.

    Examples
    --------
    >>> sds_tcs = load_TCS_CIE2017(SpectralShape(interval=5))
    >>> len(sds_tcs.labels)
    99
    """

    global _CACHE_TCS_CIE2017

    interval = shape.interval

    assert interval in (1, 5), (
        'Spectral shape interval must be either 1nm or 5nm!')

    filename = 'tcs_cfi2017_{0}_nm.csv.gz'.format(as_int(interval))

    if filename in _CACHE_TCS_CIE2017:
        return _CACHE_TCS_CIE2017[filename]

    data = np.genfromtxt(
        str(os.path.join(RESOURCES_DIRECTORY_CIE2017, filename)),
        delimiter=',')
    labels = ['TCS{0} (CIE 2017)'.format(i) for i in range(99)]

    return MultiSpectralDistributions(data[:, 1:], data[:, 0], labels)


def CCT_reference_illuminant(sd):
    """
    Computes the reference illuminant correlated colour temperature
    :math:`T_{cp}` and :math:`\\Delta_{uv}` for given test spectral
    distribution using *Ohno (2013)* method.

    Parameters
    ----------
    sd : SpectralDistribution
        Test spectral distribution.

    Returns
    -------
    ndarray
        Correlated colour temperature :math:`T_{cp}`, :math:`\\Delta_{uv}`.

    Examples
    --------
    >>> from colour import SDS_ILLUMINANTS
    >>> sd = SDS_ILLUMINANTS['FL2']
    >>> CCT_reference_illuminant(sd)  # doctest: +ELLIPSIS
    (4224.4697052..., 0.0017871...)
    """

    XYZ = sd_to_XYZ(sd)

    CCT, D_uv = uv_to_CCT_Ohno2013(UCS_to_uv(XYZ_to_UCS(XYZ)))

    return CCT, D_uv


def sd_reference_illuminant(CCT, shape):
    """
    Computes the reference illuminant for a given correlated colour temperature
    :math:`T_{cp}` for use in *CIE 2017 Colour Fidelity Index* (CFI)
    computation.

    Parameters
    ----------
    CCT : numeric
        Correlated colour temperature :math:`T_{cp}`.
    shape : SpectralShape
        Desired shape of the returned spectral distribution.

    Returns
    -------
    SpectralDistribution
        Reference illuminant for *CIE 2017 Colour Fidelity Index* (CFI)
        computation.

    Examples
    --------
    >>> from colour.utilities import numpy_print_options
    >>> with numpy_print_options(suppress=True):
    ...     sd_reference_illuminant(  # doctest: +ELLIPSIS
    ...         4224.469705295263300, SpectralShape(380, 780, 20))
    SpectralDistribution([[ 380.        ,    0.0034089...],
                          [ 400.        ,    0.0044208...],
                          [ 420.        ,    0.0053260...],
                          [ 440.        ,    0.0062857...],
                          [ 460.        ,    0.0072767...],
                          [ 480.        ,    0.0080207...],
                          [ 500.        ,    0.0086590...],
                          [ 520.        ,    0.0092242...],
                          [ 540.        ,    0.0097686...],
                          [ 560.        ,    0.0101444...],
                          [ 580.        ,    0.0104475...],
                          [ 600.        ,    0.0107642...],
                          [ 620.        ,    0.0110439...],
                          [ 640.        ,    0.0112535...],
                          [ 660.        ,    0.0113922...],
                          [ 680.        ,    0.0115185...],
                          [ 700.        ,    0.0113155...],
                          [ 720.        ,    0.0108192...],
                          [ 740.        ,    0.0111582...],
                          [ 760.        ,    0.0101299...],
                          [ 780.        ,    0.0105638...]],
                         interpolator=SpragueInterpolator,
                         interpolator_kwargs={},
                         extrapolator=Extrapolator,
                         extrapolator_kwargs={...})
    """

    if CCT <= 5000:
        sd_planckian = sd_blackbody(CCT, shape)

    if CCT >= 4000:
        xy = CCT_to_xy_CIE_D(CCT)
        sd_daylight = sd_CIE_illuminant_D_series(xy).align(shape)

    if CCT < 4000:
        sd_reference = sd_planckian
    elif 4000 <= CCT <= 5000:
        # Planckian and daylight illuminant must be normalised so that the
        # mixture isn't biased.
        sd_planckian /= sd_to_XYZ(sd_planckian)[1]
        sd_daylight /= sd_to_XYZ(sd_daylight)[1]

        # Mixture: 4200K should be 80% Planckian, 20% CIE Illuminant D Series.
        m = (CCT - 4000) / 1000
        values = lerp(sd_planckian.values, sd_daylight.values, m)
        name = ('{0}K Blackbody & CIE Illuminant D Series Mixture - {1:.1f}%'
                .format(as_int(CCT), 100 * m))
        sd_reference = SpectralDistribution(values, shape.range(), name=name)
    elif CCT > 5000:
        sd_reference = sd_daylight

    return sd_reference


def tcs_colorimetry_data(sd_irradiance, sds_tcs, cmfs):
    """
    Returns the *test colour samples* colorimetry data under given test light
    source or reference illuminant spectral distribution for the
    *CIE 2017 Colour Fidelity Index* (CFI) computations.

    Parameters
    ----------
    sd_irradiance : SpectralDistribution
        Test light source or reference illuminant spectral distribution, i.e.
        the irradiance emitter.
    sds_tcs : MultiSpectralDistributions
        *Test colour samples* spectral distributions.
    cmfs : XYZ_ColourMatchingFunctions
        Standard observer colour matching functions.

    Returns
    -------
    list
        *Test colour samples* colorimetry data under the given test light
        source or reference illuminant spectral distribution.

    Examples
    --------
    >>> delta_E_to_R_f(4.4410383190)  # doctest: +ELLIPSIS
    70.1208254...
    """

    XYZ_w = sd_to_XYZ(sd_ones(), cmfs, sd_irradiance)
    Y_b = 20
    L_A = 100
    surround = VIEWING_CONDITIONS_CIECAM02['Average']

    tcs_data = []
    for sd_tcs in sds_tcs.to_sds():
        XYZ = sd_to_XYZ(sd_tcs, cmfs, sd_irradiance)
        CAM = XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround, True)
        JMh = CAM.J, CAM.M, CAM.h
        Jpapbp = JMh_CIECAM02_to_CAM02UCS(JMh)

        tcs_data.append(
            TCS_ColorimetryData_CIE2017(sd_tcs.name, XYZ, CAM, JMh, Jpapbp))

    return tcs_data


def delta_E_to_R_f(delta_E):
    """
    Converts from colour-appearance difference to
    *CIE 2017 Colour Fidelity Index* (CFI) :math:`R_f` value.

    Parameters
    ----------
    delta_E : numeric
        Euclidean distance between two colours in *CAM02-UCS* colourspace.

    Returns
    -------
    float
        Corresponding *CIE 2017 Colour Fidelity Index* (CFI) :math:`R_f` value.
    """

    c_f = 6.73

    return 10 * np.log(np.exp((100 - c_f * delta_E) / 10) + 1)
