///////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2004, Industrial Light & Magic, a division of Lucas
// Digital Ltd. LLC
// 
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
// *       Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// *       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.
// *       Neither the name of Industrial Light & Magic 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
// OWNER 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.
//
///////////////////////////////////////////////////////////////////////////


#include <ImfOutputFile.h>
#include <ImfInputFile.h>
#include <ImfChannelList.h>
#include <ImfArray.h>
#include <ImfVersion.h>
#include "half.h"

#include <ImfBoxAttribute.h>
#include <ImfChannelListAttribute.h>
#include <ImfCompressionAttribute.h>
#include <ImfChromaticitiesAttribute.h>
#include <ImfFloatAttribute.h>
#include <ImfEnvmapAttribute.h>
#include <ImfDoubleAttribute.h>
#include <ImfIntAttribute.h>
#include <ImfLineOrderAttribute.h>
#include <ImfMatrixAttribute.h>
#include <ImfOpaqueAttribute.h>
#include <ImfStringAttribute.h>
#include <ImfStringVectorAttribute.h>
#include <ImfVecAttribute.h>


#include <stdio.h>
#include <assert.h>
#include <string.h>


using namespace std;
namespace IMF = OPENEXR_IMF_NAMESPACE;
using namespace IMF;

namespace IMATH = IMATH_NAMESPACE;
using namespace IMATH;


