/* ============================================================
 *
 * This file is a part of digiKam project
 * http://www.digikam.org
 *
 * Date        : 2003-01-15
 * Description : digiKam KIO slave to get image thumbnails. 
 *               This kio-slave support this freedesktop 
 *               specification about thumbnails mamagement:
 *               http://jens.triq.net/thumbnail-spec
 *
 * Copyright (C) 2003-2005 by Renchi Raju <renchi@pooh.tam.uiuc.edu>
 * Copyright (C) 2003-2009 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
#define PNG_BYTES_TO_CHECK 4

// C++ includes.

#include <cstdlib>
#include <cstdio>
#include <cstring>

// TQt includes.

#include <tqcstring.h>
#include <tqstring.h>
#include <tqimage.h>
#include <tqdatastream.h>
#include <tqfile.h>
#include <tqfileinfo.h>
#include <tqdir.h>
#include <tqwmatrix.h>
#include <tqregexp.h>
#include <tqapplication.h>

// KDE includes.

#include <kdebug.h>
#include <kurl.h>
#include <kinstance.h>
#include <kimageio.h>
#include <klocale.h>
#include <kglobal.h>
#include <kstandarddirs.h>
#include <kmdcodec.h>
#include <ktempfile.h>
#include <ktrader.h>
#include <klibloader.h>
#include <kmimetype.h>
#include <kprocess.h>
#include <kio/global.h>
#include <kio/thumbcreator.h>
#include <kfilemetainfo.h>

// LibKDcraw includes.

#include <libkdcraw/version.h>
#include <libkdcraw/kdcraw.h>

#if KDCRAW_VERSION < 0x000106
#include <libkdcraw/dcrawbinary.h>
#endif

// Local includes.

#include "dimg.h"
#include "dmetadata.h"
#include "jpegutils.h"
#include "digikamthumbnail.h"
#include "digikam_export.h"

// C Ansi includes.

extern "C"
{
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/time.h>
#include <png.h>
}

using namespace KIO;
using namespace Digikam;

kio_digikamthumbnailProtocol::kio_digikamthumbnailProtocol(int argc, char** argv) 
                            : SlaveBase("kio_digikamthumbnail", argv[2], argv[3])
{
    argc_              = argc;
    argv_              = argv;
    app_               = 0;
    digiKamFingerPrint = TQString("Digikam Thumbnail Generator");
    createThumbnailDirs();
}

kio_digikamthumbnailProtocol::~kio_digikamthumbnailProtocol()
{
}

void kio_digikamthumbnailProtocol::get(const KURL& url)
{
    int  size =  metaData("size").toInt();
    bool exif = (metaData("exif") == "yes");

    cachedSize_ = (size <= 128) ? 128 : 256;

    if (cachedSize_ <= 0)
    {
        error(KIO::ERR_INTERNAL, i18n("No or invalid size specified"));
        kdWarning() << "No or invalid size specified" << endl;
        return;
    }

    // generate the thumbnail path
    TQString uri = KURL(url.path()).url();     // "file://" uri
    KMD5 md5( TQFile::encodeName(uri).data() );
    TQString thumbPath = (cachedSize_ == 128) ? smallThumbPath_ : bigThumbPath_;
    thumbPath += TQFile::encodeName( md5.hexDigest() + ".png" ).data();

    TQImage img;
    bool regenerate = true;

    // stat the original file
    struct stat st;
    if (::stat(TQFile::encodeName(url.path(-1)), &st) != 0)
    {
        error(KIO::ERR_INTERNAL, i18n("File does not exist"));
        return;
    }

    // NOTE: if thumbnail have not been generated by digiKam (konqueror for example),
    //       force to recompute it, else we use it.

    img = loadPNG(thumbPath);
    if (!img.isNull())
    {
        if (img.text("Thumb::MTime") == TQString::number(st.st_mtime) &&
            img.text("Software")     == digiKamFingerPrint)
            regenerate = false;
    }

    if (regenerate)
    {
        // To speed-up thumb extraction, we trying to load image using the file extension.
        if ( !loadByExtension(img, url.path()) )
        {
            // Try JPEG loading : JPEG files without using Exif Thumb.
            if ( !loadJPEG(img, url.path()) )
            {
                // Try to load with dcraw : RAW files.
                if (!KDcrawIface::KDcraw::loadDcrawPreview(img, url.path()) )
                {
                    // Try to load with DImg : TIFF, PNG, etc.
                    if (!loadDImg(img, url.path()) )
                    {
                        // Try to load with KDE thumbcreators : video files and others stuff.
                        loadKDEThumbCreator(img, url.path());
                    }
                }
            }
        }

        if (img.isNull())
        {
            error(KIO::ERR_INTERNAL, i18n("Cannot create thumbnail for %1")
                  .tqarg(url.prettyURL()));
            kdWarning() << "Cannot create thumbnail for " << url.path() << endl;
            return;
        }

        if (TQMAX(img.width(),img.height()) != cachedSize_)
            img = img.smoothScale(cachedSize_, cachedSize_, TQ_ScaleMin);

        if (img.depth() != 32)
            img = img.convertDepth(32);

        if (exif)
            exifRotate(url.path(), img);

        img.setText(TQString("Thumb::URI").latin1(),   0, uri);
        img.setText(TQString("Thumb::MTime").latin1(), 0, TQString::number(st.st_mtime));
        img.setText(TQString("Software").latin1(),     0, digiKamFingerPrint);

        KTempFile temp(thumbPath + "-digikam-", ".png");
        if (temp.status() == 0)
        {
            img.save(temp.name(), "PNG", 0);
            ::rename(TQFile::encodeName(temp.name()),
                     TQFile::encodeName(thumbPath));
        }
    }

    img = img.smoothScale(size, size, TQ_ScaleMin);

    if (img.isNull())
    {
        error(KIO::ERR_INTERNAL, "Thumbnail is null");
        return;
    }

    TQByteArray imgData;
    TQDataStream stream( imgData, IO_WriteOnly );

    const TQString shmid = metaData("shmid");
    if (shmid.isEmpty())
    {
        stream << img;
    }
    else
    {
        void *shmaddr = shmat(shmid.toInt(), 0, 0);

        if (shmaddr == (void *)-1)
        {
            error(KIO::ERR_INTERNAL, "Failed to attach to shared memory segment " + shmid);
            kdWarning() << "Failed to attach to shared memory segment " << shmid << endl;
            return;
        }

        if (img.width() * img.height() > cachedSize_ * cachedSize_)
        {
            error(KIO::ERR_INTERNAL, "Image is too big for the shared memory segment");
            kdWarning() << "Image is too big for the shared memory segment" << endl;
            shmdt((char*)shmaddr);
            return;
        }

        stream << img.width() << img.height() << img.depth();
        memcpy(shmaddr, img.bits(), img.numBytes());
        shmdt((char*)shmaddr);
    }

    data(imgData);
    finished();
}

bool kio_digikamthumbnailProtocol::loadByExtension(TQImage& image, const TQString& path)
{
    TQFileInfo fileInfo(path);
    if (!fileInfo.exists())
        return false;

    // Try to use embedded preview image from metadata.
    DMetadata metadata(path);
    if (metadata.getImagePreview(image))
    {
        kdDebug() << "Use Exif/Iptc preview extraction. Size of image: "
                  << image.width() << "x" << image.height() << endl;
        return true;
    }

    // Else, use the right way depending of image file extension.
    TQString ext = fileInfo.extension(false).upper();
#if KDCRAW_VERSION < 0x000106
    TQString rawFilesExt(KDcrawIface::DcrawBinary::instance()->rawFiles());
#else
    TQString rawFilesExt(KDcrawIface::KDcraw::rawFiles());
#endif

    if (!ext.isEmpty())
    {
        if (ext == TQString("JPEG") || ext == TQString("JPG") || ext == TQString("JPE"))
            return (loadJPEG(image, path));
        else if (ext == TQString("PNG"))
            return (loadDImg(image, path));
        else if (ext == TQString("TIFF") || ext == TQString("TIF"))
            return (loadDImg(image, path));
        else if (rawFilesExt.upper().contains(ext))
            return (KDcrawIface::KDcraw::loadDcrawPreview(image, path));
    }

    return false;
}

bool kio_digikamthumbnailProtocol::loadJPEG(TQImage& image, const TQString& path)
{
    return Digikam::loadJPEGScaled(image, path, cachedSize_);
}

void kio_digikamthumbnailProtocol::exifRotate(const TQString& filePath, TQImage& thumb)
{
    // Rotate thumbnail based on metadata orientation information

    DMetadata metadata(filePath);
    DMetadata::ImageOrientation orientation = metadata.getImageOrientation();

    if (orientation == DMetadata::ORIENTATION_NORMAL ||
        orientation == DMetadata::ORIENTATION_UNSPECIFIED)
        return;

    TQWMatrix matrix;

    switch (orientation)
    {
        case DMetadata::ORIENTATION_NORMAL:
        case DMetadata::ORIENTATION_UNSPECIFIED:
            break;

        case DMetadata::ORIENTATION_HFLIP:
            matrix.scale(-1, 1);
            break;

        case DMetadata::ORIENTATION_ROT_180:
            matrix.rotate(180);
            break;

        case DMetadata::ORIENTATION_VFLIP:
            matrix.scale(1, -1);
            break;

        case DMetadata::ORIENTATION_ROT_90_HFLIP:
            matrix.scale(-1, 1);
            matrix.rotate(90);
            break;

        case DMetadata::ORIENTATION_ROT_90:
            matrix.rotate(90);
            break;

        case DMetadata::ORIENTATION_ROT_90_VFLIP:
            matrix.scale(1, -1);
            matrix.rotate(90);
            break;

        case DMetadata::ORIENTATION_ROT_270:
            matrix.rotate(270);
            break;
    }

    // transform accordingly
    thumb = thumb.xForm(matrix);
}

TQImage kio_digikamthumbnailProtocol::loadPNG(const TQString& path)
{
    png_uint_32  w32, h32;
    int          w, h;
    bool         has_alpha;
    bool         has_grey;
    FILE        *f;
    png_structp  png_ptr = NULL;
    png_infop    info_ptr = NULL;
    int          bit_depth, color_type, interlace_type;

    has_alpha = 0;
    has_grey  = 0;

    TQImage qimage;

    f = fopen(path.latin1(), "rb");
    if (!f)
        return qimage;

    unsigned char buf[PNG_BYTES_TO_CHECK];

    fread(buf, 1, PNG_BYTES_TO_CHECK, f);
    if (!png_check_sig(buf, PNG_BYTES_TO_CHECK))
    {
        fclose(f);
        return qimage;
    }
    rewind(f);

    png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
    if (!png_ptr)
    {
        fclose(f);
        return qimage;
    }

    info_ptr = png_create_info_struct(png_ptr);
    if (!info_ptr)
    {
        png_destroy_read_struct(&png_ptr, NULL, NULL);
        fclose(f);
        return qimage;
    }

    if (setjmp(png_ptr->jmpbuf))
    {
        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
        fclose(f);
        return qimage;
    }

    png_init_io(png_ptr, f);
    png_read_info(png_ptr, info_ptr);
    png_get_IHDR(png_ptr, info_ptr, (png_uint_32 *) (&w32),
                 (png_uint_32 *) (&h32), &bit_depth, &color_type,
                 &interlace_type, NULL, NULL);

    w = w32;
    h = h32;

    qimage.create(w, h, 32);

    if (color_type == PNG_COLOR_TYPE_PALETTE)
        png_set_expand(png_ptr);

    if (info_ptr->color_type == PNG_COLOR_TYPE_RGB_ALPHA)
        has_alpha = 1;

    if (info_ptr->color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
    {
        has_alpha = 1;
        has_grey = 1;
    }

    if (info_ptr->color_type == PNG_COLOR_TYPE_GRAY)
        has_grey = 1;

    unsigned char **lines;
    int             i;

    if (has_alpha)
        png_set_expand(png_ptr);

    if (TQImage::systemByteOrder() == TQImage::LittleEndian)
    {
        png_set_filler(png_ptr, 0xff, PNG_FILLER_AFTER);
        png_set_bgr(png_ptr);
    }
    else
    {
        png_set_swap_alpha(png_ptr);
        png_set_filler(png_ptr, 0xff, PNG_FILLER_BEFORE);
    }

    /* 16bit color -> 8bit color */
    if ( bit_depth == 16 )
        png_set_strip_16(png_ptr);

    /* pack all pixels to byte boundaires */

    png_set_packing(png_ptr);
    if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
        png_set_expand(png_ptr);

    lines = (unsigned char **)malloc(h * sizeof(unsigned char *));
    if (!lines)
    {
        png_read_end(png_ptr, info_ptr);
        png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp) NULL);
        fclose(f);
        return qimage;
    }

    if (has_grey)
    {
        png_set_gray_to_rgb(png_ptr);
        if (png_get_bit_depth(png_ptr, info_ptr) < 8)
            png_set_gray_1_2_4_to_8(png_ptr);
    }

    int sizeOfUint = sizeof(unsigned int);
    for (i = 0 ; i < h ; i++)
        lines[i] = ((unsigned char *)(qimage.bits())) + (i * w * sizeOfUint);

    png_read_image(png_ptr, lines);
    free(lines);

    png_textp text_ptr;
    int num_text=0;
    png_get_text(png_ptr,info_ptr,&text_ptr,&num_text);
    while (num_text--) 
    {
        qimage.setText(text_ptr->key,0,text_ptr->text);
        text_ptr++;
    }

    png_read_end(png_ptr, info_ptr);
    png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp) NULL);
    fclose(f);

    return qimage;
}

