/* ============================================================
 *
 * This file is a part of kipi-plugins project
 * http://www.kipi-plugins.org
 *
 * Date        : 2004-06-08
 * Description : Loss less JPEG files transformations.
 * 
 * Copyright (C) 2004      by  Ralf Hoelzer <kde at ralfhoelzer.com>
 * Copyright (C) 2004-2005 by Marcel Wiesweg <marcel.wiesweg@gmx.de>
 * Copyright (C) 2006-2007 by Gilles Caulier <caulier dot gilles at gmail dot com>
 *
 * NOTE: Do not use kdDebug() in this implementation because 
 *       it will be multithreaded. Use tqDebug() instead. 
 *       See B.K.O #133026 for details.
 *
 * 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.
 * 
 * ============================================================ */

// C++ includes.

#include <cstdio>
#include <cstdlib>

// C Ansi includes.

extern "C" 
{
#include <sys/types.h>
#include <unistd.h>
#include <setjmp.h>
#include <jpeglib.h>
}

// TQt includes.

#include <tqstring.h>
#include <tqwmatrix.h>
#include <tqfile.h>

// KDE includes.

#include <tdelocale.h>
#include <tdetempfile.h>

// Local includes.

#include "pluginsversion.h"
#include "transupp.h"
#include "jpegtransform.h"

