// Copyright (C) 2015 Jolla Ltd.
// 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 "qdeclarativegeomapitemview_p.h"
#include "qdeclarativegeomap_p.h"
#include "qdeclarativegeomapitembase_p.h"

#include <QtCore/QAbstractItemModel>
#include <QtQml/QQmlContext>
#include <QtQuick/private/qquickanimation_p.h>
#include <QtQml/QQmlListProperty>

QT_BEGIN_NAMESPACE

/*!
    \qmltype MapItemView
    \instantiates QDeclarativeGeoMapItemView
    \inqmlmodule QtLocation
    \ingroup qml-QtLocation5-maps
    \since QtLocation 5.5
    \inherits QObject

    \brief The MapItemView is used to populate Map from a model.

    The MapItemView is used to populate Map with MapItems from a model.
    The MapItemView type only makes sense when contained in a Map,
    meaning that it has no standalone presentation.

    \section2 Example Usage

    This example demonstrates how to use the MapViewItem object to display
    a \l{route}{route} on a \l{Map}{map}:

    \snippet declarative/maps.qml QtQuick import
    \snippet declarative/maps.qml QtLocation import
    \codeline
    \snippet declarative/maps.qml MapRoute
*/

/*!
    \qmlproperty Transition QtLocation::MapItemView::add

    This property holds the transition that is applied to the map items created by the view
    when they are instantiated and added to the map.

    \since QtLocation 5.12
*/

/*!
    \qmlproperty Transition QtLocation::MapItemView::remove

    This property holds the transition that is applied to the map items created by the view
    when they are removed.

    \since QtLocation 5.12
*/

QDeclarativeGeoMapItemView::QDeclarativeGeoMapItemView(QQuickItem *parent)
    : QDeclarativeGeoMapItemGroup(parent)
{
        m_exit = new QQuickTransition(this);
        QQmlListProperty<QQuickAbstractAnimation> anims = m_exit->animations();
        QQuickNumberAnimation *ani = new QQuickNumberAnimation(m_exit);
        ani->setProperty(QStringLiteral("opacity"));
        ani->setTo(0.0);
        ani->setDuration(300.0);
        anims.append(&anims, ani);
}

QDeclarativeGeoMapItemView::~QDeclarativeGeoMapItemView()
{
    // No need to remove instantiated items: if the MIV has instantiated items because it has been added
    // to a Map (or is child of a Map), the Map destructor takes care of removing it and the instantiated items.
}

/*!
    \internal
*/
void QDeclarativeGeoMapItemView::componentComplete()
{
    QDeclarativeGeoMapItemGroup::componentComplete();
    m_componentCompleted = true;
    if (!m_itemModel.isNull())
        m_delegateModel->setModel(m_itemModel);

    if (m_delegate)
        m_delegateModel->setDelegate(m_delegate);

    m_delegateModel->componentComplete();
}

void QDeclarativeGeoMapItemView::classBegin()
{
    QDeclarativeGeoMapItemGroup::classBegin();
    QQmlContext *ctx = qmlContext(this);
    m_delegateModel = new QQmlDelegateModel(ctx, this);
    m_delegateModel->classBegin();

    connect(m_delegateModel, &QQmlInstanceModel::modelUpdated, this, &QDeclarativeGeoMapItemView::modelUpdated);
    connect(m_delegateModel, &QQmlInstanceModel::createdItem, this, &QDeclarativeGeoMapItemView::createdItem);
//    connect(m_delegateModel, &QQmlInstanceModel::destroyingItem, this, &QDeclarativeGeoMapItemView::destroyingItem);
//    connect(m_delegateModel, &QQmlInstanceModel::initItem, this, &QDeclarativeGeoMapItemView::initItem);
}

void QDeclarativeGeoMapItemView::destroyingItem(QObject * /*object*/)
{

}

void QDeclarativeGeoMapItemView::initItem(int /*index*/, QObject * /*object*/)
{

}

