/*
 * ark -- archiver for the KDE project
 *
 * Copyright (C) 2007 Henrique Pinto <henrique.pinto@kdemail.net>
 * Copyright (C) 2008-2009 Harald Hvaal <haraldhv@stud.ntnu.no>
 * Copyright (C) 2010-2012 Raphael Kubo da Costa <rakuco@FreeBSD.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.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 */

#include "archivemodel.h"
#include "ark_debug.h"
#include "kerfuffle/archive_kerfuffle.h"
#include "kerfuffle/jobs.h"

#include <KIconLoader>
#include <KLocalizedString>
#include <kio/global.h>

#include <QDateTime>
#include <QDBusConnection>
#include <QElapsedTimer>
#include <QMimeData>
#include <QMimeDatabase>
#include <QPersistentModelIndex>
#include <QRegularExpression>
#include <QUrl>

using namespace Kerfuffle;

class ArchiveDirNode;

//used to speed up the loading of large archives
static ArchiveNode* s_previousMatch = Q_NULLPTR;
Q_GLOBAL_STATIC(QStringList, s_previousPieces)


// TODO: This class hierarchy needs some love.
//       Having a parent take a child class as a parameter in the constructor
//       should trigger one's spider-sense (TM).
class ArchiveNode
{
public:
    ArchiveNode(ArchiveDirNode *parent, const ArchiveEntry & entry)
        : m_parent(parent)
    {
        setEntry(entry);
    }

    virtual ~ArchiveNode()
    {
    }

    const ArchiveEntry &entry() const
    {
        return m_entry;
    }

    void setEntry(const ArchiveEntry& entry)
    {
        m_entry = entry;

        const QStringList pieces = entry[FileName].toString().split(QLatin1Char( '/' ), QString::SkipEmptyParts);
        m_name = pieces.isEmpty() ? QString() : pieces.last();

        QMimeDatabase db;
        if (entry[IsDirectory].toBool()) {
            m_icon = QIcon::fromTheme(db.mimeTypeForName(QStringLiteral("inode/directory")).iconName()).pixmap(IconSize(KIconLoader::Small),
                                                                                                               IconSize(KIconLoader::Small));
        } else {
            m_icon = QIcon::fromTheme(db.mimeTypeForFile(m_entry[FileName].toString()).iconName()).pixmap(IconSize(KIconLoader::Small),
                                                                                                          IconSize(KIconLoader::Small));
        }
    }

    ArchiveDirNode *parent() const
    {
        return m_parent;
    }

    int row() const;

    virtual bool isDir() const
    {
        return false;
    }

    QPixmap icon() const
    {
        return m_icon;
    }

    QString name() const
    {
        return m_name;
    }

protected:
    void setIcon(const QPixmap &icon)
    {
        m_icon = icon;
    }

private:
    ArchiveEntry    m_entry;
    QPixmap         m_icon;
    QString         m_name;
    ArchiveDirNode *m_parent;
};


class ArchiveDirNode: public ArchiveNode
{
public:
    ArchiveDirNode(ArchiveDirNode *parent, const ArchiveEntry & entry)
        : ArchiveNode(parent, entry)
    {
    }

    ~ArchiveDirNode()
    {
        clear();
    }

    QList<ArchiveNode*> entries()
    {
        return m_entries;
    }

    void setEntryAt(int index, ArchiveNode* value)
    {
        m_entries[index] = value;
    }

    void appendEntry(ArchiveNode* entry)
    {
        m_entries.append(entry);
    }

    void removeEntryAt(int index)
    {
        delete m_entries.takeAt(index);
    }

    virtual bool isDir() const
    {
        return true;
    }

    ArchiveNode* find(const QString & name)
    {
        foreach(ArchiveNode *node, m_entries) {
            if (node && (node->name() == name)) {
                return node;
            }
        }
        return 0;
    }

