/*
    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 "modelspy.h"

#include <QDebug>

ModelSpy::ModelSpy(QObject *parent)
  : QObject(parent), QList<QVariantList>(), m_model(0), m_isSpying(false), m_lazyPersist(false)
{
  qRegisterMetaType<QModelIndex>("QModelIndex");
}

void ModelSpy::setModel(QAbstractItemModel *model)
{
  stopSpying();
  m_model = model;
}

void ModelSpy::clearTestData()
{
  m_changeList.clear();
  m_unchangedIndexes.clear();
  m_unchangedPersistentIndexes.clear();
}

void ModelSpy::startSpying()
{

  // If a signal is connected to a slot multiple times, the slot gets called multiple times.
  // As we're doing start and stop spying all the time, we disconnect here first to make sure.
  stopSpying();

  m_isSpying = true;

  connect(m_model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)),
          SLOT(rowsAboutToBeInserted(QModelIndex,int,int)));
  connect(m_model, SIGNAL(rowsInserted(QModelIndex,int,int)),
          SLOT(rowsInserted(QModelIndex,int,int)));
  connect(m_model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)),
          SLOT(rowsAboutToBeRemoved(QModelIndex,int,int)));
  connect(m_model, SIGNAL(rowsRemoved(QModelIndex,int,int)),
          SLOT(rowsRemoved(QModelIndex,int,int)));
  connect(m_model, SIGNAL(layoutAboutToBeChanged()),
          SLOT(layoutAboutToBeChanged()));
  connect(m_model, SIGNAL(layoutChanged()),
          SLOT(layoutChanged()));
  connect(m_model, SIGNAL(modelAboutToBeReset()),
          SLOT(modelAboutToBeReset()));
  connect(m_model, SIGNAL(modelReset()),
          SLOT(modelReset()));
  connect(m_model, SIGNAL(rowsAboutToBeMoved(QModelIndex,int,int,QModelIndex,int)),
          SLOT(rowsAboutToBeMoved(QModelIndex,int,int,QModelIndex,int)));
  connect(m_model, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)),
          SLOT(rowsMoved(QModelIndex,int,int,QModelIndex,int)));

  connect(m_model, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
          SLOT(dataChanged(QModelIndex,QModelIndex)));

  connect(m_model, SIGNAL(destroyed()), SLOT(modelDestroyed()));
}


void ModelSpy::stopSpying()
{
  m_isSpying = false;
  if (!m_model)
    return;

  disconnect(m_model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)),
          this, SLOT(rowsAboutToBeInserted(QModelIndex,int,int)));
  disconnect(m_model, SIGNAL(rowsInserted(QModelIndex,int,int)),
          this, SLOT(rowsInserted(QModelIndex,int,int)));
  disconnect(m_model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)),
          this, SLOT(rowsAboutToBeRemoved(QModelIndex,int,int)));
  disconnect(m_model, SIGNAL(rowsRemoved(QModelIndex,int,int)),
          this, SLOT(rowsRemoved(QModelIndex,int,int)));
  disconnect(m_model, SIGNAL(layoutAboutToBeChanged()),
          this, SLOT(layoutAboutToBeChanged()));
  disconnect(m_model, SIGNAL(layoutChanged()),
          this, SLOT(layoutChanged()));
  disconnect(m_model, SIGNAL(modelAboutToBeReset()),
          this, SLOT(modelAboutToBeReset()));
  disconnect(m_model, SIGNAL(modelReset()),
          this, SLOT(modelReset()));
  disconnect(m_model, SIGNAL(rowsAboutToBeMoved(QModelIndex,int,int,QModelIndex,int)),
          this, SLOT(rowsAboutToBeMoved(QModelIndex,int,int,QModelIndex,int)));
  disconnect(m_model, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)),
          this, SLOT(rowsMoved(QModelIndex,int,int,QModelIndex,int)));

  disconnect(m_model, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
          this, SLOT(dataChanged(QModelIndex,QModelIndex)));

  disconnect(m_model, SIGNAL(destroyed(QObject*)), this, SLOT(modelDestroyed()));
}

void ModelSpy::rowsAboutToBeInserted(const QModelIndex &parent, int start, int end)
{
  append(QVariantList() << RowsAboutToBeInserted << QVariant::fromValue(parent) << start << end);

  if (m_lazyPersist)
    doPersist();
}

void ModelSpy::rowsInserted(const QModelIndex &parent, int start, int end)
{
  append(QVariantList() << RowsInserted << QVariant::fromValue(parent) << start << end);
}

void ModelSpy::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
{
  append(QVariantList() << RowsAboutToBeRemoved << QVariant::fromValue(parent) << start << end);

  if (m_lazyPersist)
    doPersist();
}

void ModelSpy::rowsRemoved(const QModelIndex &parent, int start, int end)
{
  append(QVariantList() << RowsRemoved << QVariant::fromValue(parent) << start << end);
}

void ModelSpy::layoutAboutToBeChanged()
{
  append(QVariantList() << LayoutAboutToBeChanged);

  if (m_lazyPersist)
    doPersist();
}

void ModelSpy::layoutChanged()
{
  append(QVariantList() << LayoutChanged);
}

void ModelSpy::modelAboutToBeReset()
{
  append(QVariantList() << ModelAboutToBeReset);

  // This is called in setSourceModel for example, which is not when we want to persist.
  if (m_lazyPersist && m_model->hasChildren())
    doPersist();
}

void ModelSpy::modelReset()
{
  append(QVariantList() << ModelReset);
}

void ModelSpy::modelDestroyed()
{
  stopSpying();
  m_model = 0;
}

void ModelSpy::rowsAboutToBeMoved(const QModelIndex &srcParent, int start, int end, const QModelIndex &destParent, int destStart)
{
  append(QVariantList() << RowsAboutToBeMoved << QVariant::fromValue(srcParent) << start << end << QVariant::fromValue(destParent) << destStart);

  // Don't do a lazy persist here. That will be done on the layoutAboutToBeChanged signal.
//   if (m_lazyPersist)
//     doPersist();
}

void ModelSpy::rowsMoved(const QModelIndex &srcParent, int start, int end, const QModelIndex &destParent, int destStart)
{
  append(QVariantList() << RowsMoved << QVariant::fromValue(srcParent) << start << end << QVariant::fromValue(destParent) << destStart);
}

void ModelSpy::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
{
  append(QVariantList() << DataChanged << QVariant::fromValue(topLeft) << QVariant::fromValue(bottomRight));
}

QModelIndexList ModelSpy::getDescendantIndexes(const QModelIndex &parent)
{
  QModelIndexList list;
  const int column = 0;
  for(int row = 0; row < m_model->rowCount(parent); ++row)
  {
    QModelIndex idx = m_model->index(row, column, parent);
    list << idx;
    list << getDescendantIndexes(idx);
  }
  return list;
}

QList< QPersistentModelIndex > ModelSpy::toPersistent(QModelIndexList list)
{
  QList<QPersistentModelIndex > persistentList;
  foreach(QModelIndex idx, list)
  {
    persistentList << QPersistentModelIndex(idx);
  }
  return persistentList;
}

QModelIndexList ModelSpy::getUnchangedIndexes(const QModelIndex &parent, QList<QItemSelectionRange> ignoredRanges)
{
  QModelIndexList list;
  int rowCount = m_model->rowCount(parent);
  for (int row = 0; row < rowCount; )
  {
    int column = 0;
    QModelIndex idx = m_model->index( row, column, parent);
    Q_ASSERT(idx.isValid());
    bool found = false;
    foreach(QItemSelectionRange range, ignoredRanges)
    {
      if (range.topLeft().parent() == parent &&  range.topLeft().row() == idx.row())
      {
        row = range.bottomRight().row() + 1;
        found = true;
        break;
      }
    }
    if (!found)
    {
      for (column = 0; column < m_model->columnCount(); ++column )
        list << m_model->index( row, column, parent);
      list << getUnchangedIndexes(idx, ignoredRanges);
      ++row;
    }
  }
  return list;
}

void ModelSpy::preTestPersistIndexes(const PersistentChangeList &changeList)
{
  m_changeList = changeList;
  if (!m_lazyPersist)
    doPersist();
}

void ModelSpy::doPersist()
{
  Q_ASSERT(m_unchangedIndexes.isEmpty());
  Q_ASSERT(m_unchangedPersistentIndexes.isEmpty());

  const int columnCount = m_model->columnCount();
  QMutableListIterator<PersistentIndexChange> it(m_changeList);

  // The indexes are defined by the test are described with IndexFinder before anything in the model exists.
  // Now that the indexes should exist, resolve them in the change objects.
  QList<QItemSelectionRange> changedRanges;

  while (it.hasNext())
  {
    PersistentIndexChange change = it.next();
    change.parentFinder.setModel(m_model);
    QModelIndex parent = change.parentFinder.getIndex();

    Q_ASSERT(change.startRow >= 0);
    Q_ASSERT(change.startRow <= change.endRow);

    if (change.endRow >= m_model->rowCount(parent))
      qDebug() << m_model << parent << change.startRow << change.endRow << parent.data() << m_model->rowCount(parent);

    Q_ASSERT(change.endRow < m_model->rowCount(parent));

    QModelIndex topLeft = m_model->index( change.startRow, 0, parent );
    QModelIndex bottomRight = m_model->index( change.endRow, columnCount - 1, parent );

    // We store the changed ranges so that we know which ranges should not be changed
    changedRanges << QItemSelectionRange(topLeft, bottomRight);

    // Store the inital state of the indexes in the model which we expect to change.
    for (int row = change.startRow; row <= change.endRow; ++row )
    {
      for (int column = 0; column < columnCount; ++column)
      {
        QModelIndex idx = m_model->index(row, column, parent);
        Q_ASSERT(idx.isValid());
        change.indexes << idx;
        change.persistentIndexes << QPersistentModelIndex(idx);
      }

      // Also store the descendants of changed indexes so that we can verify the effect on them
      QModelIndex idx = m_model->index(row, 0, parent);
      QModelIndexList descs = getDescendantIndexes(idx);
      change.descendantIndexes << descs;
      change.persistentDescendantIndexes << toPersistent(descs);
    }
    it.setValue(change);
  }
  // Any indexes outside of the ranges we expect to be changed are stored
  // so that we can later verify that they remain unchanged.
  m_unchangedIndexes = getUnchangedIndexes(QModelIndex(), changedRanges);
  m_unchangedPersistentIndexes = toPersistent(m_unchangedIndexes);
}

static const char * const signaltypes[] = {
  "NoSignal",
  "RowsAboutToBeInserted",
  "RowsInserted",
  "RowsAboutToBeRemoved",
  "RowsRemoved",
  "RowsAboutToBeMoved",
  "RowsMoved",
  "DataChanged",
  "LayoutAboutToBeChanged",
  "LayoutChanged",
  "ModelAboutToBeReset",
  "ModelReset"
};


QDebug operator<<(QDebug d, ModelSpy* modelSpy)
{
  d << "ModelSpy(";
  foreach (const QVariantList &list, (QList<QVariantList>)(*modelSpy)) {
    d << "SIGNAL(";
    int sigType = list.first().toInt();
    d << signaltypes[sigType];
    if (list.size() > 1) {
      d << ", " << list.mid(1);
    }
    d << ")";
  }
  d << ")";
  return d;
}

