/* ============================================================
 *
 * This file is a part of digiKam project
 * http://www.digikam.org
 * 
 * Date        : 2004-06-15
 * Description : Albums manager interface.
 * 
 * Copyright (C) 2004-2005 by Renchi Raju <renchi@pooh.tam.uiuc.edu>
 * Copyright (C) 2006-2008 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.
 * 
 * ============================================================ */

#include <config.h>

// C Ansi includes.

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

// C++ includes.

#include <clocale>
#include <cstdlib>
#include <cstdio>
#include <cerrno> 

// TQt includes.

#include <tqfile.h>
#include <tqdir.h>
#include <tqdict.h>
#include <tqintdict.h>
#include <tqcstring.h>
#include <tqtextcodec.h>
#include <tqdatetime.h>

// KDE includes.

#include <kconfig.h>
#include <klocale.h>
#include <kdeversion.h>
#include <kmessagebox.h>
#include <kstandarddirs.h>
#include <kio/netaccess.h>
#include <kio/global.h>
#include <kio/job.h>
#include <kdirwatch.h>

// Local includes.

#include "ddebug.h"
#include "album.h"
#include "albumdb.h"
#include "albumitemhandler.h"
#include "dio.h"
#include "albumsettings.h"
#include "scanlib.h"
#include "splashscreen.h"
#include "upgradedb_sqlite2tosqlite3.h"
#include "albummanager.h"
#include "albummanager.moc"

namespace Digikam
{

typedef TQDict<PAlbum>    PAlbumDict;
typedef TQIntDict<Album>  AlbumIntDict;
typedef TQValueList<TQDateTime> DDateList;

class AlbumManagerPriv
{

public:

    AlbumManagerPriv()
    {
        db            = 0;
        dateListJob   = 0;
        albumListJob  = 0;
        tagListJob    = 0;
        rootPAlbum    = 0;
        rootTAlbum    = 0;
        rootDAlbum    = 0;
        rootSAlbum    = 0;
        itemHandler   = 0;
        currentAlbum  = 0;
        dirWatch      = 0;
        changed       = false;
    }
    
    bool              changed;
    
    TQString           libraryPath;
    TQStringList       dirtyAlbums;

    DDateList         dbPathModificationDateList;

    KDirWatch        *dirWatch;

    KIO::TransferJob *albumListJob;
    KIO::TransferJob *dateListJob;
    KIO::TransferJob *tagListJob;

    PAlbum           *rootPAlbum;
    TAlbum           *rootTAlbum;
    DAlbum           *rootDAlbum;
    SAlbum           *rootSAlbum;

    PAlbumDict        pAlbumDict;
    AlbumIntDict      albumIntDict;

    Album            *currentAlbum;
    AlbumDB          *db;
    AlbumItemHandler *itemHandler;

