/*
 * This file Copyright (C) 2009-2015 Mnemosyne LLC
 *
 * It may be used under the GNU GPL versions 2 or 3
 * or any future license endorsed by Mnemosyne LLC.
 *
 * $Id: FileTreeModel.cc 14609 2015-12-06 17:39:18Z jordan $
 */

#include <cassert>

#include <libtransmission/transmission.h> // priorities

#include "FileTreeItem.h"
#include "FileTreeModel.h"

namespace
{
  class PathIteratorBase
  {
    protected:
      PathIteratorBase(const QString& path, int slashIndex):
        myPath (path),
        mySlashIndex (slashIndex),
        myToken ()
      {
        myToken.reserve (path.size () / 2);
      }

    protected:
      const QString& myPath;
      int mySlashIndex;
      QString myToken;

      static const QChar SlashChar;
  };

  const QChar PathIteratorBase::SlashChar = QLatin1Char ('/');

  class ForwardPathIterator: public PathIteratorBase
  {
    public:
      ForwardPathIterator (const QString& path):
        PathIteratorBase (path, path.size () - 1)
      {
      }

      bool hasNext () const
      {
        return mySlashIndex > 0;
      }

      const QString& next ()
      {
        int newSlashIndex = myPath.lastIndexOf (SlashChar, mySlashIndex);
        myToken.truncate (0);
        myToken += myPath.midRef (newSlashIndex + 1, mySlashIndex - newSlashIndex);
        mySlashIndex = newSlashIndex - 1;
        return myToken;
      }
  };

  class BackwardPathIterator: public PathIteratorBase
  {
    public:
      BackwardPathIterator (const QString& path):
        PathIteratorBase (path, 0)
      {
      }

      bool hasNext () const
      {
        return mySlashIndex < myPath.size ();
      }

      const QString& next ()
      {
        int newSlashIndex = myPath.indexOf (SlashChar, mySlashIndex);
        if (newSlashIndex == -1)
          newSlashIndex = myPath.size ();
        myToken.truncate (0);
        myToken += myPath.midRef (mySlashIndex, newSlashIndex - mySlashIndex);
        mySlashIndex = newSlashIndex + 1;
        return myToken;
      }
  };
}

FileTreeModel::FileTreeModel (QObject * parent, bool isEditable):
  QAbstractItemModel(parent),
  myIsEditable (isEditable),
  myRootItem (new FileTreeItem),
  myIndexCache ()
{
}

FileTreeModel::~FileTreeModel()
{
  clear();

  delete myRootItem;
}

void
FileTreeModel::setEditable (bool editable)
{
  myIsEditable = editable;
}

FileTreeItem *
FileTreeModel::itemFromIndex (const QModelIndex& index) const
{
  if (!index.isValid())
    return nullptr;

  assert (index.model () == this);
  return static_cast<FileTreeItem*>(index.internalPointer());
}

QModelIndexList
FileTreeModel::getOrphanIndices (const QModelIndexList& indices) const
{
  QModelIndexList orphanIndices = indices;

  qSort (orphanIndices);

  for (QMutableListIterator<QModelIndex> it (orphanIndices); it.hasNext ();)
    {
      QModelIndex walk = it.next ();

      for (;;)
        {
          walk = parent (walk, walk.column ());
          if (!walk.isValid ())
            break;

          if (qBinaryFind (orphanIndices, walk) != orphanIndices.end ())
            {
              it.remove ();
              break;
            }
        }
    }

  return orphanIndices;
}

QVariant
FileTreeModel::data (const QModelIndex &index, int role) const
{
  QVariant value;

  if (index.isValid())
    value = itemFromIndex(index)->data (index.column(), role);

  return value;
}

Qt::ItemFlags
FileTreeModel::flags (const QModelIndex& index) const
{
  int i(Qt::ItemIsSelectable | Qt::ItemIsEnabled);

  if(myIsEditable && (index.column() == COL_NAME))
    i |= Qt::ItemIsEditable;

  if(index.column() == COL_WANTED)
    i |= Qt::ItemIsUserCheckable | Qt::ItemIsTristate;

  return Qt::ItemFlags(i);
}

bool
FileTreeModel::setData (const QModelIndex& index, const QVariant& newname, int role)
{
  if (role == Qt::EditRole)
    {
      FileTreeItem * item = itemFromIndex (index);

      emit pathEdited (item->path (), newname.toString ());
    }

  return false; // don't update the view until the session confirms the change
}

