/*
 *  resourcelocaldir.cpp  -  KAlarm local directory calendar resource
 *  Program:  kalarm
 *  Copyright © 2006-2011 by David Jarvie <djarvie@kde.org>
 *  Based on resourcelocaldir.cpp in libkcal (updated to rev 779953,938673,938806),
 *  Copyright (c) 2003 Cornelius Schumacher <schumacher@kde.org>
 *
 *  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 of the License, 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.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "kalarm.h"

#include "resourcelocaldir.moc"

#include <kcal/calendarlocal.h>
#include <kcal/event.h>

#include <klocale.h>
#include <kstandarddirs.h>
#include <kconfiggroup.h>

#include <QDir>
#include <QFile>
#include <QtAlgorithms>

using namespace KCal;

static QDateTime readLastModified(const QString& filePath);


KAResourceLocalDir::KAResourceLocalDir()
    : AlarmResource(),
      mDirReadOnly(false)
{
    init();
}

KAResourceLocalDir::KAResourceLocalDir(const KConfigGroup& group)
    : AlarmResource(group),
      mDirReadOnly(false)
{
    mURL = KUrl(group.readPathEntry("CalendarURL", QString()));
    init();
}

KAResourceLocalDir::KAResourceLocalDir(CalEvent::Type type, const QString& dirName)
    : AlarmResource(type),
      mURL(KUrl::fromPath(dirName)),
      mDirReadOnly(false)
{
    init();
}

void KAResourceLocalDir::init()
{
    setType("dir");   // set resource type

    //setSavePolicy(SaveDelayed);  // unnecessary for KAlarm, and would override base class setting

    connect(&mDirWatch, SIGNAL(dirty(QString)), SLOT(slotUpdated(QString)));
    connect(&mDirWatch, SIGNAL(created(QString)), SLOT(slotUpdated(QString)));
    connect(&mDirWatch, SIGNAL(deleted(QString)), SLOT(slotUpdated(QString)));
    mDirWatch.addDir(mURL.toLocalFile(), KDirWatch::WatchFiles);

    enableResource(isActive());

    // Initially load all files in the directory, then just load changes
    setReloadPolicy(ReloadOnStartup);
}

KAResourceLocalDir::~KAResourceLocalDir()
{
    mDirWatch.stopScan();
    if (isOpen())
        close();
}

void KAResourceLocalDir::writeConfig(KConfigGroup& group)
{
    group.writePathEntry("CalendarURL", mURL.prettyUrl());
    AlarmResource::writeConfig(group);
}

void KAResourceLocalDir::startReconfig()
{
    mNewURL = mURL;
    AlarmResource::startReconfig();
}

void KAResourceLocalDir::applyReconfig()
{
    if (mReconfiguring)
    {
        AlarmResource::applyReconfig();
        if (setDirName(mNewURL))
            mReconfiguring = 3;    // indicate that location has changed
        AlarmResource::applyReconfig();
    }
}

bool KAResourceLocalDir::readOnly() const
{
    return mDirReadOnly || AlarmResource::readOnly();
}

void KAResourceLocalDir::setReadOnly(bool ro)
{
    // Re-evaluate the directory's read-only status (since KDirWatch
    // doesn't pick up permissions changes on the directory itself).
    QFileInfo dirInfo(mURL.toLocalFile());
    mDirReadOnly = !dirInfo.isWritable();
    AlarmResource::setReadOnly(ro);
}

void KAResourceLocalDir::enableResource(bool enable)
{
    kDebug(KARES_DEBUG) << enable << ":" << mURL.toLocalFile();
    if (enable)
    {
        lock(mURL.toLocalFile());
        mDirWatch.startScan();
    }
    else
    {
        lock(QString());
        mDirWatch.stopScan();
    }
}

bool KAResourceLocalDir::doOpen()
{
    QFileInfo dirInfo(mURL.toLocalFile());
    return dirInfo.isDir()  &&  dirInfo.isReadable();
}

void KAResourceLocalDir::slotUpdated(const QString& filepath)
{
#ifdef __GNUC__
#warning Only reload the individual file which has changed
#endif
    if (filepath.contains(QRegExp("(~|\\.new|\\.tmp)$"))
    ||  filepath.startsWith(mURL.toLocalFile() + "/qt_temp."))
        return;   // ignore backup or temporary file
    doLoad(false);
}
/******************************************************************************
* Load the files in the local directory, and add their events to our calendar.
* If 'syncCache' is true, all files are loaded; if false, only changed files
* are loaded.
* Events which contain no alarms are ignored..
* Reply = true if any file in the directory was loaded successfully.
*/
bool KAResourceLocalDir::doLoad(bool syncCache)
{
    kDebug(KARES_DEBUG) << mURL.toLocalFile() << (syncCache ?": load all" :": load changes only");
    if (!isActive()  ||  !isOpen())
        return false;
    ModifiedMap      oldLastModified;
    CompatibilityMap oldCompatibilityMap;
    Incidence::List  changes;
    mLoading = true;
    mLoaded = false;
    disableChangeNotification();
    setCompatibility(KACalendar::ByEvent);
    if (syncCache)
    {
        emit invalidate(this);
        calendar()->close();
        setWrongAlarmType(false, false);
        clearChanges();
    }
    else
    {
emit invalidate(this);  // necessary until load changes only is fixed
        oldLastModified = mLastModified;
        oldCompatibilityMap = mCompatibilityMap;
        changes = changedIncidences();
    }
    mLastModified.clear();
    mCompatibilityMap.clear();
    QString dirName = mURL.toLocalFile();
    bool success = false;
    bool foundFile = false;
    if (KStandardDirs::exists(dirName)  ||  KStandardDirs::exists(dirName + '/'))
    {
        kDebug(KARES_DEBUG) << "Opening" << dirName;
        FixFunc prompt = PROMPT_PART;
        QFileInfo dirInfo(dirName);
        if (!(dirInfo.isDir()  &&  dirInfo.isReadable()))
        {
            setWrongAlarmType(false, false);
            return false;
        }
        mDirReadOnly = !dirInfo.isWritable();
        QDir dir(dirName, QString(), QDir::Unsorted, QDir::Files | QDir::Readable);
        QStringList entries = dir.entryList(QDir::Files | QDir::Readable);
        QStringList writable = dir.entryList(QDir::Files | QDir::Writable);
        for (int i = 0, end = entries.count();  i < end;  ++i)
        {
            // Check the next file in the directory
            QString id = entries[i];
            if (id.contains(QRegExp("(~|\\.new|\\.tmp)$"))
            ||  id.startsWith(QLatin1String("qt_temp.")))
                continue;   // ignore backup or temporary file
            QString fileName = dirName + '/' + id;
            foundFile = true;

            if (!syncCache)
            {
                // Only load new or changed events
#ifdef __GNUC__
#warning All events are actually loaded, not just new or changed
#endif
                clearChange(id);
                Event* ev = calendar()->event(id);
                if (ev  &&  changes.indexOf(ev) < 0)
                {
                    ModifiedMap::ConstIterator mit = oldLastModified.constFind(id);
                    if (mit != oldLastModified.constEnd()  &&  mit.value() == readLastModified(fileName))
                    {
                        // The file hasn't changed, and its event is unchanged
                        // in our calendar, so just transfer the event to the
                        // new maps without rereading the file.
                        mCompatibilityMap[ev] = oldCompatibilityMap[ev];
                        mLastModified[id] = mit.value();
                        success = true;
                        continue;
                    }
                }
                // It's either a new file, or it has changed
                if (ev)
                    calendar()->deleteEvent(ev);
            }
            // Load the file and check whether it's the current KAlarm format.
            // If not, only prompt the user once whether to convert it.
            if (loadFile(fileName, id, !writable.contains(id), prompt))
                success = true;
        }
        if (!foundFile)
            success = true;    // don't return error if there are no files
    }
    else if (syncCache)
    {
        kDebug(KARES_DEBUG) << "Creating" << dirName;

        // Create the directory. Use 0775 to allow group-writable if the umask
        // allows it (permissions will be 0775 & ~umask). This is desired e.g. for
        // group-shared directories!
        success = KStandardDirs::makeDir(dirName, 0775);
        mDirReadOnly = false;
    }

    if (!syncCache)
    {
        if (mLastModified.isEmpty())
        {
            emit invalidate(this);
            calendar()->close();
            setWrongAlarmType(false, false);
        }
        else
        {
            // Delete any events in the calendar for which files were not found
            Event::List oldEvents = calendar()->rawEvents();
            for (int i = 0, end = oldEvents.count();  i < end;  ++i)
            {
                if (!mCompatibilityMap.contains(oldEvents[i]))
                    calendar()->deleteEvent(oldEvents[i]);
            }
        }
    }

    setWrongAlarmType(!checkAlarmTypes(*calendar()));   // check if it contains only wrong alarm types

    mLoading = false;
    enableChangeNotification();
    updateCustomEvents();
    if (success)
    {
        mLoaded = true;
        setReloaded(true);   // the resource has now been loaded at least once
        emit loaded(this);
        if (!syncCache)
            emit resourceChanged(this);
    }
    return success;
}

