/****************************************************************************
**
** Copyright (C) 2012 Róbert Márki
**
** This file is part of Web Feeds.
**
** Web Feeds 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 3 of the License, or
** (at your option) any later version.
**
** Web Feeds 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 Web Feeds.  If not, see <http://www.gnu.org/licenses/>.
****************************************************************************/

#include "googlereaderservice.h"
#include "applicationsettings.h"
#include <QtDeclarative>
#include "googlereaderenum.h"
#include "storyitem.h"

/*!
  \namespace GoogleReader
  \brief GoogleReader plugin impelemntation.
*/
namespace GoogleReader
{
/*!
  \class GoogleReaderService
  \brief The GoogleReaderService class is responsible for maintaining
  the GoogleReader branch of the item tree.
*/
/*!
  \property GoogleReaderService::busy
  True if the GoogleReader branch is currently being updated
  and no interaction with the service is allowed,
  otherwise false.
*/
/*!
  \property GoogleReaderService::itemFilter
  The current filtering type of the service based on the
  items' unread status. It's a GoogleReaderEnum::ItemFilterType value
  cast to an int.
*/
/*!
  \property GoogleReaderService::itemSortType
  The current item sorting method of the service.
  It's a GoogleReaderEnum::ItemSortType value
  cast to an int.
*/
/*!
  \property GoogleReaderService::availableTags
  The list of the tags that currently exist.
*/
/*!
  \property GoogleReaderService::unreadCountLimit
  The maximal value of a subscription's or tag's unread count.
  Unread counts above this value are not updated as often as smaller values.
*/
const char* GoogleReaderService::s_serviceName                      = "Google Reader";
const char* GoogleReaderService::s_serviceId                        = "googlereader";
const int GoogleReaderService::s_unreadCountUpdateIntervalMinMs     = 1 * 60;
const int GoogleReaderService::s_unreadCountUpdateIntervalMaxMs     = 2 * 60;
const int GoogleReaderService::s_preferredStoryCount                = 20;

const char* GoogleReaderService::s_serviceImageUrl                  = ":/plugins/google_reader/images/googlereader.png";
const char* GoogleReaderService::s_starGroupImageUrl                = ":/shared/images/star_icon.png";
const char* GoogleReaderService::s_generalFeedImageUrl              = ":/shared/images/feed_icon.png";
const char* GoogleReaderService::s_folderImageUrl                   = ":/shared/images/folder_icon.png";
const char* GoogleReaderService::s_tagImageUrl                      = ":/shared/images/tag_icon.png";
const char* GoogleReaderService::s_userNameKey                      = "userName";
const char* GoogleReaderService::s_passwordKey                      = "password";
const char* GoogleReaderService::s_itemFilterTypeKey                = "filterType";
const char* GoogleReaderService::s_itemSortTypeKey                  = "sortType";
const char* GoogleReaderService::s_servicePageUrl                   = "qrc:/plugins/google_reader/qml/pages/GoogleReaderPage.qml";
const char* GoogleReaderService::s_authenticationSheetUrl           = "qrc:/plugins/google_reader/qml/sheets/AuthenticationSheet.qml";

const char* GoogleReaderService::s_busyPropertyName                 = "busy";
const char* GoogleReaderService::s_starredBusyPropertyName          = "starredBusy";
const char* GoogleReaderService::s_feedReadBusyPropertyName         = "feedReadBusy";
const char* GoogleReaderService::s_editingBusyPropertyName          = "editingBusy";

const char* GoogleReaderService::s_serviceIconId                    = "service-icon";
const char* GoogleReaderService::s_folderIconId                     = "folder-icon";
const char* GoogleReaderService::s_feedIconId                       = "feed-icon";
const char* GoogleReaderService::s_starredTagIconId                 = "starred-tag-icon";
const char* GoogleReaderService::s_tagIconId                        = "tag-icon";

const char* GoogleReaderService::s_sortSelectionMenuItemName        = "GRSortSelectionMenuItem";
const char* GoogleReaderService::s_markStoryAsUnreadMenuItemName    = "GRMarkStoryAsUnreadMenuItem";
const char* GoogleReaderService::s_editStoryTagsMenuItemName        = "GREditStoryTagsMenuItem";
const char* GoogleReaderService::s_shareStoryMenuItemName           = "GRShareStoryMenuItem";
const char* GoogleReaderService::s_starStoryMenuItemName            = "GRStarStoryMenuItem";
const char* GoogleReaderService::s_unstarStoryMenuItemName          = "GRUnstarStoryMenuItem";
const char* GoogleReaderService::s_renameMenuItemName               = "GRRenameMenuItem";
const char* GoogleReaderService::s_deleteMenuItemName               = "GRDeleteMenuItem";
const char* GoogleReaderService::s_markAllAsReadMenuItemName        = "GRMarkAllItemsAsReadMenuItem";
const char* GoogleReaderService::s_helpMenuItemName                 = "GRGoogleReaderHelpMenuItem";

/*!
  Constructs a GoogleReaderService object with the given \a parent.
*/
GoogleReaderService::GoogleReaderService(QObject *parent) :
    AbstractFeedAggreagatorService(parent),
    m_client(new GoogleReaderClient(this)),
    m_busy(false),
    m_unreadCountsOutOfDate(false),
    m_unreadCountLimit(0)
{
    //register the custorm item types
    qmlRegisterType<GoogleReader::GoogleReaderService>("GoogleReader", 1, 0, "GoogleReaderService");
    qmlRegisterType<GoogleReader::GoogleReaderItem>("GoogleReader", 1, 0, "GoogleReaderItem");
    qmlRegisterType<GoogleReader::RootItem>("GoogleReader", 1, 0, "RootItem");
    qmlRegisterType<GoogleReader::FeedItem>("GoogleReader", 1, 0, "FeedItem");
    qmlRegisterType<GoogleReader::TagItem>("GoogleReader", 1, 0, "TagItem");
    qmlRegisterType<GoogleReader::ParentItem>("GoogleReader", 1, 0, "ParentItem");
    qmlRegisterType<GoogleReader::FolderItem>("GoogleReader", 1, 0, "FolderItem");
    qmlRegisterType<GoogleReader::EverythingTagItem>("GoogleReader", 1, 0, "EverythingTagItem");
    qmlRegisterType<GoogleReader::StarredTagItem>("GoogleReader", 1, 0, "StarredTagItem");
    qmlRegisterType<GoogleReader::StoryItem>("GoogleReader", 1, 0, "StoryItem");
    qmlRegisterUncreatableType<GoogleReader::GoogleReaderEnum>("GoogleReader", 1, 0, "GoogleReaderEnum", "GoogleReaderEnum can't be created.");

    //initialize the unread count update timer
#ifdef QTM_USE_NAMESPACE
    m_unreadCountUpdateTimer.setMinimumInterval(GoogleReaderService::s_unreadCountUpdateIntervalMinMs);
    m_unreadCountUpdateTimer.setMaximumInterval(GoogleReaderService::s_unreadCountUpdateIntervalMaxMs);
#else
    m_unreadCountUpdateTimer.setInterval(((GoogleReaderService::s_unreadCountUpdateIntervalMaxMs
                                          + GoogleReaderService::s_unreadCountUpdateIntervalMinMs) / 2) * 1000);
#endif

    connect(&m_unreadCountUpdateTimer, SIGNAL(timeout()),
            this, SLOT(updateUnreadCounts()));
    connect(m_client, SIGNAL(requestCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)),
            this, SLOT(onRequestCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)));
    connect(m_client, SIGNAL(requestFailed(WebServiceReplyPtr)),
            this, SLOT(onRequestFailed(WebServiceReplyPtr)));
    connect(m_client, SIGNAL(requestAborted(WebServiceReplyPtr)),
            this, SLOT(onRequestAborted(WebServiceReplyPtr)));
    connect(m_client, SIGNAL(authenticationRequired(QAuthenticator*)),
            this, SLOT(onAuthenticationRequired(QAuthenticator*)));
}