QVariant
FileTreeModel::headerData (int column, Qt::Orientation orientation, int role) const
{
  QVariant data;

  if (orientation==Qt::Horizontal && role==Qt::DisplayRole)
    {
      switch (column)
        {
          case COL_NAME:
            data.setValue (tr("File"));
            break;

          case COL_SIZE:
            data.setValue (tr("Size"));
            break;

          case COL_PROGRESS:
            data.setValue (tr("Progress"));
            break;

          case COL_WANTED:
            data.setValue (tr("Download"));
            break;

          case COL_PRIORITY:
            data.setValue (tr("Priority"));
            break;

          default:
            break;
        }
    }

  return data;
}

QModelIndex
FileTreeModel::index (int row, int column, const QModelIndex& parent) const
{
  QModelIndex i;

  if (hasIndex (row, column, parent))
    {
      FileTreeItem * parentItem;

      if (!parent.isValid ())
        parentItem = myRootItem;
      else
        parentItem = itemFromIndex (parent);

      FileTreeItem * childItem = parentItem->child (row);

      if (childItem)
        i = createIndex (row, column, childItem);
    }

  return i;
}

QModelIndex
FileTreeModel::parent (const QModelIndex& child) const
{
  return parent (child, 0); // QAbstractItemModel::parent() wants col 0
}

QModelIndex
FileTreeModel::parent (const QModelIndex& child, int column) const
{
  QModelIndex parent;

  if (child.isValid())
    parent = indexOf (itemFromIndex(child)->parent(), column);

  return parent;
}

int
FileTreeModel::rowCount (const QModelIndex& parent) const
{
  FileTreeItem * parentItem;

  if (parent.isValid())
    parentItem = itemFromIndex (parent);
  else
    parentItem = myRootItem;

  return parentItem->childCount();
}

int
FileTreeModel::columnCount (const QModelIndex& parent) const
{
  Q_UNUSED(parent);

  return NUM_COLUMNS;
}

QModelIndex
FileTreeModel::indexOf (FileTreeItem * item, int column) const
{
  if (!item || item==myRootItem)
    return QModelIndex();

  return createIndex(item->row(), column, item);
}

void
FileTreeModel::clearSubtree (const QModelIndex& top)
{
  size_t i = rowCount (top);

  while (i > 0)
    clearSubtree(index(--i, 0, top));

  FileTreeItem * const item = itemFromIndex (top);
  if (item == 0)
    return;

  if (item->fileIndex () != -1)
    myIndexCache.remove (item->fileIndex ());

  delete item;
}

void
FileTreeModel::clear ()
{
  beginResetModel ();
  clearSubtree (QModelIndex());
  endResetModel ();

  assert (myIndexCache.isEmpty ());
}

FileTreeItem *
FileTreeModel::findItemForFileIndex (int fileIndex) const
{
  return myIndexCache.value (fileIndex, 0);
}

void
FileTreeModel::addFile (int            fileIndex,
                        const QString& filename,
                        bool           wanted,
                        int            priority,
                        uint64_t       totalSize,
                        uint64_t       have,
                        bool           updateFields)
{
  FileTreeItem * item;

  item = findItemForFileIndex (fileIndex);

  if (item) // this file is already in the tree, we've added this
    {
      QModelIndex indexWithChangedParents;
      ForwardPathIterator filenameIt (filename);
      while (filenameIt.hasNext ())
        {
          const QString& token = filenameIt.next ();
          const std::pair<int,int> changed = item->update (token, wanted, priority, have, updateFields);
          if (changed.first >= 0)
            {
              emit dataChanged (indexOf (item, changed.first), indexOf (item, changed.second));
              if (!indexWithChangedParents.isValid () &&
                  changed.first <= COL_PRIORITY && changed.second >= COL_SIZE)
                indexWithChangedParents = indexOf (item, 0);
            }
          item = item->parent();
        }
      assert (item == myRootItem);
      if (indexWithChangedParents.isValid ())
        emitParentsChanged (indexWithChangedParents, COL_SIZE, COL_PRIORITY);
    }
  else // we haven't build the FileTreeItems for these tokens yet
    {
      bool added = false;

      item = myRootItem;
      BackwardPathIterator filenameIt (filename);
      while (filenameIt.hasNext ())
        {
          const QString& token = filenameIt.next ();
          FileTreeItem * child(item->child(token));
          if (!child)
            {
              added = true;
              QModelIndex parentIndex (indexOf(item, 0));
              const int n (item->childCount());

              beginInsertRows (parentIndex, n, n);
              if (!filenameIt.hasNext ())
                child = new FileTreeItem (token, fileIndex, totalSize);
              else
                child = new FileTreeItem (token);
              item->appendChild (child);
              endInsertRows ();
            }
          item = child;
        }

      if (item != myRootItem)
        {
          assert (item->fileIndex() == fileIndex);
          assert (item->totalSize() == totalSize);

          myIndexCache[fileIndex] = item;

          const std::pair<int,int> changed = item->update (item->name(), wanted, priority, have, added || updateFields);
          if (changed.first >= 0)
            emit dataChanged (indexOf (item, changed.first), indexOf (item, changed.second));
        }
    }
}