    ArchiveNode* findByPath(const QStringList & pieces, int index = 0)
    {
        if (index == pieces.count()) {
            return 0;
        }

        ArchiveNode *next = find(pieces.at(index));

        if (index == pieces.count() - 1) {
            return next;
        }
        if (next && next->isDir()) {
            return static_cast<ArchiveDirNode*>(next)->findByPath(pieces, index + 1);
        }
        return 0;
    }

    void returnDirNodes(QList<ArchiveDirNode*> *store)
    {
        foreach(ArchiveNode *node, m_entries) {
            if (node->isDir()) {
                store->prepend(static_cast<ArchiveDirNode*>(node));
                static_cast<ArchiveDirNode*>(node)->returnDirNodes(store);
            }
        }
    }

    void clear()
    {
        qDeleteAll(m_entries);
        m_entries.clear();
    }

private:
    QList<ArchiveNode*> m_entries;
};

/**
 * Helper functor used by qStableSort.
 *
 * It always sorts folders before files.
 *
 * @internal
 */
class ArchiveModelSorter
{
public:
    ArchiveModelSorter(int column, Qt::SortOrder order)
        : m_sortColumn(column)
        , m_sortOrder(order)
    {
    }

    virtual ~ArchiveModelSorter()
    {
    }

    inline bool operator()(const QPair<ArchiveNode*, int> &left, const QPair<ArchiveNode*, int> &right) const
    {
        if (m_sortOrder == Qt::AscendingOrder) {
            return lessThan(left, right);
        } else {
            return !lessThan(left, right);
        }
    }

protected:
    bool lessThan(const QPair<ArchiveNode*, int> &left, const QPair<ArchiveNode*, int> &right) const
    {
        const ArchiveNode * const leftNode = left.first;
        const ArchiveNode * const rightNode = right.first;

        // #234373: sort folders before files
        if ((leftNode->isDir()) && (!rightNode->isDir())) {
            return (m_sortOrder == Qt::AscendingOrder);
        } else if ((!leftNode->isDir()) && (rightNode->isDir())) {
            return !(m_sortOrder == Qt::AscendingOrder);
        }

        const QVariant &leftEntry = leftNode->entry()[m_sortColumn];
        const QVariant &rightEntry = rightNode->entry()[m_sortColumn];

        switch (m_sortColumn) {
        case FileName:
            return leftNode->name() < rightNode->name();
        case Size:
        case CompressedSize:
            return leftEntry.toInt() < rightEntry.toInt();
        default:
            return leftEntry.toString() < rightEntry.toString();
        }

        // We should not get here.
        Q_ASSERT(false);
        return false;
    }

private:
    int m_sortColumn;
    Qt::SortOrder m_sortOrder;
};

int ArchiveNode::row() const
{
    if (parent()) {
        return parent()->entries().indexOf(const_cast<ArchiveNode*>(this));
    }
    return 0;
}

ArchiveModel::ArchiveModel(const QString &dbusPathName, QObject *parent)
    : QAbstractItemModel(parent)
    , m_rootNode(new ArchiveDirNode(0, ArchiveEntry()))
    , m_dbusPathName(dbusPathName)
    , m_numberOfFiles(0)
    , m_numberOfFolders(0)
{
}

ArchiveModel::~ArchiveModel()
{
    delete m_rootNode;
    m_rootNode = 0;
}

