/* ============================================================
 *
 * This file is a part of digiKam project
 * http://www.digikam.org
 *
 * Date        : 2005-06-14
 * Description : A JPEG IO file for DImg framework
 *
 * Copyright (C) 2005 by Renchi Raju <renchi@pooh.tam.uiuc.edu>
 * Copyright (C) 2005-2007 by Gilles Caulier <caulier dot gilles at gmail dot com> 
 *
 * This program 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 2, 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 General Public License for more details.
 *
 * ============================================================ */

#define XMD_H 

// This line must be commented to prevent any latency time
// when we use threaded image loader interface for each image
// files io. Uncomment this line only for debugging.
//#define ENABLE_DEBUG_MESSAGES

// C ansi includes.

extern "C" 
{
#include "iccjpeg.h"
}

// C+ includes.

#include <cstdio>
#include <cstdlib>

// TQt includes.

#include <tqfile.h>
#include <tqcstring.h>

// Local includes.

#include "ddebug.h"
#include "dimg.h"
#include "dimgloaderobserver.h"
#include "jpegloader.h"

namespace Digikam
{

// To manage Errors/Warnings handling provide by libjpeg

void JPEGLoader::dimg_jpeg_error_exit(j_common_ptr cinfo)
{
    dimg_jpeg_error_mgr* myerr = (dimg_jpeg_error_mgr*) cinfo->err;

    char buffer[JMSG_LENGTH_MAX];
    (*cinfo->err->format_message)(cinfo, buffer);

#ifdef ENABLE_DEBUG_MESSAGES
    DDebug() << k_funcinfo << buffer << endl;
#endif

    longjmp(myerr->setjmp_buffer, 1);
}

void JPEGLoader::dimg_jpeg_emit_message(j_common_ptr cinfo, int msg_level)
{
    char buffer[JMSG_LENGTH_MAX];
    (*cinfo->err->format_message)(cinfo, buffer);

#ifdef ENABLE_DEBUG_MESSAGES
    DDebug() << k_funcinfo << buffer << " (" << msg_level << ")" << endl;
#else
    Q_UNUSED(msg_level);
#endif
}

void JPEGLoader::dimg_jpeg_output_message(j_common_ptr cinfo)
{
    char buffer[JMSG_LENGTH_MAX];
    (*cinfo->err->format_message)(cinfo, buffer);

#ifdef ENABLE_DEBUG_MESSAGES
    DDebug() << k_funcinfo << buffer << endl;
#endif
}

JPEGLoader::JPEGLoader(DImg* image)
          : DImgLoader(image)
{
}

bool JPEGLoader::load(const TQString& filePath, DImgLoaderObserver *observer)
{
    readMetadata(filePath, DImg::JPEG);

    FILE *file = fopen(TQFile::encodeName(filePath), "rb");
    if (!file)
        return false;

    unsigned char header[2];

    if (fread(&header, 2, 1, file) != 1)
    {
        fclose(file);
        return false;
    }

    unsigned char jpegID[] = { 0xFF, 0xD8 };

    if (memcmp(header, jpegID, 2) != 0)
    {
        // not a jpeg file
        fclose(file);
        return false;
    }

    fseek(file, 0L, SEEK_SET);

    struct jpeg_decompress_struct cinfo;
    struct dimg_jpeg_error_mgr    jerr;

    // -------------------------------------------------------------------
    // JPEG error handling.

    cinfo.err                 = jpeg_std_error(&jerr);
    cinfo.err->error_exit     = dimg_jpeg_error_exit;
    cinfo.err->emit_message   = dimg_jpeg_emit_message;
    cinfo.err->output_message = dimg_jpeg_output_message;

    // If an error occurs during reading, libjpeg will jump here

    if (setjmp(jerr.setjmp_buffer)) 
    {
        jpeg_destroy_decompress(&cinfo);
        fclose(file);
        return false;
    }

    // -------------------------------------------------------------------
    // Find out if we do the fast-track loading with reduced size. Jpeg specific.
    int scaledLoadingSize = 0;
    TQVariant attribute = imageGetAttribute("jpegScaledLoadingSize");
    if (attribute.isValid())
        scaledLoadingSize = attribute.toInt();

    // -------------------------------------------------------------------
    // Set JPEG decompressor instance

    jpeg_create_decompress(&cinfo);
    jpeg_stdio_src(&cinfo, file);

    // Recording ICC profile marker (from iccjpeg.c)
    setup_read_icc_profile(&cinfo);

    // read image information
    jpeg_read_header(&cinfo, true);

    // set decompression parameters
    cinfo.do_fancy_upsampling = false;
    cinfo.do_block_smoothing  = false;

    if (scaledLoadingSize)
    {
        int imgSize = TQMAX(cinfo.image_width, cinfo.image_height);

        // libjpeg supports 1/1, 1/2, 1/4, 1/8
        int scale=1;
        while(scaledLoadingSize*scale*2<=imgSize)
        {
            scale*=2;
        }
        if(scale>8) scale=8;

        cinfo.scale_num=1;
        cinfo.scale_denom=scale;
    }

    // Libjpeg handles the following conversions:
    // YCbCr => GRAYSCALE, YCbCr => RGB, GRAYSCALE => RGB, YCCK => CMYK
    // So we cannot get RGB from CMYK or YCCK, CMYK conversion is handled below
    switch (cinfo.jpeg_color_space)
    {
        case JCS_UNKNOWN:
            // perhaps jpeg_read_header did some guessing, leave value unchanged
            break;
        case JCS_GRAYSCALE:
        case JCS_RGB:
        case JCS_YCbCr:
            cinfo.out_color_space     = JCS_RGB;
            break;
        case JCS_CMYK:
        case JCS_YCCK:
            cinfo.out_color_space     = JCS_CMYK;
            break;
    }

    // initialize decompression
    jpeg_start_decompress(&cinfo);

    // some pseudo-progress
    if (observer)
        observer->progressInfo(m_image, 0.1);

    // -------------------------------------------------------------------
    // Get image data.

    int w = cinfo.output_width;
    int h = cinfo.output_height;
    uchar *dest = 0;
    
    uchar *ptr, *line[16], *data=0;
    uchar *ptr2=0;
    int    x, y, l, i, scans, count, prevy;

    if (cinfo.rec_outbuf_height > 16)
    {
        jpeg_destroy_decompress(&cinfo);
        fclose(file);
        DDebug() << k_funcinfo << "Height of JPEG scanline buffer out of range!" << endl;
        return false;
    }

    // We only take RGB with 1 or 3 components, or CMYK with 4 components
    if (!(
           (cinfo.out_color_space == JCS_RGB  && (cinfo.output_components == 3 || cinfo.output_components == 1))
        || (cinfo.out_color_space == JCS_CMYK &&  cinfo.output_components == 4)
        ))
    {
        jpeg_destroy_decompress(&cinfo);
        fclose(file);
        DDebug() << k_funcinfo
                  << "JPEG colorspace ("
                  << cinfo.out_color_space
                  << ") or Number of JPEG color components ("
                  << cinfo.output_components
                  << ") unsupported!" << endl;
        return false;
    }

    data = new uchar[w * 16 * cinfo.output_components];

    if (!data)
    {
        jpeg_destroy_decompress(&cinfo);
        fclose(file);
        DDebug() << k_funcinfo << "Cannot allocate memory!" << endl;
        return false;
    }

    dest = new uchar[w * h * 4];

    if (!dest)
    {
        delete [] data;
        jpeg_destroy_decompress(&cinfo);
        fclose(file);
        DDebug() << k_funcinfo << "Cannot allocate memory!" << endl;
        return false;
    }

    ptr2  = dest;
    count = 0;
    prevy = 0;

    if (cinfo.output_components == 3)
    {
        for (i = 0; i < cinfo.rec_outbuf_height; i++)
            line[i] = data + (i * w * 3);

        int checkPoint = 0;
        for (l = 0; l < h; l += cinfo.rec_outbuf_height)
        {
            // use 0-10% and 90-100% for pseudo-progress
            if (observer && l >= checkPoint)
            {
                checkPoint += granularity(observer, h, 0.8);
                if (!observer->continueQuery(m_image))
                {
                    delete [] data;
                    delete [] dest;
                    jpeg_destroy_decompress(&cinfo);
                    fclose(file);
                    return false;
                }
                observer->progressInfo(m_image, 0.1 + (0.8 * ( ((float)l)/((float)h) )));
            }

            jpeg_read_scanlines(&cinfo, &line[0], cinfo.rec_outbuf_height);
            scans = cinfo.rec_outbuf_height;

            if ((h - l) < scans)
                scans = h - l;

            ptr = data;

            for (y = 0; y < scans; y++)
            {
                for (x = 0; x < w; x++)
                {
                    ptr2[3] = 0xFF;
                    ptr2[2] = ptr[0];
                    ptr2[1] = ptr[1];
                    ptr2[0] = ptr[2];

                    ptr  += 3;
                    ptr2 += 4;
                }
            }
        }
    }
    else if (cinfo.output_components == 1)
    {
        for (i = 0; i < cinfo.rec_outbuf_height; i++)
            line[i] = data + (i * w);

        int checkPoint = 0;
        for (l = 0; l < h; l += cinfo.rec_outbuf_height)
        {
            if (observer && l >= checkPoint)
            {
                checkPoint += granularity(observer, h, 0.8);
                if (!observer->continueQuery(m_image))
                {
                    delete [] data;
                    delete [] dest;
                    jpeg_destroy_decompress(&cinfo);
                    fclose(file);
                    return false;
                }
                observer->progressInfo(m_image, 0.1 + (0.8 * ( ((float)l)/((float)h) )));
            }

            jpeg_read_scanlines(&cinfo, line, cinfo.rec_outbuf_height);
            scans = cinfo.rec_outbuf_height;

            if ((h - l) < scans)
                scans = h - l;

            ptr = data;

            for (y = 0; y < scans; y++)
            {
                for (x = 0; x < w; x++)
                {
                    ptr2[3] = 0xFF;
                    ptr2[2] = ptr[0];
                    ptr2[1] = ptr[0];
                    ptr2[0] = ptr[0];

                    ptr  ++;
                    ptr2 += 4;
                }
            }
        }
    }
    else // CMYK
    {
        for (i = 0; i < cinfo.rec_outbuf_height; i++)
            line[i] = data + (i * w * 4);

        int checkPoint = 0;
        for (l = 0; l < h; l += cinfo.rec_outbuf_height)
        {
            // use 0-10% and 90-100% for pseudo-progress
            if (observer && l >= checkPoint)
            {
                checkPoint += granularity(observer, h, 0.8);
                if (!observer->continueQuery(m_image))
                {
                    delete [] data;
                    delete [] dest;
                    jpeg_destroy_decompress(&cinfo);
                    fclose(file);
                    return false;
                }
                observer->progressInfo(m_image, 0.1 + (0.8 * ( ((float)l)/((float)h) )));
            }

            jpeg_read_scanlines(&cinfo, &line[0], cinfo.rec_outbuf_height);
            scans = cinfo.rec_outbuf_height;

            if ((h - l) < scans)
                scans = h - l;

            ptr = data;

            for (y = 0; y < scans; y++)
            {
                for (x = 0; x < w; x++)
                {
                    // Inspired by TQt's JPEG loader

                    int k = ptr[3];

                    ptr2[3] = 0xFF;
                    ptr2[2] = k * ptr[0] / 255;
                    ptr2[1] = k * ptr[1] / 255;
                    ptr2[0] = k * ptr[2] / 255;

                    ptr  += 4;
                    ptr2 += 4;
                }
            }
        }
    }

    delete [] data;

    // -------------------------------------------------------------------
    // Read image ICC profile

    TQMap<int, TQByteArray>& metaData = imageMetaData();

    JOCTET *profile_data=NULL;
    uint    profile_size;

    read_icc_profile (&cinfo, &profile_data, &profile_size);

    if (profile_data != NULL) 
    {
        TQByteArray profile_rawdata(profile_size);
        memcpy(profile_rawdata.data(), profile_data, profile_size);
        metaData.insert(DImg::ICC, profile_rawdata);
        free (profile_data);
    }
    else
    {
        // If ICC profile is null, check Exif metadata.
        checkExifWorkingColorSpace();
    }

    // -------------------------------------------------------------------

    jpeg_finish_decompress(&cinfo);
    jpeg_destroy_decompress(&cinfo);

    // -------------------------------------------------------------------

    fclose(file);

    if (observer)
        observer->progressInfo(m_image, 1.0);

    imageWidth()  = w;
    imageHeight() = h;
    imageData()   = dest;
    imageSetAttribute("format", "JPEG");
    
    return true;
}

bool JPEGLoader::save(const TQString& filePath, DImgLoaderObserver *observer)
{
    FILE *file = fopen(TQFile::encodeName(filePath), "wb");
    if (!file)
        return false;

    struct jpeg_compress_struct  cinfo;
    struct dimg_jpeg_error_mgr jerr;    

    // -------------------------------------------------------------------
    // JPEG error handling. 
    
    cinfo.err                 = jpeg_std_error(&jerr);
    cinfo.err->error_exit     = dimg_jpeg_error_exit;
    cinfo.err->emit_message   = dimg_jpeg_emit_message;
    cinfo.err->output_message = dimg_jpeg_output_message;
    
    // If an error occurs during writing, libjpeg will jump here
    
    if (setjmp(jerr.setjmp_buffer)) 
    {
        jpeg_destroy_compress(&cinfo);
        fclose(file);
        return false;
    }
    
    // -------------------------------------------------------------------
    // Set JPEG compressor instance
    
    jpeg_create_compress(&cinfo);
    jpeg_stdio_dest(&cinfo, file);

    uint&              w   = imageWidth();
    uint&              h   = imageHeight();
    unsigned char*& data   = imageData();
    
    // Size of image.
    cinfo.image_width      = w;
    cinfo.image_height     = h;

    // Color components of image in RGB.
    cinfo.input_components = 3;
    cinfo.in_color_space   = JCS_RGB;    

    TQVariant qualityAttr = imageGetAttribute("quality");
    int quality = qualityAttr.isValid() ? qualityAttr.toInt() : 90;

    if (quality < 0)
        quality = 90;
    if (quality > 100)
        quality = 100;

    TQVariant subSamplingAttr = imageGetAttribute("subsampling");
    int subsampling = subSamplingAttr.isValid() ? subSamplingAttr.toInt() : 1;  // Medium
   
    jpeg_set_defaults(&cinfo);

    // B.K.O #149578: set horizontal and vertical chroma subsampling factor to encoder.
    // See this page for details: http://en.wikipedia.org/wiki/Chroma_subsampling

    switch (subsampling)
    {
        case 1:  // 2x1, 1x1, 1x1 (4:2:2) : Medium
        {
            DDebug() << "Using LibJPEG medium chroma-subsampling (4:2:2)" << endl;
            cinfo.comp_info[0].h_samp_factor = 2;
            cinfo.comp_info[0].v_samp_factor = 1;
            cinfo.comp_info[1].h_samp_factor = 1;
            cinfo.comp_info[1].v_samp_factor = 1;
            cinfo.comp_info[2].h_samp_factor = 1;
            cinfo.comp_info[2].v_samp_factor = 1;
            break;
        }
        case 2:  // 2x2, 1x1, 1x1 (4:1:1) : High
        {
            DDebug() << "Using LibJPEG high chroma-subsampling (4:1:1)" << endl;
            cinfo.comp_info[0].h_samp_factor = 2;
            cinfo.comp_info[0].v_samp_factor = 2;
            cinfo.comp_info[1].h_samp_factor = 1;
            cinfo.comp_info[1].v_samp_factor = 1;
            cinfo.comp_info[2].h_samp_factor = 1;
            cinfo.comp_info[2].v_samp_factor = 1;
            break;
        }
        default:  // 1x1 1x1 1x1 (4:4:4) : None
        {
            DDebug() << "Using LibJPEG none chroma-subsampling (4:4:4)" << endl;
            cinfo.comp_info[0].h_samp_factor = 1;
            cinfo.comp_info[0].v_samp_factor = 1;
            cinfo.comp_info[1].h_samp_factor = 1;
            cinfo.comp_info[1].v_samp_factor = 1;
            cinfo.comp_info[2].h_samp_factor = 1;
            cinfo.comp_info[2].v_samp_factor = 1;
            break;
        }
    }

    jpeg_set_quality(&cinfo, quality, true);
    jpeg_start_compress(&cinfo, true);

    DDebug() << "Using LibJPEG quality compression value: " << quality << endl;

    if (observer)
        observer->progressInfo(m_image, 0.1);

    // -------------------------------------------------------------------
    // Write ICC profil.
    
    TQByteArray profile_rawdata = m_image->getICCProfil();
    
    if (!profile_rawdata.isEmpty())
    {
        write_icc_profile (&cinfo, (JOCTET *)profile_rawdata.data(), profile_rawdata.size());
    }    
        
    if (observer)
        observer->progressInfo(m_image, 0.2);
    
    // -------------------------------------------------------------------
    // Write Image data.
    
    uchar* line       = new uchar[w*3];
    uchar* dstPtr     = 0;
    uint   checkPoint = 0;

    if (!imageSixteenBit())     // 8 bits image.
    {

        uchar* srcPtr = data;

        for (uint j=0; j<h; j++)
        {

            if (observer && j == checkPoint)
            {
                checkPoint += granularity(observer, h, 0.8);
                if (!observer->continueQuery(m_image))
                {
                    delete [] line;
                    jpeg_destroy_compress(&cinfo);
                    fclose(file);
                    return false;
                }
                // use 0-20% for pseudo-progress, now fill 20-100%
                observer->progressInfo(m_image, 0.2 + (0.8 * ( ((float)j)/((float)h) )));
            }

            dstPtr = line;
            
            for (uint i = 0; i < w; i++)
            {
                dstPtr[2] = srcPtr[0];
                dstPtr[1] = srcPtr[1];
                dstPtr[0] = srcPtr[2];
        
                srcPtr += 4;
                dstPtr += 3;
            }

            jpeg_write_scanlines(&cinfo, &line, 1);
        }
    }
    else
    {
        unsigned short* srcPtr = (unsigned short*)data;

        for (uint j=0; j<h; j++)
        {

            if (observer && j == checkPoint)
            {
                checkPoint += granularity(observer, h, 0.8);
                if (!observer->continueQuery(m_image))
                {
                    delete [] line;
                    jpeg_destroy_compress(&cinfo);
                    fclose(file);
                    return false;
                }
                // use 0-20% for pseudo-progress, now fill 20-100%
                observer->progressInfo(m_image, 0.2 + (0.8 * ( ((float)j)/((float)h) )));
            }

            dstPtr = line;
            
            for (uint i = 0; i < w; i++)
            {
                dstPtr[2] = (srcPtr[0] * 255UL)/65535UL;
                dstPtr[1] = (srcPtr[1] * 255UL)/65535UL;
                dstPtr[0] = (srcPtr[2] * 255UL)/65535UL;
        
                srcPtr += 4;
                dstPtr += 3;
            }
    
            jpeg_write_scanlines(&cinfo, &line, 1);
        }
    }

    delete [] line;

    // -------------------------------------------------------------------
    
    jpeg_finish_compress(&cinfo);
    jpeg_destroy_compress(&cinfo);
    fclose(file);
    
    imageSetAttribute("savedformat", "JPEG");
    
    saveMetadata(filePath);
    
    return true;
}

}  // NameSpace Digikam
