/***************************************************************************
 *   Copyright (C) 2011 by Peter Penz <peter.penz19@gmail.com>             *
 *                                                                         *
 *   Based on the Itemviews NG project from Trolltech Labs:                *
 *   http://qt.gitorious.org/qt-labs/itemviews-ng                          *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA            *
 ***************************************************************************/

#include "kitemlistview.h"

#include "kitemlistcontroller.h"
#include "kitemlistgroupheader.h"
#include "kitemlistselectionmanager.h"
#include "kitemlistsizehintresolver_p.h"
#include "kitemlistviewlayouter_p.h"
#include "kitemlistviewanimation_p.h"
#include "kitemlistwidget.h"

#include <KDebug>

#include <QGraphicsSceneMouseEvent>
#include <QPropertyAnimation>
#include <QStyle>
#include <QTimer>

KItemListView::KItemListView(QGraphicsWidget* parent) :
    QGraphicsWidget(parent),
    m_grouped(false),
    m_activeTransactions(0),
    m_itemSize(),
    m_controller(0),
    m_model(0),
    m_visibleRoles(),
    m_visibleRolesSizes(),
    m_widgetCreator(0),
    m_groupHeaderCreator(0),
    m_styleOption(),
    m_visibleItems(),
    m_visibleGroups(),
    m_sizeHintResolver(0),
    m_layouter(0),
    m_animation(0),
    m_layoutTimer(0),
    m_oldOffset(0),
    m_oldMaximumOffset(0)
{
    setAcceptHoverEvents(true);

    m_sizeHintResolver = new KItemListSizeHintResolver(this);

    m_layouter = new KItemListViewLayouter(this);
    m_layouter->setSizeHintResolver(m_sizeHintResolver);

    m_animation = new KItemListViewAnimation(this);
    connect(m_animation, SIGNAL(finished(QGraphicsWidget*,KItemListViewAnimation::AnimationType)),
            this, SLOT(slotAnimationFinished(QGraphicsWidget*,KItemListViewAnimation::AnimationType)));

    m_layoutTimer = new QTimer(this);
    m_layoutTimer->setInterval(300);
    m_layoutTimer->setSingleShot(true);
    connect(m_layoutTimer, SIGNAL(timeout()), this, SLOT(slotLayoutTimerFinished()));
}

KItemListView::~KItemListView()
{
    delete m_sizeHintResolver;
    m_sizeHintResolver = 0;
}

void KItemListView::setScrollOrientation(Qt::Orientation orientation)
{
    const Qt::Orientation previousOrientation = m_layouter->scrollOrientation();
    if (orientation == previousOrientation) {
        return;
    }

    m_layouter->setScrollOrientation(orientation);
    m_animation->setScrollOrientation(orientation);
    m_sizeHintResolver->clearCache();
    updateLayout();
    onScrollOrientationChanged(orientation, previousOrientation);
}

Qt::Orientation KItemListView::scrollOrientation() const
{
    return m_layouter->scrollOrientation();
}

void KItemListView::setItemSize(const QSizeF& itemSize)
{
    const QSizeF previousSize = m_itemSize;
    if (itemSize == previousSize) {
        return;
    }

    m_itemSize = itemSize;

    if (!markVisibleRolesSizesAsDirty()) {
        if (itemSize.width() < previousSize.width() || itemSize.height() < previousSize.height()) {
            prepareLayoutForIncreasedItemCount(itemSize, ItemSize);
        } else {
            m_layouter->setItemSize(itemSize);
        }
    }

    m_sizeHintResolver->clearCache();
    updateLayout();
    onItemSizeChanged(itemSize, previousSize);
}

QSizeF KItemListView::itemSize() const
{
    return m_itemSize;
}

void KItemListView::setOffset(qreal offset)
{
    if (offset < 0) {
        offset = 0;
    }

    const qreal previousOffset = m_layouter->offset();
    if (offset == previousOffset) {
        return;
    }

    m_layouter->setOffset(offset);
    m_animation->setOffset(offset);
    if (!m_layoutTimer->isActive()) {
        doLayout(NoAnimation, 0, 0);
        update();
    }
    onOffsetChanged(offset, previousOffset);
}

qreal KItemListView::offset() const
{
    return m_layouter->offset();
}

qreal KItemListView::maximumOffset() const
{
    return m_layouter->maximumOffset();
}