QVariant ArchiveModel::data(const QModelIndex &index, int role) const
{
    if (index.isValid()) {
        ArchiveNode *node = static_cast<ArchiveNode*>(index.internalPointer());
        switch (role) {
        case Qt::DisplayRole: {
            //TODO: complete the columns
            int columnId = m_showColumns.at(index.column());
            switch (columnId) {
            case FileName:
                return node->name();
            case Size:
                if (node->isDir()) {
                    int dirs;
                    int files;
                    const int children = childCount(index, dirs, files);
                    return KIO::itemsSummaryString(children, files, dirs, 0, false);
                } else if (node->entry().contains(Link)) {
                    return QVariant();
                } else {
                    return KIO::convertSize(node->entry()[ Size ].toULongLong());
                }
            case CompressedSize:
                if (node->isDir() || node->entry().contains(Link)) {
                    return QVariant();
                } else {
                    qulonglong compressedSize = node->entry()[ CompressedSize ].toULongLong();
                    if (compressedSize != 0) {
                        return KIO::convertSize(compressedSize);
                    } else {
                        return QVariant();
                    }
                }
            case Ratio: // TODO: Use node->entry()[Ratio] when available
                if (node->isDir() || node->entry().contains(Link)) {
                    return QVariant();
                } else {
                    qulonglong compressedSize = node->entry()[ CompressedSize ].toULongLong();
                    qulonglong size = node->entry()[ Size ].toULongLong();
                    if (compressedSize == 0 || size == 0) {
                        return QVariant();
                    } else {
                        int ratio = int(100 * ((double)size - compressedSize) / size);
                        return QString(QString::number(ratio) + QStringLiteral(" %"));
                    }
                }

            case Timestamp: {
                const QDateTime timeStamp = node->entry().value(Timestamp).toDateTime();
                return QLocale().toString(timeStamp, QLocale::ShortFormat);
            }

            default:
                return node->entry().value(columnId);
            }
            break;
        }
        case Qt::DecorationRole:
            if (index.column() == 0) {
                return node->icon();
            }
            return QVariant();
        case Qt::FontRole: {
            QFont f;
            f.setItalic(node->entry()[ IsPasswordProtected ].toBool());
            return f;
        }
        default:
            return QVariant();
        }
    }
    return QVariant();
}

Qt::ItemFlags ArchiveModel::flags(const QModelIndex &index) const
{
    Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index);

    if (index.isValid()) {
        return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | defaultFlags;
    }

    return 0;
}

QVariant ArchiveModel::headerData(int section, Qt::Orientation, int role) const
{
    if (role == Qt::DisplayRole) {
        if (section >= m_showColumns.size()) {
            qCDebug(ARK) << "WEIRD: showColumns.size = " << m_showColumns.size()
            << " and section = " << section;
            return QVariant();
        }

        int columnId = m_showColumns.at(section);

        switch (columnId) {
        case FileName:
            return i18nc("Name of a file inside an archive", "Name");
        case Size:
            return i18nc("Uncompressed size of a file inside an archive", "Size");
        case CompressedSize:
            return i18nc("Compressed size of a file inside an archive", "Compressed");
        case Ratio:
            return i18nc("Compression rate of file", "Rate");
        case Owner:
            return i18nc("File's owner username", "Owner");
        case Group:
            return i18nc("File's group", "Group");
        case Permissions:
            return i18nc("File permissions", "Mode");
        case CRC:
            return i18nc("CRC hash code", "CRC");
        case Method:
            return i18nc("Compression method", "Method");
        case Version:
            //TODO: what exactly is a file version?
            return i18nc("File version", "Version");
        case Timestamp:
            return i18nc("Timestamp", "Date");
        case Comment:
            return i18nc("File comment", "Comment");
        default:
            return i18nc("Unnamed column", "??");

        }
    }
    return QVariant();
}

QModelIndex ArchiveModel::index(int row, int column, const QModelIndex &parent) const
{
    if (hasIndex(row, column, parent)) {
        ArchiveDirNode *parentNode = parent.isValid() ? static_cast<ArchiveDirNode*>(parent.internalPointer()) : m_rootNode;

        Q_ASSERT(parentNode->isDir());

        ArchiveNode *item = parentNode->entries().value(row, 0);
        if (item) {
            return createIndex(row, column, item);
        }
    }

    return QModelIndex();
}

QModelIndex ArchiveModel::parent(const QModelIndex &index) const
{
    if (index.isValid()) {
        ArchiveNode *item = static_cast<ArchiveNode*>(index.internalPointer());
        Q_ASSERT(item);
        if (item->parent() && (item->parent() != m_rootNode)) {
            return createIndex(item->parent()->row(), 0, item->parent());
        }
    }
    return QModelIndex();
}

