/***************************************************************************
                          contactlistmodelfilter.cpp  -  description
                             -------------------
    begin                : Thu Jul 10 2008
    copyright            : (C) 2008 by Valerio Pilo
    email                : valerio@kmess.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.                                   *
 *                                                                         *
 ***************************************************************************/

#include "contactlistmodelfilter.h"

#include "../contact/specialgroups.h"
#include "../currentaccount.h"
#include "../kmessdebug.h"
#include "contactlistmodelitem.h"


#ifdef KMESSDEBUG_CONTACTLISTMODEL
//   #define KMESSDEBUG_CONTACTLISTMODELFILTER_VISIBILITY
#endif


// The constructor
ContactListModelFilter::ContactListModelFilter( QObject *parent )
: QSortFilterProxyModel( parent )
, currentAccount_( 0 )
, showAllowed_   ( true )
, showEmpty_     ( false )
, showGroupsMode_( Account::VIEW_MIXED )
, showOffline_   ( true )
, showRemoved_   ( true )
{
  // Set filtering options
  setDynamicSortFilter( true );
  setFilterKeyColumn( 0 );
  setFilterRole( ContactListModelItem::SearchRole );
  setFilterCaseSensitivity( Qt::CaseInsensitive );
  setSortRole( ContactListModelItem::SortRole );
  setSortCaseSensitivity( Qt::CaseInsensitive );
}



/**
 * Dumps the contact list contents to the debug output
 *
 * This method is temporary and for debugging purposes exclusively.
 * This method is recursive. Do not call with any arguments, they are for internal use only.
 *
 * @param start Item in the model where to start from
 * @param depth Current search depth
 */
void ContactListModelFilter::dump( QModelIndex start, int depth ) const
{
#ifdef KMESSDEBUG_CONTACTLISTMODEL
  bool started = false;

  if( ! start.isValid() )
  {
    start = QModelIndex();
    kmDebug() << "Contact List Filter model stats";
    kmDebug() << "--------------------------------------";
    kmDebug() << "Columns:"            << columnCount();
    kmDebug() << "Rows:"               << rowCount();
    kmDebug();
    kmDebug() << "Contact List Filter model dump";
    kmDebug() << "--------------------------------------";
    kmDebug() << "Root @" << start;
    started = true;
  }

  QString spacer( QString().fill( '-', 2 * depth ) + ">" );

  // Print the current node's contents, and manage children recursively
  for( int i = 0; i < rowCount( start ); i++ )
  {
    QModelIndex itemIndex( index( i, 0, start ) );
    ModelDataList itemData( itemIndex.data().toMap() );

    switch( itemData["type"].toInt() )
    {
      case ContactListModelItem::ItemContact:
        kmDebug() << spacer << (i+1) << ") Contact:"
                           << itemData["handle"].toString()
                           << "@" << itemIndex;
        break;

      case ContactListModelItem::ItemGroup:
        kmDebug() << spacer << (i+1) << ") Group:"
                           << itemData["id"].toString()
                           << "@" << itemIndex;
        dump( itemIndex, depth + 1 );
        break;

      default:
        kmDebug() << spacer << "Unknown type of item: " + itemData["type"].toInt() << "@" << itemIndex;
        break;
    }
  }

  if( started )
  {
    kmDebug() << "--------------------------------------";
    kmDebug() << "Persistent indices:" << endl << persistentIndexList();
    kmDebug() << "--------------------------------------";
  }
#else
  Q_UNUSED( start );
  Q_UNUSED( depth );
#endif
}



/**
 * @brief Determine if a row is visible or not
 *
 * Uses the internal data from the given index to determine if, according to the current
 * account options, a row should be hid or shown.
 *
 * @param sourceRow     Where the item to check is located inside the sourceParent index
 * @param sourceParent  The index containing the item to check
 */