void KItemListView::setVisibleRoles(const QHash<QByteArray, int>& roles)
{
    const QHash<QByteArray, int> previousRoles = m_visibleRoles;
    m_visibleRoles = roles;

    QHashIterator<int, KItemListWidget*> it(m_visibleItems);
    while (it.hasNext()) {
        it.next();
        KItemListWidget* widget = it.value();
        widget->setVisibleRoles(roles);
        widget->setVisibleRolesSizes(m_visibleRolesSizes);
    }

    m_sizeHintResolver->clearCache();
    m_layouter->markAsDirty();
    onVisibleRolesChanged(roles, previousRoles);

    markVisibleRolesSizesAsDirty();
    updateLayout();
}

QHash<QByteArray, int> KItemListView::visibleRoles() const
{
    return m_visibleRoles;
}

KItemListController* KItemListView::controller() const
{
    return m_controller;
}

KItemModelBase* KItemListView::model() const
{
    return m_model;
}

void KItemListView::setWidgetCreator(KItemListWidgetCreatorBase* widgetCreator)
{
    m_widgetCreator = widgetCreator;
}

KItemListWidgetCreatorBase* KItemListView::widgetCreator() const
{
    return m_widgetCreator;
}

void KItemListView::setGroupHeaderCreator(KItemListGroupHeaderCreatorBase* groupHeaderCreator)
{
    m_groupHeaderCreator = groupHeaderCreator;
}

KItemListGroupHeaderCreatorBase* KItemListView::groupHeaderCreator() const
{
    return m_groupHeaderCreator;
}

void KItemListView::setStyleOption(const KItemListStyleOption& option)
{
    const KItemListStyleOption previousOption = m_styleOption;
    m_styleOption = option;

    QHashIterator<int, KItemListWidget*> it(m_visibleItems);
    while (it.hasNext()) {
        it.next();
        it.value()->setStyleOption(option);
    }

    m_sizeHintResolver->clearCache();
    updateLayout();
    onStyleOptionChanged(option, previousOption);
}

const KItemListStyleOption& KItemListView::styleOption() const
{
    return m_styleOption;
}

void KItemListView::setGeometry(const QRectF& rect)
{
    QGraphicsWidget::setGeometry(rect);
    if (!m_model) {
        return;
    }

    if (m_itemSize.isEmpty()) {
        m_layouter->setItemSize(QSizeF());
    }

    if (m_model->count() > 0) {
        prepareLayoutForIncreasedItemCount(rect.size(), LayouterSize);
    } else {
        m_layouter->setSize(rect.size());
    }

    m_layoutTimer->start();
}

int KItemListView::itemAt(const QPointF& pos) const
{
    if (!m_model) {
        return -1;
    }

    QHashIterator<int, KItemListWidget*> it(m_visibleItems);
    while (it.hasNext()) {
        it.next();

        const KItemListWidget* widget = it.value();
        const QPointF mappedPos = widget->mapFromItem(this, pos);
        if (widget->contains(mappedPos)) {
            return it.key();
        }
    }

    return -1;
}

bool KItemListView::isAboveSelectionToggle(int index, const QPointF& pos) const
{
    Q_UNUSED(index);
    Q_UNUSED(pos);
    return false;
}

bool KItemListView::isAboveExpansionToggle(int index, const QPointF& pos) const
{
    const KItemListWidget* widget = m_visibleItems.value(index);
    if (widget) {
        const QRectF expansionToggleRect = widget->expansionToggleRect();
        if (!expansionToggleRect.isEmpty()) {
            const QPointF mappedPos = widget->mapFromItem(this, pos);
            return expansionToggleRect.contains(mappedPos);
        }
    }
    return false;
}

int KItemListView::firstVisibleIndex() const
{
    return m_layouter->firstVisibleIndex();
}

int KItemListView::lastVisibleIndex() const
{
    return m_layouter->lastVisibleIndex();
}

QSizeF KItemListView::itemSizeHint(int index) const
{
    Q_UNUSED(index);
    return itemSize();
}

QHash<QByteArray, QSizeF> KItemListView::visibleRoleSizes() const
{
    return QHash<QByteArray, QSizeF>();
}

void KItemListView::beginTransaction()
{
    ++m_activeTransactions;
    if (m_activeTransactions == 1) {
        onTransactionBegin();
    }
}

void KItemListView::endTransaction()
{
    --m_activeTransactions;
    if (m_activeTransactions < 0) {
        m_activeTransactions = 0;
        kWarning() << "Mismatch between beginTransaction()/endTransaction()";
    }

    if (m_activeTransactions == 0) {
        onTransactionEnd();
        updateLayout();
    }
}

