/******************************************************************************
 *
 *  File : preprocessormanager.h
 *  Creation date : Sat 18 Jul 2009 01:58:50
 *
 *  Copyright (c) 2009 Szymon Stefanek <s.stefanek at gmail dot com>
 *
 *  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.
 *
 *****************************************************************************/

#ifndef AKONADI_PREPROCESSORMANAGER_H
#define AKONADI_PREPROCESSORMANAGER_H

#include <QtCore/QObject>
#include <QtCore/QList>
#include <QtCore/QHash>

#include <deque>

class QTimer;
class QMutex;

#include "preprocessorinstance.h"


namespace Akonadi
{

class PimItem;
class DataStore;

/**
 * \class PreprocessorManager
 * \brief The manager for preprocessor agents
 *
 * This class takes care of synchronizing the preprocessor agents.
 *
 * The preprocessors see the incoming PimItem objects before the user
 * can see them (as long as the UI applications honor the hidden attribute).
 * The items are marked as hidden (by the Append and AkAppend
 * handlers) and then enqueued to the preprocessor chain via this class.
 * Once all the preprocessors have done their work the item is unhidden again.
 * 
 * Preprocessing isn't designed for critical tasks. There may
 * be circumstances under that the Akonadi server fails to push an item
 * to all the preprocessors. Most notably after a server restart all
 * the items for that preprocessing was interrupted are just unhidden
 * without any attempt to resume the preprocessor jobs.
 *
 * The enqueue requests may or may not arrive from "inside" a database
 * transaction. The uncommitted transaction would "hide" the newly created items
 * from the preprocessor instances (which are separate processes).
 * This class, then, takes care of holding the newly arrived items
 * in a wait queue until their transaction is committed (or rolled back).
 */
class PreprocessorManager : public QObject
{
  friend class PreprocessorInstance;

  Q_OBJECT
  Q_CLASSINFO( "D-Bus Interface", "org.freedesktop.Akonadi.PreprocessorManager" )

protected:

  /**
   * Creates an instance of PreprocessorManager
   */
  PreprocessorManager();

  /**
   * Destroys the instance of PreprocessorManager
   * and frees all the relevant resources
   */
  ~PreprocessorManager();

protected:

  /**
   * The one and only instance pointer for the class PreprocessorManager
   */
  static PreprocessorManager * mSelf;

  /**
   * The hashtable of transaction wait queues. There is one wait
   * queue for each DataStore that is currently in a transaction.
   */
  QHash< const DataStore *, std::deque< qint64 > * > mTransactionWaitQueueHash;

  /**
   * The preprocessor chain.
   * The pointers inside the list are owned.
   *
   * In all the algorithms we assume that this list is actually very short
   * (say 3-4 elements) and reverse lookup (pointer->index) is really fast.
   */
  QList< PreprocessorInstance * > mPreprocessorChain;

  /**
   * Is preprocessing enabled at all in ths Akonad server instance ?
   * This is true by default and can be set via setEnabled().
   * Mainly used to disable preprocessing via configuration file.
   */
  bool mEnabled;

  /**
   * The mutex used to protect the internals of this class  (mainly
   * the mPreprocessorChain member).
   */
  QMutex * mMutex;

  /**
   * The heartbeat timer. Used mainly to expire preprocessor jobs.
   */
  QTimer * mHeartbeatTimer;

public:

  /**
   * Returns the one and only instance pointer for the class PreprocessorManager
   * The returned pointer is valid only after a succesfull call to init().
   *
   * \sa init()
   * \sa done()
   */
  static PreprocessorManager * instance()
  {
    return mSelf;
  }

  /**
   * Initializes this class singleton by creating its one and only instance.
   * This is actually called in the AkonadiServer constructor.
   *
   * The instance is later available via the static instance() method.
   * You must call done() when you've finished using this class services.
   * Returns true upon succesfull initialisation and false when the initialization fails.
   *
   * \sa done()
   */
  static bool init();

  /**
   * Deinitializes this class singleton (if it was initialized at all).
   * This is actually called in the AkonadiServer::quit() method.
   *
   * \sa init()
   */
  static void done();

  /**
   * Returns true if preprocessing is active in this Akonadi server.
   * This means that we have at least one active preprocessor and
   * preprocessing hasn't been explicitly disabled via configuration
   * (so if isActive() returns true then also isEnabled() will return true).
   *
   * This function is thread-safe.
   */
  bool isActive();

