/*
    Copyright (c) 2009 Stephen Kelly <steveire@gmail.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.
*/

#include "krecursivefilterproxymodel.h"

#include <kdebug.h>

// Maintainability note:
// This class invokes some Q_PRIVATE_SLOTs in QSortFilterProxyModel which are
// private API and could be renamed or removed at any time.
// If they are renamed, the invokations can be updated with an #if (QT_VERSION(...))
// If they are removed, then layout{AboutToBe}Changed signals should be used when the source model
// gets new rows or has rowsremoved or moved. The Q_PRIVATE_SLOT invokation is an optimization
// because layout{AboutToBe}Changed is expensive and causes the entire mapping of the tree in QSFPM
// to be cleared, even if only a part of it is dirty.
// Stephen Kelly, 30 April 2010.

class KRecursiveFilterProxyModelPrivate
{
  Q_DECLARE_PUBLIC(KRecursiveFilterProxyModel)
  KRecursiveFilterProxyModel *q_ptr;
public:
  KRecursiveFilterProxyModelPrivate(KRecursiveFilterProxyModel *model)
    : q_ptr(model),
      ignoreRemove(false),
      completeInsert(false),
      completeRemove(false)
  {
    qRegisterMetaType<QModelIndex>( "QModelIndex" );
  }

  // Convenience methods for invoking the QSFPM slots. Those slots must be invoked with invokeMethod
  // because they are Q_PRIVATE_SLOTs
  inline void invokeDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
  {
    Q_Q(KRecursiveFilterProxyModel);
    bool success = QMetaObject::invokeMethod(q, "_q_sourceDataChanged", Qt::DirectConnection,
        Q_ARG(QModelIndex, topLeft),
        Q_ARG(QModelIndex, bottomRight));
    Q_UNUSED(success);
    Q_ASSERT(success);
  }

  inline void invokeRowsInserted(const QModelIndex &source_parent, int start, int end)
  {
    Q_Q(KRecursiveFilterProxyModel);
    bool success = QMetaObject::invokeMethod(q, "_q_sourceRowsInserted", Qt::DirectConnection,
        Q_ARG(QModelIndex, source_parent),
        Q_ARG(int, start),
        Q_ARG(int, end));
    Q_UNUSED(success);
    Q_ASSERT(success);
  }

  inline void invokeRowsAboutToBeInserted(const QModelIndex &source_parent, int start, int end)
  {
    Q_Q(KRecursiveFilterProxyModel);
    bool success = QMetaObject::invokeMethod(q, "_q_sourceRowsAboutToBeInserted", Qt::DirectConnection,
        Q_ARG(QModelIndex, source_parent),
        Q_ARG(int, start),
        Q_ARG(int, end));
    Q_UNUSED(success);
    Q_ASSERT(success);
  }

  inline void invokeRowsRemoved(const QModelIndex &source_parent, int start, int end)
  {
    Q_Q(KRecursiveFilterProxyModel);
    bool success = QMetaObject::invokeMethod(q, "_q_sourceRowsRemoved", Qt::DirectConnection,
        Q_ARG(QModelIndex, source_parent),
        Q_ARG(int, start),
        Q_ARG(int, end));
    Q_UNUSED(success);
    Q_ASSERT(success);
  }

  inline void invokeRowsAboutToBeRemoved(const QModelIndex &source_parent, int start, int end)
  {
    Q_Q(KRecursiveFilterProxyModel);
    bool success = QMetaObject::invokeMethod(q, "_q_sourceRowsAboutToBeRemoved", Qt::DirectConnection,
        Q_ARG(QModelIndex, source_parent),
        Q_ARG(int, start),
        Q_ARG(int, end));
    Q_UNUSED(success);
    Q_ASSERT(success);
  }

  void sourceDataChanged(const QModelIndex &source_top_left, const QModelIndex &source_bottom_right);
  void sourceRowsAboutToBeInserted(const QModelIndex &source_parent, int start, int end);
  void sourceRowsInserted(const QModelIndex &source_parent, int start, int end);
  void sourceRowsAboutToBeRemoved(const QModelIndex &source_parent, int start, int end);
  void sourceRowsRemoved(const QModelIndex &source_parent, int start, int end);

  /**
    Given that @p index does not match the filter, clear mappings in the QSortFilterProxyModel up to and excluding the
    first ascendant that does match, and remake the mappings.

    If @p refreshAll is true, this method also refreshes intermediate mappings. This is significant when removing rows.
  */
  void refreshAscendantMapping(const QModelIndex &index, bool refreshAll = false);

  bool ignoreRemove;
  bool completeInsert;
  bool completeRemove;
};