/******************************************************************************
* Load one file from the local directory, and return its event in 'event'.
* Any event whose ID is not the same as the file name, or any event not
* containing alarms, is ignored.
* Reply = true if the calendar loaded successfully (even if empty).
*/
bool KAResourceLocalDir::loadFile(const QString& fileName, const QString& id, bool readOnly, FixFunc& prompt)
{
    bool success = false;
    CalendarLocal calendar(this->calendar()->timeSpec());
    if (!calendar.load(fileName))
    {
        // Loading this file failed, but just assume that it's not a calendar file
        kDebug(KARES_DEBUG) << fileName << "failed";
    }
    else
    {
        KACalendar::Compat compat = checkCompatibility(calendar, fileName, prompt);
        switch (compat)
        {
            case KACalendar::Converted:   // user elected to convert. Don't prompt again.
                prompt = CONVERT;
                compat = KACalendar::Current;
                break;
            case KACalendar::Convertible: // user elected not to convert. Don't prompt again.
                prompt = NO_CONVERT;
                break;
            case KACalendar::Current:
            case KACalendar::Incompatible:
            case KACalendar::ByEvent:
                break;
        }
        kDebug(KARES_DEBUG) << fileName << ": compatibility=" << compat;
        Event::List rawEvents = calendar.rawEvents();
        for (int i = 0, end = rawEvents.count();  i < end;  ++i)
        {
            Event* ev = rawEvents[i];
            if (ev->uid() != id)
            {
                kError(KARES_DEBUG) << "Wrong event ID (" << ev->uid();
                continue;    // ignore any event with the wrong ID - it shouldn't be there!
            }
            Alarm::List alarms = ev->alarms();
            if (!alarms.isEmpty())
            {
                Event* event = ev->clone();
                if (readOnly)
                    event->setReadOnly(true);
                this->calendar()->addEvent(event);
                mCompatibilityMap[event] = compat;
            }
        }
        success = true;     // at least one file has been opened successfully
    }
    mLastModified[id] = readLastModified(fileName);
    return success;
}