/*!
  Destroy the GoogleReaderService object.
*/
GoogleReaderService::~GoogleReaderService()
{
    if(m_rootItem != 0)
        disable();
}

void GoogleReaderService::enable(PageItemPtr parentItem)
{
    m_rootItem = RootItemPtr(new RootItem(parentItem, this));
    m_rootItem->setTitle(GoogleReaderService::s_serviceName);
    m_rootItem->setItemId(QString());
    m_rootItem->setIconSource(imageUrl(GoogleReaderService::s_serviceIconId));
    parentItem->appendChild(m_rootItem);
    registerItem(m_rootItem);
}

void GoogleReaderService::disable()
{
    //stop refreshing unread counts
    m_unreadCountUpdateTimer.stop();
    //abort all running requests
    abortAllRequests();
    //clear all members
    m_feedItems.clear();
    m_tagItems.clear();
    m_folderItems.clear();
    emit availableTagsChanged();
    m_everythingTagItem.clear();
    m_starredTagItem.clear();
    m_busy = false;
    m_unreadCountsOutOfDate = false;
    m_unreadCountLimit = 0;
    //remove the root item from it's parent
    PageItemPtr parentItem = m_rootItem->parentItem().toStrongRef();
    if(parentItem && m_rootItem)
        parentItem->removeChild(m_rootItem);
    m_rootItem.clear();
}

QString GoogleReaderService::serviceId() const
{
    return GoogleReaderService::s_serviceId;
}

QStringList GoogleReaderService::menuItems(PageItemPtr item) const
{
    QStringList ret;
    GoogleReaderItemPtr googleReaderItem = item.staticCast<GoogleReaderItem>();    
    if(googleReaderItem->itemType() == GoogleReaderItem::ItRootItem)
    {
        ret << GoogleReaderService::s_helpMenuItemName;
    }
    else if(googleReaderItem->itemType() == GoogleReaderItem::ItFolderItem)
    {
        ret << GoogleReaderService::s_renameMenuItemName;
        ret << GoogleReaderService::s_deleteMenuItemName;
    }
    else if(googleReaderItem->itemType() == GoogleReaderItem::ItTagItem)
    {
        ret << GoogleReaderService::s_sortSelectionMenuItemName;
        ret << GoogleReaderService::s_renameMenuItemName;
        ret << GoogleReaderService::s_deleteMenuItemName;
    }
    else if((googleReaderItem->itemType() == GoogleReaderItem::ItEverythingTagItem)
            || (googleReaderItem->itemType() == GoogleReaderItem::ItStarredTagItem))
    {
        ret << GoogleReaderService::s_sortSelectionMenuItemName;
    }
    else if(googleReaderItem->itemType() == GoogleReaderItem::ItFeedItem)
    {
        ret << GoogleReaderService::s_sortSelectionMenuItemName;        
        ret << GoogleReaderService::s_renameMenuItemName;
        ret << GoogleReaderService::s_editStoryTagsMenuItemName;
        ret << GoogleReaderService::s_deleteMenuItemName;
    }
    else if(googleReaderItem->itemType() == GoogleReaderItem::ItStoryItem)
    {
        StoryItemPtr storyItem = item.staticCast<StoryItem>();
        if(storyItem->isRead() && !storyItem->isReadStateLocked())
            ret << GoogleReaderService::s_markStoryAsUnreadMenuItemName;
        ret << GoogleReaderService::s_editStoryTagsMenuItemName;
    }
    return ret;
}

QStringList GoogleReaderService::contextMenuItems(PageItemPtr item) const
{
    QStringList ret;
    GoogleReaderItemPtr googleReaderItem = item.staticCast<GoogleReaderItem>();
    ParentItemPtr parentItem = item.staticCast<ParentItem>();
    if(googleReaderItem->itemType() == GoogleReaderItem::ItFolderItem)
    {
        ret << GoogleReaderService::s_markAllAsReadMenuItemName;
        ret << GoogleReaderService::s_renameMenuItemName;
        ret << GoogleReaderService::s_deleteMenuItemName;
    }
    else if(googleReaderItem->itemType() == GoogleReaderItem::ItTagItem)
    {
        ret << GoogleReaderService::s_markAllAsReadMenuItemName;
        ret << GoogleReaderService::s_renameMenuItemName;
        ret << GoogleReaderService::s_deleteMenuItemName;
    }
    else if(googleReaderItem->itemType() == GoogleReaderItem::ItEverythingTagItem)
    {
        if(parentItem->unreadCount() > 0)
            ret << GoogleReaderService::s_markAllAsReadMenuItemName;
    }
    else if(googleReaderItem->itemType() == GoogleReaderItem::ItStarredTagItem)
    {
        ret << GoogleReaderService::s_markAllAsReadMenuItemName;
    }
    else if(googleReaderItem->itemType() == GoogleReaderItem::ItFeedItem)
    {        
        if(parentItem->unreadCount() > 0)
            ret << GoogleReaderService::s_markAllAsReadMenuItemName;
        ret << GoogleReaderService::s_renameMenuItemName;
        ret << GoogleReaderService::s_editStoryTagsMenuItemName;
        ret << GoogleReaderService::s_deleteMenuItemName;
    }
    else if(googleReaderItem->itemType() == GoogleReaderItem::ItStoryItem)
    {
        StoryItemPtr storyItem = item.staticCast<StoryItem>();
        if(storyItem->isRead() && !storyItem->isReadStateLocked())
            ret << GoogleReaderService::s_markStoryAsUnreadMenuItemName;
        if(storyItem->isStarred())
            ret << GoogleReaderService::s_unstarStoryMenuItemName;
        else
            ret << GoogleReaderService::s_starStoryMenuItemName;        
        ret << GoogleReaderService::s_shareStoryMenuItemName;
        ret << GoogleReaderService::s_editStoryTagsMenuItemName;
    }
    return ret;
}

bool GoogleReaderService::isItemAcceptable(PageItemPtr item)
{
    Q_UNUSED(item);
    return true;
}

QSet<QString> GoogleReaderService::customItemTypeProperties() const
{
    QSet<QString> ret;
    ret += AbstractFeedAggreagatorService::itemTypeProperties<GoogleReaderItem>();
    ret += AbstractFeedAggreagatorService::itemTypeProperties<RootItem>();
    ret += AbstractFeedAggreagatorService::itemTypeProperties<FeedItem>();
    ret += AbstractFeedAggreagatorService::itemTypeProperties<TagItem>();
    ret += AbstractFeedAggreagatorService::itemTypeProperties<FolderItem>();
    ret += AbstractFeedAggreagatorService::itemTypeProperties<ParentItem>();
    ret += AbstractFeedAggreagatorService::itemTypeProperties<EverythingTagItem>();
    ret += AbstractFeedAggreagatorService::itemTypeProperties<StarredTagItem>();
    ret += AbstractFeedAggreagatorService::itemTypeProperties<StoryItem>();
    return ret;
}

bool GoogleReaderService::isBusy() const
{
    return m_busy;
}

void GoogleReaderService::setBusy(bool busy)
{
    m_busy = busy;
    emit busyChanged();
}

int GoogleReaderService::itemFilter() const
{
    return pluginSettings()->value(GoogleReaderService::s_itemFilterTypeKey,
                                   GoogleReaderEnum::IftNewItems).toInt();
}

