/*
*  This file is part of RawTherapee.
*
*  Copyright (c) 2012 Oliver Duis <www.oliverduis.de>
*
*  RawTherapee is free software: you can redistribute it and/or modify
*  it under the terms of the GNU General Public License as published by
*  the Free Software Foundation, either version 3 of the License, or
*  (at your option) any later version.
* 
*  RawTherapee 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 General Public License for more details.
*
*  You should have received a copy of the GNU General Public License
*  along with RawTherapee.  If not, see <http://www.gnu.org/licenses/>.
*/
#include <cstring>

#include "dcp.h"
#include "safegtk.h"
#include "iccmatrices.h"
#include "iccstore.h"
#include "rawimagesource.h"
#include "improcfun.h"
#include "rt_math.h"

using namespace std;
using namespace rtengine;
using namespace rtexif;

static void Invert3x3(const double (*A)[3], double (*B)[3]) {

    double a00 = A[0][0];
    double a01 = A[0][1];
    double a02 = A[0][2];
    double a10 = A[1][0];
    double a11 = A[1][1];
    double a12 = A[1][2];
    double a20 = A[2][0];
    double a21 = A[2][1];
    double a22 = A[2][2];
    double temp [3][3];

    temp[0][0] = a11 * a22 - a21 * a12;
    temp[0][1] = a21 * a02 - a01 * a22;
    temp[0][2] = a01 * a12 - a11 * a02;
    temp[1][0] = a20 * a12 - a10 * a22;
    temp[1][1] = a00 * a22 - a20 * a02;
    temp[1][2] = a10 * a02 - a00 * a12;
    temp[2][0] = a10 * a21 - a20 * a11;
    temp[2][1] = a20 * a01 - a00 * a21;
    temp[2][2] = a00 * a11 - a10 * a01;

    double det = a00 * temp[0][0] + a01 * temp[1][0] + a02 * temp[2][0];

    if (fabs(det) < 1.0E-10) {
        abort(); // can't be inverted, we shouldn't be dealing with such matrices
    }

    for (int j = 0; j < 3; j++)	{
        for (int k = 0; k < 3; k++) {
            B[j][k] = temp[j][k] / det;
        }
    }
}

static void Multiply3x3(const double (*A)[3], const double (*B)[3], double (*C)[3]) {

    // use temp to support having output same as input
    double M[3][3];
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            M[i][j] = 0;
            for (int k = 0; k < 3; k++) {
                M[i][j] += A[i][k] * B[k][j];
            }
        }
    }
    memcpy(C, M, 3 * 3 * sizeof(double));
}

static void Multiply3x3_v3(const double (*A)[3], const double B[3], double C[3]) {

    // use temp to support having output same as input
    double M[3] = { 0, 0, 0 };
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            M[i] += A[i][j] * B[j];
        }
    }
    memcpy(C, M, 3 * sizeof(double));
}

static void Mix3x3(const double (*A)[3], double mulA, const double (*B)[3], double mulB, double (*C)[3]) {

    double M[3][3];
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            M[i][j] = A[i][j] * mulA + B[i][j] * mulB;
        }
    }
    memcpy(C, M, 3 * 3 * sizeof(double));
}

