#include "favouritecollection.hpp"
#include "ioutil.hpp"

#include <cassert>
#include <QDebug>
#include <QIcon>
#include <QMimeData>

#include <memory>

FavouriteCollection::FavouriteCollection(QObject *parent)
    : QAbstractItemModel(parent)
{

}

FavouriteCollection::FavouriteCollection(const FavouriteCollection &other)
{
    for(auto const & grp : other.root.children)
    {
        auto const & src_group = grp->as<GroupNode>();
        auto dst_group = std::make_unique<GroupNode>();

        dst_group->title = src_group.title;

        for(auto const & id : src_group.children) {
            auto const & src_id = id->as<FavouriteNode>();
            auto dst_id = std::make_unique<FavouriteNode>();

            dst_id->favourite = src_id.favourite;

            dst_group->children.emplace_back(std::move(dst_id));
        }

        root.children.emplace_back(std::move(dst_group));
    }


    relayout();
}

FavouriteCollection::FavouriteCollection(FavouriteCollection &&other)
{
    this->root.children = std::move(other.root.children);
}


FavouriteCollection::~FavouriteCollection()
{

}

FavouriteCollection &FavouriteCollection::operator=(const FavouriteCollection & other)
{
    beginResetModel();

    root.children.clear();
    for(auto const & grp : other.root.children)
    {
        auto const & src_group = grp->as<GroupNode>();
        auto dst_group = std::make_unique<GroupNode>();

        dst_group->title = src_group.title;

        for(auto const & id : src_group.children) {
            auto const & src_id = id->as<FavouriteNode>();
            auto dst_id = std::make_unique<FavouriteNode>();

            dst_id->favourite = src_id.favourite;

            dst_group->children.emplace_back(std::move(dst_id));
        }

        root.children.emplace_back(std::move(dst_group));
    }

    this->relayout();
    endResetModel();
    return *this;
}

FavouriteCollection &FavouriteCollection::operator=(FavouriteCollection && other)
{
    beginResetModel();
    this->root.children = std::move(other.root.children);
    this->relayout();
    endResetModel();
    return *this;
}

void FavouriteCollection::load(QSettings &settings)
{
    this->beginResetModel();

    this->root.children.clear();

    int group_cnt = settings.beginReadArray("groups");
    for(int i = 0; i < group_cnt; i++)
    {
        settings.setArrayIndex(i);
        auto group = std::make_unique<GroupNode>();

        group->title = settings.value("name").toString();

        int id_cnt = settings.beginReadArray("favourites");

        for(int j = 0; j < id_cnt; j++)
        {
            settings.setArrayIndex(j);
            auto fav = std::make_unique<FavouriteNode>();

            fav->favourite.title = settings.value("title").toString();
            fav->favourite.destination = settings.value("url").toUrl();

            group->children.emplace_back(std::move(fav));
        }

        settings.endArray();

        this->root.children.emplace_back(std::move(group));
    }
    settings.endArray();

    relayout();

    this->endResetModel();
}

void FavouriteCollection::save(QSettings &settings) const
{
    settings.beginWriteArray("groups", int(root.children.size()));

    int grp_index = 0;
    for(auto const & grp : root.children)
    {
        settings.setArrayIndex(grp_index);
        grp_index += 1;

        auto & group = grp->as<GroupNode>();
        settings.setValue("name", group.title);

        settings.beginWriteArray("favourites", int(group.children.size()));

        int id_index = 0;
        for(auto const & _id : group.children)
        {
            settings.setArrayIndex(id_index);
            id_index += 1;

            auto & id = _id->as<FavouriteNode>();

            settings.setValue("title", id.favourite.title);
            settings.setValue("url", id.favourite.destination);
        }

        settings.endArray();
    }

    settings.endArray();
}

bool FavouriteCollection::addGroup(const QString &group_name)
{
    // Check if group already exists
    for (auto const & grp : root.children)
    {
        if (static_cast<GroupNode*>(grp.get())->title == group_name)
        {
            return false;
        }
    }

    GroupNode * group;
    return internalAddGroup(group_name, group);
}

