/****************************************************************************
**
** Copyright (C) 2020 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing/
**
** This file is part of the QtPDF module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL3$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see http://www.qt.io/terms-conditions. For further
** information use the contact form at http://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPLv3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or later as published by the Free
** Software Foundation and appearing in the file LICENSE.GPL included in
** the packaging of this file. Please review the following information to
** ensure the GNU General Public License version 2.0 requirements will be
** met: http://www.gnu.org/licenses/gpl-2.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "qpdfbookmarkmodel.h"

#include "qpdfdocument.h"
#include "qpdfdocument_p.h"

#include "third_party/pdfium/public/fpdf_doc.h"
#include "third_party/pdfium/public/fpdfview.h"

#include <QPointer>
#include <QScopedPointer>
#include <private/qabstractitemmodel_p.h>

QT_BEGIN_NAMESPACE

class BookmarkNode
{
public:
    explicit BookmarkNode(BookmarkNode *parentNode = nullptr)
        : m_parentNode(parentNode)
        , m_level(0)
        , m_pageNumber(0)
    {
    }

    ~BookmarkNode()
    {
        clear();
    }

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

    void appendChild(BookmarkNode *child)
    {
        m_childNodes.append(child);
    }

    BookmarkNode *child(int row) const
    {
        return m_childNodes.at(row);
    }

    int childCount() const
    {
        return m_childNodes.count();
    }

    int row() const
    {
        if (m_parentNode)
            return m_parentNode->m_childNodes.indexOf(const_cast<BookmarkNode*>(this));

        return 0;
    }

    BookmarkNode *parentNode() const
    {
        return m_parentNode;
    }

    QString title() const
    {
        return m_title;
    }

    void setTitle(const QString &title)
    {
        m_title = title;
    }

    int level() const
    {
        return m_level;
    }

    void setLevel(int level)
    {
        m_level = level;
    }

    int pageNumber() const
    {
        return m_pageNumber;
    }

    void setPageNumber(int pageNumber)
    {
        m_pageNumber = pageNumber;
    }

private:
    QVector<BookmarkNode*> m_childNodes;
    BookmarkNode *m_parentNode;

    QString m_title;
    int m_level;
    int m_pageNumber;
};


class QPdfBookmarkModelPrivate : public QAbstractItemModelPrivate
{
public:
    QPdfBookmarkModelPrivate()
        : QAbstractItemModelPrivate()
        , m_rootNode(new BookmarkNode(nullptr))
        , m_document(nullptr)
        , m_structureMode(QPdfBookmarkModel::TreeMode)
    {
    }

    void rebuild()
    {
        Q_Q(QPdfBookmarkModel);

        const bool documentAvailable = (m_document && m_document->status() == QPdfDocument::Ready);

        if (documentAvailable) {
            q->beginResetModel();
            m_rootNode->clear();
            QPdfMutexLocker lock;
            appendChildNode(m_rootNode.data(), nullptr, 0, m_document->d->doc);
            lock.unlock();
            q->endResetModel();
        } else {
            if (m_rootNode->childCount() == 0) {
                return;
            } else {
                q->beginResetModel();
                m_rootNode->clear();
                q->endResetModel();
            }
        }
    }

    void appendChildNode(BookmarkNode *parentBookmarkNode, FPDF_BOOKMARK parentBookmark, int level, FPDF_DOCUMENT document)
    {
        FPDF_BOOKMARK bookmark = FPDFBookmark_GetFirstChild(document, parentBookmark);

        while (bookmark) {
            BookmarkNode *childBookmarkNode = nullptr;

            if (m_structureMode == QPdfBookmarkModel::TreeMode) {
                childBookmarkNode = new BookmarkNode(parentBookmarkNode);
                parentBookmarkNode->appendChild(childBookmarkNode);
            } else if (m_structureMode == QPdfBookmarkModel::ListMode) {
                childBookmarkNode = new BookmarkNode(m_rootNode.data());
                m_rootNode->appendChild(childBookmarkNode);
            }

            const int titleLength = int(FPDFBookmark_GetTitle(bookmark, nullptr, 0));

            QVector<ushort> titleBuffer(titleLength);
            FPDFBookmark_GetTitle(bookmark, titleBuffer.data(), quint32(titleBuffer.length()));

            const FPDF_DEST dest = FPDFBookmark_GetDest(document, bookmark);
            const int pageNumber = FPDFDest_GetDestPageIndex(document, dest);

            childBookmarkNode->setTitle(QString::fromUtf16(titleBuffer.data()));
            childBookmarkNode->setLevel(level);
            childBookmarkNode->setPageNumber(pageNumber);

            // recurse down
            appendChildNode(childBookmarkNode, bookmark, level + 1, document);

            bookmark = FPDFBookmark_GetNextSibling(document, bookmark);
        }
    }

    void _q_documentStatusChanged()
    {
        rebuild();
    }

    Q_DECLARE_PUBLIC(QPdfBookmarkModel)

    QScopedPointer<BookmarkNode> m_rootNode;
    QPointer<QPdfDocument> m_document;
    QPdfBookmarkModel::StructureMode m_structureMode;
};


QPdfBookmarkModel::QPdfBookmarkModel(QObject *parent)
    : QAbstractItemModel(*new QPdfBookmarkModelPrivate, parent)
{
}

QPdfDocument* QPdfBookmarkModel::document() const
{
    Q_D(const QPdfBookmarkModel);

    return d->m_document;
}

void QPdfBookmarkModel::setDocument(QPdfDocument *document)
{
    Q_D(QPdfBookmarkModel);

    if (d->m_document == document)
        return;

    if (d->m_document)
        disconnect(d->m_document, SIGNAL(statusChanged(QPdfDocument::Status)), this, SLOT(_q_documentStatusChanged()));

    d->m_document = document;
    emit documentChanged(d->m_document);

    if (d->m_document)
        connect(d->m_document, SIGNAL(statusChanged(QPdfDocument::Status)), this, SLOT(_q_documentStatusChanged()));

    d->rebuild();
}

QPdfBookmarkModel::StructureMode QPdfBookmarkModel::structureMode() const
{
    Q_D(const QPdfBookmarkModel);

    return d->m_structureMode;
}

void QPdfBookmarkModel::setStructureMode(StructureMode mode)
{
    Q_D(QPdfBookmarkModel);

    if (d->m_structureMode == mode)
        return;

    d->m_structureMode = mode;
    emit structureModeChanged(d->m_structureMode);

    d->rebuild();
}

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

QHash<int, QByteArray> QPdfBookmarkModel::roleNames() const
{
    QHash<int, QByteArray> names;

    names[TitleRole] = "title";
    names[LevelRole] = "level";
    names[PageNumberRole] = "pageNumber";

    return names;
}

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

    const BookmarkNode *node = static_cast<BookmarkNode*>(index.internalPointer());
    switch (role) {
    case TitleRole:
        return node->title();
    case LevelRole:
        return node->level();
    case PageNumberRole:
        return node->pageNumber();
    default:
        return QVariant();
    }
}

QModelIndex QPdfBookmarkModel::index(int row, int column, const QModelIndex &parent) const
{
    Q_D(const QPdfBookmarkModel);

    if (!hasIndex(row, column, parent))
        return QModelIndex();

    BookmarkNode *parentNode;

    if (!parent.isValid())
        parentNode = d->m_rootNode.data();
    else
        parentNode = static_cast<BookmarkNode*>(parent.internalPointer());

    BookmarkNode *childNode = parentNode->child(row);
    if (childNode)
        return createIndex(row, column, childNode);
    else
        return QModelIndex();
}

QModelIndex QPdfBookmarkModel::parent(const QModelIndex &index) const
{
    Q_D(const QPdfBookmarkModel);

    if (!index.isValid())
        return QModelIndex();

    const BookmarkNode *childNode = static_cast<BookmarkNode*>(index.internalPointer());
    BookmarkNode *parentNode = childNode->parentNode();

    if (parentNode == d->m_rootNode.data())
        return QModelIndex();

    return createIndex(parentNode->row(), 0, parentNode);
}

int QPdfBookmarkModel::rowCount(const QModelIndex &parent) const
{
    Q_D(const QPdfBookmarkModel);

    if (parent.column() > 0)
        return 0;

    BookmarkNode *parentNode = nullptr;

    if (!parent.isValid())
        parentNode = d->m_rootNode.data();
    else
        parentNode = static_cast<BookmarkNode*>(parent.internalPointer());

    return parentNode->childCount();
}

QT_END_NAMESPACE

#include "moc_qpdfbookmarkmodel.cpp"