namespace
{

static const int IMAGE_2K_HEIGHT = 1152;
static const int IMAGE_2K_WIDTH = 2048;

static const char* CHANNEL_NAMES[] = {"R", "G", "B", "A"};
static const char* CHANNEL_NAMES_LEFT[] = {"left.R", "left.G", "left.B", "left.A"};

static const half ALPHA_DEFAULT_VALUE(1.0f);

#define RGB_FILENAME "imf_optimized_aces_rgb.exr"
#define RGBA_FILENAME "imf_optimized_aces_rgba.exr"
#define RGB_STEREO_FILENAME "imf_optimized_aces_rgb_stereo.exr"
#define RGBA_STEREO_FILENAME "imf_optimized_aces_rgba_stereo.exr"

typedef enum EImageType
{
    IMAGE_TYPE_RGB         = 1,
    IMAGE_TYPE_RGBA        = 2,
    IMAGE_TYPE_OTHER        =3
} EImageType;

int
getNbChannels(EImageType pImageType)
{
    int retVal = 0;

    switch(pImageType)
    {
        case IMAGE_TYPE_RGB:
            retVal = 3;
            break;
        case IMAGE_TYPE_RGBA:
            retVal = 4;
            break;
        case IMAGE_TYPE_OTHER:
            retVal = 2;
            break;
        default:
            retVal = 0;
            break;
    }

    return retVal;
}

//
//
//
void
generatePixel (int i, int j, half* rgbaValue, bool pIsLeft)
{
    float redValue = 0.0f;
    float greenValue = 0.0f;
    float blueValue = 0.0f;
    float alphaValue = 0.0f;

    // These formulas are arbitrary but generate results that vary
    // depending on pixel position.  They are used to validate that
    // pixels are read/written correctly, because if we had only one
    // value for the whole image, the tests would still pass if we read
    // only one pixel and copied it all across the buffer.
    if(pIsLeft)
    {
        redValue = ((i + j) % 10 + 0.004f * j) / 10.0f;
        greenValue = ((j + j) % 10 + 0.006f * i) / 10.0f;
        blueValue = ((j * j + i) % 10 + 0.003f * j) / 10.0f;
        alphaValue = ALPHA_DEFAULT_VALUE;
    }
    else
    {
        redValue = ((i * j) % 10 + 0.005f * j) / 10.0f;
        greenValue = ((i + i) % 10 + 0.007f * i) / 10.0f;
        blueValue = ((i * i + j) % 10 + 0.006f * j) / 10.0f;
        alphaValue = ALPHA_DEFAULT_VALUE;
    }


    rgbaValue[0] = redValue;
    rgbaValue[1] = greenValue;
    rgbaValue[2] = blueValue;
    rgbaValue[3] = alphaValue;
}

//
// Given a buffer, fill the pixels with arbitrary but deterministic values.
// Used to fill the pixels in a buffer before writing them to a file.
//
void
fillPixels (const int& pImageHeight,
            const int& pImageWidth,
            Array2D<half>& pPixels,
            int pNbChannels,
            bool pIsLeft)
{
    for(int i = 0; i < pImageHeight; ++i)
    {
        for(int j = 0; j < pImageWidth; ++j)
        {
            half rgbaValue[4];

            generatePixel(i, j, &rgbaValue[0], pIsLeft);
            memcpy( (void*)&pPixels[i][j * pNbChannels],
                    &rgbaValue[0],
                    pNbChannels * sizeof(half));
        }
    }
}

//
// Validate that the pixels value are the same as what we used to fill them.
// Used after reading the pixels from the file to validate that it was read
// properly.
//
void
validatePixels (const int& pImageHeight,
                const int& pImageWidth,
                Array2D<half>& pPixels,
                int pNbChannels,
                bool pIsLeft)
{
    for(int i = 0; i < pImageHeight; ++i)
    {
        for(int j = 0; j < pImageWidth; ++j)
        {
            int retVal = -1;
            half rgbaValue[4];
            generatePixel(i, j, &rgbaValue[0], pIsLeft);

            retVal = memcmp ((void*)&pPixels[i][j * pNbChannels],
                             (void*)&rgbaValue[0],
                             pNbChannels * sizeof(half));

            if(retVal != 0)
            {
                cout << "ERROR at pixel [" << i << ";" << j << "]" << endl;
                cout << "\tExpected [" << rgbaValue[0] << ", "
                        << rgbaValue[1] << ", "
                        << rgbaValue[2] << "] " << endl;

                cout << "\tReceived ["      << pPixels[i][j * pNbChannels] << ", "
                        << pPixels[i][j * pNbChannels + 1] << ", "
                        << pPixels[i][j * pNbChannels + 2] << "]" << endl;
                assert(retVal == 0);
            }

        }
    }
}

//
//  Write pixels to a file (mono version)
//
void
writePixels (const char pFilename[],
             const int& pImageHeight,
             const int& pImageWidth,
             Array2D<half>& pPixels,
             int pNbChannels,
             Compression pCompression)
{
    Header header (pImageWidth,
                   pImageHeight,
                   1.0f,
                   V2f(0.0f,0.0f),
                   1.0f,
                   INCREASING_Y,
                   pCompression);
    for(int i = 0; i < pNbChannels; ++i)
    {
        header.channels().insert (CHANNEL_NAMES[i], Channel(HALF));
    }

    OutputFile lFile(pFilename, header);
    FrameBuffer lOutputFrameBuffer;

    for(int i = 0; i < pNbChannels; ++i)
    {
        lOutputFrameBuffer.insert (CHANNEL_NAMES[i],
                Slice (HALF,
                        (char *) &pPixels[0][i],
                        sizeof (pPixels[0][0]) * pNbChannels,
                        sizeof (pPixels[0][0]) * pNbChannels * pImageWidth));
    }

    lFile.setFrameBuffer (lOutputFrameBuffer);
    lFile.writePixels (pImageHeight);
}

//
//  Write pixels to a file (stereo version)
//
void
writePixels (const char pFilename[],
             const int& pImageHeight,
             const int& pImageWidth,
             Array2D<half>& pPixels,
             Array2D<half>& pPixelsLeft,
             int pNbChannels,
             Compression pCompression)
{
    Header header (pImageWidth,
                   pImageHeight,
                   1.0f,
                   V2f(0.0f,0.0f),
                   1.0f,
                   INCREASING_Y,
                   pCompression);
    for(int i = 0; i < pNbChannels; ++i)
    {
        header.channels().insert (CHANNEL_NAMES[i], Channel(HALF));
        header.channels().insert (CHANNEL_NAMES_LEFT[i], Channel(HALF));
    }

    StringVector multiView;
    multiView.push_back ("right");
    multiView.push_back ("left");
    header.insert("multiView", IMF::TypedAttribute<IMF::StringVector>(multiView));

    OutputFile lFile(pFilename, header);
    FrameBuffer lOutputFrameBuffer;

    for(int i = 0; i < pNbChannels; ++i)
    {
        lOutputFrameBuffer.insert (CHANNEL_NAMES[i],
                Slice (HALF,
                        (char *) &pPixels[0][i],
                        sizeof (pPixels[0][0]) * pNbChannels,
                        sizeof (pPixels[0][0]) * pNbChannels * pImageWidth));

        lOutputFrameBuffer.insert (CHANNEL_NAMES_LEFT[i],
                Slice (HALF,
                        (char *) &pPixelsLeft[0][i],
                        sizeof (pPixelsLeft[0][0]) * pNbChannels,
                        sizeof (pPixelsLeft[0][0]) * pNbChannels * pImageWidth));
    }

    lFile.setFrameBuffer (lOutputFrameBuffer);
    lFile.writePixels (pImageHeight);
}

//
//  Read pixels from a file (mono version).
//
void
readPixels (const char pFilename[], int pNbChannels, Array2D<half>& pPixels)
{
    InputFile lFile(pFilename);
    IMATH::Box2i lDataWindow = lFile.header().dataWindow();

    int lWidth = lDataWindow.max.x - lDataWindow.min.x + 1;
    int lHeight = lDataWindow.max.y - lDataWindow.min.y + 1;

    FrameBuffer lInputFrameBuffer;

    for(int i = 0; i < pNbChannels; ++i)
    {
        lInputFrameBuffer.insert (CHANNEL_NAMES[i],
                Slice (HALF,
                        (char *) &pPixels[0][i],
                        sizeof (pPixels[0][0]) * pNbChannels,
                        sizeof (pPixels[0][0]) * pNbChannels * lWidth,
                        1,
                        1,
                        ALPHA_DEFAULT_VALUE));
    }

    lFile.setFrameBuffer (lInputFrameBuffer);
    
    bool is_optimized = lFile.isOptimizationEnabled();
    if(is_optimized)
    {
        cout << " optimization enabled\n";
        
        if(pNbChannels==2)
        {
            cerr << " error: isOptimizationEnabled returned TRUE, but "
            "optimization not known to work for two channel images\n";
            assert(pNbChannels!=2);
        }
            
    }else{
        cout << " optimization disabled\n";
#ifdef IMF_HAVE_SSE2
        if(pNbChannels!=2)
        {
            cerr << " error: isOptimizationEnabled returned FALSE, but "
            "should work for " << pNbChannels << "channel images\n";
            assert(pNbChannels==2);
        }
        
#endif
    }
    
    lFile.readPixels (lDataWindow.min.y, lDataWindow.max.y);
}

//
//  Read pixels from a file (stereo version).
//
void
readPixels (const char pFilename[],
            int pNbChannels,
            Array2D<half>& pPixels,
            Array2D<half>& pPixelsLeft)
{
    InputFile lFile(pFilename);
    IMATH::Box2i lDataWindow = lFile.header().dataWindow();

    int lWidth = lDataWindow.max.x - lDataWindow.min.x + 1;
    int lHeight = lDataWindow.max.y - lDataWindow.min.y + 1;

    FrameBuffer lInputFrameBuffer;

    for(int i = 0; i < pNbChannels; ++i)
    {
        lInputFrameBuffer.insert (CHANNEL_NAMES[i],
                Slice (HALF,
                        (char *) &pPixels[0][i],
                        sizeof (pPixels[0][0]) * pNbChannels,
                        sizeof (pPixels[0][0]) * pNbChannels * lWidth,
                        1,
                        1,
                        ALPHA_DEFAULT_VALUE));

        lInputFrameBuffer.insert (CHANNEL_NAMES_LEFT[i],
                Slice (HALF,
                        (char *) &pPixelsLeft[0][i],
                        sizeof (pPixelsLeft[0][0]) * pNbChannels,
                        sizeof (pPixelsLeft[0][0]) * pNbChannels * lWidth,
                        1,
                        1,
                        ALPHA_DEFAULT_VALUE));
    }

    lFile.setFrameBuffer (lInputFrameBuffer);
    lFile.readPixels (lDataWindow.min.y, lDataWindow.max.y);
}

//
// Allocate an array of pixels, fill them with values and then write
// them to a file.
void
writeFile (const char pFilename[],
           int pHeight,
           int pWidth,
           EImageType pImageType,
           bool pIsStereo,
           Compression pCompression)
{
    const int lNbChannels = getNbChannels (pImageType);
    Array2D<half> lPixels;

    lPixels.resizeErase (pHeight, pWidth * lNbChannels);
    fillPixels (pHeight, pWidth, lPixels, lNbChannels, false);

    if(pIsStereo)
    {
        Array2D<half> lPixelsLeft;

        lPixelsLeft.resizeErase (pHeight, pWidth * lNbChannels);
        fillPixels (pHeight,
                    pWidth,
                    lPixelsLeft,
                    lNbChannels,
                    true);

        writePixels (pFilename,
                     pHeight,
                     pWidth,
                     lPixels,
                     lPixelsLeft,
                     lNbChannels,
                     pCompression);
    }
    else
    {
        writePixels (pFilename,
                    pHeight,
                    pWidth,
                    lPixels,
                    lNbChannels,
                    pCompression);
    }
}

//
// Read pixels from a file and then validate that the values are the
// same as what was used to fill them before writing.
//
void
readValidateFile (const char pFilename[],
                  int pHeight,
                  int pWidth,
                  EImageType
                  pImageType,
                  bool pIsStereo)
{
    const int lNbChannels = getNbChannels(pImageType);

    Array2D<half> lPixels;
    lPixels.resizeErase(pHeight, pWidth * lNbChannels);
    //readPixels(pFilename, lNbChannels, lPixels);
    //writePixels("pkTest.exr", pHeight, pWidth, lPixels, lNbChannels, NO_COMPRESSION);


    if(pIsStereo)
    {
        Array2D<half> lPixelsLeft;
        lPixelsLeft.resizeErase (pHeight, pWidth * lNbChannels);
        readPixels (pFilename, lNbChannels, lPixels, lPixelsLeft);
        validatePixels (pHeight, pWidth, lPixels, lNbChannels, false);
        validatePixels (pHeight, pWidth, lPixelsLeft, lNbChannels, true);
    }
    else
    {
        readPixels (pFilename, lNbChannels, lPixels);
        validatePixels (pHeight, pWidth, lPixels, lNbChannels, false);
    }
}

//
// confirm the optimization flag returns false for non-RGB files
//
void
testNonOptimized (const std::string & tempDir)
{
    const int pHeight = IMAGE_2K_HEIGHT - 1;
    const int pWidth  =  IMAGE_2K_WIDTH - 1;
    std::string fn = tempDir +  RGB_FILENAME;
    const char* filename  = fn.c_str();

    remove(filename);
    writeFile (filename,  pHeight, pWidth, IMAGE_TYPE_OTHER,  false, NO_COMPRESSION);
    readValidateFile(filename,pHeight,pWidth,IMAGE_TYPE_OTHER,false);
    remove(filename);
}

//
// Test all combinations of file/framebuffer
//  RGB  file to RGB  framebuffer
//  RGB  file to RGBA framebuffer
//  RGBA file to RGB  framebuffer
//  RGBA file to RGBA framebuffer
// Given a switch that determines whether the pixels will be SSE-aligned,
// whether the file is mono or stereo, and the compression algorithm used
// to write the file.
//
void
testAllCombinations (bool isAligned,
                     bool isStereo,
                     Compression pCompression,
                     const std::string & tempDir)
{
    std::string pRgb  = isStereo ?  RGB_STEREO_FILENAME :
                                    RGB_FILENAME;
    pRgb = tempDir + pRgb;
    std::string pRgba = isStereo ?  RGBA_STEREO_FILENAME :
                                    RGBA_FILENAME;
    pRgba = tempDir + pRgba;

    const char * pRgbFilename  = pRgb.c_str();
    const char * pRgbaFilename = pRgba.c_str();

    const int pHeight = isAligned ? IMAGE_2K_HEIGHT : IMAGE_2K_HEIGHT - 1;
    const int pWidth  = isAligned ? IMAGE_2K_WIDTH  : IMAGE_2K_WIDTH  - 1;

    remove(pRgbFilename);
    remove(pRgbaFilename);

    writeFile (pRgbFilename,  pHeight, pWidth, IMAGE_TYPE_RGB,  isStereo, pCompression);
    writeFile (pRgbaFilename, pHeight, pWidth, IMAGE_TYPE_RGBA, isStereo, pCompression);

    cout << "\t\tRGB file to RGB framebuffer" << endl;
    readValidateFile (pRgbFilename,  pHeight, pWidth, IMAGE_TYPE_RGB,  isStereo);
    
    
    cout << "\t\tRGB file to RGB framebuffer" << endl;
    readValidateFile (pRgbFilename,  pHeight, pWidth, IMAGE_TYPE_RGB,  isStereo);

    cout << "\t\tRGB file to RGBA framebuffer" << endl;
    readValidateFile (pRgbFilename,  pHeight, pWidth, IMAGE_TYPE_RGBA, isStereo);

    cout << "\t\tRGBA file to RGB framebuffer" << endl;
    readValidateFile (pRgbaFilename, pHeight, pWidth, IMAGE_TYPE_RGB,  isStereo);

    cout << "\t\tRGBA file to RGBA framebuffer" << endl;
    readValidateFile (pRgbaFilename, pHeight, pWidth, IMAGE_TYPE_RGBA, isStereo);

    remove(pRgbFilename);
    remove(pRgbaFilename);

}

} // anonymous namespace


