/* ============================================================
 *
 * This file is a part of digiKam project
 * http://www.digikam.org
 *
 * Date        : 2004-06-26
 * Description : Albums lister.
 *
 * Copyright (C) 2004-2005 by Renchi Raju <renchi@pooh.tam.uiuc.edu>
 * Copyright (C) 2007-2009 by Gilles Caulier <caulier dot gilles at gmail dot com>
 * Copyright (C) 2007 by Arnd Baecker <arnd dot baecker at web dot de>
 *
 * 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 <sys/time.h>
}

// C++ includes.

#include <cstdio>
#include <ctime>

// TQt includes.

#include <tqstring.h>
#include <tqcstring.h>
#include <tqdatastream.h>
#include <tqfileinfo.h>
#include <tqdir.h>
#include <tqmap.h>
#include <tqpair.h>
#include <tqvaluelist.h>
#include <tqtimer.h>

// KDE includes.

#include <tdeapplication.h>
#include <kcursor.h>
#include <tdeio/job.h>
#include <kurl.h>

// Local includes.

#include "ddebug.h"
#include "mimefilter.h"
#include "album.h"
#include "albummanager.h"
#include "albumsettings.h"
#include "albumlister.h"
#include "albumlister.moc"

namespace Digikam
{

class AlbumListerPriv
{
public:

    AlbumListerPriv()
    {
        untaggedFilter = false;
        ratingFilter   = 0;
        filterTimer    = 0;
        job            = 0;
        currAlbum      = 0;
        namesFilter    = "*";
        mimeTypeFilter = MimeFilter::AllFiles;
        ratingCond     = AlbumLister::GreaterEqualCondition;
        matchingCond   = AlbumLister::OrCondition;
        recurseAlbums  = false;
        recurseTags    = false;
    }

    bool                            untaggedFilter;

    int                             ratingFilter;
    int                             recurseAlbums;
    int                             recurseTags;

    TQString                         namesFilter;
    TQString                         textFilter;

    TQMap<TQ_LLONG, ImageInfo*>       itemMap;
    TQMap<int, int>                  invalidatedItems;
    TQMap<TQDateTime, bool>           dayFilter;

    TQValueList<int>                 tagFilter;

    TQTimer                         *filterTimer;

    TDEIO::TransferJob               *job;

    ImageInfoList                   itemList;

    Album                          *currAlbum;

    MimeFilter::TypeMimeFilter      mimeTypeFilter;

    AlbumLister::MatchingCondition  matchingCond;

    AlbumLister::RatingCondition    ratingCond;
};

AlbumLister* AlbumLister::m_instance = 0;

AlbumLister* AlbumLister::instance()
{
    if (!m_instance)
        new AlbumLister();

    return m_instance;
}

AlbumLister::AlbumLister()
{
    m_instance = this;

    d = new AlbumListerPriv;
    d->itemList.setAutoDelete(true);
    d->filterTimer = new TQTimer(this);

    connect(d->filterTimer, TQ_SIGNAL(timeout()),
            this, TQ_SLOT(slotFilterItems()));
}

AlbumLister::~AlbumLister()
{
    delete d->filterTimer;
    delete d;
    m_instance = 0;
}

void AlbumLister::openAlbum(Album *album)
{
    d->currAlbum = album;
    d->filterTimer->stop();
    emit signalClear();
    d->itemList.clear();
    d->itemMap.clear();

    if (d->job)
    {
        d->job->kill();
        d->job = 0;
    }

    if (!album)
        return;

    TQByteArray ba;
    TQDataStream ds(ba, IO_WriteOnly);
    ds << AlbumManager::instance()->getLibraryPath();
    ds << album->kurl();
    ds << d->namesFilter;
    ds << AlbumSettings::instance()->getIconShowResolution();
    ds << d->recurseAlbums;
    ds << d->recurseTags;

    // Protocol = digikamalbums -> tdeio_digikamalbums
    d->job = new TDEIO::TransferJob(album->kurl(), TDEIO::CMD_SPECIAL,
                                  ba, TQByteArray(), false);

    connect(d->job, TQ_SIGNAL(result(TDEIO::Job*)),
            this, TQ_SLOT(slotResult(TDEIO::Job*)));

    connect(d->job, TQ_SIGNAL(data(TDEIO::Job*, const TQByteArray&)),
            this, TQ_SLOT(slotData(TDEIO::Job*, const TQByteArray&)));
}

void AlbumLister::refresh()
{
    if (!d->currAlbum)
        return;

    d->filterTimer->stop();

    if (d->job)
    {
        d->job->kill();
        d->job = 0;
    }

    d->itemMap.clear();
    ImageInfo* item;
    for (ImageInfoListIterator it(d->itemList); (item = it.current()); ++it)
    {
        d->itemMap.insert(item->id(), item);
    }

    TQByteArray ba;
    TQDataStream ds(ba, IO_WriteOnly);
    ds << AlbumManager::instance()->getLibraryPath();
    ds << d->currAlbum->kurl();
    ds << d->namesFilter;
    ds << AlbumSettings::instance()->getIconShowResolution();
    ds << d->recurseAlbums;
    ds << d->recurseTags;

    d->job = new TDEIO::TransferJob(d->currAlbum->kurl(), TDEIO::CMD_SPECIAL,
                                  ba, TQByteArray(), false);

    connect(d->job, TQ_SIGNAL(result(TDEIO::Job*)),
            this, TQ_SLOT(slotResult(TDEIO::Job*)));

    connect(d->job, TQ_SIGNAL(data(TDEIO::Job*, const TQByteArray&)),
            this, TQ_SLOT(slotData(TDEIO::Job*, const TQByteArray&)));
}

void AlbumLister::setDayFilter(const TQValueList<TQDateTime>& days)
{
    d->dayFilter.clear();

    for (TQValueList<TQDateTime>::const_iterator it = days.begin(); it != days.end(); ++it)
        d->dayFilter.insert(*it, true);

    d->filterTimer->start(100, true);
}

bool AlbumLister::tagFiltersIsActive()
{
    if (!d->tagFilter.isEmpty() || d->untaggedFilter)
        return true;

    return false;
}

void AlbumLister::setTagFilter(const TQValueList<int>& tags, const MatchingCondition& matchingCond,
                               bool showUnTagged)
{
    d->tagFilter      = tags;
    d->matchingCond   = matchingCond;
    d->untaggedFilter = showUnTagged;
    d->filterTimer->start(100, true);
}

void AlbumLister::setRatingFilter(int rating, const RatingCondition& ratingCond)
{
    d->ratingFilter = rating;
    d->ratingCond   = ratingCond;
    d->filterTimer->start(100, true);
}

void AlbumLister::setMimeTypeFilter(int mimeTypeFilter)
{
    d->mimeTypeFilter = (MimeFilter::TypeMimeFilter)mimeTypeFilter;
    d->filterTimer->start(100, true);
}

void AlbumLister::setTextFilter(const TQString& text)
{
    d->textFilter = text;
    d->filterTimer->start(100, true);
}

void AlbumLister::setRecurseAlbums(bool recursive)
{
    d->recurseAlbums = recursive;
    refresh();
}

void AlbumLister::setRecurseTags(bool recursive)
{
    d->recurseTags = recursive;
    refresh();
}

bool AlbumLister::matchesFilter(const ImageInfo* info, bool &foundText)
{
    if (d->dayFilter.isEmpty() && d->tagFilter.isEmpty() && d->textFilter.isEmpty() &&
        !d->untaggedFilter && d->ratingFilter==-1)
        return true;

    bool match = false;

    if (!d->tagFilter.isEmpty())
    {
        TQValueList<int> tagIDs = info->tagIDs();
        TQValueList<int>::iterator it;

        if (d->matchingCond == OrCondition)
        {
            for (it = d->tagFilter.begin(); it != d->tagFilter.end(); ++it)
            {
                if (tagIDs.contains(*it))
                {
                    match = true;
                    break;
                }
            }
        }
        else
        {
            // AND matching condition...

            for (it = d->tagFilter.begin(); it != d->tagFilter.end(); ++it)
            {
                if (!tagIDs.contains(*it))
                    break;
            }

            if (it == d->tagFilter.end())
                match = true;
        }

        match |= (d->untaggedFilter && tagIDs.isEmpty());
    }
    else if (d->untaggedFilter)
    {
        match = info->tagIDs().isEmpty();
    }
    else
    {
        match = true;
    }

    if (!d->dayFilter.isEmpty())
    {
        match &= d->dayFilter.contains(TQDateTime(info->dateTime().date(), TQTime()));
    }

    //-- Filter by rating ---------------------------------------------------------

    if (d->ratingFilter >= 0)
    {
        if (d->ratingCond == GreaterEqualCondition)
        {
            // If the rating is not >=, i.e it is <, then it does not match.
            if (info->rating() < d->ratingFilter)
            {
                match = false;
            }
        }
        else if (d->ratingCond == EqualCondition)
        {
            // If the rating is not =, i.e it is !=, then it does not match.
            if (info->rating() != d->ratingFilter)
            {
                match = false;
            }
        }
        else
        {
            // If the rating is not <=, i.e it is >, then it does not match.
            if (info->rating() > d->ratingFilter)
            {
                match = false;
            }
        }
    }

    // -- Filter by mime type -----------------------------------------------------

    TQFileInfo fi(info->filePath());
    TQString mimeType = fi.extension(false).upper();

    switch(d->mimeTypeFilter)
    {
        case MimeFilter::ImageFiles:
        {
            TQString imageFilesExt(AlbumSettings::instance()->getImageFileFilter());
            imageFilesExt.append(AlbumSettings::instance()->getRawFileFilter());
            if (!imageFilesExt.upper().contains(mimeType))
                match = false;
            break;
        }
        case MimeFilter::JPGFiles:
        {
            if (mimeType != TQString("JPG") && mimeType != TQString("JPE") &&
                mimeType != TQString("JPEG"))
                match = false;
            break;
        }
        case MimeFilter::PNGFiles:
        {
            if (mimeType != TQString("PNG"))
                match = false;
            break;
        }
        case MimeFilter::TIFFiles:
        {
            if (mimeType != TQString("TIF") && mimeType != TQString("TIFF"))
                match = false;
            break;
        }
        case MimeFilter::NoRAWFiles:
        {
            TQString rawFilesExt(AlbumSettings::instance()->getRawFileFilter());
            if (rawFilesExt.upper().contains(mimeType))
                match = false;
            break;
        }
        case MimeFilter::RAWFiles:
        {
            TQString rawFilesExt(AlbumSettings::instance()->getRawFileFilter());
            if (!rawFilesExt.upper().contains(mimeType))
                match = false;
            break;
        }
        case MimeFilter::MoviesFiles:
        {
            TQString moviesFilesExt(AlbumSettings::instance()->getMovieFileFilter());
            if (!moviesFilesExt.upper().contains(mimeType))
                match = false;
            break;
        }
        case MimeFilter::AudioFiles:
        {
            TQString audioFilesExt(AlbumSettings::instance()->getAudioFileFilter());
            if (!audioFilesExt.upper().contains(mimeType))
                match = false;
            break;
        }
        default:        // All Files: do nothing...
            break;
    }

    //-- Filter by text -----------------------------------------------------------

    if (!d->textFilter.isEmpty())
    {
        foundText = false;
        if (info->name().lower().contains(d->textFilter.lower()))
        {
            foundText = true;
        }
        if (info->caption().lower().contains(d->textFilter.lower()))
            foundText = true;
        TQStringList tags = info->tagNames();
        for (TQStringList::const_iterator it = tags.constBegin() ; it != tags.constEnd() ; ++it)
        {
            if ((*it).lower().contains(d->textFilter.lower()))
                foundText = true;
        }
        // check for folder names
        PAlbum* palbum = AlbumManager::instance()->findPAlbum(info->albumID());
        if ((palbum && palbum->title().lower().contains(d->textFilter.lower())))
        {
            foundText = true;
        }
        match &= foundText;
    }

    return match;
}

void AlbumLister::stop()
{
    d->currAlbum = 0;
    d->filterTimer->stop();
    emit signalClear();

    d->itemList.clear();
    d->itemMap.clear();

    if (d->job)
    {
        d->job->kill();
        d->job = 0;
    }
}

void AlbumLister::setNamesFilter(const TQString& namesFilter)
{
    d->namesFilter = namesFilter;
}

void AlbumLister::invalidateItem(const ImageInfo *item)
{
    d->invalidatedItems.insert(item->id(), item->id());
}

void AlbumLister::slotFilterItems()
{
    if (d->job)
    {
        d->filterTimer->start(100, true);
        return;
    }

    TQPtrList<ImageInfo> newFilteredItemsList;
    TQPtrList<ImageInfo> deleteFilteredItemsList;
    ImageInfo *item   = 0;
    bool matchForText = false;
    bool match        = false;

    for (ImageInfoListIterator it(d->itemList);
         (item = it.current()); ++it)
    {
        bool foundText = false;
        if (matchesFilter(item, foundText))
        {
            match = true;
            if (!item->getViewItem())
                newFilteredItemsList.append(item);
        }
        else
        {
            if (item->getViewItem())
                deleteFilteredItemsList.append(item);
        }

        if (foundText)
            matchForText = true;
    }

    // This takes linear time - and deleting seems to take longer. Set wait cursor for large numbers.
    bool setCursor = (3*deleteFilteredItemsList.count() + newFilteredItemsList.count()) > 1500;
    if (setCursor)
        kapp->setOverrideCursor(KCursor::waitCursor());

    emit signalItemsTextFilterMatch(matchForText);
    emit signalItemsFilterMatch(match);

    if (!deleteFilteredItemsList.isEmpty())
    {
        for (ImageInfo *info=deleteFilteredItemsList.first(); info; info = deleteFilteredItemsList.next())
            emit signalDeleteFilteredItem(info);
    }
    if (!newFilteredItemsList.isEmpty())
    {
        emit signalNewFilteredItems(newFilteredItemsList);
    }

    if (setCursor)
        kapp->restoreOverrideCursor();
}

void AlbumLister::slotResult(TDEIO::Job* job)
{
    d->job = 0;

    if (job->error())
    {
        DWarning() << "Failed to list url: " << job->errorString() << endl;
        d->itemMap.clear();
        d->invalidatedItems.clear();
        return;
    }

    typedef TQMap<TQ_LLONG, ImageInfo*> ImMap;

    for (ImMap::iterator it = d->itemMap.begin();
         it != d->itemMap.end(); ++it)
    {
        emit signalDeleteItem(it.data());
        emit signalDeleteFilteredItem(it.data());
        d->itemList.remove(it.data());
    }

    d->itemMap.clear();
    d->invalidatedItems.clear();

    emit signalCompleted();
}

void AlbumLister::slotData(TDEIO::Job*, const TQByteArray& data)
{
    if (data.isEmpty())
        return;

    TQ_LLONG imageID;
    int     albumID;
    TQString name;
    TQString date;
    size_t  size;
    TQSize   dims;

    ImageInfoList newItemsList;
    ImageInfoList newFilteredItemsList;

    TQDataStream ds(data, IO_ReadOnly);

    while (!ds.atEnd())
    {
        bool foundText = false;

        ds >> imageID;
        ds >> albumID;
        ds >> name;
        ds >> date;
        ds >> size;
        ds >> dims;

        if (d->itemMap.contains(imageID))
        {
            ImageInfo* info = d->itemMap[imageID];
            d->itemMap.remove(imageID);

            if (d->invalidatedItems.contains(imageID))
            {
                emit signalDeleteItem(info);
                emit signalDeleteFilteredItem(info);
                d->itemList.remove(info);
            }
            else
            {
                if (!matchesFilter(info, foundText))
                {
                    emit signalDeleteFilteredItem(info);
                }
                continue;
            }
        }

        ImageInfo* info = new ImageInfo(imageID, albumID, name,
                                        TQDateTime::fromString(date, TQt::ISODate),
                                        size, dims);

        if (matchesFilter(info, foundText))
            newFilteredItemsList.append(info);

        newItemsList.append(info);
        d->itemList.append(info);
    }

    if (!newFilteredItemsList.isEmpty())
        emit signalNewFilteredItems(newFilteredItemsList);

    if (!newItemsList.isEmpty())
        emit signalNewItems(newItemsList);

    slotFilterItems();
}

}  // namespace Digikam