void KRecursiveFilterProxyModelPrivate::sourceDataChanged(const QModelIndex &source_top_left, const QModelIndex &source_bottom_right)
{
  Q_Q(KRecursiveFilterProxyModel);

  QModelIndex source_parent = source_top_left.parent();

  if (!source_parent.isValid() || q->filterAcceptsRow(source_parent.row(), source_parent.parent()))
  {
    invokeDataChanged(source_top_left, source_bottom_right);
    return;
  }

  bool requireRow = false;
  for (int row = source_top_left.row(); row <= source_bottom_right.row(); ++row)
    if (q->filterAcceptsRow(row, source_parent))
    {
      requireRow = true;
      break;
    }

  if (!requireRow) // None of the changed rows are now required in the model.
    return;

  refreshAscendantMapping(source_parent);
}

void KRecursiveFilterProxyModelPrivate::refreshAscendantMapping(const QModelIndex &index, bool refreshAll)
{
  Q_Q(KRecursiveFilterProxyModel);

  Q_ASSERT(index.isValid());
  QModelIndex lastAscendant = index;
  QModelIndex sourceAscendant = index.parent();
  // We got a matching descendant, so find the right place to insert the row.
  // We need to tell the QSortFilterProxyModel that the first child between an existing row in the model
  // has changed data so that it will get a mapping.
  while(sourceAscendant.isValid() && !q->acceptRow(sourceAscendant.row(), sourceAscendant.parent()))
  {
    if (refreshAll)
      invokeDataChanged(sourceAscendant, sourceAscendant);

    lastAscendant = sourceAscendant;
    sourceAscendant = sourceAscendant.parent();
  }

  // Inform the model that its data changed so that it creates new mappings and finds the rows which now match the filter.
  invokeDataChanged(lastAscendant, lastAscendant);
}

void KRecursiveFilterProxyModelPrivate::sourceRowsAboutToBeInserted(const QModelIndex &source_parent, int start, int end)
{
  Q_Q(KRecursiveFilterProxyModel);

  if (!source_parent.isValid() || q->filterAcceptsRow(source_parent.row(), source_parent.parent()))
  {
    invokeRowsAboutToBeInserted(source_parent, start, end);
    completeInsert = true;
  }
}

void KRecursiveFilterProxyModelPrivate::sourceRowsInserted(const QModelIndex &source_parent, int start, int end)
{
  Q_Q(KRecursiveFilterProxyModel);

  if (completeInsert)
  {
    completeInsert = false;
    invokeRowsInserted(source_parent, start, end);
    // If the parent is already in the model, we can just pass on the signal.
    return;
  }

  bool requireRow = false;
  for (int row = start; row <= end; ++row)
  {
    if (q->filterAcceptsRow(row, source_parent))
    {
      requireRow = true;
      break;
    }
  }

  if (!requireRow)
  {
    // The row doesn't have descendants that match the filter. Filter it out.
    return;
  }

  refreshAscendantMapping(source_parent);
}

void KRecursiveFilterProxyModelPrivate::sourceRowsAboutToBeRemoved(const QModelIndex &source_parent, int start, int end)
{
  Q_Q(KRecursiveFilterProxyModel);

  if (source_parent.isValid() && q->filterAcceptsRow(source_parent.row(), source_parent.parent()))
  {
    invokeRowsAboutToBeRemoved(source_parent, start, end);
    completeRemove = true;
    return;
  }

  bool accepted = false;
  for (int row = start; row <= end; ++row)
  {
    if (q->filterAcceptsRow(row, source_parent))
    {
      accepted = true;
      break;
    }
  }
  if (!accepted)
  {
    // All removed rows are already filtered out. We don't care about the signal.
    ignoreRemove = true;
    return;
  }
  completeRemove = true;
  invokeRowsAboutToBeRemoved(source_parent, start, end);
}

void KRecursiveFilterProxyModelPrivate::sourceRowsRemoved(const QModelIndex &source_parent, int start, int end)
{
  if (completeRemove)
  {
    completeRemove = false;
    // Source parent is already in the model.
    invokeRowsRemoved(source_parent, start, end);
    // fall through. After removing rows, we need to refresh things so that intermediates will be removed too if necessary.
  }

  if (ignoreRemove)
  {
    ignoreRemove = false;
    return;
  }

  // Refresh intermediate rows too.
  // This is needed because QSFPM only invalidates the mapping for the
  // index range given to dataChanged, not its children.
  if (source_parent.isValid())
    refreshAscendantMapping(source_parent, true);
}

KRecursiveFilterProxyModel::KRecursiveFilterProxyModel(QObject* parent)
    : QSortFilterProxyModel(parent), d_ptr(new KRecursiveFilterProxyModelPrivate(this))
{
  setDynamicSortFilter(true);
}

KRecursiveFilterProxyModel::~KRecursiveFilterProxyModel()
{
  delete d_ptr;
}

bool KRecursiveFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
{
  // TODO: Implement some caching so that if one match is found on the first pass, we can return early results
  // when the subtrees are checked by QSFPM.
  if (acceptRow(sourceRow, sourceParent))
    return true;

  QModelIndex source_index = sourceModel()->index(sourceRow, 0, sourceParent);
  Q_ASSERT(source_index.isValid());
  bool accepted = false;

  for (int row = 0 ; row < sourceModel()->rowCount(source_index); ++row)
    if (filterAcceptsRow(row, source_index))
      accepted = true; // Need to do this in a loop so that all siblings in a parent get processed, not just the first.

  return accepted;
}

