/*
 * Copyright (c) 2022 Niklas Haas
 * This file is part of FFmpeg.
 *
 * FFmpeg 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.
 *
 * FFmpeg 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 FFmpeg; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include "libavutil/csp.h"

#include "fflcms2.h"

static void log_cb(cmsContext ctx, cmsUInt32Number error, const char *str)
{
    FFIccContext *s = cmsGetContextUserData(ctx);
    av_log(s->avctx, AV_LOG_ERROR, "lcms2: [%"PRIu32"] %s\n", error, str);
}

int ff_icc_context_init(FFIccContext *s, void *avctx)
{
    memset(s, 0, sizeof(*s));
    s->avctx = avctx;
    s->ctx = cmsCreateContext(NULL, s);
    if (!s->ctx)
        return AVERROR(ENOMEM);

    cmsSetLogErrorHandlerTHR(s->ctx, log_cb);
    return 0;
}

void ff_icc_context_uninit(FFIccContext *s)
{
    for (int i = 0; i < FF_ARRAY_ELEMS(s->curves); i++)
        cmsFreeToneCurve(s->curves[i]);
    cmsDeleteContext(s->ctx);
    memset(s, 0, sizeof(*s));
}

static int get_curve(FFIccContext *s, enum AVColorTransferCharacteristic trc,
                     cmsToneCurve **out_curve)
{
    if ((unsigned)trc < AVCOL_TRC_NB && s->curves[trc])
        goto done;

    switch (trc) {
    case AVCOL_TRC_LINEAR:
        s->curves[trc] = cmsBuildGamma(s->ctx, 1.0);
        break;
    case AVCOL_TRC_GAMMA22:
        s->curves[trc] = cmsBuildGamma(s->ctx, 2.2);
        break;
    case AVCOL_TRC_GAMMA28:
        s->curves[trc] = cmsBuildGamma(s->ctx, 2.8);
        break;
    case AVCOL_TRC_BT709:
    case AVCOL_TRC_SMPTE170M:
    case AVCOL_TRC_BT2020_10:
    case AVCOL_TRC_BT2020_12:
        s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 4, (double[5]) {
            /* γ = */ 1/0.45,
            /* a = */ 1/1.099296826809442,
            /* b = */ 1 - 1/1.099296826809442,
            /* c = */ 1/4.5,
            /* d = */ 4.5 * 0.018053968510807,
        });
        break;
    case AVCOL_TRC_SMPTE240M:
        s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 4, (double[5]) {
            /* γ = */ 1/0.45,
            /* a = */ 1/1.1115,
            /* b = */ 1 - 1/1.1115,
            /* c = */ 1/4.0,
            /* d = */ 4.0 * 0.0228,
        });
        break;
    case AVCOL_TRC_LOG:
        s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 8, (double[5]) {
            /* a = */ 1.0,
            /* b = */ 10.0,
            /* c = */ 2.0,
            /* d = */ -1.0,
            /* e = */ 0.0
        });
        break;
    case AVCOL_TRC_LOG_SQRT:
        s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 8, (double[5]) {
            /* a = */ 1.0,
            /* b = */ 10.0,
            /* c = */ 2.5,
            /* d = */ -1.0,
            /* e = */ 0.0
        });
        break;
    case AVCOL_TRC_IEC61966_2_1:
        s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 4, (double[5]) {
            /* γ = */ 2.4,
            /* a = */ 1/1.055,
            /* b = */ 1 - 1/1.055,
            /* c = */ 1/12.92,
            /* d = */ 12.92 * 0.0031308,
        });
        break;
    case AVCOL_TRC_SMPTE428:
        s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 2, (double[3]) {
            /* γ = */ 2.6,
            /* a = */ pow(52.37/48.0, 1/2.6),
            /* b = */ 0.0
        });
        break;

    /* Can't be represented using the existing parametric tone curves.
     * FIXME: use cmsBuildTabulatedToneCurveFloat instead */
    case AVCOL_TRC_IEC61966_2_4:
    case AVCOL_TRC_BT1361_ECG:
    case AVCOL_TRC_SMPTE2084:
    case AVCOL_TRC_ARIB_STD_B67:
    case AVCOL_TRC_V_LOG:
        return AVERROR_PATCHWELCOME;

    default:
        return AVERROR_INVALIDDATA;
    }

    if (!s->curves[trc])
        return AVERROR(ENOMEM);

done:
    *out_curve = s->curves[trc];
    return 0;
}

