/*****************************************************************************
 * d3d_shaders.c: Direct3D Shader APIs
 *****************************************************************************
 * Copyright (C) 2017-2021 VLC authors and VideoLAN
 *
 * Authors: Steve Lhomme <robux4@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include <vlc_common.h>

#include <assert.h>

#include "common.h"

#include "d3d_shaders.h"

#define SPHERE_RADIUS 1.f

#define SPHERE_SLICES 128
#define nbLatBands SPHERE_SLICES
#define nbLonBands SPHERE_SLICES

float (D3D_GetFormatLuminance)(vlc_object_t *o, const video_format_t *fmt)
{
    switch (fmt->transfer)
    {
        case TRANSFER_FUNC_SMPTE_ST2084:
            /* that's the default PQ value if the metadata are not set */
            return MAX_PQ_BRIGHTNESS;
        case TRANSFER_FUNC_HLG:
            return MAX_HLG_BRIGHTNESS;
        case TRANSFER_FUNC_BT470_BG:
        case TRANSFER_FUNC_BT470_M:
        case TRANSFER_FUNC_BT709:
        case TRANSFER_FUNC_SRGB:
            return DEFAULT_BRIGHTNESS;
        default:
            msg_Dbg(o, "unhandled source transfer %d", fmt->transfer);
            return DEFAULT_BRIGHTNESS;
    }
}

struct xy_primary {
    double x, y;
};

struct cie1931_primaries {
    struct xy_primary red, green, blue, white;
};

static const struct cie1931_primaries STANDARD_PRIMARIES[] = {
#define CIE_D65 {0.31271, 0.32902}
#define CIE_C   {0.31006, 0.31616}

    [COLOR_PRIMARIES_BT601_525] = {
        .red   = {0.630, 0.340},
        .green = {0.310, 0.595},
        .blue  = {0.155, 0.070},
        .white = CIE_D65
    },
    [COLOR_PRIMARIES_BT601_625] = {
        .red   = {0.640, 0.330},
        .green = {0.290, 0.600},
        .blue  = {0.150, 0.060},
        .white = CIE_D65
    },
    [COLOR_PRIMARIES_BT709] = {
        .red   = {0.640, 0.330},
        .green = {0.300, 0.600},
        .blue  = {0.150, 0.060},
        .white = CIE_D65
    },
    [COLOR_PRIMARIES_BT2020] = {
        .red   = {0.708, 0.292},
        .green = {0.170, 0.797},
        .blue  = {0.131, 0.046},
        .white = CIE_D65
    },
    [COLOR_PRIMARIES_DCI_P3] = {
        .red   = {0.680, 0.320},
        .green = {0.265, 0.690},
        .blue  = {0.150, 0.060},
        .white = CIE_D65
    },
    [COLOR_PRIMARIES_FCC1953] = {
        .red   = {0.670, 0.330},
        .green = {0.210, 0.710},
        .blue  = {0.140, 0.080},
        .white = CIE_C
    },
#undef CIE_D65
#undef CIE_C
};

static void ChromaticAdaptation(const struct xy_primary *src_white,
                                const struct xy_primary *dst_white,
                                double in_out[3 * 3])
{
    if (fabs(src_white->x - dst_white->x) < 1e-6 &&
        fabs(src_white->y - dst_white->y) < 1e-6)
        return;

    /* TODO, see http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html */
}

static void Float3x3Inverse(double in_out[3 * 3])
{
    double m00 = in_out[0 + 0*3], m01 = in_out[1 + 0*3], m02 = in_out[2 + 0*3],
          m10 = in_out[0 + 1*3], m11 = in_out[1 + 1*3], m12 = in_out[2 + 1*3],
          m20 = in_out[0 + 2*3], m21 = in_out[1 + 2*3], m22 = in_out[2 + 2*3];

    // calculate the adjoint
    in_out[0 + 0*3] =  (m11 * m22 - m21 * m12);
    in_out[1 + 0*3] = -(m01 * m22 - m21 * m02);
    in_out[2 + 0*3] =  (m01 * m12 - m11 * m02);
    in_out[0 + 1*3] = -(m10 * m22 - m20 * m12);
    in_out[1 + 1*3] =  (m00 * m22 - m20 * m02);
    in_out[2 + 1*3] = -(m00 * m12 - m10 * m02);
    in_out[0 + 2*3] =  (m10 * m21 - m20 * m11);
    in_out[1 + 2*3] = -(m00 * m21 - m20 * m01);
    in_out[2 + 2*3] =  (m00 * m11 - m10 * m01);

    // calculate the determinant (as inverse == 1/det * adjoint,
    // adjoint * m == identity * det, so this calculates the det)
    double det = m00 * in_out[0 + 0*3] + m10 * in_out[1 + 0*3] + m20 * in_out[2 + 0*3];
    det = 1.0f / det;

    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++)
            in_out[j + i*3] *= det;
    }
}

static void Float3x3Multiply(double m1[3 * 3], const double m2[3 * 3])
{
    double a00 = m1[0 + 0*3], a01 = m1[1 + 0*3], a02 = m1[2 + 0*3],
           a10 = m1[0 + 1*3], a11 = m1[1 + 1*3], a12 = m1[2 + 1*3],
           a20 = m1[0 + 2*3], a21 = m1[1 + 2*3], a22 = m1[2 + 2*3];

    for (int i = 0; i < 3; i++) {
        m1[i + 0*3] = a00 * m2[i + 0*3] + a01 * m2[i + 1*3] + a02 * m2[i + 2*3];
        m1[i + 1*3] = a10 * m2[i + 0*3] + a11 * m2[i + 1*3] + a12 * m2[i + 2*3];
        m1[i + 2*3] = a20 * m2[i + 0*3] + a21 * m2[i + 1*3] + a22 * m2[i + 2*3];
    }
}

static void Float3Multiply(const double in[3], const double mult[3 * 3], double out[3])
{
    for (size_t i=0; i<3; i++)
    {
        out[i] = mult[i + 0*3] * in[0] +
                 mult[i + 1*3] * in[1] +
                 mult[i + 2*3] * in[2];
    }
}

