/*
  This file is part of the kcal library.

  Copyright © 2006 by David Jarvie <software@astrojar.org.uk>
  Copyright (c) 2003,2004 Cornelius Schumacher <schumacher@kde.org>

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Library General Public
  License as published by the Free Software Foundation; either
  version 2 of the License, or (at your option) any later version.

  This library 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
  Library General Public License for more details.

  You should have received a copy of the GNU Library General Public License
  along with this library; see the file COPYING.LIB.  If not, write to
  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
  Boston, MA 02110-1301, USA.
*/

#include "resourcecached.h"
#include "calendarlocal.h"
#include "event.h"
#include "exceptions.h"
#include "incidence.h"
#include "journal.h"
#include "todo.h"

#include "kresources/idmapper.h"

#include <kconfiggroup.h>
#include <kdebug.h>
#include <klocalizedstring.h>
#include <kstandarddirs.h>
#include <kurl.h>

#include <QtCore/QDateTime>
#include <QtCore/QDataStream>
#include <QtCore/QFile>
#include <QtCore/QString>
#include <QtCore/QTimer>


using namespace KCal;

//@cond PRIVATE
class ResourceCached::Private
{
  public:
    Private()
      : mCalendar( QLatin1String( "UTC" ) ),
        mReloadPolicy( ResourceCached::ReloadNever ),
        mReloadInterval( 10 ),
        mInhibitReload( false ),
        mReloaded( false ),
        mSavePending( false ),
        mSavePolicy( ResourceCached::SaveNever ),
        mSaveInterval( 10 ),
        mIdMapper( "kcal/uidmaps/" )
    {}

    CalendarLocal mCalendar;

    int mReloadPolicy;
    int mReloadInterval;
    QTimer mReloadTimer;
    bool mInhibitReload;   // true to prevent downloads by load(DefaultCache)
    bool mReloaded;        // true once it has been downloaded
    bool mSavePending;     // true if a save of changes has been scheduled on the timer

    int mSavePolicy;
    int mSaveInterval;
    QTimer mSaveTimer;

    KDateTime mLastLoad;
    KDateTime mLastSave;

    QMap<KCal::Incidence *,bool> mAddedIncidences;
    QMap<KCal::Incidence *,bool> mChangedIncidences;
    QMap<KCal::Incidence *,bool> mDeletedIncidences;

    KRES::IdMapper mIdMapper;
};
//@endcond

ResourceCached::ResourceCached()
  : ResourceCalendar(),
    d( new Private )
{
  connect( &d->mReloadTimer, SIGNAL(timeout()), SLOT(slotReload()) );
  connect( &d->mSaveTimer, SIGNAL(timeout()), SLOT(slotSave()) );
}

ResourceCached::ResourceCached( const KConfigGroup &group )
  : ResourceCalendar( group ),
    d( new Private )
{
  connect( &d->mReloadTimer, SIGNAL(timeout()), SLOT(slotReload()) );
  connect( &d->mSaveTimer, SIGNAL(timeout()), SLOT(slotSave()) );
}

ResourceCached::~ResourceCached()
{
  delete d;
}

CalendarLocal *ResourceCached::calendar() const
{
  return &d->mCalendar;
}

bool ResourceCached::defaultReloadInhibited() const
{
  return d->mInhibitReload;
}

bool ResourceCached::reloaded() const
{
  return d->mReloaded;
}

void ResourceCached::setReloaded( bool done )
{
  d->mReloaded = done;
}

void ResourceCached::setReloadPolicy( int i )
{
  d->mReloadPolicy = i;

  setupReloadTimer();
}

int ResourceCached::reloadPolicy() const
{
  return d->mReloadPolicy;
}

void ResourceCached::setReloadInterval( int minutes )
{
  d->mReloadInterval = minutes;
}

int ResourceCached::reloadInterval() const
{
  return d->mReloadInterval;
}

bool ResourceCached::inhibitDefaultReload( bool inhibit )
{
  if ( inhibit == d->mInhibitReload ) {
    return false;
  }
  d->mInhibitReload = inhibit;
  return true;
}

void ResourceCached::setSavePolicy( int i )
{
  d->mSavePolicy = i;

  setupSaveTimer();
}

