/****************************************************************************
**
** 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 "newsblurservice.h"
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QUrl>
#include <QDebug>
#include <QEventLoop>
#include <QNetworkCookieJar>
#include <QNetworkCookie>
#include <QDateTime>
#include <QtDeclarative>
#include <QTextDocumentFragment>
#include <QFile>

/*!
  \namespace NewsBlur
  \brief NewsBlur plugin impelemntation.
*/
namespace NewsBlur
{
/*!
  \class NewsBlurService
  \brief The NewsBlurService class is responsible for maintaining
  the NewsBlur subtree of the item tree.
*/
/*!
  \property NewsBlurService::busy
  True if the NewsBlur subtree is currently being updated
  and no interaction with the service is allowed,
  otherwise false.
*/
/*!
  \property NewsBlurService::markingEverythingReadBusy
  If true the operation of marking every story in
  every feed is in progress, otherwise false.
*/
/*!
  \property NewsBlurService::filterType
  The current filtering type of the service.
*/
const char* NewsBlurService::s_serviceName                          = "NewsBlur";
const char* NewsBlurService::s_serviceId                            = "newsblur";
const char* NewsBlurService::s_serviceImageUrl                      = ":/plugins/newsblur/images/newsblur.png";
const char* NewsBlurService::s_starGroupImageUrl                    = ":/shared/images/star_icon.png";
const char* NewsBlurService::s_generalFeedImageUrl                  = ":/shared/images/feed_icon.png";
const char* NewsBlurService::s_folderImageUrl                       = ":/shared/images/folder_icon.png";
const char* NewsBlurService::s_folderIconId                         = "folder-icon";
const char* NewsBlurService::s_feedIconId                           = "feed-icon";
const char* NewsBlurService::s_starredGroupIconId                   = "starred-group-icon";
const char* NewsBlurService::s_serviceIconId                        = "service-icon";

const char* NewsBlurService::s_userNameKeyName                      = "username";
const char* NewsBlurService::s_passwordKeyName                      = "password";
const char* NewsBlurService::s_filterTypeKeyName                    = "filterType";

const int NewsBlurService::s_unreadCountUpdateIntervalMinMs         = 2 * 60;
const int NewsBlurService::s_unreadCountUpdateIntervalMaxMs         = 3 * 60;
const int NewsBlurService::s_savedStoriesPerPage                    = 10;
const int NewsBlurService::s_riverStoriesPerPage                    = 18;
const int NewsBlurService::s_feedStoriesPerPage                     = 6;
const int NewsBlurService::s_preferredStoryCountPerRequest          = 10;
const int NewsBlurService::s_preferredFilteredStoryCountPerRequest  = 1;
const int NewsBlurService::s_preferredMinimalStoryCount             = 7;

const int NewsBlurService::s_itemIconWidth                          = 64;
const int NewsBlurService::s_itemIconHeight                         = 64;
const char* NewsBlurService::s_busyPropertyName                     = "busy";
const char* NewsBlurService::s_childrenBusyPropertyName             = "childrenBusy";
const char* NewsBlurService::s_starredBusyPropertyName              = "starredBusy";
const char* NewsBlurService::s_feedReadBusyPropertyName             = "feedReadBusy";
const char* NewsBlurService::s_serviceBusyPropertyName              = "busy";
const char* NewsBlurService::s_deletingBusyPropertyName             = "deletingBusy";
const char* NewsBlurService::s_movingBusyPropertyName               = "movingBusy";

const char* NewsBlurService::s_authenticationPageUrl                = "qrc:/plugins/newsblur/qml/sheets/AuthenticationSheet.qml";
const char* NewsBlurService::s_servicePageUrl                       = "qrc:/plugins/newsblur/qml/pages/NewsBlurPage.qml";
const char* NewsBlurService::s_markFeedAsReadToolButtonUrl          = "qrc:/newsblur/buttons/MarkFeedAsReadToolButton.qml";
const char* NewsBlurService::s_markFolderAsReadToolButtonUrl        = "qrc:/newsblur/buttons/MarkFolderAsReadToolButton.qml";
const char* NewsBlurService::s_markEverythingAsReadToolButtonUrl    = "qrc:/newsblur/buttons/MarkEverythingAsReadToolButton.qml";
const char* NewsBlurService::s_starUnstarStoryToolButtonUrl         = "qrc:/newsblur/buttons/StarStoryToolButton.qml";
const char* NewsBlurService::s_shareStoryToolButtonUrl              = "qrc:/newsblur/buttons/ShareStoryToolButton.qml";
const char* NewsBlurService::s_markStoryAsUnreadMenuItemName        = "MarkStoryAsUneadMenuItem";
const char* NewsBlurService::s_intelligenceTrainerMenuItemName      = "IntelligenceTrainerMenuItem";
const char* NewsBlurService::s_renameMenuItemName                   = "RenameMenuItem";
const char* NewsBlurService::s_moveMenuItemName                     = "MoveMenuItem";
const char* NewsBlurService::s_deleteMenuItemName                   = "DeleteMenuItem";
const char* NewsBlurService::s_markFeedAsReadMenuItemName           = "MarkFeedAsReadMenuItem";
const char* NewsBlurService::s_shareStoryMenuItemName               = "ShareStoryMenuItem";
const char* NewsBlurService::s_starStoryMenuItemName                = "StarStoryMenuItem";
const char* NewsBlurService::s_unstarStoryMenuItemName              = "UnstarStoryMenuItem";
const char* NewsBlurService::s_newsBlurHelpMenuItemName             = "NewsBlurHelpMenuItem";

/*!
  Constructs a NewsBlurService object with the given \a parent.
*/
NewsBlurService::NewsBlurService(QObject *parent) :
    AbstractFeedAggreagatorService(parent),
    m_unreadCountsOutOfDate(false),
    m_serviceBusy(false),
    m_markingEverythingReadBusy(false),
    m_client(new NewsBlurClient(this))
{
    //register the custorm item types
    qmlRegisterType<NewsBlur::FeedClassifiersModel>("NewsBlur", 1, 0, "ClassificationModel");
    qmlRegisterType<NewsBlur::NewsBlurService>("NewsBlur", 1, 0, "NewsBlurService");
    qmlRegisterType<NewsBlur::NewsBlurItem>("NewsBlur", 1, 0, "NewsBlurItem");
    qmlRegisterType<NewsBlur::RootItem>("NewsBlur", 1, 0, "RootItem");
    qmlRegisterType<NewsBlur::FolderItem>("NewsBlur", 1, 0, "FolderItem");
    qmlRegisterType<NewsBlur::FeedItem>("NewsBlur", 1, 0, "FeedItem");
    qmlRegisterType<NewsBlur::EverythingGroup>("NewsBlur", 1, 0, "EverythingGroup");
    qmlRegisterType<NewsBlur::StarredGroup>("NewsBlur", 1, 0, "StarredGroup");
    qmlRegisterType<NewsBlur::StoryItem>("NewsBlur", 1, 0, "StoryItem");
    qmlRegisterUncreatableType<NewsBlur::NewsBlurEnum>("NewsBlur", 1, 0, "NewsBlurEnum", "NewsBlurEnum can't be created.");

    //initialize the unread count update timer
#ifdef QTM_USE_NAMESPACE
    m_unreadCountUpdateTimer.setMinimumInterval(NewsBlurService::s_unreadCountUpdateIntervalMinMs);
    m_unreadCountUpdateTimer.setMaximumInterval(NewsBlurService::s_unreadCountUpdateIntervalMaxMs);
#else
    m_unreadCountUpdateTimer.setInterval(((NewsBlurService::s_unreadCountUpdateIntervalMaxMs
                                          + NewsBlurService::s_unreadCountUpdateIntervalMinMs) / 2) * 1000);
#endif
    m_client->setPreferredFaviconSize(QSize(NewsBlurService::s_itemIconWidth, NewsBlurService::s_itemIconHeight));
    connect(&m_unreadCountUpdateTimer, SIGNAL(timeout()),
            this, SLOT(onUnreadCountUpdateTimerTimeout()));
    connect(m_client, SIGNAL(requestCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)),
            this, SLOT(onRequestCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)));
    connect(m_client, SIGNAL(requestFinished(WebServiceReplyPtr)),
            this, SLOT(onRequestFinished(WebServiceReplyPtr)));
    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*)));
}

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

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

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

QStringList NewsBlurService::menuItems(PageItemPtr item) const
{
    QStringList ret;
    NewsBlurItemPtr newsBlurItem = item.dynamicCast<NewsBlurItem>();
    if(newsBlurItem)
    {
        if(newsBlurItem->itemType() == NewsBlurItem::RootItem)
        {
            ret << NewsBlurService::s_newsBlurHelpMenuItemName;
        }
        else if(newsBlurItem->itemType() == NewsBlurItem::StoryItem)
        {
            NewsBlurItemPtr parentItem = item->parentItem().toStrongRef().staticCast<NewsBlurItem>();
            StoryItemPtr storyItem = item.staticCast<StoryItem>();
            if(parentItem
                && (parentItem->itemType() != NewsBlurItem::StarredGroup))
            {
                ret << NewsBlurService::s_intelligenceTrainerMenuItemName;
                if(storyItem && storyItem->isRead())
                    ret << NewsBlurService::s_markStoryAsUnreadMenuItemName;                
            }
        }
        else if(newsBlurItem->itemType() == NewsBlurItem::FeedItem)
        {
            ret << NewsBlurService::s_intelligenceTrainerMenuItemName;
            ret << NewsBlurService::s_renameMenuItemName;
            ret << NewsBlurService::s_moveMenuItemName;
            ret << NewsBlurService::s_deleteMenuItemName;
        }
        else if(newsBlurItem->itemType() == NewsBlurItem::FolderItem)
        {
            ret << NewsBlurService::s_renameMenuItemName;
            ret << NewsBlurService::s_moveMenuItemName;
            ret << NewsBlurService::s_deleteMenuItemName;
        }
    }
    return ret;
}

QStringList NewsBlurService::contextMenuItems(PageItemPtr item) const
{
    QStringList ret;
    NewsBlurItemPtr newsBlurItem = item.staticCast<NewsBlurItem>();
    if(newsBlurItem)
    {
        if(newsBlurItem->itemType() == NewsBlurItem::StoryItem)
        {
            NewsBlurItemPtr parentItem = item->parentItem().toStrongRef().staticCast<NewsBlurItem>();
            StoryItemPtr storyItem = item.staticCast<StoryItem>();
            if((parentItem
                && (parentItem->itemType() != NewsBlurItem::StarredGroup))
                && storyItem && storyItem->isRead())
                ret << NewsBlurService::s_markStoryAsUnreadMenuItemName;                        
            if(!storyItem->isStarred())
                ret << NewsBlurService::s_starStoryMenuItemName;
            else
                ret << NewsBlurService::s_unstarStoryMenuItemName;
            ret << NewsBlurService::s_shareStoryMenuItemName;
        }
        else if(newsBlurItem->itemType() == NewsBlurItem::FeedItem)
        {
            FeedItemPtr feedItem = item.staticCast<FeedItem>();
            if(feedItem && !feedItem->isRead())
                ret << NewsBlurService::s_markFeedAsReadMenuItemName;
            ret << NewsBlurService::s_renameMenuItemName;
            ret << NewsBlurService::s_moveMenuItemName;
            ret << NewsBlurService::s_deleteMenuItemName;
        }
        else if(newsBlurItem->itemType() == NewsBlurItem::FolderItem)
        {
            ret << NewsBlurService::s_renameMenuItemName;
            ret << NewsBlurService::s_moveMenuItemName;
            ret << NewsBlurService::s_deleteMenuItemName;
        }
    }
    return ret;
}