void QDeclarativeGeoMapItemView::createdItem(int index, QObject * /*object*/)
{
    if (!m_map)
        return;
    // createdItem is emitted on asynchronous creation. In which case, object has to be invoked again.
    // See QQmlDelegateModel::object for further info.

    // DelegateModel apparently triggers this method in any case, that is:
    // 1. Synchronous incubation, delegate instantiated on the first object() call (during the object() call!)
    // 2. Async incubation, delegate not instantiated on the first object() call
    // 3. Async incubation, delegate present in the cache, and returned on the first object() call.
    //   createdItem also called during the object() call.
    if (m_creatingObject) {
        // Falling into case 1. or 3. Returning early to prevent double referencing the delegate instance.
        return;
    }

    QQuickItem *item = qobject_cast<QQuickItem *>(m_delegateModel->object(index, m_incubationMode));
    if (item)
        addDelegateToMap(item, index, true);
    else
        qWarning() << "QQmlDelegateModel:: object called in createdItem for " << index << " produced a null item";
}

void QDeclarativeGeoMapItemView::modelUpdated(const QQmlChangeSet &changeSet, bool reset)
{
    if (!m_map) // everything will be done in instantiateAllItems. Removal is done by declarativegeomap.
        return;

    // move changes are expressed as one remove + one insert, with the same moveId.
    // For simplicity, they will be treated as remove + insert.
    // Changes will be also ignored, as they represent only data changes, not layout changes
    if (reset) { // Assuming this means "remove everything already instantiated"
        removeInstantiatedItems();
    } else {
        // Remove items from the back to the front to retain the mapping to what is received from the changesets
        const QList<QQmlChangeSet::Change> &removes = changeSet.removes();
        std::map<int, int> mapRemoves;
        for (qsizetype i = 0; i < removes.size(); i++)
            mapRemoves.insert(std::pair<int, int>(removes.at(i).start(), i));

        for (auto rit = mapRemoves.rbegin(); rit != mapRemoves.rend(); ++rit) {
            const QQmlChangeSet::Change &c = removes.at(rit->second);
            for (auto idx = c.end() - 1; idx >= c.start(); --idx)
                removeDelegateFromMap(idx);
        }
    }

    QBoolBlocker createBlocker(m_creatingObject, true);
    for (const QQmlChangeSet::Change &c: changeSet.inserts()) {
        for (auto idx = c.start(); idx < c.end(); idx++) {
            QObject *delegateInstance = m_delegateModel->object(idx, m_incubationMode);
            addDelegateToMap(qobject_cast<QQuickItem *>(delegateInstance), idx);
        }
    }

    fitViewport();
}

/*!
    \qmlproperty model QtLocation::MapItemView::model

    This property holds the model that provides data used for creating the map items defined by the
    delegate. Only QAbstractItemModel based models are supported.
*/
QVariant QDeclarativeGeoMapItemView::model() const
{
    return m_itemModel;
}

void QDeclarativeGeoMapItemView::setModel(const QVariant &model)
{
    if (model == m_itemModel)
        return;

    m_itemModel = model;
    if (m_componentCompleted)
        m_delegateModel->setModel(m_itemModel);

    emit modelChanged();
}

/*!
    \qmlproperty Component QtLocation::MapItemView::delegate

    This property holds the delegate which defines how each item in the
    model should be displayed. The Component must contain exactly one
    MapItem -derived object as the root object.
*/
QQmlComponent *QDeclarativeGeoMapItemView::delegate() const
{
    return m_delegate;
}

void QDeclarativeGeoMapItemView::setDelegate(QQmlComponent *delegate)
{
    if (m_delegate == delegate)
        return;

    m_delegate = delegate;
    if (m_componentCompleted)
        m_delegateModel->setDelegate(m_delegate);

    emit delegateChanged();
}

