/****************************************************************************
**
** Copyright (C) 2015 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing/
**
** This file is part of the Qt Labs Templates 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 "qquicktumbler_p.h"

#include <QtQuick/private/qquickflickable_p.h>
#include <QtLabsTemplates/private/qquickcontrol_p_p.h>

QT_BEGIN_NAMESPACE

/*!
    \qmltype Tumbler
    \inherits Control
    \instantiates QQuickTumbler
    \inqmlmodule Qt.labs.controls
    \ingroup qtlabscontrols-input
    \brief A spinnable wheel of items that can be selected.

    \code
    Tumbler {
        model: 5
        // ...
    }
    \endcode

    \section1 Non-wrapping Tumbler

    The default contentItem of Tumbler is a \l PathView, which wraps when it
    reaches the top and bottom. To achieve a non-wrapping Tumbler, use ListView
    as the contentItem:

    \snippet tst_tumbler.qml contentItem

    \image qtlabscontrols-tumbler-wrap.gif

    \labs

    \sa {Customizing Tumbler}, {Input Controls}
*/

class QQuickTumblerPrivate : public QQuickControlPrivate, public QQuickItemChangeListener
{
    Q_DECLARE_PUBLIC(QQuickTumbler)

public:
    QQuickTumblerPrivate() :
        delegate(Q_NULLPTR),
        visibleItemCount(3)
    {
    }

    ~QQuickTumblerPrivate()
    {
    }

    QVariant model;
    QQmlComponent *delegate;
    int visibleItemCount;

    void _q_updateItemHeights();
    void _q_updateItemWidths();

    void itemChildAdded(QQuickItem *, QQuickItem *) Q_DECL_OVERRIDE;
    void itemChildRemoved(QQuickItem *, QQuickItem *) Q_DECL_OVERRIDE;
};

static QList<QQuickItem *> contentItemChildItems(QQuickItem *contentItem)
{
    if (!contentItem)
        return QList<QQuickItem *>();

    // PathView has no contentItem property, but ListView does.
    QQuickFlickable *flickable = qobject_cast<QQuickFlickable *>(contentItem);
    return flickable ? flickable->contentItem()->childItems() : contentItem->childItems();
}

namespace {
    static inline qreal delegateHeight(const QQuickTumbler *tumbler)
    {
        return tumbler->availableHeight() / tumbler->visibleItemCount();
    }

    enum ContentItemType {
        UnsupportedContentItemType,
        PathViewContentItem,
        ListViewContentItem
    };

    static inline QQuickItem *actualContentItem(QQuickItem *rootContentItem, ContentItemType contentType)
    {
        if (contentType == PathViewContentItem)
            return rootContentItem;
        else if (contentType == ListViewContentItem)
            return qobject_cast<QQuickFlickable*>(rootContentItem)->contentItem();

        return Q_NULLPTR;
    }

    static inline ContentItemType contentItemType(QQuickItem *rootContentItem)
    {
        if (rootContentItem->inherits("QQuickPathView"))
            return PathViewContentItem;
        else if (rootContentItem->inherits("QQuickListView"))
            return ListViewContentItem;

        return UnsupportedContentItemType;
    }

    static inline ContentItemType contentItemTypeFromDelegate(QQuickItem *delegateItem)
    {
        if (delegateItem->parentItem()->inherits("QQuickPathView")) {
            return PathViewContentItem;
        } else if (delegateItem->parentItem()->parentItem()
            && delegateItem->parentItem()->parentItem()->inherits("QQuickListView")) {
            return ListViewContentItem;
        }

        return UnsupportedContentItemType;
    }
}

void QQuickTumblerPrivate::_q_updateItemHeights()
{
    // Can't use our own private padding members here, as the padding property might be set,
    // which doesn't affect them, only their getters.
    Q_Q(const QQuickTumbler);
    const qreal itemHeight = delegateHeight(q);
    foreach (QQuickItem *childItem, contentItemChildItems(contentItem))
        childItem->setHeight(itemHeight);
}

void QQuickTumblerPrivate::_q_updateItemWidths()
{
    Q_Q(const QQuickTumbler);
    const qreal availableWidth = q->availableWidth();
    foreach (QQuickItem *childItem, contentItemChildItems(contentItem))
        childItem->setWidth(availableWidth);
}

void QQuickTumblerPrivate::itemChildAdded(QQuickItem *, QQuickItem *)
{
    _q_updateItemWidths();
    _q_updateItemHeights();
}