bool ContactListModelFilter::filterAcceptsRow( int sourceRow, const QModelIndex& sourceParent ) const
{
  bool isVisible = true;

  // Retrieve the item data from the source model
  const QModelIndex index( sourceModel()->index( sourceRow, 0, sourceParent ) );

  if( ! index.isValid() || ! index.data().isValid() || ! index.data().canConvert( QVariant::Map ) )
  {
    return false;
  }

  const ModelDataList itemData( index.data().toMap() );

  if( itemData.isEmpty() )
  {
#ifdef KMESSDEBUG_CONTACTLISTMODEL
    kmDebug() << "Item" << index << "does not contain any data!";
#endif
    return false;
  }


  /**
   * Contact searches HOWTO: Most groups should be kept visible - the view will
   * autonomously hide any group without results (we can't do it here, because
   * the list is recursively iterated by the filter, so the group is filtered
   * before its children are).
   * The groups of the current view mode should be kept visible: we must however
   * make sure that, regardless of the current view mode, all possible contacts
   * are included in the search.
   */


  switch( itemData[ "type" ].toInt() )
  {
    case ContactListModelItem::ItemContact:
    {
      // don't show non-messenger users.
      if ( itemData[ "isMessengerUser" ].toBool() == false )
      {
        return false;
      }

      // Manage contact searches: the list view sets the filter expression, and we use it here;
      // however only contacts within their original group are shown
      if( ! filterRegExp().isEmpty() )
      {
        isVisible = (  itemData[ "group" ] != SpecialGroups::ONLINE
                    && itemData[ "group" ] != SpecialGroups::OFFLINE );
        isVisible = isVisible && ( QSortFilterProxyModel::filterAcceptsRow( sourceRow, sourceParent ) );
        break;
      }

      Status contactStatus = (Status) itemData[ "status" ].toInt();

      // Contacts in special groups are always visible. Their group may be not, though.
      // Also apply filtering to contacts in the Individuals group
      if( ! itemData[ "isInSpecialGroup" ].toBool()
      || itemData[ "group" ] == SpecialGroups::INDIVIDUALS )
      {
        switch( showGroupsMode_ )
        {
          /**
           * When showing the list by status, the user-defined groups are not visible,
           * therefore the contacts within aren't, either, regardless if the contacts
           * are visible or not.
           */
          case Account::VIEW_BYSTATUS:
            // Keep the item visible anyways.
            isVisible = true;
            break;

          /**
           * When showing by group, a contact is visible depending on the 'Show Offline
           * Contacts' option:
           *
           * -  If it is enabled: an offline contact should be visible within its group,
           *    but not within the Offline group (which will be hidden)
           * -  If it is disabled: an offline contact should always be hidden *except when searching*.
           */
          case Account::VIEW_BYGROUP:
            isVisible = ( showOffline_ || contactStatus != STATUS_OFFLINE || ! filterRegExp().isEmpty() );
            break;

          /**
           * On other view modes, a contact is visible if it's not offline.
           */
          default:
            isVisible = ( ( (Status) itemData[ "status" ].toInt() ) != STATUS_OFFLINE );
            break;
        }
      }
    }
    break;


    case ContactListModelItem::ItemGroup:
    {
      QString id( itemData[ "id" ].toString() );

      /**
       * The Allowed and Removed groups are visible only depending on their list view
       * options, not the group mode. In the next switch, these two groups will be
       * ignored because they're always handled here.
       * When searching, these two groups need to always be included in the search,
       * and therefore need to be kept visible.
       */
      if( id == SpecialGroups::ALLOWED )
      {
        isVisible = ( showAllowed_ || ! filterRegExp().isEmpty() );
        break;
      }
      else if( id == SpecialGroups::REMOVED )
      {
        isVisible = ( showRemoved_ || ! filterRegExp().isEmpty() );
        break;
      }

      // if we are searching in a group which is not special, and contains at least 1 contact (total),
      // show it: its data must be available in the model for searching.
      if( ! filterRegExp().isEmpty()
      &&  ( itemData[ "isSpecialGroup" ].toBool() == false || id == SpecialGroups::INDIVIDUALS )
      && itemData[ "totalContacts" ].toInt() > 0 )
      {
        isVisible = true;
        break;
      }

      switch( showGroupsMode_ )
      {
        /**
         * When showing the list by status, the user-defined groups are not visible;
         * the Online special group is visible; the Offline group is visible if the
         * "Show Offline Contacts" option is enabled; and the Individuals group is
         * not visible.
         * When searching, the Offline group cannot be hidden, or searching would
         * miss offline contacts.
         */
        case Account::VIEW_BYSTATUS:
          if( id == SpecialGroups::ONLINE )
          {
            isVisible = true;
          }
          else if( id == SpecialGroups::OFFLINE )
          {
            isVisible = ( showOffline_ || ! filterRegExp().isEmpty() );
          }
          else
          {
            isVisible = false;
          }
          break;

        /**
         * When showing by group, the user-defined and the Individuals groups are
         * visible (if they're not empty and the "Show Empty Groups" option is on),
         * but the Online and Offline ones are not.
         * Searching does not affect this mode.
         */
        case Account::VIEW_BYGROUP:
          if( itemData[ "isSpecialGroup" ].toBool() == false )
          {
            // TODO: Groups with no contacts at all (like new groups) are always
            // shown, because they would disappear from the view and that would
            // be confusing for users. Maybe there's a better way?
            if( ! showEmpty_ && itemData[ "totalContacts" ].toInt() > 0 )
            {
              isVisible = ( itemData[ "onlineContacts" ].toInt() > 0 );
            }
          }
          else if( id == SpecialGroups::INDIVIDUALS )
          {
            isVisible = ( itemData[ "totalContacts" ].toInt() > 0 );
            break;
          }
          else
          {
            isVisible = false;
          }
          break;

        /**
         * On the Mixed view mode, the Online group is hidden; the Offline group is shown
         * if the user enables the "Show Offline Contacts" option; the Individuals
         * group and the user-defined ones are hidden if they're empty (and if the
         * "Show Empty Groups" option is off).
         * Here, too, searching requires the Offline group to be shown.
         */
        case Account::VIEW_MIXED:
          if( id == SpecialGroups::ONLINE )
          {
            isVisible = false;
          }
          else if( id == SpecialGroups::OFFLINE )
          {
            isVisible = ( showOffline_ || ! filterRegExp().isEmpty() );
          }
          else if( id == SpecialGroups::INDIVIDUALS )
          {
            isVisible = ( itemData[ "totalContacts" ].toInt() > 0 );
            break;
          }
          else // User-defined groups
          {
            // TODO: Groups with no contacts at all (like new groups) are always
            // shown, because they would disappear from the view and that would
            // be confusing for users. Maybe there's a better way?
            if( ! showEmpty_ && itemData[ "totalContacts" ].toInt() > 0 )
            {
              isVisible = ( itemData[ "onlineContacts" ].toInt() > 0 );
            }
          }
          break;
      }
    }
    break;

    // Invalid items
    default:
      kmWarning() << "Unknown contents for index:" << index;
      kmWarning() << "-- Item data:" << itemData;
      break;

  }

#ifdef KMESSDEBUG_CONTACTLISTMODELFILTER_VISIBILITY
  switch( itemData[ "type" ].toInt() )
  {
    case ContactListModelItem::ItemContact:
      kmDebug() << "Visibility check: contact" << itemData[ "handle" ].toString() << "is"
               << ( isVisible ? "visible" : "hidden" );
      break;
    case ContactListModelItem::ItemGroup:
      kmDebug() << "Visibility check: group" << itemData[ "name" ].toString() << "is"
               << ( isVisible ? "visible" : "hidden" );
      break;
    default:
      break;
  }
#endif

  return isVisible;
}