bool NewsBlurService::isItemAcceptable(PageItemPtr item)
{
    bool ret = true;
    StoryItemPtr storyItem = item.dynamicCast<StoryItem>();
    if(storyItem)
    {
        PageItemPtr parentItem = storyItem->parentItem().toStrongRef();
        if(parentItem != m_starredGroupItem)
            ret = NewsBlurEnum::filterAcceptsClassification(NewsBlurEnum::ClassificationFilter(filterType()),
                                                            NewsBlurEnum::ClassifierValue(storyItem->itemClassification()));
    }
    return ret;
}

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

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

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

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

void NewsBlurService::enable(PageItemPtr parentItem)
{
    //add the root item of the NewsBlur tree to parentItem
    m_rootItem = RootItemPtr(new RootItem(parentItem, this));
    m_rootItem->setTitle(NewsBlurService::s_serviceName);
    m_rootItem->setItemId(QString());
    m_rootItem->setIconSource(imageUrl(NewsBlurService::s_serviceIconId));
    parentItem->appendChild(m_rootItem);
    registerItem(m_rootItem);
}

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

void NewsBlurService::disable()
{
    //stop refreshing unread counts
    m_unreadCountUpdateTimer.stop();
    //abort all running requests
    abortAllRequests();
    //clear all members
    m_feedItems.clear();
    m_unreadCountsOutOfDate = false;
    m_feedFavicons.clear();
    m_starredGroupItem.clear();
    m_everythingGroupItem.clear();
    m_serviceBusy = false;
    //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 NewsBlurService::serviceId() const
{
    return NewsBlurService::s_serviceId;
}

QSet<QString> NewsBlurService::customItemTypeProperties() const
{
    QSet<QString> ret;
    ret += AbstractFeedAggreagatorService::itemTypeProperties<NewsBlurItem>();
    ret += AbstractFeedAggreagatorService::itemTypeProperties<FolderItem>();
    ret += AbstractFeedAggreagatorService::itemTypeProperties<RootItem>();
    ret += AbstractFeedAggreagatorService::itemTypeProperties<FeedItem>();
    ret += AbstractFeedAggreagatorService::itemTypeProperties<StarredGroup>();
    ret += AbstractFeedAggreagatorService::itemTypeProperties<EverythingGroup>();
    ret += AbstractFeedAggreagatorService::itemTypeProperties<StoryItem>();
    return ret;
}

/*!
  Executes the appropriate operation when the given \a item is activated.
*/
void NewsBlurService::onItemActivated(PageItemPtr item)
{
    NewsBlurItemPtr itemPtr = item.staticCast<NewsBlurItem>();
    if(itemPtr)
    {
        if(itemPtr->itemType() == NewsBlurItem::RootItem)
        {
            //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(itemPtr->itemType() == NewsBlurItem::FeedItem)
        {
            FeedItemPtr feedItem = item.staticCast<FeedItem>();
            //if new stories have appeared for the feed and it's not empty then update it
            if(feedItem->isOutOfDate() && (feedItem->childCount() > 0))
                loadNewFeedStories(feedItem);
        }
        else if(itemPtr->itemType() == NewsBlurItem::StoryItem)
        {
            StoryItemPtr storyItem = item.staticCast<StoryItem>();
            //mark the story is read if it's marked as unread
            if(!storyItem->isRead() && isRequestAllowed(NewsBlurClient::TpMarkStoryRead, storyItem))
            {
                WebServiceReplyPtr reply = m_client->markStoryAsRead(storyItem->feedId(), storyItem->itemId());
                connect(reply.data(), SIGNAL(finished(WebServiceReplyPtr)),
                        this, SLOT(onMarkStoryAsReadFinished(WebServiceReplyPtr)));
                addRequest(storyItem, reply);
            }
        }
        else if(itemPtr->itemType() == NewsBlurItem::StarredGroup)
        {
            //if new starred stories have appeared and the starred feed is not empty then update it
            StarredGroupPtr starredGroupItem =  item.staticCast<StarredGroup>();
            if(starredGroupItem->isOutOfDate() && (starredGroupItem->childCount() > 0))
                loadNewStarredStories();
        }
        if(m_unreadCountsOutOfDate)
            updateUnreadCounts();
    }
}

/*!
  Executes the appropriate operation when the given \a item is deactivated.
*/
void NewsBlurService::onItemDeactivated(PageItemPtr item)
{
    if(item)
    {
        NewsBlurItemPtr newsBlurItem = item.staticCast<NewsBlurItem>();
        abortRequest(0, WebServiceReply::RlUpdate, newsBlurItem);
        if(newsBlurItem->itemType() == NewsBlurItem::EverythingGroup)
        {
            //clear the everything feed on deactivation
            m_everythingGroupItem->clearChildItems();
            m_everythingGroupItem->markAsOutOfDate(false);
            m_everythingGroupItem->setCanFetchMore(true);
        }
        else if((newsBlurItem->itemType() == NewsBlurItem::FeedItem)
                || (newsBlurItem->itemType() == NewsBlurItem::StarredGroup))
        {
            //make sure that the number of cached stories is within the defined limit
            trimCachedItems();
        }
    }
}

/*!
  Requests additional child items for the given \a parentItem.
*/
void NewsBlurService::onMoreChildrenRequested(PageItemPtr parentItem)
{
    NewsBlurItemPtr parentItemPtr = parentItem.staticCast<NewsBlurItem>();
    if(parentItemPtr)
    {
        if(parentItemPtr->itemType() == NewsBlurItem::FeedItem)
        {
            FeedItemPtr feedItem =  parentItem.staticCast<FeedItem>();
            loadMoreFeedStories(feedItem);
        }
        else if(parentItemPtr->itemType() == NewsBlurItem::StarredGroup)
        {
            loadMoreStarredStories();
        }
        else if(parentItemPtr->itemType() == NewsBlurItem::EverythingGroup)
        {
            loadMoreRiverStories();
        }
    }
}

/*!
  Calls the onItemActivated or the onItemDeactivated function depending on
  the \a active parameter for the given \a item.
*/
void NewsBlurService::onActivityStatusChanged(PageItemPtr item, bool active)
{
    if(active)
        onItemActivated(item);
    else
        onItemDeactivated(item);
}

/*!
  If the feed with the given \a feedId is active,
  it will be updated, otherwise it will be marked
  as out of date.
*/
void NewsBlurService::onFeedOutOfDate(int feedId)
{
    foreach(FeedItemPtr feedItemPtr, m_feedItems.values(feedId))
    {
        feedItemPtr->markAsOutOfDate(true);
        if(feedItemPtr->isActive() && (feedItemPtr->childCount() > 0))
            loadNewFeedStories(feedItemPtr);
    }
}

/*!
  If the everything feed is active it will be either
  updated or older stories will be loaded for it
  depending on wether is empty or not.
*/
void NewsBlurService::onRiverOutOfDate()
{
    if(m_everythingGroupItem->isActive())
    {
        if(m_everythingGroupItem->childCount() > 0)
            loadNewRiverStories();
        else
            loadMoreRiverStories();
    }
}

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

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

/*!
  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 NewsBlurService::updateUnreadCounts(bool resetTimer)
{
#ifdef QTM_USE_NAMESPACE
    if(resetTimer)
        m_unreadCountUpdateTimer.wokeUp();
#else
    Q_UNUSED(resetTimer);
#endif
    if(m_rootItem)
    {
        m_unreadCountsOutOfDate = true;
        if(m_rootItem->isActive())
        {
            if(isRequestAllowed(NewsBlurClient::TpRefreshUnreadCounts, NewsBlurItemPtr()))
            {
                WebServiceReplyPtr reply = m_client->updateUnreadCounts();
                connect(reply.data(), SIGNAL(finished(WebServiceReplyPtr)),
                        this, SLOT(onUpdateUnreadCountsFinished(WebServiceReplyPtr)));
                addRequest(NewsBlurItemPtr(), reply);
            }
        }
    }
}

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

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

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

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

bool NewsBlurService::isBusy() const
{
    return m_serviceBusy;
}

void NewsBlurService::setBusy(bool busy)
{
    m_serviceBusy = busy;
    emit busyChanged();
}

bool NewsBlurService::markingEverythingReadBusy() const
{
    return m_markingEverythingReadBusy;
}

void NewsBlurService::setMarkingEverythingReadBusy(bool busy)
{
    m_markingEverythingReadBusy = busy;
    emit markingEverythingReadBusyChanged();
}

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

void NewsBlurService::clearAuthenticationData()
{
    clearPluginSettings();
    emit authenticationDataChanged(this);
}

/*!
  Starts the operation which marks the \a story as
  starred or saved.
*/
void NewsBlurService::saveStory(NewsBlur::StoryItem *story)
{    
    if(story)
    {
        StoryItemPtr storyItem = StoryItemWeakRef(story).toStrongRef();
        if(isRequestAllowed(NewsBlurClient::TpSaveStory, storyItem))
        {
            WebServiceReplyPtr reply = m_client->saveStory(storyItem->feedId(), storyItem->itemId());
            QList<StoryItemPtr> stories = matchingStoryItems(storyItem->feedId(), storyItem->itemId());
            foreach(StoryItemPtr matchingStory, stories)
                assingBusyPropertyToRequest(reply, matchingStory, NewsBlurService::s_starredBusyPropertyName);
            connect(reply.data(), SIGNAL(finished(WebServiceReplyPtr)),
                    this, SLOT(onSaveStoryFinished(WebServiceReplyPtr)));
            connect(reply.data(), SIGNAL(failed(WebServiceReplyPtr)),
                    this, SLOT(onCommitRequestFailed(WebServiceReplyPtr)));
            addRequest(storyItem, reply);
        }
    }
}

/*!
  Aborts the operation which marks the \a story as
  starred or saved.
*/
void NewsBlurService::unSaveStory(NewsBlur::StoryItem *story)
{
    if(story)
    {
        StoryItemPtr storyItem = StoryItemWeakRef(story).toStrongRef();
        if(isRequestAllowed(NewsBlurClient::TpUnsaveStory, storyItem))
        {
            WebServiceReplyPtr reply = m_client->unSaveStory(storyItem->itemId());
            QList<StoryItemPtr> stories = matchingStoryItems(storyItem->feedId(), storyItem->itemId());
            foreach(StoryItemPtr matchingStory, stories)
                assingBusyPropertyToRequest(reply, matchingStory, NewsBlurService::s_starredBusyPropertyName);
            connect(reply.data(), SIGNAL(finished(WebServiceReplyPtr)),
                    this, SLOT(onUnsaveStoryFinished(WebServiceReplyPtr)));
            connect(reply.data(), SIGNAL(failed(WebServiceReplyPtr)),
                    this, SLOT(onCommitRequestFailed(WebServiceReplyPtr)));
            addRequest(storyItem, reply);
        }
    }
}

/*!
  Starts the operation which marks all the stories
  of the \a feed as read.
*/
void NewsBlurService::markFeedAsRead(NewsBlur::FeedItem* feed)
{
    if(feed)
    {
        FeedItemPtr feedItem = FeedItemWeakRef(feed).toStrongRef();
        if(isRequestAllowed(NewsBlurClient::TpMarkFeedRead, feedItem))
        {
            WebServiceReplyPtr reply = m_client->markFeedAsRead(feedItem->itemId().toInt());
            assingBusyPropertyToRequest(reply, feedItem, NewsBlurService::s_feedReadBusyPropertyName);
            connect(reply.data(), SIGNAL(finished(WebServiceReplyPtr)),
                    this, SLOT(onMarkFeedReadFinished(WebServiceReplyPtr)));
            connect(reply.data(), SIGNAL(failed(WebServiceReplyPtr)),
                    this, SLOT(onCommitRequestFailed(WebServiceReplyPtr)));
            addRequest(feedItem, reply);
        }
    }
}

/*!
  Starts the operation whichc marks the stories of the child
  feeds of the \a folder as read.
*/
void NewsBlurService::markFolderAsRead(NewsBlur::FolderItem *folder)
{
    if(folder)
    {
        FolderItemPtr folderItem = FolderItemWeakRef(folder).toStrongRef();
        if(isRequestAllowed(NewsBlurClient::TpMarkFolderRead, folderItem))
        {
            QList<FeedItemPtr> feedItems = unreadFeeds(folderItem);
            QList<int> feedIds;
            foreach(FeedItemPtr feedItem, feedItems)
                feedIds << feedItem->itemId().toInt();
            WebServiceReplyPtr reply = m_client->markFolderAsRead(feedIds);
            assingBusyPropertyToRequest(reply, folderItem, NewsBlurService::s_feedReadBusyPropertyName);
            connect(reply.data(), SIGNAL(finished(WebServiceReplyPtr)),
                    this, SLOT(onMarkFolderReadFinished(WebServiceReplyPtr)));
            connect(reply.data(), SIGNAL(failed(WebServiceReplyPtr)),
                    this, SLOT(onCommitRequestFailed(WebServiceReplyPtr)));
            addRequest(folderItem, reply);
        }
    }
}

/*!
  Starts the operation which marks every story of
  every feed as read.
*/
void NewsBlurService::markEverythingAsRead()
{
    if(isRequestAllowed(NewsBlurClient::TpMarkEverythingRead, m_rootItem))
    {
        WebServiceReplyPtr reply = m_client->markEverythingAsRead();
        setMarkingEverythingReadBusy(true);
        assingBusyPropertyToRequest(reply, m_rootItem, NewsBlurService::s_feedReadBusyPropertyName);
        connect(reply.data(), SIGNAL(completed(WebServiceReplyPtr,WebServiceReply::CompletedState)),
                this, SLOT(onMarkEverythingReadCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)));
        connect(reply.data(), SIGNAL(failed(WebServiceReplyPtr)),
                this, SLOT(onCommitRequestFailed(WebServiceReplyPtr)));
        addRequest(m_rootItem, reply);
    }
}

/*!
  Starts the operation which marks the \a story as unread.
*/
void NewsBlurService::markStoryAsUnread(NewsBlur::StoryItem *story)
{
    if(story)
    {
        StoryItemPtr storyItem = StoryItemWeakRef(story).toStrongRef();
        StarredGroupPtr storyStarredGroupParent = story->parentItem().toStrongRef().dynamicCast<StarredGroup>();
        if(storyStarredGroupParent)
        {
            storyItem = m_feedItems.value(storyItem->feedId())->childItem(storyItem->itemId()).staticCast<StoryItem>();
        }
        if(storyItem->isRead() && isRequestAllowed(NewsBlurClient::TpMarkStoryUnread, storyItem))
        {
            WebServiceReplyPtr reply = m_client->markStoryAsUnread(storyItem->feedId(), storyItem->itemId());
            connect(reply.data(), SIGNAL(finished(WebServiceReplyPtr)),
                    this, SLOT(onMarkStoryAsUnreadFinished(WebServiceReplyPtr)));
            addRequest(storyItem, reply);
        }
    }
}

/*!
  Returns the list of feed items in the \a parent folder
  which contain unread stories.
*/
QList<FeedItemPtr> NewsBlurService::unreadFeeds(FolderItemPtr parent) const
{
    QList<FeedItemPtr> ret;
    QList<FolderItemPtr> folders = childFolders(parent);
    folders << parent;
    foreach(FeedItemPtr feedItem, m_feedItems.values())
    {
        if(folders.contains(feedItem->parentItem().toStrongRef().staticCast<FolderItem>())
            && !feedItem->isRead())
        {
            ret << feedItem;
        }
    }
    return ret;
}

/*!
  Returns the list of child folders of the \a parent folder.
*/
QList<FolderItemPtr> NewsBlurService::childFolders(FolderItemPtr parent) const
{
    QList<FolderItemPtr> ret;
    RootItemPtr rootItemParent = parent.staticCast<RootItem>();
    if(rootItemParent && (rootItemParent == m_rootItem))
    {
        ret = m_folderItems;
    }
    else
    {
        for(int i = 0; i < parent->childCount(); ++i)
        {
            FolderItemPtr childFolderItem = parent->childItem(i).staticCast<FolderItem>();
            if(childFolderItem)
            {
                ret << childFolderItem;
                ret << childFolders(childFolderItem);
            }
        }
    }
    return ret;
}

/*!
  Updates the unread counts of the \a folderItem based on the unread counts
  of it's child feeds. To do a complete update starting from the root item
  the function should be called without any parameters.
*/
void NewsBlurService::updateFolderUnreadCounts(FolderItemPtr folderItem,
                                               int *unreadCountSum,
                                               int *posUnreadCoundSum,
                                               int *negUnreadCountSum)
{
    if(folderItem == 0)
        folderItem = m_rootItem;
    if(folderItem != 0)
    {
        int unreadCount = 0;
        int posUnreadCount = 0;
        int negUnreadCount = 0;
        foreach(PageItemPtr item, folderItem->childItems())
        {
            NewsBlurItemPtr childItem = item.staticCast<NewsBlurItem>();
            if(childItem->itemType() == NewsBlurItem::FeedItem)
            {
                FeedItemPtr feedChildItem = item.staticCast<FeedItem>();
                unreadCount += feedChildItem->unreadCount();
                posUnreadCount += feedChildItem->posUnreadCount();
                negUnreadCount += feedChildItem->negUnreadCount();
            }
            else if(childItem->itemType() == NewsBlurItem::FolderItem)
            {                
                FolderItemPtr folderChildItem = item.staticCast<FolderItem>();
                updateFolderUnreadCounts(folderChildItem, &unreadCount, &posUnreadCount, &negUnreadCount);
            }
        }
        if(unreadCountSum && posUnreadCoundSum && negUnreadCountSum)
        {
            *unreadCountSum += unreadCount;
            *posUnreadCoundSum += posUnreadCount;
            *negUnreadCountSum += negUnreadCount;
        }        

        RootItemPtr rootItem = folderItem.staticCast<RootItem>();
        if(rootItem && (rootItem == m_rootItem))
        {
            unreadCount = 0;
            posUnreadCount = 0;
            negUnreadCount = 0;
            foreach(int feedId, m_feedItems.uniqueKeys())
            {
                FeedItemPtr feed = m_feedItems.value(feedId);
                unreadCount += feed->unreadCount();
                posUnreadCount += feed->posUnreadCount();
                negUnreadCount += feed->negUnreadCount();
            }
            m_everythingGroupItem->setUnreadCount(unreadCount);
            m_everythingGroupItem->setPosUnreadCount(posUnreadCount);
            m_everythingGroupItem->setNegUnreadCount(negUnreadCount);
        }
        folderItem->setUnreadCount(unreadCount);
        folderItem->setPosUnreadCount(posUnreadCount);
        folderItem->setNegUnreadCount(negUnreadCount);
    }
}

/*!
  Starts the operation which sets the name of the given feed or folder \a item
  to the given \a name.
*/
void NewsBlurService::renameItem(NewsBlur::NewsBlurItem *item, const QString &name)
{
    if(item && !name.isEmpty() && (item->title() != name))
    {
        NewsBlurItemPtr newsBlurItem = NewsBlurItemWeakRef(item).toStrongRef();
        NewsBlurItemPtr parentItem = newsBlurItem->parentItem().toStrongRef().staticCast<NewsBlurItem>();
        if(newsBlurItem->itemType() == NewsBlurItem::FeedItem)
        {
            if(isRequestAllowed(NewsBlurClient::TpRenameFeed, newsBlurItem))
            {
                WebServiceReplyPtr reply = m_client->renameFeed(newsBlurItem->itemId().toInt(), name);
                connect(reply.data(), SIGNAL(completed(WebServiceReplyPtr, WebServiceReply::CompletedState)),
                        this, SLOT(onItemRenameCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)));
                addRequest(newsBlurItem, reply);
            }
        }
        else if(newsBlurItem->itemType() == NewsBlurItem::FolderItem)
        {
            if(isRequestAllowed(NewsBlurClient::TpRenameFolder, newsBlurItem))
            {
                WebServiceReplyPtr reply = m_client->renameFolder(parentItem->itemId(), newsBlurItem->itemId(), name);
                connect(reply.data(), SIGNAL(completed(WebServiceReplyPtr, WebServiceReply::CompletedState)),
                        this, SLOT(onItemRenameCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)));
                addRequest(newsBlurItem, reply);
            }
        }
    }
}