bool KItemListView::isTransactionActive() const
{
    return m_activeTransactions > 0;
}

void KItemListView::initializeItemListWidget(KItemListWidget* item)
{
    Q_UNUSED(item);
}

void KItemListView::onControllerChanged(KItemListController* current, KItemListController* previous)
{
    Q_UNUSED(current);
    Q_UNUSED(previous);
}

void KItemListView::onModelChanged(KItemModelBase* current, KItemModelBase* previous)
{
    Q_UNUSED(current);
    Q_UNUSED(previous);
}

void KItemListView::onScrollOrientationChanged(Qt::Orientation current, Qt::Orientation previous)
{
    Q_UNUSED(current);
    Q_UNUSED(previous);
}

void KItemListView::onItemSizeChanged(const QSizeF& current, const QSizeF& previous)
{
    Q_UNUSED(current);
    Q_UNUSED(previous);
}

void KItemListView::onOffsetChanged(qreal current, qreal previous)
{
    Q_UNUSED(current);
    Q_UNUSED(previous);
}

void KItemListView::onVisibleRolesChanged(const QHash<QByteArray, int>& current, const QHash<QByteArray, int>& previous)
{
    Q_UNUSED(current);
    Q_UNUSED(previous);
}

void KItemListView::onStyleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous)
{
    Q_UNUSED(current);
    Q_UNUSED(previous);
}

void KItemListView::onTransactionBegin()
{
}

void KItemListView::onTransactionEnd()
{
}

bool KItemListView::event(QEvent* event)
{
    // Forward all events to the controller and handle them there
    if (m_controller && m_controller->processEvent(event, transform())) {
        event->accept();
        return true;
    }
    return QGraphicsWidget::event(event);
}

void KItemListView::mousePressEvent(QGraphicsSceneMouseEvent* event)
{
    event->accept();
}

void KItemListView::hoverMoveEvent(QGraphicsSceneHoverEvent* event)
{
    if (!m_model) {
        return;
    }

    QHashIterator<int, KItemListWidget*> it(m_visibleItems);
    while (it.hasNext()) {
        it.next();

        KItemListWidget* widget = it.value();
        KItemListStyleOption styleOption = widget->styleOption();
        const QPointF mappedPos = widget->mapFromItem(this, event->pos());

        const bool hovered = widget->contains(mappedPos) &&
                             !widget->expansionToggleRect().contains(mappedPos) &&
                             !widget->selectionToggleRect().contains(mappedPos);
        if (hovered) {
            if (!(styleOption.state & QStyle::State_MouseOver)) {
                styleOption.state |= QStyle::State_MouseOver;
                widget->setStyleOption(styleOption);
            }
        } else if (styleOption.state & QStyle::State_MouseOver) {
            styleOption.state &= ~QStyle::State_MouseOver;
            widget->setStyleOption(styleOption);
        }
    }
}

void KItemListView::hoverLeaveEvent(QGraphicsSceneHoverEvent* event)
{
    Q_UNUSED(event);

    if (!m_model) {
        return;
    }

    QHashIterator<int, KItemListWidget*> it(m_visibleItems);
    while (it.hasNext()) {
        it.next();

        KItemListWidget* widget = it.value();
        KItemListStyleOption styleOption = widget->styleOption();
        if (styleOption.state & QStyle::State_MouseOver) {
            styleOption.state &= ~QStyle::State_MouseOver;
            widget->setStyleOption(styleOption);
        }
    }
}

QList<KItemListWidget*> KItemListView::visibleItemListWidgets() const
{
    return m_visibleItems.values();
}