/* from http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html */
static void GetRGB2XYZMatrix(const struct cie1931_primaries *primaries,
                             double out[3 * 3])
{
#define RED   0
#define GREEN 1
#define BLUE  2
    double X[3], Y[3], Z[3], S[3], W[3];
    double W_TO_S[3 * 3];

    X[RED  ] = primaries->red.x / primaries->red.y;
    X[GREEN] = 1;
    X[BLUE ] = (1 - primaries->red.x - primaries->red.y) / primaries->red.y;

    Y[RED  ] = primaries->green.x / primaries->green.y;
    Y[GREEN] = 1;
    Y[BLUE ] = (1 - primaries->green.x - primaries->green.y) / primaries->green.y;

    Z[RED  ] = primaries->blue.x / primaries->blue.y;
    Z[GREEN] = 1;
    Z[BLUE ] = (1 - primaries->blue.x - primaries->blue.y) / primaries->blue.y;

    W_TO_S[0 + 0*3] = X[RED  ];
    W_TO_S[1 + 0*3] = X[GREEN];
    W_TO_S[2 + 0*3] = X[BLUE ];
    W_TO_S[0 + 1*3] = Y[RED  ];
    W_TO_S[1 + 1*3] = Y[GREEN];
    W_TO_S[2 + 1*3] = Y[BLUE ];
    W_TO_S[0 + 2*3] = Z[RED  ];
    W_TO_S[1 + 2*3] = Z[GREEN];
    W_TO_S[2 + 2*3] = Z[BLUE ];

    Float3x3Inverse(W_TO_S);

    W[0] = primaries->white.x / primaries->white.y; /* Xw */
    W[1] = 1;                  /* Yw */
    W[2] = (1 - primaries->white.x - primaries->white.y) / primaries->white.y; /* Yw */

    Float3Multiply(W, W_TO_S, S);

    out[0 + 0*3] = S[RED  ] * X[RED  ];
    out[1 + 0*3] = S[GREEN] * Y[RED  ];
    out[2 + 0*3] = S[BLUE ] * Z[RED  ];
    out[0 + 1*3] = S[RED  ] * X[GREEN];
    out[1 + 1*3] = S[GREEN] * Y[GREEN];
    out[2 + 1*3] = S[BLUE ] * Z[GREEN];
    out[0 + 2*3] = S[RED  ] * X[BLUE ];
    out[1 + 2*3] = S[GREEN] * Y[BLUE ];
    out[2 + 2*3] = S[BLUE ] * Z[BLUE ];
#undef RED
#undef GREEN
#undef BLUE
}

/* from http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html */
static void GetXYZ2RGBMatrix(const struct cie1931_primaries *primaries,
                             double out[3 * 3])
{
    GetRGB2XYZMatrix(primaries, out);
    Float3x3Inverse(out);
}

static void GetPrimariesTransform(FLOAT Primaries[4*3], video_color_primaries_t src,
                                  video_color_primaries_t dst)
{
    const struct cie1931_primaries *p_src = &STANDARD_PRIMARIES[src];
    const struct cie1931_primaries *p_dst = &STANDARD_PRIMARIES[dst];
    double rgb2xyz[3 * 3], xyz2rgb[3 * 3];

    /* src[RGB] -> src[XYZ] */
    GetRGB2XYZMatrix(p_src, rgb2xyz);

    /* src[XYZ] -> dst[XYZ] */
    ChromaticAdaptation(&p_src->white, &p_dst->white, rgb2xyz);

    /* dst[XYZ] -> dst[RGB] */
    GetXYZ2RGBMatrix(p_dst, xyz2rgb);

    /* src[RGB] -> src[XYZ] -> dst[XYZ] -> dst[RGB] */
    Float3x3Multiply(xyz2rgb, rgb2xyz);

    for (size_t i=0;i<3; ++i)
    {
        for (size_t j=0;j<3; ++j)
            Primaries[j + i*4] = xyz2rgb[j + i*3];
        Primaries[3 + i*4] = 0;
    }
}

bool D3D_UpdateQuadOpacity(d3d_quad_t *quad, float opacity)
{
    if (quad->shaderConstants->Opacity == opacity)
        return false;

    quad->shaderConstants->Opacity = opacity;
    return true;
}

bool D3D_UpdateQuadLuminanceScale(d3d_quad_t *quad, float luminanceScale)
{
    if (quad->shaderConstants->LuminanceScale == luminanceScale)
        return false;

    quad->shaderConstants->LuminanceScale = luminanceScale;
    return true;
}

static void MultMat43(FLOAT dst[4*3], const FLOAT left[4*3], const FLOAT right[4*3])
{
    // Cache the invariants in registers
    FLOAT x = left[0*4 + 0];
    FLOAT y = left[0*4 + 1];
    FLOAT z = left[0*4 + 2];
    FLOAT w = left[0*4 + 3];
    // Perform the operation on the first row
    dst[0*4 + 0] = (right[0*4 + 0] * x) + (right[1*4 + 0] * y) + (right[2*4 + 0] * z);
    dst[0*4 + 1] = (right[0*4 + 1] * x) + (right[1*4 + 1] * y) + (right[2*4 + 1] * z);
    dst[0*4 + 2] = (right[0*4 + 2] * x) + (right[1*4 + 2] * y) + (right[2*4 + 2] * z);
    dst[0*4 + 3] = (right[0*4 + 3] * x) + (right[1*4 + 3] * y) + (right[2*4 + 3] * z) + w;
    // Repeat for all the other rows
    x = left[1*4 + 0];
    y = left[1*4 + 1];
    z = left[1*4 + 2];
    w = left[1*4 + 3];
    dst[1*4 + 0] = (right[0*4 + 0] * x) + (right[1*4 + 0] * y) + (right[2*4 + 0] * z);
    dst[1*4 + 1] = (right[0*4 + 1] * x) + (right[1*4 + 1] * y) + (right[2*4 + 1] * z);
    dst[1*4 + 2] = (right[0*4 + 2] * x) + (right[1*4 + 2] * y) + (right[2*4 + 2] * z);
    dst[1*4 + 3] = (right[0*4 + 3] * x) + (right[1*4 + 3] * y) + (right[2*4 + 3] * z) + w;
    x = left[2*4 + 0];
    y = left[2*4 + 1];
    z = left[2*4 + 2];
    w = left[2*4 + 3];
    dst[2*4 + 0] = (right[0*4 + 0] * x) + (right[1*4 + 0] * y) + (right[2*4 + 0] * z);
    dst[2*4 + 1] = (right[0*4 + 1] * x) + (right[1*4 + 1] * y) + (right[2*4 + 1] * z);
    dst[2*4 + 2] = (right[0*4 + 2] * x) + (right[1*4 + 2] * y) + (right[2*4 + 2] * z);
    dst[2*4 + 3] = (right[0*4 + 3] * x) + (right[1*4 + 3] * y) + (right[2*4 + 3] * z) + w;
    // x = left[3*4 + 0];
    // y = left[3*4 + 1];
    // z = left[3*4 + 2];
    // w = left[3*4 + 3];
    // dst[3*4 + 0] = (right[0*4 + 0] * x) + (right[1*4 + 0] * y) + (right[2*4 + 0] * z) + (right[3*4 + 0] * w);
    // dst[3*4 + 1] = (right[0*4 + 1] * x) + (right[1*4 + 1] * y) + (right[2*4 + 1] * z) + (right[3*4 + 1] * w);
    // dst[3*4 + 2] = (right[0*4 + 2] * x) + (right[1*4 + 2] * y) + (right[2*4 + 2] * z) + (right[3*4 + 2] * w);
    // dst[3*4 + 3] = (right[0*4 + 3] * x) + (right[1*4 + 3] * y) + (right[2*4 + 3] * z) + (right[3*4 + 3] * w);
}