namespace KIPIJPEGLossLessPlugin
{

const Matrix Matrix::none                   ( 1,  0,  0,  1);
const Matrix Matrix::rotate90               ( 0, -1,  1,  0);
const Matrix Matrix::rotate180              (-1,  0,  0, -1);
const Matrix Matrix::rotate270              ( 0,  1, -1,  0);
const Matrix Matrix::flipHorizontal         (-1,  0,  0,  1);
const Matrix Matrix::flipVertical           ( 1,  0,  0, -1);
const Matrix Matrix::rotate90flipHorizontal ( 0,  1,  1,  0);
const Matrix Matrix::rotate90flipVertical   ( 0, -1, -1,  0);


// To manage Errors/Warnings handling provide by libjpeg

//#define ENABLE_DEBUG_MESSAGES

struct jpegtransform_jpeg_error_mgr : public jpeg_error_mgr
{
    jmp_buf setjmp_buffer;
};

static void jpegtransform_jpeg_error_exit(j_common_ptr cinfo);
static void jpegtransform_jpeg_emit_message(j_common_ptr cinfo, int msg_level);
static void jpegtransform_jpeg_output_message(j_common_ptr cinfo);

static void jpegtransform_jpeg_error_exit(j_common_ptr cinfo)
{
    jpegtransform_jpeg_error_mgr* myerr = (jpegtransform_jpeg_error_mgr*) cinfo->err;

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

#ifdef ENABLE_DEBUG_MESSAGES
    tqDebug("%s", buffer)
#endif

    longjmp(myerr->setjmp_buffer, 1);
}

static void jpegtransform_jpeg_emit_message(j_common_ptr cinfo, int msg_level)
{
    Q_UNUSED(msg_level)
    char buffer[JMSG_LENGTH_MAX];
    (*cinfo->err->format_message)(cinfo, buffer);

#ifdef ENABLE_DEBUG_MESSAGES
    tqDebug("%s (%i)", buffer, msg_level);
#endif
}

static void jpegtransform_jpeg_output_message(j_common_ptr cinfo)
{
    char buffer[JMSG_LENGTH_MAX];
    (*cinfo->err->format_message)(cinfo, buffer);

#ifdef ENABLE_DEBUG_MESSAGES
    tqDebug("%s", buffer)
#endif
}

bool transformJPEG(const TQString& src, const TQString& destGiven,
                   Matrix &userAction, TQString& err)
{
    //may be modified
    TQString dest(destGiven);   
    
    JCOPY_OPTION copyoption = JCOPYOPT_ALL;
    jpeg_transform_info transformoption;
    memset(&transformoption, 0, sizeof(jpeg_transform_info));

    transformoption.force_grayscale = false;
    transformoption.trim            = false;

    struct jpeg_decompress_struct srcinfo;
    struct jpeg_compress_struct dstinfo;
    struct jpegtransform_jpeg_error_mgr jsrcerr, jdsterr;
    jvirt_barray_ptr * src_coef_arrays;
    jvirt_barray_ptr * dst_coef_arrays;

    Matrix      exifAction, action;
    JXFORM_CODE flip, rotate;

    // Initialize the JPEG decompression object with default error handling
    srcinfo.err                 = jpeg_std_error(&jsrcerr);
    srcinfo.err->error_exit     = jpegtransform_jpeg_error_exit;
    srcinfo.err->emit_message   = jpegtransform_jpeg_emit_message;
    srcinfo.err->output_message = jpegtransform_jpeg_output_message;

    // Initialize the JPEG compression object with default error handling
    dstinfo.err                 = jpeg_std_error(&jdsterr);
    dstinfo.err->error_exit     = jpegtransform_jpeg_error_exit;
    dstinfo.err->emit_message   = jpegtransform_jpeg_emit_message;
    dstinfo.err->output_message = jpegtransform_jpeg_output_message;

    FILE *input_file;
    FILE *output_file;

    input_file = fopen(TQFile::encodeName(src), "rb");
    if (!input_file)
    {
        tqDebug("ImageRotate/ImageFlip: Error in opening input file");
        err = i18n("Error in opening input file");
        return false;
    }

    output_file = fopen(TQFile::encodeName(dest), "wb");
    if (!output_file)
    {
        fclose(input_file);
        tqDebug("ImageRotate/ImageFlip: Error in opening output file");
        err = i18n("Error in opening output file");
        return false;
    }

    if (setjmp(jsrcerr.setjmp_buffer) || setjmp(jdsterr.setjmp_buffer))
    {
        jpeg_destroy_decompress(&srcinfo);
        jpeg_destroy_compress(&dstinfo);
        fclose(input_file);
        fclose(output_file);
        return false;
    }

    jpeg_create_decompress(&srcinfo);
    jpeg_create_compress(&dstinfo);

    jpeg_stdio_src(&srcinfo, input_file);
    jcopy_markers_setup(&srcinfo, copyoption);

    (void) jpeg_read_header(&srcinfo, true);

    // Get Exif orientation action to do.
    KExiv2Iface::KExiv2 exiv2Iface;
    exiv2Iface.load(src);
    getExifAction(exifAction, exiv2Iface.getImageOrientation());

    // Compose actions: first exif, then user
    action*=exifAction;
    action*=userAction;

    // Convert action into flip+rotate action
    convertTransform(action, flip, rotate);
    tqDebug("Transforming with option %i %i", flip, rotate);
    if (flip == JXFORM_NONE && rotate == JXFORM_NONE)
    {
        err = "nothing to do"; // magic string
        fclose(output_file);
        fclose(input_file);
        return false;
    }

    bool twoPass = (flip != JXFORM_NONE);

    // If twoPass is true, we need another file (src -> tempFile -> destGiven)
    if (twoPass) 
    {
        KTempFile tempFile;
        tempFile.setAutoDelete(false);
        dest=tempFile.name();
    }

    output_file = fopen(TQFile::encodeName(dest), "wb");
    if (!output_file)
    {
        fclose(input_file);
        tqDebug("ImageRotate/ImageFlip: Error in opening output file");
        err = i18n("Error in opening output file");
        return false;
    }

    // First rotate - execute even if rotate is JXFORM_NONE to apply new EXIF settings
    transformoption.transform=rotate;

    jtransform_request_workspace(&srcinfo, &transformoption);

     // Read source file as DCT coefficients
    src_coef_arrays = jpeg_read_coefficients(&srcinfo);

    // Initialize destination compression parameters from source values
    jpeg_copy_critical_parameters(&srcinfo, &dstinfo);

    dst_coef_arrays = jtransform_adjust_parameters(&srcinfo, &dstinfo,
                                                   src_coef_arrays, &transformoption);

    // Specify data destination for compression
    jpeg_stdio_dest(&dstinfo, output_file);
    
    // Do not write a JFIF header if previously the image did not contain it
    dstinfo.write_JFIF_header = false;

    // Start compressor (note no image data is actually written here)
    jpeg_write_coefficients(&dstinfo, dst_coef_arrays);

    // Copy to the output file any extra markers that we want to preserve
    jcopy_markers_execute(&srcinfo, &dstinfo, copyoption);

    jtransform_execute_transformation(&srcinfo, &dstinfo,
                                      src_coef_arrays, &transformoption);

    // Finish compression and release memory
    jpeg_finish_compress(&dstinfo);
    jpeg_destroy_compress(&dstinfo);
    (void) jpeg_finish_decompress(&srcinfo);
    jpeg_destroy_decompress(&srcinfo);

    fclose(input_file);
    fclose(output_file);

    // Flip if needed
    if (twoPass) 
    {
        // Initialize the JPEG decompression object with default error handling
        srcinfo.err = jpeg_std_error(&jsrcerr);
        jpeg_create_decompress(&srcinfo);

        // Initialize the JPEG compression object with default error handling
        dstinfo.err = jpeg_std_error(&jdsterr);
        jpeg_create_compress(&dstinfo);

        input_file = fopen(TQFile::encodeName(dest), "rb");
        if (!input_file)
        {
            tqDebug("ImageRotate/ImageFlip: Error in opening input file");
            err = i18n("Error in opening input file");
            return false;
        }

        output_file = fopen(TQFile::encodeName(destGiven), "wb");
        if (!output_file)
        {
            fclose(input_file);
            tqDebug("ImageRotate/ImageFlip: Error in opening output file");
            err = i18n("Error in opening output file");
            return false;
        }

        jpeg_stdio_src(&srcinfo, input_file);
        jcopy_markers_setup(&srcinfo, copyoption);

        (void) jpeg_read_header(&srcinfo, true);

        transformoption.transform=flip;
        jtransform_request_workspace(&srcinfo, &transformoption);

        // Read source file as DCT coefficients
        src_coef_arrays = jpeg_read_coefficients(&srcinfo);

        // Initialize destination compression parameters from source values
        jpeg_copy_critical_parameters(&srcinfo, &dstinfo);

        dst_coef_arrays = jtransform_adjust_parameters(&srcinfo,
                &dstinfo,
                src_coef_arrays,
                &transformoption);

        // Specify data destination for compression
        jpeg_stdio_dest(&dstinfo, output_file);

        // Do not write a JFIF header if previously the image did not contain it
        dstinfo.write_JFIF_header = false;

        // Start compressor (note no image data is actually written here)
        jpeg_write_coefficients(&dstinfo, dst_coef_arrays);

        // Copy to the output file any extra markers that we want to preserve
        jcopy_markers_execute(&srcinfo, &dstinfo, copyoption);

        jtransform_execute_transformation(&srcinfo, &dstinfo,
                                          src_coef_arrays, &transformoption);

        // Finish compression and release memory
        jpeg_finish_compress(&dstinfo);
        jpeg_destroy_compress(&dstinfo);
        (void) jpeg_finish_decompress(&srcinfo);
        jpeg_destroy_decompress(&srcinfo);

        fclose(input_file);
        fclose(output_file);

        // Unlink temp file
        unlink(TQFile::encodeName(dest));
    }

    // And set finaly update the metadata to target file.

    TQImage img(destGiven);
    TQImage exifThumbnail = img.scale(160, 120, TQImage::ScaleMin);
    exiv2Iface.load(destGiven);
    exiv2Iface.setImageOrientation(KExiv2Iface::KExiv2::ORIENTATION_NORMAL);
    exiv2Iface.setImageProgramId(TQString("Kipi-plugins"), TQString(kipiplugins_version));
    exiv2Iface.setImageDimensions(img.size());
    exiv2Iface.setExifThumbnail(exifThumbnail);
    exiv2Iface.save(destGiven);

    return true;
}

/** Converts the mathematically correct description
    into the primitive operations that can be carried out losslessly.
*/
void convertTransform(Matrix &action, JXFORM_CODE &flip, JXFORM_CODE &rotate) 
{
    flip   = JXFORM_NONE;
    rotate = JXFORM_NONE;
    
    if (action == Matrix::rotate90) 
    {
        rotate = JXFORM_ROT_90;
    }
    else if (action == Matrix::rotate180) 
    {
        rotate = JXFORM_ROT_180;
    }
    else if (action == Matrix::rotate270) 
    {
        rotate = JXFORM_ROT_270;
    }
    else if (action == Matrix::flipHorizontal) 
    {
        flip = JXFORM_FLIP_H;
    }
    else if (action == Matrix::flipVertical) 
    {
        flip = JXFORM_FLIP_V;
    }
    else if (action == Matrix::rotate90flipHorizontal) 
    {
        //first rotate, then flip!
        rotate = JXFORM_ROT_90;
        flip   = JXFORM_FLIP_H;
    }
    else if (action == Matrix::rotate90flipVertical) 
    {
        //first rotate, then flip!
        rotate = JXFORM_ROT_90;
        flip   = JXFORM_FLIP_V;
    }
}

void getExifAction(Matrix &action, KExiv2Iface::KExiv2::ImageOrientation exifOrientation) 
{
    switch (exifOrientation) 
    {
        case KExiv2Iface::KExiv2::ORIENTATION_NORMAL:
            break;

        case KExiv2Iface::KExiv2::ORIENTATION_HFLIP:
            action*=Matrix::flipHorizontal;
            break;

        case KExiv2Iface::KExiv2::ORIENTATION_ROT_180:
            action*=Matrix::rotate180;
            break;

        case KExiv2Iface::KExiv2::ORIENTATION_VFLIP:
            action*=Matrix::flipVertical;
            break;

        case KExiv2Iface::KExiv2::ORIENTATION_ROT_90_HFLIP:
            action*=Matrix::rotate90flipHorizontal;
            break;

        case KExiv2Iface::KExiv2::ORIENTATION_ROT_90:
            action*=Matrix::rotate90;
            break;

        case KExiv2Iface::KExiv2::ORIENTATION_ROT_90_VFLIP:
            action*=Matrix::rotate90flipVertical;
            break;

        case KExiv2Iface::KExiv2::ORIENTATION_ROT_270:
            action*=Matrix::rotate270;
            break;

        case KExiv2Iface::KExiv2::ORIENTATION_UNSPECIFIED:
            action*=Matrix::none;
            break;
    }
}

}  // NameSpace KIPIJPEGLossLessPlugin