void KItemListView::slotItemsInserted(const KItemRangeList& itemRanges)
{
    markVisibleRolesSizesAsDirty();

    const bool hasMultipleRanges = (itemRanges.count() > 1);
    if (hasMultipleRanges) {
        beginTransaction();
    }

    foreach (const KItemRange& range, itemRanges) {
        const int index = range.index;
        const int count = range.count;
        if (index < 0 || count <= 0) {
            kWarning() << "Invalid item range (index:" << index << ", count:" << count << ")";
            continue;
        }

        m_sizeHintResolver->itemsInserted(index, count);

        // Determine which visible items must be moved
        QList<int> itemsToMove;
        QHashIterator<int, KItemListWidget*> it(m_visibleItems);
        while (it.hasNext()) {
            it.next();
            const int visibleItemIndex = it.key();
            if (visibleItemIndex >= index) {
                itemsToMove.append(visibleItemIndex);
            }
        }

        // Update the indexes of all KItemListWidget instances that are located
        // after the inserted items. It is important to adjust the indexes in the order
        // from the highest index to the lowest index to prevent overlaps when setting the new index.
        qSort(itemsToMove);
        for (int i = itemsToMove.count() - 1; i >= 0; --i) {
            KItemListWidget* widget = m_visibleItems.value(itemsToMove[i]);
            Q_ASSERT(widget);
            setWidgetIndex(widget, widget->index() + count);
        }

        m_layouter->markAsDirty();
        if (m_model->count() == count && maximumOffset() > size().height()) {
            kDebug() << "Scrollbar required, skipping layout";
            const int scrollBarExtent = style()->pixelMetric(QStyle::PM_ScrollBarExtent);
            QSizeF layouterSize = m_layouter->size();
            if (scrollOrientation() == Qt::Vertical) {
                layouterSize.rwidth() -= scrollBarExtent;
            } else {
                layouterSize.rheight() -= scrollBarExtent;
            }
            m_layouter->setSize(layouterSize);
        }

        if (!hasMultipleRanges) {
            doLayout(Animation, index, count);
            update();
        }

        if (m_controller) {
            KItemListSelectionManager* selectionManager = m_controller->selectionManager();
            const int current = selectionManager->currentItem();
            if (current < 0) {
                selectionManager->setCurrentItem(0);
            } else if (current >= index) {
                selectionManager->setCurrentItem(current + count);
            }
        }
    }

    if (hasMultipleRanges) {
        endTransaction();
    }
}

void KItemListView::slotItemsRemoved(const KItemRangeList& itemRanges)
{
    markVisibleRolesSizesAsDirty();

    const bool hasMultipleRanges = (itemRanges.count() > 1);
    if (hasMultipleRanges) {
        beginTransaction();
    }

    for (int i = itemRanges.count() - 1; i >= 0; --i) {
        const KItemRange& range = itemRanges.at(i);
        const int index = range.index;
        const int count = range.count;
        if (index < 0 || count <= 0) {
            kWarning() << "Invalid item range (index:" << index << ", count:" << count << ")";
            continue;
        }

        m_sizeHintResolver->itemsRemoved(index, count);

        const int firstRemovedIndex = index;
        const int lastRemovedIndex = index + count - 1;
        const int lastIndex = m_model->count() + count - 1;

        // Remove all KItemListWidget instances that got deleted
        for (int i = firstRemovedIndex; i <= lastRemovedIndex; ++i) {
            KItemListWidget* widget = m_visibleItems.value(i);
            if (!widget) {
                continue;
            }

            m_animation->stop(widget);
            // Stopping the animation might lead to recycling the widget if
            // it is invisible (see slotAnimationFinished()).
            // Check again whether it is still visible:
            if (!m_visibleItems.contains(i)) {
                continue;
            }

            if (m_model->count() == 0) {
                // For performance reasons no animation is done when all items have
                // been removed.
                recycleWidget(widget);
            } else {
                // Animate the removing of the items. Special case: When removing an item there
                // is no valid model index available anymore. For the
                // remove-animation the item gets removed from m_visibleItems but the widget
                // will stay alive until the animation has been finished and will
                // be recycled (deleted) in KItemListView::slotAnimationFinished().
                m_visibleItems.remove(i);
                widget->setIndex(-1);
                m_animation->start(widget, KItemListViewAnimation::DeleteAnimation);
            }
        }

        // Update the indexes of all KItemListWidget instances that are located
        // after the deleted items
        for (int i = lastRemovedIndex + 1; i <= lastIndex; ++i) {
            KItemListWidget* widget = m_visibleItems.value(i);
            if (widget) {
                const int newIndex = i - count;
                setWidgetIndex(widget, newIndex);
            }
        }

        m_layouter->markAsDirty();
        if (!hasMultipleRanges) {
            doLayout(Animation, index, -count);
            update();
        }

        /*KItemListSelectionManager* selectionManager = m_controller->selectionManager();
        const int current = selectionManager->currentItem();
        if (count() <= 0) {
            selectionManager->setCurrentItem(-1);
        } else if (current >= index) {
            selectionManager->setCurrentItem(current + count);
        }*/
    }

    if (hasMultipleRanges) {
        endTransaction();
    }
}

void KItemListView::slotItemsChanged(const KItemRangeList& itemRanges,
                                     const QSet<QByteArray>& roles)
{
    foreach (const KItemRange& itemRange, itemRanges) {
        const int index = itemRange.index;
        const int count = itemRange.count;

        m_sizeHintResolver->itemsChanged(index, count, roles);

        const int lastIndex = index + count - 1;
        for (int i = index; i <= lastIndex; ++i) {
            KItemListWidget* widget = m_visibleItems.value(i);
            if (widget) {
                widget->setData(m_model->data(i), roles);
            }
        }
    }
}

