/***************************************************************************
 *   Copyright (C) 2006 by Pino Toscano <pino@kde.org>                     *
 *   Copyright (C) 2017    Klarälvdalens Datakonsult AB, a KDAB Group      *
 *                         company, info@kdab.com. Work sponsored by the   *
 *                         LiMux project of the city of Munich             *
 *                                                                         *
 *   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.                                   *
 ***************************************************************************/

#include "annotationmodel.h"

#include <QLinkedList>
#include <QList>
#include <QPointer>

#include <KLocalizedString>
#include <QIcon>

#include "core/annotations.h"
#include "core/document.h"
#include "core/observer.h"
#include "core/page.h"
#include "ui/guiutils.h"

struct AnnItem {
    AnnItem();
    AnnItem(AnnItem *parent, Okular::Annotation *ann);
    AnnItem(AnnItem *parent, int page);
    ~AnnItem();

    AnnItem(const AnnItem &) = delete;
    AnnItem &operator=(const AnnItem &) = delete;

    AnnItem *parent;
    QList<AnnItem *> children;

    Okular::Annotation *annotation;
    int page;
};

static QLinkedList<Okular::Annotation *> filterOutWidgetAnnotations(const QLinkedList<Okular::Annotation *> &annotations)
{
    QLinkedList<Okular::Annotation *> result;

    for (Okular::Annotation *annotation : annotations) {
        if (annotation->subType() == Okular::Annotation::AWidget)
            continue;

        result.append(annotation);
    }

    return result;
}

class AnnotationModelPrivate : public Okular::DocumentObserver
{
public:
    AnnotationModelPrivate(AnnotationModel *qq);
    ~AnnotationModelPrivate() override;

    void notifySetup(const QVector<Okular::Page *> &pages, int setupFlags) override;
    void notifyPageChanged(int page, int flags) override;

    QModelIndex indexForItem(AnnItem *item) const;
    void rebuildTree(const QVector<Okular::Page *> &pages);
    AnnItem *findItem(int page, int *index) const;

    AnnotationModel *q;
    AnnItem *root;
    QPointer<Okular::Document> document;
};

AnnItem::AnnItem()
    : parent(nullptr)
    , annotation(nullptr)
    , page(-1)
{
}

AnnItem::AnnItem(AnnItem *_parent, Okular::Annotation *ann)
    : parent(_parent)
    , annotation(ann)
    , page(_parent->page)
{
    Q_ASSERT(!parent->annotation);
    parent->children.append(this);
}

AnnItem::AnnItem(AnnItem *_parent, int _page)
    : parent(_parent)
    , annotation(nullptr)
    , page(_page)
{
    Q_ASSERT(!parent->parent);
    parent->children.append(this);
}

AnnItem::~AnnItem()
{
    qDeleteAll(children);
}

AnnotationModelPrivate::AnnotationModelPrivate(AnnotationModel *qq)
    : q(qq)
    , root(new AnnItem)
{
}

AnnotationModelPrivate::~AnnotationModelPrivate()
{
    delete root;
}

static void updateAnnotationPointer(AnnItem *item, const QVector<Okular::Page *> &pages)
{
    if (item->annotation) {
        item->annotation = pages[item->page]->annotation(item->annotation->uniqueName());
        if (!item->annotation)
            qWarning() << "Lost annotation on document save, something went wrong";
    }

    for (AnnItem *child : qAsConst(item->children)) {
        updateAnnotationPointer(child, pages);
    }
}

void AnnotationModelPrivate::notifySetup(const QVector<Okular::Page *> &pages, int setupFlags)
{
    if (!(setupFlags & Okular::DocumentObserver::DocumentChanged)) {
        if (setupFlags & Okular::DocumentObserver::UrlChanged) {
            // Here with UrlChanged and no document changed it means we
            // need to update all the Annotation* otherwise
            // they still point to the old document ones, luckily the old ones are still
            // around so we can look for the new ones using unique ids, etc
            updateAnnotationPointer(root, pages);
        }
        return;
    }

    q->beginResetModel();
    qDeleteAll(root->children);
    root->children.clear();

    rebuildTree(pages);
    q->endResetModel();
}

