///////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2012, Weta Digital Ltd
// 
// 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 Weta Digital 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 "testCompositeDeepScanLine.h"

#include <vector>
#include <string>
#include <Iex.h>
#include <ostream>
#include <iostream>
#include <typeinfo>
#include <stdlib.h>
#include <assert.h>
#include <sstream>

#include <ImfMultiPartOutputFile.h>
#include <ImfMultiPartInputFile.h>
#include <ImfDeepScanLineOutputPart.h>
#include <ImfDeepScanLineInputPart.h>
#include <ImfChannelList.h>
#include <ImfHeader.h>
#include <ImfDeepFrameBuffer.h>
#include <ImfFrameBuffer.h>
#include <ImfPartType.h>
#include <ImfCompression.h>
#include <ImfInputFile.h>
#include <ImfCompositeDeepScanLine.h>
#include <ImfThreading.h>
#include <IlmThread.h>
#include <ImfNamespace.h>

namespace
{


using std::vector;
using std::string;
using std::ostream;
using std::endl;
using std::cout;
using std::ostringstream;


using OPENEXR_IMF_NAMESPACE::DeepScanLineOutputFile;
using OPENEXR_IMF_NAMESPACE::MultiPartInputFile;
using OPENEXR_IMF_NAMESPACE::DeepScanLineOutputPart;
using OPENEXR_IMF_NAMESPACE::DeepScanLineInputPart;

using OPENEXR_IMF_NAMESPACE::Header;
using OPENEXR_IMF_NAMESPACE::PixelType;
using OPENEXR_IMF_NAMESPACE::DeepFrameBuffer;
using OPENEXR_IMF_NAMESPACE::FrameBuffer;
using OPENEXR_IMF_NAMESPACE::FLOAT;
using OPENEXR_IMF_NAMESPACE::HALF;
using OPENEXR_IMF_NAMESPACE::UINT;
using OPENEXR_IMF_NAMESPACE::Slice;
using OPENEXR_IMF_NAMESPACE::DeepSlice;
using OPENEXR_IMF_NAMESPACE::MultiPartOutputFile;
using IMATH_NAMESPACE::Box2i;
using OPENEXR_IMF_NAMESPACE::DEEPSCANLINE;
using OPENEXR_IMF_NAMESPACE::ZIPS_COMPRESSION;
using OPENEXR_IMF_NAMESPACE::InputFile;
using OPENEXR_IMF_NAMESPACE::setGlobalThreadCount;
using OPENEXR_IMF_NAMESPACE::CompositeDeepScanLine;

// a marker to say we've done inserting values into a sample: do mydata << end()
struct end{};

// a marker to say we're about to send the final result, not another sample: do mydata << result()
struct result{};


//
// support class that generates deep data, along with the 'ground truth' 
// result 
//
template<class T>
class data
{
    public:
    vector<string> _channels;               // channel names - same size and order as in all other arrays,
    vector<T> _current_result;              // one value per channel: the ground truth value for the given pixel
    vector<vector <T> >_results;            // a list of result pixels
    
    
    bool _inserting_result;
    bool _started;                          // we've started to assemble the values - no more channels permitted
    vector<T> _current_sample;              // one value per channel for the sample currently being inserted
    vector< vector <T> > _current_pixel;    // a list of results for the current pixwel
    vector< vector< vector<T> > > _samples; // a list of pixels
    PixelType _type;
    
    data() : _inserting_result(false),_started(false)
    {  
        if(typeid(T)==typeid(half))
        {
            _type = OPENEXR_IMF_NAMESPACE::HALF;
        }
        else
        {
            _type = OPENEXR_IMF_NAMESPACE::FLOAT;
        }
    }


    // add a value to the current sample
    data & operator << (float value)
    {
        if(_inserting_result)
        {
          _current_result.push_back(value);
        }else{
          _current_sample.push_back(T(value));
        }
        _started=true;
        return *this;
    }
    
    // switch between writing samples and the result
    data & operator << (const result &)
    {
        if(_current_sample.size()!=0)
        {
            throw IEX_NAMESPACE::ArgExc("bug in test code: can't switch to inserting result: values written without 'end' statement");
        }
        if(_current_result.size()!=0)
        {
            throw IEX_NAMESPACE::ArgExc("bug in test suite: already inserting result");
        }
        _inserting_result=true;
        return *this;
    }
    
    // finalise the current sample/results
    