void
testOptimized (const std::string & tempDir)
{    
    try
    {
        // Test all combinations
        // Aligned file with no compression
        // Unaligned file with no compression
        // Aligned file with zip compression 
        // Unaligned file with zip compression
        //
        // Other algorithms are not necessary because we are not testing
        // compression but whetherthe optimized readPixels() code works
        // with a compressed file.
        // MONO
        cout << "\nTesting optimized code path for rgb(a) images-- "
                "2048x1152 (alignment respected) UNCOMPRESSED" << endl;

                         
        cout << "\tNON-OPTIMIZABLE file" << endl;
        testNonOptimized(tempDir);
                
        cout << "\tALIGNED -- MONO -- NO COMPRESSION" << endl;
        testAllCombinations (true, false, NO_COMPRESSION, tempDir);

        cout << "\tUNALIGNED -- MONO -- NO COMPRESSION" << endl;
        testAllCombinations (false, false, NO_COMPRESSION, tempDir);

        cout << "\tALIGNED -- MONO -- ZIP COMPRESSION" << endl;
        testAllCombinations (true, false, ZIP_COMPRESSION, tempDir);

        cout << "\tUNALIGNED -- MONO -- ZIP COMPRESSION" << endl;
        testAllCombinations (false, false, ZIP_COMPRESSION, tempDir);


        //// STEREO
        cout << "\tALIGNED -- STEREO -- NO COMPRESSION" << endl;
        testAllCombinations (true, true, NO_COMPRESSION, tempDir);

        cout << "\tUNALIGNED -- STEREO -- NO COMPRESSION" << endl;
        testAllCombinations (false, true, NO_COMPRESSION, tempDir);

        cout << "\tALIGNED -- STEREO -- ZIP COMPRESSION" << endl;
        testAllCombinations (true, true, ZIP_COMPRESSION, tempDir);

        cout << "\tUNALIGNED -- STEREO -- ZIP COMPRESSION" << endl;
        testAllCombinations (false, true, ZIP_COMPRESSION, tempDir);

        cout << "RGB(A) files validation complete \n" << endl;
    }
    catch (const std::exception &e)
    {
	    cerr << "ERROR -- caught exception: " << e.what() << endl;
	    assert (false);
    }

}