void D3D_SetupQuad(vlc_object_t *o, const video_format_t *fmt, d3d_quad_t *quad,
                   const display_info_t *displayFormat)
{
    quad->shaderConstants->LuminanceScale = (float)displayFormat->luminance_peak / D3D_GetFormatLuminance(o, fmt);

    /* pixel shader constant buffer */
    quad->shaderConstants->Opacity = 1.0;
    if (fmt->i_visible_width == fmt->i_width)
        quad->shaderConstants->BoundaryX = 1.0; /* let texture clamping happen */
    else
        quad->shaderConstants->BoundaryX = (FLOAT) (fmt->i_visible_width - 1) / fmt->i_width;
    if (fmt->i_visible_height == fmt->i_height)
        quad->shaderConstants->BoundaryY = 1.0; /* let texture clamping happen */
    else
        quad->shaderConstants->BoundaryY = (FLOAT) (fmt->i_visible_height - 1) / fmt->i_height;

#define COLOR_CONSTANTS_601_KR 0.2990f
#define COLOR_CONSTANTS_601_KB 0.1140f
#define COLOR_CONSTANTS_601_KG (1.0f - COLOR_CONSTANTS_601_KR - COLOR_CONSTANTS_601_KB)

#define COLOR_CONSTANTS_709_KR 0.2126f
#define COLOR_CONSTANTS_709_KB 0.0722f
#define COLOR_CONSTANTS_709_KG (1.0f - COLOR_CONSTANTS_709_KR - COLOR_CONSTANTS_709_KB)

#define COLOR_CONSTANTS_2020_KR 0.2627f
#define COLOR_CONSTANTS_2020_KB 0.0593f
#define COLOR_CONSTANTS_2020_KG (1.0f - COLOR_CONSTANTS_2020_KR - COLOR_CONSTANTS_2020_KB)

#define COLOR_SHIFT_STUDIO_8_MIN_Y   16
#define COLOR_SHIFT_STUDIO_8_MAX_Y   235
#define COLOR_SHIFT_STUDIO_8_MIN_UV  16
#define COLOR_SHIFT_STUDIO_8_MAX_UV  240
#define COLOR_SHIFT_STUDIO_8_UV     ((double)128)
#define COLOR_COEFF_STUDIO_8_Y      ((double)(COLOR_SHIFT_STUDIO_8_MAX_Y - COLOR_SHIFT_STUDIO_8_MIN_Y))
#define COLOR_COEFF_STUDIO_8_UV     ((double)((COLOR_SHIFT_STUDIO_8_MAX_UV - COLOR_SHIFT_STUDIO_8_MIN_UV) / 2))

#define COLOR_SHIFT_STUDIO_10_MIN_Y   64
#define COLOR_SHIFT_STUDIO_10_MAX_Y   940
#define COLOR_SHIFT_STUDIO_10_MIN_UV  64
#define COLOR_SHIFT_STUDIO_10_MAX_UV  960
#define COLOR_SHIFT_STUDIO_10_UV     ((double)512)
#define COLOR_COEFF_STUDIO_10_Y      ((double)(COLOR_SHIFT_STUDIO_10_MAX_Y - COLOR_SHIFT_STUDIO_10_MIN_Y))
#define COLOR_COEFF_STUDIO_10_UV     ((double)((COLOR_SHIFT_STUDIO_10_MAX_UV - COLOR_SHIFT_STUDIO_10_MIN_UV) / 2))

#define COLOR_SHIFT_STUDIO_12_MIN_Y   256
#define COLOR_SHIFT_STUDIO_12_MAX_Y   3760
#define COLOR_SHIFT_STUDIO_12_MIN_UV  256
#define COLOR_SHIFT_STUDIO_12_MAX_UV  3840
#define COLOR_SHIFT_STUDIO_12_UV     ((double)2048)
#define COLOR_COEFF_STUDIO_12_Y      ((double)(COLOR_SHIFT_STUDIO_12_MAX_Y - COLOR_SHIFT_STUDIO_12_MIN_Y))
#define COLOR_COEFF_STUDIO_12_UV     ((double)((COLOR_SHIFT_STUDIO_12_MAX_UV - COLOR_SHIFT_STUDIO_12_MIN_UV) / 2))

#define COLOR_COEFF_FULL_8_RGB      ((double)255)
#define COLOR_COEFF_FULL_10_RGB     ((double)1023)
#define COLOR_COEFF_FULL_12_RGB     ((double)4095)

#define COLOR_SHIFT_FULL_8_MIN_Y   0
#define COLOR_SHIFT_FULL_8_MAX_Y   COLOR_COEFF_FULL_8_RGB
#define COLOR_COEFF_FULL_8_Y      ((double)(COLOR_SHIFT_FULL_8_MAX_Y - COLOR_SHIFT_FULL_8_MIN_Y))
#define COLOR_COEFF_FULL_8_UV     ((double)(COLOR_SHIFT_FULL_8_MAX_Y - COLOR_SHIFT_FULL_8_MIN_Y) / 2.0f)
#define COLOR_SHIFT_FULL_8_UV     ((double)128)

#define COLOR_SHIFT_FULL_10_MIN_Y   0
#define COLOR_SHIFT_FULL_10_MAX_Y   COLOR_COEFF_FULL_10_RGB
#define COLOR_COEFF_FULL_10_Y      ((double)(COLOR_SHIFT_FULL_10_MAX_Y - COLOR_SHIFT_FULL_10_MIN_Y))
#define COLOR_COEFF_FULL_10_UV     ((double)(COLOR_SHIFT_FULL_10_MAX_Y - COLOR_SHIFT_FULL_10_MIN_Y) / 2.0f)
#define COLOR_SHIFT_FULL_10_UV     ((double)512)

#define COLOR_SHIFT_FULL_12_MIN_Y   0
#define COLOR_SHIFT_FULL_12_MAX_Y   COLOR_COEFF_FULL_12_RGB
#define COLOR_COEFF_FULL_12_Y      ((double)(COLOR_SHIFT_FULL_12_MAX_Y - COLOR_SHIFT_FULL_12_MIN_Y))
#define COLOR_COEFF_FULL_12_UV     ((double)(COLOR_SHIFT_FULL_12_MAX_Y - COLOR_SHIFT_FULL_12_MIN_Y) / 2.0f)
#define COLOR_SHIFT_FULL_12_UV     ((double)2048)

#define COLOR_SHIFT_STUDIO_8_MIN_RGB   16
#define COLOR_SHIFT_STUDIO_8_MAX_RGB   235
#define COLOR_COEFF_STUDIO_8_RGB      ((double)(COLOR_SHIFT_STUDIO_8_MAX_RGB - COLOR_SHIFT_STUDIO_8_MIN_RGB))


#define COLOR_MATRIX_YUV2RGB(src, bits, yuv_range, rgb_range) \
    static const FLOAT COLORSPACE_BT##src##_##yuv_range##_##bits##_TO_##rgb_range##_RGBA[4*3] = { \
       COLOR_COEFF_##rgb_range##_##bits##_RGB / COLOR_COEFF_##yuv_range##_##bits##_Y, \
       0.f, \
       COLOR_COEFF_##rgb_range##_##bits##_RGB / COLOR_COEFF_##yuv_range##_##bits##_UV * (1.f - COLOR_CONSTANTS_##src##_KR), \
       0.f, \
       \
       COLOR_COEFF_##rgb_range##_##bits##_RGB / COLOR_COEFF_##yuv_range##_##bits##_Y, \
       - COLOR_COEFF_##rgb_range##_##bits##_RGB / COLOR_COEFF_##yuv_range##_##bits##_UV * (1.f - COLOR_CONSTANTS_##src##_KB) * COLOR_CONSTANTS_##src##_KB / COLOR_CONSTANTS_##src##_KG, \
       - COLOR_COEFF_##rgb_range##_##bits##_RGB / COLOR_COEFF_##yuv_range##_##bits##_UV * (1.f - COLOR_CONSTANTS_##src##_KR) * COLOR_CONSTANTS_##src##_KR / COLOR_CONSTANTS_##src##_KG, \
       0.f, \
       \
       COLOR_COEFF_##rgb_range##_##bits##_RGB / COLOR_COEFF_##yuv_range##_##bits##_Y, \
       COLOR_COEFF_##rgb_range##_##bits##_RGB / COLOR_COEFF_##yuv_range##_##bits##_UV * (1.f - COLOR_CONSTANTS_##src##_KB), \
       0.f, \
       0.f, \
    };

    const bool RGB_src_shader = DxgiIsRGBFormat(quad->textureFormat);

    FLOAT itu_black_level = 0.f;
    FLOAT itu_achromacy   = 0.f;
    if (!RGB_src_shader || fmt->color_range == COLOR_RANGE_LIMITED)
    {
        switch (quad->textureFormat->bitsPerChannel)
        {
        case 8:
            /* Rec. ITU-R BT.709-6 ¶4.6 */
            itu_black_level = COLOR_SHIFT_STUDIO_8_MIN_Y / COLOR_SHIFT_FULL_8_MAX_Y;
            itu_achromacy   = COLOR_SHIFT_STUDIO_8_UV    / COLOR_SHIFT_FULL_8_MAX_Y;
            break;
        case 10:
            /* Rec. ITU-R BT.709-6 ¶4.6 */
            itu_black_level = COLOR_SHIFT_STUDIO_10_MIN_Y / COLOR_SHIFT_FULL_10_MAX_Y;
            itu_achromacy   = COLOR_SHIFT_STUDIO_10_UV    / COLOR_SHIFT_FULL_10_MAX_Y;
            break;
        case 12:
            /* Rec. ITU-R BT.2020-2 Table 5 */
            itu_black_level = COLOR_SHIFT_STUDIO_12_MIN_Y / COLOR_SHIFT_FULL_12_MAX_Y;
            itu_achromacy   = COLOR_SHIFT_STUDIO_12_UV    / COLOR_SHIFT_FULL_12_MAX_Y;
            break;
        default:
            /* unknown bitdepth, use approximation for infinite bit depth */
            msg_Warn(o, "unsupported YUV bitdepth %u", (unsigned)quad->textureFormat->bitsPerChannel);
            itu_black_level =  16.f / 256.f;
            itu_achromacy   = 128.f / 256.f;
            break;
        }
    }

    static const FLOAT IDENTITY_4X3[4 * 3] = {
        1.f, 0.f, 0.f, 0.f,
        0.f, 1.f, 0.f, 0.f,
        0.f, 0.f, 1.f, 0.f,
    };


    COLOR_MATRIX_YUV2RGB(601,8,STUDIO,FULL);
    COLOR_MATRIX_YUV2RGB(709,8,STUDIO,FULL);
    COLOR_MATRIX_YUV2RGB(2020,8,STUDIO,FULL);
    COLOR_MATRIX_YUV2RGB(601,10,STUDIO,FULL);
    COLOR_MATRIX_YUV2RGB(709,10,STUDIO,FULL);
    COLOR_MATRIX_YUV2RGB(2020,10,STUDIO,FULL);
    COLOR_MATRIX_YUV2RGB(601,12,STUDIO,FULL);
    COLOR_MATRIX_YUV2RGB(709,12,STUDIO,FULL);
    COLOR_MATRIX_YUV2RGB(2020,12,STUDIO,FULL);
    COLOR_MATRIX_YUV2RGB(601,8,FULL,FULL);
    COLOR_MATRIX_YUV2RGB(709,8,FULL,FULL);
    COLOR_MATRIX_YUV2RGB(2020,8,FULL,FULL);
    COLOR_MATRIX_YUV2RGB(601,10,FULL,FULL);
    COLOR_MATRIX_YUV2RGB(709,10,FULL,FULL);
    COLOR_MATRIX_YUV2RGB(2020,10,FULL,FULL);
    COLOR_MATRIX_YUV2RGB(601,12,FULL,FULL);
    COLOR_MATRIX_YUV2RGB(709,12,FULL,FULL);
    COLOR_MATRIX_YUV2RGB(2020,12,FULL,FULL);

    static const FLOAT COLORSPACE_FULL_RGBA_TO_BT601_YUV[4*3] = {
        0.299000f,  0.587000f,  0.114000f, 0.f,
       -0.168736f, -0.331264f,  0.500000f, 0.f,
        0.500000f, -0.418688f, -0.081312f, 0.f,
    };

    FLOAT WhitePoint[4*3];
    memcpy(WhitePoint, IDENTITY_4X3, sizeof(WhitePoint));

    const FLOAT *ppColorspace;
    if (RGB_src_shader == DxgiIsRGBFormat(displayFormat->pixelFormat))
    {
        ppColorspace = IDENTITY_4X3;
        WhitePoint[0*4 + 3] = -itu_black_level;
        WhitePoint[1*4 + 3] = -itu_black_level;
        WhitePoint[2*4 + 3] = -itu_black_level;
    }
    else if (RGB_src_shader)
    {
        ppColorspace = COLORSPACE_FULL_RGBA_TO_BT601_YUV;
        WhitePoint[0*4 + 3] = -itu_black_level;
        WhitePoint[1*4 + 3] = itu_achromacy;
        WhitePoint[2*4 + 3] = itu_achromacy;
    }
    else
    {
        switch (fmt->space){
            case COLOR_SPACE_BT709:
                if (fmt->color_range == COLOR_RANGE_FULL)
                {
                    if (quad->textureFormat->bitsPerChannel == 12)
                        ppColorspace = COLORSPACE_BT709_FULL_12_TO_FULL_RGBA;
                    else if (quad->textureFormat->bitsPerChannel == 10)
                        ppColorspace = COLORSPACE_BT709_FULL_10_TO_FULL_RGBA;
                    else
                        ppColorspace = COLORSPACE_BT709_FULL_8_TO_FULL_RGBA;
                }
                else
                {
                    if (quad->textureFormat->bitsPerChannel == 12)
                        ppColorspace = COLORSPACE_BT709_STUDIO_12_TO_FULL_RGBA;
                    else if (quad->textureFormat->bitsPerChannel == 10)
                        ppColorspace = COLORSPACE_BT709_STUDIO_10_TO_FULL_RGBA;
                    else
                        ppColorspace = COLORSPACE_BT709_STUDIO_8_TO_FULL_RGBA;
                }
                break;
            case COLOR_SPACE_BT2020:
                if (fmt->color_range == COLOR_RANGE_FULL)
                {
                    if (quad->textureFormat->bitsPerChannel == 12)
                        ppColorspace = COLORSPACE_BT2020_FULL_12_TO_FULL_RGBA;
                    else if (quad->textureFormat->bitsPerChannel == 10)
                        ppColorspace = COLORSPACE_BT2020_FULL_10_TO_FULL_RGBA;
                    else
                        ppColorspace = COLORSPACE_BT2020_FULL_8_TO_FULL_RGBA;
                }
                else
                {
                    if (quad->textureFormat->bitsPerChannel == 12)
                        ppColorspace = COLORSPACE_BT2020_STUDIO_12_TO_FULL_RGBA;
                    else if (quad->textureFormat->bitsPerChannel == 10)
                        ppColorspace = COLORSPACE_BT2020_STUDIO_10_TO_FULL_RGBA;
                    else
                        ppColorspace = COLORSPACE_BT2020_STUDIO_8_TO_FULL_RGBA;
                }
                break;
            case COLOR_SPACE_BT601:
                if (fmt->color_range == COLOR_RANGE_FULL)
                {
                    if (quad->textureFormat->bitsPerChannel == 12)
                        ppColorspace = COLORSPACE_BT601_FULL_12_TO_FULL_RGBA;
                    else if (quad->textureFormat->bitsPerChannel == 10)
                        ppColorspace = COLORSPACE_BT601_FULL_10_TO_FULL_RGBA;
                    else
                        ppColorspace = COLORSPACE_BT601_FULL_8_TO_FULL_RGBA;
                }
                else
                {
                    if (quad->textureFormat->bitsPerChannel == 12)
                        ppColorspace = COLORSPACE_BT601_STUDIO_12_TO_FULL_RGBA;
                    else if (quad->textureFormat->bitsPerChannel == 10)
                        ppColorspace = COLORSPACE_BT601_STUDIO_10_TO_FULL_RGBA;
                    else
                        ppColorspace = COLORSPACE_BT601_STUDIO_8_TO_FULL_RGBA;
                }
                break;
            default:
            case COLOR_SPACE_UNDEF:
                msg_Warn(o, "unknown colorspace, using BT709");
                ppColorspace = COLORSPACE_BT709_STUDIO_8_TO_FULL_RGBA;
                break;
        }

        /* all matrices work in studio range and output in full range */
        if (fmt->color_range != COLOR_RANGE_FULL)
            WhitePoint[0*4 + 3] = -itu_black_level;
        WhitePoint[1*4 + 3] = -itu_achromacy;
        WhitePoint[2*4 + 3] = -itu_achromacy;
    }

    WhitePoint[0*4 + 0] *= quad->textureFormat->resourceFactor;
    WhitePoint[1*4 + 1] *= quad->textureFormat->resourceFactor;
    WhitePoint[2*4 + 2] *= quad->textureFormat->resourceFactor;

    MultMat43(quad->shaderConstants->Colorspace, ppColorspace, WhitePoint);

    if (fmt->primaries != displayFormat->primaries)
    {
        FLOAT Primaries[4*3];
        GetPrimariesTransform(Primaries, fmt->primaries, displayFormat->primaries);
        MultMat43(quad->shaderConstants->Colorspace, Primaries, quad->shaderConstants->Colorspace);
    }
}