    data & operator << (const end &)
    {
        if(_inserting_result)
        {
            if(_current_result.size()!=_channels.size())
            {
                throw IEX_NAMESPACE::ArgExc("bug in test suite: cannot end result: wrong number of values written");
            }
            _results.push_back(_current_result);
            _current_result.resize(0);
            
            //
            // also cause the current_samples to be written as the given number of pixels
            //
            _samples.push_back(_current_pixel);
            _current_pixel.resize(0);
            _inserting_result=false;
        }
        else
        {
            if(_current_sample.size()!=_channels.size())
            {
                throw IEX_NAMESPACE::ArgExc("bug in test suite: cannot end sample: wrong number of values written");
            }
            _current_pixel.push_back(_current_sample);
            _current_sample.resize(0);
        }
        return *this;
    }
    
    // add a new channel
    
    data & operator << (const string & s) 
    {
        if(_started)
        {
            throw IEX_NAMESPACE::ArgExc("bug in test suite: cannot insert new channels here");
        }
        _channels.push_back(s);
        return *this;
    }
    
    
    // total number of samples - storage for one copy of everything is sizeof(T)*channels.size()*totalSamples
    size_t totalSamples() const
    {
        size_t answer=0;
        for(size_t i=0;i<_samples.size();i++)
        {
            answer+=_samples[i].size();
        }
        return answer;
    }
    
    //copy the channels into the header list
    void 
    setHeader(Header & hdr) const
    {
      for(size_t i=0;i<_channels.size();i++)
      {
         hdr.channels().insert(_channels[i],_type);
      }
    }
    
    
    void frak(vector< data<T> > & parts) const
    {
        for(size_t i=0;i<parts.size();i++)
        {
            parts[i]._channels = _channels;
            parts[i]._results = _results;
            parts[i]._type = _type;
            parts[i]._samples.resize(_samples.size());
        }
        
        //
        // loop over each pixel, pushing its values to a random part
        //
        for(size_t i=0;i<_samples.size();i++)
        {
            // copy sample to a random part
            
            for(int s=0;s<_samples[i].size();s++)
            {
              int part = rand()% parts.size();
              parts[part]._samples[i].push_back(_samples[i][s]);
            }
        }
    }
    
    void 
    writeData(DeepScanLineOutputPart & part) const
    {
        Box2i dw=part.header().dataWindow();
        size_t output_pixels = (dw.size().x+1)*(dw.size().y+1);
        
        // how many times we'll write the same pattern
        size_t repeats = 1+(output_pixels/_results.size());
        
        size_t sample_buffer_size = totalSamples()*repeats;
        
        // buffer for sample counts
        vector<unsigned int> counts(output_pixels);
        
        // buffers for sample pointers
        vector< vector<T *> > sample_pointers(_channels.size());
        
        // buffer for actual sample data
        vector< vector<T> > sample_buffers(_channels.size());
        
        for(size_t i=0;i<sample_buffers.size();i++)
        {
            sample_pointers[i].resize(output_pixels);
            sample_buffers[i].resize(sample_buffer_size);
        }
        
        
        size_t pixel=0; // which pixel we are currently writing
        size_t sample=0; // which sample we are currently writing into
        
        for(size_t p=0;p<output_pixels;p++)
        {
            size_t count = _samples[pixel].size();
            counts[p]=count;
            if( count>0 )
            {
                for(size_t c=0 ; c<_channels.size() ; c++)
                {
                    for(size_t s=0 ; s < count ; s++ )
                    {
                        sample_buffers[c][sample+s]=_samples[pixel][s][c];
                    }
                    sample_pointers[c][p]=&sample_buffers[c][sample];
                }
                sample+=count;
            }
            pixel++;
            if(pixel==_samples.size()) pixel=0;
        }
        cout << " wrote " << sample << " samples  into " << output_pixels << " pixels\n";
        
        DeepFrameBuffer fb;
        fb.insertSampleCountSlice(Slice(UINT,
                                        (char *)(&counts[0]-dw.min.x-(dw.size().x+1)*dw.min.y),
                                        sizeof(unsigned int),
                                        sizeof(unsigned int)*(dw.size().x+1)
                                        )
                                  );
        for(size_t c=0;c<_channels.size();c++)
        {
            fb.insert(_channels[c],
                      DeepSlice(_type,(char *)(&sample_pointers[c][0]-dw.min.x-(dw.size().x+1)*dw.min.y),
                            sizeof(T *),
                            sizeof(T *)*(dw.size().x+1),
                            sizeof(T)
                            )
                     );
        }
        part.setFrameBuffer(fb);
        part.writePixels(dw.size().y+1);
        
    }
    