void KItemListView::currentChanged(int current, int previous)
{
    Q_UNUSED(previous);

    QHashIterator<int, KItemListWidget*> it(m_visibleItems);
    while (it.hasNext()) {
        it.next();

        KItemListWidget* widget = it.value();
        KItemListStyleOption styleOption = widget->styleOption();
        if (it.key() == current) {
            styleOption.state |= QStyle::State_HasFocus;
            widget->setStyleOption(styleOption);
        }
        else if (styleOption.state & QStyle::State_HasFocus) {
            styleOption.state &= ~QStyle::State_HasFocus;
            widget->setStyleOption(styleOption);
        }
    }
}

void KItemListView::slotAnimationFinished(QGraphicsWidget* widget,
                                          KItemListViewAnimation::AnimationType type)
{
    KItemListWidget* itemListWidget = qobject_cast<KItemListWidget*>(widget);
    Q_ASSERT(itemListWidget);

    switch (type) {
    case KItemListViewAnimation::DeleteAnimation: {
        // As we recycle the widget in this case it is important to assure that no
        // other animation has been started. This is a convention in KItemListView and
        // not a requirement defined by KItemListViewAnimation.
        Q_ASSERT(!m_animation->isStarted(itemListWidget));

        // All KItemListWidgets that are animated by the DeleteAnimation are not maintained
        // by m_visibleWidgets and must be deleted manually after the animation has
        // been finished.
        KItemListGroupHeader* header = m_visibleGroups.value(itemListWidget);
        if (header) {
            m_groupHeaderCreator->recycle(header);
            m_visibleGroups.remove(itemListWidget);
        }
        m_widgetCreator->recycle(itemListWidget);
        break;
    }

    case KItemListViewAnimation::CreateAnimation:
    case KItemListViewAnimation::MovingAnimation:
    case KItemListViewAnimation::ResizeAnimation: {
        const int index = itemListWidget->index();
        const bool invisible = (index < m_layouter->firstVisibleIndex()) ||
                               (index > m_layouter->lastVisibleIndex());
        if (invisible && !m_animation->isStarted(itemListWidget)) {
            recycleWidget(itemListWidget);
        }
        break;
    }

    default: break;
    }
}

void KItemListView::slotLayoutTimerFinished()
{
    m_layouter->setSize(geometry().size());
    doLayout(Animation, 0, 0);
}

void KItemListView::setController(KItemListController* controller)
{
    if (m_controller != controller) {
        KItemListController* previous = m_controller;
        m_controller = controller;
        onControllerChanged(controller, previous);
    }
}

void KItemListView::setModel(KItemModelBase* model)
{
    if (m_model == model) {
        return;
    }

    KItemModelBase* previous = m_model;

    if (m_model) {
        disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)),
                   this,    SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>)));
        disconnect(m_model, SIGNAL(itemsInserted(KItemRangeList)),
                   this,    SLOT(slotItemsInserted(KItemRangeList)));
        disconnect(m_model, SIGNAL(itemsRemoved(KItemRangeList)),
                   this,    SLOT(slotItemsRemoved(KItemRangeList)));
    }

    m_model = model;
    m_layouter->setModel(model);
    m_grouped = !model->groupRole().isEmpty();

    if (m_model) {
        connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)),
                this,    SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>)));
        connect(m_model, SIGNAL(itemsInserted(KItemRangeList)),
                this,    SLOT(slotItemsInserted(KItemRangeList)));
        connect(m_model, SIGNAL(itemsRemoved(KItemRangeList)),
                this,    SLOT(slotItemsRemoved(KItemRangeList)));
    }

    onModelChanged(model, previous);
}

void KItemListView::updateLayout()
{
    doLayout(Animation, 0, 0);
    update();
}