/*!
  Aborts the operation whichc renames the given feed or folder
  \a item.
*/
void NewsBlurService::abortItemRename(NewsBlur::NewsBlurItem *item)
{
    NewsBlurItemPtr newsBlurItem = NewsBlurItemWeakRef(item).toStrongRef();
    if(newsBlurItem->itemType() == NewsBlurItem::FeedItem)
        abortRequest(NewsBlurClient::TpRenameFeed, WebServiceReply::RlUndefined, newsBlurItem);
    else if(newsBlurItem->itemType() == NewsBlurItem::FolderItem)
        abortRequest(NewsBlurClient::TpRenameFolder, WebServiceReply::RlUndefined, newsBlurItem);
}

/*!
  Starts the operation which removes the given feed or folder \a item.
*/
void NewsBlurService::deleteItem(NewsBlur::NewsBlurItem *item)
{
    if(item)
    {
        NewsBlurItemPtr newsBlurItem = NewsBlurItemWeakRef(item).toStrongRef();
        NewsBlurItemPtr parentItem = newsBlurItem->parentItem().toStrongRef().staticCast<NewsBlurItem>();
        if(newsBlurItem->itemType() == NewsBlurItem::FeedItem)
        {
            if(isRequestAllowed(NewsBlurClient::TpDeleteFeed, newsBlurItem))
            {
                WebServiceReplyPtr reply = m_client->deleteFeed(newsBlurItem->itemId().toInt(), parentItem->itemId());
                assingBusyPropertyToRequest(reply, newsBlurItem, NewsBlurService::s_deletingBusyPropertyName);
                connect(reply.data(), SIGNAL(failed(WebServiceReplyPtr)),
                        this, SLOT(onCommitRequestFailed(WebServiceReplyPtr)));
                addRequest(newsBlurItem, reply);
            }
        }
        else if(newsBlurItem->itemType() == NewsBlurItem::FolderItem)
        {
            if(isRequestAllowed(NewsBlurClient::TpDeleteFolder, newsBlurItem))
            {
                WebServiceReplyPtr reply = m_client->deleteFolder(newsBlurItem->itemId(), parentItem->itemId());
                assingBusyPropertyToRequest(reply, newsBlurItem, NewsBlurService::s_deletingBusyPropertyName);
                connect(reply.data(), SIGNAL(failed(WebServiceReplyPtr)),
                        this, SLOT(onCommitRequestFailed(WebServiceReplyPtr)));
                addRequest(newsBlurItem, reply);
            }
        }
    }
}