int ResourceCached::savePolicy() const
{
  return d->mSavePolicy;
}

void ResourceCached::setSaveInterval( int minutes )
{
  d->mSaveInterval = minutes;
}

int ResourceCached::saveInterval() const
{
  return d->mSaveInterval;
}

void ResourceCached::readConfig( const KConfigGroup &group )
{
  d->mReloadPolicy = group.readEntry( "ReloadPolicy", int(ReloadNever) );
  d->mReloadInterval = group.readEntry( "ReloadInterval", 10 );

  d->mSaveInterval = group.readEntry( "SaveInterval", 10 );
  d->mSavePolicy = group.readEntry( "SavePolicy", int(SaveNever) );

  QDateTime curDt = QDateTime::currentDateTime();
  QDateTime dt = group.readEntry( "LastLoad", curDt );
  d->mLastLoad = KDateTime( dt, KDateTime::UTC );
  dt = group.readEntry( "LastSave", curDt );
  d->mLastSave = KDateTime( dt, KDateTime::UTC );

  setupSaveTimer();
  setupReloadTimer();
}

void ResourceCached::setupSaveTimer()
{
  if ( d->mSavePolicy == SaveInterval ) {
    kDebug() << "start save timer (interval " << d->mSaveInterval << "mins)";
    d->mSaveTimer.start( d->mSaveInterval * 60 * 1000 ); // n minutes
  } else {
    d->mSaveTimer.stop();
  }
}

void ResourceCached::setupReloadTimer()
{
  if ( d->mReloadPolicy == ReloadInterval ) {
    kDebug() << "start reload timer (interval " << d->mReloadInterval << "mins)";
    d->mReloadTimer.start( d->mReloadInterval * 60 * 1000 ); // n minutes
  } else {
    d->mReloadTimer.stop();
  }
}

void ResourceCached::writeConfig( KConfigGroup &group )
{
  group.writeEntry( "ReloadPolicy", d->mReloadPolicy );
  group.writeEntry( "ReloadInterval", d->mReloadInterval );

  group.writeEntry( "SavePolicy", d->mSavePolicy );
  group.writeEntry( "SaveInterval", d->mSaveInterval );

  group.writeEntry( "LastLoad", d->mLastLoad.toUtc().dateTime() );
  group.writeEntry( "LastSave", d->mLastSave.toUtc().dateTime() );
}

bool ResourceCached::addEvent( Event *event )
{
  return d->mCalendar.addEvent( event );
}

// probably not really efficient, but...it works for now.
bool ResourceCached::deleteEvent( Event *event )
{
  kDebug();

  return d->mCalendar.deleteEvent( event );
}

void ResourceCached::deleteAllEvents()
{
  d->mCalendar.deleteAllEvents();
}

Event *ResourceCached::event( const QString &uid )
{
  return d->mCalendar.event( uid );
}

Event::List ResourceCached::rawEventsForDate( const QDate &qd, const KDateTime::Spec &timeSpec,
                                              EventSortField sortField,
                                              SortDirection sortDirection )
{
  Event::List list = d->mCalendar.rawEventsForDate( qd, timeSpec, sortField, sortDirection );

  return list;
}

Event::List ResourceCached::rawEvents( const QDate &start, const QDate &end,
                                       const KDateTime::Spec &timeSpec, bool inclusive )
{
  return d->mCalendar.rawEvents( start, end, timeSpec, inclusive );
}

Event::List ResourceCached::rawEventsForDate( const KDateTime &kdt )
{
  return d->mCalendar.rawEventsForDate( kdt );
}

Event::List ResourceCached::rawEvents( EventSortField sortField, SortDirection sortDirection )
{
  return d->mCalendar.rawEvents( sortField, sortDirection );
}

bool ResourceCached::addTodo( Todo *todo )
{
  return d->mCalendar.addTodo( todo );
}

bool ResourceCached::deleteTodo( Todo *todo )
{
  return d->mCalendar.deleteTodo( todo );
}

void ResourceCached::deleteAllTodos()
{
  d->mCalendar.deleteAllTodos();
}

bool ResourceCached::deleteJournal( Journal *journal )
{
  return d->mCalendar.deleteJournal( journal );
}