    void 
    setUpFrameBuffer(vector<T> & data,FrameBuffer & framebuf,const Box2i & dw,bool dontbotherloadingdepth) const
    {
        
        // allocate enough space for all channels (even the depth channel)
        data.resize(_channels.size()*(dw.size().x+1)*(dw.size().y+1));
        for(size_t i=0;i<_channels.size();i++)
        {
            if(!dontbotherloadingdepth || (_channels[i]!="Z" && _channels[i]!="ZBack") )
            {
                framebuf.insert(_channels[i].c_str(),
                                Slice(_type,(char *) (&data[i] - (dw.min.x + dw.min.y*(dw.size().x+1))*_channels.size() ),
                                      sizeof(T)*_channels.size(),
                                      sizeof(T)*(dw.size().x+1)*_channels.size())
                                      );
            }
        }
    }
    
    
    //
    // check values are within a suitable tolerance of the expected value (expect some errors due to half float storage etc)
    //
    void 
    checkValues(const vector<T> & data,const Box2i & dw,bool dontbothercheckingdepth)
    {
        size_t size = _channels.size()+(dw.size().x+1)*(dw.size().y+1);
        size_t pel=0;
        size_t channel=0;
        if(dontbothercheckingdepth)
        {
            for(size_t i=0;i<size;i++)
            {
                if(_channels[channel]!="Z" && _channels[channel]!="ZBack")
                {
                    if(fabs(_results[pel][channel] - data[i])>0.005)
                    {
                      cout << "sample " << i << " (channel " << _channels[channel] << " of pixel " << i % _channels.size() << ") ";
                      cout << "doesn't match expected value (channel " << channel << " of pixel " << pel << ") : ";
                      cout << "got " << data[i] << " expected " << _results[pel][channel] << endl;
                    }
                    assert(fabs(_results[pel][channel]- data[i])<=0.005);
                }
                channel++;
                if(channel==_channels.size())
                {
                    channel=0;
                    pel++;
                    if(pel==_results.size())
                    {
                        pel=0;
                    }
                }
                
            }
        }else{
            for(size_t i=0;i<size;i++)
            {
                if(fabs(_results[pel][channel] - data[i])>0.005)
                {
                    cout << "sample " << i << " (channel " << _channels[channel] << " of pixel " << i % _channels.size() << ") ";
                    cout << "doesn't match expected value (channel " << channel << " of pixel " << pel << ") : ";
                    cout << "got " << data[i] << " expected " << _results[pel][channel] << endl;
                }
                assert(fabs(_results[pel][channel] - data[i])<=0.005);
                channel++;
                if(channel==_channels.size())
                {
                    channel=0;
                    pel++;
                    if(pel==_results.size())
                    {
                        pel=0;
                    }
                }
            }
        }
        
    }
    
};

template<class T>
ostream & operator << (ostream & o,data<T> & d)
{
    o << "channels: [ ";
    for(size_t i=0;i<d._channels.size();i++)
    {
        o << d._channels[i] << " ";
    }
    o << "]" << endl;
    
    for(size_t i=0;i<d._samples.size();i++)
    {
        o << "pixel: " << d._samples[i].size() << " samples" << endl;
        
        for(size_t j=0;j<d._samples[i].size();j++)
        {
          o << "     " << j << ": [ ";
          for(size_t k = 0; k < d._samples[i][j].size();k++)
          {
              o << d._samples[i][j][k] << ' ';
          }
          o << "]" << endl;
        }
        o << "result: [ ";
        for(size_t k=0;k<d._results[i].size();k++)
        {
            o << d._results[i][k] << ' ' ;
        }
        o << "]\n" << endl;
    }
    
    return o;
}

template<class DATA>
void
make_pattern(data<DATA> & bob,int pattern_number)
{
    
    if (pattern_number==0)
    {
        // set channels
        
        bob << string("Z") << string("ZBack") << string("A") << string("R");
        PixelType t;

        // regular two-pixel composite
        bob << 1.0 << 2.0 << 0.0 << 1.0 << end();
        bob << 2.1 << 2.3 << 0.5 << 0.4 << end();
        bob << result();
        bob << 3.1 << 4.3 << 0.5 << 1.4 << end();
        
        bob << 10 << 20 << 1.0 << 1.0 << end();
        bob << 20 << 30 << 1.0 << 2.0 << end();
        bob << result();
        bob << 10 << 20 << 1.0 << 1.0 << end();
        
        bob << result();
        bob << 0.0 << 0.0 << 0.0 << 0.0 << end();
    }
    else if (pattern_number==1)
    {
        //
        // out of order channels, no zback - should-re-order them for us
        //
        bob << string("Z") << string("R") << string("G") << string("B") << string("A");
        
        
        // write this four times, so we get various patterns for splitting the blocks
        for(int pass=0;pass<4;pass++)
        {
          // regular four-pixel composite
          bob << 1.0 << 0.4 << 1.25 << -0.1 << 0.7 << end();
          bob << 2.2 << 0.2 << -0.1 << 0.0 << 0.24 << end();
          bob << 2.3 << 0.9 << 0.56 << 2.26 << 0.9 << end();
          bob << 5.0 << 1.0 << 0.5  << 0.60 << 0.2 << end();
          bob << result();
        
          // eight-pixel composite
          bob << 2.2984 << 0.68800 <<  1.35908 << 0.42896 << 0.9817 <<  end();
          bob << 1.0  << 0.4 << 1.25 << -0.1 << 0.7 << end();
          bob << 2.2  << 0.2 << -0.1 << 0.0 << 0.24 << end();
          bob << 2.3  << 0.9 << 0.56 << 2.26 << 0.9 << end();
          bob << 5.0  << 1.0 << 0.5  << 0.60 << 0.2 << end();
          bob << 11.0 << 0.4 << 1.25 << -0.1 << 0.7 << end();
          bob << 12.2 << 0.2 << -0.1 << 0.0 << 0.24 << end();
          bob << 12.3 << 0.9 << 0.56 << 2.26 << 0.9 << end();
          bob << 15.0 << 1.0 << 0.5  << 0.60 << 0.2 << end();
          bob << result();
          bob << 2.62319 << 0.7005 <<  1.38387 << 0.43678 << 0.99967 <<  end();
        
          // one-pixel composite
         
          bob << 27.0 << 1.0 << -1.0 << 42.0 << 14 << end(); // alpha>1 should still work
          bob << result();
          bob << 27.0 << 1.0 << -1.0 << 42.0 << 14 << end();
        }
        
        
    }
}

template<class T>
void
write_file(const char * filename, const data<T> & master, int number_of_parts)
{
    vector<Header> headers(number_of_parts);
    
    // all headers are the same in this test
    headers[0].displayWindow().max.x=164;
    headers[0].displayWindow().max.y=216;
    headers[0].dataWindow().min.x=rand()%400 - 200;
    headers[0].dataWindow().max.x=headers[0].dataWindow().min.x+40+rand()%400;
    headers[0].dataWindow().min.y=rand()%400 - 200;
    headers[0].dataWindow().max.y=headers[0].dataWindow().min.y+40+rand()%400;
    cout << "data window: " << headers[0].dataWindow().min.x << ',' << headers[0].dataWindow().min.y << ' ' << 
    headers[0].dataWindow().max.x << ',' << headers[0].dataWindow().max.y << endl;
    headers[0].setType(DEEPSCANLINE);
    headers[0].compression()=ZIPS_COMPRESSION;
    headers[0].setName("Part0");
    
    for(int i=1;i<number_of_parts;i++)
    {
        headers[i]=headers[0];
        ostringstream s;
        s << "Part" << i;
        headers[i].setName(s.str());
    }
    
    vector< data<T> > sub_parts(number_of_parts);
    
    if(number_of_parts>1)
    {
      master.frak(sub_parts);
    }  
    
    if(number_of_parts==1)
    {
        master.setHeader(headers[0]);
    }else{
        
        for(int i=0;i<number_of_parts;i++)
        {
            sub_parts[i].setHeader(headers[i]);
        }
    }
    
    MultiPartOutputFile f(filename,&headers[0],headers.size());
    for(int i=0;i<number_of_parts;i++)
    {
        DeepScanLineOutputPart p(f,i);
        if(number_of_parts==1)
        {
            master.writeData(p);
        }else{
            sub_parts[i].writeData(p);
        }
    }
}

template<class T>
void
test_parts (int pattern_number,
            int number_of_parts,
            bool load_depths,
            bool entire_buffer,
            const std::string &tempDir)
{
     std::string fn = tempDir + "imf_test_composite_deep_scanline_source.exr";

     data<T> master;
     make_pattern (master, pattern_number);
     write_file (fn.c_str(), master,number_of_parts);
     

     {
        vector<T> data;         
        CompositeDeepScanLine comp;
        FrameBuffer testbuf;
        MultiPartInputFile input(fn.c_str());
        vector<DeepScanLineInputPart *> parts(number_of_parts);
     
     
        // use 'part' interface TODO test file interface too
        for(int i=0;i<number_of_parts;i++)
        {
            parts[i] = new DeepScanLineInputPart(input,i);
            comp.addSource(parts[i]);
        }
     
     
        master.setUpFrameBuffer(data,testbuf,comp.dataWindow(),load_depths);
     
        comp.setFrameBuffer(testbuf);
     
         //
         // try loading the whole buffer
         //
         if(entire_buffer)
         {
            comp.readPixels(comp.dataWindow().min.y,comp.dataWindow().max.y);
         }else{
           int low = comp.dataWindow().min.y;
           while(low<comp.dataWindow().max.y)
           {
               int high = low + rand()%64;
               if(high>comp.dataWindow().max.y) 
                   high = comp.dataWindow().max.y;
               comp.readPixels(low,high);
               low = high;
           }
         }
        
         master.checkValues(data,comp.dataWindow(),load_depths);
  
         for(int i=0;i<number_of_parts;i++)
         {
            delete parts[i];
         }
     }
     if(number_of_parts==1)
     {
         // also test InputFile interface
         InputFile file(fn.c_str());
         vector<T> data;
         FrameBuffer testbuf;
         const Box2i & dataWindow = file.header().dataWindow();
         master.setUpFrameBuffer(data,testbuf,dataWindow,load_depths);
         file.setFrameBuffer(testbuf);
         if(entire_buffer)
         {
             file.readPixels(dataWindow.min.y,dataWindow.max.y);
         }else{
             int low = dataWindow.min.y;
             while(low<dataWindow.max.y)
             {
                 int high = low + rand()%64;
                 if(high>dataWindow.max.y) 
                     high = dataWindow.max.y;
                 file.readPixels(low,high);
                 low = high;
             }
         }
         
         master.checkValues (data, dataWindow, load_depths);
         
     }
     remove (fn.c_str());
}

}