int ff_icc_profile_generate(FFIccContext *s,
                            enum AVColorPrimaries color_prim,
                            enum AVColorTransferCharacteristic color_trc,
                            cmsHPROFILE *out_profile)
{
    cmsToneCurve *tonecurve;
    const AVColorPrimariesDesc *prim;
    int ret;

    if (!(prim = av_csp_primaries_desc_from_id(color_prim)))
        return AVERROR_INVALIDDATA;
    if ((ret = get_curve(s, color_trc, &tonecurve)) < 0)
        return ret;

    *out_profile = cmsCreateRGBProfileTHR(s->ctx,
        &(cmsCIExyY) { av_q2d(prim->wp.x), av_q2d(prim->wp.y), 1.0 },
        &(cmsCIExyYTRIPLE) {
            .Red    = { av_q2d(prim->prim.r.x), av_q2d(prim->prim.r.y), 1.0 },
            .Green  = { av_q2d(prim->prim.g.x), av_q2d(prim->prim.g.y), 1.0 },
            .Blue   = { av_q2d(prim->prim.b.x), av_q2d(prim->prim.b.y), 1.0 },
        },
        (cmsToneCurve *[3]) { tonecurve, tonecurve, tonecurve }
    );

    return *out_profile == NULL ? AVERROR(ENOMEM) : 0;
}

int ff_icc_profile_attach(FFIccContext *s, cmsHPROFILE profile, AVFrame *frame)
{
    cmsUInt32Number size;
    AVBufferRef *buf;

    if (!cmsSaveProfileToMem(profile, NULL, &size))
        return AVERROR_EXTERNAL;

    buf = av_buffer_alloc(size);
    if (!buf)
        return AVERROR(ENOMEM);

    if (!cmsSaveProfileToMem(profile, buf->data, &size) || size != buf->size) {
        av_buffer_unref(&buf);
        return AVERROR_EXTERNAL;
    }

    if (!av_frame_new_side_data_from_buf(frame, AV_FRAME_DATA_ICC_PROFILE, buf)) {
        av_buffer_unref(&buf);
        return AVERROR(ENOMEM);
    }

    return 0;
}

static av_always_inline void XYZ_xy(cmsCIEXYZ XYZ, AVCIExy *xy)
{
    double k = 1.0 / (XYZ.X + XYZ.Y + XYZ.Z);
    xy->x = av_d2q(k * XYZ.X, 100000);
    xy->y = av_d2q(k * XYZ.Y, 100000);
}

static av_always_inline AVRational abs_sub_q(AVRational r1, AVRational r2)
{
    AVRational diff = av_sub_q(r1, r2);
    /* denominator assumed to be positive */
    return av_make_q(abs(diff.num), diff.den);
}

static const AVCIExy wp_d50 = { {3457, 10000}, {3585, 10000} }; /* CIE D50 */

int ff_icc_profile_sanitize(FFIccContext *s, cmsHPROFILE profile)
{
    cmsCIEXYZ *white, fixed;
    AVCIExy wpxy;
    AVRational diff, z;
    if (!profile)
        return 0;

    if (cmsGetEncodedICCversion(profile) >= 0x4000000) { // ICC v4
        switch (cmsGetHeaderRenderingIntent(profile)) {
        case INTENT_RELATIVE_COLORIMETRIC:
        case INTENT_ABSOLUTE_COLORIMETRIC: ;
            /* ICC v4 colorimetric profiles are specified to always use D50
             * media white point, anything else is a violation of the spec.
             * Sadly, such profiles are incredibly common (Apple...), so make
             * an effort to fix them. */
            if (!(white = cmsReadTag(profile, cmsSigMediaWhitePointTag)))
                return AVERROR_INVALIDDATA;
            XYZ_xy(*white, &wpxy);
            diff = av_add_q(abs_sub_q(wpxy.x, wp_d50.x), abs_sub_q(wpxy.y, wp_d50.y));
            if (av_cmp_q(diff, av_make_q(1, 1000)) > 0) {
                av_log(s->avctx, AV_LOG_WARNING, "Invalid colorimetric ICCv4 "
                       "profile media white point tag (expected %.4f %.4f, "
                       "got %.4f %.4f)\n",
                       av_q2d(wp_d50.x), av_q2d(wp_d50.y),
                       av_q2d(wpxy.x), av_q2d(wpxy.y));
                /* x+y+z = 1 */
                z = av_sub_q(av_sub_q(av_make_q(1, 1), wp_d50.x), wp_d50.y);
                fixed.X = av_q2d(av_div_q(wp_d50.x, wp_d50.y)) * white->Y;
                fixed.Y = white->Y;
                fixed.Z = av_q2d(av_div_q(z, wp_d50.y)) * white->Y;
                if (!cmsWriteTag(profile, cmsSigMediaWhitePointTag, &fixed))
                    return AVERROR_EXTERNAL;
            }
            break;
        default: break;
        }
    }

    return 0;
}