void AnnotationModelPrivate::notifyPageChanged(int page, int flags)
{
    // we are strictly interested in annotations
    if (!(flags & Okular::DocumentObserver::Annotations))
        return;

    const QLinkedList<Okular::Annotation *> annots = filterOutWidgetAnnotations(document->page(page)->annotations());
    int annItemIndex = -1;
    AnnItem *annItem = findItem(page, &annItemIndex);
    // case 1: the page has no more annotations
    //         => remove the branch, if any
    if (annots.isEmpty()) {
        if (annItem) {
            q->beginRemoveRows(indexForItem(root), annItemIndex, annItemIndex);
            delete root->children.at(annItemIndex);
            root->children.removeAt(annItemIndex);
            q->endRemoveRows();
        }
        return;
    }
    // case 2: no existing branch
    //         => add a new branch, and add the annotations for the page
    if (!annItem) {
        int i = 0;
        while (i < root->children.count() && root->children.at(i)->page < page)
            ++i;

        AnnItem *annItem = new AnnItem();
        annItem->page = page;
        annItem->parent = root;
        q->beginInsertRows(indexForItem(root), i, i);
        annItem->parent->children.insert(i, annItem);
        q->endInsertRows();
        QLinkedList<Okular::Annotation *>::ConstIterator it = annots.begin(), itEnd = annots.end();
        int newid = 0;
        for (; it != itEnd; ++it, ++newid) {
            q->beginInsertRows(indexForItem(annItem), newid, newid);
            new AnnItem(annItem, *it);
            q->endInsertRows();
        }
        return;
    }
    // case 3: existing branch, less annotations than items
    //         => lookup and remove the annotations
    if (annItem->children.count() > annots.count()) {
        for (int i = annItem->children.count(); i > 0; --i) {
            Okular::Annotation *ref = annItem->children.at(i - 1)->annotation;
            bool found = false;
            QLinkedList<Okular::Annotation *>::ConstIterator it = annots.begin(), itEnd = annots.end();
            for (; !found && it != itEnd; ++it) {
                if ((*it) == ref)
                    found = true;
            }
            if (!found) {
                q->beginRemoveRows(indexForItem(annItem), i - 1, i - 1);
                delete annItem->children.at(i - 1);
                annItem->children.removeAt(i - 1);
                q->endRemoveRows();
            }
        }
        return;
    }
    // case 4: existing branch, less items than annotations
    //         => lookup and add annotations if not in the branch
    if (annots.count() > annItem->children.count()) {
        QLinkedList<Okular::Annotation *>::ConstIterator it = annots.begin(), itEnd = annots.end();
        for (; it != itEnd; ++it) {
            Okular::Annotation *ref = *it;
            bool found = false;
            int count = annItem->children.count();
            for (int i = 0; !found && i < count; ++i) {
                if (ref == annItem->children.at(i)->annotation)
                    found = true;
            }
            if (!found) {
                q->beginInsertRows(indexForItem(annItem), count, count);
                new AnnItem(annItem, ref);
                q->endInsertRows();
            }
        }
        return;
    }
    // case 5: the data of some annotation changed
    // TODO: what do we do in this case?
    // FIXME: for now, update ALL the annotations for that page
    for (int i = 0; i < annItem->children.count(); ++i) {
        QModelIndex index = indexForItem(annItem->children.at(i));
        emit q->dataChanged(index, index);
    }
}

QModelIndex AnnotationModelPrivate::indexForItem(AnnItem *item) const
{
    if (item->parent) {
        int id = item->parent->children.indexOf(item);
        if (id >= 0 && id < item->parent->children.count())
            return q->createIndex(id, 0, item);
    }
    return QModelIndex();
}