/*!
    \qmlproperty bool QtLocation::MapItemView::autoFitViewport

    This property controls whether to automatically pan and zoom the viewport
    to display all map items when items are added or removed.

    Defaults to false.
*/
bool QDeclarativeGeoMapItemView::autoFitViewport() const
{
    return m_fitViewport;
}

void QDeclarativeGeoMapItemView::setAutoFitViewport(const bool &fit)
{
    if (fit == m_fitViewport)
        return;
    m_fitViewport = fit;
    fitViewport();
    emit autoFitViewportChanged();
}

/*!
    \internal
*/
void QDeclarativeGeoMapItemView::fitViewport()
{

    if (!m_map || !m_map->mapReady() || !m_fitViewport)
        return;

    if (m_map->mapItems().size() > 0)
        m_map->fitViewportToMapItems();
}

/*!
    \internal
*/
void QDeclarativeGeoMapItemView::setMap(QDeclarativeGeoMap *map)
{
    if (!map || m_map) // changing map on the fly not supported
        return;
    m_map = map;
    instantiateAllItems();
}

/*!
    \internal
*/
void QDeclarativeGeoMapItemView::removeInstantiatedItems(bool transition)
{
    if (!m_map)
        return;

    // with transition = false removeInstantiatedItems aborts ongoing exit transitions //QTBUG-69195
    // Backward as removeItemFromMap modifies m_instantiatedItems
    for (qsizetype i = m_instantiatedItems.size() -1; i >= 0 ; i--)
        removeDelegateFromMap(i, transition);
}

/*!
    \internal

    Instantiates all items.
*/
void QDeclarativeGeoMapItemView::instantiateAllItems()
{
    // The assumption is that if m_instantiatedItems isn't empty, instantiated items have been already added
    if (!m_componentCompleted || !m_map || !m_delegate || m_itemModel.isNull() || !m_instantiatedItems.isEmpty())
        return;

    // If here, m_delegateModel may contain data, but QQmlInstanceModel::object for each row hasn't been called yet.
    QBoolBlocker createBlocker(m_creatingObject, true);
    for (qsizetype i = 0; i < m_delegateModel->count(); i++) {
        QObject *delegateInstance = m_delegateModel->object(i, m_incubationMode);
        addDelegateToMap(qobject_cast<QQuickItem *>(delegateInstance), i);
    }

    fitViewport();
}

void QDeclarativeGeoMapItemView::setIncubateDelegates(bool useIncubators)
{
    const QQmlIncubator::IncubationMode incubationMode =
            (useIncubators) ? QQmlIncubator::Asynchronous : QQmlIncubator::Synchronous;
    if (m_incubationMode == incubationMode)
        return;
    m_incubationMode = incubationMode;
    emit incubateDelegatesChanged();
}

bool QDeclarativeGeoMapItemView::incubateDelegates() const
{
    return m_incubationMode == QQmlIncubator::Asynchronous;
}

QList<QQuickItem *> QDeclarativeGeoMapItemView::mapItems()
{
    return m_instantiatedItems;
}

QQmlInstanceModel::ReleaseFlags QDeclarativeGeoMapItemView::disposeDelegate(QQuickItem *item)
{
    disconnect(item, 0, this, 0);
    removeDelegateFromMap(item);
    item->setParentItem(nullptr);   // Needed because
    item->setParent(nullptr);       // m_delegateModel->release(item) does not destroy the item most of the times!!
    QQmlInstanceModel::ReleaseFlags releaseStatus = m_delegateModel->release(item);
    return releaseStatus;
}