void ResourceCached::deleteAllJournals()
{
  d->mCalendar.deleteAllJournals();
}

Todo::List ResourceCached::rawTodos( TodoSortField sortField, SortDirection sortDirection )
{
  return d->mCalendar.rawTodos( sortField, sortDirection );
}

Todo *ResourceCached::todo( const QString &uid )
{
  return d->mCalendar.todo( uid );
}

Todo::List ResourceCached::rawTodosForDate( const QDate &date )
{
  return d->mCalendar.rawTodosForDate( date );
}

bool ResourceCached::addJournal( Journal *journal )
{
  return d->mCalendar.addJournal( journal );
}

Journal *ResourceCached::journal( const QString &uid )
{
  return d->mCalendar.journal( uid );
}

Journal::List ResourceCached::rawJournals( JournalSortField sortField, SortDirection sortDirection )
{
  return d->mCalendar.rawJournals( sortField, sortDirection );
}

Journal::List ResourceCached::rawJournalsForDate( const QDate &date )
{
  return d->mCalendar.rawJournalsForDate( date );
}

Alarm::List ResourceCached::alarmsTo( const KDateTime &to )
{
  return d->mCalendar.alarmsTo( to );
}

Alarm::List ResourceCached::alarms( const KDateTime &from, const KDateTime &to )
{
  return d->mCalendar.alarms( from, to );
}

void ResourceCached::setTimeSpec( const KDateTime::Spec &timeSpec )
{
  d->mCalendar.setTimeSpec( timeSpec );
}

KDateTime::Spec ResourceCached::timeSpec() const
{
  return d->mCalendar.timeSpec();
}

void ResourceCached::setTimeZoneId( const QString &tzid )
{
  d->mCalendar.setTimeZoneId( tzid );
}

QString ResourceCached::timeZoneId() const
{
  return d->mCalendar.timeZoneId();
}

void ResourceCached::shiftTimes( const KDateTime::Spec &oldSpec, const KDateTime::Spec &newSpec )
{
  d->mCalendar.shiftTimes( oldSpec, newSpec );
}

void ResourceCached::clearChanges()
{
  d->mAddedIncidences.clear();
  d->mChangedIncidences.clear();
  d->mDeletedIncidences.clear();
}

bool ResourceCached::load( CacheAction action )
{
  kDebug() << resourceName();

  setReceivedLoadError( false );

  bool success = true;
  if ( !isOpen() ) {
    success = open(); //krazy:exclude=syscalls open is a class method
  }
  if ( success ) {
    bool update = false;
    switch ( action ) {
    case DefaultCache:
      if ( !d->mReloaded && !d->mInhibitReload ) {
        update = checkForReload();
      }
      break;
    case NoSyncCache:
      break;
    case SyncCache:
      update = true;
      break;
    }
    success = doLoad( update );
  }
  if ( !success && !receivedLoadError() ) {
    loadError();
  }

  // If the resource is read-only, we need to set its incidences to read-only,
  // too. This can't be done at a lower-level, since the read-only setting
  // happens at this level
  if ( !noReadOnlyOnLoad() && readOnly() ) {
    Incidence::List incidences( rawIncidences() );
    Incidence::List::Iterator it;
    for ( it = incidences.begin(); it != incidences.end(); ++it ) {
      (*it)->setReadOnly( true );
    }
  }

  kDebug() << "Done loading resource" << resourceName();

  if ( success ) {
    emit resourceLoaded( this );
  }

  return success;
}

bool ResourceCached::load()
{
  return load( SyncCache );
}

bool ResourceCached::loadFromCache()
{
  setIdMapperIdentifier();
  d->mIdMapper.load();

  if ( !KStandardDirs::exists( cacheFile() ) ) {
    return false;
  }
  d->mCalendar.load( cacheFile() );
  if ( !noReadOnlyOnLoad() && readOnly() ) {
    Incidence::List incidences( rawIncidences() );
    Incidence::List::Iterator it;
    for ( it = incidences.begin(); it != incidences.end(); ++it ) {
      (*it)->setReadOnly( true );
    }
  }
  return true;
}