void QQuickTumblerPrivate::itemChildRemoved(QQuickItem *, QQuickItem *)
{
    _q_updateItemWidths();
    _q_updateItemHeights();
}

QQuickTumbler::QQuickTumbler(QQuickItem *parent) :
    QQuickControl(*(new QQuickTumblerPrivate), parent)
{
    setActiveFocusOnTab(true);

    connect(this, SIGNAL(leftPaddingChanged()), this, SLOT(_q_updateItemWidths()));
    connect(this, SIGNAL(rightPaddingChanged()), this, SLOT(_q_updateItemWidths()));
    connect(this, SIGNAL(topPaddingChanged()), this, SLOT(_q_updateItemHeights()));
    connect(this, SIGNAL(bottomPaddingChanged()), this, SLOT(_q_updateItemHeights()));
}

QQuickTumbler::~QQuickTumbler()
{
}

/*!
    \qmlproperty variant Qt.labs.controls::Tumbler::model

    This property holds the model that provides data for this tumbler.
*/
QVariant QQuickTumbler::model() const
{
    Q_D(const QQuickTumbler);
    return d->model;
}

void QQuickTumbler::setModel(const QVariant &model)
{
    Q_D(QQuickTumbler);
    if (model != d->model) {
        d->model = model;
        emit modelChanged();
    }
}

/*!
    \qmlproperty int Qt.labs.controls::Tumbler::count
    \readonly

    This property holds the number of items in the model.
*/
int QQuickTumbler::count() const
{
    Q_D(const QQuickTumbler);
    return d->contentItem->property("count").toInt();
}

/*!
    \qmlproperty int Qt.labs.controls::Tumbler::currentIndex

    This property holds the index of the current item.
*/
int QQuickTumbler::currentIndex() const
{
    Q_D(const QQuickTumbler);
    return d->contentItem ? d->contentItem->property("currentIndex").toInt() : -1;
}

void QQuickTumbler::setCurrentIndex(int currentIndex)
{
    Q_D(QQuickTumbler);
    d->contentItem->setProperty("currentIndex", currentIndex);
}

/*!
    \qmlproperty Item Qt.labs.controls::Tumbler::currentItem
    \readonly

    This property holds the item at the current index.
*/
QQuickItem *QQuickTumbler::currentItem() const
{
    Q_D(const QQuickTumbler);
    return d->contentItem ? d->contentItem->property("currentItem").value<QQuickItem*>() : Q_NULLPTR;
}

/*!
    \qmlproperty component Qt.labs.controls::Tumbler::delegate

    This property holds the delegate used to display each item.
*/
QQmlComponent *QQuickTumbler::delegate() const
{
    Q_D(const QQuickTumbler);
    return d->delegate;
}

void QQuickTumbler::setDelegate(QQmlComponent *delegate)
{
    Q_D(QQuickTumbler);
    if (delegate != d->delegate) {
        d->delegate = delegate;
        emit delegateChanged();
    }
}

/*!
    \qmlproperty int Qt.labs.controls::Tumbler::visibleItemCount

    This property holds the number of items visible in the tumbler. It must be
    an odd number, as the current item is always vertically centered.
*/
int QQuickTumbler::visibleItemCount() const
{
    Q_D(const QQuickTumbler);
    return d->visibleItemCount;
}

void QQuickTumbler::setVisibleItemCount(int visibleItemCount)
{
    Q_D(QQuickTumbler);
    if (visibleItemCount != d->visibleItemCount) {
        d->visibleItemCount = visibleItemCount;
        d->_q_updateItemHeights();
        emit visibleItemCountChanged();
    }
}

QQuickTumblerAttached *QQuickTumbler::qmlAttachedProperties(QObject *object)
{
    QQuickItem *delegateItem = qobject_cast<QQuickItem *>(object);
    if (!delegateItem) {
        qWarning() << "Tumbler: attached properties of Tumbler must be accessed from within a delegate item";
        return Q_NULLPTR;
    }

    return new QQuickTumblerAttached(delegateItem);
}

void QQuickTumbler::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
{
    Q_D(QQuickTumbler);

    QQuickControl::geometryChanged(newGeometry, oldGeometry);

    d->_q_updateItemHeights();

    if (newGeometry.width() != oldGeometry.width())
        d->_q_updateItemWidths();
}