  /**
   * Returns true if this preprocessor hasn't been explicitly disabled
   * via setEnabled( false ). This is used to disable preprocessing
   * via configuration even if we have a valid chain of preprocessors.
   *
   * Please note that this flag doesn't tell if we actually have
   * some registered preprocessors and thus we can do some meaningful job.
   * You should use isActive() for this purpose.
   */
  bool isEnabled() const
  {
    return mEnabled;
  }

  /**
   * Explicitly enables or disables the preprocessing in this Akonadi server.
   * The PreprocessorManager starts in enabled state but can be disabled
   * at a later stage: this is mainly used to disable preprocessing via
   * configuration.
   *
   * Please note that setting this to true doesn't interrupt the currently
   * running preprocessing jobs. Anything that was enqueued will be processed
   * anyway. However, in Akonadi this is only invoked very early,
   * when no preprocessors are alive yet.
   */
  void setEnabled( bool enabled )
  {
    mEnabled = enabled;
  }

  /**
   * Trigger the preprocessor chain for the specified item.
   * The item should have been added to the Akonadi database via
   * the specified DataStore object. If the DataStore is in a
   * transaction then this class will put the item in a wait
   * queue until the transaction is committed. If the transaction
   * is rolled back the whole wait queue will be discarded.
   * If the DataStore is not in a transaction then the item
   * will be pushed directly to the preprocessing chain.
   *
   * You should make sure that the preprocessor chain isActive()
   * before calling this method. The items you pass to this method,
   * also, should have the hidden attribute set.
   *
   * This function is thread-safe.
   */
  void beginHandleItem( const PimItem &item, const DataStore * dataStore );

  /**
   * This is called via D-Bus from AgentManager to register a preprocessor instance.
   *
   * This function is thread-safe.
   */
  void registerInstance( const QString &id );

  /**
   * This is called via D-Bus from AgentManager to unregister a preprocessor instance.
   *
   * This function is thread-safe.
   */
  void unregisterInstance( const QString &id );

protected:

  /**
   * This is called by PreprocessorInstance to signal that a certain preprocessor has finished
   * handling an item.
   *
   * This function is thread-safe.
   */
  void preProcessorFinishedHandlingItem( PreprocessorInstance * preProcessor, qint64 itemId );

private:

  /**
   * Finds the preprocessor instance by its identifier.
   *
   * This must be called with mMutex locked.
   */
  PreprocessorInstance * lockedFindInstance( const QString &id );

  /**
   * Pushes the specified item to the first preprocessor.
   * The caller *MUST* make sure that there is at least one preprocessor in the chain.
   */
  void lockedActivateFirstPreprocessor( qint64 itemId );

  /**
   * This is called internally to terminate the pre-processing
   * chain for the specified Item. All the preprocessors have
   * been triggered for it.
   *
   * This must be called with mMutex locked.
   */
  void lockedEndHandleItem( qint64 itemId );

  /**
   * This is the unprotected core of the unregisterInstance() function above.
   */
  void lockedUnregisterInstance( const QString &id );

  /**
   * Kill the wait queue for the specific DataStore object.
   */
  void lockedKillWaitQueue( const DataStore * dataStore, bool disconnectSlots );

private Q_SLOTS:

  /**
   * Connected to the mHeartbeatTimer. Triggered every minute or something like that :D
   * Mainly used to expire preprocessor jobs.
   */
  void heartbeat();

  /**
   * This is used to handle database transactions and wait queues.
   * The call to this slot usually comes from a queued signal/slot connection
   * (i.e. from the *Append handler thread).
   */
  void dataStoreDestroyed();

  /**
   * This is used to handle database transactions and wait queues.
   * The call to this slot usually comes from a queued signal/slot connection
   * (i.e. from the *Append handler thread).
   */
  void dataStoreTransactionCommitted();

  /**
   * This is used to handle database transactions and wait queues.
   * The call to this slot usually comes from a queued signal/slot connection
   * (i.e. from the *Append handler thread).
   */
  void dataStoreTransactionRolledBack();

}; // class PreprocessorManager

} // namespace Akonadi

#endif //!_PREPROCESSORMANAGER_H_