bool ResourceCached::save( CacheAction action, Incidence *incidence )
{
  if ( !incidence && ( d->mSavePolicy == SaveAlways || d->mSavePolicy == SaveDelayed ) ) {
    d->mSaveTimer.stop();   // in case it's called manually while save is pending
  }
  d->mSavePending = false;
  if ( saveInhibited() ) {
    return true;
  }
  if ( !readOnly() ) {
    kDebug() << "Save resource" << resourceName();

    setReceivedSaveError( false );

    if ( !isOpen() ) {
      return true;
    }
    bool upload = false;
    switch ( action ) {
    case DefaultCache:
      upload = checkForSave();
      break;
    case NoSyncCache:
      break;
    case SyncCache:
      upload = true;
      break;
    }
    bool success = incidence ? doSave( upload, incidence ) : doSave( upload );
    if ( !success && !receivedSaveError() ) {
      saveError();
    } else {
      emit resourceSaved( this );
    }
    return success;
  } else {
    // Read-only, just don't save...
    kDebug() << "Don't save read-only resource" << resourceName();
    return true;
  }
}

bool ResourceCached::save( Incidence *incidence )
{
  return save( SyncCache, incidence );
}

bool ResourceCached::doSave( bool syncCache, Incidence *incidence )
{
  Q_UNUSED( incidence );
  return doSave( syncCache );
}

void ResourceCached::saveToCache()
{
  kDebug() << cacheFile();

  setIdMapperIdentifier();
  d->mIdMapper.save();

  d->mCalendar.save( cacheFile() );
}

void ResourceCached::setIdMapperIdentifier()
{
  d->mIdMapper.setIdentifier( type() + '_' + identifier() );
}

void ResourceCached::clearCache()
{
  d->mCalendar.close();
}

void ResourceCached::cleanUpEventCache( const Event::List &eventList )
{
  CalendarLocal calendar ( QLatin1String( "UTC" ) );

  if ( KStandardDirs::exists( cacheFile() ) ) {
    calendar.load( cacheFile() );
  } else {
    return;
  }

  Event::List list = calendar.events();
  Event::List::ConstIterator cacheIt, it;
  for ( cacheIt = list.constBegin(); cacheIt != list.constEnd(); ++cacheIt ) {
    bool found = false;
    for ( it = eventList.begin(); it != eventList.end(); ++it ) {
      if ( (*it)->uid() == (*cacheIt)->uid() ) {
        found = true;
        break;
      }
    }

    if ( !found ) {
      d->mIdMapper.removeRemoteId( d->mIdMapper.remoteId( (*cacheIt)->uid() ) );
      Event *event = d->mCalendar.event( (*cacheIt)->uid() );
      if ( event ) {
        d->mCalendar.deleteEvent( event );
      }
    }
  }

  calendar.close();
}

void ResourceCached::cleanUpTodoCache( const Todo::List &todoList )
{
  CalendarLocal calendar ( QLatin1String( "UTC" ) );

  if ( KStandardDirs::exists( cacheFile() ) ) {
    calendar.load( cacheFile() );
  } else {
    return;
  }

  Todo::List list = calendar.todos();
  Todo::List::ConstIterator cacheIt, it;
  for ( cacheIt = list.constBegin(); cacheIt != list.constEnd(); ++cacheIt ) {

    bool found = false;
    for ( it = todoList.constBegin(); it != todoList.constEnd(); ++it ) {
      if ( (*it)->uid() == (*cacheIt)->uid() ) {
        found = true;
      }
    }

    if ( !found ) {
      d->mIdMapper.removeRemoteId( d->mIdMapper.remoteId( (*cacheIt)->uid() ) );
      Todo *todo = d->mCalendar.todo( (*cacheIt)->uid() );
      if ( todo ) {
        d->mCalendar.deleteTodo( todo );
      }
    }
  }

  calendar.close();
}

KRES::IdMapper &ResourceCached::idMapper()
{
  return d->mIdMapper;
}

QString ResourceCached::cacheFile() const
{
  return KStandardDirs::locateLocal( "cache", "kcal/kresources/" + identifier() );
}

QString ResourceCached::changesCacheFile( const QString &type ) const
{
  return KStandardDirs::locateLocal( "cache", "kcal/changescache/" + identifier() + '_' + type );
}