/**
 * Compute the vertex ordering needed to rotate the video. Without
 * rotation, the vertices of the rectangle are defined in a counterclockwise
 * order. This function computes a remapping of the coordinates to
 * implement the rotation, given fixed texture coordinates.
 * The unrotated order is the following:
 * 3--2
 * |  |
 * 0--1
 * For a 180 degrees rotation it should like this:
 * 1--0
 * |  |
 * 2--3
 * Vertex 0 should be assigned coordinates at index 2 from the
 * unrotated order and so on, thus yielding order: 2 3 0 1.
 */
static void orientationVertexOrder(video_transform_t orientation, int vertex_order[static 4])
{
    switch (orientation) {
        case TRANSFORM_R90:
            vertex_order[0] = 3;
            vertex_order[1] = 0;
            vertex_order[2] = 1;
            vertex_order[3] = 2;
            break;
        case TRANSFORM_R270:
            vertex_order[0] = 1;
            vertex_order[1] = 2;
            vertex_order[2] = 3;
            vertex_order[3] = 0;
            break;
        case TRANSFORM_R180:
            vertex_order[0] = 2;
            vertex_order[1] = 3;
            vertex_order[2] = 0;
            vertex_order[3] = 1;
            break;
        case TRANSFORM_TRANSPOSE:
            vertex_order[0] = 2;
            vertex_order[1] = 1;
            vertex_order[2] = 0;
            vertex_order[3] = 3;
            break;
        case TRANSFORM_HFLIP:
            vertex_order[0] = 1;
            vertex_order[1] = 0;
            vertex_order[2] = 3;
            vertex_order[3] = 2;
            break;
        case TRANSFORM_VFLIP:
            vertex_order[0] = 3;
            vertex_order[1] = 2;
            vertex_order[2] = 1;
            vertex_order[3] = 0;
            break;
        case TRANSFORM_ANTI_TRANSPOSE: /* transpose + vflip */
            vertex_order[0] = 0;
            vertex_order[1] = 3;
            vertex_order[2] = 2;
            vertex_order[3] = 1;
            break;
       default:
            vertex_order[0] = 0;
            vertex_order[1] = 1;
            vertex_order[2] = 2;
            vertex_order[3] = 3;
            break;
    }
}