void AnnotationModelPrivate::rebuildTree(const QVector<Okular::Page *> &pages)
{
    if (pages.isEmpty())
        return;

    emit q->layoutAboutToBeChanged();
    for (int i = 0; i < pages.count(); ++i) {
        const QLinkedList<Okular::Annotation *> annots = filterOutWidgetAnnotations(pages.at(i)->annotations());
        if (annots.isEmpty())
            continue;

        AnnItem *annItem = new AnnItem(root, i);
        QLinkedList<Okular::Annotation *>::ConstIterator it = annots.begin(), itEnd = annots.end();
        for (; it != itEnd; ++it) {
            new AnnItem(annItem, *it);
        }
    }
    emit q->layoutChanged();
}

AnnItem *AnnotationModelPrivate::findItem(int page, int *index) const
{
    for (int i = 0; i < root->children.count(); ++i) {
        AnnItem *tmp = root->children.at(i);
        if (tmp->page == page) {
            if (index)
                *index = i;
            return tmp;
        }
    }
    if (index)
        *index = -1;
    return nullptr;
}

AnnotationModel::AnnotationModel(Okular::Document *document, QObject *parent)
    : QAbstractItemModel(parent)
    , d(new AnnotationModelPrivate(this))
{
    d->document = document;

    d->document->addObserver(d);
}

AnnotationModel::~AnnotationModel()
{
    if (d->document)
        d->document->removeObserver(d);

    delete d;
}

int AnnotationModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent)
    return 1;
}

QVariant AnnotationModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();

    AnnItem *item = static_cast<AnnItem *>(index.internalPointer());
    if (!item->annotation) {
        if (role == Qt::DisplayRole)
            return i18n("Page %1", item->page + 1);
        else if (role == Qt::DecorationRole)
            return QIcon::fromTheme(QStringLiteral("text-plain"));
        else if (role == PageRole)
            return item->page;

        return QVariant();
    }
    switch (role) {
    case Qt::DisplayRole:
        return GuiUtils::captionForAnnotation(item->annotation);
        break;
    case Qt::DecorationRole:
        return QIcon::fromTheme(QStringLiteral("okular"));
        break;
    case Qt::ToolTipRole:
        return GuiUtils::prettyToolTip(item->annotation);
        break;
    case AuthorRole:
        return item->annotation->author();
        break;
    case PageRole:
        return item->page;
        break;
    }
    return QVariant();
}

bool AnnotationModel::hasChildren(const QModelIndex &parent) const
{
    if (!parent.isValid())
        return true;

    AnnItem *item = static_cast<AnnItem *>(parent.internalPointer());
    return !item->children.isEmpty();
}

QVariant AnnotationModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if (orientation != Qt::Horizontal)
        return QVariant();

    if (section == 0 && role == Qt::DisplayRole)
        return QString::fromLocal8Bit("Annotations");

    return QVariant();
}

QModelIndex AnnotationModel::index(int row, int column, const QModelIndex &parent) const
{
    if (row < 0 || column != 0)
        return QModelIndex();

    AnnItem *item = parent.isValid() ? static_cast<AnnItem *>(parent.internalPointer()) : d->root;
    if (row < item->children.count())
        return createIndex(row, column, item->children.at(row));

    return QModelIndex();
}

QModelIndex AnnotationModel::parent(const QModelIndex &index) const
{
    if (!index.isValid())
        return QModelIndex();

    AnnItem *item = static_cast<AnnItem *>(index.internalPointer());
    return d->indexForItem(item->parent);
}

int AnnotationModel::rowCount(const QModelIndex &parent) const
{
    AnnItem *item = parent.isValid() ? static_cast<AnnItem *>(parent.internalPointer()) : d->root;
    return item->children.count();
}

bool AnnotationModel::isAnnotation(const QModelIndex &index) const
{
    return annotationForIndex(index);
}

Okular::Annotation *AnnotationModel::annotationForIndex(const QModelIndex &index) const
{
    if (!index.isValid())
        return nullptr;

    AnnItem *item = static_cast<AnnItem *>(index.internalPointer());
    return item->annotation;
}

#include "moc_annotationmodel.cpp"