void testCompositeDeepScanLine (const std::string & tempDir)
{
    
    cout << "\n\nTesting deep compositing interface basic functionality:\n" << endl;

    int passes=2;
    if (!ILMTHREAD_NAMESPACE::supportsThreads ())
    {
            passes=1;
    }
  

    srand(1);
    
    for(int pass=0;pass<2;pass++)
    {

        test_parts<float>(0, 1, true,  true,  tempDir);
        test_parts<float>(0, 1, false, false, tempDir);
        test_parts<half> (0, 1, true,  false, tempDir);
        test_parts<half> (0, 1, false, true,  tempDir);

        //
        // test pattern 1: tested by confirming data is written correctly and
        // then reading correct results in Nuke
        //
        test_parts<float>(1, 1, true,  false, tempDir);
        test_parts<float>(1, 1, false, true,  tempDir);
        test_parts<half> (1, 1, true,  true,  tempDir);
        test_parts<half> (1, 1, false, false, tempDir);


        cout << "Testing deep compositing across multiple parts:\n" << endl;

        test_parts<float>(0, 5, true,  false, tempDir);
        test_parts<float>(0, 5, false, true,  tempDir);
        test_parts<half> (0, 5, true,  false, tempDir);
        test_parts<half> (0, 5, false, true,  tempDir);

        test_parts<float>(1, 3, true,  true,  tempDir);
        test_parts<float>(1, 3, false, false, tempDir);
        test_parts<half> (1, 3, true,  true,  tempDir);
        test_parts<half> (1, 3, false, false, tempDir);

        test_parts<float>(1, 4, true,  true,  tempDir);
        test_parts<float>(1, 4, false, false, tempDir);
        test_parts<half> (1, 4, true,  false, tempDir);
        test_parts<half> (1, 4, false, true,  tempDir);

        if(passes==2 && pass==0)
        {
            cout << " testing with multithreading...\n";
            setGlobalThreadCount(64);
        }
    }
    cout << " ok\n" << endl;
}