void KItemListView::doLayout(LayoutAnimationHint hint, int changedIndex, int changedCount)
{
    if (m_layoutTimer->isActive()) {
        kDebug() << "Stopping layout timer, synchronous layout requested";
        m_layoutTimer->stop();
    }

    if (m_model->count() < 0 || m_activeTransactions > 0) {
        return;
    }

    applyDynamicItemSize();

    const int firstVisibleIndex = m_layouter->firstVisibleIndex();
    const int lastVisibleIndex = m_layouter->lastVisibleIndex();
    if (firstVisibleIndex < 0) {
        emitOffsetChanges();
        return;
    }

    // Do a sanity check of the offset-property: When properties of the itemlist-view have been changed
    // it might be possible that the maximum offset got changed too. Assure that the full visible range
    // is still shown if the maximum offset got decreased.
    const qreal visibleOffsetRange = (scrollOrientation() == Qt::Horizontal) ? size().width() : size().height();
    const qreal maxOffsetToShowFullRange = maximumOffset() - visibleOffsetRange;
    if (offset() > maxOffsetToShowFullRange) {
        m_layouter->setOffset(qMax(qreal(0), maxOffsetToShowFullRange));
    }

    // Determine all items that are completely invisible and might be
    // reused for items that just got (at least partly) visible.
    // Items that do e.g. an animated moving of their position are not
    // marked as invisible: This assures that a scrolling inside the view
    // can be done without breaking an animation.
    QList<int> reusableItems;
    QHashIterator<int, KItemListWidget*> it(m_visibleItems);
    while (it.hasNext()) {
        it.next();
        KItemListWidget* widget = it.value();
        const int index = widget->index();
        const bool invisible = (index < firstVisibleIndex) || (index > lastVisibleIndex);
        if (invisible && !m_animation->isStarted(widget)) {
            widget->setVisible(false);
            reusableItems.append(index);
        }
    }

    // Assure that for each visible item a KItemListWidget is available. KItemListWidget
    // instances from invisible items are reused. If no reusable items are
    // found then new KItemListWidget instances get created.
    const bool animate = (hint == Animation);
    for (int i = firstVisibleIndex; i <= lastVisibleIndex; ++i) {
        bool applyNewPos = true;
        bool wasHidden = false;

        const QRectF itemBounds = m_layouter->itemBoundingRect(i);
        const QPointF newPos = itemBounds.topLeft();
        KItemListWidget* widget = m_visibleItems.value(i);
        if (!widget) {
            wasHidden = true;
            if (!reusableItems.isEmpty()) {
                // Reuse a KItemListWidget instance from an invisible item
                const int oldIndex = reusableItems.takeLast();
                widget = m_visibleItems.value(oldIndex);
                setWidgetIndex(widget, i);
            } else {
                // No reusable KItemListWidget instance is available, create a new one
                widget = createWidget(i);
            }
            widget->resize(itemBounds.size());

            if (animate && changedCount < 0) {
                // Items have been deleted, move the created item to the
                // imaginary old position.
                const QRectF itemBoundingRect = m_layouter->itemBoundingRect(i - changedCount);
                if (itemBoundingRect.isEmpty()) {
                    const QPointF invisibleOldPos = (scrollOrientation() == Qt::Vertical)
                                                    ? QPointF(0, size().height()) : QPointF(size().width(), 0);
                    widget->setPos(invisibleOldPos);
                } else {
                    widget->setPos(itemBoundingRect.topLeft());
                }
                applyNewPos = false;
            }
        } else if (m_animation->isStarted(widget, KItemListViewAnimation::MovingAnimation)) {
            applyNewPos = false;
        }

        if (animate) {
            const bool itemsRemoved = (changedCount < 0);
            const bool itemsInserted = (changedCount > 0);

            if (itemsRemoved && (i >= changedIndex + changedCount + 1)) {
                // The item is located after the removed items. Animate the moving of the position.
                m_animation->start(widget, KItemListViewAnimation::MovingAnimation, newPos);
                applyNewPos = false;
            } else if (itemsInserted && i >= changedIndex) {
                // The item is located after the first inserted item
                if (i <= changedIndex + changedCount - 1) {
                    // The item is an inserted item. Animate the appearing of the item.
                    // For performance reasons no animation is done when changedCount is equal
                    // to all available items.
                    if (changedCount < m_model->count()) {
                        m_animation->start(widget, KItemListViewAnimation::CreateAnimation);
                    }
                } else if (!m_animation->isStarted(widget, KItemListViewAnimation::CreateAnimation)) {
                    // The item was already there before, so animate the moving of the position.
                    // No moving animation is done if the item is animated by a create animation: This
                    // prevents a "move animation mess" when inserting several ranges in parallel.
                    m_animation->start(widget, KItemListViewAnimation::MovingAnimation, newPos);
                    applyNewPos = false;
                }
            } else if (!itemsRemoved && !itemsInserted && !wasHidden) {
                // The size of the view might have been changed. Animate the moving of the position.
                m_animation->start(widget, KItemListViewAnimation::MovingAnimation, newPos);
                applyNewPos = false;
            }
        }

        if (applyNewPos) {
            widget->setPos(newPos);
        }

        Q_ASSERT(widget->index() == i);
        widget->setVisible(true);

        if (widget->size() != itemBounds.size()) {
            m_animation->start(widget, KItemListViewAnimation::ResizeAnimation, itemBounds.size());
        }
    }

    // Delete invisible KItemListWidget instances that have not been reused
    foreach (int index, reusableItems) {
        recycleWidget(m_visibleItems.value(index));
    }

    emitOffsetChanges();
}