void QDeclarativeGeoMapItemView::removeDelegateFromMap(int index, bool transition)
{
    if (index >= 0 && index < m_instantiatedItems.size()) {
        QQuickItem *item = m_instantiatedItems.takeAt(index);
        if (!item) { // not yet incubated
            // Don't cancel incubation explicitly when model rows are removed, as DelegateModel
            // apparently takes care of incubating elements when the model remove those indices.
            // Cancel them explicitly only when a MIV is removed from a map.
            if (!transition)
                m_delegateModel->cancel(index);
            return;
        }
        // item can be either a QDeclarativeGeoMapItemBase or a QDeclarativeGeoMapItemGroup (subclass)
        if (m_exit && m_map && transition) {
            transitionItemOut(item);
        } else {
            if (m_exit && m_map && !transition) {
                // check if the exit transition is still running, if so stop it.
                // This can happen when explicitly calling Map.removeMapItemView, soon after adding it.
                terminateExitTransition(item);
            }
            QQmlInstanceModel::ReleaseFlags releaseStatus = disposeDelegate(item);
#ifdef QT_DEBUG
            if (releaseStatus == QQmlInstanceModel::Referenced)
                qWarning() << "item "<< index << "(" << item << ") still referenced";
#else
            Q_UNUSED(releaseStatus);
#endif
        }
    }
}

void QDeclarativeGeoMapItemView::removeDelegateFromMap(QQuickItem *o)
{
    if (!m_map)
        return;

    QDeclarativeGeoMapItemBase *item = qobject_cast<QDeclarativeGeoMapItemBase *>(o);
    if (item) {
        m_map->removeMapItem(item);
        return;
    }
    QDeclarativeGeoMapItemView *view = qobject_cast<QDeclarativeGeoMapItemView *>(o);
    if (view) {
        m_map->removeMapItemView(view);
        return;
    }
    QDeclarativeGeoMapItemGroup *group = qobject_cast<QDeclarativeGeoMapItemGroup *>(o);
    if (group) {
        m_map->removeMapItemGroup(group);
        return;
    }
}

void QDeclarativeGeoMapItemView::transitionItemOut(QQuickItem *o)
{
    QDeclarativeGeoMapItemGroup *group = qobject_cast<QDeclarativeGeoMapItemGroup *>(o);
    if (group) {
        if (!group->m_transitionManager) {
            std::unique_ptr<QDeclarativeGeoMapItemTransitionManager>manager(new QDeclarativeGeoMapItemTransitionManager(group));
            group->m_transitionManager.swap(manager);
            group->m_transitionManager->m_view = this;
        }
        connect(group, &QDeclarativeGeoMapItemGroup::removeTransitionFinished,
                this, &QDeclarativeGeoMapItemView::exitTransitionFinished);

        group->m_transitionManager->transitionExit();
        return;
    }
    QDeclarativeGeoMapItemBase *item = qobject_cast<QDeclarativeGeoMapItemBase *>(o);
    if (item) {
        if (!item->m_transitionManager) {
            std::unique_ptr<QDeclarativeGeoMapItemTransitionManager> manager(new QDeclarativeGeoMapItemTransitionManager(item));
            item->m_transitionManager.swap(manager);
            item->m_transitionManager->m_view = this;
        }
        connect(item, &QDeclarativeGeoMapItemBase::removeTransitionFinished,
                this, &QDeclarativeGeoMapItemView::exitTransitionFinished);

        item->m_transitionManager->transitionExit();
        return;
    }
}

void QDeclarativeGeoMapItemView::terminateExitTransition(QQuickItem *o)
{
    QDeclarativeGeoMapItemGroup *group = qobject_cast<QDeclarativeGeoMapItemGroup *>(o);
    if (group && group->m_transitionManager) {
        group->m_transitionManager->cancel();
        return;
    }
    QDeclarativeGeoMapItemBase *item = qobject_cast<QDeclarativeGeoMapItemBase *>(o);
    if (item && item->m_transitionManager) {
        item->m_transitionManager->cancel();
        return;
    }
}