bool FavouriteCollection::renameGroup(const QString &old_name, const QString &new_name)
{
    GroupNode * to_set = nullptr;
    for (auto const & grp : root.children)
    {
        auto * g = static_cast<GroupNode*>(grp.get());
        if (g->title == old_name)
        {
            to_set = g;
        }
        if (g->title == new_name)
        {
            return false;
        }
    }

    if (to_set)
    {
        to_set->title = new_name;
        return true;
    }

    return false;
}

bool FavouriteCollection::addFavourite(const QString &group_name, const Favourite &fav)
{
    GroupNode * group;
    internalAddGroup(group_name, group);

    QModelIndex parent_index = createIndex(group->index, 0, group);

    beginInsertRows(parent_index, group->children.size(), group->children.size() + 1);

    auto id = std::make_unique<FavouriteNode>();
    id->favourite = fav;
    group->children.emplace_back(std::move(id));

    this->relayout();

    this->endInsertRows();

    return true;
}

void FavouriteCollection::editFavouriteTitle(const QModelIndex &index, const QString &title)
{
    this->getMutableFavourite(index)->title = title;
}

bool FavouriteCollection::editFavouriteTitle(const QUrl &u, const QString &new_title)
{
    QUrl url = IoUtil::uniformUrl(u);
    for(auto const & group : this->root.children)
    {
        for(auto const & ident : group->children)
        {
            FavouriteNode* node = &ident->as<FavouriteNode>();
            if(IoUtil::uniformUrl(node->favourite.destination) == url)
            {
                node->favourite.title = new_title;
                return true;
            }
        }
    }
    return false;
}

void FavouriteCollection::editFavouriteDest(const QModelIndex &index, const QUrl &url)
{
    this->getMutableFavourite(index)->destination = url;
}

bool FavouriteCollection::editFavouriteGroup(const QUrl &u, const QString &group_name)
{
    // Find and erase favourite node
    QUrl url = IoUtil::uniformUrl(u);
    for (auto const & group : this->root.children)
    {
        size_t index = 0;
        for (auto it = group->children.begin(); it != group->children.end(); it++, index++)
        {
            auto& fav = it->get()->as<FavouriteNode>();
            if(IoUtil::uniformUrl(fav.favourite.destination) == url)
            {
                Favourite f = Favourite {
                    fav.favourite.title,
                    fav.favourite.destination
                };

                beginRemoveRows(this->index(fav.parent->index, 0), index, index + 1);
                group->children.erase(it);
                endRemoveRows();
                this->relayout();

                this->addFavourite(group_name, f);
                return true;
            }
        }
    }
    return false;
}

Favourite FavouriteCollection::getFavourite(const QUrl &u) const
{
    QUrl url = IoUtil::uniformUrl(u);
    for(auto const & group : this->root.children)
    {
        for(auto const & ident : group->children)
        {
            FavouriteNode* node = &ident->as<FavouriteNode>();
            if(IoUtil::uniformUrl(node->favourite.destination) == url)
                return node->favourite;
        }
    }
    return Favourite();
}

Favourite FavouriteCollection::getFavourite(const QModelIndex &index) const
{
    if (!index.isValid())
        return Favourite();

    if (index.column() != 0)
        return Favourite();

    Node const *item = static_cast<Node const*>(index.internalPointer());
    switch(item->type) {
    case Node::Favourite: return static_cast<FavouriteNode const *>(item)->favourite;
    default:
        return Favourite();
    }
}

Favourite * FavouriteCollection::getMutableFavourite(const QModelIndex &index)
{
    if (!index.isValid())
        return nullptr;

    if (index.column() != 0)
        return nullptr;

    Node *item = static_cast<Node*>(index.internalPointer());
    switch(item->type) {
    case Node::Favourite: return &static_cast<FavouriteNode *>(item)->favourite;
    default:
        return nullptr;
    }
}