/**
 * @brief Sets the given sourceModel to be processed by the proxy model.
 *
 * Overridden from QSortFilterProxyModel to instantly refresh the proxy
 * with the current view settings.
 *
 * @param sourceModel   Model to filter
 */
void ContactListModelFilter::setSourceModel( QAbstractItemModel *sourceModel )
{
  QSortFilterProxyModel::setSourceModel( sourceModel );

  // Update the account-dependant options and re-sort the list
  updateOptions();
}



/**
 * @brief Update the internal filter/sort options
 *
 * Updates the internal preferences to reflect the current account's
 */
void ContactListModelFilter::updateOptions()
{
#ifdef KMESSDEBUG_CONTACTLISTMODEL
  kmDebug() << "Updating account options from" << sender() << "( this:" << this << ", source model:" << sourceModel() << ")";
#endif

  // Connect the account signals that we need
  if( CurrentAccount::instance() != currentAccount_ )
  {
    currentAccount_ = CurrentAccount::instance();
    connect( currentAccount_, SIGNAL( changedDisplaySettings() ),
             this,            SLOT  (          updateOptions() ) );
    connect( currentAccount_, SIGNAL(        changedViewMode() ),
             this,            SLOT  (          updateOptions() ) );
  }

  // Read the settings
  showAllowed_    = currentAccount_->getShowAllowedContacts();
  showRemoved_    = currentAccount_->getShowRemovedContacts();
  showGroupsMode_ = currentAccount_->getContactListDisplayMode();
  showOffline_    = currentAccount_->getShowOfflineContacts();
  showEmpty_      = currentAccount_->getShowEmptyGroups();

  // Invalidate the proxy model, to update sorting and visibility of the model's items
  invalidate();
  sort( 0, Qt::AscendingOrder );
}



#include "contactlistmodelfilter.moc"
