/*
    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)
  {
    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);

  /**
    Force QSortFilterProxyModel to re-evaluate whether to hide or show index and its parents.
  */
  void refreshAscendantMapping(const QModelIndex &index);

  QModelIndex lastFilteredOutAscendant(const QModelIndex &index);

  bool ignoreRemove;
  bool completeInsert;

  QModelIndex lastHiddenAscendantForInsert;
};

void KRecursiveFilterProxyModelPrivate::sourceDataChanged(const QModelIndex &source_top_left, const QModelIndex &source_bottom_right)
{
  QModelIndex source_parent = source_top_left.parent();
  Q_ASSERT(source_bottom_right.parent() == source_parent); // don't know how to handle different parents in this code...

  // Tell the world.
  invokeDataChanged(source_top_left, source_bottom_right);

  // We can't find out if the change really matters to us or not, for a lack of a dataAboutToBeChanged signal (or a cache).
  // TODO: add a set of roles that we care for, so we can at least ignore the rest.

  // Even if we knew the visibility was just toggled, we also can't find out what
  // was the last filtered out ascendant (on show, like sourceRowsAboutToBeInserted does)
  // or the last to-be-filtered-out ascendant (on hide, like sourceRowsRemoved does)
  // So we have to refresh all parents.
  QModelIndex sourceParent = source_parent;
  while(sourceParent.isValid())
  {
    invokeDataChanged(sourceParent, sourceParent);
    sourceParent = sourceParent.parent();
  }
}

QModelIndex KRecursiveFilterProxyModelPrivate::lastFilteredOutAscendant(const QModelIndex &idx)
{
    Q_Q(KRecursiveFilterProxyModel);
    QModelIndex last = idx;
    QModelIndex index = idx.parent();
    while(index.isValid() && !q->filterAcceptsRow(index.row(), index.parent()))
    {
        last = index;
        index = index.parent();
    }
    return last;
}

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()))
  {
    // If the parent is already in the model (directly or indirectly), we can just pass on the signal.
    invokeRowsAboutToBeInserted(source_parent, start, end);
    completeInsert = true;
  } else {
    // OK, so parent is not in the model.
    // Maybe the grand parent neither.. Go up until the first one that is.
    lastHiddenAscendantForInsert = lastFilteredOutAscendant(source_parent);
  }
}

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

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

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

  if (!requireRow)
  {
    // The new rows doesn't have any descendants that match the filter. Filter them out.
    return;
  }

  // Make QSFPM realize that lastHiddenAscendantForInsert should be shown now
  invokeDataChanged(lastHiddenAscendantForInsert, lastHiddenAscendantForInsert);
}

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

  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;
  }

  invokeRowsAboutToBeRemoved(source_parent, start, end);
}

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

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

  invokeRowsRemoved(source_parent, start, end);

  // Find out if removing this visible row means that some ascendant
  // row can now be hidden.
  // We go up until we find a row that should still be visible
  // and then make QSFPM re-evaluate the last one we saw before that, to hide it.

  QModelIndex toHide;
  QModelIndex sourceAscendant = source_parent;
  while(sourceAscendant.isValid())
  {
    if (q->filterAcceptsRow(sourceAscendant.row(), sourceAscendant.parent())) {
        break;
    }
    toHide = sourceAscendant;
    sourceAscendant = sourceAscendant.parent();
  }
  if (toHide.isValid())
      invokeDataChanged(toHide, toHide);
}

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, rows = sourceModel()->rowCount(source_index); row < rows; ++row) {
    if (filterAcceptsRow(row, source_index)) {
      accepted = true;
      break;
    }
  }

  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 sourceRowsInserted.

  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"