void QQuickTumbler::componentComplete()
{
    Q_D(QQuickTumbler);
    QQuickControl::componentComplete();
    d->_q_updateItemHeights();
    d->_q_updateItemWidths();
}

void QQuickTumbler::contentItemChange(QQuickItem *newItem, QQuickItem *oldItem)
{
    Q_D(QQuickTumbler);

    QQuickControl::contentItemChange(newItem, oldItem);

    // Since we use the currentIndex of the contentItem directly, we must
    // ensure that we keep track of the currentIndex so it doesn't get lost
    // between contentItem changes.
    const int previousCurrentIndex = currentIndex();

    if (oldItem) {
        disconnect(oldItem, SIGNAL(currentIndexChanged()), this, SIGNAL(currentIndexChanged()));
        disconnect(oldItem, SIGNAL(currentItemChanged()), this, SIGNAL(currentItemChanged()));
        disconnect(oldItem, SIGNAL(countChanged()), this, SIGNAL(countChanged()));

        ContentItemType oldContentItemType = contentItemType(oldItem);
        QQuickItem *actualOldContentItem = actualContentItem(oldItem, oldContentItemType);
        QQuickItemPrivate *actualContentItemPrivate = QQuickItemPrivate::get(actualOldContentItem);
        actualContentItemPrivate->removeItemChangeListener(d, QQuickItemPrivate::Children);
    }

    if (newItem) {
        ContentItemType contentType = contentItemType(newItem);
        if (contentType == UnsupportedContentItemType) {
            qWarning() << "Tumbler: contentItems other than PathView and ListView are not supported";
            return;
        }

        connect(newItem, SIGNAL(currentIndexChanged()), this, SIGNAL(currentIndexChanged()));
        connect(newItem, SIGNAL(currentItemChanged()), this, SIGNAL(currentItemChanged()));
        connect(newItem, SIGNAL(countChanged()), this, SIGNAL(countChanged()));

        QQuickItem *actualNewContentItem = actualContentItem(newItem, contentType);
        QQuickItemPrivate *actualContentItemPrivate = QQuickItemPrivate::get(actualNewContentItem);
        actualContentItemPrivate->addItemChangeListener(d, QQuickItemPrivate::Children);

        // If the previous currentIndex is -1, it means we had no contentItem previously.
        if (previousCurrentIndex != -1) {
            // Can't call setCurrentIndex here, as contentItemChange() is
            // called *before* the contentItem is set.
            newItem->setProperty("currentIndex", previousCurrentIndex);
        }
    }
}

void QQuickTumbler::keyPressEvent(QKeyEvent *event)
{
    Q_D(QQuickTumbler);

    QQuickControl::keyPressEvent(event);

    if (event->isAutoRepeat())
        return;

    if (event->key() == Qt::Key_Up) {
        QMetaObject::invokeMethod(d->contentItem, "decrementCurrentIndex");
    } else if (event->key() == Qt::Key_Down) {
        QMetaObject::invokeMethod(d->contentItem, "incrementCurrentIndex");
    }
}

class QQuickTumblerAttachedPrivate : public QObjectPrivate, public QQuickItemChangeListener
{
    Q_DECLARE_PUBLIC(QQuickTumblerAttached)
public:
    QQuickTumblerAttachedPrivate(QQuickItem *delegateItem) :
        tumbler(Q_NULLPTR),
        index(-1),
        displacement(1)
    {
        if (!delegateItem->parentItem()) {
            qWarning() << "Tumbler: attached properties must be accessed from within a delegate item that has a parent";
            return;
        }

        QVariant indexContextProperty = qmlContext(delegateItem)->contextProperty(QStringLiteral("index"));
        if (!indexContextProperty.isValid()) {
            qWarning() << "Tumbler: attempting to access attached property on item without an \"index\" property";
            return;
        }

        index = indexContextProperty.toInt();
        const ContentItemType contentItemType = contentItemTypeFromDelegate(delegateItem);
        if (contentItemType == UnsupportedContentItemType)
            return;

        // ListView has an "additional" content item.
        tumbler = qobject_cast<QQuickTumbler* >(contentItemType == PathViewContentItem
            ? delegateItem->parentItem()->parentItem() : delegateItem->parentItem()->parentItem()->parentItem());
        Q_ASSERT(tumbler);
    }

    ~QQuickTumblerAttachedPrivate() {
    }