QModelIndexList KRecursiveFilterProxyModel::match( const QModelIndex& start, int role, const QVariant& value, int hits, Qt::MatchFlags flags ) const
{
  if ( role < Qt::UserRole )
    return QSortFilterProxyModel::match( start, role, value, hits, flags );

  QModelIndexList list;
  QModelIndex proxyIndex;
  foreach ( const QModelIndex &idx, sourceModel()->match( mapToSource( start ), role, value, hits, flags ) ) {
    proxyIndex = mapFromSource( idx );
    if ( proxyIndex.isValid() )
      list << proxyIndex;
  }

  return list;
}

bool KRecursiveFilterProxyModel::acceptRow(int sourceRow, const QModelIndex& sourceParent) const
{
  return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent);
}

void KRecursiveFilterProxyModel::setSourceModel(QAbstractItemModel* model)
{
  // Standard disconnect.
  disconnect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
      this, SLOT(sourceDataChanged(QModelIndex,QModelIndex)));

  disconnect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)),
      this, SLOT(sourceRowsAboutToBeInserted(QModelIndex,int,int)));

  disconnect(model, SIGNAL(rowsInserted(QModelIndex,int,int)),
      this, SLOT(sourceRowsInserted(QModelIndex,int,int)));

  disconnect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)),
      this, SLOT(sourceRowsAboutToBeRemoved(QModelIndex,int,int)));

  disconnect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)),
      this, SLOT(sourceRowsRemoved(QModelIndex,int,int)));

  QSortFilterProxyModel::setSourceModel(model);

  // Disconnect in the QSortFilterProxyModel. These methods will be invoked manually
  // in invokeDataChanged, invokeRowsInserted etc.
  //
  // The reason for that is that when the source model adds new rows for example, the new rows
  // May not match the filter, but maybe their child items do match.
  //
  // Source model before insert:
  //
  // - A
  // - B
  // - - C
  // - - D
  // - - - E
  // - - - F
  // - - - G
  // - H
  // - I
  //
  // If the A F and L (which doesn't exist in the source model yet) match the filter
  // the proxy will be:
  //
  // - A
  // - B
  // - - D
  // - - - F
  //
  // New rows are inserted in the source model below H:
  //
  // - A
  // - B
  // - - C
  // - - D
  // - - - E
  // - - - F
  // - - - G
  // - H
  // - - J
  // - - K
  // - - - L
  // - I
  //
  // As L matches the filter, it should be part of the KRecursiveFilterProxyModel.
  //
  // - A
  // - B
  // - - D
  // - - - F
  // - H
  // - - K
  // - - - L
  //
  // when the QSortFilterProxyModel gets a notification about new rows in H, it only checks
  // J and K to see if they match, ignoring L, and therefore not adding it to the proxy.
  // To work around that, we make sure that the QSFPM slot which handles that change in
  // the source model (_q_sourceRowsAboutToBeInserted) does not get called directly.
  // Instead we connect the sourceModel signal to our own slot in *this (sourceRowsAboutToBeInserted)
  // Inside that method, the entire new subtree is queried (J, K *and* L) to see if there is a match,
  // then the relevant slots in QSFPM are invoked.
  // In the example above, we need to tell the QSFPM that H should be queried again to see if
  // it matches the filter. It did not before, because L did not exist before. Now it does. That is
  // achieved by telling the QSFPM that the data changed for H, which causes it to requery this class
  // to see if H matches the filter (which it now does as L now exists).
  // That is done in refreshAscendantMapping.

  disconnect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
      this, SLOT(_q_sourceDataChanged(QModelIndex,QModelIndex)));

  disconnect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)),
      this, SLOT(_q_sourceRowsAboutToBeInserted(QModelIndex,int,int)));

  disconnect(model, SIGNAL(rowsInserted(QModelIndex,int,int)),
      this, SLOT(_q_sourceRowsInserted(QModelIndex,int,int)));

  disconnect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)),
      this, SLOT(_q_sourceRowsAboutToBeRemoved(QModelIndex,int,int)));

  disconnect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)),
      this, SLOT(_q_sourceRowsRemoved(QModelIndex,int,int)));

  // Slots for manual invoking of QSortFilterProxyModel methods.
  connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
      this, SLOT(sourceDataChanged(QModelIndex,QModelIndex)));

  connect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)),
      this, SLOT(sourceRowsAboutToBeInserted(QModelIndex,int,int)));

  connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)),
      this, SLOT(sourceRowsInserted(QModelIndex,int,int)));

  connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)),
      this, SLOT(sourceRowsAboutToBeRemoved(QModelIndex,int,int)));

  connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)),
      this, SLOT(sourceRowsRemoved(QModelIndex,int,int)));

}

#include "krecursivefilterproxymodel.moc"