int ff_icc_profile_read_primaries(FFIccContext *s, cmsHPROFILE profile,
                                  AVColorPrimariesDesc *out_primaries)
{
    static const uint8_t testprimaries[4][3] = {
        { 0xFF,    0,    0 }, /* red */
        {    0, 0xFF,    0 }, /* green */
        {    0,    0, 0xFF }, /* blue */
        { 0xFF, 0xFF, 0xFF }, /* white */
    };

    AVWhitepointCoefficients *wp = &out_primaries->wp;
    AVPrimaryCoefficients *prim = &out_primaries->prim;
    cmsFloat64Number prev_adapt;
    cmsHPROFILE xyz;
    cmsHTRANSFORM tf;
    cmsCIEXYZ dst[4];

    xyz = cmsCreateXYZProfileTHR(s->ctx);
    if (!xyz)
        return AVERROR(ENOMEM);

    /* We need to use an unadapted observer to get the raw values */
    prev_adapt = cmsSetAdaptationStateTHR(s->ctx, 0.0);
    tf = cmsCreateTransformTHR(s->ctx, profile, TYPE_RGB_8, xyz, TYPE_XYZ_DBL,
                               INTENT_ABSOLUTE_COLORIMETRIC,
                               /* Note: These flags mostly don't do anything
                                * anyway, but specify them regardless */
                               cmsFLAGS_NOCACHE |
                               cmsFLAGS_NOOPTIMIZE |
                               cmsFLAGS_LOWRESPRECALC |
                               cmsFLAGS_GRIDPOINTS(2));
    cmsSetAdaptationStateTHR(s->ctx, prev_adapt);
    cmsCloseProfile(xyz);
    if (!tf) {
        av_log(s->avctx, AV_LOG_ERROR, "Invalid ICC profile (e.g. CMYK)\n");
        return AVERROR_INVALIDDATA;
    }

    cmsDoTransform(tf, testprimaries, dst, 4);
    cmsDeleteTransform(tf);
    XYZ_xy(dst[0], &prim->r);
    XYZ_xy(dst[1], &prim->g);
    XYZ_xy(dst[2], &prim->b);
    XYZ_xy(dst[3], wp);
    return 0;
}

int ff_icc_profile_detect_transfer(FFIccContext *s, cmsHPROFILE profile,
                                   enum AVColorTransferCharacteristic *out_trc)
{
    /* 8-bit linear grayscale ramp */
    static const uint8_t testramp[16][3] = {
        {  1,   1,   1}, /* avoid exact zero due to log100 etc. */
        { 17,  17,  17},
        { 34,  34,  34},
        { 51,  51,  51},
        { 68,  68,  68},
        { 85,  85,  85},
        { 02,  02,  02},
        {119, 119, 119},
        {136, 136, 136},
        {153, 153, 153},
        {170, 170, 170},
        {187, 187, 187},
        {204, 204, 204},
        {221, 221, 221},
        {238, 238, 238},
        {255, 255, 255},
    };

    double dst[FF_ARRAY_ELEMS(testramp)];

    for (enum AVColorTransferCharacteristic trc = 0; trc < AVCOL_TRC_NB; trc++) {
        cmsToneCurve *tonecurve;
        cmsHPROFILE ref;
        cmsHTRANSFORM tf;
        double delta = 0.0;
        if (get_curve(s, trc, &tonecurve) < 0)
            continue;

        ref = cmsCreateGrayProfileTHR(s->ctx, cmsD50_xyY(), tonecurve);
        if (!ref)
            return AVERROR(ENOMEM);

        tf = cmsCreateTransformTHR(s->ctx, profile, TYPE_RGB_8, ref, TYPE_GRAY_DBL,
                                   INTENT_RELATIVE_COLORIMETRIC,
                                   cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE);
        cmsCloseProfile(ref);
        if (!tf) {
            av_log(s->avctx, AV_LOG_ERROR, "Invalid ICC profile (e.g. CMYK)\n");
            return AVERROR_INVALIDDATA;
        }

        cmsDoTransform(tf, testramp, dst, FF_ARRAY_ELEMS(dst));
        cmsDeleteTransform(tf);

        for (int i = 0; i < FF_ARRAY_ELEMS(dst); i++)
            delta += fabs(testramp[i][0] / 255.0 - dst[i]);
        if (delta < 0.01) {
            *out_trc = trc;
            return 0;
        }
    }

    *out_trc = AVCOL_TRC_UNSPECIFIED;
    return 0;
}