static void MapWhiteMatrix(const double white1[3], const double white2[3], double (*B)[3]) {

    // code adapted from dng_color_spec::MapWhiteMatrix

    // Use the linearized Bradford adaptation matrix.
    double Mb[3][3] = { { 0.8951,  0.2664, -0.1614 }, { -0.7502,  1.7135,  0.0367 }, { 0.0389, -0.0685,  1.0296 }};

    double w1[3];
    Multiply3x3_v3(Mb, white1, w1);
    double w2[3];
    Multiply3x3_v3(Mb, white2, w2);

    // Negative white coordinates are kind of meaningless.
    w1[0] = std::max(w1[0], 0.0);
    w1[1] = std::max(w1[1], 0.0);
    w1[2] = std::max(w1[2], 0.0);
    w2[0] = std::max(w2[0], 0.0);
    w2[1] = std::max(w2[1], 0.0);
    w2[2] = std::max(w2[2], 0.0);

    // Limit scaling to something reasonable.
    double A[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
    A[0][0] = std::max(0.1, std::min(w1[0] > 0.0 ? w2[0] / w1[0] : 10.0, 10.0));
    A[1][1] = std::max(0.1, std::min(w1[1] > 0.0 ? w2[1] / w1[1] : 10.0, 10.0));
    A[2][2] = std::max(0.1, std::min(w1[2] > 0.0 ? w2[2] / w1[2] : 10.0, 10.0));

    double temp[3][3];
    Invert3x3(Mb, temp);
    Multiply3x3(temp, A, temp);
    Multiply3x3(temp, Mb, B);
}

enum dngCalibrationIlluminant {
    lsUnknown = 0,
    lsDaylight = 1,
    lsFluorescent = 2,
    lsTungsten = 3,
    lsFlash = 4,
    lsFineWeather = 9,
    lsCloudyWeather = 10,
    lsShade = 11,
    lsDaylightFluorescent = 12,	// D  5700 - 7100K
    lsDayWhiteFluorescent = 13,	// N  4600 - 5500K
    lsCoolWhiteFluorescent = 14, // W  3800 - 4500K
    lsWhiteFluorescent = 15, // WW 3250 - 3800K
    lsWarmWhiteFluorescent = 16, // L  2600 - 3250K
    lsStandardLightA = 17,
    lsStandardLightB = 18,
    lsStandardLightC = 19,
    lsD55 = 20,
    lsD65 = 21,
    lsD75 = 22,
    lsD50 = 23,
    lsISOStudioTungsten = 24,
    lsOther = 255
};

// should probably be moved to colortemp.cc
static double calibrationIlluminantToTemperature(int light) {

    // these temperatures are those found in DNG SDK reference code.
    switch (light) {
    case lsStandardLightA:
    case lsTungsten:
        return 2850.0;
    case lsISOStudioTungsten:
        return 3200.0;
    case lsD50:
        return 5000.0;
    case lsD55:
    case lsDaylight:
    case lsFineWeather:
    case lsFlash:
    case lsStandardLightB:
        return 5500.0;
    case lsD65:
    case lsStandardLightC:
    case lsCloudyWeather:
        return 6500.0;
    case lsD75:
    case lsShade:
        return 7500.0;
    case lsDaylightFluorescent:
        return (5700.0 + 7100.0) * 0.5;
    case lsDayWhiteFluorescent:
        return (4600.0 + 5500.0) * 0.5;
    case lsCoolWhiteFluorescent:
    case lsFluorescent:
        return (3800.0 + 4500.0) * 0.5;
    case lsWhiteFluorescent:
        return (3250.0 + 3800.0) * 0.5;
    case lsWarmWhiteFluorescent:
        return (2600.0 + 3250.0) * 0.5;
    default:
        return 0.0;
    }
}

void DCPProfile::MakeXYZCAM(ColorTemp &wb, int preferredIlluminant, double (*mXYZCAM)[3]) const
{
    // code adapted from dng_color_spec::FindXYZtoCamera
    // note that we do not support monochrome or colorplanes > 3 (no reductionMatrix support)
    // we do not support cameracalibration either

    bool hasFwd1 = hasForwardMatrix1;
    bool hasFwd2 = hasForwardMatrix2;
    bool hasCol1 = hasColorMatrix1;
    bool hasCol2 = hasColorMatrix2;
    if (preferredIlluminant == 1) {
        if (hasFwd1) hasFwd2 = false;
        if (hasCol1) hasCol2 = false;
    } else if (preferredIlluminant == 2) {
        if (hasFwd2) hasFwd1 = false;
        if (hasCol2) hasCol1 = false;
    }

    // mix if we have two matrices
    double mix;
    if (wb.getTemp() <= temperature1) {
        mix = 1.0;
    } else if (wb.getTemp() >= temperature2) {
        mix = 0.0;
    } else {
        double invT = 1.0 / wb.getTemp();
        mix = (invT - (1.0 / temperature2)) / ((1.0 / temperature1) - (1.0 / temperature2));
    }

    if (hasFwd1 || hasFwd2) {
        // always prefer ForwardMatrix ahead of ColorMatrix
        double mFwd[3][3];
        if (hasFwd1 && hasFwd2) {
            // interpolate
            if (mix >= 1.0) {
                memcpy(mFwd, mForwardMatrix1, sizeof(mFwd));
            } else if (mix <= 0.0) {
                memcpy(mFwd, mForwardMatrix2, sizeof(mFwd));
            } else {
                Mix3x3(mForwardMatrix1, mix, mForwardMatrix2, 1.0 - mix, mFwd);
            }
        } else if (hasFwd1) {
            memcpy(mFwd, mForwardMatrix1, sizeof(mFwd));
        } else {
            memcpy(mFwd, mForwardMatrix2, sizeof(mFwd));
        }
        ConvertDNGForwardMatrix2XYZCAM(mFwd,mXYZCAM,wb);
    } else {
        // Colormatrix
        double mCol[3][3];
        if (hasCol1 && hasCol2) {
            // interpolate
            if (mix >= 1.0) {
                memcpy(mCol, mColorMatrix1, sizeof(mCol));
            } else if (mix <= 0.0) {
                memcpy(mCol, mColorMatrix2, sizeof(mCol));
            } else {
                Mix3x3(mColorMatrix1, mix, mColorMatrix2, 1.0 - mix, mCol);
            }
        } else if (hasCol1) {
            memcpy(mCol, mColorMatrix1, sizeof(mCol));
        } else {
            memcpy(mCol, mColorMatrix2, sizeof(mCol));
        }
        ConvertDNGMatrix2XYZCAM(mCol,mXYZCAM);
    }
}

const DCPProfile::HSBModify* DCPProfile::MakeHueSatMap(ColorTemp &wb, int preferredIlluminant, HSBModify **deleteHandle) const {

    *deleteHandle = NULL;
    if (!aDeltas1) {
        return NULL;
    }
    if (!aDeltas2) {
        return aDeltas1;
    }

    if (preferredIlluminant == 1) {
        return aDeltas1;
    } else if (preferredIlluminant == 2) {
        return aDeltas2;
    }

    // Interpolate based on color temperature.
    if (temperature1 <= 0.0 || temperature2 <= 0.0 || temperature1 == temperature2) {
        return aDeltas1;
    }
    bool reverseOrder = temperature1 > temperature2;
    double t1, t2;
    if (reverseOrder) {
        t1 = temperature2;
        t2 = temperature1;
    } else {
        t1 = temperature1;
        t2 = temperature2;
    }

    double mix;
    if (wb.getTemp() <= t1) {
        mix = 1.0;
    } else if (wb.getTemp() >= t2) {
        mix = 0.0;
    } else {
        double invT = 1.0 / wb.getTemp();
        mix = (invT - (1.0 / t2)) / ((1.0 / t1) - (1.0 / t2));
    }

    if (reverseOrder) {
        mix = 1.0 - mix;
    }

    if (mix >= 1.0) {
        return aDeltas1;
    } else if (mix <= 0.0) {
        return aDeltas2;
    }

    // Interpolate between the tables.
    HSBModify *aDeltas = new HSBModify[DeltaInfo.iArrayCount];
    *deleteHandle = aDeltas;
    float w1 = (float)mix;
    float w2 = 1.0f - (float)mix;
    for (int i = 0; i < DeltaInfo.iArrayCount; i++) {
        aDeltas[i].fHueShift = w1 * aDeltas1[i].fHueShift + w2 * aDeltas2[i].fHueShift;
        aDeltas[i].fSatScale = w1 * aDeltas1[i].fSatScale + w2 * aDeltas2[i].fSatScale;
        aDeltas[i].fValScale = w1 * aDeltas1[i].fValScale + w2 * aDeltas2[i].fValScale;
    }
    return aDeltas;
}

DCPProfile::DCPProfile(Glib::ustring fname, bool isRTProfile) {
    const int TIFFFloatSize=4;
    const int TagColorMatrix1=50721, TagColorMatrix2=50722, TagProfileHueSatMapDims=50937;
    const int TagForwardMatrix1=50964, TagForwardMatrix2=50965;
    const int TagProfileHueSatMapData1=50938, TagProfileHueSatMapData2=50939;
    const int TagCalibrationIlluminant1=50778, TagCalibrationIlluminant2=50779;
    const int TagProfileLookTableData=50982, TagProfileLookTableDims=50981;  // ProfileLookup is the low quality variant
    const int TagProfileToneCurve=50940;

    aDeltas1=aDeltas2=aLookTable=NULL;

    FILE *pFile = safe_g_fopen(fname, "rb");

    TagDirectory *tagDir=ExifManager::parseTIFF(pFile, false);

    Tag* tag = tagDir->getTag(TagCalibrationIlluminant1); iLightSource1 = (tag!=NULL ? tag->toInt(0,rtexif::SHORT) : -1);
    tag = tagDir->getTag(TagCalibrationIlluminant2); iLightSource2 = (tag!=NULL ? tag->toInt(0,rtexif::SHORT) : -1);
    temperature1 = calibrationIlluminantToTemperature(iLightSource1);
    temperature2 = calibrationIlluminantToTemperature(iLightSource2);

    bool hasSecondHueSat = tagDir->getTag(TagProfileHueSatMapData2)!=NULL;  // some profiles have two matrices, but just one huesat

    // Fetch Forward Matrices, if any
    hasForwardMatrix1 = false;
    hasForwardMatrix2 = false;
    hasColorMatrix1 = false;
    hasColorMatrix2 = false;
    hasToneCurve = false;
    tag = tagDir->getTag(TagForwardMatrix1);
    if (tag) {
        hasForwardMatrix1 = true;
        for (int row=0;row<3;row++) { 
            for (int col=0;col<3;col++) {
                mForwardMatrix1[col][row]=(float)tag->toDouble((col+row*3)*8);
            }
        }
    }
    tag = tagDir->getTag(TagForwardMatrix2);
    if (tag) {
        hasForwardMatrix2 = true;
        for (int row=0;row<3;row++) { 
            for (int col=0;col<3;col++) {
                mForwardMatrix2[col][row]=(float)tag->toDouble((col+row*3)*8);
            }
        }
    }

    // Color Matrix (1 is always there)
    tag = tagDir->getTag(TagColorMatrix1);
    if (!tag) {
        // FIXME: better error handling
        fprintf(stderr, "Bad DCP, no ColorMatrix1\n");
        abort();
    }
    hasColorMatrix1 = true;

    for (int row=0;row<3;row++) { 
        for (int col=0;col<3;col++) {
            mColorMatrix1[col][row]=(float)tag->toDouble((col+row*3)*8);
        }
    }

    tag=tagDir->getTag(TagProfileLookTableDims);
    if (tag!=NULL) {
        LookInfo.iHueDivisions=tag->toInt(0); LookInfo.iSatDivisions=tag->toInt(4); LookInfo.iValDivisions=tag->toInt(8);

        tag = tagDir->getTag(TagProfileLookTableData);
        LookInfo.iArrayCount = tag->getCount()/3;

        aLookTable =new HSBModify[LookInfo.iArrayCount];

        for (int i=0;i<LookInfo.iArrayCount;i++) {
            aLookTable[i].fHueShift=tag->toDouble((i*3)*TIFFFloatSize);
            aLookTable[i].fSatScale=tag->toDouble((i*3+1)*TIFFFloatSize);
            aLookTable[i].fValScale=tag->toDouble((i*3+2)*TIFFFloatSize);
        }

        // precalculated constants for table application
        LookInfo.pc.hScale = (LookInfo.iHueDivisions < 2) ? 0.0f : (LookInfo.iHueDivisions * (1.0f / 6.0f));
        LookInfo.pc.sScale = (float) (LookInfo.iSatDivisions - 1);
        LookInfo.pc.vScale = (float) (LookInfo.iValDivisions - 1);
        LookInfo.pc.maxHueIndex0 = LookInfo.iHueDivisions - 1;
        LookInfo.pc.maxSatIndex0 = LookInfo.iSatDivisions - 2;
        LookInfo.pc.maxValIndex0 = LookInfo.iValDivisions - 2;
        LookInfo.pc.hueStep = LookInfo.iSatDivisions;
        LookInfo.pc.valStep = LookInfo.iHueDivisions * LookInfo.pc.hueStep;
    }

    tag = tagDir->getTag(TagProfileHueSatMapDims);
    if (tag!=NULL) {
        DeltaInfo.iHueDivisions=tag->toInt(0); DeltaInfo.iSatDivisions=tag->toInt(4); DeltaInfo.iValDivisions=tag->toInt(8);

        tag = tagDir->getTag(TagProfileHueSatMapData1);
        DeltaInfo.iArrayCount = tag->getCount()/3;

        aDeltas1=new HSBModify[DeltaInfo.iArrayCount];

        for (int i=0;i<DeltaInfo.iArrayCount;i++) {
            aDeltas1[i].fHueShift=tag->toDouble((i*3)*TIFFFloatSize);
            aDeltas1[i].fSatScale=tag->toDouble((i*3+1)*TIFFFloatSize);
            aDeltas1[i].fValScale=tag->toDouble((i*3+2)*TIFFFloatSize);
        }

        DeltaInfo.pc.hScale = (DeltaInfo.iHueDivisions < 2) ? 0.0f : (DeltaInfo.iHueDivisions * (1.0f / 6.0f));
        DeltaInfo.pc.sScale = (float) (DeltaInfo.iSatDivisions - 1);
        DeltaInfo.pc.vScale = (float) (DeltaInfo.iValDivisions - 1);
        DeltaInfo.pc.maxHueIndex0 = DeltaInfo.iHueDivisions - 1;
        DeltaInfo.pc.maxSatIndex0 = DeltaInfo.iSatDivisions - 2;
        DeltaInfo.pc.maxValIndex0 = DeltaInfo.iValDivisions - 2;
        DeltaInfo.pc.hueStep = DeltaInfo.iSatDivisions;
        DeltaInfo.pc.valStep = DeltaInfo.iHueDivisions * DeltaInfo.pc.hueStep;
    }

    if (iLightSource2!=-1) {
        // Second matrix
        tag = tagDir->getTag(TagColorMatrix2);
        hasColorMatrix2 = true;

        for (int row=0;row<3;row++) { 
            for (int col=0;col<3;col++) {
                mColorMatrix2[col][row]= (tag!=NULL ? (float)tag->toDouble((col+row*3)*8) : mColorMatrix1[col][row]);
            }
        }

        // Second huesatmap
        if (hasSecondHueSat) {
            aDeltas2=new HSBModify[DeltaInfo.iArrayCount];

            // Saturation maps. Need to be unwinded.
            tag = tagDir->getTag(TagProfileHueSatMapData2);

            for (int i=0;i<DeltaInfo.iArrayCount;i++) {
                aDeltas2[i].fHueShift=tag->toDouble((i*3)*TIFFFloatSize);
                aDeltas2[i].fSatScale=tag->toDouble((i*3+1)*TIFFFloatSize);
                aDeltas2[i].fValScale=tag->toDouble((i*3+2)*TIFFFloatSize);
            }
        }
    }

    // Read tone curve points, if any, but disable to RTs own profiles
    // the DCP tone curve is subjective and of low quality in comparison to RTs tone curves
    tag = tagDir->getTag(TagProfileToneCurve);
    if (tag!=NULL && !isRTProfile) {
        std::vector<double> cPoints;
        cPoints.push_back(double(DCT_Spline));  // The first value is the curve type

        // push back each X/Y coordinates in a loop
        bool curve_is_linear = true;
        for (int i=0;i<tag->getCount(); i+= 2) {
            double x = tag->toDouble((i+0)*TIFFFloatSize);
            double y = tag->toDouble((i+1)*TIFFFloatSize);
            if (x != y) {
                curve_is_linear = false;
            }
            cPoints.push_back( x );
            cPoints.push_back( y );
        }

        if (!curve_is_linear) {
            // Create the curve
            DiagonalCurve rawCurve(cPoints, CURVES_MIN_POLY_POINTS);

            toneCurve.Set((Curve*)&rawCurve);
            hasToneCurve = true;
        }
    }

    willInterpolate = false;
    if (hasForwardMatrix1) {
        if (hasForwardMatrix2) {
            if (memcmp(mForwardMatrix1, mForwardMatrix2, sizeof(mForwardMatrix1)) != 0) {
                // common that forward matrices are the same!
                willInterpolate = true;
            }
            if (aDeltas1 && aDeltas2) {
                // we assume tables are different
                willInterpolate = true;
            }
        }
    } else if (hasColorMatrix1 && hasColorMatrix2) {
        if (memcmp(mColorMatrix1, mColorMatrix2, sizeof(mColorMatrix1)) != 0) {
            willInterpolate = true;
        }
        if (aDeltas1 && aDeltas2) {
            willInterpolate = true;
        }
    }

    if (pFile!=NULL) fclose(pFile);
    delete tagDir;
}

DCPProfile::~DCPProfile() {
    delete[] aDeltas1; delete[] aDeltas2;
}

// Convert DNG color matrix to xyz_cam compatible matrix
void DCPProfile::ConvertDNGMatrix2XYZCAM(const double (*mColorMatrix)[3], double (*mXYZCAM)[3]) const {
    int i,j,k;

    double cam_xyz[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
    for (i=0; i<3; i++)
        for (j=0; j<3; j++)
            for (k=0; k<3; k++)
                cam_xyz[i][j] += mColorMatrix[j][k] * (i==k);


    // Multiply out XYZ colorspace 
    double cam_rgb[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
    for (i=0; i < 3; i++)	
        for (j=0; j < 3; j++)
            for (k=0; k < 3; k++) 
                cam_rgb[i][j] += cam_xyz[i][k] * xyz_sRGB[k][j];

    // Normalize cam_rgb so that:  cam_rgb * (1,1,1) is (1,1,1,1)
    double num;
    for (i=0; i<3; i++) {		
        for (num=j=0; j<3; j++) num += cam_rgb[i][j];
        for (j=0; j<3; j++) cam_rgb[i][j] /= num;
    }

    double rgb_cam[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
    RawImageSource::inverse33 (cam_rgb, rgb_cam);

    for (i=0; i<3; i++)
        for (j=0; j<3; j++) mXYZCAM[i][j]=0;

    for (i=0; i<3; i++)
        for (j=0; j<3; j++)
            for (k=0; k<3; k++)
                mXYZCAM[i][j] += xyz_sRGB[i][k] * rgb_cam[k][j];
}

void DCPProfile::HSDApply(const HSDTableInfo &ti, const HSBModify *tableBase, const float hs, const float ss, const float vs, float &h, float &s, float &v) const {

    // Apply the HueSatMap. Ported from Adobes reference implementation
    float hueShift, satScale, valScale;

    if (ti.iValDivisions < 2)  // Optimize most common case of "2.5D" table.
    {
        float hScaled = hs * ti.pc.hScale;
        float sScaled = ss * ti.pc.sScale;

        int hIndex0 = max((int)hScaled, 0);
        int sIndex0 = max(min((int)sScaled,ti.pc.maxSatIndex0),0);

        int hIndex1 = hIndex0 + 1;

        if (hIndex0 >= ti.pc.maxHueIndex0)
        {
            hIndex0 = ti.pc.maxHueIndex0;
            hIndex1 = 0;
        }

        float hFract1 = hScaled - (float) hIndex0;
        float sFract1 = sScaled - (float) sIndex0;

        float hFract0 = 1.0f - hFract1;
        float sFract0 = 1.0f - sFract1;

        const HSBModify *entry00 = tableBase + hIndex0 * ti.pc.hueStep + sIndex0;
        const HSBModify *entry01 = entry00 + (hIndex1 - hIndex0) * ti.pc.hueStep;

        float hueShift0 = hFract0 * entry00->fHueShift + hFract1 * entry01->fHueShift;
        float satScale0 = hFract0 * entry00->fSatScale + hFract1 * entry01->fSatScale;
        float valScale0 = hFract0 * entry00->fValScale + hFract1 * entry01->fValScale;

        entry00++;
        entry01++;

        float hueShift1 = hFract0 * entry00->fHueShift +
            hFract1 * entry01->fHueShift;

        float satScale1 = hFract0 * entry00->fSatScale +
            hFract1 * entry01->fSatScale;

        float valScale1 = hFract0 * entry00->fValScale +
            hFract1 * entry01->fValScale;

        hueShift = sFract0 * hueShift0 + sFract1 * hueShift1;
        satScale = sFract0 * satScale0 + sFract1 * satScale1;
        valScale = sFract0 * valScale0 + sFract1 * valScale1;

    } else {

        float hScaled = hs * ti.pc.hScale;
        float sScaled = ss * ti.pc.sScale;
        float vScaled = vs * ti.pc.vScale;

        int hIndex0 = (int) hScaled;
        int sIndex0 = max(min((int)sScaled,ti.pc.maxSatIndex0),0);
        int vIndex0 = max(min((int)vScaled,ti.pc.maxValIndex0),0);

        int hIndex1 = hIndex0 + 1;

        if (hIndex0 >= ti.pc.maxHueIndex0)
        {
            hIndex0 = ti.pc.maxHueIndex0;
            hIndex1 = 0;
        }

        float hFract1 = hScaled - (float) hIndex0;
        float sFract1 = sScaled - (float) sIndex0;
        float vFract1 = vScaled - (float) vIndex0;

        float hFract0 = 1.0f - hFract1;
        float sFract0 = 1.0f - sFract1;
        float vFract0 = 1.0f - vFract1;

        const HSBModify *entry00 = tableBase + vIndex0 * ti.pc.valStep + hIndex0 * ti.pc.hueStep + sIndex0;

        const HSBModify *entry01 = entry00 + (hIndex1 - hIndex0) * ti.pc.hueStep;

        const HSBModify *entry10 = entry00 + ti.pc.valStep;
        const HSBModify *entry11 = entry01 + ti.pc.valStep;

        float hueShift0 = vFract0 * (hFract0 * entry00->fHueShift +
                                     hFract1 * entry01->fHueShift) +
            vFract1 * (hFract0 * entry10->fHueShift +
                       hFract1 * entry11->fHueShift);

        float satScale0 = vFract0 * (hFract0 * entry00->fSatScale +
                                     hFract1 * entry01->fSatScale) +
            vFract1 * (hFract0 * entry10->fSatScale +
                       hFract1 * entry11->fSatScale);

        float valScale0 = vFract0 * (hFract0 * entry00->fValScale +
                                     hFract1 * entry01->fValScale) +
            vFract1 * (hFract0 * entry10->fValScale +
                       hFract1 * entry11->fValScale);

        entry00++;
        entry01++;
        entry10++;
        entry11++;

        float hueShift1 = vFract0 * (hFract0 * entry00->fHueShift +
                                     hFract1 * entry01->fHueShift) +
            vFract1 * (hFract0 * entry10->fHueShift +
                       hFract1 * entry11->fHueShift);

        float satScale1 = vFract0 * (hFract0 * entry00->fSatScale +
                                     hFract1 * entry01->fSatScale) +
            vFract1 * (hFract0 * entry10->fSatScale +
                       hFract1 * entry11->fSatScale);

        float valScale1 = vFract0 * (hFract0 * entry00->fValScale +
                                     hFract1 * entry01->fValScale) +
            vFract1 * (hFract0 * entry10->fValScale +
                       hFract1 * entry11->fValScale);

        hueShift = sFract0 * hueShift0 + sFract1 * hueShift1;
        satScale = sFract0 * satScale0 + sFract1 * satScale1;
        valScale = sFract0 * valScale0 + sFract1 * valScale1;
    }

    hueShift *= (6.0f / 360.0f);	// Convert to internal hue range.

    h += hueShift;
    s *= satScale;  // no clipping here, we are RT float :-)
    v *= valScale;
}

// Convert DNG forward matrix to xyz_cam compatible matrix
void DCPProfile::ConvertDNGForwardMatrix2XYZCAM(const double (*mForwardMatrix)[3], double (*mXYZCAM)[3], ColorTemp &wb) const {

    // Convert ForwardMatrix (white-balanced CameraRGB -> XYZ D50 matrix)
    // into a ColorMatrix (XYZ -> CameraRGB)

    double X, Z;
    ColorTemp::temp2mulxyz(wb.getTemp(), wb.getGreen(), wb.getMethod(), X, Z);

    const double white_xyz[3] = { X, 1, Z };
    const double white_d50[3] = { 0.3457, 0.3585, 0.2958 }; // D50

    // Cancel out the white balance to get a CameraRGB -> XYZ D50 matrixx (CameraToPCS in dng terminology)
    double whiteDiag[3][3] = {{white_xyz[0], 0, 0}, {0, white_xyz[1], 0}, {0, 0, white_xyz[2]}};
    double whiteDiagInv[3][3];
    Invert3x3(whiteDiag, whiteDiagInv);
    double rgb2xyzD50[3][3];
    Multiply3x3(mForwardMatrix, whiteDiagInv, rgb2xyzD50);

    // Through chromatic adaptation convert XYZ D50 to XYZ camera white
    double whiteMatrix[3][3];
    MapWhiteMatrix(white_d50, white_xyz, whiteMatrix);
    double rgb2xyz[3][3];
    Multiply3x3(rgb2xyzD50, whiteMatrix, rgb2xyz);

    // Now we have CameraRGB -> XYZ, invert so we get XYZ -> CameraRGB (ColorMatrix format)
    double dngColorMatrix[3][3];
    Invert3x3(rgb2xyz, dngColorMatrix);

    // now we can run the ordinary ColorMatrix conversion
    ConvertDNGMatrix2XYZCAM(dngColorMatrix, mXYZCAM);
}

void DCPProfile::Apply(Imagefloat *pImg, int preferredIlluminant, Glib::ustring workingSpace, ColorTemp &wb, float rawWhiteFac, bool useToneCurve) const {

    TMatrix mWork = iccStore->workingSpaceInverseMatrix (workingSpace);

    double mXYZCAM[3][3];
    MakeXYZCAM(wb, preferredIlluminant, mXYZCAM);
    HSBModify *deleteTableHandle;
    const HSBModify *deltaBase = MakeHueSatMap(wb, preferredIlluminant, &deleteTableHandle);

    useToneCurve&=toneCurve;

    if (deltaBase == NULL && aLookTable == NULL && !useToneCurve) {
        //===== The fast path: no LUT and not tone curve- Calculate matrix for direct conversion raw>working space
        double mat[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
        for (int i=0; i<3; i++) 
            for (int j=0; j<3; j++)
                for (int k=0; k<3; k++)
                    mat[i][j] += mWork[i][k] * mXYZCAM[k][j];

        // Apply the matrix part
#pragma omp parallel for
        for (int y=0; y<pImg->height; y++) {
            float newr, newg, newb;
            for (int x=0; x<pImg->width; x++) {
                newr = mat[0][0]*pImg->r(y,x) + mat[0][1]*pImg->g(y,x) + mat[0][2]*pImg->b(y,x);
                newg = mat[1][0]*pImg->r(y,x) + mat[1][1]*pImg->g(y,x) + mat[1][2]*pImg->b(y,x);
                newb = mat[2][0]*pImg->r(y,x) + mat[2][1]*pImg->g(y,x) + mat[2][2]*pImg->b(y,x);

                pImg->r(y,x) = newr; pImg->g(y,x) = newg; pImg->b(y,x) = newb;
            }
        }
    }
    else {
        //===== LUT available- Calculate matrix for conversion raw>ProPhoto
        double m2ProPhoto[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
        for (int i=0; i<3; i++) 
            for (int j=0; j<3; j++)
                for (int k=0; k<3; k++)
                    m2ProPhoto[i][j] += prophoto_xyz[i][k] * mXYZCAM[k][j];

        double m2Work[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
        for (int i=0; i<3; i++) 
            for (int j=0; j<3; j++)
                for (int k=0; k<3; k++)
                    m2Work[i][j] += mWork[i][k] * xyz_prophoto[k][j];

        bool useRawWhite=fabs(rawWhiteFac)>0.001;

        // Convert to prophoto and apply LUT
#pragma omp parallel for
        for (int y=0; y<pImg->height; y++) {
            float newr, newg, newb, h,s,v,hs,ss,vs;
            for (int x=0; x<pImg->width; x++) {
                newr = m2ProPhoto[0][0]*pImg->r(y,x) + m2ProPhoto[0][1]*pImg->g(y,x) + m2ProPhoto[0][2]*pImg->b(y,x);
                newg = m2ProPhoto[1][0]*pImg->r(y,x) + m2ProPhoto[1][1]*pImg->g(y,x) + m2ProPhoto[1][2]*pImg->b(y,x);
                newb = m2ProPhoto[2][0]*pImg->r(y,x) + m2ProPhoto[2][1]*pImg->g(y,x) + m2ProPhoto[2][2]*pImg->b(y,x);

                // if point is in negative area, just the matrix, but not the LUT
                if ((deltaBase || aLookTable) && newr>=0 && newg>=0 && newb>=0) {
                    Color::rgb2hsv(newr, newg, newb, h , s, v);
                    h*=6.f;  // RT calculates in [0,1]

                    if (useRawWhite) {
                        // Retro-calculate what the point was like before RAW white came in
                        Color::rgb2hsv(newr/rawWhiteFac, newg/rawWhiteFac, newb/rawWhiteFac, hs, ss, vs);
                        hs*=6.f;  // RT calculates in [0,1]
                    } else {
                        hs=h; ss=s; vs=v;
                    }

                    if (deltaBase) {
                        HSDApply(DeltaInfo, deltaBase, hs, ss, vs, h, s, v);
                    }
                    if (aLookTable) {
                        HSDApply(LookInfo, aLookTable, hs, ss, vs, h, s, v);
                    }

                    // RT range correction
                    if (h < 0.0f) h += 6.0f;
                    if (h >= 6.0f) h -= 6.0f;
                    h/=6.f;  
                    Color::hsv2rgb( h, s, v, newr, newg, newb);
                }

                // tone curve
                if (useToneCurve) toneCurve.Apply(newr, newg, newb);

                pImg->r(y,x) = m2Work[0][0]*newr + m2Work[0][1]*newg + m2Work[0][2]*newb;
                pImg->g(y,x) = m2Work[1][0]*newr + m2Work[1][1]*newg + m2Work[1][2]*newb;
                pImg->b(y,x) = m2Work[2][0]*newr + m2Work[2][1]*newg + m2Work[2][2]*newb;
            }
        }
    }

    if (deleteTableHandle) delete deleteTableHandle;
}

// Integer variant is legacy, only used for thumbs. Simply take the matrix here
void DCPProfile::Apply(Image16 *pImg, int preferredIlluminant, Glib::ustring workingSpace, ColorTemp &wb, bool useToneCurve) const {
    TMatrix mWork = iccStore->workingSpaceInverseMatrix (workingSpace);

    double mXYZCAM[3][3];
    MakeXYZCAM(wb, preferredIlluminant, mXYZCAM);

    useToneCurve&=toneCurve;

    // Calculate matrix for direct conversion raw>working space
    double mat[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
    for (int i=0; i<3; i++) 
        for (int j=0; j<3; j++)
            for (int k=0; k<3; k++)
                mat[i][j] += mWork[i][k] * mXYZCAM[k][j];

    // Apply the matrix part
#pragma omp parallel for
    for (int y=0; y<pImg->height; y++) {
        float newr, newg, newb;
        for (int x=0; x<pImg->width; x++) {
            newr = mat[0][0]*pImg->r(y,x) + mat[0][1]*pImg->g(y,x) + mat[0][2]*pImg->b(y,x);
            newg = mat[1][0]*pImg->r(y,x) + mat[1][1]*pImg->g(y,x) + mat[1][2]*pImg->b(y,x);
            newb = mat[2][0]*pImg->r(y,x) + mat[2][1]*pImg->g(y,x) + mat[2][2]*pImg->b(y,x);

            // tone curve
            if (useToneCurve) toneCurve.Apply(newr, newg, newb);

            pImg->r(y,x) = CLIP((int)newr); pImg->g(y,x) = CLIP((int)newg); pImg->b(y,x) = CLIP((int)newb);
        }
    }
}


// Generates as singleton
DCPStore* DCPStore::getInstance()
{
    static DCPStore* instance_ = 0;
    if ( instance_ == 0 )
    {
        static MyMutex smutex_;
        MyMutex::MyLock lock(smutex_);
        if ( instance_ == 0 )
        {
            instance_ = new DCPStore();
        }
    }
    return instance_;
}

// Reads all profiles from the given profiles dir
void DCPStore::init (Glib::ustring rtProfileDir) {
    MyMutex::MyLock lock(mtx);

    fileStdProfiles.clear();

    Glib::ustring rootDirName=rtProfileDir;

    if (rootDirName!="") {
        std::deque<Glib::ustring> qDirs;

        qDirs.push_front(rootDirName);

        while (!qDirs.empty()) {
            // process directory
            Glib::ustring dirname = qDirs.back();
            qDirs.pop_back();

            Glib::Dir* dir = NULL;
            try {
                if (!safe_file_test (dirname, Glib::FILE_TEST_IS_DIR)) return;
                dir = new Glib::Dir (dirname);
            }
            catch (Glib::Exception& fe) {
                return;
            }
            dirname = dirname + "/";
            for (Glib::DirIterator i = dir->begin(); i!=dir->end(); ++i) {
                Glib::ustring fname = dirname + *i;
                Glib::ustring sname = *i;
                // ignore directories
                if (!safe_file_test (fname, Glib::FILE_TEST_IS_DIR)) {
                    size_t lastdot = sname.find_last_of ('.');
                    if (lastdot!=Glib::ustring::npos && lastdot<=sname.size()-4 && (!sname.casefold().compare (lastdot, 4, ".dcp"))) {
                        Glib::ustring camShortName = sname.substr(0,lastdot).uppercase();
                        fileStdProfiles[camShortName]=fname;  // they will be loaded and cached on demand
                    }
                } else qDirs.push_front(fname);  // for later scanning
            }
            delete dir;
        }
    }
}

DCPProfile* DCPStore::getProfile (Glib::ustring filename, bool isRTProfile) {
    MyMutex::MyLock lock(mtx);

    std::map<Glib::ustring, DCPProfile*>::iterator r = profileCache.find (filename);
    if (r!=profileCache.end()) return r->second;

    // Add profile
    profileCache[filename]=new DCPProfile(filename, isRTProfile);

    return profileCache[filename];
}

DCPProfile* DCPStore::getStdProfile(Glib::ustring camShortName) {
    Glib::ustring name2=camShortName.uppercase();

    // Warning: do NOT use map.find(), since it does not seem to work reliably here
    for (std::map<Glib::ustring, Glib::ustring>::iterator i=fileStdProfiles.begin();i!=fileStdProfiles.end();i++)
        if (name2==(*i).first) return getProfile((*i).second, true);

    return NULL;
}

bool DCPStore::isValidDCPFileName(Glib::ustring filename) const {
    if (!safe_file_test (filename, Glib::FILE_TEST_EXISTS) || safe_file_test (filename, Glib::FILE_TEST_IS_DIR)) return false;
    size_t pos=filename.find_last_of ('.');
    return pos>0 && (!filename.casefold().compare (pos, 4, ".dcp") || !filename.casefold().compare (pos, 4, ".dng"));
}