void KItemListView::emitOffsetChanges()
{
    const int newOffset = m_layouter->offset();
    if (m_oldOffset != newOffset) {
        emit offsetChanged(newOffset, m_oldOffset);
        m_oldOffset = newOffset;
    }

    const int newMaximumOffset = m_layouter->maximumOffset();
    if (m_oldMaximumOffset != newMaximumOffset) {
        emit maximumOffsetChanged(newMaximumOffset, m_oldMaximumOffset);
        m_oldMaximumOffset = newMaximumOffset;
    }
}

KItemListWidget* KItemListView::createWidget(int index)
{
    KItemListWidget* widget = m_widgetCreator->create(this);
    widget->setVisibleRoles(m_visibleRoles);
    widget->setVisibleRolesSizes(m_visibleRolesSizes);

    KItemListStyleOption option = m_styleOption;
    if (index == m_controller->selectionManager()->currentItem()) {
        option.state |= QStyle::State_HasFocus;
    }
    widget->setStyleOption(option);

    widget->setIndex(index);
    widget->setData(m_model->data(index));
    m_visibleItems.insert(index, widget);

    if (m_grouped) {
        if (m_layouter->isFirstGroupItem(index)) {
            KItemListGroupHeader* header = m_groupHeaderCreator->create(widget);
            header->setPos(0, -50);
            header->resize(50, 50);
            m_visibleGroups.insert(widget, header);
        }
    }

    initializeItemListWidget(widget);
    return widget;
}

void KItemListView::recycleWidget(KItemListWidget* widget)
{
    if (m_grouped) {
        KItemListGroupHeader* header = m_visibleGroups.value(widget);
        if (header) {
            m_groupHeaderCreator->recycle(header);
            m_visibleGroups.remove(widget);
        }
    }

    m_visibleItems.remove(widget->index());
    m_widgetCreator->recycle(widget);
}

void KItemListView::setWidgetIndex(KItemListWidget* widget, int index)
{
    if (m_grouped) {
        bool createHeader = m_layouter->isFirstGroupItem(index);
        KItemListGroupHeader* header = m_visibleGroups.value(widget);
        if (header) {
            if (createHeader) {
                createHeader = false;
            } else {
                m_groupHeaderCreator->recycle(header);
                m_visibleGroups.remove(widget);
            }
        }

        if (createHeader) {
            KItemListGroupHeader* header = m_groupHeaderCreator->create(widget);
            header->setPos(0, -50);
            header->resize(50, 50);
            m_visibleGroups.insert(widget, header);
        }
    }

    const int oldIndex = widget->index();
    m_visibleItems.remove(oldIndex);
    widget->setVisibleRoles(m_visibleRoles);
    widget->setVisibleRolesSizes(m_visibleRolesSizes);

    KItemListStyleOption option = m_styleOption;
    if (index == m_controller->selectionManager()->currentItem()) {
        option.state |= QStyle::State_HasFocus;
    }
    widget->setStyleOption(option);

    widget->setIndex(index);
    widget->setData(m_model->data(index));
    m_visibleItems.insert(index, widget);

    initializeItemListWidget(widget);
}