ArchiveEntry ArchiveModel::entryForIndex(const QModelIndex &index)
{
    if (index.isValid()) {
        ArchiveNode *item = static_cast<ArchiveNode*>(index.internalPointer());
        Q_ASSERT(item);
        return item->entry();
    }
    return ArchiveEntry();
}

int ArchiveModel::childCount(const QModelIndex &index, int &dirs, int &files) const
{
    if (index.isValid()) {
        dirs = files = 0;
        ArchiveNode *item = static_cast<ArchiveNode*>(index.internalPointer());
        Q_ASSERT(item);
        if (item->isDir()) {
            const QList<ArchiveNode*> entries = static_cast<ArchiveDirNode*>(item)->entries();
            foreach(const ArchiveNode *node, entries) {
                if (node->isDir()) {
                    dirs++;
                } else {
                    files++;
                }
            }
            return entries.count();
        }
        return 0;
    }
    return -1;
}

int ArchiveModel::rowCount(const QModelIndex &parent) const
{
    if (parent.column() <= 0) {
        ArchiveNode *parentNode = parent.isValid() ? static_cast<ArchiveNode*>(parent.internalPointer()) : m_rootNode;

        if (parentNode && parentNode->isDir()) {
            return static_cast<ArchiveDirNode*>(parentNode)->entries().count();
        }
    }
    return 0;
}

int ArchiveModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent)
    return m_showColumns.size();
}

void ArchiveModel::sort(int column, Qt::SortOrder order)
{
    if (m_showColumns.size() <= column) {
        return;
    }

    emit layoutAboutToBeChanged();

    QList<ArchiveDirNode*> dirNodes;
    m_rootNode->returnDirNodes(&dirNodes);
    dirNodes.append(m_rootNode);

    const ArchiveModelSorter modelSorter(m_showColumns.at(column), order);

    foreach(ArchiveDirNode* dir, dirNodes) {
        QVector < QPair<ArchiveNode*,int> > sorting(dir->entries().count());
        for (int i = 0; i < dir->entries().count(); ++i) {
            ArchiveNode *item = dir->entries().at(i);
            sorting[i].first = item;
            sorting[i].second = i;
        }

        qStableSort(sorting.begin(), sorting.end(), modelSorter);

        QModelIndexList fromIndexes;
        QModelIndexList toIndexes;
        for (int r = 0; r < sorting.count(); ++r) {
            ArchiveNode *item = sorting.at(r).first;
            toIndexes.append(createIndex(r, 0, item));
            fromIndexes.append(createIndex(sorting.at(r).second, 0, sorting.at(r).first));
            dir->setEntryAt(r, sorting.at(r).first);
        }

        changePersistentIndexList(fromIndexes, toIndexes);

        emit dataChanged(
            index(0, 0, indexForNode(dir)),
            index(dir->entries().size() - 1, 0, indexForNode(dir)));
    }

    emit layoutChanged();
}

Qt::DropActions ArchiveModel::supportedDropActions() const
{
    return Qt::CopyAction | Qt::MoveAction;
}

QStringList ArchiveModel::mimeTypes() const
{
    QStringList types;

    // MIME types we accept for dragging (eg. Dolphin -> Ark).
    types << QStringLiteral("text/uri-list")
          << QStringLiteral("text/plain")
          << QStringLiteral("text/x-moz-url");

    // MIME types we accept for dropping (eg. Ark -> Dolphin).
    types << QStringLiteral("application/x-kde-ark-dndextract-service")
          << QStringLiteral("application/x-kde-ark-dndextract-path");

    return types;
}

QMimeData *ArchiveModel::mimeData(const QModelIndexList &indexes) const
{
    Q_UNUSED(indexes)

    QMimeData *mimeData = new QMimeData;
    mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-service"),
                      QDBusConnection::sessionBus().baseService().toUtf8());
    mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-path"),
                      m_dbusPathName.toUtf8());

    return mimeData;
}