void QDeclarativeGeoMapItemView::exitTransitionFinished()
{
    QQuickItem *item = qobject_cast<QQuickItem *>(sender());
    if (!item)
        return;
    QQmlInstanceModel::ReleaseFlags releaseStatus = disposeDelegate(item);
#ifdef QT_DEBUG
    if (releaseStatus == QQmlInstanceModel::Referenced)
        qWarning() << "item "<<item<<" still referenced";
#else
    Q_UNUSED(releaseStatus);
#endif
}

void QDeclarativeGeoMapItemView::addItemToMap(QDeclarativeGeoMapItemBase *item, int index, bool createdItem)
{

    if (m_map && item->quickMap() == m_map) // test for *item done in the caller
        return;

    if (m_map) {
        insertInstantiatedItem(index, item, createdItem);
        item->setParentItem(this);
        m_map->addMapItem(item);
        if (m_enter) {
            if (!item->m_transitionManager) {
                std::unique_ptr<QDeclarativeGeoMapItemTransitionManager>manager(new QDeclarativeGeoMapItemTransitionManager(item));
                item->m_transitionManager.swap(manager);
            }
            item->m_transitionManager->m_view = this;
            item->m_transitionManager->transitionEnter();
        }
    }
}

void QDeclarativeGeoMapItemView::insertInstantiatedItem(int index, QQuickItem *o, bool createdItem)
{
    if (createdItem)
        m_instantiatedItems.replace(index, o);
    else
        m_instantiatedItems.insert(index, o);
}

void QDeclarativeGeoMapItemView::addItemViewToMap(QDeclarativeGeoMapItemView *item, int index, bool createdItem)
{
    if (m_map && item->quickMap() == m_map)  // test for *item done in the caller
        return;

    if (m_map) {
        insertInstantiatedItem(index, item, createdItem);
        item->setParentItem(this);
        m_map->addMapItemView(item);
        if (m_enter) {
            if (!item->m_transitionManager) {
                std::unique_ptr<QDeclarativeGeoMapItemTransitionManager> manager(new QDeclarativeGeoMapItemTransitionManager(item));
                item->m_transitionManager.swap(manager);
            }
            item->m_transitionManager->m_view = this;
            item->m_transitionManager->transitionEnter();
        }
    }
}

void QDeclarativeGeoMapItemView::addItemGroupToMap(QDeclarativeGeoMapItemGroup *item, int index, bool createdItem)
{
    if (m_map && item->quickMap() == m_map) // test for *item done in the caller
        return;

    if (m_map) {
        insertInstantiatedItem(index, item, createdItem);
        item->setParentItem(this);
        m_map->addMapItemGroup(item);
        if (m_enter) {
            if (!item->m_transitionManager) {
                std::unique_ptr<QDeclarativeGeoMapItemTransitionManager>manager(new QDeclarativeGeoMapItemTransitionManager(item));
                item->m_transitionManager.swap(manager);
            }
            item->m_transitionManager->m_view = this;
            item->m_transitionManager->transitionEnter();
        }
    }
}

void QDeclarativeGeoMapItemView::addDelegateToMap(QQuickItem *object, int index, bool createdItem)
{
    if (!object) {
        if (!createdItem)
            m_instantiatedItems.insert(index, nullptr); // insert placeholder
        return;
    }
    QDeclarativeGeoMapItemBase *item = qobject_cast<QDeclarativeGeoMapItemBase *>(object);
    if (item) { // else createdItem will be emitted.
        addItemToMap(item, index, createdItem);
        return;
    }
    QDeclarativeGeoMapItemView *view = qobject_cast<QDeclarativeGeoMapItemView *>(object);
    if (view) {
        addItemViewToMap(view, index, createdItem);
        return;
    }
    QDeclarativeGeoMapItemGroup *group = qobject_cast<QDeclarativeGeoMapItemGroup *>(object);
    if (group) {
        addItemGroupToMap(group, index, createdItem);
        return;
    }
    qWarning() << "addDelegateToMap called with a "<< object->metaObject()->className();
}

QT_END_NAMESPACE