/*!
  Starts the operation which moves the given feed or folder \a item to
  a new parent folder specified by \a destItem.
*/
void NewsBlurService::moveItem(NewsBlur::NewsBlurItem *item, NewsBlur::FolderItem* destItem)
{
    if(item && destItem)
    {
        NewsBlurItemPtr newsBlurItem = NewsBlurItemWeakRef(item).toStrongRef();
        NewsBlurItemPtr parentItem = newsBlurItem->parentItem().toStrongRef().dynamicCast<NewsBlurItem>();
        NewsBlurItemPtr destFolderItem = FolderItemWeakRef(destItem).toStrongRef();
        if(newsBlurItem->itemType() == NewsBlurItem::FeedItem)
        {
            if(isRequestAllowed(NewsBlurClient::TpMoveFeed, newsBlurItem))
            {
                WebServiceReplyPtr reply = m_client->moveFeed(newsBlurItem->itemId().toInt(), parentItem->itemId(), destFolderItem->itemId());
                connect(reply.data(), SIGNAL(failed(WebServiceReplyPtr)),
                        this, SLOT(onCommitRequestFailed(WebServiceReplyPtr)));
                assingBusyPropertyToRequest(reply, newsBlurItem, NewsBlurService::s_movingBusyPropertyName);
                addRequest(newsBlurItem, reply);
            }
        }
        else if(newsBlurItem->itemType() == NewsBlurItem::FolderItem)
        {
            if(isRequestAllowed(NewsBlurClient::TpMoveFolder, newsBlurItem))
            {
                WebServiceReplyPtr reply = m_client->moveFolder(newsBlurItem->itemId(), parentItem->itemId(), destFolderItem->itemId());
                connect(reply.data(), SIGNAL(failed(WebServiceReplyPtr)),
                        this, SLOT(onCommitRequestFailed(WebServiceReplyPtr)));
                assingBusyPropertyToRequest(reply, newsBlurItem, NewsBlurService::s_movingBusyPropertyName);
                addRequest(newsBlurItem, reply);
            }
        }
    }
}

/*!
  Starts the operation which adds a new folder with the given \a name as
  a child of \a parentItem.
*/
void NewsBlurService::addFolder(NewsBlur::FolderItem *parentItem, const QString &name)
{
    if(parentItem && !name.isEmpty())
    {
        NewsBlurItemPtr parentFolderItem = FolderItemWeakRef(parentItem).toStrongRef();
        if(isRequestAllowed(NewsBlurClient::TpAddFolder, parentFolderItem))
        {
            WebServiceReplyPtr reply = m_client->addFolder(name, parentFolderItem->itemId());
            connect(reply.data(), SIGNAL(completed(WebServiceReplyPtr, WebServiceReply::CompletedState)),
                    this, SLOT(onAddFolderCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)));
            addRequest(parentFolderItem, reply);
        }
    }
}

/*!
  Starts the operation which adds a new feed with the given \a url
  as a child of \a parentItem.
*/
void NewsBlurService::addFeed(NewsBlur::FolderItem *parentItem, const QString &url)
{
    if(parentItem && !url.isEmpty())
    {
        NewsBlurItemPtr parentFolderItem = FolderItemWeakRef(parentItem).toStrongRef();
        if(isRequestAllowed(NewsBlurClient::TpAddFeed, parentFolderItem))
        {
            WebServiceReplyPtr reply = m_client->addFeed(url, parentFolderItem->itemId());
            connect(reply.data(), SIGNAL(completed(WebServiceReplyPtr, WebServiceReply::CompletedState)),
                    this, SLOT(onAddFeedCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)));
            addRequest(parentFolderItem, reply);
        }
    }
}

/*!
  Aborts the operation which adds a new folder item to the \a parentItem.
*/
void NewsBlurService::abortAddFolder(NewsBlur::FolderItem *parentItem)
{
    NewsBlurItemPtr newsBlurItem = NewsBlurItemWeakRef(parentItem).toStrongRef();
    abortRequest(NewsBlurClient::TpAddFolder, WebServiceReply::RlUndefined, newsBlurItem);
}

/*!
  Aborts the operation which adds a new feed item to the \a parentItem.
*/
void NewsBlurService::abortAddFeed(NewsBlur::FolderItem *parentItem)
{
    NewsBlurItemPtr newsBlurItem = NewsBlurItemWeakRef(parentItem).toStrongRef();
    abortRequest(NewsBlurClient::TpAddFeed, WebServiceReply::RlUndefined, newsBlurItem);
}

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

/*!
  Starts the operation which retrieves the feed classifiers for the
  given feed or story \a item.
*/
void NewsBlurService::loadFeedClassifiers(NewsBlur::NewsBlurItem* item)
{
    if(item)
    {
        FeedItemPtr feedItem;
        NewsBlurItemPtr newsBlurItem = NewsBlurItemWeakRef(item).toStrongRef();
        if(item->itemType() == NewsBlurItem::FeedItem)
        {
            feedItem = NewsBlurItemWeakRef(item).toStrongRef().staticCast<FeedItem>();
        }
        else if(item->itemType() == NewsBlurItem::StoryItem)
        {
            StoryItemPtr storyItem = NewsBlurItemWeakRef(item).toStrongRef().staticCast<StoryItem>();
            FeedItemPtr parentFeedItem = storyItem->parentItem().toStrongRef().staticCast<FeedItem>();
            if(parentFeedItem == m_everythingGroupItem)
                feedItem = m_feedItems.value(storyItem->feedId());
            else
                feedItem = parentFeedItem;
            if(feedItem && storyItem && !feedItem->classifiers().isNull())
            {                
                FeedClassifiers classifiers = createStorySpecificFeedClassifiers(storyItem, feedItem->classifiers());
                FeedClassifiersModel* model = new FeedClassifiersModel;
                model->setClassifiers(classifiers);
                model->setFeedTitle(feedItem->title());
                QDeclarativeEngine::setObjectOwnership(model, QDeclarativeEngine::JavaScriptOwnership);
                emit feedClassifiersLoaded(NewsBlurEnum::SreNoError, model);
                feedItem.clear();
            }
        }
        if(feedItem)
        {
            if(isRequestAllowed(NewsBlurClient::TpFeedClassifiers, newsBlurItem))
            {
                FeedClassifiersReplyPtr reply = m_client->feedClassifiers(feedItem->itemId().toInt());
                connect(reply.data(), SIGNAL(completed(WebServiceReplyPtr, WebServiceReply::CompletedState)),
                        this, SLOT(onLoadFeedClassifiersCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)));
                addRequest(newsBlurItem, reply);
            }
        }
    }
}

/*!
  Aborts the operation which retrieves the feed classifiers for the
  given feed or story \a item.
*/
void NewsBlurService::abortLoadingFeedClassifiers(NewsBlur::NewsBlurItem* item)
{
    if(item)
    {
        if(item->itemType() == NewsBlurItem::FeedItem)
        {
            FeedItemPtr feedItem = NewsBlurItemWeakRef(item).toStrongRef().staticCast<FeedItem>();
            abortRequest(NewsBlurClient::TpFeedClassifiers, WebServiceReply::RlUndefined, feedItem);
        }
    }
}

/*!
  Starts the operation which sets the feed classifiers contained in the \a model,
  for the feed with the given \a feedId. In case a story is being classified
  optionally the \a storyId can be specified.
*/
void NewsBlurService::saveFeedClassifications(NewsBlur::FeedClassifiersModel* model, const QString& feedId,
                                              const QString& storyId)
{
    FeedItemPtr feed = m_feedItems.value(feedId.toInt());
    if(feed)
    {
        FeedItemPtr feedItem = FeedItemWeakRef(feed).toStrongRef();
        if(isRequestAllowed(NewsBlurClient::TpSaveClassifiers, feedItem))
        {
            SaveFeedClassifiersReplyPtr reply = m_client->saveClassifiers(model->classifiers(),
                                                                          feedItem->itemId().toInt(),
                                                                          storyId);
            connect(reply.data(), SIGNAL(completed(WebServiceReplyPtr, WebServiceReply::CompletedState)),
                    this, SLOT(onSaveFeedClassificationsCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)));
            addRequest(feedItem, reply);
        }
    }
}

/*!
  Aborts the operation which saves the feed classifiers.
*/
void NewsBlurService::abortSavingFeedClassifications()
{
    abortRequest(NewsBlurClient::TpSaveClassifiers);
}

/*!
  Sets the filter \a type used by the service. The \a currentFeed
  is the feed item currently being viewed. If it contains less
  then the preferred number of stories which match the filter
  \a type more stories will be retrieved.
*/
void NewsBlurService::setFilterType(int type, NewsBlur::FeedItem* currentFeed)
{
    pluginSettings()->setValue(s_filterTypeKeyName, type);
    emit filterChanged();
    emit filterTypeChanged();
    FeedItemPtr currentFeedItem = FeedItemWeakRef(currentFeed).toStrongRef();
    if(currentFeedItem)
    {
        if(currentFeedItem != m_everythingGroupItem)
        {
            abortRequest(NewsBlurClient::TpLoadMoreFeedStories, WebServiceReply::RlUndefined,
                           currentFeedItem);
            if(currentFeedItem->filteredChildCount(NewsBlurEnum::ClassificationFilter(filterType()))
                    < NewsBlurService::s_preferredMinimalStoryCount)
            {
                loadMoreFeedStories(currentFeedItem);
            }
        }
        else
        {
            abortRequest(NewsBlurClient::TpLoadMoreRiverStories, WebServiceReply::RlUndefined,
                           currentFeedItem);
            if(currentFeedItem->filteredChildCount(NewsBlurEnum::ClassificationFilter(filterType()))
                    < NewsBlurService::s_preferredMinimalStoryCount)
            {
                loadMoreRiverStories();
            }
        }
    }
}