static void SetupQuadFlat(d3d_vertex_t *dst_data, const RECT *output,
                          const d3d_quad_t *quad,
                          WORD *triangle_pos, video_transform_t orientation)
{
    unsigned int src_width = quad->i_width;
    unsigned int src_height = quad->i_height;
    float MidX,MidY;

    float top, bottom, left, right;
    /* find the middle of the visible part of the texture, it will be a 0,0
     * the rest of the visible area must correspond to -1,1 */
    switch (orientation)
    {
    case TRANSFORM_R90: /* 90° anti clockwise */
        /* right/top aligned */
        MidY = (output->left + output->right) / 2.f;
        MidX = (output->top + output->bottom) / 2.f;
        top    =  MidY / (MidY - output->top);
        bottom = -(src_height - MidX) / (MidX - output->top);
        left   =  (MidX - src_height) / (MidX - output->left);
        right  =                 MidX / (MidX - (src_width - output->right));
        break;
    case TRANSFORM_R180: /* 180° */
        /* right/top aligned */
        MidY = (output->top + output->bottom) / 2.f;
        MidX = (output->left + output->right) / 2.f;
        top    =  (src_height - MidY) / (output->bottom - MidY);
        bottom = -MidY / (MidY - output->top);
        left   = -MidX / (MidX - output->left);
        right  =  (src_width  - MidX) / (output->right - MidX);
        break;
    case TRANSFORM_R270: /* 90° clockwise */
        /* right/top aligned */
        MidY = (output->left + output->right) / 2.f;
        MidX = (output->top + output->bottom) / 2.f;
        top    =  (src_width  - MidX) / (output->right - MidX);
        bottom = -MidY / (MidY - output->top);
        left   = -MidX / (MidX - output->left);
        right  =  (src_height - MidX) / (output->bottom - MidX);
        break;
    case TRANSFORM_ANTI_TRANSPOSE:
        MidY = (output->left + output->right) / 2.f;
        MidX = (output->top + output->bottom) / 2.f;
        top    =  (src_width  - MidX) / (output->right - MidX);
        bottom = -MidY / (MidY - output->top);
        left   = -(src_height - MidY) / (output->bottom - MidY);
        right  =  MidX / (MidX - output->left);
        break;
    case TRANSFORM_TRANSPOSE:
        MidY = (output->left + output->right) / 2.f;
        MidX = (output->top + output->bottom) / 2.f;
        top    =  (src_width  - MidX) / (output->right - MidX);
        bottom = -MidY / (MidY - output->top);
        left   = -MidX / (MidX - output->left);
        right  =  (src_height - MidY) / (output->bottom - MidY);
        break;
    case TRANSFORM_VFLIP:
        MidY = (output->top + output->bottom) / 2.f;
        MidX = (output->left + output->right) / 2.f;
        top    =  (src_height - MidY) / (output->bottom - MidY);
        bottom = -MidY / (MidY - output->top);
        left   = -MidX / (MidX - output->left);
        right  =  (src_width  - MidX) / (output->right - MidX);
        break;
    case TRANSFORM_HFLIP:
        MidY = (output->top + output->bottom) / 2.f;
        MidX = (output->left + output->right) / 2.f;
        top    =  MidY / (MidY - output->top);
        bottom = -(src_height - MidY) / (output->bottom - MidY);
        left   = -(src_width  - MidX) / (output->right - MidX);
        right  =  MidX / (MidX - output->left);
        break;
    case TRANSFORM_IDENTITY:
    default:
        /* left/top aligned */
        MidY = (output->top + output->bottom) / 2.f;
        MidX = (output->left + output->right) / 2.f;
        top    =  MidY / (MidY - output->top);
        bottom = -(src_height - MidY) / (output->bottom - MidY);
        left   = -MidX / (MidX - output->left);
        right  =  (src_width  - MidX) / (output->right - MidX);
        break;
    }

    const float vertices_coords[4][2] = {
        { left,  bottom },
        { right, bottom },
        { right, top    },
        { left,  top    },
    };

    /* Compute index remapping necessary to implement the rotation. */
    int vertex_order[4];
    orientationVertexOrder(orientation, vertex_order);

    for (int i = 0; i < 4; ++i) {
        dst_data[i].position.x  = vertices_coords[vertex_order[i]][0];
        dst_data[i].position.y  = vertices_coords[vertex_order[i]][1];
    }

    // bottom left
    dst_data[0].position.z = 0.0f;
    dst_data[0].texture.uv[0] = 0.0f;
    dst_data[0].texture.uv[1] = 1.0f;

    // bottom right
    dst_data[1].position.z = 0.0f;
    dst_data[1].texture.uv[0] = 1.0f;
    dst_data[1].texture.uv[1] = 1.0f;

    // top right
    dst_data[2].position.z = 0.0f;
    dst_data[2].texture.uv[0] = 1.0f;
    dst_data[2].texture.uv[1] = 0.0f;

    // top left
    dst_data[3].position.z = 0.0f;
    dst_data[3].texture.uv[0] = 0.0f;
    dst_data[3].texture.uv[1] = 0.0f;

    /* Make sure surfaces are facing the right way */
    if( orientation == TRANSFORM_HFLIP || orientation == TRANSFORM_VFLIP
     || orientation == TRANSFORM_TRANSPOSE  || orientation == TRANSFORM_ANTI_TRANSPOSE )
    {
        triangle_pos[0] = 0;
        triangle_pos[1] = 1;
        triangle_pos[2] = 3;

        triangle_pos[3] = 3;
        triangle_pos[4] = 1;
        triangle_pos[5] = 2;
    }
    else
    {
        triangle_pos[0] = 3;
        triangle_pos[1] = 1;
        triangle_pos[2] = 0;

        triangle_pos[3] = 2;
        triangle_pos[4] = 1;
        triangle_pos[5] = 3;
    }
}