// -- Load using DImg ---------------------------------------------------------------------

bool kio_digikamthumbnailProtocol::loadDImg(TQImage& image, const TQString& path)
{
    Digikam::DImg dimg_im;

    // to disable raw loader - does not work from ioslave
    dimg_im.setAttribute("noeventloop", true);

    if (!dimg_im.load(path))
    {
        return false;
    }

    image = dimg_im.copyTQImage();
    org_width_  = image.width();
    org_height_ = image.height();

    if ( TQMAX(org_width_, org_height_) != cachedSize_ )
    {
        TQSize sz(dimg_im.width(), dimg_im.height());
        sz.tqscale(cachedSize_, cachedSize_, TQSize::ScaleMin);
        image.scale(sz.width(), sz.height());
    }

    new_width_  = image.width();
    new_height_ = image.height();

    image.setAlphaBuffer(true) ;

    return true;
}

// -- Load using KDE API ---------------------------------------------------------------------

bool kio_digikamthumbnailProtocol::loadKDEThumbCreator(TQImage& image, const TQString& path)
{
    // this sucks royally. some of the thumbcreators need an instance of
    // app running so that they can use pixmap. till they get their 
    // code fixed, we will have to create a qapp instance.
    if (!app_)
        app_ = new TQApplication(argc_, argv_);

    TQString mimeType = KMimeType::findByURL(path)->name();
    if (mimeType.isEmpty())
    {
        kdDebug() << "Mimetype not found" << endl;
        return false;
    }

    TQString mimeTypeAlt = mimeType.replace(TQRegExp("/.*"), "/*");

    TQString plugin;

    KTrader::OfferList plugins = KTrader::self()->query("ThumbCreator");
    for (KTrader::OfferList::ConstIterator it = plugins.begin(); it != plugins.end(); ++it)
    {
        TQStringList mimeTypes = (*it)->property("MimeTypes").toStringList();
        for (TQStringList::ConstIterator mt = mimeTypes.begin(); mt != mimeTypes.end(); ++mt)
        {
            if  ((*mt) == mimeType || (*mt) == mimeTypeAlt)
            {
                plugin=(*it)->library();
                break;
            }
        }

        if (!plugin.isEmpty())
            break;
    }

    if (plugin.isEmpty())
    {
        kdDebug() << "No relevant plugin found " << endl;
        return false;
    }

    KLibrary *library = KLibLoader::self()->library(TQFile::encodeName(plugin));
    if (!library)
    {
        kdDebug() << "Plugin library not found " << plugin << endl;
        return false;
    }

    ThumbCreator *creator = 0;
    newCreator create = (newCreator)library->symbol("new_creator");
    if (create)
        creator = create();

    if (!creator)
    {
        kdDebug() << "Cannot load ThumbCreator " << plugin << endl;
        return false;
    }

    if (!creator->create(path, cachedSize_, cachedSize_, image))
    {
        kdDebug() << "Cannot create thumbnail for " << path << endl;
        delete creator;
        return false;
    }  

    delete creator;
    return true;
}

void kio_digikamthumbnailProtocol::createThumbnailDirs()
{
    TQString path = TQDir::homeDirPath() + "/.thumbnails/";

    smallThumbPath_ = path + "normal/";
    bigThumbPath_   = path + "large/";

    KStandardDirs::makeDir(smallThumbPath_, 0700);
    KStandardDirs::makeDir(bigThumbPath_, 0700);
}

// -- KIO slave registration ---------------------------------------------------------------------

extern "C"
{
    DIGIKAM_EXPORT int kdemain(int argc, char **argv)
    {
        KLocale::setMainCatalogue("digikam");
        KInstance instance( "kio_digikamthumbnail" );
        ( void ) KGlobal::locale();

        if (argc != 4) 
        {
            kdDebug() << "Usage: kio_digikamthumbnail  protocol domain-socket1 domain-socket2"
                      << endl;
            exit(-1);
        }

        KImageIO::registerFormats();

        kio_digikamthumbnailProtocol slave(argc, argv);
        slave.dispatchLoop();

        return 0;
    }
}