void ResourceCached::saveChangesCache( const QMap<Incidence *, bool> &map, const QString &type )
{
  CalendarLocal calendar ( QLatin1String( "UTC" ) );

  bool isEmpty = true;
  QMap<Incidence *,bool>::ConstIterator it;
  for ( it = map.begin(); it != map.end(); ++it ) {
    isEmpty = false;
    calendar.addIncidence( it.key()->clone() );
  }

  if ( !isEmpty ) {
    calendar.save( changesCacheFile( type ) );
  } else {
    QFile file( changesCacheFile( type ) );
    file.remove();
  }

  calendar.close();
}

void ResourceCached::saveChangesCache()
{
  saveChangesCache( d->mAddedIncidences, "added" );
  saveChangesCache( d->mDeletedIncidences, "deleted" );
  saveChangesCache( d->mChangedIncidences, "changed" );
}

void ResourceCached::loadChangesCache( QMap<Incidence *, bool> &map, const QString &type )
{
  CalendarLocal calendar ( QLatin1String( "UTC" ) );

  if ( KStandardDirs::exists( changesCacheFile( type ) ) ) {
    calendar.load( changesCacheFile( type ) );
  } else {
    return;
  }

  const Incidence::List list = calendar.incidences();
  Incidence::List::ConstIterator it;
  for ( it = list.begin(); it != list.end(); ++it ) {
    map.insert( (*it)->clone(), true );
  }

  calendar.close();
}

void ResourceCached::loadChangesCache()
{
  loadChangesCache( d->mAddedIncidences, "added" );
  loadChangesCache( d->mDeletedIncidences, "deleted" );
  loadChangesCache( d->mChangedIncidences, "changed" );
}

void ResourceCached::calendarIncidenceAdded( Incidence *i )
{
  kDebug() << i->uid();

  QMap<Incidence *,bool>::ConstIterator it;
  it = d->mAddedIncidences.constFind( i );
  if ( it == d->mAddedIncidences.constEnd() ) {
    d->mAddedIncidences.insert( i, true );
  }

  checkForAutomaticSave();
}

void ResourceCached::calendarIncidenceChanged( Incidence *i )
{
  kDebug() << i->uid();

  QMap<Incidence *,bool>::ConstIterator it;
  it = d->mChangedIncidences.constFind( i );
  // FIXME: If you modify an added incidence, there's no need to add it to d->mChangedIncidences!
  if ( it == d->mChangedIncidences.constEnd() ) {
    d->mChangedIncidences.insert( i, true );
  }

  checkForAutomaticSave();
}

void ResourceCached::calendarIncidenceDeleted( Incidence *i )
{
  kDebug() << i->uid();

  QMap<Incidence *,bool>::ConstIterator it;
  it = d->mDeletedIncidences.constFind( i );
  if ( it == d->mDeletedIncidences.constEnd() ) {
    d->mDeletedIncidences.insert( i, true );
  }

  checkForAutomaticSave();
}

Incidence::List ResourceCached::addedIncidences() const
{
  Incidence::List added;
  QMap<Incidence *,bool>::ConstIterator it;
  for ( it = d->mAddedIncidences.constBegin(); it != d->mAddedIncidences.constEnd(); ++it ) {
    added.append( it.key() );
  }
  return added;
}

Incidence::List ResourceCached::changedIncidences() const
{
  Incidence::List changed;
  QMap<Incidence *,bool>::ConstIterator it;
  for ( it = d->mChangedIncidences.constBegin(); it != d->mChangedIncidences.constEnd(); ++it ) {
    changed.append( it.key() );
  }
  return changed;
}

Incidence::List ResourceCached::deletedIncidences() const
{
  Incidence::List deleted;
  QMap<Incidence *,bool>::ConstIterator it;
  for ( it = d->mDeletedIncidences.constBegin(); it != d->mDeletedIncidences.constEnd(); ++it ) {
    deleted.append( it.key() );
  }
  return deleted;
}

