/* ============================================================
 *
 * This file is a part of digiKam project
 * http://www.digikam.org
 * 
 * Date        : 2005-04-21
 * Description : a kio-slave to process search on digiKam albums
 *
 * Copyright (C) 2005 by Renchi Raju <renchi@pooh.tam.uiuc.edu>
 *
 * 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 Ansi includes.

extern "C"
{
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>
#include <utime.h>
}

// C++ includes.

#include <cerrno>
#include <cstdlib>
#include <cstdio>
#include <ctime>

// TQt includes.

#include <tqfile.h>
#include <tqdatastream.h>
#include <tqtextstream.h>
#include <tqregexp.h>
#include <tqdir.h>
#include <tqvariant.h>
#include <tqmap.h>

// KDE includes.

#include <kglobal.h>
#include <klocale.h>
#include <kcalendarsystem.h>
#include <kinstance.h>
#include <kfilemetainfo.h>
#include <kmimetype.h>
#include <kdebug.h>
#include <kio/global.h>
#include <kio/ioslave_defaults.h>
#include <klargefile.h>

// Local includes.

#include "digikam_export.h"
#include "digikamsearch.h"

kio_digikamsearch::kio_digikamsearch(const TQCString &pool_socket,
                                     const TQCString &app_socket)
                 : SlaveBase("kio_digikamsearch", pool_socket, app_socket)
{
    // build a lookup table for month names
    const KCalendarSystem* cal = KGlobal::locale()->calendar();
    for (int i=1; i<=12; ++i)
    {
        m_shortMonths[i-1] = cal->monthName(i, 2000, true).lower();
        m_longMonths[i-1]  = cal->monthName(i, 2000, false).lower();
    }
}

kio_digikamsearch::~kio_digikamsearch()
{
}

static TQValueList<TQRegExp> makeFilterList( const TQString &filter )
{
    TQValueList<TQRegExp> regExps;
    if ( filter.isEmpty() )
        return regExps;

    TQChar sep( ';' );
    int i = filter.find( sep, 0 );
    if ( i == -1 && filter.find( ' ', 0 ) != -1 )
        sep = TQChar( ' ' );

    TQStringList list = TQStringList::split( sep, filter );
    TQStringList::Iterator it = list.begin();
    while ( it != list.end() ) 
    {
        regExps << TQRegExp( (*it).stripWhiteSpace(), false, true );
        ++it;
    }
    return regExps;
}

static bool matchFilterList( const TQValueList<TQRegExp>& filters,
                             const TQString &fileName )
{
    TQValueList<TQRegExp>::ConstIterator rit = filters.begin();
    while ( rit != filters.end() ) 
    {
        if ( (*rit).exactMatch(fileName) )
            return true;
        ++rit;
    }
    return false;
}

void kio_digikamsearch::special(const TQByteArray& data)
{
    TQString libraryPath;
    KURL    url;
    TQString filter;
    int     getDimensions;
    int     listingType = 0;
    int     recurseAlbums;
    int     recurseTags;

    TQDataStream ds(data, IO_ReadOnly);
    ds >> libraryPath;
    ds >> url;
    ds >> filter;
    ds >> getDimensions;
    ds >> recurseAlbums;
    ds >> recurseTags;

    if (!ds.atEnd())
        ds >> listingType;

    if (m_libraryPath != libraryPath)
    {
        m_libraryPath = libraryPath;
        m_db.closeDB();
        m_db.openDB(libraryPath);
    }

    TQValueList<TQRegExp> regex = makeFilterList(filter);
    TQByteArray ba;

    if (listingType == 0)
    {
        TQString sqlQuery;

        // query head
        sqlQuery = "SELECT Images.id, Images.name, Images.dirid, Images.datetime, Albums.url "
                   "FROM Images, Albums LEFT JOIN ImageProperties ON Images.id = Imageproperties.imageid "
                   "WHERE ( ";

        // query body
        sqlQuery += buildQuery(url);

        // query tail
        sqlQuery += " ) ";
        sqlQuery += " AND (Albums.id=Images.dirid); ";

        TQStringList values;
        TQString     errMsg;
        if (!m_db.execSql(sqlQuery, &values))
        {
            error(KIO::ERR_INTERNAL, errMsg);
            return;
        }

        TQ_LLONG     imageid;
        TQString     name;
        TQString     path;
        int         dirid;
        TQString     date;
        TQString     purl;
        TQSize       dims;
        struct stat stbuf;

        int  count = 0;
        TQDataStream* os = new TQDataStream(ba, IO_WriteOnly);

        for (TQStringList::iterator it = values.begin(); it != values.end();)
        {
            imageid = (*it).toLongLong();
            ++it;
            name  = *it;
            ++it;
            dirid = (*it).toInt();
            ++it;
            date  = *it;
            ++it;
            purl  = *it;
            ++it;

            if (!matchFilterList(regex, name))
                continue;

            path = m_libraryPath + purl + '/' + name;
            if (::stat(TQFile::encodeName(path), &stbuf) != 0)
                continue;

            dims = TQSize();
            if (getDimensions)
            {
                KFileMetaInfo metaInfo(path);
                if (metaInfo.isValid())
                {
                    if (metaInfo.containsGroup("Jpeg EXIF Data"))
                    {
                        dims = metaInfo.group("Jpeg EXIF Data").
                               item("Dimensions").value().toSize();
                    }
                    else if (metaInfo.containsGroup("General"))
                    {
                        dims = metaInfo.group("General").
                               item("Dimensions").value().toSize();
                    }
                    else if (metaInfo.containsGroup("Technical"))
                    {
                        dims = metaInfo.group("Technical").
                               item("Dimensions").value().toSize();
                    }
                }
            }

            *os << imageid;
            *os << dirid;
            *os << name;
            *os << date;
            *os << static_cast<size_t>(stbuf.st_size);
            *os << dims;

            count++;

            if (count > 200)
            {
                delete os;
                os = 0;

                SlaveBase::data(ba);
                ba.resize(0);

                count = 0;
                os = new TQDataStream(ba, IO_WriteOnly);
            }
        }

        delete os;
    }
    else
    {
        TQString sqlQuery;

        // query head
        sqlQuery = "SELECT Albums.url||'/'||Images.name "
                   "FROM Images, Albums LEFT JOIN ImageProperties on Images.id = ImageProperties.imageid "
                   "WHERE ( ";

        // query body
        sqlQuery += buildQuery(url);

        // query tail
        sqlQuery += " ) ";
        sqlQuery += " AND (Albums.id=Images.dirid) ";
        sqlQuery += " LIMIT 500;";

        TQStringList values;
        TQString     errMsg;
        if (!m_db.execSql(sqlQuery, &values, &errMsg))
        {
            error(KIO::ERR_INTERNAL, errMsg);
            return;
        }

        TQDataStream ds(ba, IO_WriteOnly);
        for (TQStringList::iterator it = values.begin(); it != values.end(); ++it)
        {
            if (matchFilterList(regex, *it))
            {
                ds << m_libraryPath + *it;
            }
        }
    }

    SlaveBase::data(ba);

    finished();
}

TQString kio_digikamsearch::buildQuery(const KURL& url) const
{
    int  count = url.queryItem("count").toInt();
    if (count <= 0)
        return TQString();

    TQMap<int, RuleType> rulesMap;

    for (int i=1; i<=count; i++)
    {
        RuleType rule;

        TQString key = url.queryItem(TQString::number(i) + ".key").lower();
        TQString op  = url.queryItem(TQString::number(i) + ".op").lower();

        if (key == "album")
        {
            rule.key = ALBUM;
        }
        else if (key == "albumname")
        {
            rule.key = ALBUMNAME;
        }
        else if (key == "albumcaption")
        {
            rule.key = ALBUMCAPTION;
        }
        else if (key == "albumcollection")
        {
            rule.key = ALBUMCOLLECTION;
        }
        else if (key == "imagename")
        {
            rule.key = IMAGENAME;
        }
        else if (key == "imagecaption")
        {
            rule.key = IMAGECAPTION;
        }
        else if (key == "imagedate")
        {
            rule.key = IMAGEDATE;
        }
        else if (key == "tag")
        {
            rule.key = TAG;
        }
        else if (key == "tagname")
        {
            rule.key = TAGNAME;
        }
        else if (key == "keyword")
        {
            rule.key = KEYWORD;
        }
        else if (key == "rating")
        {
            rule.key = RATING;
        }
        else
        {
            kdWarning() << "Unknown rule type: " << key << " passed to kioslave"
                        << endl;
            continue;
        }

        if (op == "eq")
            rule.op = EQ;
        else if (op == "ne")
            rule.op = NE;
        else if (op == "lt")
            rule.op = LT;
        else if (op == "lte")
            rule.op = LTE;
        else if (op == "gt")
            rule.op = GT;
        else if (op == "gte")
            rule.op = GTE;
        else if (op == "like")
            rule.op = LIKE;
        else if (op == "nlike")
            rule.op = NLIKE;
        else
        {
            kdWarning() << "Unknown op type: " << op << " passed to kioslave"
                        << endl;
            continue;
        }

        rule.val = url.queryItem(TQString::number(i) + ".val");

        rulesMap.insert(i, rule);
    }

    TQString sqlQuery;

    TQStringList strList = TQStringList::split(" ", url.path());
    for ( TQStringList::Iterator it = strList.begin(); it != strList.end(); ++it )
    {
        bool ok;
        int  num = (*it).toInt(&ok);
        if (ok)
        {
            RuleType rule = rulesMap[num];
            if (rule.key == KEYWORD)
            {
                bool exact;
                TQString possDate = possibleDate(rule.val, exact);
                if (!possDate.isEmpty())
                {
                    rule.key = IMAGEDATE;
                    rule.val = possDate;
                    if (exact)
                    {
                        rule.op = EQ;
                    }
                    else
                    {
                        rule.op = LIKE;
                    }

                    sqlQuery += subQuery(rule.key, rule.op, rule.val);                    
                }
                else
                {
                    TQValueList<SKey> todo;
                    todo.append( ALBUMNAME );
                    todo.append( IMAGENAME );
                    todo.append( TAGNAME );
                    todo.append( ALBUMCAPTION );
                    todo.append( ALBUMCOLLECTION );
                    todo.append( IMAGECAPTION );
                    todo.append( RATING );

                    sqlQuery += '(';
                    TQValueListIterator<SKey> it;
                    it = todo.begin();
                    while ( it != todo.end() )
                    {
                        sqlQuery += subQuery(*it, rule.op, rule.val);
                        ++it;
                        if ( it != todo.end() )
                            sqlQuery += " OR ";
                    }
                    sqlQuery += ')';
                }
            }
            else
            {
                sqlQuery += subQuery(rule.key, rule.op, rule.val);
            }
        }
        else
        {
            sqlQuery += ' ' + *it + ' ';
        }
    }

    return sqlQuery;
}

TQString kio_digikamsearch::subQuery(enum kio_digikamsearch::SKey key,
                                    enum kio_digikamsearch::SOperator op,
                                    const TQString& val) const
{
    TQString query;

    switch (key)
    {
        case(ALBUM):
        {
            if (op == EQ || op == NE)
                query = " (Images.dirid $$##$$ $$@@$$) ";
            else // LIKE AND NLIKE
                query = " (Images.dirid IN "
                        "    (SELECT a.id FROM Albums a, Albums b "
                        "      WHERE a.url $$##$$ '%' || b.url || '%' AND b.id = $$@@$$))";
            query.replace("$$@@$$", TQString::fromLatin1("'") + escapeString(val)
                    + TQString::fromLatin1("'"));
            break;
        }
        case(ALBUMNAME):
        {
            query = " (Images.dirid IN "
                    "  (SELECT id FROM Albums WHERE url $$##$$ $$@@$$)) ";
            break;
        }
        case(ALBUMCAPTION):
        {
            query = " (Images.dirid IN "
                    "  (SELECT id FROM Albums WHERE caption $$##$$ $$@@$$)) ";
            break;
        }
        case(ALBUMCOLLECTION):
        {
            query = " (Images.dirid IN "
                    "  (SELECT id FROM Albums WHERE collection $$##$$ $$@@$$)) ";
            break;
        }
        case(TAG):
        {
            if (op == EQ)
                query = " (Images.id IN "
                        "   (SELECT imageid FROM ImageTags "
                        "    WHERE tagid = $$@@$$)) ";
            else if (op == NE)
                query = " (Images.id NOT IN "
                        "   (SELECT imageid FROM ImageTags "
                        "    WHERE tagid = $$@@$$)) ";
            else if (op == LIKE) 
                query = " (Images.id IN "
                        "   (SELECT ImageTags.imageid FROM ImageTags JOIN TagsTree on ImageTags.tagid = TagsTree.id "
                        "    WHERE TagsTree.pid = $$@@$$ or ImageTags.tagid = $$@@$$ )) ";
            else // op == NLIKE
                query = " (Images.id NOT IN "
                        "   (SELECT ImageTags.imageid FROM ImageTags JOIN TagsTree on ImageTags.tagid = TagsTree.id "
                        "    WHERE TagsTree.pid = $$@@$$ or ImageTags.tagid = $$@@$$ )) ";
    
    //         query = " (Images.id IN "
    //                 "   (SELECT imageid FROM ImageTags "
    //                 "    WHERE tagid $$##$$ $$@@$$)) ";
    
            query.replace("$$@@$$", TQString::fromLatin1("'") + escapeString(val)
                        + TQString::fromLatin1("'"));
    
            break;
        }
        case(TAGNAME):
        {
            if (op == EQ)
                query = " (Images.id IN "
                        "   (SELECT imageid FROM ImageTags "
                        "    WHERE tagid IN "
                        "   (SELECT id FROM Tags WHERE name = $$@@$$))) ";
            else if (op == NE)
                query = " (Images.id NOT IN "
                        "   (SELECT imageid FROM ImageTags "
                        "    WHERE tagid IN "
                        "   (SELECT id FROM Tags WHERE name = $$@@$$))) ";
            else if (op == LIKE)
                query = " (Images.id IN "
                        "   (SELECT ImageTags.imageid FROM ImageTags JOIN TagsTree on ImageTags.tagid = TagsTree.id "
                        "    WHERE TagsTree.pid = (SELECT id FROM Tags WHERE name LIKE $$@@$$) "
                        "    OR ImageTags.tagid = (SELECT id FROM Tags WHERE name LIKE $$@@$$) )) ";
            else // op == NLIKE
                query = " (Images.id NOT IN "
                        "   (SELECT ImageTags.imageid FROM ImageTags JOIN TagsTree on ImageTags.tagid = TagsTree.id "
                        "    WHERE TagsTree.pid = (SELECT id FROM Tags WHERE name LIKE $$@@$$) "
                        "    OR ImageTags.tagid = (SELECT id FROM Tags WHERE name LIKE $$@@$$) )) ";

//             query.replace("$$@@$$", TQString::fromLatin1("'") + escapeString(val)
//                         + TQString::fromLatin1("'"));

            break;
        }
        case(IMAGENAME):
        {
            query = " (Images.name $$##$$ $$@@$$) ";
            break;
        }
        case(IMAGECAPTION):
        {
            query = " (Images.caption $$##$$ $$@@$$) ";
            break;
        }
        case(IMAGEDATE):
        {
            query = " (Images.datetime $$##$$ $$@@$$) ";
            break;
        }
        case (KEYWORD):
        {
            kdWarning() << "KEYWORD Detected which is not possible" << endl;
            break;
        }
        case(RATING):
        {   
            // For searches for `rating=0`, `rating>=0`, `rating<=0`, 
            // `rating <c`, `rating<=c`, `rating<>c` with c=1,2,3,4,5, 
            // special care has to be taken: Images which were never rated  
            // have no ImageProperties.property='Rating', but 
            // need to be treated like having a rating of 0.
            // This is achieved by including all images which do
            // not have property='Rating'.
            if ( (   val=="0" and (op==EQ or op==GTE or op==LTE) )
                 or (val!="0" and (op==LT or op==LTE or op==NE) ) ) {
                query = " ( (ImageProperties.value $$##$$ $$@@$$ and ImageProperties.property='Rating') or (Images.id NOT IN (SELECT imageid FROM ImageProperties WHERE property='Rating') ) )";
            } else {           
                query = " (ImageProperties.value $$##$$ $$@@$$ and ImageProperties.property='Rating') ";
            }
            break;
        }

    }
    
    if (key != TAG) 
    {
        switch (op)
        {
            case(EQ):
            {
                query.replace("$$##$$", "=");
                query.replace("$$@@$$", TQString::fromLatin1("'") + escapeString(val)
                            + TQString::fromLatin1("'"));
                break;
            }
            case(NE):
            {
                query.replace("$$##$$", "<>");
                query.replace("$$@@$$", TQString::fromLatin1("'") + escapeString(val)
                            + TQString::fromLatin1("'"));
                break;
            }
            case(LT):
            {
                query.replace("$$##$$", "<");
                query.replace("$$@@$$", TQString::fromLatin1("'") + escapeString(val)
                            + TQString::fromLatin1("'"));
                break;
            }
            case(GT):
            {
                query.replace("$$##$$", ">");
                query.replace("$$@@$$", TQString::fromLatin1("'") + escapeString(val)
                            + TQString::fromLatin1("'"));
                break;
            }
            case(LTE):
            {
                query.replace("$$##$$", "<=");
                query.replace("$$@@$$", TQString::fromLatin1("'") + escapeString(val)
                            + TQString::fromLatin1("'"));
                break;
            }
            case(GTE):
            {
                query.replace("$$##$$", ">=");
                query.replace("$$@@$$", TQString::fromLatin1("'") + escapeString(val)
                            + TQString::fromLatin1("'"));
                break;
            }
            case(LIKE):
            {
                query.replace("$$##$$", "LIKE");
                query.replace("$$@@$$", TQString::fromLatin1("'%") + escapeString(val)
                            + TQString::fromLatin1("%'"));
                break;
            }
            case(NLIKE):
            {
                query.replace("$$##$$", "NOT LIKE");
                query.replace("$$@@$$", TQString::fromLatin1("'%") + escapeString(val)
                            + TQString::fromLatin1("%'"));
                break;
            }
        }
    }

    // special case for imagedate. If the key is imagedate and the operator is EQ,
    // we need to split it into two rules
    if (key == IMAGEDATE && op == EQ)
    {
        TQDate date = TQDate::fromString(val, Qt::ISODate);
        if (!date.isValid())
            return query;

        query = TQString(" (Images.datetime > '%1' AND Images.datetime < '%2') ")
                .arg(date.addDays(-1).toString(Qt::ISODate))
                .arg(date.addDays( 1).toString(Qt::ISODate));
    }
    
    return query;
}

/* KIO slave registration */

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

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

        kio_digikamsearch slave(argv[2], argv[3]);
        slave.dispatchLoop();

        return 0;
    }
}

TQString kio_digikamsearch::possibleDate(const TQString& str, bool& exact) const
{
    TQDate date = TQDate::fromString(str, Qt::ISODate);
    if (date.isValid())
    {
        exact = true;
        return date.toString(Qt::ISODate);
    }

    exact = false;

    bool ok;
    int num = str.toInt(&ok);
    if (ok)
    {
        // ok. its an int, does it look like a year?
        if (1970 <= num && num <= TQDate::currentDate().year())
        {
            // very sure its a year
            return TQString("%1-%-%").arg(num);
        }
    }
    else
    {
        // hmm... not a year. is it a particular month?
        for (int i=1; i<=12; i++)
        {
            if (str.lower() == m_shortMonths[i-1] ||
                str.lower() == m_longMonths[i-1])
            {
                TQString monGlob;
                monGlob.sprintf("%.2d", i);
                monGlob = "%-" + monGlob + "-%";
                return monGlob;
            }
        }
    }

    return TQString();
}