    void itemGeometryChanged(QQuickItem *item, const QRectF &newGeometry, const QRectF &oldGeometry) Q_DECL_OVERRIDE;
    void itemChildAdded(QQuickItem *, QQuickItem *) Q_DECL_OVERRIDE;
    void itemChildRemoved(QQuickItem *, QQuickItem *) Q_DECL_OVERRIDE;

    void _q_calculateDisplacement();

    // The Tumbler that contains the delegate. Required to calculated the displacement.
    QQuickTumbler *tumbler;
    // The index of the delegate. Used to calculate the displacement.
    int index;
    // The displacement for our delegate.
    qreal displacement;
};

void QQuickTumblerAttachedPrivate::itemGeometryChanged(QQuickItem *, const QRectF &, const QRectF &)
{
    _q_calculateDisplacement();
}

void QQuickTumblerAttachedPrivate::itemChildAdded(QQuickItem *, QQuickItem *)
{
    _q_calculateDisplacement();
}

void QQuickTumblerAttachedPrivate::itemChildRemoved(QQuickItem *item, QQuickItem *child)
{
    _q_calculateDisplacement();

    if (parent == child) {
        // The child that was removed from the contentItem was the delegate
        // that our properties are attached to. If we don't remove the change
        // listener, the contentItem will attempt to notify a destroyed
        // listener, causing a crash.

        // item is the "actual content item" of Tumbler's contentItem, i.e. a PathView or ListView.contentItem
        QQuickItemPrivate *p = QQuickItemPrivate::get(item);
        p->removeItemChangeListener(this, QQuickItemPrivate::Geometry | QQuickItemPrivate::Children);
    }
}

void QQuickTumblerAttachedPrivate::_q_calculateDisplacement()
{
    const int previousDisplacement = displacement;
    displacement = 0;

    // This can happen in tests, so it may happen in normal usage too.
    if (tumbler->count() == 0)
        return;

    ContentItemType contentType = contentItemType(tumbler->contentItem());
    if (contentType == UnsupportedContentItemType)
        return;

    qreal offset = 0;

    if (contentType == PathViewContentItem) {
        offset = tumbler->contentItem()->property("offset").toReal();

        displacement = tumbler->count() - index - offset;
        int halfVisibleItems = tumbler->visibleItemCount() / 2 + 1;
        if (displacement > halfVisibleItems)
            displacement -= tumbler->count();
        else if (displacement < -halfVisibleItems)
            displacement += tumbler->count();
    } else {
        const qreal contentY = tumbler->contentItem()->property("contentY").toReal();
        const qreal delegateH = delegateHeight(tumbler);
        const qreal preferredHighlightBegin = tumbler->contentItem()->property("preferredHighlightBegin").toReal();
        // Tumbler's displacement goes from negative at the top to positive towards the bottom, so we must switch this around.
        const qreal reverseDisplacement = (contentY + preferredHighlightBegin) / delegateH;
        displacement = reverseDisplacement - index;
    }

    Q_Q(QQuickTumblerAttached);
    if (displacement != previousDisplacement)
        emit q->displacementChanged();
}

QQuickTumblerAttached::QQuickTumblerAttached(QQuickItem *delegateItem) :
    QObject(*(new QQuickTumblerAttachedPrivate(delegateItem)), delegateItem)
{
    Q_D(QQuickTumblerAttached);
    if (d->tumbler) {
        QQuickItem *rootContentItem = d->tumbler->contentItem();
        const ContentItemType contentType = contentItemType(rootContentItem);
        QQuickItemPrivate *p = QQuickItemPrivate::get(actualContentItem(rootContentItem, contentType));
        p->addItemChangeListener(d, QQuickItemPrivate::Geometry | QQuickItemPrivate::Children);

        const char *contentItemSignal = contentType == PathViewContentItem
            ? SIGNAL(offsetChanged()) : SIGNAL(contentYChanged());
        connect(d->tumbler->contentItem(), contentItemSignal, this, SLOT(_q_calculateDisplacement()));
    }
}

QQuickTumblerAttached::~QQuickTumblerAttached()
{
}

QQuickTumbler *QQuickTumblerAttached::tumbler() const
{
    Q_D(const QQuickTumblerAttached);
    return d->tumbler;
}

qreal QQuickTumblerAttached::displacement() const
{
    Q_D(const QQuickTumblerAttached);
    return d->displacement;
}

QT_END_NAMESPACE

#include "moc_qquicktumbler_p.cpp"