bool ArchiveModel::dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent)
{
    Q_UNUSED(action)
    Q_UNUSED(row)
    Q_UNUSED(column)
    Q_UNUSED(parent)

    if (!data->hasUrls()) {
        return false;
    }

    if (archive()->isReadOnly() ||
        (archive()->encryptionType() != Archive::Unencrypted &&
         archive()->password().isEmpty())) {
        emit messageWidget(KMessageWidget::Error, i18n("Adding files is not supported for this archive."));
        return false;
    }

    QStringList paths;
    foreach(const QUrl &url, data->urls()) {
        paths << url.toLocalFile();
    }

    //for now, this code is not used because adding files to paths inside the
    //archive is not supported yet. need a solution for this later.
    QString path;
#if 0
    if (parent.isValid()) {
        QModelIndex droppedOnto = index(row, column, parent);
        if (entryForIndex(droppedOnto).value(IsDirectory).toBool()) {
            qCDebug(ARK) << "Using entry";
            path = entryForIndex(droppedOnto).value(FileName).toString();
        } else {
            path = entryForIndex(parent).value(FileName).toString();
        }
    }

    qCDebug(ARK) << "Dropped onto " << path;

#endif

    emit droppedFiles(paths, path);

    return true;
}

// For a rationale, see bugs #194241, #241967 and #355839
QString ArchiveModel::cleanFileName(const QString& fileName)
{
    // Skip entries with filename "/" or "//" or "."
    // "." is present in ISO files
    QRegularExpression pattern(QStringLiteral("/+|\\."));
    QRegularExpressionMatch match;
    if (fileName.contains(pattern, &match) && match.captured() == fileName) {
        qCDebug(ARK) << "Skipping entry with filename" << fileName;
        return QString();
    } else if (fileName.startsWith(QLatin1String("./"))) {
        return fileName.mid(2);
    }

    return fileName;
}

ArchiveDirNode* ArchiveModel::parentFor(const ArchiveEntry& entry)
{
    QStringList pieces = entry[ FileName ].toString().split(QLatin1Char( '/' ), QString::SkipEmptyParts);
    if (pieces.isEmpty()) {
        return Q_NULLPTR;
    }
    pieces.removeLast();

    if (s_previousMatch) {
        //the number of path elements must be the same for the shortcut
        //to work
        if (s_previousPieces->count() == pieces.count()) {
            bool equal = true;

            //make sure all the pieces match up
            for (int i = 0; i < s_previousPieces->count(); ++i) {
                if (s_previousPieces->at(i) != pieces.at(i)) {
                    equal = false;
                    break;
                }
            }

            //if match return it
            if (equal) {
                return static_cast<ArchiveDirNode*>(s_previousMatch);
            }
        }
    }

    ArchiveDirNode *parent = m_rootNode;

    foreach(const QString &piece, pieces) {
        ArchiveNode *node = parent->find(piece);
        if (!node) {
            ArchiveEntry e;
            e[ FileName ] = (parent == m_rootNode) ?
                            piece + QLatin1Char('/') : parent->entry()[FileName].toString() + piece + QLatin1Char('/');
            e[ InternalID ] = e.value(FileName);
            e[ IsDirectory ] = true;
            node = new ArchiveDirNode(parent, e);
            insertNode(node);
        }
        if (!node->isDir()) {
            ArchiveEntry e(node->entry());
            node = new ArchiveDirNode(parent, e);
            //Maybe we have both a file and a directory of the same name
            // We avoid removing previous entries unless necessary
            insertNode(node);
        }
        parent = static_cast<ArchiveDirNode*>(node);
    }

    s_previousMatch = parent;
    *s_previousPieces = pieces;

    return parent;
}
QModelIndex ArchiveModel::indexForNode(ArchiveNode *node)
{
    Q_ASSERT(node);
    if (node != m_rootNode) {
        Q_ASSERT(node->parent());
        Q_ASSERT(node->parent()->isDir());
        return createIndex(node->row(), 0, node);
    }
    return QModelIndex();
}