void
FileTreeModel::emitParentsChanged (const QModelIndex& index, int firstColumn, int lastColumn, QSet<QModelIndex> * visitedParentIndices)
{
  assert (firstColumn <= lastColumn);

  QModelIndex walk = index;

  for (;;)
    {
      walk = parent (walk, firstColumn);
      if (!walk.isValid ())
        break;

      if (visitedParentIndices != nullptr)
        {
          if (visitedParentIndices->contains (walk))
            break;
          visitedParentIndices->insert (walk);
        }

      emit dataChanged (walk, walk.sibling (walk.row (), lastColumn));
    }
}

void
FileTreeModel::emitSubtreeChanged (const QModelIndex& index, int firstColumn, int lastColumn)
{
  assert (firstColumn <= lastColumn);

  const int childCount = rowCount (index);
  if (!childCount)
    return;

  // tell everyone that this item changed
  emit dataChanged (index.child (0, firstColumn), index.child (childCount - 1, lastColumn));

  // walk the subitems
  for (int i=0; i<childCount; ++i)
    emitSubtreeChanged (index.child (i, 0), firstColumn, lastColumn);
}

void
FileTreeModel::twiddleWanted (const QModelIndexList& indices)
{
  QMap<bool, QModelIndexList> wantedIndices;
  for (const QModelIndex& i: getOrphanIndices (indices))
    {
      const FileTreeItem * const item = itemFromIndex (i);
      wantedIndices[item->isSubtreeWanted () != Qt::Checked] << i;
    }

  for (int i = 0; i <= 1; ++i)
    {
      if (wantedIndices.contains (i))
        setWanted (wantedIndices[i], i != 0);
    }
}

void
FileTreeModel::twiddlePriority (const QModelIndexList& indices)
{
  QMap<int, QModelIndexList> priorityIndices;
  for (const QModelIndex& i: getOrphanIndices (indices))
    {
      const FileTreeItem * const item = itemFromIndex (i);
      int priority = item->priority ();

      // ... -> normal -> high -> low -> normal -> ...; mixed -> normal
      if (priority == FileTreeItem::NORMAL)
        priority = TR_PRI_HIGH;
      else if (priority == FileTreeItem::HIGH)
        priority = TR_PRI_LOW;
      else
        priority = TR_PRI_NORMAL;

      priorityIndices[priority] << i;
    }

  for (int i = TR_PRI_LOW; i <= TR_PRI_HIGH; ++i)
    {
      if (priorityIndices.contains (i))
        setPriority (priorityIndices[i], i);
    }
}

void
FileTreeModel::setWanted (const QModelIndexList& indices, bool wanted)
{
  if (indices.isEmpty ())
    return;

  const QModelIndexList orphanIndices = getOrphanIndices (indices);

  QSet<int> fileIds;
  for (const QModelIndex& i: orphanIndices)
    {
      FileTreeItem * const item = itemFromIndex (i);
      item->setSubtreeWanted (wanted, fileIds);

      emit dataChanged (i, i);
      emitSubtreeChanged (i, COL_WANTED, COL_WANTED);
    }

  // emit parent changes separately to avoid multiple updates for same items
  QSet<QModelIndex> parentIndices;
  for (const QModelIndex& i: orphanIndices)
    emitParentsChanged (i, COL_SIZE, COL_WANTED, &parentIndices);

  if (!fileIds.isEmpty ())
    emit wantedChanged (fileIds, wanted);
}

void
FileTreeModel::setPriority (const QModelIndexList& indices, int priority)
{
  if (indices.isEmpty ())
    return;

  const QModelIndexList orphanIndices = getOrphanIndices (indices);

  QSet<int> fileIds;
  for (const QModelIndex& i: orphanIndices)
    {
      FileTreeItem * const item = itemFromIndex (i);
      item->setSubtreePriority (priority, fileIds);

      emit dataChanged (i, i);
      emitSubtreeChanged (i, COL_PRIORITY, COL_PRIORITY);
    }

  // emit parent changes separately to avoid multiple updates for same items
  QSet<QModelIndex> parentIndices;
  for (const QModelIndex& i: orphanIndices)
    emitParentsChanged (i, COL_PRIORITY, COL_PRIORITY, &parentIndices);

  if (!fileIds.isEmpty ())
    emit priorityChanged (fileIds, priority);
}

bool
FileTreeModel::openFile (const QModelIndex& index)
{
  if (!index.isValid ())
    return false;

  FileTreeItem * const item = itemFromIndex (index);
  if (item->fileIndex () < 0 || !item->isComplete ())
    return false;

  emit openRequested (item->path ());
  return true;
}