/*!
  Return the type of the filter currently being used by the service.
*/
int NewsBlurService::filterType() const
{
    NewsBlurEnum::ClassificationFilter ret = NewsBlurEnum::CfNoFilter;
    QSettings* settings = pluginSettings();
    if(settings->contains(s_filterTypeKeyName))
        ret = NewsBlurEnum::ClassificationFilter(settings->value(s_filterTypeKeyName).toInt());
    return ret;
}

/*!
  Returns the name or title of the feed for the given
  \a feedId.
*/
QString NewsBlurService::feedTitle(int feedId) const
{
    QString ret;
    if(m_feedItems.contains(feedId))
        ret = m_feedItems.value(feedId)->title();
    return ret;
}

/*!
  Removes the loaded stories from the everything feed and tries
  to reload them.
*/
void NewsBlurService::refreshEverythingGroup()
{
    if(m_everythingGroupItem->isActive())
    {
        abortRequest(NewsBlurClient::TpUnknown, WebServiceReply::RlUndefined, m_everythingGroupItem);
        m_everythingGroupItem->clearChildItems();
        m_everythingGroupItem->setCanFetchMore(true);
        onRiverOutOfDate();
    }
}

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

/*!
  Requests additional pages of older stories for the given
  \a feedItem.
*/
void NewsBlurService::loadMoreFeedStories(FeedItemPtr feedItem)
{
    if((feedItem->childCount() == 0) && (m_feedItems.count(feedItem->itemId().toInt()) > 1))
    {
        foreach(FeedItemPtr nonEmptyFeed, m_feedItems.values(feedItem->itemId().toInt()))
        {
            if(nonEmptyFeed->childCount() > 0)
            {
                QList<PageItemPtr> stories = nonEmptyFeed->childItems();
                nonEmptyFeed->clearChildItems();
                feedItem->setChildItems(stories);
                feedItem->setCanFetchMore(nonEmptyFeed->canFetchMore());
                feedItem->markAsOutOfDate(nonEmptyFeed->isOutOfDate());                
                nonEmptyFeed->setCanFetchMore(true);
                break;
            }
        }
    }
    if(isRequestAllowed(NewsBlurClient::TpLoadMoreFeedStories, feedItem)
       && feedItem->canFetchMore())
    {
        NewsBlurEnum::ClassificationFilter filter = NewsBlurEnum::ClassificationFilter(filterType());
        int preferredStoryCount = NewsBlurService::s_preferredStoryCountPerRequest;
        if(filter != NewsBlurEnum::CfNoFilter)
            preferredStoryCount = NewsBlurService::s_preferredFilteredStoryCountPerRequest;
        int loadedPages = (feedItem->childCount() / NewsBlurService::s_feedStoriesPerPage);
        FeedStoryListReplyPtr reply = m_client->loadFeedStories(feedItem->itemId().toInt(),
                                                                loadedPages,
                                                                feedItem->classifiers().isNull(),
                                                                feedItem->childItemIds(),
                                                                filter,
                                                                preferredStoryCount);
        connect(reply.data(), SIGNAL(completed(WebServiceReplyPtr,WebServiceReply::CompletedState)),
                this, SLOT(onLoadFeedStoriesCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)));
        addRequest(feedItem, reply);
        assingBusyPropertyToRequest(reply, feedItem, NewsBlurService::s_busyPropertyName);
    }
}

/*!
  Requests newly appeared stories for the given \a feedItem.
*/
void NewsBlurService::loadNewFeedStories(FeedItemPtr feedItem)
{
    if(isRequestAllowed(NewsBlurClient::TpLoadNewFeedStories, feedItem))
    {
        FeedStoryListReplyPtr reply = m_client->loadNewFeedStories(feedItem->itemId().toInt(),
                                                             feedItem->childItemIds());
        connect(reply.data(), SIGNAL(finished(WebServiceReplyPtr)),
                this, SLOT(onLoadNewFeedStoriesFinished(WebServiceReplyPtr)));
        assingBusyPropertyToRequest(reply, feedItem, NewsBlurService::s_busyPropertyName);
        addRequest(feedItem, reply);
    }
}

/*!
  Requests additional pages of older unread stories for all the
  feeds which might contain unread stories.
*/
void NewsBlurService::loadMoreRiverStories()
{
    if(isRequestAllowed(NewsBlurClient::TpLoadMoreRiverStories, m_everythingGroupItem)
       && m_everythingGroupItem->canFetchMore())
    {
        StoryListReplyPtr reply;
        QList<int> feedIds;
        foreach(FeedItemPtr feed, m_feedItems.values())
        {
            if((!feedIds.contains(feed->itemId().toInt()))
                && !feed->isRead())
            {
                feedIds << feed->itemId().toInt();
            }
        }
        NewsBlurEnum::ClassificationFilter filter = NewsBlurEnum::ClassificationFilter(filterType());
        int preferredStoryCount = NewsBlurService::s_preferredStoryCountPerRequest;
        if(filter != NewsBlurEnum::CfNoFilter)
            preferredStoryCount = NewsBlurService::s_preferredFilteredStoryCountPerRequest;

        int loadedPages = (m_everythingGroupItem->unreadChildCount() / NewsBlurService::s_riverStoriesPerPage);
        reply = m_client->loadRiverStories(loadedPages,
                                           m_everythingGroupItem->childItemIds(),
                                           feedIds,
                                           NewsBlurEnum::ClassificationFilter(filterType()),
                                           preferredStoryCount);
        connect(reply.data(), SIGNAL(completed(WebServiceReplyPtr,WebServiceReply::CompletedState)),
                this, SLOT(onLoadRiverStoriesCompleted(WebServiceReplyPtr,WebServiceReply::CompletedState)));
        addRequest(m_everythingGroupItem, reply);
        assingBusyPropertyToRequest(reply, m_everythingGroupItem, NewsBlurService::s_busyPropertyName);
    }
}

/*!
  Requests newly appeared unread stories for all the
  feeds which might contain unread stories.
*/
void NewsBlurService::loadNewRiverStories()
{
    if(isRequestAllowed(NewsBlurClient::TpUpdateRiverStories, m_everythingGroupItem)
       && (m_everythingGroupItem->childCount() > 0))
    {
        QList<int> feedIds;
        foreach(FeedItemPtr feed, m_feedItems.values())
        {
            if((!feedIds.contains(feed->itemId().toInt()))
                && !feed->isRead())
            {
                feedIds << feed->itemId().toInt();
            }
        }
        StoryItemPtr latestStory = m_everythingGroupItem->childItem(0).staticCast<StoryItem>();
        StoryListReplyPtr storySetReply = m_client->loadNewRiverStories(m_everythingGroupItem->childItemIds(),
                                                                        feedIds,
                                                                        latestStory->date());
        connect(storySetReply.data(), SIGNAL(finished(WebServiceReplyPtr)),
                this, SLOT(onLoadNewRiverStoriesFinished(WebServiceReplyPtr)));
        addRequest(m_everythingGroupItem, storySetReply);
    }
}

/*!
  Requests additional pages of older starred stories.
*/
void NewsBlurService::loadMoreStarredStories()
{
    if(isRequestAllowed(NewsBlurClient::TpLoadMoreStarredStories, m_starredGroupItem))
    {
        int loadedPages = (m_starredGroupItem->childCount() / NewsBlurService::s_savedStoriesPerPage);
        StoryListReplyPtr reply = m_client->loadStarredStories(loadedPages,
                                                               m_starredGroupItem->childItemIds());
        connect(reply.data(), SIGNAL(finished(WebServiceReplyPtr)),
                this, SLOT(onLoadStarredStoriesFinished(WebServiceReplyPtr)));
        addRequest(m_starredGroupItem, reply);
        assingBusyPropertyToRequest(reply, m_starredGroupItem, NewsBlurService::s_busyPropertyName);
    }
}

/*!
  Requests newly appeared starred stories.
*/
void NewsBlurService::loadNewStarredStories()
{
    if(isRequestAllowed(NewsBlurClient::TpLoadNewStarredStories, m_starredGroupItem))
    {
        StoryListReplyPtr reply = m_client->loadNewStarredStories(m_starredGroupItem->childItemIds());
        connect(reply.data(), SIGNAL(finished(WebServiceReplyPtr)),
                this, SLOT(onLoadNewStarredStoriesFinished(WebServiceReplyPtr)));
        addRequest(m_starredGroupItem, reply);
        assingBusyPropertyToRequest(reply, m_starredGroupItem, NewsBlurService::s_busyPropertyName);
    }
}

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