void ArchiveModel::slotEntryRemoved(const QString & path)
{
    const QString entryFileName(cleanFileName(path));
    if (entryFileName.isEmpty()) {
        return;
    }

    ArchiveNode *entry = m_rootNode->findByPath(entryFileName.split(QLatin1Char( '/' ), QString::SkipEmptyParts));
    if (entry) {
        ArchiveDirNode *parent = entry->parent();
        QModelIndex index = indexForNode(entry);
        Q_UNUSED(index);

        beginRemoveRows(indexForNode(parent), entry->row(), entry->row());

        //delete parent->entries()[ entry->row() ];
        //parent->entries()[ entry->row() ] = 0;
        parent->removeEntryAt(entry->row());

        endRemoveRows();
    }
}

void ArchiveModel::slotUserQuery(Kerfuffle::Query *query)
{
    query->execute();
}

void ArchiveModel::slotNewEntryFromSetArchive(const ArchiveEntry& entry)
{
    // we cache all entries that appear when opening a new archive
    // so we can all them together once it's done, this is a huge
    // performance improvement because we save from doing lots of
    // begin/endInsertRows
    m_newArchiveEntries.push_back(entry);
}

void ArchiveModel::slotNewEntry(const ArchiveEntry& entry)
{
    newEntry(entry, NotifyViews);
}

void ArchiveModel::newEntry(const ArchiveEntry& receivedEntry, InsertBehaviour behaviour)
{
    if (receivedEntry[FileName].toString().isEmpty()) {
        qCDebug(ARK) << "Weird, received empty entry (no filename) - skipping";
        return;
    }

    //if there are no addidional columns registered, then have a look at the
    //entry and populate some
    if (m_showColumns.isEmpty()) {
        //these are the columns we are interested in showing in the display
        static const QList<int> columnsForDisplay =
            QList<int>()
            << FileName
            << Size
            << CompressedSize
            << Permissions
            << Owner
            << Group
            << Ratio
            << CRC
            << Method
            << Version
            << Timestamp
            << Comment;

        QList<int> toInsert;

        foreach(int column, columnsForDisplay) {
            if (receivedEntry.contains(column)) {
                toInsert << column;
            }
        }
        beginInsertColumns(QModelIndex(), 0, toInsert.size() - 1);
        m_showColumns << toInsert;
        endInsertColumns();

        qCDebug(ARK) << "Showing columns: " << m_showColumns;
    }

    //make a copy
    ArchiveEntry entry = receivedEntry;

    //#194241: Filenames such as "./file" should be displayed as "file"
    //#241967: Entries called "/" should be ignored
    //#355839: Entries called "//" should be ignored
    QString entryFileName = cleanFileName(entry[FileName].toString());
    if (entryFileName.isEmpty()) { // The entry contains only "." or "./"
        return;
    }
    entry[FileName] = entryFileName;

    // For some archive formats (e.g. AppImage and RPM) paths of folders do not
    // contain a trailing slash, so we append it.
    if (entry[IsDirectory].toBool() && !entry[FileName].toString().endsWith(QLatin1Char('/'))) {
        entry[FileName] = entry[FileName].toString() + QLatin1Char('/');
        entry[InternalID] = entry[FileName];
        qCDebug(ARK) << "Trailing slash appended to entry:" << entry[FileName];
    }

    /// 1. Skip already created nodes
    if (m_rootNode) {
        ArchiveNode *existing = m_rootNode->findByPath(entry[ FileName ].toString().split(QLatin1Char( '/' )));
        if (existing) {
            qCDebug(ARK) << "Refreshing entry for" << entry[FileName].toString();

            // Multi-volume files are repeated at least in RAR archives.
            // In that case, we need to sum the compressed size for each volume
            qulonglong currentCompressedSize = existing->entry()[CompressedSize].toULongLong();
            entry[CompressedSize] = currentCompressedSize + entry[CompressedSize].toULongLong();

            //TODO: benchmark whether it's a bad idea to reset the entry here.
            existing->setEntry(entry);
            return;
        }
    }

    /// 2. Find Parent Node, creating missing ArchiveDirNodes in the process
    ArchiveDirNode *parent = parentFor(entry);

    /// 3. Create an ArchiveNode
    const QStringList path = entry[FileName].toString().split(QLatin1Char('/'), QString::SkipEmptyParts);
    const QString name = path.last();
    ArchiveNode *node = parent->find(name);
    if (node) {
        node->setEntry(entry);
    } else {
        if (entry[ FileName ].toString().endsWith(QLatin1Char( '/' )) || (entry.contains(IsDirectory) && entry[ IsDirectory ].toBool())) {
            node = new ArchiveDirNode(parent, entry);
        } else {
            node = new ArchiveNode(parent, entry);
        }
        insertNode(node, behaviour);
    }
}