static void SetupQuadSphere(d3d_vertex_t *dst_data, const RECT *output,
                            const d3d_quad_t *quad, WORD *triangle_pos)
{
    const float scaleX = (float)(RECTWidth(*output))  / quad->i_width;
    const float scaleY = (float)(RECTHeight(*output)) / quad->i_height;
    for (unsigned lat = 0; lat <= nbLatBands; lat++) {
        float theta = lat * (float) M_PI / nbLatBands;
        float sinTheta, cosTheta;

        sincosf(theta, &sinTheta, &cosTheta);

        for (unsigned lon = 0; lon <= nbLonBands; lon++) {
            float phi = lon * 2 * (float) M_PI / nbLonBands;
            float sinPhi, cosPhi;

            sincosf(phi, &sinPhi, &cosPhi);

            float x = -sinPhi * sinTheta;
            float y = cosTheta;
            float z = cosPhi * sinTheta;

            unsigned off1 = lat * (nbLonBands + 1) + lon;
            dst_data[off1].position.x = SPHERE_RADIUS * x;
            dst_data[off1].position.y = SPHERE_RADIUS * y;
            dst_data[off1].position.z = SPHERE_RADIUS * z;

            dst_data[off1].texture.uv[0] = scaleX * lon / (float) nbLonBands; // 0(left) to 1(right)
            dst_data[off1].texture.uv[1] = scaleY * lat / (float) nbLatBands; // 0(top) to 1 (bottom)
        }
    }

    for (unsigned lat = 0; lat < nbLatBands; lat++) {
        for (unsigned lon = 0; lon < nbLonBands; lon++) {
            unsigned first = (lat * (nbLonBands + 1)) + lon;
            unsigned second = first + nbLonBands + 1;

            unsigned off = (lat * nbLatBands + lon) * 3 * 2;

            triangle_pos[off] = first;
            triangle_pos[off + 1] = first + 1;
            triangle_pos[off + 2] = second;

            triangle_pos[off + 3] = second;
            triangle_pos[off + 4] = first + 1;
            triangle_pos[off + 5] = second + 1;
        }
    }
}


