// Copyright 2026 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
//    list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
//    contributors may be used to endorse or promote products derived from
//    this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#ifndef SRC_DAWN_COMMON_COLORSPACE_H_
#define SRC_DAWN_COMMON_COLORSPACE_H_

#include "src/dawn/common/Algebra.h"

namespace dawn {

// Colorspaces define how numbers used to encode a color are related to physical quantities of
// light (for example captured by a camera, or emitted by a display). Dawn needs some handling of
// colorspace because ExternalTexture allows importing data in a source colorspace and sampling it
// in shaders with a different colorspace called the destination colorspace.
//
// Dawn also needs to tell the OS how to display values in a wgpu::Surface texture to the user, but
// that's done by passing the correct enum to the backend API's swapchain/surface configuration so
// Dawn doesn't need to do any math for that path.
//
// # RGB color spaces
//
// Colorspaces are defined with respect to a reference colorspace, usually the CIE XYZ one but the
// exact reference doesn't matter much in Dawn as long as all conversion are defined from/to it (so
// combining a transform from colorspace from A to XYZ and from XYZ to B gives a transform from A to
// B). Numbers in an RGB colorspace (as opposed to YCbCr, see below) can be converted to XYZ by
// performing two steps.
//
// The electro-optical transfer function (EOTF) that's used to transform colors in "perceptual"
// space to colors in "physical" space. This is because the human eye has a logarithmic response
// to light intensity (such as doubling the amount of light "feels" like a single step increase,
// the same mechanism as decibels for audition). Storing values in perceptual space is done to have
// as much perceptual precision for low and high color values and allows for smooth perceptual
// color gradients. (Read up on sRGB, that's the most used and well known perceptual color space).
// The inverse transfer function is called the opto-electronic transfer function (OETF).
//
// The color primaries, what it means to say "red of 1" in that colorspace. This is where CIE XYZ is
// used as the primaries for the colorspace are mapped to colors in the XYZ colorspace. XYZ itself
// is a physical standard developed through experimentation on the human perception of color. The
// mapping from a colorspace to XYZ is a linear mapping represented by a matrix whose column vectors
// are the XYZ colors corresponding to the colorspace's primaries.
//
// Thus the transformation between colorspace A and B is:
//
//  - Apply A's EOTF.
//  - Transform the primaries, this is done with a single matrix multiply that's a combination or:
//    - Apply the matrix from A to XYZ
//    - Apply the matrix from XYZ to B
//  - Apply B's OETF (the inverse of the EOTF)
//
// # YCbCr color spaces
//
// The human vision is more sensitive to variation in light intensity (luminance) than color
// (chroma) so video (and some images) are stored in a YCbCr representation with the luminance Y and
// two dimensions for the chroma: Cr "the difference in red" and "Cb the difference in blue". Green
// is usually the luminance as it has the most response from human vision. Then the chroma data can
// be stored with lower frequency than luminance data, which gives storage and memory traffic gains.
//
// YCbCr colorspaces define how to convert YCbCr data to RGB, and how that RGB data converts to XYZ
// (with an EOTF and primaries). YCbCr data is converted to RGB by performing two steps.
//
// The transfer function defines how to map numbers to values with Y in [0, 1] and both Cb and Cr in
// [-0.5, 0.5] (assuming we are not using some wide gamut). The "full range" converts by normalizing
// values almost as one would expect (a uint8_t Y by dividing by 255 and a uint8_t Cb/Cr by dividing
// by 128 and offsetting by -0.5). However historically some values where used as control signal and
// a "narrow range" of values was used (for Y this is [16, 235]).
//
// The YCbCr to RGB linear transformation that defines how Y, Cb and Cr map to RGB..
//
// Thus the transformation between YCbCr colorspace A and RGB colorspace B is:
//
// - Transform numbers representing YCbCr to RGB with a single 4x3 matrix multiply that's a
//   combination of:
//   - Apply the 4x3 matrix in homogeneous coordinate space that computes the range (the homogeneous
//     coordinate space is used to allow for translation to be encoded in the matrix).
//   - Apply the 3x3 YCbCr to RGB linear transformation.
//
//  (same steps as for the RGB case)
//
//  - Apply A's EOTF.
//  - Transform the primaries, this is done with a single matrix multiply that's a combination or:
//    - Apply the matrix from A to XYZ
//    - Apply the matrix from XYZ to B
//  - Apply B's OETF (the inverse of the EOTF)
//
// # Resources
//
// The Khronos Data Format Specification that makes the ITU standards more approachable (it is
// linked to many times below):
// https://registry.khronos.org/DataFormat/specs/1.4/dataformat.1.4.html
//
// The CSS color module are another great reference:
//
//  - CSS Color Module Level 4 for the definition of many SDR color spaces:
//    https://www.w3.org/TR/css-color-4/
//  - CSS Color Module HDR Level 1 for the definition of HDR color spaces:
//    https://www.w3.org/TR/css-color-hdr-1/
//
// The ITU specifications for the "source of truth" on many of the YCbCr colorspaces.
//
// Chromium's gfx::Colorspace.

// YCbCr range transforms, assuming that the Y and Cb Cr values are already normalized to [0, 1]
// since we get them via texture sampling.

// https://registry.khronos.org/DataFormat/specs/1.4/dataformat.1.4.html#QUANTIZATION_FULL
inline constexpr math::Mat4x3f kYCbCrRange_Full = {
    {1.0, 0.0, 0.0},
    {0.0, 1.0, 0.0},
    {0.0, 0.0, 1.0},
    {0.0, -128.0 / 255.0, -128.0 / 255.0},
};

// https://registry.khronos.org/DataFormat/specs/1.4/dataformat.1.4.html#QUANTIZATION_NARROW
constexpr float kYCbCrRange_NarrowYFactor = 255.0 / 219.0;
constexpr float kYCbCrRange_NarrowChromaFactor = 255.0 / 224.0;
constexpr math::Mat4x3f kYCbCrRange_Narrow = {
    {kYCbCrRange_NarrowYFactor, 0.0, 0.0},
    {0.0, kYCbCrRange_NarrowChromaFactor, 0.0},
    {0.0, 0.0, kYCbCrRange_NarrowChromaFactor},
    {-16.0 / 255.0 * kYCbCrRange_NarrowYFactor,        //
     -128.0 / 255.0 * kYCbCrRange_NarrowChromaFactor,  //
     -128.0 / 255.0 * kYCbCrRange_NarrowChromaFactor},
};

// YCbCr to RGB matrices for various standards

// https://registry.khronos.org/DataFormat/specs/1.4/dataformat.1.4.html#MODEL_BT601
inline constexpr math::Mat3x3f kYCbCrToRGB_Rec601 = {
    {1.0, 1.0, 1.0},
    {0.0, -(0.202008 / 0.587), 1.772},
    {1.402, -(0.419198 / 0.587), 0.0},
};

// https://registry.khronos.org/DataFormat/specs/1.4/dataformat.1.4.html#MODEL_BT709
inline constexpr math::Mat3x3f kYCbCrToRGB_Rec709 = {
    {1.0, 1.0, 1.0},
    {0.0, -(0.13397432 / 0.7152), 1.8556},
    {1.5748, -(0.33480248 / 0.7152), 0.0},
};

// https://registry.khronos.org/DataFormat/specs/1.4/dataformat.1.4.html#MODEL_BT2020
inline constexpr math::Mat3x3f kYCbCrToRGB_Rec2020 = {
    {1.0, 1.0, 1.0},
    {0.0, -(0.11156702 / 0.6780), 1.8814},
    {1.4746, -(0.38737742 / 0.6780), 0.0},
};

// RGB to XYZ (D65) for various standards.
// TODO(https://crbug.com/468988322): Define an XYZPrimaries structure and compute the matrices from
// it. The wx and wy give the coordinates of the white point and would allow adapting to D50 if
// needed.

// https://registry.khronos.org/DataFormat/specs/1.4/dataformat.1.4.html#PRIMARIES_BT601_EBU
// Rec 601 625-line.
inline constexpr math::Mat3x3f kRGBToXYZ_Rec601 = {{0.430554, 0.222004, 0.020182},
                                                   {0.341550, 0.706655, 0.129553},
                                                   {0.178352, 0.071341, 0.939322}};
inline constexpr math::Mat3x3f kXYZToRGB_Rec601 = kRGBToXYZ_Rec601.Inverse();

// https://registry.khronos.org/DataFormat/specs/1.4/dataformat.1.4.html#PRIMARIES_BT709
inline constexpr math::Mat3x3f kRGBToXYZ_Rec709 = {
    {0.412391, 0.212639, 0.019331},
    {0.357584, 0.715169, 0.119195},
    {0.180481, 0.072192, 0.950532},
};
inline constexpr math::Mat3x3f kXYZToRGB_Rec709 = kRGBToXYZ_Rec709.Inverse();

// sRGB is the same as Rec709.
inline constexpr math::Mat3x3f kRGBToXYZ_sRGB = kRGBToXYZ_Rec709;
inline constexpr math::Mat3x3f kXYZToRGB_sRGB = kXYZToRGB_Rec709;

// https://registry.khronos.org/DataFormat/specs/1.4/dataformat.1.4.html#PRIMARIES_BT2020
inline constexpr math::Mat3x3f kRGBToXYZ_Rec2020 = {
    {0.636958, 0.262700, 0.000000},
    {0.144617, 0.677998, 0.028073},
    {0.168881, 0.059302, 1.060985},
};
inline constexpr math::Mat3x3f kXYZToRGB_Rec2020 = kRGBToXYZ_Rec2020.Inverse();

// https://registry.khronos.org/DataFormat/specs/1.4/dataformat.1.4.html#PRIMARIES_BT2020
inline constexpr math::Mat3x3f kRGBToXYZ_DisplayP3 = {
    {0.4865709486, 0.2289745641, 0.0000000000},
    {0.2656676932, 0.6917385218, 0.0451133819},
    {0.1982172852, 0.0792869141, 1.0439443689},
};
inline constexpr math::Mat3x3f kXYZToRGB_DisplayP3 = kRGBToXYZ_DisplayP3.Inverse();

// Transfer functions for various standards, in a format.

// Returns an EOTF that performs the following transformation.
//
//    if (abs(v) < D) {
//        return sign(v) * (C * abs(v) + F);
//    }
//    return pow(A * x + B, G) + E
//
// TODO(https://crbug.com/497571469): Add support for PQ and HLG.
struct TransferFunction {
    float g = 1;
    float a = 1;
    float b = 0;
    float c = 0;
    float d = 0;
    float e = 0;
    float f = 0;
};

inline constexpr TransferFunction kEOTF_Identity = {};

// https://registry.khronos.org/DataFormat/specs/1.4/dataformat.1.4.html#TRANSFER_SRGB
inline constexpr TransferFunction kEOTF_sRGB = {
    .g = 2.4,
    .a = 1.0 / 1.055,
    .b = 0.055 / 1.055,
    .c = 1.0 / 12.92,
    .d = 0.04045,
    .e = 0,
    .f = 0,
};
inline constexpr TransferFunction kEOTFInverse_sRGB = {
    .g = 1.0 / 2.4,
    .a = 1.13711,  // 1.055 ^ 2.4
    .b = 0,
    .c = 12.92,
    .d = 0.0031308,
    .e = -0.055,
    .f = 0,
};

// https://registry.khronos.org/DataFormat/specs/1.4/dataformat.1.4.html#TRANSFER_DCIP3
inline constexpr TransferFunction kEOTF_DisplayP3 = {
    .g = 2.6,
    .a = 1.0,
    .b = 0,
    .c = 0,
    .d = 0,
    .e = 0,
    .f = 0,
};
inline constexpr TransferFunction kEOTFInverse_DisplayP3 = {
    .g = 1.0 / 2.6,
    .a = 1.0,
    .b = 0,
    .c = 0,
    .d = 0,
    .e = 0,
    .f = 0,
};

// https://registry.khronos.org/DataFormat/specs/1.4/dataformat.1.4.html#TRANSFER_ITU
inline constexpr TransferFunction kEOTF_SMPTE_170M = {
    .g = 1.0 / 0.45,
    .a = 1.0 / 1.099,
    .b = 0.099 / 1.099,
    .c = 1.0 / 4.5,
    .d = 0.0812,
    .e = 0,
    .f = 0,
};

// Luminance values of various color spaces for the white of value "1" after the transfer function.

// When SDR content is composited in HDR, it should be assumed to have a white at 203 nits, even if
// the sRGB spec defines it as 80 nits. See
// https://www.w3.org/TR/css-color-hdr-1/#Compositing-SDR-HDR
inline constexpr float kSDRLuminanceOf1 = 203;

// https://www.w3.org/TR/css-color-hdr-1/#valdef-color-rec2100-pq
inline constexpr float kPQLuminanceOf1 = 10000;

// https://www.w3.org/TR/css-color-hdr-1/#valdef-color-rec2100-hlg
// The magic constant is used to place "media white" (so 1) at HLG value 0.75
inline constexpr float kHLGLuminanceOf1 = 3.7743 * kSDRLuminanceOf1;

}  // namespace dawn

#endif  // SRC_DAWN_COMMON_COLORSPACE_H_