void ArchiveModel::slotLoadingFinished(KJob *job)
{
    if (!job->error()) {
        int i = 0;
        foreach(const ArchiveEntry &entry, m_newArchiveEntries) {
            newEntry(entry, DoNotNotifyViews);
            i++;
        }
        beginResetModel();
        endResetModel();
        m_newArchiveEntries.clear();

        qCDebug(ARK) << "Added" << i << "entries to model";
    }

    emit loadingFinished(job);
}

void ArchiveModel::insertNode(ArchiveNode *node, InsertBehaviour behaviour)
{
    Q_ASSERT(node);
    ArchiveDirNode *parent = node->parent();
    Q_ASSERT(parent);
    if (behaviour == NotifyViews) {
        beginInsertRows(indexForNode(parent), parent->entries().count(), parent->entries().count());
    }
    parent->appendEntry(node);
    if (behaviour == NotifyViews) {
        endInsertRows();
    }
}

Kerfuffle::Archive* ArchiveModel::archive() const
{
    return m_archive.data();
}

KJob* ArchiveModel::setArchive(Kerfuffle::Archive *archive)
{
    m_archive.reset(archive);

    m_rootNode->clear();
    s_previousMatch = Q_NULLPTR;
    s_previousPieces->clear();

    Kerfuffle::ListJob *job = Q_NULLPTR;

    m_newArchiveEntries.clear();
    if (m_archive) {
        job = m_archive->list(); // TODO: call "open" or "create"?
        if (job) {
            connect(job, &Kerfuffle::ListJob::newEntry, this, &ArchiveModel::slotNewEntryFromSetArchive);
            connect(job, &Kerfuffle::ListJob::result, this, &ArchiveModel::slotLoadingFinished);
            connect(job, &Kerfuffle::ListJob::userQuery, this, &ArchiveModel::slotUserQuery);

            emit loadingStarted();

            // TODO: make sure if it's ok to not have calls to beginRemoveColumns here
            m_showColumns.clear();
        }
    }
    beginResetModel();
    endResetModel();
    return job;
}

ExtractJob* ArchiveModel::extractFile(const QVariant& fileName, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const
{
    QList<QVariant> files;
    files << QVariant::fromValue(fileRootNodePair(fileName.toString()));
    return extractFiles(files, destinationDir, options);
}

ExtractJob* ArchiveModel::extractFiles(const QList<QVariant>& files, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const
{
    Q_ASSERT(m_archive);
    ExtractJob *newJob = m_archive->copyFiles(files, destinationDir, options);
    connect(newJob, &ExtractJob::userQuery, this, &ArchiveModel::slotUserQuery);
    return newJob;
}

Kerfuffle::PreviewJob *ArchiveModel::preview(const QString& file) const
{
    Q_ASSERT(m_archive);
    PreviewJob *job = m_archive->preview(file);
    connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery);
    return job;
}

OpenJob *ArchiveModel::open(const QString& file) const
{
    Q_ASSERT(m_archive);
    OpenJob *job = m_archive->open(file);
    connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery);
    return job;
}

OpenWithJob *ArchiveModel::openWith(const QString& file) const
{
    Q_ASSERT(m_archive);
    OpenWithJob *job = m_archive->openWith(file);
    connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery);
    return job;
}