QString FavouriteCollection::groupForFavourite(const QUrl &url) const
{
    for (auto const & group : this->root.children)
    {
        for (auto const & ident : group->children)
        {
            FavouriteNode* node = &ident->as<FavouriteNode>();
            if (node->favourite.destination == url)
                return node->parent->as<GroupNode>().title;
        }
    }
    return QString { };
}

QStringList FavouriteCollection::groups() const
{
    QStringList result;
    for(auto const & grp : root.children)
    {
        result.append(grp->as<GroupNode>().title);
    }
    return result;
}

QString FavouriteCollection::group(const QModelIndex &index) const
{
    if (!index.isValid())
        return QString { };

    Node const *item = static_cast<Node const*>(index.internalPointer());

    switch(item->type) {
    case Node::Root:     return QString { };
    case Node::Group:    return static_cast<GroupNode const *>(item)->title;
    case Node::Favourite: return static_cast<FavouriteNode const *>(item)->parent->as<GroupNode>().title;
    default:             return QString { };
    }
}

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

    Node * childItem = static_cast<Node *>(index.internalPointer());
    Node * parent = childItem->parent;

    if (parent == &root)
        return false;

    beginRemoveRows(this->parent(index), index.row(), index.row() + 1);

    parent->children.erase(parent->children.begin() + childItem->index);

    endRemoveRows();

    return true;
}

bool FavouriteCollection::canDeleteGroup(const QString &group_name)
{
    for(auto const & group_node : root.children)
    {
        auto & group = group_node->as<GroupNode>();
        if((group.children.size() == 0) and (group.title == group_name))
            return true;

    }
    return false;
}

bool FavouriteCollection::deleteGroup(const QString &group_name)
{
    size_t index = 0;
    for(auto it = root.children.begin(); it != root.children.end(); it++, index++)
    {
        auto & group = it->get()->as<GroupNode>();
        if(group.title == group_name) {
            if(group.children.size() > 0) {
                qDebug() << "cannot delete non-empty group" << group_name;
                return false;
            }

            beginRemoveRows(QModelIndex { }, index, index + 1);

            root.children.erase(it);

            endRemoveRows();

            return true;
        }
    }
    return false;
}

bool FavouriteCollection::deleteGroupRecursive(const QString &group_name)
{
    size_t index = 0;
    for (auto it = root.children.begin(); it != root.children.end(); ++it, ++index)
    {
        auto & group = it->get()->as<GroupNode>();
        if (group.title == group_name)
        {
            // Delete group children
            auto group_idx = this->index(index, 0);
            beginRemoveRows(group_idx, 0, group.children.size());
            group.children.clear();
            endRemoveRows();

            // Delete the group
            beginRemoveRows(QModelIndex { }, index, index + 1);
            root.children.erase(it);
            endRemoveRows();

            return true;
        }
    }
    return false;
}

QVector<QPair<QString, Favourite const *>> FavouriteCollection::allFavourites() const
{
    QVector<QPair<QString, Favourite const *>> identities;

    for(auto const & group : this->root.children)
    {
        for(auto const & ident : group->children)
        {
            identities.append(QPair<QString, Favourite const *> {
                group->as<GroupNode>().title,
                &ident->as<FavouriteNode>().favourite
            });
        }
    }

    return identities;
}

bool FavouriteCollection::containsUrl(const QUrl &u) const
{
    QUrl url = IoUtil::uniformUrl(u);
    for(auto const & group : this->root.children)
    {
        for(auto const & ident : group->children)
        {
            if(IoUtil::uniformUrl(ident->as<FavouriteNode>().favourite.destination) == url)
                return true;
        }
    }
    return false;
}

bool FavouriteCollection::addUnsorted(const QUrl &url, const QString &t)
{
    if(containsUrl(url))
        return false;
    return addFavourite(tr("Unsorted"), Favourite {
        t,
        url,
    });
}

bool FavouriteCollection::removeUrl(const QUrl &u)
{
    QUrl url = IoUtil::uniformUrl(u);
    for(auto const & group : this->root.children)
    {
        size_t index = 0;
        for(auto it = group->children.begin(); it != group->children.end(); ++it, ++index)
        {
            auto & fav = it->get()->as<FavouriteNode>();
            if(IoUtil::uniformUrl(fav.favourite.destination) == url) {
                beginRemoveRows(QModelIndex { }, index, index + 1);

                group->children.erase(it);

                endRemoveRows();

                return true;
            }
        }
    }
    return false;
}

QModelIndex FavouriteCollection::index(int row, int column, const QModelIndex &parent) const
{
    if (not hasIndex(row, column, parent))
        return QModelIndex();

    Node const * parentItem;

    if(!parent.isValid())
        parentItem = &this->root;
    else
        parentItem = static_cast<Node*>(parent.internalPointer());

    auto & children = parentItem->children;
    if(row < 0 or size_t(row) >= children.size())
        return QModelIndex { };
    return createIndex(
        row,
        column,
        reinterpret_cast<quintptr>(children[row].get())
    );
}

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

    Node const  *childItem = static_cast<Node const *>(index.internalPointer());
    Node const * parent = childItem->parent;

    if (parent == &root)
        return QModelIndex();

    return createIndex(
        parent->index,
        0,
        reinterpret_cast<quintptr>(parent));
}

int FavouriteCollection::rowCount(const QModelIndex &parent) const
{
    Node const * parentItem;

    if (!parent.isValid())
        parentItem = &root;
    else
        parentItem = static_cast<Node const *>(parent.internalPointer());

    return parentItem->children.size();
}

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

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

    Node const *item = static_cast<Node const*>(index.internalPointer());
    if (role == Qt::DisplayRole)
    {
        switch(item->type) {
        case Node::Root: return "root";
        case Node::Group: return static_cast<GroupNode const *>(item)->title;
        case Node::Favourite: return static_cast<FavouriteNode const *>(item)->favourite.getTitle();
        default:
            return "Unknown";
        }
    }
    if (role == Qt::EditRole)
    {
        switch(item->type) {
        case Node::Root: return "root";
        case Node::Group: return static_cast<GroupNode const *>(item)->title;
        case Node::Favourite: return static_cast<FavouriteNode const *>(item)->favourite.title;
        default:
            return "Unknown";
        }
    }
    else if(role == Qt::DecorationRole) {

        switch(item->type) {
        case Node::Root: return QVariant { };
        case Node::Group: return QIcon::fromTheme("folder");
        case Node::Favourite: return QIcon::fromTheme("favourite");
        default: return QVariant { };
        }
    }

    return QVariant();
}

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

    Node *item = static_cast<Node*>(index.internalPointer());
    if (role == Qt::EditRole)
    {
        switch(item->type) {
        case Node::Root: return false;
        case Node::Group:
            item->as<GroupNode>().title = value.toString();
            emit this->dataChanged(index, index, { Qt::EditRole });
            return true;
        case Node::Favourite:
            item->as<FavouriteNode>().favourite.title = value.toString();
            emit this->dataChanged(index, index, { Qt::EditRole });
            return true;
        default: return false;
        }
    }

    return false;
}

Qt::ItemFlags FavouriteCollection::flags(const QModelIndex &index) const
{
    if (!index.isValid())
        return Qt::NoItemFlags;
    Node const *item = static_cast<Node const*>(index.internalPointer());
    switch(item->type) {
    case Node::Favourite:
        return QAbstractItemModel::flags(index) | Qt::ItemIsDragEnabled;
    case Node::Group:
        return QAbstractItemModel::flags(index) | Qt::ItemIsDropEnabled | Qt::ItemIsEditable;
    default:
        return QAbstractItemModel::flags(index);
    }
}

QStringList FavouriteCollection::mimeTypes() const
{
    QStringList mimes;
    mimes << "x-kristall/identity";
    return mimes;
}

#include <QBuffer>
#include <QDataStream>

QMimeData *FavouriteCollection::mimeData(const QModelIndexList &indexes) const
{
    if(indexes.size() != 1)
        return nullptr;
    auto const & index = indexes.at(0);

    if (not index.isValid())
        return nullptr;

    Node const *item = static_cast<Node const*>(index.internalPointer());
    switch(item->type) {
    case Node::Favourite: {
        auto const & favourite = item->as<FavouriteNode>().favourite;

        QByteArray buffer;

        {
            QDataStream stream { &buffer, QIODevice::WriteOnly };

            stream << favourite.title;
            stream << favourite.destination;
        }
        assert(buffer.size() > 0);

        auto mime = std::make_unique<QMimeData>();

        mime->setData("x-kristall/favourite", buffer);

        return mime.release();
    }
    default:
        return nullptr;
    }
}

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

    if (not parent.isValid())
        return false;

    Node const *item = static_cast<Node const*>(parent.internalPointer());
    switch(item->type) {
    case Node::Group: {
        return data->hasFormat("x-kristall/favourite") and (action == Qt::MoveAction);
    }
    default:
        return false;
    }
}

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

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

    if (not parent.isValid())
        return false;

    Node *item = static_cast<Node *>(parent.internalPointer());
    switch(item->type) {
    case Node::Group: {
        auto ident_blob = data->data("x-kristall/favourite");

        auto node = std::make_unique<FavouriteNode>();
        Favourite & fav = node->favourite;
        {
            QDataStream stream { &ident_blob, QIODevice::ReadOnly };

            stream >> fav.title;
            stream >> fav.destination;
        }

        if(not fav.isValid())
            return false;

        auto & insert_list = item->as<GroupNode>().children;

        if((row < 0) or (size_t(row) >= insert_list.size())) {
            beginInsertRows(parent, insert_list.size(), insert_list.size());
            insert_list.emplace_back(std::move(node));
        } else {
            beginInsertRows(parent, row, row);
            insert_list.emplace(insert_list.begin() + size_t(row), std::move(node));
        }
        endInsertRows();

        qDebug() << "dropping" << data->formats() << row;

        this->relayout();
        return true;
    }
    default:
        return false;
    }
}

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

Qt::DropActions FavouriteCollection::supportedDragActions() const
{
    return Qt::MoveAction;
}

bool FavouriteCollection::removeRows(int row, int count, const QModelIndex &parent)
{
    if (not parent.isValid())
        return false;

    if(count != 1)
        return false;

    Node *item = static_cast<Node *>(parent.internalPointer());
    switch(item->type) {
    case Node::Group: {
        auto & children = item->as<GroupNode>().children;

        if((row < 0) or (size_t(row) >= children.size()))
            return false;

        beginRemoveRows(parent, row, row + 1);
        children.erase(children.begin() + size_t(row));
        endRemoveRows();

        return true;
    }
    default:
        return false;
    }
}

void FavouriteCollection::relayout()
{
    for(size_t i = 0; i < root.children.size(); i++)
    {
        auto & group = *root.children[i];
        group.parent = &root;
        group.index = i;

        // qDebug() << "group[" << group.index << "]" << group.as<GroupNode>().title;

        for(size_t j = 0; j < group.children.size(); j++)
        {
            auto & id = *group.children[j];
            id.parent = &group;
            id.index = j;
            assert(id.children.size() == 0);

            // qDebug() << "id[" << id.index << "]" << id.as<IdentityNode>().identity.display_name;
        }
    }
}

bool FavouriteCollection::internalAddGroup(const QString &group_name, GroupNode * & group)
{
    for(auto const & grp : root.children)
    {
        auto * g = static_cast<GroupNode*>(grp.get());
        if(g->title == group_name) {
            group = g;
            return false;
        }
    }

    auto parent = QModelIndex { };

    beginInsertRows(parent, this->root.children.size(), this->root.children.size() + 1);

    group = new GroupNode();
    group->title = group_name;
    this->root.children.emplace_back(group);

    this->relayout();

    endInsertRows();

    return true;
}