/*!
  Updates the feed and folder hierarchy of the service based on the contents of the \a reply
  if the completition state is WebServiceReply::CsFinished.
*/
void NewsBlurService::onUpdateTreeCompleted(WebServiceReplyPtr reply, WebServiceReply::CompletedState state)
{
    if(state == WebServiceReply::CsFinished)
    {
        UpdateTreeReplyPtr updateTreeReply = reply.staticCast<UpdateTreeReply>();
        //merge the set of new favicons with the existing ones
        m_feedFavicons.unite(updateTreeReply->feedFavicons());

        QMultiHash<int, FeedItemPtr> newFeedItems = updateTreeReply->feedItems();
        QList<FolderItemPtr> newFolderItems = updateTreeReply->folderItems();

        //true when the set of subscribed feeds changes
        bool feedSetChanged = false;
        //if the root item has some child items, then the existing structure should be updated
        if(m_rootItem->childCount() > 0)
        {
            //check if the set of subscribed feeds changed
            QSet<int> existingFeedSet = m_feedItems.keys().toSet();
            QSet<int> newFeedSet = updateTreeReply->feedItems().keys().toSet();
            feedSetChanged = (existingFeedSet != newFeedSet);

            //move the loaded story items from the existing feed items to the new feed items
            foreach(FeedItemPtr feed, m_feedItems.values())
            {
                if(updateTreeReply->feedItems().contains(feed->itemId().toInt()) && feed->childCount())
                {
                    FeedItemPtr newFeed = updateTreeReply->feedItems().value(feed->itemId().toInt());
                    QList<PageItemPtr> stories = feed->childItems();
                    feed->clearChildItems();
                    newFeed->setChildItems(stories);
                    newFeed->setCanFetchMore(feed->canFetchMore());
                    newFeed->markAsOutOfDate(feed->isOutOfDate());
                }
            }            
            //move the stories of the existing starred feed to the new starred feed
            QList<PageItemPtr> stories = m_starredGroupItem->childItems();
            m_starredGroupItem->clearChildItems();
            updateTreeReply->starredGroup()->setChildItems(stories);

            /*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;
            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))
                    {
                        existingItemCandidate = existingItemChild;
                        newItemCandidate = newItem->children(existingItemChild->itemId()).first();
                    }
                    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());
                        existingItemCandidate->setCanFetchMore(newItemCandidate->canFetchMore());
                        FolderItemPtr folderItem = newItemCandidate.dynamicCast<FolderItem>();
                        FeedItemPtr feedItem = newItemCandidate.dynamicCast<FeedItem>();
                        if(folderItem && newFolderItems.contains(folderItem))
                        {
                            newFolderItems.replace(newFolderItems.indexOf(folderItem),
                                                   existingItemCandidate.staticCast<FolderItem>());

                        }
                        if(feedItem && newFeedItems.values().contains(feedItem))
                        {
                            newFeedItems.remove(feedItem->itemId().toInt(), feedItem);
                            newFeedItems.insertMulti(feedItem->itemId().toInt(), existingItemCandidate.staticCast<FeedItem>());

                        }
                    }
                    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);

            //loop throught the favicons
            foreach(const QString& feedId, m_feedFavicons.keys())
            {
                //if a favicon is found which can't be assigned to any feed then remove it
                if(!m_feedItems.contains(feedId.toInt()) && !updateTreeReply->feedItems().contains(feedId.toInt()))
                    m_feedFavicons.remove(feedId);
            }
        }
        else //if the root item has no children, the tree structure should be created
        {
            QList<PageItemPtr> topLevelItems = updateTreeReply->rootItem()->childItems();
            updateTreeReply->rootItem()->clearChildItems();
            m_rootItem->setChildItems(topLevelItems);
        }
        //update the member data of the service
        m_feedItems = newFeedItems;
        m_folderItems = newFolderItems;
        m_everythingGroupItem = updateTreeReply->everythingGroup();        
        m_starredGroupItem = updateTreeReply->starredGroup();
        registerItem(m_rootItem, true);

        //update the favicons of every feed item
        foreach(FeedItemPtr feedItem, m_feedItems.values())
        {
            //if a favicon exists for the given feed then use it
            if(m_feedFavicons.contains(feedItem->itemId()))
                feedItem->setIconSource(imageUrl(feedItem->itemId()));
            else //if a favicon doesn't exist for the given feed, use the general favicon for feeds
                feedItem->setIconSource(imageUrl(NewsBlurService::s_feedIconId));
        }
        //update the icon of every folder item
        foreach(FolderItemPtr folderItem, m_folderItems)
            folderItem->setIconSource(imageUrl(NewsBlurService::s_folderIconId));
        //set the title and icon for the everything and starred feed
        //since these properties are not initialized by the client
        m_everythingGroupItem->setTitle(tr("Everything"));
        m_everythingGroupItem->setIconSource(imageUrl(NewsBlurService::s_feedIconId));
        m_everythingGroupItem->markAsOutOfDate(false);
        m_starredGroupItem->setTitle(tr("Saved stories"));
        m_starredGroupItem->setIconSource(imageUrl(NewsBlurService::s_starredGroupIconId));

        updateFolderUnreadCounts();
        removeRequest(reply);
        /*if the set of subscribed feed changed, update the unread counts
        because the newly added feed doesn't contains correct unread counts
        until a refresh is called. (odd NewsBlur behavior)
        */
        if(feedSetChanged)
            updateUnreadCounts();
    }
    setBusy(false);
}

/*!
  Appends the loaded story items in \a reply to the related feed item if the completition
  \a state of the \a reply is WebServiceReply::CsFinished or WebServiceReply::CsAborted
*/
void NewsBlurService::onLoadFeedStoriesCompleted(WebServiceReplyPtr reply, WebServiceReply::CompletedState state)
{
    FeedStoryListReplyPtr feedStoryListReply = reply.staticCast<FeedStoryListReply>();
    FeedItemPtr relatedFeedItem = relatedNewsBlurItem(reply).dynamicCast<FeedItem>();
    if(relatedFeedItem
            && ((state == WebServiceReply::CsFinished)
                || ((state == WebServiceReply::CsAborted) && (feedStoryListReply->stories().count() > 0))))
    {
        if(feedStoryListReply->classifiersLoaded())
            relatedFeedItem->setClassifiers(feedStoryListReply->classifiers());
        QList<PageItemPtr> stories = feedStoryListReply->stories();
        foreach(PageItemPtr itemPtr, stories)
            registerItem(itemPtr);
        relatedFeedItem->appendChildren(stories);
        if((stories.count() == 0) && (relatedFeedItem->childCount() > 0))
            relatedFeedItem->setCanFetchMore(false);

        if((state == WebServiceReply::CsFinished) && relatedFeedItem->canFetchMore())
        {
            removeRequest(reply);
            NewsBlurEnum::ClassificationFilter filter = NewsBlurEnum::ClassificationFilter(filterType());
            int filteredStoryCount = relatedFeedItem->filteredChildCount(filter);
            if(filteredStoryCount < NewsBlurService::s_preferredMinimalStoryCount)
                loadMoreFeedStories(relatedFeedItem);
        }
    }
}

/*!
  Prepends the loaded story items in \a reply to the related feed item.
*/
void NewsBlurService::onLoadNewFeedStoriesFinished(WebServiceReplyPtr reply)
{
    FeedStoryListReplyPtr feedStoryListReply = reply.staticCast<FeedStoryListReply>();
    FeedItemPtr relatedFeedItem = relatedNewsBlurItem(reply).dynamicCast<FeedItem>();
    if(relatedFeedItem)
    {
        QList<PageItemPtr> stories = feedStoryListReply->stories();
        foreach(PageItemPtr itemPtr, stories)
        {
            StoryItemPtr storyPtr = itemPtr.dynamicCast<StoryItem>();
            registerItem(storyPtr);
        }
        relatedFeedItem->prependChildren(stories);
        relatedFeedItem->markAsOutOfDate(false);
    }
}

/*!
  Appends the loaded story items in \a reply to the everything feed item if the completition
  \a state of the \a reply is WebServiceReply::CsFinished or WebServiceReply::CsAborted
*/
void NewsBlurService::onLoadRiverStoriesCompleted(WebServiceReplyPtr reply, WebServiceReply::CompletedState state)
{
    StoryListReplyPtr storyListReply = reply.staticCast<StoryListReply>();
    EverythingGroupPtr relatedEverythingGroupItem = relatedNewsBlurItem(reply).dynamicCast<EverythingGroup>();
    if(relatedEverythingGroupItem
            && ((state == WebServiceReply::CsFinished)
                || ((state == WebServiceReply::CsAborted) && (storyListReply->stories().count() > 0))))
    {
        if(relatedEverythingGroupItem->isOutOfDate())
        {
            relatedEverythingGroupItem->clearChildItems();
            relatedEverythingGroupItem->setCanFetchMore(true);
        }
        QList<PageItemPtr> stories = storyListReply->stories();
        foreach(PageItemPtr itemPtr, stories)
            registerItem(itemPtr);
        relatedEverythingGroupItem->appendChildren(stories);
        if((stories.count() == 0) && (relatedEverythingGroupItem->childCount() > 0))
            relatedEverythingGroupItem->setCanFetchMore(false);

        if((state == WebServiceReply::CsFinished) && relatedEverythingGroupItem->canFetchMore()
                && relatedEverythingGroupItem->childCount())
        {
            removeRequest(reply);
            NewsBlurEnum::ClassificationFilter filter = NewsBlurEnum::ClassificationFilter(filterType());
            int filteredStoryCount = relatedEverythingGroupItem->filteredChildCount(filter);
            if(filteredStoryCount < NewsBlurService::s_preferredMinimalStoryCount)
                loadMoreRiverStories();
        }
    }
}

/*!
  Prepends the loaded story items in \a reply to the everything feed item.
*/
void NewsBlurService::onLoadNewRiverStoriesFinished(WebServiceReplyPtr reply)
{
    StoryListReplyPtr storyListReply = reply.staticCast<StoryListReply>();
    EverythingGroupPtr relatedEverythingGroupItem = relatedNewsBlurItem(reply).dynamicCast<EverythingGroup>();
    if(relatedEverythingGroupItem)
    {
        QList<PageItemPtr> stories = storyListReply->stories();
        foreach(PageItemPtr itemPtr, stories)
            registerItem(itemPtr);
        relatedEverythingGroupItem->prependChildren(stories);
    }
}

/*!
  Appends the loaded story items in \a reply to the starred feed item.
*/
void NewsBlurService::onLoadStarredStoriesFinished(WebServiceReplyPtr reply)
{
    StoryListReplyPtr storyListReply = reply.staticCast<StoryListReply>();
    StarredGroupPtr relatedStarredItem = relatedNewsBlurItem(reply).dynamicCast<StarredGroup>();
    if(relatedStarredItem)
    {
        QList<PageItemPtr> stories = storyListReply->stories();
        foreach(PageItemPtr itemPtr, stories)
            registerItem(itemPtr);
        relatedStarredItem->appendChildren(stories);
        if(stories.count() == 0)
            relatedStarredItem->setCanFetchMore(false);
    }
}

/*!
  Prepends the loaded story items in \a reply to the starred feed item.
*/
void NewsBlurService::onLoadNewStarredStoriesFinished(WebServiceReplyPtr reply)
{
    StoryListReplyPtr storyListReply = reply.staticCast<StoryListReply>();
    StarredGroupPtr relatedStarredItem = relatedNewsBlurItem(reply).dynamicCast<StarredGroup>();
    if(relatedStarredItem)
    {
        QList<PageItemPtr> stories = storyListReply->stories();
        foreach(PageItemPtr itemPtr, stories)
            registerItem(itemPtr);
        relatedStarredItem->prependChildren(stories);
        relatedStarredItem->markAsOutOfDate(false);
    }
}

/*!
  Marks the story items related to the \a reply as read.
*/
void NewsBlurService::onMarkStoryAsReadFinished(WebServiceReplyPtr reply)
{
    StoryItemPtr relatedStoryItem = relatedNewsBlurItem(reply).dynamicCast<StoryItem>();
    if(relatedStoryItem)
        markStoryItemsAsRead(relatedStoryItem->feedId(), relatedStoryItem->itemId());
}

/*!
  Marks the story items related to the \a reply as unread.
*/
void NewsBlurService::onMarkStoryAsUnreadFinished(WebServiceReplyPtr reply)
{
    StoryItemPtr relatedStoryItem = relatedNewsBlurItem(reply).dynamicCast<StoryItem>();
    if(relatedStoryItem)
    {
        NewsBlurEnum::ClassifierValue classification
                = NewsBlurEnum::ClassifierValue(relatedStoryItem->itemClassification());
        QList<StoryItemPtr> stories = matchingStoryItems(relatedStoryItem->feedId(), relatedStoryItem->itemId());
        foreach(StoryItemPtr story, stories)
        {
            if(story->parentItem() != m_starredGroupItem)
            {
                story->setRead(false);
            }
        }
        QList<FeedItemPtr> feedItems = m_feedItems.values(relatedStoryItem->feedId());
        foreach(FeedItemPtr feedItem, feedItems)
        {
            if(classification == NewsBlurEnum::CvNeutralClassifier)
                feedItem->setUnreadCount(feedItem->unreadCount() + 1);
            else if(classification == NewsBlurEnum::CvNegativeClassifier)
                feedItem->setNegUnreadCount(feedItem->negUnreadCount() + 1);
            else if(classification == NewsBlurEnum::CvPositiveClassifier)
                feedItem->setPosUnreadCount(feedItem->posUnreadCount() + 1);
        }
        m_everythingGroupItem->setCanFetchMore(true);
        updateFolderUnreadCounts();
    }
}

/*!
  Marks the story items related to the \a reply as starred or saved.
*/
void NewsBlurService::onSaveStoryFinished(WebServiceReplyPtr reply)
{
    StoryItemPtr relatedStoryItem = relatedNewsBlurItem(reply).dynamicCast<StoryItem>();
    if(m_starredGroupItem && relatedStoryItem)
    {
        if(!relatedStoryItem->isStarred())
        {
            m_starredGroupItem->setUnreadCount(m_starredGroupItem->unreadCount() + 1);
            QList<StoryItemPtr> stories = matchingStoryItems(relatedStoryItem->feedId(), relatedStoryItem->itemId());
            foreach(StoryItemPtr story, stories)
                story->setStarred(true);
            m_starredGroupItem->markAsOutOfDate(true);
            m_starredGroupItem->setCanFetchMore(true);
        }
    }
}

/*!
  Removes the starred or saved marks of story items related to the \a reply.
*/
void NewsBlurService::onUnsaveStoryFinished(WebServiceReplyPtr reply)
{
    StoryItemPtr relatedStoryItem = relatedNewsBlurItem(reply).dynamicCast<StoryItem>();
    if(m_starredGroupItem && relatedStoryItem)
    {
        if(relatedStoryItem->isStarred())
        {
            QList<StoryItemPtr> stories = matchingStoryItems(relatedStoryItem->feedId(), relatedStoryItem->itemId());
            foreach(StoryItemPtr story, stories)
                story->setStarred(false);
            if(m_starredGroupItem->contains(relatedStoryItem->itemId()))
            {
                PageItemPtr starredStory = m_starredGroupItem->childItem(relatedStoryItem->itemId());
                m_starredGroupItem->removeChild(starredStory);
            }
            m_starredGroupItem->setUnreadCount(m_starredGroupItem->unreadCount() - 1);
        }
    }
}

/*!
  Marks all the stories of the feeds related to the \a reply as read.
*/
void NewsBlurService::onMarkFeedReadFinished(WebServiceReplyPtr reply)
{
    FeedItemPtr relatedFeedItem = relatedNewsBlurItem(reply).dynamicCast<FeedItem>();
    if(relatedFeedItem)
        markFeedsAsRead(m_feedItems.values(relatedFeedItem->itemId().toInt()));
}

/*!
  Marks all stories of all child feeds of the folder related to the \a reply.
*/
void NewsBlurService::onMarkFolderReadFinished(WebServiceReplyPtr reply)
{
    FolderItemPtr relatedFolderItem = relatedNewsBlurItem(reply).dynamicCast<FolderItem>();
    if(relatedFolderItem)
    {
        QList<FeedItemPtr> unreadChildFeeds = unreadFeeds(relatedFolderItem);
        QList<FeedItemPtr> feeds;
        foreach(FeedItemPtr feedItem, unreadChildFeeds)
            feeds << m_feedItems.values(feedItem->itemId().toInt());
        markFeedsAsRead(feeds);
    }
}

/*!
  Marks every story item in every feed as read if the completition \a state of
  the \a reply is WebServiceReply::CsFinished.
*/
void NewsBlurService::onMarkEverythingReadCompleted(WebServiceReplyPtr reply, WebServiceReply::CompletedState state)
{
    Q_UNUSED(reply);
    if(state == WebServiceReply::CsFinished)
    {
        QList<FeedItemPtr> feeds;        
        feeds << m_everythingGroupItem;
        feeds << m_feedItems.values();
        markFeedsAsRead(feeds);
    }
    setMarkingEverythingReadBusy(false);
}

/*!
  Updates the unread counts of every folder and feed item,
  based on \a reply's contents.
*/
void NewsBlurService::onUpdateUnreadCountsFinished(WebServiceReplyPtr reply)
{
    UnreadCountsReplyPtr unreadCountsReply = reply.staticCast<UnreadCountsReply>();
    QHash<int, QHash<NewsBlurEnum::ClassifierValue, int> > unreadCounts = unreadCountsReply->unreadCounts();
    bool riverOutOfDate = false;
    foreach(int feedId, unreadCounts.keys())
    {
        if(m_feedItems.contains(feedId))
        {
            QHash<NewsBlurEnum::ClassifierValue, int> unreadCountsPerClassification = unreadCounts.value(feedId);
            foreach(FeedItemPtr feedItem, m_feedItems.values(feedId))
            {
                if((feedItem->unreadCount() != unreadCountsPerClassification.value(NewsBlurEnum::CvNeutralClassifier))
                        || (feedItem->negUnreadCount() != unreadCountsPerClassification.value(NewsBlurEnum::CvNegativeClassifier))
                        || (feedItem->posUnreadCount() != unreadCountsPerClassification.value(NewsBlurEnum::CvPositiveClassifier)))
                {
                    feedItem->setUnreadCount(unreadCountsPerClassification.value(NewsBlurEnum::CvNeutralClassifier));
                    feedItem->setNegUnreadCount(unreadCountsPerClassification.value(NewsBlurEnum::CvNegativeClassifier));
                    feedItem->setPosUnreadCount(unreadCountsPerClassification.value(NewsBlurEnum::CvPositiveClassifier));
                    onFeedOutOfDate(feedId);
                    riverOutOfDate = true;
                }
            }
        }
    }
    if(riverOutOfDate)
        onRiverOutOfDate();
    m_unreadCountsOutOfDate = false;
    updateFolderUnreadCounts();
}

/*!
  Notifies the view that the renaming of some item is finished and updates the item tree
  if the completition \a state of the \a reply is WebServiceReply::CsFinished or WebServiceReply::CsAborted.
*/
void NewsBlurService::onItemRenameCompleted(WebServiceReplyPtr reply, WebServiceReply::CompletedState state)
{
    emit itemRenamingFinished(reply->error());
    if((state == WebServiceReply::CsFinished)
       || (state == WebServiceReply::CsAborted))
        updateTreeStructure();
}

/*!
  Notifies the view that the adding of a feed is finished and updates the item tree
  if the completition \a state of the \a reply is WebServiceReply::CsFinished or WebServiceReply::CsAborted.
*/
void NewsBlurService::onAddFeedCompleted(WebServiceReplyPtr reply, WebServiceReply::CompletedState state)
{
    emit feedAddingFinished(reply->error());
    if((state == WebServiceReply::CsFinished)
       || (state == WebServiceReply::CsAborted))
        updateTreeStructure();
}

/*!
  Notifies the view that the adding of a folder is finished and updates the item tree
  if the completition \a state of the \a reply is WebServiceReply::CsFinished or WebServiceReply::CsAborted.
*/
void NewsBlurService::onAddFolderCompleted(WebServiceReplyPtr reply, WebServiceReply::CompletedState state)
{
    emit folderAddingFinished(reply->error());
    if((state == WebServiceReply::CsFinished)
       || (state == WebServiceReply::CsAborted))
        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 NewsBlurService::onFindFeedsCompleted(WebServiceReplyPtr reply, WebServiceReply::CompletedState state)
{
    FeedSearchResultsReplyPtr searchResultsReply = reply.staticCast<FeedSearchResultsReply>();
    if(state == WebServiceReply::CsFinished)
        emit feedsFound(reply->error(), searchResultsReply->feedTitles(), searchResultsReply->feedUrls());
    else
        emit feedsFound(reply->error(), QStringList(), QStringList());
}

/*!
  Notifies the view that the loading of feed classifiers is finished.
  If the completition \a state of the \a reply is WebServiceReply::CsFinished
  the view gets the recieved feed classifiers, otherwise it's notified about the error.
*/
void NewsBlurService::onLoadFeedClassifiersCompleted(WebServiceReplyPtr reply, WebServiceReply::CompletedState state)
{
    FeedClassifiersReplyPtr classifiersReply = reply.staticCast<FeedClassifiersReply>();
    FeedClassifiersModel* model = 0;
    NewsBlurItemPtr relatedItem = relatedNewsBlurItem(reply);
    if(relatedItem && (state == WebServiceReply::CsFinished))
    {
        foreach(FeedItemPtr feedItem, m_feedItems.values(classifiersReply->feedId()))
            feedItem->setClassifiers(classifiersReply->classifiers());
        model = new FeedClassifiersModel;
        if(relatedItem->itemType() == NewsBlurItem::FeedItem)
        {
            model->setClassifiers(classifiersReply->classifiers());
            model->setFeedTitle(relatedItem->title());
        }
        else if(relatedItem->itemType() == NewsBlurItem::StoryItem)
        {
            StoryItemPtr storyItem = relatedItem.staticCast<StoryItem>();
            model->setClassifiers(createStorySpecificFeedClassifiers(storyItem, classifiersReply->classifiers()));
            model->setFeedTitle(feedTitle(storyItem->feedId()));
        }
        QDeclarativeEngine::setObjectOwnership(model, QDeclarativeEngine::JavaScriptOwnership);
    }
    emit feedClassifiersLoaded(reply->error(), model);
}

/*!
  Notifies the view that the saving of feed classifiers is finished and updates the item tree
  if the completition \a state of the \a reply is WebServiceReply::CsFinished the classifications
  of the loaded story items will be updated.
*/
void NewsBlurService::onSaveFeedClassificationsCompleted(WebServiceReplyPtr reply, WebServiceReply::CompletedState state)
{
    Q_UNUSED(state);    
    if(reply && (state == WebServiceReply::CsFinished))
    {
        SaveFeedClassifiersReplyPtr saveClassifiersReply = reply.staticCast<SaveFeedClassifiersReply>();
        FeedClassifiers classifiers = saveClassifiersReply->classifiers();
        ClassifierHash titleClassifiers = classifiers.titleClassifiers();
        foreach(const QString& title, titleClassifiers.keys())
        {
            if(titleClassifiers.value(title) == NewsBlurEnum::CvNeutralClassifier)
                titleClassifiers.remove(title);
        }
        classifiers.setTitleClassifiers(titleClassifiers);

        foreach(FeedItemPtr feedItem, m_feedItems.values(saveClassifiersReply->feedId()))
        {
            if(!saveClassifiersReply->storyId().isEmpty())
                feedItem->setClassifiers(classifiers);
            foreach(PageItemPtr pageItem, feedItem->childItems())
            {
                StoryItemPtr storyItem = pageItem.staticCast<StoryItem>();
                NewsBlurEnum::ClassifierValue classification
                        =  NewsBlurEnum::storyClassification(classifiers, storyItem->title(),
                                                             storyItem->author(), storyItem->tags());                
                storyItem->setItemClassification(classification);
            }            
        }
        foreach(PageItemPtr pageItem, m_everythingGroupItem->childItems())
        {
            StoryItemPtr storyItem = pageItem.staticCast<StoryItem>();
            if(storyItem->feedId() == saveClassifiersReply->feedId())
            {
                NewsBlurEnum::ClassifierValue classification
                        =  NewsBlurEnum::storyClassification(classifiers, storyItem->title(),
                                                             storyItem->author(), storyItem->tags());
                storyItem->setItemClassification(classification);
            }
        }
    }
    updateUnreadCounts();
    emit feedClassificationsSaved(reply->error());    
}


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

/*!
  Removes the the completed \a reply from the list of stored replies.
*/
void NewsBlurService::onRequestCompleted(WebServiceReplyPtr reply, WebServiceReply::CompletedState state)
{
    Q_UNUSED(state);
    removeRequest(reply);
    if(reply)
    {
        if(reply->requestType() == NewsBlurClient::TpUpdateTree)
            setBusy(false);
    }
}

/*!
  Calls NewsBlurService::updateTreeStructure if the \a reply
  represenst some operation that might altered the tree structure.
*/
void NewsBlurService::onRequestFinished(WebServiceReplyPtr reply)
{
    NewsBlurClient::RequestType requestType = NewsBlurClient::RequestType(reply->requestType());
    if((requestType == NewsBlurClient::TpDeleteFeed)
        || (requestType == NewsBlurClient::TpDeleteFolder)
        || (requestType == NewsBlurClient::TpMoveFeed)
        || (requestType == NewsBlurClient::TpMoveFolder))
    {
        updateTreeStructure();
    }
}

/*!
  Calls NewsBlurService::onUpdateRequestFailed if the \a reply
  has a role of WebServiceReply::RlUpdate.
*/
void NewsBlurService::onRequestFailed(WebServiceReplyPtr reply)
{
    Q_UNUSED(reply);
    if(reply->role() == WebServiceReply::RlUpdate)
        onUpdateRequestFailed(reply);
}

/*!
  Notifies the view about the faliure of a request if the
  \a reply has a role of WebServiceReply::RlUpdate.
*/
void NewsBlurService::onUpdateRequestFailed(WebServiceReplyPtr reply)
{
    NewsBlurEnum::ServiceRequestError errorType = NewsBlurEnum::ServiceRequestError(reply->error());
    if((reply->role() == WebServiceReply::RlUpdate)
       && (reply->completedState() == WebServiceReply::CsFailed))
    {
        if((errorType == NewsBlurEnum::SreNetworkError)
           && 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"));
    }
}

/*!
  Notifies the view about the faliure of a request if the
  \a reply has a role of WebServiceReply::RlCommit.
*/
void NewsBlurService::onCommitRequestFailed(WebServiceReplyPtr reply)
{
    NewsBlurEnum::ServiceRequestError errorType = NewsBlurEnum::ServiceRequestError(reply->error());
    NewsBlurItemPtr relatedItem = this->relatedNewsBlurItem(reply);
    if((reply->role() == WebServiceReply::RlCommit)
       && (reply->completedState() == WebServiceReply::CsFailed))
    {
        if(m_rootItem && (errorType == NewsBlurEnum::SreNetworkError))
        {
            m_rootItem->showMessage(tr("Network error"));
        }
        else if(relatedItem)
        {
            if(reply->requestType() == NewsBlurClient::TpSaveStory)
                relatedItem->showMessage(tr("Failed to save the story"));
            else if(reply->requestType() == NewsBlurClient::TpUnsaveStory)
                relatedItem->showMessage(tr("Failed to remove the story from the saved ones"));
            else if(reply->requestType() == NewsBlurClient::TpMarkFeedRead)
                relatedItem->showMessage(tr("Failed to mark feed as read"));
            else if(reply->requestType() == NewsBlurClient::TpMarkFolderRead)
                relatedItem->showMessage(tr("Failed to mark folder as read"));
            else if(reply->requestType() == NewsBlurClient::TpMarkEverythingRead)
                relatedItem->showMessage(tr("Failed to everything as read"));
            else if(reply->requestType() == NewsBlurClient::TpDeleteFeed)
                relatedItem->showMessage(tr("Failed to delete the feed"));
            else if(reply->requestType() == NewsBlurClient::TpDeleteFolder)
                relatedItem->showMessage(tr("Failed to delete the folder"));
            else if(reply->requestType() == NewsBlurClient::TpMoveFeed)
                relatedItem->showMessage(tr("Failed to move the feed"));
            else if(reply->requestType() == NewsBlurClient::TpMoveFolder)
                relatedItem->showMessage(tr("Failed to move the folder"));
            else if(reply->requestType() == NewsBlurClient::TpMarkStoriesRead)
                relatedItem->showMessage(tr("Failed to mark stories as read"));
        }
    }
}

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

/*!
  Returns the list of story items which are related to the feed with an id of \a feedId and
  their id is \a storyId. If the \a skipStarredStories is true, the child items of
  the starred feed wont be returned.
*/
QList<StoryItemPtr> NewsBlurService::matchingStoryItems(int feedId, const QString& storyId, bool skipStarredStories) const
{
    QList<StoryItemPtr> ret;
    QList<FeedItemPtr> parentCandidates;
    if(!skipStarredStories)
        parentCandidates << m_starredGroupItem;
    parentCandidates << m_everythingGroupItem;
    parentCandidates << m_feedItems.values(feedId);
    foreach(FeedItemPtr parentCandidate, parentCandidates)
    {
        if(parentCandidate->contains(storyId))
            ret << parentCandidate->childItem(storyId).staticCast<StoryItem>();
    }
    return ret;
}

/*!
  Marks the story items as read which are related to the feed with an id of \a feedId and
  their id is \a storyId.
*/
void NewsBlurService::markStoryItemsAsRead(int feedId, const QString &storyId)
{
    QList<StoryItemPtr> stories = matchingStoryItems(feedId, storyId, true);
    if(stories.count())
    {
        NewsBlurEnum::ClassifierValue classification = NewsBlurEnum::ClassifierValue(stories.first()->itemClassification());
        foreach(StoryItemPtr story, stories)
            story->setRead(true);

        QList<FeedItemPtr> feedItems = m_feedItems.values(feedId);
        foreach(FeedItemPtr feedItem, feedItems)
        {
            if(classification == NewsBlurEnum::CvNeutralClassifier)
                feedItem->setUnreadCount(feedItem->unreadCount() - 1);
            else if(classification == NewsBlurEnum::CvNegativeClassifier)
                feedItem->setNegUnreadCount(feedItem->negUnreadCount() - 1);
            else if(classification == NewsBlurEnum::CvPositiveClassifier)
                feedItem->setPosUnreadCount(feedItem->posUnreadCount() - 1);
        }
    }
    updateFolderUnreadCounts();
}

/*!
  Returns the feed classifiers specific to the \a story created from the given \a feedClassifiers.
*/
FeedClassifiers NewsBlurService::createStorySpecificFeedClassifiers(StoryItemPtr story, const FeedClassifiers &feedClassifiers) const
{
    FeedClassifiers classifiers;
    ClassifierHash titleClassifiers;
    ClassifierHash authorClassifiers;
    ClassifierHash tagClassifiers;
    ClassifierHash feedTitleClassifiers = feedClassifiers.titleClassifiers();
    ClassifierHash feedAuthorClassifiers = feedClassifiers.authorClassifiers();
    ClassifierHash feedTagClassifiers = feedClassifiers.tagClassifiers();
    if(feedAuthorClassifiers.contains(story->author()))
        authorClassifiers.insert(story->author(), feedAuthorClassifiers.value(story->author()));
    else if(!story->author().isEmpty())
        authorClassifiers.insert(story->author(), NewsBlurEnum::CvNeutralClassifier);
    foreach(const QString& title, feedTitleClassifiers.keys())
    {
        if(story->title().contains(title))
            titleClassifiers.insert(title, feedTitleClassifiers.value(title));
    }    
    foreach(const QString& tag, story->tags())
    {
        if(feedTagClassifiers.contains(tag))
            tagClassifiers.insert(tag, feedTagClassifiers.value(tag));
        else
            tagClassifiers.insert(tag, NewsBlurEnum::CvNeutralClassifier);
    }
    classifiers.setTitleClassifiers(titleClassifiers);
    classifiers.setAuthorClassifiers(authorClassifiers);
    classifiers.setTagClassifiers(tagClassifiers);
    classifiers.setFeedClassifier(feedClassifiers.feedClassifier());
    return classifiers;
}

/*!
  Marks all child items of the feed items in \a feeds as read.
*/
void NewsBlurService::markFeedsAsRead(const QList<FeedItemPtr> &feeds)
{
    foreach(FeedItemPtr feed, feeds)
    {
        if(feed->itemType() != NewsBlurItem::EverythingGroup)
        {
            feed->setChildrenRead(true);
            foreach(PageItemPtr child, feed->childItems())
            {
                if(m_everythingGroupItem->contains(child->itemId()))
                    m_everythingGroupItem->removeChild(m_everythingGroupItem->childItem(child->itemId()));
            }
        }
        else
        {
            feed->clearChildItems();
            feed->setCanFetchMore(true);
        }
        feed->setUnreadCount(0);
        feed->setNegUnreadCount(0);
        feed->setPosUnreadCount(0);
    }
    updateFolderUnreadCounts();
}

/*!
  Removes the oldest story items of feed items if the
  total count of story items is larger then the maximum number
  of stories allowed to be cached.
*/
void NewsBlurService::trimCachedItems()
{
    int maxCachedItems = applicationSettings()->maximumCachedItemCount();
    if(maxCachedItems > 0)
    {
        QMultiMap<QDateTime, FeedItemPtr> feedItemMap;
        int cachedItemCount = 0;
        QList<FeedItemPtr> feedItems = m_feedItems.values();
        feedItems << m_starredGroupItem;
        foreach(FeedItemPtr feedItem, feedItems)
        {
            if(feedItem->childCount() > 0)
            {
                feedItemMap.insertMulti(feedItem->lastAccessDate(), feedItem);
                cachedItemCount += feedItem->childCount();
            }
        }
        if(cachedItemCount > maxCachedItems)
        {
            foreach(FeedItemPtr feedItem, feedItemMap.values())
            {
                int firstItem = (feedItem->childCount() - (cachedItemCount - maxCachedItems));
                if(firstItem < 0)
                    firstItem = 0;
                for(int i = (feedItem->childCount() - 1); i >= firstItem; --i)
                {
                    feedItem->removeChild(feedItem->childItem(i));
                    --cachedItemCount;
                }
                feedItem->setCanFetchMore(true);
                if(cachedItemCount <= maxCachedItems)
                    break;
            }
        }
    }
}


/*!
  Returns the item related to the given \a reply.
*/
NewsBlurItemPtr NewsBlurService::relatedNewsBlurItem(WebServiceReplyWeakRef reply)
{
    return relatedItem(reply).staticCast<NewsBlurItem>();
}

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

QString NewsBlurService::authenticationSheet() const
{
    return NewsBlurService::s_authenticationPageUrl;
}
} // namespace NewsBlur
Q_EXPORT_PLUGIN2(newsblurservice, NewsBlur::NewsBlurService)