AddJob* ArchiveModel::addFiles(const QStringList & filenames, const CompressionOptions& options)
{
    if (!m_archive) {
        return Q_NULLPTR;
    }

    if (!m_archive->isReadOnly()) {
        AddJob *job = m_archive->addFiles(filenames, options);
        connect(job, &AddJob::newEntry, this, &ArchiveModel::slotNewEntry);
        connect(job, &AddJob::userQuery, this, &ArchiveModel::slotUserQuery);


        return job;
    }
    return Q_NULLPTR;
}

DeleteJob* ArchiveModel::deleteFiles(const QList<QVariant> & files)
{
    Q_ASSERT(m_archive);
    if (!m_archive->isReadOnly()) {
        DeleteJob *job = m_archive->deleteFiles(files);
        connect(job, &DeleteJob::entryRemoved, this, &ArchiveModel::slotEntryRemoved);

        connect(job, &DeleteJob::finished, this, &ArchiveModel::slotCleanupEmptyDirs);

        connect(job, &DeleteJob::userQuery, this, &ArchiveModel::slotUserQuery);
        return job;
    }
    return Q_NULLPTR;
}

void ArchiveModel::encryptArchive(const QString &password, bool encryptHeader)
{
    if (!m_archive) {
        return;
    }

    m_archive->encrypt(password, encryptHeader);
}

void ArchiveModel::slotCleanupEmptyDirs()
{
    QList<QPersistentModelIndex> queue;
    QList<QPersistentModelIndex> nodesToDelete;

    //add root nodes
    for (int i = 0; i < rowCount(); ++i) {
        queue.append(QPersistentModelIndex(index(i, 0)));
    }

    //breadth-first traverse
    while (!queue.isEmpty()) {
        QPersistentModelIndex node = queue.takeFirst();
        ArchiveEntry entry = entryForIndex(node);

        if (!hasChildren(node)) {
            if (!entry.contains(InternalID)) {
                nodesToDelete << node;
            }
        } else {
            for (int i = 0; i < rowCount(node); ++i) {
                queue.append(QPersistentModelIndex(index(i, 0, node)));
            }
        }
    }

    foreach(const QPersistentModelIndex& node, nodesToDelete) {
        ArchiveNode *rawNode = static_cast<ArchiveNode*>(node.internalPointer());
        qCDebug(ARK) << "Delete with parent entries " << rawNode->parent()->entries() << " and row " << rawNode->row();
        beginRemoveRows(parent(node), rawNode->row(), rawNode->row());
        rawNode->parent()->removeEntryAt(rawNode->row());
        endRemoveRows();
    }
}

void ArchiveModel::countEntriesAndSize() {

    // This function is used to count the number of folders/files and
    // the total compressed size. This is needed for PropertiesDialog
    // to update the corresponding values after adding/deleting files.

    // When ArchiveModel has been properly fixed, this code can likely
    // be removed.

    m_numberOfFiles = 0;
    m_numberOfFolders = 0;
    m_uncompressedSize = 0;

    QElapsedTimer timer;
    timer.start();

    traverseAndCountDirNode(m_rootNode);

    qCDebug(ARK) << "Time to count entries and size:" << timer.elapsed() << "ms";
}

void ArchiveModel::traverseAndCountDirNode(ArchiveDirNode *dir)
{
    foreach(ArchiveNode *node, dir->entries()) {
        if (node->isDir()) {
            traverseAndCountDirNode(dynamic_cast<ArchiveDirNode*>(node));
            m_numberOfFolders++;
        } else {
            m_numberOfFiles++;
            m_uncompressedSize += node->entry()[Size].toULongLong();
        }
    }
}

qulonglong ArchiveModel::numberOfFiles() const
{
    return m_numberOfFiles;
}

qulonglong ArchiveModel::numberOfFolders() const
{
    return m_numberOfFolders;
}

qulonglong ArchiveModel::uncompressedSize() const
{
    return m_uncompressedSize;
}