    TQValueList<TQDateTime> buildDirectoryModList(const TQFileInfo &dbFile)
    {
        // retrieve modification dates of all files in the database-file dir
        TQValueList<TQDateTime> modList;
        const TQFileInfoList *fileInfoList = dbFile.dir().entryInfoList(TQDir::Files | TQDir::Dirs );

        // build list
        TQFileInfoListIterator it(*fileInfoList);
        TQFileInfo *fi;

        while ( (fi = it.current()) != 0 )
        {
            if ( fi->fileName() != dbFile.fileName())
            {
                modList << fi->lastModified();
            }
            ++it;
        }

        return modList;
    }

};

AlbumManager* AlbumManager::m_instance = 0;

AlbumManager* AlbumManager::instance()
{
    return m_instance;
}

AlbumManager::AlbumManager()
{
    m_instance = this;

    d = new AlbumManagerPriv;
    d->db = new AlbumDB;
}

AlbumManager::~AlbumManager()
{
    if (d->dateListJob)
    {
        d->dateListJob->kill();
        d->dateListJob = 0;
    }
    
    if (d->albumListJob)
    {
        d->albumListJob->kill();
        d->albumListJob = 0;
    }
    
    if (d->tagListJob)
    {
        d->tagListJob->kill();
        d->tagListJob = 0;
    }

    delete d->rootPAlbum;
    delete d->rootTAlbum;
    delete d->rootDAlbum;
    delete d->rootSAlbum;

    delete d->dirWatch;
    
    delete d->db;
    delete d;

    m_instance = 0;
}

AlbumDB* AlbumManager::albumDB()
{
    return d->db;    
}

void AlbumManager::setLibraryPath(const TQString& path, SplashScreen *splash)
{
    TQString cleanPath = TQDir::cleanDirPath(path);
    
    if (cleanPath == d->libraryPath)
        return;

    d->changed = true;
    
    if (d->dateListJob)
    {
        d->dateListJob->kill();
        d->dateListJob = 0;
    }

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

    delete d->dirWatch;
    d->dirWatch = 0;
    d->dirtyAlbums.clear();
    
    d->currentAlbum = 0;
    emit signalAlbumCurrentChanged(0);
    emit signalAlbumsCleared();
    
    d->pAlbumDict.clear();
    d->albumIntDict.clear();

    delete d->rootPAlbum;
    delete d->rootTAlbum;
    delete d->rootDAlbum;
    
    d->rootPAlbum = 0;
    d->rootTAlbum = 0;
    d->rootDAlbum = 0;
    d->rootSAlbum = 0;
    
    d->libraryPath = cleanPath;

    TQString dbPath = cleanPath + "/digikam3.db";

#ifdef NFS_HACK
    dbPath = locateLocal("appdata", KIO::encodeFileName(TQDir::cleanDirPath(dbPath)));
#endif

    d->db->setDBPath(dbPath);

    // -- Locale Checking ---------------------------------------------------------
    
    TQString currLocale(TQTextCodec::codecForLocale()->name());
    TQString dbLocale = d->db->getSetting("Locale");

    // guilty until proven innocent
    bool localeChanged = true;
    
    if (dbLocale.isNull())
    {
        DDebug() << "No locale found in database" << endl;

        // Copy an existing locale from the settings file (used < 0.8)
        // to the database.
        KConfig* config = KGlobal::config();
        config->setGroup("General Settings");
        if (config->hasKey("Locale"))
        {
            DDebug() << "Locale found in configfile" << endl;
            dbLocale = config->readEntry("Locale");

            // this hack is necessary, as we used to store the entire
            // locale info LC_ALL (for eg: en_US.UTF-8) earlier, 
            // we now save only the encoding (UTF-8)

            TQString oldConfigLocale = ::setlocale(0, 0);

            if (oldConfigLocale == dbLocale)
            {
                dbLocale = currLocale;
                localeChanged = false;
                d->db->setSetting("Locale", dbLocale);
            }
        }
        else
        {
            DDebug() << "No locale found in config file"  << endl;
            dbLocale = currLocale;

            localeChanged = false;
            d->db->setSetting("Locale",dbLocale);
        }
    }
    else
    {
        if (dbLocale == currLocale)
            localeChanged = false;
    }

    if (localeChanged)
    {
        // TODO it would be better to replace all yes/no confirmation dialogs with ones that has custom
        // buttons that denote the actions directly, i.e.:  ["Ignore and Continue"]  ["Adjust locale"]
        int result =
            KMessageBox::warningYesNo(0,
                                      i18n("Your locale has changed since this album "
                                           "was last opened.\n"
                                           "Old Locale : %1, New Locale : %2\n"
                                           "This can cause unexpected problems. "
                                           "If you are sure that you want to "
                                           "continue, click 'Yes' to work with this album. "
                                           "Otherwise, click 'No' and correct your "
                                           "locale setting before restarting digiKam")
                                      .tqarg(dbLocale)
                                      .tqarg(currLocale));
        if (result != KMessageBox::Yes)
            exit(0);

        d->db->setSetting("Locale",currLocale);
    }

    // -- Check if we need to upgrade 0.7.x db to 0.8 db ---------------------

    if (!upgradeDB_Sqlite2ToSqlite3(d->libraryPath))
    {
        KMessageBox::error(0, i18n("Failed to update the old Database to the new Database format\n"
                                   "This error can happen if the Album Path '%1' does not exist or is write-protected.\n"
                                   "If you have moved your photo collection, you need to adjust the 'Album Path' in digikam's configuration file.")
                                   .tqarg(d->libraryPath));
        exit(0);
    }
    
    // set an initial modification list to filter out KDirWatch signals
    // caused by database operations
    TQFileInfo dbFile(dbPath);
    d->dbPathModificationDateList = d->buildDirectoryModList(dbFile);

    // -- Check if we need to do scanning -------------------------------------

    KConfig* config = KGlobal::config();
    config->setGroup("General Settings");
    if (config->readBoolEntry("Scan At Start", true) ||
        d->db->getSetting("Scanned").isEmpty())
    {
        ScanLib sLib(splash);
        sLib.startScan();
    }
}

TQString AlbumManager::getLibraryPath() const
{
    return d->libraryPath;    
}

void AlbumManager::startScan()
{
    if (!d->changed)
        return;
    d->changed = false;
    
    d->dirWatch = new KDirWatch(this);
    connect(d->dirWatch, TQT_SIGNAL(dirty(const TQString&)),
            this, TQT_SLOT(slotDirty(const TQString&)));
    
    KDirWatch::Method m = d->dirWatch->internalMethod();
    TQString mName("FAM");
    if (m == KDirWatch::DNotify)
        mName = TQString("DNotify");
    else if (m == KDirWatch::Stat)
        mName = TQString("Stat");
    else if (m == KDirWatch::INotify)
        mName = TQString("INotify");
    DDebug() << "KDirWatch method = " << mName << endl;

    d->dirWatch->addDir(d->libraryPath);

    d->rootPAlbum = new PAlbum(i18n("My Albums"), 0, true);
    insertPAlbum(d->rootPAlbum);
    
    d->rootTAlbum = new TAlbum(i18n("My Tags"), 0, true);
    insertTAlbum(d->rootTAlbum);

    d->rootSAlbum = new SAlbum(0, KURL(), true, true);

    d->rootDAlbum = new DAlbum(TQDate(), true);

    refresh();
    
    emit signalAllAlbumsLoaded();
}

void AlbumManager::refresh()
{
    scanPAlbums();
    scanTAlbums();
    scanSAlbums();
    scanDAlbums();

    if (!d->dirtyAlbums.empty())
    {
        KURL u;
        u.setProtocol("digikamalbums");
        u.setPath(d->dirtyAlbums.first());
        d->dirtyAlbums.pop_front();
    
        DIO::scan(u);
    }
}

void AlbumManager::scanPAlbums()
{
    // first insert all the current PAlbums into a map for quick lookup
    typedef TQMap<TQString, PAlbum*> AlbumMap;
    AlbumMap aMap;

    AlbumIterator it(d->rootPAlbum);
    while (it.current())
    {
        PAlbum* a = (PAlbum*)(*it);
        aMap.insert(a->url(), a);
        ++it;
    }

    // scan db and get a list of all albums
    AlbumInfo::List aList = d->db->scanAlbums();
    qHeapSort(aList);

    AlbumInfo::List newAlbumList;
    
    // go through all the Albums and see which ones are already present
    for (AlbumInfo::List::iterator it = aList.begin(); it != aList.end(); ++it)
    {
        AlbumInfo info = *it;
        info.url = TQDir::cleanDirPath(info.url);

        if (!aMap.contains(info.url))
        {
            newAlbumList.append(info);
        }
        else
        {
            aMap.remove(info.url);
        }
    }

    // now aMap contains all the deleted albums and
    // newAlbumList contains all the new albums

    // first inform all frontends of the deleted albums
    for (AlbumMap::iterator it = aMap.begin(); it != aMap.end(); ++it)
    {
        // the albums have to be removed with tqchildren being removed first.
        // removePAlbum takes care of that.
        // So never delete the PAlbum using it.data(). instead check if the
        // PAlbum is still in the Album Dict before trying to remove it.

        // this might look like there is memory leak here, since removePAlbum
        // doesn't delete albums and looks like child Albums don't get deleted.
        // But when the parent album gets deleted, the tqchildren are also deleted.
        
        PAlbum* album = d->pAlbumDict.find(it.key());
        if (!album)
            continue;

        removePAlbum(album);
        delete album;
    }

    qHeapSort(newAlbumList);
    for (AlbumInfo::List::iterator it = newAlbumList.begin(); it != newAlbumList.end(); ++it)
    {
        AlbumInfo info = *it;
        if (info.url.isEmpty() || info.url == "/")
            continue;

        // Despite its name info.url is a TQString.
        // setPath takes care for escaping characters that are valid for files but not for URLs ('#')
        KURL u;
        u.setPath(info.url);
        TQString name = u.fileName();
        // Get its parent
        TQString purl = u.upURL().path(-1);

        PAlbum* parent = d->pAlbumDict.find(purl);
        if (!parent)
        {
            DWarning() << k_funcinfo <<  "Could not find parent with url: "
                       << purl << " for: " << info.url << endl;
            continue;
        }

        // Create the new album
        PAlbum* album       = new PAlbum(name, info.id, false);
        album->m_caption    = info.caption;
        album->m_collection = info.collection;
        album->m_date       = info.date;
        album->m_icon       = info.icon;

        album->setParent(parent);
        d->dirWatch->addDir(album->folderPath());

        insertPAlbum(album);
    }

    if (!AlbumSettings::instance()->getShowFolderTreeViewItemsCount())
        return;
        
    // List albums using kioslave

    if (d->albumListJob)
    {
        d->albumListJob->kill();
        d->albumListJob = 0;
    }
    
    KURL u;
    u.setProtocol("digikamalbums");
    u.setPath("/");

    TQByteArray ba;
    TQDataStream ds(ba, IO_WriteOnly);
    ds << d->libraryPath;
    ds << KURL();
    ds << AlbumSettings::instance()->getAllFileFilter();
    ds << 0; // getting dimensions (not needed here)
    ds << 0; // recursive sub-album (not needed here)
    ds << 0; // recursive sub-tags (not needed here)
        
    d->albumListJob = new KIO::TransferJob(u, KIO::CMD_SPECIAL,
                                           ba, TQByteArray(), false);
    d->albumListJob->addMetaData("folders", "yes");

    connect(d->albumListJob, TQT_SIGNAL(result(KIO::Job*)),
            this, TQT_SLOT(slotAlbumsJobResult(KIO::Job*)));

    connect(d->albumListJob, TQT_SIGNAL(data(KIO::Job*, const TQByteArray&)),
            this, TQT_SLOT(slotAlbumsJobData(KIO::Job*, const TQByteArray&)));
}

void AlbumManager::scanTAlbums()
{
    // list TAlbums directly from the db
    // first insert all the current TAlbums into a map for quick lookup
    typedef TQMap<int, TAlbum*> TagMap;
    TagMap tmap;

    tmap.insert(0, d->rootTAlbum);

    AlbumIterator it(d->rootTAlbum);
    while (it.current())
    {
        TAlbum* t = (TAlbum*)(*it);
        tmap.insert(t->id(), t);
        ++it;
    }

    // Retrieve the list of tags from the database
    TagInfo::List tList = d->db->scanTags();

    // sort the list. needed because we want the tags can be read in any order,
    // but we want to make sure that we are ensure to find the parent TAlbum
    // for a new TAlbum

    {
        TQIntDict<TAlbum> tagDict;
        tagDict.setAutoDelete(false);

        // insert items into a dict for quick lookup
        for (TagInfo::List::iterator it = tList.begin(); it != tList.end(); ++it)
        {
            TagInfo info = *it;
            TAlbum* album = new TAlbum(info.name, info.id);
            album->m_icon = info.icon;
            album->m_pid  = info.pid;
            tagDict.insert(info.id, album);
        }
        tList.clear();

        // also add root tag
        TAlbum* rootTag = new TAlbum("root", 0, true);
        tagDict.insert(0, rootTag);

        // build tree
        TQIntDictIterator<TAlbum> iter(tagDict);
        for ( ; iter.current(); ++iter )
        {
            TAlbum* album = iter.current();
            if (album->m_id == 0)
                continue;
            
            TAlbum* parent = tagDict.find(album->m_pid);
            if (parent)
            {
                album->setParent(parent);
            }
            else
            {
                DWarning() << "Failed to find parent tag for tag "
                            << iter.current()->m_title
                            << " with pid "
                            << iter.current()->m_pid << endl;
            }
        }

        // now insert the items into the list. becomes sorted
        AlbumIterator it(rootTag);
        while (it.current())
        {
            TAlbum* album = (TAlbum*)it.current();
            TagInfo info;
            info.id   = album->m_id;
            info.pid  = album->m_pid;
            info.name = album->m_title;
            info.icon = album->m_icon;
            tList.append(info);
            ++it;
        }

        // this will also delete all child albums
        delete rootTag;
    }
    
    for (TagInfo::List::iterator it = tList.begin(); it != tList.end(); ++it)
    {
        TagInfo info = *it;

        // check if we have already added this tag
        if (tmap.contains(info.id))
            continue;

        // Its a new album. Find the parent of the album
        TagMap::iterator iter = tmap.find(info.pid);
        if (iter == tmap.end())
        {
            DWarning() << "Failed to find parent tag for tag "
                        << info.name 
                        << " with pid "
                        << info.pid << endl;
            continue;
        }

        TAlbum* parent = iter.data();
        
        // Create the new TAlbum
        TAlbum* album = new TAlbum(info.name, info.id, false);
        album->m_icon = info.icon;
        album->setParent(parent);
        insertTAlbum(album);

        // also insert it in the map we are doing lookup of parent tags
        tmap.insert(info.id, album);
    }

    if (!AlbumSettings::instance()->getShowFolderTreeViewItemsCount())
        return;
        
    // List tags using kioslave

    if (d->tagListJob)
    {
        d->tagListJob->kill();
        d->tagListJob = 0;
    }
    
    KURL u;
    u.setProtocol("digikamtags");
    u.setPath("/");

    TQByteArray ba;
    TQDataStream ds(ba, IO_WriteOnly);
    ds << d->libraryPath;
    ds << KURL();
    ds << AlbumSettings::instance()->getAllFileFilter();
    ds << 0; // getting dimensions (not needed here)
    ds << 0; // recursive sub-album (not needed here)
    ds << 0; // recursive sub-tags (not needed here)
        
    d->tagListJob = new KIO::TransferJob(u, KIO::CMD_SPECIAL,
                                         ba, TQByteArray(), false);
    d->tagListJob->addMetaData("folders", "yes");

    connect(d->tagListJob, TQT_SIGNAL(result(KIO::Job*)),
            this, TQT_SLOT(slotTagsJobResult(KIO::Job*)));

    connect(d->tagListJob, TQT_SIGNAL(data(KIO::Job*, const TQByteArray&)),
            this, TQT_SLOT(slotTagsJobData(KIO::Job*, const TQByteArray&)));
}

void AlbumManager::scanSAlbums()
{
    // list SAlbums directly from the db
    // first insert all the current SAlbums into a map for quick lookup
    typedef TQMap<int, SAlbum*> SearchMap;
    SearchMap sMap;

    AlbumIterator it(d->rootSAlbum);
    while (it.current())
    {
        SAlbum* t = (SAlbum*)(*it);
        sMap.insert(t->id(), t);
        ++it;
    }

    // Retrieve the list of searches from the database
    SearchInfo::List sList = d->db->scanSearches();

    for (SearchInfo::List::iterator it = sList.begin(); it != sList.end(); ++it)
    {
        SearchInfo info = *it;

        // check if we have already added this search
        if (sMap.contains(info.id))
            continue;

        bool simple = (info.url.queryItem("1.key") == TQString::tqfromLatin1("keyword"));
        
        // Its a new album.
        SAlbum* album = new SAlbum(info.id, info.url, simple, false);
        album->setParent(d->rootSAlbum);
        d->albumIntDict.insert(album->globalID(), album);
        emit signalAlbumAdded(album);
    }
}

void AlbumManager::scanDAlbums()
{
    // List dates using kioslave

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

    KURL u;
    u.setProtocol("digikamdates");
    u.setPath("/");

    TQByteArray ba;
    TQDataStream ds(ba, IO_WriteOnly);
    ds << d->libraryPath;
    ds << KURL();
    ds << AlbumSettings::instance()->getAllFileFilter();
    ds << 0; // getting dimensions (not needed here)
    ds << 0; // recursive sub-album (not needed here)
    ds << 0; // recursive sub-tags (not needed here)
    
    d->dateListJob = new KIO::TransferJob(u, KIO::CMD_SPECIAL,
                                          ba, TQByteArray(), false);
    d->dateListJob->addMetaData("folders", "yes");

    connect(d->dateListJob, TQT_SIGNAL(result(KIO::Job*)),
            this, TQT_SLOT(slotDatesJobResult(KIO::Job*)));

    connect(d->dateListJob, TQT_SIGNAL(data(KIO::Job*, const TQByteArray&)),
            this, TQT_SLOT(slotDatesJobData(KIO::Job*, const TQByteArray&)));
}

AlbumList AlbumManager::allPAlbums() const
{
    AlbumList list;
    if (d->rootPAlbum)
        list.append(d->rootPAlbum);

    AlbumIterator it(d->rootPAlbum);
    while (it.current())
    {
        list.append(*it);
        ++it;
    }
    
    return list;
}

AlbumList AlbumManager::allTAlbums() const
{
    AlbumList list;
    if (d->rootTAlbum)
        list.append(d->rootTAlbum);

    AlbumIterator it(d->rootTAlbum);
    while (it.current())
    {
        list.append(*it);
        ++it;
    }
    
    return list;
}

AlbumList AlbumManager::allSAlbums() const
{
    AlbumList list;
    if (d->rootSAlbum)
        list.append(d->rootSAlbum);

    AlbumIterator it(d->rootSAlbum);
    while (it.current())
    {
        list.append(*it);
        ++it;
    }
    
    return list;
}

AlbumList AlbumManager::allDAlbums() const
{
    AlbumList list;
    if (d->rootDAlbum)
        list.append(d->rootDAlbum);

    AlbumIterator it(d->rootDAlbum);
    while (it.current())
    {
        list.append(*it);
        ++it;
    }
    
    return list;
}

void AlbumManager::setCurrentAlbum(Album *album)
{
    d->currentAlbum = album;
    emit signalAlbumCurrentChanged(album);
}

Album* AlbumManager::currentAlbum() const
{
    return d->currentAlbum;
}

PAlbum* AlbumManager::findPAlbum(const KURL& url) const
{
    TQString path = url.path();
    path.remove(d->libraryPath);
    path = TQDir::cleanDirPath(path);

    return d->pAlbumDict.find(path);
}

PAlbum* AlbumManager::findPAlbum(int id) const
{
    if (!d->rootPAlbum)
        return 0;

    int gid = d->rootPAlbum->globalID() + id;

    return (PAlbum*)(d->albumIntDict.find(gid));
}

TAlbum* AlbumManager::findTAlbum(int id) const
{
    if (!d->rootTAlbum)
        return 0;

    int gid = d->rootTAlbum->globalID() + id;

    return (TAlbum*)(d->albumIntDict.find(gid));
}

SAlbum* AlbumManager::findSAlbum(int id) const
{
    if (!d->rootTAlbum)
        return 0;

    int gid = d->rootSAlbum->globalID() + id;

    return (SAlbum*)(d->albumIntDict.find(gid));
}

DAlbum* AlbumManager::findDAlbum(int id) const
{
    if (!d->rootDAlbum)
        return 0;

    int gid = d->rootDAlbum->globalID() + id;

    return (DAlbum*)(d->albumIntDict.find(gid));
}

Album* AlbumManager::findAlbum(int gid) const
{
    return d->albumIntDict.find(gid);
}

TAlbum* AlbumManager::findTAlbum(const TQString &tagPath) const
{
    // handle gracefully with or without leading slash
    bool withLeadingSlash = tagPath.startsWith("/");
    AlbumIterator it(d->rootTAlbum);
    while (it.current())
    {
        TAlbum *talbum = static_cast<TAlbum *>(*it);
        if (talbum->tagPath(withLeadingSlash) == tagPath)
            return talbum;
        ++it;
    }
    return 0;

}


PAlbum* AlbumManager::createPAlbum(PAlbum* parent,
                                   const TQString& name,
                                   const TQString& caption,
                                   const TQDate& date,
                                   const TQString& collection,
                                   TQString& errMsg)
{
    if (!parent)
    {
        errMsg = i18n("No parent found for album.");
        return 0;
    }

    // sanity checks
    if (name.isEmpty())
    {
        errMsg = i18n("Album name cannot be empty.");
        return 0;
    }
    
    if (name.contains("/"))
    {
        errMsg = i18n("Album name cannot contain '/'.");
        return 0;
    }
    
    // first check if we have another album with the same name
    Album *child = parent->m_firstChild;
    while (child)
    {
        if (child->title() == name)
        {
            errMsg = i18n("An existing album has the same name.");
            return 0;
        }
        child = child->m_next;
    }

    TQString path = parent->folderPath();
    path += '/' + name;
    path = TQDir::cleanDirPath(path);

    // make the directory synchronously, so that we can add the
    // album info to the database directly
    if (::mkdir(TQFile::encodeName(path), 0777) != 0)
    {
        if (errno == EEXIST)
            errMsg = i18n("Another file or folder with same name exists");
        else if (errno == EACCES)
            errMsg = i18n("Access denied to path");
        else if (errno == ENOSPC)
            errMsg = i18n("Disk is full");
        else
            errMsg = i18n("Unknown error"); // being lazy

        return 0;
    }

    // Now insert the album properties into the database
    path = path.remove(0, d->libraryPath.length());
    if (!path.startsWith("/"))
        path.prepend("/");

    int id = d->db->addAlbum(path, caption, date, collection);
    if (id == -1)
    {
        errMsg = i18n("Failed to add album to database");
        return 0;
    }

    PAlbum *album = new PAlbum(name, id, false);
    album->m_caption    = caption;
    album->m_collection = collection;
    album->m_date       = date;
    
    album->setParent(parent);
    
    d->dirWatch->addDir(album->folderPath());
        
    insertPAlbum(album);

    return album;
}

bool AlbumManager::renamePAlbum(PAlbum* album, const TQString& newName,
                                TQString& errMsg)
{
    if (!album)
    {
        errMsg = i18n("No such album");
        return false;
    }

    if (album == d->rootPAlbum)
    {
        errMsg = i18n("Cannot rename root album");
        return false;
    }

    if (newName.contains("/"))
    {
        errMsg = i18n("Album name cannot contain '/'");
        return false;
    }
    
    // first check if we have another sibling with the same name
    Album *sibling = album->m_parent->m_firstChild;
    while (sibling)
    {
        if (sibling->title() == newName)
        {
            errMsg = i18n("Another album with same name exists\n"
                          "Please choose another name");
            return false;
        }
        sibling = sibling->m_next;
    }

    TQString oldURL = album->url();
    
    KURL u = KURL::fromPathOrURL(album->folderPath()).upURL();
    u.addPath(newName);
    u.cleanPath();

    if (::rename(TQFile::encodeName(album->folderPath()),
                 TQFile::encodeName(u.path(-1))) != 0)
    {
        errMsg = i18n("Failed to rename Album");
        return false;
    }
    
    // now rename the album and subalbums in the database

    // all we need to do is set the title of the album which is being
    // renamed correctly and all the sub albums will automatically get
    // their url set correctly

    album->setTitle(newName);
    d->db->setAlbumURL(album->id(), album->url());

    Album* subAlbum = 0;
    AlbumIterator it(album);
    while ((subAlbum = it.current()) != 0)
    {
        d->db->setAlbumURL(subAlbum->id(), ((PAlbum*)subAlbum)->url());
        ++it;
    }

    // Update AlbumDict. basically clear it and rebuild from scratch
    {
        d->pAlbumDict.clear();
        d->pAlbumDict.insert(d->rootPAlbum->url(), d->rootPAlbum);
        AlbumIterator it(d->rootPAlbum);
        PAlbum* subAlbum = 0;
        while ((subAlbum = (PAlbum*)it.current()) != 0)
        {
            d->pAlbumDict.insert(subAlbum->url(), subAlbum);
            ++it;
        }
    }
    
    emit signalAlbumRenamed(album);
    
    return true;
}

bool AlbumManager::updatePAlbumIcon(PAlbum *album, TQ_LLONG iconID, TQString& errMsg)
{
    if (!album)
    {
        errMsg = i18n("No such album");
        return false;
    }

    if (album == d->rootPAlbum)
    {
        errMsg = i18n("Cannot edit root album");
        return false;
    }

    d->db->setAlbumIcon(album->id(), iconID);
    album->m_icon = d->db->getAlbumIcon(album->id());

    emit signalAlbumIconChanged(album);    

    return true;
}

TAlbum* AlbumManager::createTAlbum(TAlbum* parent, const TQString& name,
                                   const TQString& iconkde, TQString& errMsg)
{
    if (!parent)
    {
        errMsg = i18n("No parent found for tag");
        return 0;
    }

    // sanity checks
    if (name.isEmpty())
    {
        errMsg = i18n("Tag name cannot be empty");
        return 0;
    }
    
    if (name.contains("/"))
    {
        errMsg = i18n("Tag name cannot contain '/'");
        return 0;
    }
    
    // first check if we have another album with the same name
    Album *child = parent->m_firstChild;
    while (child)
    {
        if (child->title() == name)
        {
            errMsg = i18n("Tag name already exists");
            return 0;
        }
        child = child->m_next;
    }

    int id = d->db->addTag(parent->id(), name, iconkde, 0);
    if (id == -1)
    {
        errMsg = i18n("Failed to add tag to database");
        return 0;
    }
    
    TAlbum *album = new TAlbum(name, id, false);
    album->m_icon = iconkde;
    album->setParent(parent);

    insertTAlbum(album);
    
    return album;
}

AlbumList AlbumManager::findOrCreateTAlbums(const TQStringList &tagPaths)
{
    IntList tagIDs;

    // find tag ids for tag paths in list, create if they don't exist
    tagIDs = d->db->getTagsFromTagPaths(tagPaths);

    // create TAlbum objects for the newly created tags
    scanTAlbums();

    AlbumList resultList;

    for (IntList::iterator it = tagIDs.begin(); it != tagIDs.end(); ++it)
    {
        resultList.append(findTAlbum(*it));
    }

    return resultList;
}

bool AlbumManager::deleteTAlbum(TAlbum* album, TQString& errMsg)
{
    if (!album)
    {
        errMsg = i18n("No such album");
        return false;
    }

    if (album == d->rootTAlbum)
    {
        errMsg = i18n("Cannot delete Root Tag");
        return false;
    }

    d->db->deleteTag(album->id());

    Album* subAlbum = 0;
    AlbumIterator it(album);
    while ((subAlbum = it.current()) != 0)
    {
        d->db->deleteTag(subAlbum->id());
        ++it;
    }
    
    removeTAlbum(album);

    d->albumIntDict.remove(album->globalID());
    delete album;
    
    return true;
}

bool AlbumManager::renameTAlbum(TAlbum* album, const TQString& name,
                                TQString& errMsg)
{
    if (!album)
    {
        errMsg = i18n("No such album");
        return false;
    }

    if (album == d->rootTAlbum)
    {
        errMsg = i18n("Cannot edit root tag");
        return false;
    }

    if (name.contains("/"))
    {
        errMsg = i18n("Tag name cannot contain '/'");
        return false;
    }

    // first check if we have another sibling with the same name
    Album *sibling = album->m_parent->m_firstChild;
    while (sibling)
    {
        if (sibling->title() == name)
        {
            errMsg = i18n("Another tag with same name exists\n"
                          "Please choose another name");
            return false;
        }
        sibling = sibling->m_next;
    }

    d->db->setTagName(album->id(), name);
    album->setTitle(name);
    emit signalAlbumRenamed(album);
    
    return true;
}

bool AlbumManager::moveTAlbum(TAlbum* album, TAlbum *newParent, TQString &errMsg)
{
    if (!album)
    {
        errMsg = i18n("No such album");
        return false;
    }

    if (album == d->rootTAlbum)
    {
        errMsg = i18n("Cannot move root tag");
        return false;
    }
    
    d->db->setTagParentID(album->id(), newParent->id());
    album->parent()->removeChild(album);
    album->setParent(newParent);

    emit signalTAlbumMoved(album, newParent);
    
    return true;
}

bool AlbumManager::updateTAlbumIcon(TAlbum* album, const TQString& iconKDE,
                                    TQ_LLONG iconID, TQString& errMsg)
{
    if (!album)
    {
        errMsg = i18n("No such tag");
        return false;
    }

    if (album == d->rootTAlbum)
    {
        errMsg = i18n("Cannot edit root tag");
        return false;
    }

    d->db->setTagIcon(album->id(), iconKDE, iconID);
    album->m_icon = d->db->getTagIcon(album->id());
    
    emit signalAlbumIconChanged(album);
    
    return true;
}

SAlbum* AlbumManager::createSAlbum(const KURL& url, bool simple)
{
    TQString name = url.queryItem("name");

    // first iterate through all the search albums and see if there's an existing
    // SAlbum with same name. (Remember, SAlbums are arranged in a flat list)
    for (Album* album = d->rootSAlbum->firstChild(); album; album = album->next())
    {
        if (album->title() == name)
        {
            SAlbum* sa = (SAlbum*)album;
            sa->m_kurl = url;
            d->db->updateSearch(sa->id(), url.queryItem("name"), url);
            return sa;
        }
    }
    
    int id = d->db->addSearch(url.queryItem("name"), url);
    if (id == -1)
        return 0;
    
    SAlbum* album = new SAlbum(id, url, simple, false);
    album->setTitle(url.queryItem("name"));
    album->setParent(d->rootSAlbum);

    d->albumIntDict.insert(album->globalID(), album);
    emit signalAlbumAdded(album);
    
    return album;
}

bool AlbumManager::updateSAlbum(SAlbum* album, const KURL& newURL)
{
    if (!album)
        return false;

    d->db->updateSearch(album->id(), newURL.queryItem("name"), newURL);

    TQString oldName = album->title();
    
    album->m_kurl = newURL;
    album->setTitle(newURL.queryItem("name"));
    if (oldName != album->title())
        emit signalAlbumRenamed(album);

    return true;
}

bool AlbumManager::deleteSAlbum(SAlbum* album)
{
    if (!album)
        return false;

    emit signalAlbumDeleted(album);

    d->db->deleteSearch(album->id());
    
    d->albumIntDict.remove(album->globalID());
    delete album;
    
    return true;
}

void AlbumManager::insertPAlbum(PAlbum *album)
{
    if (!album)
        return;

    d->pAlbumDict.insert(album->url(), album);
    d->albumIntDict.insert(album->globalID(), album);

    emit signalAlbumAdded(album);
}

void AlbumManager::removePAlbum(PAlbum *album)
{
    if (!album)
        return;

    // remove all tqchildren of this album
    Album* child = album->m_firstChild;
    while (child)
    {
        Album *next = child->m_next;
        removePAlbum((PAlbum*)child);
        child = next;
    }
    
    d->pAlbumDict.remove(album->url());
    d->albumIntDict.remove(album->globalID());

    d->dirtyAlbums.remove(album->url());
    d->dirWatch->removeDir(album->folderPath());

    if (album == d->currentAlbum)
    {
        d->currentAlbum = 0;
        emit signalAlbumCurrentChanged(0);
    }
    
    emit signalAlbumDeleted(album);
}

void AlbumManager::insertTAlbum(TAlbum *album)
{
    if (!album)
        return;

    d->albumIntDict.insert(album->globalID(), album);

    emit signalAlbumAdded(album);
}

void AlbumManager::removeTAlbum(TAlbum *album)
{
    if (!album)
        return;

    // remove all tqchildren of this album
    Album* child = album->m_firstChild;
    while (child)
    {
        Album *next = child->m_next;
        removeTAlbum((TAlbum*)child);
        child = next;
    }
    
    d->albumIntDict.remove(album->globalID());    

    if (album == d->currentAlbum)
    {
        d->currentAlbum = 0;
        emit signalAlbumCurrentChanged(0);
    }
    
    emit signalAlbumDeleted(album);
}

void AlbumManager::emitAlbumItemsSelected(bool val)
{
    emit signalAlbumItemsSelected(val);    
}

void AlbumManager::setItemHandler(AlbumItemHandler *handler)
{
    d->itemHandler = handler;    
}

AlbumItemHandler* AlbumManager::getItemHandler()
{
    return d->itemHandler;
}

void AlbumManager::refreshItemHandler(const KURL::List& itemList)
{
    if (itemList.empty())
        d->itemHandler->refresh();
    else
        d->itemHandler->refreshItems(itemList);
}

void AlbumManager::slotAlbumsJobResult(KIO::Job* job)
{
    d->albumListJob = 0;

    if (job->error())
    {
        DWarning() << k_funcinfo << "Failed to list albums" << endl;
        return;
    }
}

void AlbumManager::slotAlbumsJobData(KIO::Job*, const TQByteArray& data)
{
    if (data.isEmpty())
        return;

    TQMap<int, int> albumsStatMap;
    TQDataStream ds(data, IO_ReadOnly);
    ds >> albumsStatMap;

    emit signalPAlbumsDirty(albumsStatMap);
}

void AlbumManager::slotTagsJobResult(KIO::Job* job)
{
    d->tagListJob = 0;

    if (job->error())
    {
        DWarning() << k_funcinfo << "Failed to list tags" << endl;
        return;
    }
}

void AlbumManager::slotTagsJobData(KIO::Job*, const TQByteArray& data)
{
    if (data.isEmpty())
        return;

    TQMap<int, int> tagsStatMap;
    TQDataStream ds(data, IO_ReadOnly);
    ds >> tagsStatMap;

    emit signalTAlbumsDirty(tagsStatMap);
}

void AlbumManager::slotDatesJobResult(KIO::Job* job)
{
    d->dateListJob = 0;

    if (job->error())
    {
        DWarning() << k_funcinfo << "Failed to list dates" << endl;
        return;
    }

    emit signalAllDAlbumsLoaded();
}

void AlbumManager::slotDatesJobData(KIO::Job*, const TQByteArray& data)
{
    if (data.isEmpty())
        return;

    // insert all the DAlbums into a qmap for quick access
    TQMap<TQDate, DAlbum*> mAlbumMap;
    TQMap<int, DAlbum*>   yAlbumMap;

    AlbumIterator it(d->rootDAlbum);
    while (it.current())
    {
        DAlbum* a = (DAlbum*)(*it);
        if (a->range() == DAlbum::Month)
            mAlbumMap.insert(a->date(), a);
        else
            yAlbumMap.insert(a->date().year(), a);
        ++it;
    }

    TQMap<TQDateTime, int> datesStatMap;
    TQDataStream ds(data, IO_ReadOnly);
    ds >> datesStatMap;

    TQMap<YearMonth, int> yearMonthMap;
    for ( TQMap<TQDateTime, int>::iterator it = datesStatMap.begin();
          it != datesStatMap.end(); ++it )
    {
        TQMap<YearMonth, int>::iterator it2 = yearMonthMap.find(YearMonth(it.key().date().year(), it.key().date().month()));
        if ( it2 == yearMonthMap.end() )
        {
            yearMonthMap.insert( YearMonth(it.key().date().year(), it.key().date().month()), it.data() );
        }
        else
        {
            yearMonthMap.replace( YearMonth(it.key().date().year(), it.key().date().month()), it2.data() + it.data() );
        }
    }

    int year, month;
    for ( TQMap<YearMonth, int>::iterator it = yearMonthMap.begin();
          it != yearMonthMap.end(); ++it )
    {
        year  = it.key().first;
        month = it.key().second;

        TQDate md(year, month, 1);

        // Do we already have this Month album
        if (mAlbumMap.contains(md))
        {
            // already there. remove Month album from map
            mAlbumMap.remove(md);

            if (yAlbumMap.contains(year))
            {
                // already there. remove from map
                yAlbumMap.remove(year);
            }

            continue;
        }

        // Check if Year Album already exist.
        DAlbum *yAlbum = 0;
        AlbumIterator it(d->rootDAlbum);
        while (it.current())
        {
            DAlbum* a = (DAlbum*)(*it);
            if (a->date() == TQDate(year, 1, 1) && a->range() == DAlbum::Year)
            {
                yAlbum = a;
                break;
            }
            ++it;
        }

        // If no, create Year album.
        if (!yAlbum)
        {
            yAlbum = new DAlbum(TQDate(year, 1, 1), false, DAlbum::Year);
            yAlbum->setParent(d->rootDAlbum);
            d->albumIntDict.insert(yAlbum->globalID(), yAlbum);
            emit signalAlbumAdded(yAlbum);
        }

        // Create Month album
        DAlbum *mAlbum = new DAlbum(md);
        mAlbum->setParent(yAlbum);
        d->albumIntDict.insert(mAlbum->globalID(), mAlbum);
        emit signalAlbumAdded(mAlbum);
    }

    // Now the items contained in the maps are the ones which
    // have been deleted. 
    for (TQMap<TQDate, DAlbum*>::iterator it = mAlbumMap.begin();
         it != mAlbumMap.end(); ++it)
    {
        DAlbum* album = it.data();
        emit signalAlbumDeleted(album);
        d->albumIntDict.remove(album->globalID());
        delete album;
    }

    for (TQMap<int, DAlbum*>::iterator it = yAlbumMap.begin();
         it != yAlbumMap.end(); ++it)
    {
        DAlbum* album = it.data();
        emit signalAlbumDeleted(album);
        d->albumIntDict.remove(album->globalID());
        delete album;
    }

    emit signalDAlbumsDirty(yearMonthMap);
    emit signalDatesMapDirty(datesStatMap);
}

void AlbumManager::slotDirty(const TQString& path)
{
    DDebug() << "Noticed file change in directory " << path << endl;
    TQString url = TQDir::cleanDirPath(path);
    url = TQDir::cleanDirPath(url.remove(d->libraryPath));

    if (url.isEmpty())
        url = "/";

    if (d->dirtyAlbums.contains(url))
        return;

    // is the signal for the directory containing the database file?
    if (url == "/")
    {
        // retrieve modification dates
        TQFileInfo dbFile(d->libraryPath);
        TQValueList<TQDateTime> modList = d->buildDirectoryModList(dbFile);

        // check for equality
        if (modList == d->dbPathModificationDateList)
        {
            DDebug() << "Filtering out db-file-triggered dir watch signal" << endl;
            // we can skip the signal
            return;
        }

        // set new list
        d->dbPathModificationDateList = modList;
    }

    d->dirtyAlbums.append(url);

    if (DIO::running())
        return;

    KURL u;
    u.setProtocol("digikamalbums");
    u.setPath(d->dirtyAlbums.first());
    d->dirtyAlbums.pop_front();
    
    DIO::scan(u);
}

}  // namespace Digikam
