/*
 * This file is part of smolbote. It's copyrighted by the contributors recorded
 * in the version control history of the file, available from its original
 * location: https://neueland.iserlohn-fortress.net/gitea/aqua/smolbote
 *
 * SPDX-License-Identifier: GPL-3.0
 */

#include "bookmarkmodel.h"
#include "bookmarkitem.h"
#include "xbel.h"
#include <QBuffer>
#include <QMimeData>
#include <QRegularExpression>

BookmarkModel::BookmarkModel(QObject *parent)
    : QAbstractItemModel(parent)
{
    rootItem = new BookmarkItem({ tr("Title"), tr("Address") }, BookmarkItem::Root, nullptr);
}

BookmarkModel::~BookmarkModel()
{
    delete rootItem;
}

QVariant BookmarkModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if(orientation == Qt::Horizontal && role == Qt::DisplayRole)
        return rootItem->data(static_cast<BookmarkItem::Fields>(section));

    return QVariant();
}

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

    if(role == Qt::DecorationRole && index.column() == 0)
        return static_cast<BookmarkItem *>(index.internalPointer())->icon();

    else if(role == Qt::ToolTipRole)
        return static_cast<BookmarkItem *>(index.internalPointer())->tooltip();

    else if(role == Qt::DisplayRole)
        return static_cast<BookmarkItem *>(index.internalPointer())->data(static_cast<BookmarkItem::Fields>(index.column()));

    else
        return QVariant();
}

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

    if(role == Qt::DisplayRole)
        return static_cast<BookmarkItem *>(index.internalPointer())->data(static_cast<BookmarkItem::Fields>(column));

    return QVariant();
}

bool BookmarkModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if(!index.isValid())
        return false;

    bool success = false;

    if(role == Qt::DisplayRole) {
        success = static_cast<BookmarkItem *>(index.internalPointer())->setData(static_cast<BookmarkItem::Fields>(index.column()), value);
    }

    if(success) {
        emit dataChanged(index, index, { role });
        m_isModified = true;
    }
    return success;
}

bool BookmarkModel::setData(const QModelIndex &index, const QVariant &value, BookmarkItem::Fields column, int role)
{
    if(!index.isValid() || role != Qt::DisplayRole)
        return false;

    bool success = static_cast<BookmarkItem *>(index.internalPointer())->setData(column, value);
    if(success) {
        emit dataChanged(index, index, { role });
        m_isModified = true;
    }
    return success;
}

Qt::ItemFlags BookmarkModel::flags(const QModelIndex &index) const
{
    if(getItem(index)->type() == BookmarkItem::Folder)
        return QAbstractItemModel::flags(index) | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;
    else
        return QAbstractItemModel::flags(index) | Qt::ItemIsDragEnabled | Qt::ItemNeverHasChildren;
}

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

    return static_cast<BookmarkItem *>(index.internalPointer())->isExpanded();
}

void BookmarkModel::setItemExpanded(const QModelIndex &index, bool expanded)
{
    BookmarkItem *item = getItem(index);
    if(item->type() == BookmarkItem::Folder) {
        item->setExpanded(expanded);
        m_isModified = true;
    }
}

int BookmarkModel::rowCount(const QModelIndex &index) const
{
    if(index.column() > 0)
        return 0;

    return getItem(index)->childCount();
}

QModelIndex BookmarkModel::appendBookmark(const QString &title, const QString &url, const QModelIndex &parent)
{
    auto *parentItem = getItem(parent);

    int row = parentItem->childCount();
    beginInsertRows(parent, row, row);
    auto *childItem = new BookmarkItem({ title, url }, BookmarkItem::Bookmark, parentItem);
    parentItem->appendChild(childItem);
    endInsertRows();

    m_isModified = true;
    return createIndex(row, 0, childItem);
}

QModelIndex BookmarkModel::appendFolder(const QString &title, const QModelIndex &parent)
{
    auto *parentItem = getItem(parent);
    const int row = parentItem->childCount();

    beginInsertRows(parent, row, row);
    auto *childItem = new BookmarkItem({ title }, BookmarkItem::Folder, parentItem);
    parentItem->appendChild(childItem);
    endInsertRows();

    m_isModified = true;
    return createIndex(row, 0, childItem);
}

bool BookmarkModel::removeRows(int position, int rows, const QModelIndex &parent)
{
    auto *parentItem = getItem(parent);

    beginRemoveRows(parent, position, position + rows - 1);
    bool success = parentItem->removeChildAt(position, rows);
    endRemoveRows();

    if(success)
        m_isModified = true;
    return success;
}

int BookmarkModel::columnCount(const QModelIndex &index) const
{
    Q_UNUSED(index);
    return 2;
}