bool KAResourceLocalDir::doSave(bool)
{
    kDebug(KARES_DEBUG) << mURL.toLocalFile();
    bool success = true;
    Incidence::List list = addedIncidences();
    list += changedIncidences();
    qSort(list);
    Incidence* last = 0;
    for (int i = 0, end = list.count();  i < end;  ++i)
    {
        if (list[i] != last)
        {
            last = list[i];
            if (!doSave(true, last))
                success = false;
        }
    }
    emit resourceSaved(this);
    return success;
}

bool KAResourceLocalDir::doSave(bool, Incidence* incidence)
{
    if (mDeletedIncidences.contains(incidence))
    {
        mDeletedIncidences.removeAll(incidence);
        return true;
    }

    QString id = incidence->uid();
    QString fileName = mURL.toLocalFile() + '/' + id;
    kDebug(KARES_DEBUG) << fileName;

    CalendarLocal cal(calendar()->timeSpec());
    cal.setCustomProperties(calendar()->customProperties());   // copy all VCALENDAR custom properties to each file
    if (mCalIDFunction)
        (*mCalIDFunction)(cal);                          // write the application ID into the calendar
    bool success = cal.addIncidence(incidence->clone());
    if (success)
    {
        mDirWatch.stopScan();  // prohibit the dirty() signal and a following reload
        success = cal.save(fileName);
        mDirWatch.startScan();
        clearChange(id);
        mLastModified[id] = readLastModified(fileName);
    }
    return success;
}

bool KAResourceLocalDir::addEvent(Event* event)
{
    if (!AlarmResource::addEvent(event))
        return false;
    mCompatibilityMap[event] = KACalendar::Current;
    return true;
}

bool KAResourceLocalDir::deleteEvent(Event* event)
{
    kDebug(KARES_DEBUG);
    if (!deleteIncidenceFile(event))
        return false;
    // Remove event from added/changed lists, to avoid it being recreated in doSave()
    clearChange(event);
    disableChangeNotification();    // don't record this deletion as pending
    bool success = calendar()->deleteEvent(event);
    if (success)
        mDeletedIncidences.append(event);
    enableChangeNotification();
    return success;
}

bool KAResourceLocalDir::deleteIncidenceFile(Incidence* incidence)
{
    QFile file(mURL.toLocalFile() + '/' + incidence->uid());
    if (!file.exists())
        return true;
    mDirWatch.stopScan();
    bool removed = file.remove();
    mDirWatch.startScan();
    return removed;
}

QString KAResourceLocalDir::dirName() const
{
    return mURL.toLocalFile();
}

bool KAResourceLocalDir::setDirName(const KUrl& newURL)
{
    if (mReconfiguring == 1)
    {
        mNewURL = newURL;
        return true;
    }
    if (!newURL.isLocalFile() || newURL.toLocalFile() == mURL.toLocalFile())
        return false;
    kDebug(KARES_DEBUG) << newURL.toLocalFile();
    if (isOpen())
        close();
    bool active = isActive();
    if (active)
        enableResource(false);
    mDirWatch.removeDir(mURL.toLocalFile());
    mURL = newURL;
    mDirWatch.addDir(mURL.toLocalFile(), KDirWatch::WatchFiles);
    if (active)
        enableResource(true);
    // Trigger loading the new resource, and ensure that the new configuration is saved
    emit locationChanged(this);
    return true;
}

bool KAResourceLocalDir::setLocation(const QString& dirName, const QString&)
{
    KUrl newURL = KUrl::fromPath(dirName);
    return setDirName(newURL);
}

QString KAResourceLocalDir::displayLocation() const
{
    return mURL.toLocalFile();
}

QString KAResourceLocalDir::displayType() const
{
    return i18nc("@info/plain Directory in filesystem", "Directory");
}

QDateTime readLastModified(const QString& filePath)
{
    QFileInfo fi(filePath);
    return fi.lastModified();
}

// vim: et sw=4:
