// Copyright (C) 2014 Aaron McCarthy <mccarthy.aaron@gmail.com>
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qdeclarativesupportedcategoriesmodel_p.h"
#include "qgeoserviceprovider.h"
#include "error_messages_p.h"
#include <QtCore/private/qobject_p.h>

#include <QCoreApplication>
#include <QtQml/QQmlInfo>
#include <QtLocation/QPlaceManager>
#include <QtLocation/QPlaceIcon>

QT_BEGIN_NAMESPACE

/*!
    \qmltype CategoryModel
    \instantiates QDeclarativeSupportedCategoriesModel
    \inqmlmodule QtLocation
    \ingroup qml-QtLocation5-places
    \ingroup qml-QtLocation5-places-models
    \since QtLocation 5.5

    \brief The CategoryModel type provides a model of the categories supported by a \l Plugin.

    The CategoryModel type provides a model of the categories that are available from the
    current \l Plugin.  The model supports both a flat list of categories and a hierarchical tree
    representing category groupings.  This can be controlled by the \l hierarchical property.

    The model supports the following roles:

    \table
        \header
            \li Role
            \li Type
            \li Description
        \row
            \li category
            \li \l Category
            \li Category object for the current item.
      \row
            \li parentCategory
            \li \l Category
            \li Parent category object for the current item.
                If there is no parent, null is returned.
    \endtable

    The following example displays a flat list of all available categories:

    \snippet declarative/places.qml QtQuick import
    \snippet declarative/maps.qml QtLocation import
    \codeline
    \snippet declarative/places.qml CategoryView

    To access the hierarchical category model it is necessary to use a \l DelegateModel to access
    the child items.
*/

/*!
    \qmlproperty Plugin CategoryModel::plugin

    This property holds the provider \l Plugin used by this model.
*/

/*!
    \qmlproperty bool CategoryModel::hierarchical

    This property holds whether the model provides a hierarchical tree of categories or a flat
    list.  The default is true.
*/

/*!
    \qmlmethod string QtLocation::CategoryModel::data(ModelIndex index, int role)
    \internal

    This method retrieves the model's data per \a index and \a role.
*/

/*!
    \qmlmethod string QtLocation::CategoryModel::errorString() const

    This read-only property holds the textual presentation of the latest category model error.
    If no error has occurred, an empty string is returned.

    An empty string may also be returned if an error occurred which has no associated
    textual representation.
*/

/*!
    \qmlmethod void QtLocation::CategoryModel::update()
    \internal

    Updates the model.

    \note The CategoryModel auto updates automatically when needed. Calling this method explicitly is normally not necessary.
*/

/*!
    \internal
    \enum QDeclarativeSupportedCategoriesModel::Roles
*/

QDeclarativeSupportedCategoriesModel::QDeclarativeSupportedCategoriesModel(QObject *parent)
    : QAbstractItemModel(parent)
{
}

QDeclarativeSupportedCategoriesModel::~QDeclarativeSupportedCategoriesModel()
{
    qDeleteAll(m_categoriesTree);
}

/*!
    \internal
*/
// From QQmlParserStatus
void QDeclarativeSupportedCategoriesModel::componentComplete()
{
    m_complete = true;
    if (m_plugin) // do not try to load or change status when trying to update in componentComplete() if the plugin hasn't been set yet even once.
        update();
}

/*!
    \internal
*/
int QDeclarativeSupportedCategoriesModel::rowCount(const QModelIndex &parent) const
{
    if (m_categoriesTree.keys().isEmpty())
        return 0;

    PlaceCategoryNode *node = static_cast<PlaceCategoryNode *>(parent.internalPointer());
    if (!node)
        node = m_categoriesTree.value(QString());
    else if (m_categoriesTree.keys(node).isEmpty())
        return 0;

    return node->childIds.count();
}

/*!
    \internal
*/
int QDeclarativeSupportedCategoriesModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);

    return 1;
}

/*!
    \internal
*/
QModelIndex QDeclarativeSupportedCategoriesModel::index(int row, int column, const QModelIndex &parent) const
{
    if (column != 0 || row < 0)
        return QModelIndex();

    PlaceCategoryNode *node = static_cast<PlaceCategoryNode *>(parent.internalPointer());

    if (!node)
        node = m_categoriesTree.value(QString());
    else if (m_categoriesTree.keys(node).isEmpty()) //return root index if parent is non-existent
        return QModelIndex();

    if (row > node->childIds.count())
        return QModelIndex();

    QString id = node->childIds.at(row);
    Q_ASSERT(m_categoriesTree.contains(id));

    return createIndex(row, 0, m_categoriesTree.value(id));
}

/*!
    \internal
*/
QModelIndex QDeclarativeSupportedCategoriesModel::parent(const QModelIndex &child) const
{
    PlaceCategoryNode *childNode = static_cast<PlaceCategoryNode *>(child.internalPointer());
    if (m_categoriesTree.keys(childNode).isEmpty())
        return QModelIndex();

    return index(childNode->parentId);
}

/*!
    \internal
*/
QVariant QDeclarativeSupportedCategoriesModel::data(const QModelIndex &index, int role) const
{
    PlaceCategoryNode *node = static_cast<PlaceCategoryNode *>(index.internalPointer());
    if (!node)
        node = m_categoriesTree.value(QString());
    else if (m_categoriesTree.keys(node).isEmpty())
        return QVariant();

   QDeclarativeCategory *category = node->declCategory.data();

    switch (role) {
    case Qt::DisplayRole:
        return category->name();
    case CategoryRole:
        return QVariant::fromValue(category);
    case ParentCategoryRole: {
        if (!m_categoriesTree.keys().contains(node->parentId))
            return QVariant();
        else
            return QVariant::fromValue(m_categoriesTree.value(node->parentId)->declCategory.data());
    }
    default:
        return QVariant();
    }
}

QHash<int, QByteArray> QDeclarativeSupportedCategoriesModel::roleNames() const
{
    QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
    roles.insert(CategoryRole, "category");
    roles.insert(ParentCategoryRole, "parentCategory");
    return roles;
}

/*!
    \internal
*/
void QDeclarativeSupportedCategoriesModel::setPlugin(QDeclarativeGeoServiceProvider *plugin)
{
    if (m_plugin == plugin)
        return;

    //disconnect the manager of the old plugin if we have one
    if (m_plugin) {
        disconnect(m_plugin, nullptr, this, nullptr);
        QGeoServiceProvider *serviceProvider = m_plugin->sharedGeoServiceProvider();
        if (serviceProvider) {
            QPlaceManager *placeManager = serviceProvider->placeManager();
            if (placeManager) {
                disconnect(placeManager, &QPlaceManager::categoryAdded,
                           this, &QDeclarativeSupportedCategoriesModel::addedCategory);
                disconnect(placeManager, &QPlaceManager::categoryUpdated,
                           this, &QDeclarativeSupportedCategoriesModel::updatedCategory);
                disconnect(placeManager, &QPlaceManager::categoryRemoved,
                           this, &QDeclarativeSupportedCategoriesModel::removedCategory);
                disconnect(placeManager, &QPlaceManager::dataChanged,
                           this, &QDeclarativeSupportedCategoriesModel::emitDataChanged);
            }
        }
    }

    m_plugin = plugin;

    // handle plugin attached changes -> update categories
    if (m_plugin) {
        if (m_plugin->isAttached()) {
            connectNotificationSignals();
            update();
        } else {
            connect(m_plugin, &QDeclarativeGeoServiceProvider::attached,
                    this, &QDeclarativeSupportedCategoriesModel::update);
            connect(m_plugin, &QDeclarativeGeoServiceProvider::attached,
                    this, &QDeclarativeSupportedCategoriesModel::connectNotificationSignals);
        }
    }

    if (m_complete)
        emit pluginChanged();
}

/*!
    \internal
*/
QDeclarativeGeoServiceProvider *QDeclarativeSupportedCategoriesModel::plugin() const
{
    return m_plugin;
}

/*!
    \internal
*/
void QDeclarativeSupportedCategoriesModel::setHierarchical(bool hierarchical)
{
    if (m_hierarchical == hierarchical)
        return;

    m_hierarchical = hierarchical;
    emit hierarchicalChanged();

    updateLayout();
}

/*!
    \internal
*/
bool QDeclarativeSupportedCategoriesModel::hierarchical() const
{
    return m_hierarchical;
}

/*!
    \internal
*/
void QDeclarativeSupportedCategoriesModel::replyFinished()
{
    if (!m_response)
        return;

    m_response->deleteLater();

    if (m_response->error() == QPlaceReply::NoError) {
        m_errorString.clear();

        m_response = nullptr;

        updateLayout();
        setStatus(QDeclarativeSupportedCategoriesModel::Ready);
    } else {
        const QString errorString = m_response->errorString();

        m_response = nullptr;

        setStatus(Error, errorString);
    }
}

/*!
    \internal
*/
void QDeclarativeSupportedCategoriesModel::addedCategory(const QPlaceCategory &category,
                                                         const QString &parentId)
{
    if (m_response)
        return;

    if (!m_categoriesTree.contains(parentId))
        return;

    if (category.categoryId().isEmpty())
        return;

    PlaceCategoryNode *parentNode = m_categoriesTree.value(parentId);
    if (!parentNode)
        return;

    int rowToBeAdded = rowToAddChild(parentNode, category);
    QModelIndex parentIndex = index(parentId);
    beginInsertRows(parentIndex, rowToBeAdded, rowToBeAdded);
    PlaceCategoryNode *categoryNode = new PlaceCategoryNode;
    categoryNode->parentId = parentId;
    categoryNode->declCategory = QSharedPointer<QDeclarativeCategory>(new QDeclarativeCategory(category, m_plugin, this));

    m_categoriesTree.insert(category.categoryId(), categoryNode);
    parentNode->childIds.insert(rowToBeAdded,category.categoryId());
    endInsertRows();

    //this is a workaround to deal with the fact that the hasModelChildren field of DelegateModel
    //does not get updated when a child is added to a model
    beginResetModel();
    endResetModel();
}

/*!
    \internal
*/
void QDeclarativeSupportedCategoriesModel::updatedCategory(const QPlaceCategory &category,
                                                           const QString &parentId)
{
    if (m_response)
        return;

    QString categoryId = category.categoryId();

    if (!m_categoriesTree.contains(parentId))
        return;

    if (category.categoryId().isEmpty() || !m_categoriesTree.contains(categoryId))
        return;

    PlaceCategoryNode *newParentNode = m_categoriesTree.value(parentId);
    if (!newParentNode)
        return;

    PlaceCategoryNode *categoryNode = m_categoriesTree.value(categoryId);
    if (!categoryNode)
        return;

    categoryNode->declCategory->setCategory(category);

    if (categoryNode->parentId == parentId) { //reparenting to same parent
        QModelIndex parentIndex = index(parentId);
        int rowToBeAdded = rowToAddChild(newParentNode, category);
        int oldRow = newParentNode->childIds.indexOf(categoryId);

        //check if we are changing the position of the category
        if (qAbs(rowToBeAdded - newParentNode->childIds.indexOf(categoryId)) > 1) {
            //if the position has changed we are moving rows
            beginMoveRows(parentIndex, oldRow, oldRow,
                          parentIndex, rowToBeAdded);

            newParentNode->childIds.removeAll(categoryId);
            newParentNode->childIds.insert(rowToBeAdded, categoryId);
            endMoveRows();
        } else {// if the position has not changed we modifying an existing row
            QModelIndex categoryIndex = index(categoryId);
            emit dataChanged(categoryIndex, categoryIndex);
        }
    } else { //reparenting to different parents
        QPlaceCategory oldCategory = categoryNode->declCategory->category();
        PlaceCategoryNode *oldParentNode = m_categoriesTree.value(categoryNode->parentId);
        if (!oldParentNode)
            return;
        QModelIndex oldParentIndex = index(categoryNode->parentId);
        QModelIndex newParentIndex = index(parentId);

        int rowToBeAdded = rowToAddChild(newParentNode, category);
        beginMoveRows(oldParentIndex, oldParentNode->childIds.indexOf(categoryId),
                      oldParentNode->childIds.indexOf(categoryId), newParentIndex, rowToBeAdded);
        oldParentNode->childIds.removeAll(oldCategory.categoryId());
        newParentNode->childIds.insert(rowToBeAdded, categoryId);
        categoryNode->parentId = parentId;
        endMoveRows();

        //this is a workaround to deal with the fact that the hasModelChildren field of DelegateModel
        //does not get updated when an index is updated to contain children
        beginResetModel();
        endResetModel();
    }
}

/*!
    \internal
*/
void QDeclarativeSupportedCategoriesModel::removedCategory(const QString &categoryId, const QString &parentId)
{
    if (m_response)
        return;

    if (!m_categoriesTree.contains(categoryId) || !m_categoriesTree.contains(parentId))
        return;

    QModelIndex parentIndex = index(parentId);
    QModelIndex categoryIndex = index(categoryId);

    beginRemoveRows(parentIndex, categoryIndex.row(), categoryIndex.row());
    PlaceCategoryNode *parentNode = m_categoriesTree.value(parentId);
    parentNode->childIds.removeAll(categoryId);
    delete m_categoriesTree.take(categoryId);
    endRemoveRows();
}

/*!
    \internal
*/
void QDeclarativeSupportedCategoriesModel::connectNotificationSignals()
{
    if (!m_plugin)
        return;

    QGeoServiceProvider *serviceProvider = m_plugin->sharedGeoServiceProvider();
    if (!serviceProvider || serviceProvider->error() != QGeoServiceProvider::NoError)
        return;

    QPlaceManager *placeManager = serviceProvider->placeManager();
    if (!placeManager)
        return;

    // listen for any category notifications so that we can reupdate the categories
    // model.
    connect(placeManager, &QPlaceManager::categoryAdded,
            this, &QDeclarativeSupportedCategoriesModel::addedCategory);
    connect(placeManager, &QPlaceManager::categoryUpdated,
            this, &QDeclarativeSupportedCategoriesModel::updatedCategory);
    connect(placeManager, &QPlaceManager::categoryRemoved,
            this, &QDeclarativeSupportedCategoriesModel::removedCategory);
    connect(placeManager, &QPlaceManager::dataChanged,
            this, &QDeclarativeSupportedCategoriesModel::emitDataChanged);
}

/*!
    \internal
*/
void QDeclarativeSupportedCategoriesModel::update()
{
    if (!m_complete)
        return;

    if (m_response)
        return;

    setStatus(Loading);

    if (!m_plugin) {
        updateLayout();
        setStatus(Error, QCoreApplication::translate(CONTEXT_NAME, PLUGIN_PROPERTY_NOT_SET));
        return;
    }

    QGeoServiceProvider *serviceProvider = m_plugin->sharedGeoServiceProvider();
    if (!serviceProvider || serviceProvider->error() != QGeoServiceProvider::NoError) {
        updateLayout();
        setStatus(Error, QCoreApplication::translate(CONTEXT_NAME, PLUGIN_PROVIDER_ERROR)
                         .arg(m_plugin->name()));
        return;
    }

    QPlaceManager *placeManager = serviceProvider->placeManager();
    if (!placeManager) {
        updateLayout();
        setStatus(Error, QCoreApplication::translate(CONTEXT_NAME, PLUGIN_ERROR)
                         .arg(m_plugin->name()).arg(serviceProvider->errorString()));
        return;
    }

    m_response = placeManager->initializeCategories();
    if (m_response) {
        connect(m_response, &QPlaceReply::finished,
                this, &QDeclarativeSupportedCategoriesModel::replyFinished);
    } else {
        updateLayout();
        setStatus(Error, QCoreApplication::translate(CONTEXT_NAME,
                    CATEGORIES_NOT_INITIALIZED));
    }
}

/*!
    \internal
*/
void QDeclarativeSupportedCategoriesModel::updateLayout()
{
    beginResetModel();
    qDeleteAll(m_categoriesTree);
    m_categoriesTree.clear();

    if (m_plugin) {
        QGeoServiceProvider *serviceProvider = m_plugin->sharedGeoServiceProvider();
        if (serviceProvider && serviceProvider->error() == QGeoServiceProvider::NoError) {
            QPlaceManager *placeManager = serviceProvider->placeManager();
            if (placeManager) {
                PlaceCategoryNode *node = new PlaceCategoryNode;
                node->childIds = populateCategories(placeManager, QPlaceCategory());
                m_categoriesTree.insert(QString(), node);
                node->declCategory = QSharedPointer<QDeclarativeCategory>
                    (new QDeclarativeCategory(QPlaceCategory(), m_plugin, this));
            }
        }
    }

    endResetModel();
}

QString QDeclarativeSupportedCategoriesModel::errorString() const
{
    return m_errorString;
}

/*!
    \qmlproperty enumeration CategoryModel::status

    This property holds the status of the model.  It can be one of:

    \table
        \row
            \li CategoryModel.Null
            \li No category fetch query has been executed.  The model is empty.
        \row
            \li CategoryModel.Ready
            \li No error occurred during the last operation, further operations may be performed on
               the model.
        \row
            \li CategoryModel.Loading
            \li The model is being updated, no other operations may be performed until complete.
        \row
            \li CategoryModel.Error
            \li An error occurred during the last operation, further operations can still be
               performed on the model.
    \endtable
*/
void QDeclarativeSupportedCategoriesModel::setStatus(Status status, const QString &errorString)
{
    Status originalStatus = m_status;
    m_status = status;
    m_errorString = errorString;

    if (originalStatus != m_status)
        emit statusChanged();
}

QDeclarativeSupportedCategoriesModel::Status QDeclarativeSupportedCategoriesModel::status() const
{
    return m_status;
}

/*!
    \internal
*/
QStringList QDeclarativeSupportedCategoriesModel::populateCategories(QPlaceManager *manager, const QPlaceCategory &parent)
{
    Q_ASSERT(manager);

    QStringList childIds;

    const auto byName = [](const QPlaceCategory &lhs, const QPlaceCategory &rhs) {
        return lhs.name() < rhs.name();
    };

    auto categories = manager->childCategories(parent.categoryId());
    std::sort(categories.begin(), categories.end(), byName);

    for (const auto &category : std::as_const(categories)) {
        auto node = new PlaceCategoryNode;
        node->parentId = parent.categoryId();
        node->declCategory = QSharedPointer<QDeclarativeCategory>(new QDeclarativeCategory(category, m_plugin ,this));

        if (m_hierarchical)
            node->childIds = populateCategories(manager, category);

        m_categoriesTree.insert(node->declCategory->categoryId(), node);
        childIds.append(category.categoryId());

        if (!m_hierarchical) {
            childIds.append(populateCategories(manager,node->declCategory->category()));
        }
    }
    return childIds;
}

/*!
    \internal
*/
QModelIndex QDeclarativeSupportedCategoriesModel::index(const QString &categoryId) const
{
    if (categoryId.isEmpty())
        return QModelIndex();

    if (!m_categoriesTree.contains(categoryId))
        return QModelIndex();

    PlaceCategoryNode *categoryNode = m_categoriesTree.value(categoryId);
    if (!categoryNode)
        return QModelIndex();

    QString parentCategoryId = categoryNode->parentId;

    PlaceCategoryNode *parentNode = m_categoriesTree.value(parentCategoryId);

    return createIndex(parentNode->childIds.indexOf(categoryId), 0, categoryNode);
}

/*!
    \internal
*/
int QDeclarativeSupportedCategoriesModel::rowToAddChild(PlaceCategoryNode *node, const QPlaceCategory &category)
{
    Q_ASSERT(node);
    for (qsizetype i = 0; i < node->childIds.count(); ++i) {
        if (category.name() < m_categoriesTree.value(node->childIds.at(i))->declCategory->name())
            return i;
    }
    return node->childIds.count();
}

/*!
    \qmlsignal CategoryModel::dataChanged()

   This signal is emitted when significant changes have been made to the underlying datastore.

   Applications should act on this signal at their own discretion.  The data
   provided by the model could be out of date and so the model should be reupdated
   sometime, however an immediate reupdate may be disconcerting to users if the categories
   change without any action on their part.

   The corresponding handler is \c onDataChanged.
*/

QT_END_NAMESPACE