static void SetupQuadCube(d3d_vertex_t *dst_data, const RECT *output,
                          const d3d_quad_t *quad, WORD *triangle_pos)
{
#define CUBEFACE(swap, value) \
    swap(value, -1.f,  1.f), \
    swap(value, -1.f, -1.f), \
    swap(value,  1.f,  1.f), \
    swap(value,  1.f, -1.f)

#define X_FACE(v, a, b) (v), (b), (a)
#define Y_FACE(v, a, b) (a), (v), (b)
#define Z_FACE(v, a, b) (a), (b), (v)

    static const float coord[] = {
        CUBEFACE(Z_FACE, -1.f), // FRONT
        CUBEFACE(Z_FACE, +1.f), // BACK
        CUBEFACE(X_FACE, -1.f), // LEFT
        CUBEFACE(X_FACE, +1.f), // RIGHT
        CUBEFACE(Y_FACE, -1.f), // BOTTOM
        CUBEFACE(Y_FACE, +1.f), // TOP
    };

#undef X_FACE
#undef Y_FACE
#undef Z_FACE
#undef CUBEFACE

    const float scaleX = (float)(output->right - output->left) / quad->i_width;
    const float scaleY = (float)(output->bottom - output->top) / quad->i_height;

    const float col[] = {0.f, scaleX / 3, scaleX * 2 / 3, scaleX};
    const float row[] = {0.f, scaleY / 2, scaleY};

    const float tex[] = {
        col[1], row[1], // front
        col[1], row[2],
        col[2], row[1],
        col[2], row[2],

        col[3], row[1], // back
        col[3], row[2],
        col[2], row[1],
        col[2], row[2],

        col[2], row[0], // left
        col[2], row[1],
        col[1], row[0],
        col[1], row[1],

        col[0], row[0], // right
        col[0], row[1],
        col[1], row[0],
        col[1], row[1],

        col[0], row[2], // bottom
        col[0], row[1],
        col[1], row[2],
        col[1], row[1],

        col[2], row[0], // top
        col[2], row[1],
        col[3], row[0],
        col[3], row[1],
    };

    const unsigned i_nbVertices = ARRAY_SIZE(coord) / 3;

    for (unsigned v = 0; v < i_nbVertices; ++v)
    {
        dst_data[v].position.x = coord[3 * v];
        dst_data[v].position.y = coord[3 * v + 1];
        dst_data[v].position.z = coord[3 * v + 2];

        dst_data[v].texture.uv[0] = tex[2 * v];
        dst_data[v].texture.uv[1] = tex[2 * v + 1];
    }

    const WORD ind[] = {
        2, 1, 0,       3, 1, 2, // front
        4, 7, 6,       5, 7, 4, // back
        8, 11, 10,     9, 11, 8, // left
        14, 13, 12,    15, 13, 14, // right
        16, 19, 18,    17, 19, 16, // bottom
        22, 21, 20,    23, 21, 22, // top
    };

    memcpy(triangle_pos, ind, sizeof(ind));
}