void GoogleReaderService::setItemFilter(int filterType)
{
    pluginSettings()->setValue(GoogleReaderService::s_itemFilterTypeKey,
                               filterType);
    emit itemFilterChanged();
}

int GoogleReaderService::itemSortType() const
{
    return pluginSettings()->value(GoogleReaderService::s_itemSortTypeKey,
                                   GoogleReaderEnum::IstSortByNewest).toInt();
}

void GoogleReaderService::setItemSortType(int sortType)
{
    pluginSettings()->setValue(GoogleReaderService::s_itemSortTypeKey,
                               sortType);
    emit itemSortTypeChanged();
}

QStringList GoogleReaderService::availableTags() const
{
    QStringList ret = m_tagItems.uniqueKeys();
    qSort(ret);
    return ret;
}

int GoogleReaderService::unreadCountLimit() const
{
    return m_unreadCountLimit;
}

QString GoogleReaderService::servicePageUrl() const
{
    return GoogleReaderService::s_servicePageUrl;
}

QString GoogleReaderService::authenticationSheet() const
{
    return GoogleReaderService::s_authenticationSheetUrl;
}

QString GoogleReaderService::serviceName() const
{
    return GoogleReaderService::s_serviceName;
}

QString GoogleReaderService::serviceImageUrl() const
{    
    return imageUrl(GoogleReaderService::s_serviceIconId);
}

bool GoogleReaderService::isAuthenticationDataSet() const
{
    return (!userName().isEmpty() && !password().isEmpty());
}

void GoogleReaderService::clearAuthenticationData()
{
    clearPluginSettings();
    m_client->logout();
    emit authenticationDataChanged(this);
}

PageItem *GoogleReaderService::rootItem() const
{
    return m_rootItem.data();
}

/*!
  Starts the verification of \a userName and \a password.
*/
void GoogleReaderService::verifyAuthenticationData(const QString &userName, const QString &password)
{
    WebServiceReplyPtr reply = m_client->verifyAccount(userName, password);
    connect(reply.data(), SIGNAL(completed(WebServiceReplyPtr, WebServiceReply::CompletedState)),
            this, SLOT(onAccountVerificationCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)));
    addRequest(PageItemPtr(), reply);
}

/*!
  Aborts the authentication data verification.
*/
void GoogleReaderService::abortVerification()
{
    abortRequest(GoogleReaderClient::TpVerifyAccount);
}

/*!
  Sets the authentication data for the current user.
*/
void GoogleReaderService::setAuthenticationData(const QString &userName, const QString &password)
{
    setUserName(userName);
    setPassword(password);
    emit authenticationDataChanged(this);
}

/*!
  Removes all child items of the \a feed and starts to load
  it's child items from the beggining.
*/
void GoogleReaderService::refreshFeed(GoogleReader::FeedItem *feed)
{
    if(feed)
    {
        FeedItemPtr feedItem = FeedItemWeakRef(feed).toStrongRef();
        refreshFeed(feedItem);
    }
}

/*!
  Starts the opration which marks the \a story as
  starred.
*/
void GoogleReaderService::starStory(StoryItem *story)
{
    if(story)
    {
        StoryItemPtr storyItem = StoryItemWeakRef(story).toStrongRef();
        if(isRequestAllowed(GoogleReaderClient::TpStarStory, storyItem))
        {
            EditReplyPtr reply = m_client->starStory(storyItem->feedId(),
                                                          storyItem->itemId());
            connect(reply.data(), SIGNAL(finished(WebServiceReplyPtr)),
                    this, SLOT(onStarStoryFinished(WebServiceReplyPtr)));
            assingBusyPropertyToRequest(reply, storyItem, GoogleReaderService::s_starredBusyPropertyName);
            addRequest(storyItem, reply);
        }
    }
}

/*!
  Starts the operation which removes the \a story from
  the list of starred or saved stories.
*/
void GoogleReaderService::unstarStory(StoryItem *story)
{
    if(story)
    {
        StoryItemPtr storyItem = StoryItemWeakRef(story).toStrongRef();
        if(isRequestAllowed(GoogleReaderClient::TpUnstarStory, storyItem))
        {
            EditReplyPtr reply = m_client->unstarStory(storyItem->feedId(),
                                                            storyItem->itemId());
            connect(reply.data(), SIGNAL(finished(WebServiceReplyPtr)),
                    this, SLOT(onUnstarStoryFinished(WebServiceReplyPtr)));
            assingBusyPropertyToRequest(reply, storyItem, GoogleReaderService::s_starredBusyPropertyName);
            addRequest(storyItem, reply);
        }
    }
}

/*!
  Starts the operation which marks the \a storyItem as read.
*/
void GoogleReaderService::markStoryRead(StoryItemPtr storyItem)
{
    if(storyItem)
    {
        if(isRequestAllowed(GoogleReaderClient::TpMarkStoryUnread, storyItem))
        {
            EditReplyPtr reply = m_client->markStoryRead(storyItem->feedId(),
                                                              storyItem->itemId(),
                                                              storyItem->isKeptUnread());
            connect(reply.data(), SIGNAL(finished(WebServiceReplyPtr)),
                    this, SLOT(onMarkStoryReadFinished(WebServiceReplyPtr)));
            addRequest(storyItem, reply);
        }
    }
}

/*!
  Starts the operation which marks the \a story as unread.
*/
void GoogleReaderService::markStoryUnread(StoryItem *story)
{
    if(story)
    {
        StoryItemPtr storyItem = StoryItemWeakRef(story).toStrongRef();
        if(isRequestAllowed(GoogleReaderClient::TpMarkStoryUnread, storyItem))
        {
            EditReplyPtr reply = m_client->markStoryUnread(storyItem->feedId(),
                                                                storyItem->itemId());
            connect(reply.data(), SIGNAL(finished(WebServiceReplyPtr)),
                    this, SLOT(onMarkStoryUnreadFinished(WebServiceReplyPtr)));
            addRequest(storyItem, reply);
        }
    }
}

/*!
  Returns the title of the feed which is associated with the
  \a feedId.
*/
QString GoogleReaderService::feedTitle(const QString &feedId) const
{
    QString ret;
    if(m_feedItems.contains(feedId))
        ret = m_feedItems.value(feedId)->title();
    return ret;
}

/*!
  Starts the operation which marks all the child items or stories of the
  \a item as read.
*/
void GoogleReaderService::markFeedAsRead(GoogleReader::ParentItem* item)
{
    if(item)
    {
        ParentItemPtr parentItem = ParentItemWeakRef(item).toStrongRef();
        if(isRequestAllowed(GoogleReaderClient::TpMarkFeedAsRead, parentItem))
        {
            EditReplyPtr reply = m_client->markFeedAsRead(parentItem->itemId());
            connect(reply.data(), SIGNAL(finished(WebServiceReplyPtr)),
                    this, SLOT(onMarkFeedAsReadFinished(WebServiceReplyPtr)));
            foreach(ParentItemPtr itemPtr, m_feedItems.values(parentItem->itemId()))
                assingBusyPropertyToRequest(reply, itemPtr, GoogleReaderService::s_feedReadBusyPropertyName);
            foreach(ParentItemPtr itemPtr, m_folderItems.values(parentItem->itemId()))
                assingBusyPropertyToRequest(reply, itemPtr, GoogleReaderService::s_feedReadBusyPropertyName);
            foreach(ParentItemPtr itemPtr, m_tagItems.values(parentItem->itemId()))
                assingBusyPropertyToRequest(reply, itemPtr, GoogleReaderService::s_feedReadBusyPropertyName);
            addRequest(parentItem, reply);
        }
    }
}

/*!
  Starts the operation which marks every story in every feed as read.
*/
void GoogleReaderService::markEverythingAsRead()
{
    if(m_rootItem && isRequestAllowed(GoogleReaderClient::TpMarkFeedAsRead, m_rootItem))
    {
        EditReplyPtr reply = m_client->markEverythingAsRead();
        connect(reply.data(), SIGNAL(finished(WebServiceReplyPtr)),
                this, SLOT(onMarkEverythingAsReadFinished(WebServiceReplyPtr)));
        assingBusyPropertyToRequest(reply, m_rootItem, GoogleReaderService::s_feedReadBusyPropertyName);
        assingBusyPropertyToRequest(reply, m_everythingTagItem, GoogleReaderService::s_feedReadBusyPropertyName);
        addRequest(m_rootItem, reply);
    }
}

/*!
  Starts the operation which sets the given \a tags for the \a item.
*/
void GoogleReaderService::setItemTags(GoogleReaderItem *item, const QStringList &tags)
{
    if(item)
    {
        GoogleReaderItemPtr googleReaderItem = GoogleReaderItemWeakRef(item).toStrongRef();        
        QStringList tagList = tags;
        tagList.removeDuplicates();
        tagList = tagList.filter(QRegExp("\\S+"));

        if((googleReaderItem->itemType() == GoogleReaderItem::ItStoryItem)
           && isRequestAllowed(GoogleReaderClient::TpEditStoryTags, googleReaderItem))
        {
            StoryItemPtr storyItem = googleReaderItem.staticCast<StoryItem>();
            if(tagList.toSet() != storyItem->tags().toSet())
            {
                EditReplyPtr reply = m_client->setStoryTags(storyItem->feedId(), storyItem->itemId(),
                                                                 tagList, storyItem->tags());
                connect(reply.data(), SIGNAL(completed(WebServiceReplyPtr,WebServiceReply::CompletedState)),
                        this, SLOT(onSettingStoryTagsCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)));
                addRequest(storyItem, reply);
            }
            else
            {
                emit settingItemTagsFinished(GoogleReaderEnum::ErrUndefinedError);
            }
        }
        else if((googleReaderItem->itemType() == GoogleReaderItem::ItFeedItem)
                && isRequestAllowed(GoogleReaderClient::TpEditFeedTags, googleReaderItem))
        {
            FeedItemPtr feedItem = googleReaderItem.staticCast<FeedItem>();
            if(tagList.toSet() != feedItem->folders().toSet())
            {
                EditReplyPtr reply = m_client->setFeedTags(feedItem->itemId(), feedItem->title(),
                                                                tagList, feedItem->folders());
                connect(reply.data(), SIGNAL(completed(WebServiceReplyPtr,WebServiceReply::CompletedState)),
                        this, SLOT(onSettingFeedTagsCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)));
                addRequest(feedItem, reply);
            }
            else
            {
                emit settingItemTagsFinished(GoogleReaderEnum::ErrUndefinedError);
            }
        }
    }
}

/*!
  Aborts the operation which sets the tags of the \a item.
*/
void GoogleReaderService::abortSettingItemTags(GoogleReaderItem *item)
{
    if(item)
    {
        GoogleReaderItemPtr googleReaderItem = GoogleReaderItemWeakRef(item).toStrongRef();
        if(googleReaderItem->itemType() == GoogleReaderItem::ItStoryItem)
        {
            abortRequest(GoogleReaderClient::TpEditStoryTags, WebServiceReply::RlUndefined,
                         googleReaderItem);
        }
        else if(googleReaderItem->itemType() == GoogleReaderItem::ItFeedItem)
        {
            abortRequest(GoogleReaderClient::TpEditFeedTags, WebServiceReply::RlUndefined,
                         googleReaderItem);
        }
    }
}

/*!
  Starts the operation which renames the \a item to \a newName.
*/
void GoogleReaderService::renameItem(GoogleReaderItem *item, const QString &newName)
{
    if(item)
    {
        GoogleReaderItemPtr googleReaderItem = GoogleReaderItemWeakRef(item).toStrongRef();
        if(googleReaderItem->title() != newName)
        {
            if(((googleReaderItem->itemType() == GoogleReaderItem::ItTagItem)
                || (googleReaderItem->itemType() == GoogleReaderItem::ItFolderItem))
                    && isRequestAllowed(GoogleReaderClient::TpRenameTag, googleReaderItem))
            {
                TagItemPtr tagItem = googleReaderItem.staticCast<TagItem>();
                EditReplyPtr reply = m_client->renameTag(tagItem->itemId(), newName);
                connect(reply.data(), SIGNAL(completed(WebServiceReplyPtr,WebServiceReply::CompletedState)),
                        this, SLOT(onItemRenamingCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)));
                addRequest(tagItem, reply);
            }
            else if((googleReaderItem->itemType() == GoogleReaderItem::ItFeedItem)
                    && isRequestAllowed(GoogleReaderClient::TpRenameFeed, googleReaderItem))
            {
                FeedItemPtr feedItem = googleReaderItem.staticCast<FeedItem>();
                EditReplyPtr reply = m_client->renameFeed(feedItem->itemId(), newName);
                connect(reply.data(), SIGNAL(completed(WebServiceReplyPtr,WebServiceReply::CompletedState)),
                        this, SLOT(onItemRenamingCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)));
                addRequest(feedItem, reply);
            }
        }
        else
        {
            emit itemRenamingFinished(GoogleReaderEnum::ErrUndefinedError);
        }
    }
}

/*!
  Aborts the operation which renames the given \a item.
*/
void GoogleReaderService::abortItemRename(GoogleReaderItem *item)
{
    if(item)
    {
        GoogleReaderItemPtr googleReaderItem = GoogleReaderItemWeakRef(item).toStrongRef();
        if((googleReaderItem->itemType() == GoogleReaderItem::ItTagItem)
          || (googleReaderItem->itemType() == GoogleReaderItem::ItFolderItem))
        {
            abortRequest(GoogleReaderClient::TpRenameTag, WebServiceReply::RlUndefined,
                         googleReaderItem);
        }
        else if(googleReaderItem->itemType() == GoogleReaderItem::ItFeedItem)
        {
            abortRequest(GoogleReaderClient::TpRenameFeed, WebServiceReply::RlUndefined,
                         googleReaderItem);
        }
    }
}

/*!
  Starts the operation which deletes the \a item.
*/
void GoogleReaderService::deleteItem(GoogleReaderItem *item)
{
    if(item)
    {
        GoogleReaderItemPtr googleReaderItem = GoogleReaderItemWeakRef(item).toStrongRef();
        if(((googleReaderItem->itemType() == GoogleReaderItem::ItTagItem)
            || (googleReaderItem->itemType() == GoogleReaderItem::ItFolderItem))
                && isRequestAllowed(GoogleReaderClient::TpDeleteTag, googleReaderItem))
        {
            EditReplyPtr reply = m_client->deleteTag(googleReaderItem->itemId());
            connect(reply.data(), SIGNAL(completed(WebServiceReplyPtr,WebServiceReply::CompletedState)),
                    this, SLOT(onItemDeleteCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)));
            assingBusyPropertyToRequest(reply, googleReaderItem, GoogleReaderService::s_editingBusyPropertyName);
            addRequest(googleReaderItem, reply);
        }
        else if((googleReaderItem->itemType() == GoogleReaderItem::ItFeedItem)
                && isRequestAllowed(GoogleReaderClient::TpDeleteFeed, googleReaderItem))
        {
            EditReplyPtr reply = m_client->deleteFeed(googleReaderItem->itemId(), googleReaderItem->title());
            connect(reply.data(), SIGNAL(completed(WebServiceReplyPtr,WebServiceReply::CompletedState)),
                    this, SLOT(onItemDeleteCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)));
            assingBusyPropertyToRequest(reply, googleReaderItem, GoogleReaderService::s_editingBusyPropertyName);
            addRequest(googleReaderItem, reply);
        }
    }
}

/*!
  Starts the operation which retrieves feed suggestions for the given
  \a searchTerms.
*/
void GoogleReaderService::findFeeds(const QString &searchTerm)
{
    abortRequest(GoogleReaderClient::TpFindFeeds,
                 WebServiceReply::RlUndefined,
                 m_rootItem);
    if(isRequestAllowed(GoogleReaderClient::TpFindFeeds, m_rootItem))
    {
        FeedSearchResultsReplyPtr reply = m_client->findFeeds(searchTerm);
        connect(reply.data(), SIGNAL(completed(WebServiceReplyPtr, WebServiceReply::CompletedState)),
                this, SLOT(onFindFeedsCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)));
        addRequest(m_rootItem, reply);
    }
}

/*!
  Starts the operation which adds a new feed with the given \a url.
*/
void GoogleReaderService::addFeed(const QString &url)
{
    if(isRequestAllowed(GoogleReaderClient::TpAddFeed, m_rootItem))
    {
        EditReplyPtr reply = m_client->addFeed(url);
        connect(reply.data(), SIGNAL(completed(WebServiceReplyPtr, WebServiceReply::CompletedState)),
                this, SLOT(onFeedAddingCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)));
        addRequest(m_rootItem, reply);
    }
}

/*!
  Aborts the operation which adds a new feed item.
*/
void GoogleReaderService::abortAddFeed()
{
    abortRequest(GoogleReaderClient::TpAddFeed, WebServiceReply::RlUndefined,
                 m_rootItem);
}

/*!
  Reimplemented from QDeclarativeImageProvider::requestImage().
*/
QImage GoogleReaderService::requestImage(const QString &id, QSize *size, const QSize &requestedSize)
{
    QImage ret;
    if(id == GoogleReaderService::s_serviceIconId)
        ret = QImage(GoogleReaderService::s_serviceImageUrl);
    else if(id == GoogleReaderService::s_feedIconId)
        ret = QImage(GoogleReaderService::s_generalFeedImageUrl);
    else if(id == GoogleReaderService::s_folderIconId)
        ret = QImage(GoogleReaderService::s_folderImageUrl);
    else if(id == GoogleReaderService::s_starredTagIconId)
        ret = QImage(GoogleReaderService::s_starGroupImageUrl);
    else if(id == GoogleReaderService::s_tagIconId)
        ret = QImage(GoogleReaderService::s_tagImageUrl);
    if((ret.size() != requestedSize) && requestedSize.isValid())
        ret = ret.scaled(requestedSize, Qt::KeepAspectRatio);
    *size = ret.size();
    return ret;
}

/*!
  Returns the username of the user.
*/
QString GoogleReaderService::userName() const
{
    return pluginSettings()->value(GoogleReaderService::s_userNameKey).toString();
}

/*!
  \sa GoogleReaderService::userName.
*/
void GoogleReaderService::setUserName(const QString &userName)
{
    pluginSettings()->setValue(GoogleReaderService::s_userNameKey, userName);
}

/*!
  Returns the password of the user.
*/
QString GoogleReaderService::password() const
{
    return pluginSettings()->value(GoogleReaderService::s_passwordKey).toString();
}

/*!
  \sa GoogleReaderService::password.
*/
void GoogleReaderService::setPassword(const QString &password)
{
    pluginSettings()->setValue(GoogleReaderService::s_passwordKey, password);
}

/*!
  Returns true if the specified \a type of request is allowed to be started
  for the given \a relatedItem.
*/
bool GoogleReaderService::isRequestAllowed(GoogleReaderClient::RequestType type,
                                           GoogleReaderItemPtr relatedItem) const
{
    return (!isRequestRunning(GoogleReaderClient::TpUpdateTree)
            && !isRequestRunning(type, relatedItem));
}

/*!
  Sets the unread count proprty of the items specified by \a unreadCounts.
  The keys in the \a unreadCounts parameter should be item ids and the items'
  new unread counts should be specified in the values of the hash.
*/
void GoogleReaderService::setItemUnreadCounts(const QHash<QString, int> &unreadCounts)
{
    foreach(FeedItemPtr feedItem, m_feedItems.values())
        feedItem->setUnreadCount(unreadCounts.value(feedItem->itemId()));
    foreach(TagItemPtr tagItem, m_tagItems.values())
        tagItem->setUnreadCount(unreadCounts.value(tagItem->itemId()));
    foreach(FolderItemPtr folderItem, m_folderItems.values())
        folderItem->setUnreadCount(unreadCounts.value(folderItem->itemId()));
    int unreadCountSum = unreadCounts.value(m_everythingTagItem->itemId());
    m_everythingTagItem->setUnreadCount(unreadCountSum);
    m_rootItem->setUnreadCount(unreadCountSum);
}

/*!
  \sa GoogleReaderService::unreadCountLimit
*/
void GoogleReaderService::setUnreadCountLimit(int limit)
{
    m_unreadCountLimit = limit;
    emit unreadCountLimitChanged();
}

/*!
  Increments the \a parentItems's unread count value by 1,
  as long as it's lower than the current GoogleReaderService::unreadCountLimit.
*/
void GoogleReaderService::incrementUnreadCount(ParentItemPtr parentItem)
{
    int newUnreadCount = parentItem->unreadCount() + 1;
    if(newUnreadCount <= unreadCountLimit())
        parentItem->setUnreadCount(newUnreadCount);
}

/*!
  Decrements the \a parentItems's unread count value by 1,
  as long as it's lower than the current GoogleReaderService::unreadCountLimit.
*/
void GoogleReaderService::decrementUnreadCount(ParentItemPtr parentItem)
{
    int unreadCount = parentItem->unreadCount();
    if(unreadCount < unreadCountLimit())
        parentItem->setUnreadCount(unreadCount - 1);
}

/*!
  Notifies the veiw that the account verification operation has completed.
*/
void GoogleReaderService::onAccountVerificationCompleted(WebServiceReplyPtr reply, WebServiceReply::CompletedState state)
{
    Q_UNUSED(state);
    emit accountVerificationComplete(reply->error());
}

void GoogleReaderService::onItemActivated(PageItemPtr item)
{
    GoogleReaderItemPtr itemPtr = item.staticCast<GoogleReaderItem>();
    if(itemPtr)
    {
        if(itemPtr->itemType() == GoogleReaderItem::ItRootItem)
        {
            //if the unread count update timer is not yet started
            //then start it
            if(!m_unreadCountUpdateTimer.isActive())
                m_unreadCountUpdateTimer.start();
            //load the feed and folder hierarchy if it's not already loaded
            if(itemPtr->childCount() == 0)
                updateTreeStructure();            
            else if(m_unreadCountsOutOfDate)
                updateUnreadCounts();
        }
        else if(itemPtr->itemType() == GoogleReaderItem::ItStoryItem)
        {
            StoryItemPtr storyItem = item.staticCast<StoryItem>();
            if(!storyItem->isRead())
                markStoryRead(storyItem);
        }
    }
}

void GoogleReaderService::onItemDeactivated(PageItemPtr item)
{
    GoogleReaderItemPtr itemPtr = item.staticCast<GoogleReaderItem>();
    if(itemPtr)
    {
        FeedItemPtr feedItemPtr = item.dynamicCast<FeedItem>();
        if(feedItemPtr)
        {
            abortRequest(0,
                         WebServiceReply::RlUpdate,
                         feedItemPtr);
            feedItemPtr->setContinuation(QString());
            feedItemPtr->setCanFetchMore(true);
            feedItemPtr->clearChildItems();
        }
    }
}

void GoogleReaderService::onMoreChildrenRequested(PageItemPtr parentItem)
{
    FeedItemPtr feedItemPtr = parentItem.dynamicCast<FeedItem>();
    if(feedItemPtr)
        loadFeedStories(feedItemPtr);
}

/*!
  Updates the unread counts of feeds without resetting the
  timer.
*/
void GoogleReaderService::onUnreadCountUpdateTimerTimeout()
{
    updateUnreadCounts(false);
}

/*!
  Sets the user's authenticatin data for the given \a auth object.
*/
void GoogleReaderService::onAuthenticationRequired(QAuthenticator *auth)
{
    auth->setUser(userName());
    auth->setPassword(password());
}

/*!
  Removes the the completed \a reply from the list of stored replies.
*/
void GoogleReaderService::onRequestCompleted(WebServiceReplyPtr reply, WebServiceReply::CompletedState state)
{
    Q_UNUSED(reply);
    Q_UNUSED(state);
    removeRequest(reply);
}

/*!
  Notifies the view about the faliure of the request
  related to the given \a reply.
*/
void GoogleReaderService::onRequestFailed(WebServiceReplyPtr reply)
{
    Q_UNUSED(reply);
    GoogleReaderEnum::ErrorType errorType = GoogleReaderEnum::ErrorType(reply->error());
    if(reply->role() == WebServiceReply::RlUpdate)
    {
        if((errorType == GoogleReaderEnum::ErrNetworkError)
           && m_rootItem)
            m_rootItem->showMessage(tr("Network error"));
        else
            m_rootItem->showMessage(tr("Failed to retrieve data from the server\n"
                                       "It might be offline"));
    }
    else if(reply->role() == WebServiceReply::RlCommit)
    {
        GoogleReaderItemPtr relatedGoogleReaderItem = relatedItem(reply).staticCast<GoogleReaderItem>();
        if(m_rootItem && (errorType == GoogleReaderEnum::ErrNetworkError))
        {
            m_rootItem->showMessage(tr("Network error"));
        }
        else if(relatedGoogleReaderItem)
        {
            if(reply->requestType() == GoogleReaderClient::TpStarStory)
                relatedGoogleReaderItem->showMessage(tr("Failed to add item to starred items"));
            else if(reply->requestType() == GoogleReaderClient::TpUnstarStory)
                relatedGoogleReaderItem->showMessage(tr("Failed to remove item from starred items"));
            else if(reply->requestType() == GoogleReaderClient::TpMarkStoryRead)
                relatedGoogleReaderItem->showMessage(tr("Failed to mark items as read"));
            else if(reply->requestType() == GoogleReaderClient::TpMarkStoryUnread)
                relatedGoogleReaderItem->showMessage(tr("Failed to mark item as unread"));
            else if(reply->requestType() == GoogleReaderClient::TpMarkFeedAsRead)
                relatedGoogleReaderItem->showMessage(tr("Failed to mark all items as read"));
            else if(reply->requestType() == GoogleReaderClient::TpDeleteFeed)
                relatedGoogleReaderItem->showMessage(tr("Failed to delete subscription"));
            else if(reply->requestType() == GoogleReaderClient::TpDeleteTag)
                relatedGoogleReaderItem->showMessage(tr("Failed to delete tag"));
        }
    }
}

/*!
  Displays a message on the console that the request related to
  \a reply has been aborted.
*/
void GoogleReaderService::onRequestAborted(WebServiceReplyPtr reply)
{
#ifdef DEBUG
    qDebug() << "OPERATION ABORTED:" << reply->requestType();
#else
    Q_UNUSED(reply);
#endif
}

/*!
  Starts the operation which updates the feed, folder and tag
  hierarchy of the service.
*/
void GoogleReaderService::updateTreeStructure()
{
    if(isRequestAllowed(GoogleReaderClient::TpUpdateTree, m_rootItem))
    {
        setBusy(true);
        UpdateTreeReplyPtr reply = m_client->updateTree();
        connect(reply.data(), SIGNAL(completed(WebServiceReplyPtr, WebServiceReply::CompletedState)),
                this, SLOT(onUpdateTreeCompleted(WebServiceReplyPtr, WebServiceReply::CompletedState)));
        addRequest(m_rootItem, reply);
    }
}

/*!
  If the root item of the service is active the unread counts
  of feeds are updated, otherwise they will be activated the next
  time the root item is activated.
*/
void GoogleReaderService::updateUnreadCounts(bool resetTimer)
{
#ifdef QTM_USE_NAMESPACE
    if(resetTimer)
        m_unreadCountUpdateTimer.wokeUp();
#else
    Q_UNUSED(resetTimer);
#endif
    if(isRequestAllowed(GoogleReaderClient::TpUpdateUnreadCounts, GoogleReaderItemPtr()))
    {
        m_unreadCountsOutOfDate = true;
        if(m_rootItem->isActive())
        {
            UnreadCountsReplyPtr reply = m_client->updateUnreadCounts();
            connect(reply.data(), SIGNAL(finished(WebServiceReplyPtr)),
                    this, SLOT(onUpdateUnreadCountsFinished(WebServiceReplyPtr)));
            addRequest(m_rootItem, reply);
        }
    }
}

/*!
  Removes all of the child items of the given \a feedItem,
  restores it to it's initial state and reloads it's child items.
*/
void GoogleReaderService::refreshFeed(FeedItemPtr feedItem)
{
    if(feedItem)
    {
        abortRequest(GoogleReaderClient::TpLoadFeedStories,
                     WebServiceReply::RlUndefined,
                     feedItem);
        feedItem->setContinuation(QString());
        feedItem->setCanFetchMore(true);
        feedItem->clearChildItems();
        loadFeedStories(feedItem);
    }
}

/*!
  Requests the next batch of child items for the given \a feedItem.
*/
void GoogleReaderService::loadFeedStories(FeedItemPtr feedItem)
{
    if(isRequestAllowed(GoogleReaderClient::TpLoadFeedStories, feedItem)
       && feedItem->canFetchMore())
    {
        int preferredStoryCount = GoogleReaderService::s_preferredStoryCount;
        GoogleReaderEnum::ItemFilterType currentItemFilter = GoogleReaderEnum::ItemFilterType(itemFilter());
        StoryListReplyPtr reply = m_client->loadFeedStories(feedItem->itemId(), currentItemFilter,
                                                            GoogleReaderEnum::ItemSortType(itemSortType()),
                                                            preferredStoryCount,
                                                            feedItem->continuation(),
                                                            feedItem->startTime());
        connect(reply.data(), SIGNAL(finished(WebServiceReplyPtr)),
                this, SLOT(onLoadFeedStoriesFinished(WebServiceReplyPtr)));
        assingBusyPropertyToRequest(reply, feedItem, GoogleReaderService::s_busyPropertyName);
        addRequest(feedItem, reply);
    }
}

/*!
  Updates the feed, folder and tag hierarchy of the service based on the contents of the \a reply
  if the completition state is WebServiceReply::CsFinished.
*/
void GoogleReaderService::onUpdateTreeCompleted(WebServiceReplyPtr reply, WebServiceReply::CompletedState state)
{
    if(state == WebServiceReply::CsFinished)
    {        
        UpdateTreeReplyPtr updateTreeReply = reply.staticCast<UpdateTreeReply>();
        QMultiHash<QString, FeedItemPtr> newFeedItems = updateTreeReply->feedItems();
        QMultiHash<QString, TagItemPtr> newTagItems = updateTreeReply->tagItems();
        QMultiHash<QString, FolderItemPtr> newFolderItems = updateTreeReply->folderItems();
        EverythingTagItemPtr newEverythingTagItem = updateTreeReply->everythingTagItem();
        StarredTagItemPtr newStarredTagItem = updateTreeReply->starredTagItem();
        if(m_rootItem->childCount() > 0)
        {
            /*loop through the existing item tree and replace every
            existing item with the corresponding new item if it's not on the
            active branch.
            */
            /*existingItem and newItem represent the match between
              the existing tree and the new tree. start from the root
              items because they are the only certeanly matching items.
            */
            PageItemPtr existingItem = m_rootItem;
            PageItemPtr newItem = updateTreeReply->rootItem();
            /*existingItemCandidate and newItemCandidate are the items
              from the existing and new tree that possibly match.
            */
            PageItemPtr existingItemCandidate;
            PageItemPtr newItemCandidate;
            bool existingItemContainsStories = false;
            do
            {
                QList<PageItemPtr> existingItemChildren = existingItem->childItems();
                /*loop through the children of the existingItem to find the active child
                  item. everything else should be removed.
                */
                foreach(PageItemPtr existingItemChild, existingItemChildren)
                {
                    /*if the current child is active and it can be found in the new tree
                      too, then these items are matching, they will become the new matching
                      items on the next iteration.
                    */                    
                    if(existingItemChild->isActive()
                       && (newItem->children(existingItemChild->itemId()).count() == 1))
                    {
                        GoogleReaderItemPtr grExistingItemChild = existingItemChild.dynamicCast<GoogleReaderItem>();
                        GoogleReaderItemPtr grNewItemChild = newItem->children(existingItemChild->itemId()).first().dynamicCast<GoogleReaderItem>();
                        if(grExistingItemChild->itemType() == grNewItemChild->itemType())
                        {
                            existingItemCandidate = existingItemChild;
                            newItemCandidate = newItem->children(existingItemChild->itemId()).first();
                        }
                        else
                        {
                            existingItem->removeChild(existingItemChild);
                        }
                    }
                    else
                    {
                        existingItem->removeChild(existingItemChild);
                    }
                }

                /*at this point the existingItem contains no other child items
                  then the active one that matches with an item on the new tree,
                  or no child items at all if no match was found.
                  loop through the children of the existing item and add them to
                  the existingItem.
                */
                QList<PageItemPtr> newItemChildren = newItem->childItems();
                /*shouldPrepend true until the new child items should be
                  prepended to the existingItem, if false the new child items
                  should be appended.
                */
                bool shouldPrepend = true;
                foreach(PageItemPtr newItemChild, newItemChildren)
                {
                    /*when the matching item is reached in the new tree it shouldn't
                      be moved to existingItem, just it's properties should be copied
                      to the matching child of existingItem.
                    */
                    if(newItemChild == newItemCandidate)
                    {
                        shouldPrepend = false;
                        existingItemCandidate->setItemId(newItemCandidate->itemId());
                        existingItemCandidate->setTitle(newItemCandidate->title());

                        TagItemPtr tagItem = newItemCandidate.dynamicCast<TagItem>();
                        FeedItemPtr feedItem = newItemCandidate.dynamicCast<FeedItem>();
                        FolderItemPtr folderItem = newItemCandidate.dynamicCast<FolderItem>();
                        if(tagItem)
                        {
                            TagItemPtr existingTagItemCandidate = existingItemCandidate.staticCast<TagItem>();
                            if(tagItem->itemType() == GoogleReaderItem::ItTagItem)
                            {
                                newTagItems.remove(tagItem->itemId(), tagItem);
                                newTagItems.insertMulti(tagItem->itemId(), existingTagItemCandidate);
                            }
                            else if(tagItem->itemType() == GoogleReaderItem::ItEverythingTagItem)
                            {
                                newEverythingTagItem = existingItemCandidate.staticCast<EverythingTagItem>();
                            }
                            else if(tagItem->itemType() == GoogleReaderItem::ItStarredTagItem)
                            {
                                newStarredTagItem = existingItemCandidate.staticCast<StarredTagItem>();
                            }
                            existingItemContainsStories = true;
                        }
                        if(folderItem && newFolderItems.values().contains(folderItem))
                        {
                            newFolderItems.remove(folderItem->itemId(), folderItem);
                            newFolderItems.insertMulti(folderItem->itemId(), existingItemCandidate.staticCast<FolderItem>());
                        }
                        if(feedItem && newFeedItems.values().contains(feedItem))
                        {
                            FeedItemPtr existingFeedItemCandidate = existingItemCandidate.staticCast<FeedItem>();
                            existingFeedItemCandidate->setStartTime(feedItem->startTime());
                            existingFeedItemCandidate->setFolders(feedItem->folders());
                            newFeedItems.remove(feedItem->itemId(), feedItem);
                            newFeedItems.insertMulti(feedItem->itemId(), existingFeedItemCandidate);
                            existingItemContainsStories = true;
                        }
                    }
                    else if(shouldPrepend && existingItemCandidate)
                    {
                        newItem->removeChild(newItemChild);
                        existingItem->insertChild(existingItemCandidate->row(), newItemChild);
                    }
                    else
                    {
                        newItem->removeChild(newItemChild);
                        existingItem->appendChild(newItemChild);
                    }
                }
                /*the matching child items of existingItem and newItem
                  will become the new existingItem and newItem in the next iteration
                */
                existingItem = existingItemCandidate;
                newItem = newItemCandidate;
                existingItemCandidate.clear();
                newItemCandidate.clear();
            }
            //stop the iteration if no matches were found
            while(existingItem && newItem && !existingItemContainsStories);
        }
        else
        {
            QList<PageItemPtr> childItems = updateTreeReply->rootItem()->childItems();
            updateTreeReply->rootItem()->clearChildItems();
            m_rootItem->setChildItems(childItems);
        }
        m_feedItems = newFeedItems;
        m_tagItems = newTagItems;
        m_folderItems = newFolderItems;
        emit availableTagsChanged();
        foreach(TagItemPtr tagItem, m_tagItems.values())
            tagItem->setIconSource(imageUrl(GoogleReaderService::s_tagIconId));
        foreach(FolderItemPtr folderItem, m_folderItems.values())
            folderItem->setIconSource(imageUrl(GoogleReaderService::s_folderIconId));
        m_everythingTagItem = newEverythingTagItem;
        m_everythingTagItem->setTitle(tr("All items"));
        m_everythingTagItem->setIconSource(imageUrl(GoogleReaderService::s_feedIconId));
        m_starredTagItem = newStarredTagItem;
        m_starredTagItem->setTitle(tr("Starred items"));
        m_starredTagItem->setIconSource(imageUrl(GoogleReaderService::s_starredTagIconId));
        setItemUnreadCounts(updateTreeReply->unreadCountsPerId());
        setUnreadCountLimit(updateTreeReply->unreadCountLimit());
        registerItem(m_rootItem, true);
    }
    setBusy(false);
}

/*!
  Updates the unread counts of every folder, tag and feed item,
  based on \a reply's contents.
*/
void GoogleReaderService::onUpdateUnreadCountsFinished(WebServiceReplyPtr reply)
{
    UnreadCountsReplyPtr unreadCountsReply = reply.staticCast<UnreadCountsReply>();
    setItemUnreadCounts(unreadCountsReply->unreadCountsPerId());
    setUnreadCountLimit(unreadCountsReply->unreadCountLimit());
    m_unreadCountsOutOfDate = false;
}

/*!
  Appends the loaded story items in \a reply to the related feed item.
*/
void GoogleReaderService::onLoadFeedStoriesFinished(WebServiceReplyPtr reply)
{
    StoryListReplyPtr storyListReply = reply.staticCast<StoryListReply>();
    FeedItemPtr relatedFeedItem = relatedItem(reply).dynamicCast<FeedItem>();
    if(relatedFeedItem)
    {
        relatedFeedItem->setContinuation(storyListReply->continuation());
        if(relatedFeedItem->continuation().isEmpty())
            relatedFeedItem->setCanFetchMore(false);
        relatedFeedItem->appendChildren(storyListReply->stories());
        registerItem(relatedFeedItem, true);
    }
}

/*!
  Marks the story items related to the \a reply as starred or saved.
*/
void GoogleReaderService::onStarStoryFinished(WebServiceReplyPtr reply)
{
    StoryItemPtr relatedStoryItem = relatedItem(reply).dynamicCast<StoryItem>();
    if(relatedStoryItem)
        relatedStoryItem->setStarred(true);
}

/*!
  Removes the starred or saved marks of story items related to the \a reply.
*/
void GoogleReaderService::onUnstarStoryFinished(WebServiceReplyPtr reply)
{
    StoryItemPtr relatedStoryItem = relatedItem(reply).dynamicCast<StoryItem>();
    if(relatedStoryItem)
        relatedStoryItem->setStarred(false);
}

/*!
  Marks the story items related to the \a reply as read.
*/
void GoogleReaderService::onMarkStoryReadFinished(WebServiceReplyPtr reply)
{
    StoryItemPtr relatedStoryItem = relatedItem(reply).dynamicCast<StoryItem>();
    if(relatedStoryItem)
    {
        relatedStoryItem->setRead(true);
        foreach(const QString& tagId, relatedStoryItem->tags())
        {
            QList<TagItemPtr> parentTags = m_tagItems.values(tagId);
            foreach(TagItemPtr tagItem, parentTags)
                decrementUnreadCount(tagItem);
            QList<FolderItemPtr> parentFolders = m_folderItems.values(tagId);
            foreach(FolderItemPtr folderItem, parentFolders)
                decrementUnreadCount(folderItem);
        }
        QList<FeedItemPtr> parentFeeds =  m_feedItems.values(relatedStoryItem->feedId());
        if(relatedStoryItem->isInReadingList())
        {
            decrementUnreadCount(m_everythingTagItem);
            decrementUnreadCount(m_rootItem);
        }
        foreach(FeedItemPtr feedItem, parentFeeds)
            decrementUnreadCount(feedItem);
    }
}

/*!
  Marks the story items related to the \a reply as unread.
*/
void GoogleReaderService::onMarkStoryUnreadFinished(WebServiceReplyPtr reply)
{
    StoryItemPtr relatedStoryItem = relatedItem(reply).dynamicCast<StoryItem>();
    if(relatedStoryItem)
    {
        relatedStoryItem->setRead(false);
        relatedStoryItem->setKeptUnread(true);
        foreach(const QString& tagId, relatedStoryItem->tags())
        {
            QList<TagItemPtr> parentTags = m_tagItems.values(tagId);
            foreach(TagItemPtr tagItem, parentTags)
                incrementUnreadCount(tagItem);
            QList<FolderItemPtr> parentFolders = m_folderItems.values(tagId);
            foreach(FolderItemPtr folderItem, parentFolders)
                incrementUnreadCount(folderItem);
        }
        QList<FeedItemPtr> parentFeeds =  m_feedItems.values(relatedStoryItem->feedId());
        if(relatedStoryItem->isInReadingList())
        {
            incrementUnreadCount(m_everythingTagItem);
            incrementUnreadCount(m_rootItem);
        }
        foreach(FeedItemPtr feedItem, parentFeeds)
            incrementUnreadCount(feedItem);
    }
}

/*!
  Marks all the stories of the feeds related to the \a reply as read.
*/
void GoogleReaderService::onMarkFeedAsReadFinished(WebServiceReplyPtr reply)
{
    updateUnreadCounts();
    FeedItemPtr relatedFeedItem = relatedItem(reply).dynamicCast<FeedItem>();
    if(relatedFeedItem && relatedFeedItem->isActive())
        refreshFeed(relatedFeedItem);
}

/*!
  Marks every story item in every feed as read.
*/
void GoogleReaderService::onMarkEverythingAsReadFinished(WebServiceReplyPtr reply)
{
    Q_UNUSED(reply);
    updateUnreadCounts();
    if(m_everythingTagItem && m_everythingTagItem->isActive())
        refreshFeed(m_everythingTagItem);
}

/*!
  Updates the tags of the story item related to the \a reply if
  the completition \a state is WebServiceReply::CsFinished.
*/
void GoogleReaderService::onSettingStoryTagsCompleted(WebServiceReplyPtr reply, WebServiceReply::CompletedState state)
{
    EditReplyPtr editReply = reply.staticCast<EditReply>();
    if(state == WebServiceReply::CsFinished)
    {
        StoryItemPtr relatedStoryItem = relatedItem(reply).dynamicCast<StoryItem>();
        if(relatedStoryItem)
        {
            QSet<QString> storyTagSet = relatedStoryItem->tags().toSet();
            storyTagSet += editReply->tagsAdded().toSet();
            storyTagSet -= editReply->tagsRemoved().toSet();
            relatedStoryItem->setTags(storyTagSet.toList());
            QSet<QString> newTagSet = storyTagSet - m_tagItems.uniqueKeys().toSet();
            if(newTagSet.count())
                updateTreeStructure();
        }
    }
    emit settingItemTagsFinished(reply->error());
}

/*!
  Updates the tree structure of the service if
  the completition \a state is WebServiceReply::CsFinished
  or notifies the view about possible errors.
*/
void GoogleReaderService::onSettingFeedTagsCompleted(WebServiceReplyPtr reply, WebServiceReply::CompletedState state)
{
    Q_UNUSED(state);
    updateTreeStructure();
    emit settingItemTagsFinished(reply->error());
}

/*!
  Updates the tree structure of the service if
  the completition \a state is WebServiceReply::CsFinished
  or notifies the view about possible errors.
*/
void GoogleReaderService::onItemRenamingCompleted(WebServiceReplyPtr reply, WebServiceReply::CompletedState state)
{
    Q_UNUSED(state);
    updateTreeStructure();
    emit itemRenamingFinished(reply->error());
}

/*!
  Updates the tree structure of the service if
  the completition \a state is WebServiceReply::CsFinished.
*/
void GoogleReaderService::onItemDeleteCompleted(WebServiceReplyPtr reply, WebServiceReply::CompletedState state)
{
    Q_UNUSED(reply);
    Q_UNUSED(state);
    updateTreeStructure();
}

/*!
  Notifies the view that the loading of feed suggestions is finished.
  If the completition \a state of the \a reply is WebServiceReply::CsFinished
  the view gets the recieved suggestions, otherwise it's notified about the error.
*/
void GoogleReaderService::onFindFeedsCompleted(WebServiceReplyPtr reply, WebServiceReply::CompletedState state)
{
    FeedSearchResultsReplyPtr searchResultsReply = reply.staticCast<FeedSearchResultsReply>();
    if(state == WebServiceReply::CsFinished)
        emit feedSearchFinished(reply->error(), searchResultsReply->feedTitles(), searchResultsReply->feedUrls());
    else
        emit feedSearchFinished(reply->error(), QStringList(), QStringList());
}

/*!
  Updates the tree structure of the service if
  the completition \a state is WebServiceReply::CsFinished
  or notifies the view about possible errors.
*/
void GoogleReaderService::onFeedAddingCompleted(WebServiceReplyPtr reply, WebServiceReply::CompletedState state)
{
    if(state == WebServiceReply::CsFinished)
        updateTreeStructure();
    emit feedAddingFinished(reply->error());
}
} // namespace GoogleReader
Q_EXPORT_PLUGIN2(googlereaderservice, GoogleReader::GoogleReaderService)