Incidence::List ResourceCached::allChanges() const
{
  Incidence::List changes;
  QMap<Incidence *,bool>::ConstIterator it;
  for ( it = d->mAddedIncidences.constBegin(); it != d->mAddedIncidences.constEnd(); ++it ) {
    changes.append( it.key() );
  }
  for ( it = d->mChangedIncidences.constBegin(); it != d->mChangedIncidences.constEnd(); ++it ) {
    changes.append( it.key() );
  }
  for ( it = d->mDeletedIncidences.constBegin(); it != d->mDeletedIncidences.constEnd(); ++it ) {
    changes.append( it.key() );
  }
  return changes;
}

bool ResourceCached::hasChanges() const
{
  return !( d->mAddedIncidences.isEmpty() && d->mChangedIncidences.isEmpty() &&
            d->mDeletedIncidences.isEmpty() );
}

void ResourceCached::clearChange( Incidence *incidence )
{
  clearChange( incidence->uid() );
}

void ResourceCached::clearChange( const QString &uid )
{
  QMap<Incidence *, bool>::Iterator it;

  for ( it = d->mAddedIncidences.begin(); it != d->mAddedIncidences.end(); ++it ) {
    if ( it.key()->uid() == uid ) {
      d->mAddedIncidences.erase( it );
      break;
    }
  }

  for ( it = d->mChangedIncidences.begin(); it != d->mChangedIncidences.end(); ++it ) {
    if ( it.key()->uid() == uid ) {
      d->mChangedIncidences.erase( it );
      break;
    }
  }

  for ( it = d->mDeletedIncidences.begin(); it != d->mDeletedIncidences.end(); ++it ) {
    if ( it.key()->uid() == uid ) {
      d->mDeletedIncidences.erase( it );
      break;
    }
  }
}

void ResourceCached::enableChangeNotification()
{
  d->mCalendar.registerObserver( this );
}

void ResourceCached::disableChangeNotification()
{
  d->mCalendar.unregisterObserver( this );
}

void ResourceCached::slotReload()
{
  if ( !isActive() ) {
    return;
  }

  kDebug();

  load( SyncCache );
}

void ResourceCached::slotSave()
{
  if ( !isActive() ) {
    return;
  }

  kDebug();

  save( SyncCache );
}

void ResourceCached::checkForAutomaticSave()
{
  if ( d->mSavePolicy == SaveAlways )  {
    kDebug() << "save now";
    d->mSavePending = true;
    d->mSaveTimer.setSingleShot( true );
    d->mSaveTimer.start( 1 * 1000 ); // 1 second
  } else if ( d->mSavePolicy == SaveDelayed ) {
    kDebug() << "save delayed";
    d->mSavePending = true;
    d->mSaveTimer.setSingleShot( true );
    d->mSaveTimer.start( 15 * 1000 ); // 15 seconds
  }
}

bool ResourceCached::checkForReload()
{
  if ( d->mReloadPolicy == ReloadNever ) {
    return false;
  }
  if ( d->mReloadPolicy == ReloadOnStartup ) {
    return !d->mReloaded;
  }
  return true;
}

bool ResourceCached::checkForSave()
{
  if ( d->mSavePolicy == SaveNever ) {
    return false;
  }
  return true;
}

void ResourceCached::addInfoText( QString &txt ) const
{
  if ( d->mLastLoad.isValid() ) {
    txt += "<br>";
    txt += i18n( "Last loaded: %1",
                 KGlobal::locale()->formatDateTime( d->mLastLoad.toUtc().dateTime() ) );
  }
  if ( d->mLastSave.isValid() ) {
    txt += "<br>";
    txt += i18n( "Last saved: %1",
                 KGlobal::locale()->formatDateTime( d->mLastSave.toUtc().dateTime() ) );
  }
}

void ResourceCached::doClose()
{
  if ( d->mSavePending ) {
    d->mSaveTimer.stop();
  }
  if ( d->mSavePending  ||  d->mSavePolicy == SaveOnExit  ||  d->mSavePolicy == SaveInterval ) {
    save( SyncCache );
  }
  d->mCalendar.close();
}

bool ResourceCached::doOpen()
{
  kDebug() << "Opening resource" << resourceName();
  return true;
}

void KCal::ResourceCached::setOwner( const Person &owner )
{
  d->mCalendar.setOwner( owner );
}

Person KCal::ResourceCached::owner() const
{
  return d->mCalendar.owner();
}