void KItemListView::prepareLayoutForIncreasedItemCount(const QSizeF& size, SizeType sizeType)
{
    // Calculate the first visible index and last visible index for the current size
    const int currentFirst = m_layouter->firstVisibleIndex();
    const int currentLast = m_layouter->lastVisibleIndex();

    const QSizeF currentSize = (sizeType == LayouterSize) ? m_layouter->size() : m_layouter->itemSize();

    // Calculate the first visible index and last visible index for the new size
    setLayouterSize(size, sizeType);
    const int newFirst = m_layouter->firstVisibleIndex();
    const int newLast = m_layouter->lastVisibleIndex();

    if ((currentFirst != newFirst) || (currentLast != newLast)) {
        // At least one index has been changed. Assure that widgets for all possible
        // visible items get created so that a move-animation can be started later.
        const int maxVisibleItems = m_layouter->maximumVisibleItems();
        int minFirst = qMin(newFirst, currentFirst);
        const int maxLast = qMax(newLast, currentLast);

        if (maxLast - minFirst + 1 < maxVisibleItems) {
            // Increasing the size might result in a smaller KItemListView::offset().
            // Decrease the first visible index in a way that at least the maximum
            // visible items are shown.
            minFirst = qMax(0, maxLast - maxVisibleItems + 1);
        }

        if (maxLast - minFirst > maxVisibleItems  + maxVisibleItems / 2) {
            // The creating of widgets is quite expensive. Assure that never more
            // than 50 % of the maximum visible items get created for the animations.
            return;
        }

        setLayouterSize(currentSize, sizeType);
        for (int i = minFirst; i <= maxLast; ++i) {
            if (!m_visibleItems.contains(i)) {
                KItemListWidget* widget = createWidget(i);
                const QPointF pos = m_layouter->itemBoundingRect(i).topLeft();
                widget->setPos(pos);
            }
        }
        setLayouterSize(size, sizeType);
    }
}

void KItemListView::setLayouterSize(const QSizeF& size, SizeType sizeType)
{
    switch (sizeType) {
    case LayouterSize: m_layouter->setSize(size); break;
    case ItemSize: m_layouter->setItemSize(size); break;
    default: break;
    }
}

bool KItemListView::markVisibleRolesSizesAsDirty()
{
    const bool dirty = m_itemSize.isEmpty();
    if (dirty) {
        m_visibleRolesSizes.clear();
        m_layouter->setItemSize(QSizeF());
    }
    return dirty;
}

void KItemListView::applyDynamicItemSize()
{
    if (!m_itemSize.isEmpty()) {
        return;
    }

    if (m_visibleRolesSizes.isEmpty()) {
        m_visibleRolesSizes = visibleRoleSizes();
        foreach (KItemListWidget* widget, visibleItemListWidgets()) {
            widget->setVisibleRolesSizes(m_visibleRolesSizes);
        }
    }

    if (m_layouter->itemSize().isEmpty()) {
        qreal requiredWidth = 0;
        qreal requiredHeight = 0;

        QHashIterator<QByteArray, QSizeF> it(m_visibleRolesSizes);
        while (it.hasNext()) {
            it.next();
            const QSizeF& visibleRoleSize = it.value();
            requiredWidth  += visibleRoleSize.width();
            requiredHeight += visibleRoleSize.height();
        }

        QSizeF dynamicItemSize = m_itemSize;
        if (dynamicItemSize.width() <= 0) {
            dynamicItemSize.setWidth(qMax(requiredWidth, size().width()));
        }
        if (dynamicItemSize.height() <= 0) {
            dynamicItemSize.setHeight(qMax(requiredHeight, size().height()));
        }

        m_layouter->setItemSize(dynamicItemSize);
    }
}

KItemListCreatorBase::~KItemListCreatorBase()
{
    qDeleteAll(m_recycleableWidgets);
    qDeleteAll(m_createdWidgets);
}

void KItemListCreatorBase::addCreatedWidget(QGraphicsWidget* widget)
{
    m_createdWidgets.insert(widget);
}

void KItemListCreatorBase::pushRecycleableWidget(QGraphicsWidget* widget)
{
    Q_ASSERT(m_createdWidgets.contains(widget));
    m_createdWidgets.remove(widget);

    if (m_recycleableWidgets.count() < 100) {
        m_recycleableWidgets.append(widget);
        widget->setVisible(false);
    } else {
        delete widget;
    }
}

QGraphicsWidget* KItemListCreatorBase::popRecycleableWidget()
{
    if (m_recycleableWidgets.isEmpty()) {
        return 0;
    }

    QGraphicsWidget* widget = m_recycleableWidgets.takeLast();
    m_createdWidgets.insert(widget);
    return widget;
}

KItemListWidgetCreatorBase::~KItemListWidgetCreatorBase()
{
}

void KItemListWidgetCreatorBase::recycle(KItemListWidget* widget)
{
    widget->setOpacity(1.0);
    pushRecycleableWidget(widget);
}

KItemListGroupHeaderCreatorBase::~KItemListGroupHeaderCreatorBase()
{
}

void KItemListGroupHeaderCreatorBase::recycle(KItemListGroupHeader* header)
{
    header->setOpacity(1.0);
    pushRecycleableWidget(header);
}

#include "kitemlistview.moc"