static void getZoomMatrix(float zoom, FLOAT matrix[static 16]) {

    const FLOAT m[] = {
        /* x   y     z     w */
        1.0f, 0.0f, 0.0f, 0.0f,
        0.0f, 1.0f, 0.0f, 0.0f,
        0.0f, 0.0f, 1.0f, 0.0f,
        0.0f, 0.0f, zoom, 1.0f
    };

    memcpy(matrix, m, sizeof(m));
}

/* perspective matrix see https://www.opengl.org/sdk/docs/man2/xhtml/gluPerspective.xml */
static void getProjectionMatrix(float sar, float fovy, FLOAT matrix[static 16]) {

    float zFar  = 1000;
    float zNear = 0.01;

    float f = 1.f / tanf(fovy / 2.f);

    const FLOAT m[] = {
        f / sar, 0.f,                   0.f,                0.f,
        0.f,     f,                     0.f,                0.f,
        0.f,     0.f,     (zNear + zFar) / (zNear - zFar), -1.f,
        0.f,     0.f, (2 * zNear * zFar) / (zNear - zFar),  0.f};

     memcpy(matrix, m, sizeof(m));
}

static float UpdateFOVy(float f_fovx, float f_sar)
{
    return 2 * atanf(tanf(f_fovx / 2) / f_sar);
}

static float UpdateZ(float f_fovx, float f_fovy)
{
    /* Do trigonometry to calculate the minimal z value
     * that will allow us to zoom out without seeing the outside of the
     * sphere (black borders). */
    float tan_fovx_2 = tanf(f_fovx / 2);
    float tan_fovy_2 = tanf(f_fovy / 2);
    float z_min = - SPHERE_RADIUS / sinf(atanf(sqrtf(
                    tan_fovx_2 * tan_fovx_2 + tan_fovy_2 * tan_fovy_2)));

    /* The FOV value above which z is dynamically calculated. */
    const float z_thresh = 90.f;

    float f_z;
    if (f_fovx <= z_thresh * M_PI / 180)
        f_z = 0;
    else
    {
        float f = z_min / ((FIELD_OF_VIEW_DEGREES_MAX - z_thresh) * M_PI / 180);
        f_z = f * f_fovx - f * z_thresh * M_PI / 180;
        if (f_z < z_min)
            f_z = z_min;
    }
    return f_z;
}

void D3D_UpdateViewpoint(d3d_quad_t *quad, const vlc_viewpoint_t *viewpoint, float f_sar)
{
    // Convert degree into radian
    float f_fovx = viewpoint->fov * (float)M_PI / 180.f;
    if ( f_fovx > FIELD_OF_VIEW_DEGREES_MAX * M_PI / 180 + 0.001f ||
         f_fovx < -0.001f )
        return;

    float f_fovy = UpdateFOVy(f_fovx, f_sar);
    float f_z = UpdateZ(f_fovx, f_fovy);

    getZoomMatrix(SPHERE_RADIUS * f_z, quad->vertexConstants->Zoom);
    getProjectionMatrix(f_sar, f_fovy, quad->vertexConstants->Projection);
    vlc_viewpoint_to_4x4(viewpoint, quad->vertexConstants->View);
}

bool D3D_QuadSetupBuffers(vlc_object_t *o, d3d_quad_t *quad, video_projection_mode_t projection)
{
    switch (projection)
    {
    case PROJECTION_MODE_RECTANGULAR:
        quad->vertexCount = 4;
        quad->indexCount = 2 * 3;
        break;
    case PROJECTION_MODE_EQUIRECTANGULAR:
        quad->vertexCount = (SPHERE_SLICES + 1) * (SPHERE_SLICES + 1);
        quad->indexCount = nbLatBands * nbLonBands * 2 * 3;
        break;
    case PROJECTION_MODE_CUBEMAP_LAYOUT_STANDARD:
        quad->vertexCount = 4 * 6;
        quad->indexCount = 6 * 2 * 3;
        break;
    default:
        msg_Warn(o, "Projection mode %d not handled", projection);
        return false;
    }

    quad->vertexStride = sizeof(d3d_vertex_t);
    quad->projection = projection;

    return true;
}

bool D3D_SetupQuadData(vlc_object_t *o, d3d_quad_t *quad, const RECT *output, d3d_vertex_t*dst_data,
                       void *pData, video_transform_t orientation)
{
    switch (quad->projection)
    {
    case PROJECTION_MODE_RECTANGULAR:
        SetupQuadFlat(dst_data, output, quad, pData, orientation);
        break;
    case PROJECTION_MODE_EQUIRECTANGULAR:
        SetupQuadSphere(dst_data, output, quad, pData);
        break;
    case PROJECTION_MODE_CUBEMAP_LAYOUT_STANDARD:
        SetupQuadCube(dst_data, output, quad, pData);
        break;
    default:
        msg_Warn(o, "Projection mode %d not handled", quad->projection);
        return false;
    }
    return true;
}