QModelIndex BookmarkModel::index(int row, int column, const QModelIndex &parent) const
{
    if(!this->hasIndex(row, column, parent))
        return QModelIndex();

    BookmarkItem *parentItem = getItem(parent);
    BookmarkItem *childItem = parentItem->child(row);
    if(childItem)
        return createIndex(row, column, childItem);
    else
        return QModelIndex();
}

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

    auto *childItem = static_cast<BookmarkItem *>(index.internalPointer());
    auto *parentItem = childItem->parent();

    if(parentItem == rootItem)
        return QModelIndex();

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

QModelIndex BookmarkModel::parentFolder(const QModelIndex &index) const
{
    // invalid index is the root index -> return it back
    if(!index.isValid())
        return QModelIndex();

    if(getItem(index)->type() == BookmarkItem::Bookmark) {
        return index.parent();
    }

    return index;
}

inline bool has(const QStringList &terms, const QStringList &where)
{
    for(const QString &term : terms) {
        if(where.contains(term))
            return true;
    }
    return false;
}

inline QStringList searchThrough(const QString &term, const QStringList &tags, BookmarkItem *item)
{
    QStringList results;

    for(int i = 0; i < item->childCount(); ++i) {
        auto *child = item->child(i);

        if(child->type() == BookmarkItem::Bookmark) {
            if((!term.isEmpty() && child->data(BookmarkItem::Href).toString().contains(term)) || has(tags, child->data(BookmarkItem::Tags).toStringList()))
                results.append(child->data(BookmarkItem::Href).toString());
        }

        else if(child->type() == BookmarkItem::Folder) {
            if(has(tags, child->data(BookmarkItem::Tags).toStringList())) {

                // append all bookmarks
                for(int i = 0; i < child->childCount(); ++i) {
                    auto *subChild = child->child(i);
                    if(subChild->type() == BookmarkItem::Bookmark)
                        results.append(subChild->data(BookmarkItem::Href).toString());
                }
            }
            results.append(searchThrough(term, tags, child));
        }
    }
    return results;
}

QStringList BookmarkModel::search(const QString &term) const
{
    QString searchTerm = term;
    QStringList tags;

    const QRegularExpression tagRE(":\\w+\\s?", QRegularExpression::CaseInsensitiveOption);
    auto i = tagRE.globalMatch(term);
    while(i.hasNext()) {
        auto match = i.next();
        QString tag = match.captured();
        searchTerm.remove(tag);
        tag = tag.remove(0, 1).trimmed();
        tags.append(tag);
    }

    return searchThrough(searchTerm, tags, rootItem);
}

BookmarkItem *BookmarkModel::getItem(const QModelIndex &index) const
{
    if(!index.isValid())
        return rootItem;
    else
        return static_cast<BookmarkItem *>(index.internalPointer());
}

/*
 * Drag'n'Drop implementation
 * How drag and drop actually works: the view encodes the data of the original
 * item (through ::mimeData), and then uses ::dropMimeData to create the new
 * item. If successful, the old item is removed (through ::removeRows).
 * This means that the encoding and decoding needs to be provided. In this case,
 * this is done through xbel.
 */

Qt::DropActions BookmarkModel::supportedDropActions() const
{
    return Qt::MoveAction;
}

QStringList BookmarkModel::mimeTypes() const
{
    return { mimeType };
}

QMimeData *BookmarkModel::mimeData(const QModelIndexList &indexes) const
{
    auto *mimeData = new QMimeData();
    QByteArray data;

    QDataStream stream(&data, QIODevice::WriteOnly);
    for(const QModelIndex &index : indexes) {
        if(index.isValid() && index.column() == 0) {
            QByteArray encodedData;
            QBuffer buffer(&encodedData);
            buffer.open(QIODevice::WriteOnly);

            Xbel::write(&buffer, getItem(index));

            stream << encodedData;
        }
    }
    mimeData->setData(mimeType, data);
    return mimeData;
}
bool BookmarkModel::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, int row, int column, const QModelIndex &parent)
{
    if(action == Qt::IgnoreAction)
        return true;

    if(action != Qt::MoveAction)
        return false;

    if(!mimeData->hasFormat(mimeType) || column > 0)
        return false;

    QByteArray data = mimeData->data(mimeType);
    QDataStream stream(&data, QIODevice::ReadOnly);
    if(stream.atEnd())
        return false;

    while(!stream.atEnd()) {
        QByteArray encodedData;
        stream >> encodedData;

        QBuffer buffer(&encodedData);
        buffer.open(QIODevice::ReadOnly);

        auto *fakeRoot = new BookmarkItem({}, BookmarkItem::Folder, nullptr);
        auto *parentItem = getItem(parent);
        Xbel::read(&buffer, fakeRoot);

        beginInsertRows(parent, row, row + fakeRoot->childCount() - 1);
        for(int i = 0; i < fakeRoot->childCount(); ++i) {
            auto *child = fakeRoot->takeChild(0, parentItem);
            parentItem->insertChild(row, child);
        }
        endInsertRows();

        delete fakeRoot;
    }

    m_isModified = true;
    return true;
}