/****************************************************************************
**
** 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 "newsblurclient.h"
#include <QDebug>
#include "updatetreereply.h"
#include "starredgroup.h"
#include "everythinggroup.h"
#include "rootitem.h"
#include "feeditem.h"
#include "folderitem.h"
#include "storyitem.h"
#include "abstractfeedaggreagatorservice.h"
#include "json.h"
#include <QPainter>
#include <QTextDocumentFragment>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QNetworkCookieJar>
#include <QRegExp>
#include <QScriptEngine>

namespace NewsBlur
{
/*!
  \class NewsBlurClient
  \brief The NewsBlurClient class encapsulates the network requests
  and the parsing of replies which are required to interact with the
  NewsBlur server.
*/
/*!
  \struct NewsBlurClient::NetworkRequest
  \brief Stores the properties and the contents of a network request.

  \var NewsBlurClient::NetworkRequest::type
  The identifier of the request type. \sa NewsBlurClient::NetworkRequestType
  \var NewsBlurClient::NetworkRequest::reply
  Pointer to the related reply object.
  \var NewsBlurClient::NetworkRequest::operationType
  The type of the operation the request is part of.
  \var NewsBlurClient::NetworkRequest::url
  The exact url of the request.
  \var NewsBlurClient::NetworkRequest::baData
  The contents of the request.
*/
/*!
  \struct NewsBlurClient::RequestProperties
  \brief Stores the properties a network request type.

  \var NewsBlurClient::RequestProperties::url
  The base url of the request type.
  \var NewsBlurClient::RequestProperties::operationType
  The type of the HTTP request.
*/
/*!
  \enum NewsBlurClient::RequestType
  Specifies the type of a NewsBlur request. Every request
  type translates to one or more network requests.
  It can be used to identify for which operation a general
  NewsBlurReply object was created.

  \var NewsBlurClient::RequestType NewsBlurClient::TpUnknown
  Unknown or unspecified request type.
  \var NewsBlurClient::RequestType NewsBlurClient::TpUpdateTree
  The type of the request used to retrieve the static part
  of the users object tree, like the feed and folder hierarchy,
  favicons.
  \var NewsBlurClient::RequestType NewsBlurClient::TpLoadNewFeedStories
  The type of the request which retrieves new stories for a specific feed.
  \var NewsBlurClient::RequestType NewsBlurClient::TpLoadMoreFeedStories
  The type of the request which retrieves older stories for a specific feed.
  \var NewsBlurClient::RequestType NewsBlurClient::TpMarkStoryRead
  The requests which marks a single story as read.
  \var NewsBlurClient::RequestType NewsBlurClient::TpMarkStoriesRead
  The request which marks multiple stories from multiple feeds as read.
  \var NewsBlurClient::RequestType NewsBlurClient::TpMarkStoryUnread
  The request which marks a single story as unread.
  \var NewsBlurClient::RequestType NewsBlurClient::TpRefreshUnreadCounts
  The request which retrieves up to date unread counts for feeds.
  \var NewsBlurClient::RequestType NewsBlurClient::TpVerifyAccount
  The request for veryfiin the validity of the authentication parameters.
  \var NewsBlurClient::RequestType NewsBlurClient::TpLoadMoreStarredStories
  The type of the request which retrieves older stories marked as starred or saved.
  \var NewsBlurClient::RequestType NewsBlurClient::TpLoadNewStarredStories
  The type of the request which retrieves new stories marked as starred or saved.
  \var NewsBlurClient::RequestType NewsBlurClient::TpSaveStory
  The type of the request which marks a single story as starred or saved.
  \var NewsBlurClient::RequestType NewsBlurClient::TpUnsaveStory
  The type of the request which removes the starred or saved status of a single story.
  \var NewsBlurClient::RequestType NewsBlurClient::TpMarkFeedRead
  The type of request which marks all the stories of a signel feed as read.
  \var NewsBlurClient::RequestType NewsBlurClient::TpMarkFolderRead
  The type of request which marks multiple feeds in a common parent folder as read.
  \var NewsBlurClient::RequestType NewsBlurClient::TpMarkEverythingRead
  The type of request which marks every story in every feed as read.
  \var NewsBlurClient::RequestType NewsBlurClient::TpRenameFeed
  The type of request which changes the name or title of a single feed.
  \var NewsBlurClient::RequestType NewsBlurClient::TpRenameFolder
  The type of request which changes the name or title of a single folder.
  \var NewsBlurClient::RequestType NewsBlurClient::TpDeleteFeed
  The type of request which deletes a single feed.
  \var NewsBlurClient::RequestType NewsBlurClient::TpDeleteFolder
  The type of request which deletes a single folder.
  \var NewsBlurClient::RequestType NewsBlurClient::TpMoveFeed
  The type of request which moves a feed to a different parent folder.
  \var NewsBlurClient::RequestType NewsBlurClient::TpMoveFolder
  The type of request which moves a folder to a different parent folder.
  \var NewsBlurClient::RequestType NewsBlurClient::TpAddFeed
  The type of request which adds a new feed to an existing parent folder.
  \var NewsBlurClient::RequestType NewsBlurClient::TpAddFolder
  The type of request which adds a new folder to an existing parent folder.
  \var NewsBlurClient::RequestType NewsBlurClient::TpFindFeed
  The type of request which retrieves feed url suggestions for search terms.
  \var NewsBlurClient::RequestType NewsBlurClient::TpLoadMoreRiverStories
  The type of the request which retrieves older unread stories from multiple feeds.
  \var NewsBlurClient::RequestType NewsBlurClient::TpUpdateRiverStories
  The type of the request which retrieves new unread stories from multiple feeds.
  \var NewsBlurClient::RequestType NewsBlurClient::TpFeedClassifiers
  The type of the request which retrieves the intelligence classifiers of a single feed.
  \var NewsBlurClient::RequestType NewsBlurClient::TpSaveClassifiers
  The type of the request which modifies the intelligence classifiers of a single feed.
*/
/*!
  \enum NewsBlurClient::NetworkRequestType
  Specifies the type of network request.

  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtLogin
  The type of network request for creating an authenticated user session.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtLogout
  The type of network request for destroying the user session.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtFeeds
  The type of network request for retrieving users subscribed feeds and folders.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtFeedStories
  The type of network request for retrieving a page of stories of a single feed.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtStatistics
  The type of network request for retrieving the statistical data of a feed.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtMarkStoryRead
  The type of network request for marking a single story as read.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtMarkFeedStoriesRead
  The type of network request for marking multiple stories from multiple feeds as read.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtMarkStoryUnread
  The type of network request for marking every story of a feed as unread.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtRefreshUnreadCounts
  The type of network request for retrieving the up to date unread counts of the users feeds.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtStarredStories
  The type of network request for retrieving a page of stories marked as starred or saved.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtFavicons
  The type of network request for retrieving the favicons of multiple feeds.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtMarkStoryStarred
  The type of network request for marking a single story as starred or saved.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtMarkStoryUnstarred
  The type of network request for marking a single story as unstarred.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtMarkFeedRead
  The type of network request for marking every story of a feed as read.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtMarkAllRead
  The type of network request for marking every story of every feed as read.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtRenameFeed
  The type of network request for modifiing the name or title of a feed.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtRenameFolder
  The type of network request for modifiing the name or title of a folder.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtDeleteFeed
  The type of network request for removing a feed from it's parent folder.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtDeleteFolder
  The type of network request for removing a folder and all of it's children.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtMoveFeed
  The type of network request for moving a feed to a different parent folder.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtMoveFolder
  The type of network request for moving a folder to a different parent folder.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtAddFeed
  The type of network request for adding a new feed to a folder.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtAddFolder
  The type of network request for adding a new child folder to an existing folder.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtFeedAutocomplete
  The type of network request for retrieving feed url suggestions for search terms.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtRiverStories
  The type of network request for retrieving a page of unread stories from multiple feeds.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtFeedTrainer
  The type of network request for retrieving the intelligence classifiers of a single feed.
  \var NewsBlurClient::NetworkRequestType  NewsBlurClient::RtSaveClassifiers
  The type of network request for modifiing the intelligence classifiers of a single feed.
*/
/*!
  \enum NewsBlurClient::ClassifierTarget
  Specifies the target of the classification.

  \var NewsBlurClient::ClassifierTarget  NewsBlurClient::CtFeed
  Classification of a feed.
  \var NewsBlurClient::ClassifierTarget  NewsBlurClient::CtTitle
  Classification of a story title.
  \var NewsBlurClient::ClassifierTarget  NewsBlurClient::CtAuthor
  Classification of an author.
  \var NewsBlurClient::ClassifierTarget  NewsBlurClient::CtTag
  Classification of a tag.
*/
const char* NewsBlurClient::s_serviceBaseUrl                = "http://www.newsblur.com/";
const char* NewsBlurClient::s_loginPath                     = "api/login";
const char* NewsBlurClient::s_logoutPath                    = "api/logout";
const char* NewsBlurClient::s_feedsPath                     = "reader/feeds";
const char* NewsBlurClient::s_feedPath                      = "reader/feed";
const char* NewsBlurClient::s_feedStatisticsPath            = "rss_feeds/statistics";
const char* NewsBlurClient::s_markStoryReadPath             = "reader/mark_story_as_read";
const char* NewsBlurClient::s_markStoryUnreadPath           = "reader/mark_story_as_unread";
const char* NewsBlurClient::s_refreshFeedsPath              = "reader/refresh_feeds";
const char* NewsBlurClient::s_starredStoriesPath            = "reader/starred_stories";
const char* NewsBlurClient::s_faviconsPath                  = "reader/favicons";
const char* NewsBlurClient::s_markStoryStarredPath          = "reader/mark_story_as_starred";
const char* NewsBlurClient::s_markStoryUnstarredPath        = "reader/mark_story_as_unstarred";
const char* NewsBlurClient::s_markFeedReadPath              = "reader/mark_feed_as_read";
const char* NewsBlurClient::s_markAllReadPath               = "reader/mark_all_as_read";
const char* NewsBlurClient::s_renameFeedPath                = "reader/rename_feed";
const char* NewsBlurClient::s_renameFolderPath              = "reader/rename_folder";
const char* NewsBlurClient::s_riverStoriesPath              = "reader/river_stories";
const char* NewsBlurClient::s_deleteFeedPath                = "reader/delete_feed";
const char* NewsBlurClient::s_deleteFolderPath              = "reader/delete_folder";
const char* NewsBlurClient::s_moveFeedPath                  = "reader/move_feed_to_folder";
const char* NewsBlurClient::s_moveFolderPath                = "reader/move_folder_to_folder";
const char* NewsBlurClient::s_addFolderPath                 = "reader/add_folder";
const char* NewsBlurClient::s_addUrlPath                    = "reader/add_url";
const char* NewsBlurClient::s_feedAutocompletePath          = "rss_feeds/feed_autocomplete";
const char* NewsBlurClient::s_feedTrainerPath               = "reader/feeds_trainer";
const char* NewsBlurClient::s_saveClassifiersPath           = "classifier/save";
const char* NewsBlurClient::s_markFeedStoriesReadPath       = "reader/mark_feed_stories_as_read";

const char* NewsBlurClient::s_userNameKeyName               = "username";
const char* NewsBlurClient::s_passwordKeyName               = "password";

const char* NewsBlurClient::s_userNameParameter             = "username";
const char* NewsBlurClient::s_passwordParameter             = "password";
const char* NewsBlurClient::s_pageParameter                 = "page";
const char* NewsBlurClient::s_feedIdParameter               = "feed_id";
const char* NewsBlurClient::s_storyIdParameter              = "story_id";
const char* NewsBlurClient::s_feedsParameter                = "feeds";
const char* NewsBlurClient::s_feedTitleParameter            = "feed_title";
const char* NewsBlurClient::s_folderNameParameter           = "folder_name";
const char* NewsBlurClient::s_inFolderParameter             = "in_folder";
const char* NewsBlurClient::s_newFolderNameParameter        = "new_folder_name";
const char* NewsBlurClient::s_toFolderParameter             = "to_folder";
const char* NewsBlurClient::s_parentFolderParameter         = "parent_folder";
const char* NewsBlurClient::s_folderParameter               = "folder";
const char* NewsBlurClient::s_urlParameter                  = "url";
const char* NewsBlurClient::s_termParameter                 = "term";
const char* NewsBlurClient::s_likeFeedParameter             = "like_feed";
const char* NewsBlurClient::s_dislikeFeedParameter          = "dislike_feed";
const char* NewsBlurClient::s_removeLikeFeedParameter       = "remove_like_feed";
const char* NewsBlurClient::s_removeDislikeFeedParameter    = "remove_dislike_feed";
const char* NewsBlurClient::s_likeTitleParameter            = "like_title";
const char* NewsBlurClient::s_dislikeTitleParameter         = "dislike_title";
const char* NewsBlurClient::s_removeLikeTitleParameter      = "remove_like_title";
const char* NewsBlurClient::s_removeDislikeTitleParameter   = "remove_dislike_title";
const char* NewsBlurClient::s_likeTagParameter              = "like_tag";
const char* NewsBlurClient::s_dislikeTagParameter           = "dislike_tag";
const char* NewsBlurClient::s_removeLikeTagParameter        = "remove_like_tag";
const char* NewsBlurClient::s_removeDislikeTagParameter     = "remove_dislike_tag";
const char* NewsBlurClient::s_likeAuthorParameter           = "like_author";
const char* NewsBlurClient::s_dislikeAuthorParameter        = "dislike_author";
const char* NewsBlurClient::s_removeLikeAuthorParameter     = "remove_like_author";
const char* NewsBlurClient::s_removeDislikeAuthorParameter  = "remove_dislike_author";
const char* NewsBlurClient::s_feedStoriesParameter          = "feeds_stories";

const char* NewsBlurClient::s_feedTitleValue                = "feed_title";
const char* NewsBlurClient::s_negativeCountValue            = "ng";
const char* NewsBlurClient::s_positiveCountValue            = "ps";
const char* NewsBlurClient::s_neutralCountValue             = "nt";
const char* NewsBlurClient::s_tagsValue                     = "tags";
const char* NewsBlurClient::s_authorsValue                  = "authors";
const char* NewsBlurClient::s_titlesValue                   = "titles";
const char* NewsBlurClient::s_feedsValue                    = "feeds";
const char* NewsBlurClient::s_negativeClassifierValue       = "-1";
const char* NewsBlurClient::s_positiveClassifierValue       = "1";
const char* NewsBlurClient::s_neutralClassifierValue        = "0";
const char* NewsBlurClient::s_idValue                       = "id";
const char* NewsBlurClient::s_storyFeedIdValue              = "story_feed_id";
const char* NewsBlurClient::s_storyTitleValue               = "story_title";
const char* NewsBlurClient::s_readStatusValue               = "read_status";
const char* NewsBlurClient::s_storyContentValue             = "story_content";
const char* NewsBlurClient::s_storyPermalinkValue           = "story_permalink";
const char* NewsBlurClient::s_storyDateValue                = "story_date";
const char* NewsBlurClient::s_storyAuthorsValue             = "story_authors";
const char* NewsBlurClient::s_storyTagsValue                = "story_tags";
const char* NewsBlurClient::s_starredValue                  = "starred";
const char* NewsBlurClient::s_intelligenceValue             = "intelligence";
const char* NewsBlurClient::s_authorValue                   = "author";
const char* NewsBlurClient::s_titleValue                    = "title";
const char* NewsBlurClient::s_feedValue                     = "feed";
const char* NewsBlurClient::s_authenticatedValue            = "authenticated";
const char* NewsBlurClient::s_trueValue                     = "true";
const char* NewsBlurClient::s_foldersValue                  = "folders";
const char* NewsBlurClient::s_starredCountValue             = "starred_count";
const char* NewsBlurClient::s_storiesValue                  = "stories";
const char* NewsBlurClient::s_classifiersValue              = "classifiers";
const char* NewsBlurClient::s_resultValue                   = "result";
const char* NewsBlurClient::s_okValue                       = "ok";
const char* NewsBlurClient::s_labelValue                    = "label";
const char* NewsBlurClient::s_valueValue                    = "value";
const char* NewsBlurClient::s_numSubscribersValue           = "num_subscribers";
const char* NewsBlurClient::s_numStoriesLastMonthValue      = "stories_last_month";
const char* NewsBlurClient::s_feedTagsValue                 = "feed_tags";
const char* NewsBlurClient::s_feedAuthorsValue              = "feed_authors";
const char* NewsBlurClient::s_storyDateFormat               = "yyyy-MM-dd hh:mm:ss";
const char* NewsBlurClient::s_nullIconContents              = "null";

/*!
  Constructs a NewsBlurClient object with the given \a parent.
*/
NewsBlurClient::NewsBlurClient(QObject *parent) :
    WebServiceClient(parent),
    m_preferredFaviconSize(QSize(64, 64))
{
    //initialize the properties of the network requests
    addNetworkRequestType(NewsBlurClient::RtLogin,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_loginPath,
                          QNetworkAccessManager::PostOperation);
    addNetworkRequestType(NewsBlurClient::RtLogout,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_logoutPath,
                          QNetworkAccessManager::PostOperation);
    addNetworkRequestType(NewsBlurClient::RtFeeds,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_feedsPath,
                          QNetworkAccessManager::GetOperation);
    addNetworkRequestType(NewsBlurClient::RtFeedStories,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_feedPath,
                          QNetworkAccessManager::GetOperation);
    addNetworkRequestType(NewsBlurClient::RtStatistics,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_feedStatisticsPath,
                          QNetworkAccessManager::GetOperation);
    addNetworkRequestType(NewsBlurClient::RtMarkStoryRead,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_markStoryReadPath,
                          QNetworkAccessManager::PostOperation);
    addNetworkRequestType(NewsBlurClient::RtMarkStoryUnread,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_markStoryUnreadPath,
                          QNetworkAccessManager::PostOperation);
    addNetworkRequestType(NewsBlurClient::RtRefreshUnreadCounts,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_refreshFeedsPath,
                          QNetworkAccessManager::GetOperation);
    addNetworkRequestType(NewsBlurClient::RtStarredStories,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_starredStoriesPath,
                          QNetworkAccessManager::GetOperation);
    addNetworkRequestType(NewsBlurClient::RtFavicons,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_faviconsPath,
                          QNetworkAccessManager::GetOperation);
    addNetworkRequestType(NewsBlurClient::RtMarkStoryStarred,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_markStoryStarredPath,
                          QNetworkAccessManager::PostOperation);
    addNetworkRequestType(NewsBlurClient::RtMarkStoryUnstarred,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_markStoryUnstarredPath,
                          QNetworkAccessManager::PostOperation);
    addNetworkRequestType(NewsBlurClient::RtMarkFeedRead,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_markFeedReadPath,
                          QNetworkAccessManager::PostOperation);
    addNetworkRequestType(NewsBlurClient::RtMarkAllRead,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_markAllReadPath,
                          QNetworkAccessManager::PostOperation);
    addNetworkRequestType(NewsBlurClient::RtRenameFeed,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_renameFeedPath,
                          QNetworkAccessManager::PostOperation);
    addNetworkRequestType(NewsBlurClient::RtRenameFolder,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_renameFolderPath,
                          QNetworkAccessManager::PostOperation);
    addNetworkRequestType(NewsBlurClient::RtRiverStories,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_riverStoriesPath,
                          QNetworkAccessManager::GetOperation);
    addNetworkRequestType(NewsBlurClient::RtDeleteFeed,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_deleteFeedPath,
                          QNetworkAccessManager::PostOperation);
    addNetworkRequestType(NewsBlurClient::RtDeleteFolder,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_deleteFolderPath,
                          QNetworkAccessManager::PostOperation);
    addNetworkRequestType(NewsBlurClient::RtMoveFeed,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_moveFeedPath,
                          QNetworkAccessManager::PostOperation);
    addNetworkRequestType(NewsBlurClient::RtMoveFolder,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_moveFolderPath,
                          QNetworkAccessManager::PostOperation);
    addNetworkRequestType(NewsBlurClient::RtAddFolder,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_addFolderPath,
                          QNetworkAccessManager::PostOperation);
    addNetworkRequestType(NewsBlurClient::RtAddFeed,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_addUrlPath,
                          QNetworkAccessManager::PostOperation);
    addNetworkRequestType(NewsBlurClient::RtFeedAutocomplete,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_feedAutocompletePath,
                          QNetworkAccessManager::GetOperation);
    addNetworkRequestType(NewsBlurClient::RtFeedTrainer,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_feedTrainerPath,
                          QNetworkAccessManager::GetOperation);
    addNetworkRequestType(NewsBlurClient::RtSaveClassifiers,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_saveClassifiersPath,
                          QNetworkAccessManager::PostOperation);
    addNetworkRequestType(NewsBlurClient::RtMarkFeedStoriesRead,
                          QString(NewsBlurClient::s_serviceBaseUrl) + NewsBlurClient::s_markFeedStoriesReadPath,
                          QNetworkAccessManager::PostOperation);
}

/*!
  Returns the preferred size for the favicons of feeds.
  The retrieved favicons are scaled to this size.
*/
QSize NewsBlurClient::preferredFaviconSize() const
{
    return m_preferredFaviconSize;
}

/*!
  \sa NewsBlurClient::preferredFaviconSize
*/
void NewsBlurClient::setPreferredFaviconSize(const QSize& size)
{
    m_preferredFaviconSize = size;
}

/*!
  Parses the contents of the \a feedMap and returns the feed items created from it.
*/
QMultiHash<int, FeedItemPtr> NewsBlurClient::createFeedItems(const QVariantMap &feedMap) const
{
    QMultiHash<int, FeedItemPtr> ret;
    //loop through the feed ids
    foreach(const QString feedId, feedMap.keys())
    {
        //set the properties related to the feed id
        QVariantMap feedPropertiesMap = feedMap.value(feedId).toMap();
        FeedItemPtr feedItem = FeedItemPtr(new FeedItem());
        feedItem->setItemId(feedId);
        feedItem->setEnabled(true);
        feedItem->setTitle(feedPropertiesMap.value(NewsBlurClient::s_feedTitleValue).toString());
        feedItem->setCanFetchMore(true);
        feedItem->setUnreadCount(feedPropertiesMap.value(NewsBlurClient::s_neutralCountValue).toInt());
        feedItem->setPosUnreadCount(feedPropertiesMap.value(NewsBlurClient::s_positiveCountValue).toInt());
        feedItem->setNegUnreadCount(feedPropertiesMap.value(NewsBlurClient::s_negativeCountValue).toInt());
        ret.insert(feedId.toInt(), feedItem);
    }
    return ret;
}

/*!
  Parses the contents of the \a folderList and return the list of top level items created from them.
  All the other folder items are children of these top level items. If a parent item is specified
  the returned items are going to be added to it as child items. In case during parsing the \a folderList
  a refference to a feed is found, the appropriate item from the \a feedItemHash will be added to the folder
  item. After execution the \a folderItems list will contain a flat list of all folder items.
  The \a _allFeedItems parameter is used for recursive calls, so nothing should be passed for this parameter.
*/
QList<PageItemPtr> NewsBlurClient::createFolderItems(const QVariantList &folderList, PageItemPtr parentItem,
                                                    QMultiHash<int, FeedItemPtr> *feedItemHash, QList<FolderItemPtr> *folderItems,
                                                    QMultiHash<int, FeedItemPtr> *_allFeedItems) const
{
    QList<PageItemPtr> ret;
    QMultiHash<int, FeedItemPtr>* allFeedItems = _allFeedItems;
    //if the _allFeedItems parameter wasn't specified, create it for passing it
    //to recursive calls
    if(allFeedItems == 0)
        allFeedItems = new QMultiHash<int, FeedItemPtr>();
    //loop through the list of items
    foreach(const QVariant& listItem, folderList)
    {
        //if the item's type is double, it's an id of a feed
        if(listItem.type() == QVariant::Double)
        {
            int feedId = listItem.toDouble();
            FeedItemPtr feedItem;
            //if the feedItemHash parameter contains a feed item for the id
            //remove and use it
            if(feedItemHash->contains(feedId))
            {
                feedItem = feedItemHash->value(feedId);
                feedItemHash->remove(feedId);
            }
            //if the feedItemHash parameter doesn't contains a feed item for the id
            //then it means that the an existing instance of the feed were already
            //added to a folder, so create a new instance based on an existing one
            else if(allFeedItems->contains(feedId))
            {
                FeedItemPtr existingFeedItem = allFeedItems->value(feedId);
                feedItem = FeedItemPtr(new FeedItem());
                feedItem->setItemId(existingFeedItem->itemId());
                feedItem->setEnabled(existingFeedItem->isEnabled());
                feedItem->setTitle(existingFeedItem->title());
                feedItem->setCanFetchMore(existingFeedItem->canFetchMore());
                feedItem->setUnreadCount(existingFeedItem->unreadCount());
                feedItem->setPosUnreadCount(existingFeedItem->posUnreadCount());
                feedItem->setNegUnreadCount(existingFeedItem->negUnreadCount());
            }
            if(feedItem != 0)
            {                
                if(parentItem != 0) //if a valid parentItem was specified, append the feed to it
                    parentItem->appendChild(feedItem);
                else //if no parentItem was specified, return the feed in the list of top level items
                    ret << feedItem;
                //add the feed to the hash wich contains all the feed item instances
                allFeedItems->insertMulti(feedId, feedItem);
            }
        }
        //if the item's type is map, then it's a child folder
        else if(listItem.type() == QVariant::Map)
        {
            QVariantMap folderMap = listItem.toMap();
            //loop through the folders
            foreach(const QString& folderName, folderMap.keys())
            {
                //create folder items
                FolderItemPtr folderItem = FolderItemPtr(new FolderItem());
                folderItem->setEnabled(true);
                folderItem->setItemId(folderName);
                folderItem->setTitle(folderName);
                //add the item to the list of all folder items
                if(folderItems != 0)
                    *folderItems << folderItem;                
                if(parentItem != 0) //if a valid parent item was specified, add the folderItem as a child to it
                    parentItem->appendChild(folderItem);
                else //if no parentItem was specified, return the folder in the list of top level items
                    ret << folderItem;
                //call this function recursively to add the contents of the current folder to the tree
                createFolderItems(folderMap.value(folderName).toList(), folderItem, feedItemHash, folderItems, allFeedItems);
            }
        }
    }
    if(_allFeedItems == 0)
    {
        feedItemHash->clear();
        *feedItemHash = *allFeedItems;
        allFeedItems->clear();
        delete allFeedItems;
    }
    return ret;
}

/*!
  Parses the \a itemList parameter and returns a list of story items created from it.
  If a valid \a parentItem is specified, the returned story items will be appended to it
  as child items.
*/
QList<StoryItemPtr> NewsBlurClient::createStoryItems(FeedItemPtr parentItem, const QVariantList &itemList)
{
    QList<StoryItemPtr> ret;
    foreach(const QVariant& listItem, itemList)
    {
        QVariantMap itemData = listItem.toMap();
        StoryItemPtr storyItem = StoryItemPtr(new StoryItem(parentItem));
        storyItem->setItemId(itemData.value(NewsBlurClient::s_idValue).toString());
        storyItem->setFeedId(itemData.value(NewsBlurClient::s_storyFeedIdValue).toInt());
        storyItem->setEnabled(true);
        QTextDocumentFragment titleFragment = QTextDocumentFragment::fromHtml(itemData.value(NewsBlurClient::s_storyTitleValue).toString());
        storyItem->setTitle(titleFragment.toPlainText());
        storyItem->setRead(itemData.value(NewsBlurClient::s_readStatusValue).toInt());
        storyItem->setContents(itemData.value(NewsBlurClient::s_storyContentValue).toString());
        storyItem->setUrl(itemData.value(NewsBlurClient::s_storyPermalinkValue).toString());
        QDateTime storyDate = QDateTime::fromString(itemData.value(NewsBlurClient::s_storyDateValue).toString(), NewsBlurClient::s_storyDateFormat);
        storyDate.setTimeSpec(Qt::UTC);
        storyItem->setDate(storyDate);
        storyItem->setAuthor(itemData.value(NewsBlurClient::s_storyAuthorsValue).toString());
        storyItem->setTags(itemData.value(NewsBlurClient::s_storyTagsValue).toStringList());
        if(itemData.value(NewsBlurClient::s_starredValue).toString() == NewsBlurClient::s_trueValue)
            storyItem->setStarred(true);
        QVariantMap intelligenceMap = itemData.value(NewsBlurClient::s_intelligenceValue).toMap();
        storyItem->setItemClassification(NewsBlurEnum::storyClassification(NewsBlurEnum::classifierFromString(intelligenceMap.value(NewsBlurClient::s_tagsValue).toString()),
                                                       NewsBlurEnum::classifierFromString(intelligenceMap.value(NewsBlurClient::s_authorValue).toString()),
                                                       NewsBlurEnum::classifierFromString(intelligenceMap.value(NewsBlurClient::s_titleValue).toString()),
                                                       NewsBlurEnum::classifierFromString(intelligenceMap.value(NewsBlurClient::s_feedValue).toString())));
        if(parentItem == 0)
        {
            ret << storyItem;
        }
        else
        {
            parentItem->appendChild(storyItem);
        }
    }
    return ret;
}


/*!
  Parses the \a classifiers parameter, and populates the \a tagClassifiers, \a authorClassifiers, \a titleClassifiers
  and \a feedClassifier parameters.
*/
void NewsBlurClient::createFeedItemClassifiers(const QVariantMap &classifiers, ClassifierHash *tagClassifiers,
                                               ClassifierHash *authorClassifiers, ClassifierHash *titleClassifiers,
                                               NewsBlurEnum::ClassifierValue *feedClassifier) const
{
    QVariantMap tags = classifiers.value(NewsBlurClient::s_tagsValue).toMap();
    QVariantMap authors = classifiers.value(NewsBlurClient::s_authorsValue).toMap();
    QVariantMap titles = classifiers.value(NewsBlurClient::s_titlesValue).toMap();
    foreach(const QString& tag, tags.keys())
        tagClassifiers->insert(tag, NewsBlurEnum::classifierFromString(tags.value(tag).toString()));
    foreach(const QString& author, authors.keys())
        authorClassifiers->insert(author, NewsBlurEnum::classifierFromString(authors.value(author).toString()));
    foreach(const QString& title, titles.keys())
        titleClassifiers->insert(title, NewsBlurEnum::classifierFromString(titles.value(title).toString()));
    QVariantMap feedClassifiers = classifiers.value(NewsBlurClient::s_feedsValue).toMap();
    if(feedClassifiers.count() > 0)
        *feedClassifier = NewsBlurEnum::classifierFromString(feedClassifiers.values().first().toString());
    else
        *feedClassifier = NewsBlurEnum::CvNeutralClassifier;
}

/*!
  Returns the string representation of a classifier \a value parameter for the given classification \a target.
*/
QString NewsBlurClient::classifierValueToParam(NewsBlurEnum::ClassifierValue value, NewsBlurClient::ClassifierTarget target) const
{
    QString ret;
    if(target == CtFeed)
    {
        if(value == NewsBlurEnum::CvNegativeClassifier)
            ret = NewsBlurClient::s_dislikeFeedParameter;
        else if(value == NewsBlurEnum::CvPositiveClassifier)
            ret = NewsBlurClient::s_likeFeedParameter;
        else
            ret = NewsBlurClient::s_removeDislikeFeedParameter;
    }
    else if(target == CtTitle)
    {
        if(value == NewsBlurEnum::CvNegativeClassifier)
            ret = NewsBlurClient::s_dislikeTitleParameter;
        else if(value == NewsBlurEnum::CvPositiveClassifier)
            ret = NewsBlurClient::s_likeTitleParameter;
        else
            ret = NewsBlurClient::s_removeDislikeTitleParameter;
    }
    else if(target == CtAuthor)
    {
        if(value == NewsBlurEnum::CvNegativeClassifier)
            ret = NewsBlurClient::s_dislikeAuthorParameter;
        else if(value == NewsBlurEnum::CvPositiveClassifier)
            ret = NewsBlurClient::s_likeAuthorParameter;
        else
            ret = NewsBlurClient::s_removeDislikeAuthorParameter;
    }
    else if(target == CtTag)
    {
        if(value == NewsBlurEnum::CvNegativeClassifier)
            ret = NewsBlurClient::s_dislikeTagParameter;
        else if(value == NewsBlurEnum::CvPositiveClassifier)
            ret = NewsBlurClient::s_likeTagParameter;
        else
            ret = NewsBlurClient::s_removeDislikeTagParameter;
    }
    return ret;
}

/*!
  Returns the \a favicon scaled to the preferred size.
*/
QImage NewsBlurClient::resizeFavicon(const QImage &favicon) const
{
    if((favicon.width() > m_preferredFaviconSize.width())
       || (favicon.height() > m_preferredFaviconSize.height()))
    {
        return favicon.scaled(m_preferredFaviconSize, Qt::KeepAspectRatio);
    }
    else
    {
        return favicon;
    }
}

/*!
  Sends a NewsBlurClient::TpUpdateTree request and returns the reply related to it.
  The favicons specified in the \a faviconsToIgnore won't be retrieved.
*/
UpdateTreeReplyPtr NewsBlurClient::updateTree(const QStringList &faviconsToIgnore)
{
    UpdateTreeReplyPtr reply(new UpdateTreeReply);
    reply->setRequestType(NewsBlurClient::TpUpdateTree);
    reply->m_ignoredFavicons = faviconsToIgnore;
    QNetworkReply* networkReply;
    if(!isLoggedIn())
    {
        QList<QPair<QString, QString> > parameters;
        QAuthenticator authenticator;
        emit authenticationRequired(&authenticator);
        parameters << qMakePair<QString, QString>(NewsBlurClient::s_userNameParameter, authenticator.user());
        parameters << qMakePair<QString, QString>(NewsBlurClient::s_passwordParameter, authenticator.password());
        networkReply = sendRequest(NewsBlurClient::RtLogin, QString(), parameters);
    }
    else
    {
        networkReply = sendRequest(NewsBlurClient::RtFeeds);
    }
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpLoadMoreFeedStories request and returns the reply related to it.
  \a feedId is the identifier of the feed for which stories will be requested, starting from
  the \a highestLoadedPage + 1'th page. If the \a shouldLoadClassifiers parameter is true,
  the returned feed classifiers will be parsed and added to the reply.
  Stories with ids contained in the \a childIdsToIgnore parameter won't be added to the reply.
  If less then the \a preferredStoryCount number of stories was retrieved which match the
  given \a filter, additional pages of stories will be retrieved until the preffered number
  of stories is reached, or until no more stories can be requested.
*/
FeedStoryListReplyPtr NewsBlurClient::loadFeedStories(int feedId, int highestLoadedPage,
                                                        bool shouldLoadClassifiers, const QStringList &childIdsToIgnore,
                                                      NewsBlurEnum::ClassificationFilter filter, int preferredStoryCount)
{
    FeedStoryListReplyPtr reply(new FeedStoryListReply);
    reply->setRequestType(NewsBlurClient::TpLoadMoreFeedStories);
    reply->m_feedId = feedId;
    reply->m_highestLoadedPage = highestLoadedPage;
    reply->m_ignoredChildIds = childIdsToIgnore;
    reply->m_shouldLoadClassifiers = shouldLoadClassifiers;
    reply->m_preferredStoryCount = preferredStoryCount;
    reply->m_filter = filter;

    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_pageParameter,
                                              QString::number(reply->m_highestLoadedPage + 1));
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtFeedStories, "/" + QString::number(feedId), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpLoadNewFeedStories request and returns the reply related to it.
  \a feedId is the identifier of the feed for which stories will be requested, starting from
  the first page, until a story is reached whos id is contained in the \a childIdsToIgnore parameter,
  or until no more stories can be requested.
*/
FeedStoryListReplyPtr NewsBlurClient::loadNewFeedStories(int feedId, const QStringList &childIdsToIgnore)
{
    FeedStoryListReplyPtr reply(new FeedStoryListReply);
    reply->setRequestType(NewsBlurClient::TpLoadNewFeedStories);
    reply->m_ignoredChildIds = childIdsToIgnore;
    reply->m_feedId = feedId;
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_pageParameter, QString::number(++reply->m_lastPageRequested));
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtFeedStories, "/" + QString::number(feedId), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpLoadMoreRiverStories request and returns the reply related to it.
  \a feedIds contains the identifiers of the feeds for which stories will be requested, starting from
  the \a highestLoadedPage + 1'th page.
  Stories with ids contained in the \a childIdsToIgnore parameter won't be added to the reply.
  If less then the \a preferredStoryCount number of stories was retrieved which match the
  given \a filter, additional pages of stories will be retrieved until the preffered number
  of stories is reached, or until no more stories can be requested.
*/
StoryListReplyPtr NewsBlurClient::loadRiverStories(int highestLoadedPage,
                                                   const QStringList &childIdsToIgnore, const QList<int>& feedIds,
                                                   NewsBlurEnum::ClassificationFilter filter, int preferredStoryCount)
{
    StoryListReplyPtr reply(new StoryListReply);
    reply->setRequestType(NewsBlurClient::TpLoadMoreRiverStories);
    reply->m_highestLoadedPage = highestLoadedPage;
    reply->m_ignoredChildIds = childIdsToIgnore;
    reply->m_feedIds = feedIds;    
    reply->m_preferredStoryCount = preferredStoryCount;
    reply->m_filter = filter;
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_pageParameter, QString::number(reply->m_highestLoadedPage + 1));
    foreach(int feedId, feedIds)
        parameters << qMakePair<QString, QString>(NewsBlurClient::s_feedsParameter, QString::number(feedId));
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtRiverStories, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpUpdateRiverStories request and returns the reply related to it.
  \a feedIds contains the identifiers of the feeds for which stories will be requested, starting from
  the first page, until a story is reached whos id is contained in the \a childIdsToIgnore parameter,
  or an older story is reached then the \a latestStoryDate
  or until no more stories can be requested.
*/
StoryListReplyPtr NewsBlurClient::loadNewRiverStories(const QStringList &childIdsToIgnore, const QList<int>& feedIds, const QDateTime &latestStoryDate)
{
    StoryListReplyPtr reply(new StoryListReply);
    reply->setRequestType(NewsBlurClient::TpUpdateRiverStories);
    reply->m_ignoredChildIds = childIdsToIgnore;
    reply->m_feedIds = feedIds;
    reply->m_latestStoryDate = latestStoryDate;
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_pageParameter, QString::number(++reply->m_lastPageRequested));
    foreach(int feedId, reply->m_feedIds)
        parameters << qMakePair<QString, QString>(NewsBlurClient::s_feedsParameter, QString::number(feedId));
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtRiverStories, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpLoadMoreStarredStories request and returns the reply related to it.
  Stories will be requested, starting from the \a highestLoadedPage + 1'th page.
  Stories with ids contained in the \a childIdsToIgnore parameter won't be added to the reply.
*/
StoryListReplyPtr NewsBlurClient::loadStarredStories(int highestLoadedPage, const QStringList &childIdsToIgnore)
{
    StoryListReplyPtr reply(new StoryListReply);
    reply->setRequestType(NewsBlurClient::TpLoadMoreStarredStories);
    reply->m_highestLoadedPage = highestLoadedPage;
    reply->m_ignoredChildIds = childIdsToIgnore;
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_pageParameter, QString::number(reply->m_highestLoadedPage + 1));
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtStarredStories, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpLoadNewStarredStories request and returns the reply related to it.
  Stories will be requested starting from the first page until a story with an id is reached
  which is contained in the \a childIdsToIgnore parameter, or until no more stories
  can be requested.
*/
StoryListReplyPtr NewsBlurClient::loadNewStarredStories(const QStringList &childIdsToIgnore)
{
    StoryListReplyPtr reply(new StoryListReply);
    reply->setRequestType(NewsBlurClient::TpLoadNewStarredStories);
    reply->m_ignoredChildIds = childIdsToIgnore;
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_pageParameter, QString::number(++reply->m_lastPageRequested));
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtStarredStories, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpMarkStoryRead request and returns the reply related to it.
  The story with the given \a storyId and \a feedId will be marked as read.
*/
WebServiceReplyPtr NewsBlurClient::markStoryAsRead(int feedId, const QString &storyId)
{
    NewsBlurReplyPtr reply(new NewsBlurReply);
    reply->setRequestType(NewsBlurClient::TpMarkStoryRead);
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_feedIdParameter, QString::number(feedId));
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_storyIdParameter, storyId);
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtMarkStoryRead, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpMarkStoriesRead request and returns the reply related to it.
  The \a storyIds parameter should contain the ids of the stories which should be marked
  as read, sorted by their feed ids.
*/
MarkStoriesReadReplyPtr NewsBlurClient::markStoriesAsRead(const QHash<int, QStringList> storyIds)
{
    QVariantMap map;
    foreach(int feedId, storyIds.keys())
    {
        QVariantList list;
        foreach(const QString& storyId, storyIds.value(feedId))
            list << storyId;
        map.insert(QString::number(feedId), list);
    }
    QString data = Json::serialize(map);

    MarkStoriesReadReplyPtr reply(new MarkStoriesReadReply);
    reply->setRequestType(NewsBlurClient::TpMarkStoriesRead);
    reply->m_storyIds = storyIds;
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_feedStoriesParameter, data);
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtMarkFeedStoriesRead, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpMarkStoryUnread request and returns the reply related to it.
  The story with the given \a storyId and \a feedId will be marked as unread.
*/
WebServiceReplyPtr NewsBlurClient::markStoryAsUnread(int feedId, const QString &storyId)
{
    NewsBlurReplyPtr reply(new NewsBlurReply);
    reply->setRequestType(NewsBlurClient::TpMarkStoryUnread);
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_feedIdParameter, QString::number(feedId));
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_storyIdParameter, storyId);
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtMarkStoryUnread, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpSaveStory request and returns the reply related to it.
  The story with the given \a storyId and \a feedId will be marked as starred or saved.
*/
WebServiceReplyPtr NewsBlurClient::saveStory(int feedId, const QString &storyId)
{
    NewsBlurReplyPtr reply(new NewsBlurReply);
    reply->setRequestType(NewsBlurClient::TpSaveStory);
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_feedIdParameter, QString::number(feedId));
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_storyIdParameter, storyId);
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtMarkStoryStarred, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpUnsaveStory request and returns the reply related to it.
  The starred or saved mark will be removed from the story with the given \a storyId and \a feedId.
*/
WebServiceReplyPtr NewsBlurClient::unSaveStory(const QString &storyId)
{
    NewsBlurReplyPtr reply(new NewsBlurReply);
    reply->setRequestType(NewsBlurClient::TpUnsaveStory);
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_storyIdParameter, storyId);
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtMarkStoryUnstarred, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpMarkFeedRead request and returns the reply related to it.
  Every story in the feed with the given \a feedId will be marked as read.
*/
WebServiceReplyPtr NewsBlurClient::markFeedAsRead(int feedId)
{
    NewsBlurReplyPtr reply(new NewsBlurReply);
    reply->setRequestType(NewsBlurClient::TpMarkFeedRead);
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_feedIdParameter, QString::number(feedId));
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtMarkFeedRead, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpMarkFolderRead request and returns the reply related to it.
  Feeds whos ids are contained in the \a feedIds parameter will be marked as read.
*/
WebServiceReplyPtr NewsBlurClient::markFolderAsRead(const QList<int> &feedIds)
{
    NewsBlurReplyPtr reply(new NewsBlurReply);
    reply->setRequestType(NewsBlurClient::TpMarkFolderRead);
    QList<QPair<QString, QString> > parameters;
    foreach(int feedId, feedIds)
        parameters << qMakePair<QString, QString>(NewsBlurClient::s_feedIdParameter, QString::number(feedId));
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtMarkFeedRead, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpMarkEverythingRead request and returns the reply related to it.
  Every story in every feed is marked as read.
*/
WebServiceReplyPtr NewsBlurClient::markEverythingAsRead()
{
    NewsBlurReplyPtr reply(new NewsBlurReply);
    reply->setRequestType(NewsBlurClient::TpMarkEverythingRead);
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtMarkAllRead);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpRenameFeed request and returns the reply related to it.
  The name or title of the feed with the id \a feedId will be set to the
  given \a name.
*/
WebServiceReplyPtr NewsBlurClient::renameFeed(int feedId, const QString &name)
{
    NewsBlurReplyPtr reply(new NewsBlurReply);
    reply->setRequestType(NewsBlurClient::TpRenameFeed);
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_feedIdParameter, QString::number(feedId));
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_feedTitleParameter, name);
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtRenameFeed, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpRenameFolder request and returns the reply related to it.
  The folder with the given \a folderName name will be renamed to \a newFolderName,
  if it's a child of the given \a parentFolder.
*/
WebServiceReplyPtr NewsBlurClient::renameFolder(const QString parentFolder, const QString& folderName, const QString newFolderName)
{
    NewsBlurReplyPtr reply(new NewsBlurReply);
    reply->setRequestType(NewsBlurClient::TpRenameFolder);
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_folderNameParameter, folderName);
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_inFolderParameter, parentFolder);
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_newFolderNameParameter, newFolderName);
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtRenameFolder, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpDeleteFeed request and returns the reply related to it.
  The with with the id of \a feedId will be removed from the given \a parentFolder.
*/
WebServiceReplyPtr NewsBlurClient::deleteFeed(int feedId, const QString &parentFolder)
{
    NewsBlurReplyPtr reply(new NewsBlurReply);
    reply->setRequestType(NewsBlurClient::TpDeleteFeed);
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_feedIdParameter, QString::number(feedId));
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_inFolderParameter, parentFolder);
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtDeleteFeed, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpDeleteFolder request and returns the reply related to it.
  The folder with the name \a folderName will be removed from the given \a parentFolder.
*/
WebServiceReplyPtr NewsBlurClient::deleteFolder(const QString &folderName, const QString &parentFolder)
{
    NewsBlurReplyPtr reply(new NewsBlurReply);
    reply->setRequestType(NewsBlurClient::TpDeleteFolder);
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_folderNameParameter, folderName);
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_inFolderParameter, parentFolder);
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtDeleteFolder, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpMoveFeed request and returns the reply related to it.
  The feed with the id of \a feedId will be moved from it's \a parentFolder to a
  different folder with the name \a newParentFolder.
*/
WebServiceReplyPtr NewsBlurClient::moveFeed(int feedId, const QString &parentFolder, const QString &newParentFolder)
{
    NewsBlurReplyPtr reply(new NewsBlurReply);
    reply->setRequestType(NewsBlurClient::TpMoveFeed);
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_feedIdParameter, QString::number(feedId));
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_inFolderParameter, parentFolder);
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_toFolderParameter, newParentFolder);
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtMoveFeed, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpMoveFolder request and returns the reply related to it.
  The folder with the name \a folderName will be moved from it's \a parentFolder to a
  different folder with the name \a newParentFolder.
*/
WebServiceReplyPtr NewsBlurClient::moveFolder(const QString &folderName, const QString &parentFolder, const QString &newParentFolder)
{
    NewsBlurReplyPtr reply(new NewsBlurReply);
    reply->setRequestType(NewsBlurClient::TpMoveFolder);
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_folderNameParameter, folderName);
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_inFolderParameter, parentFolder);
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_toFolderParameter, newParentFolder);
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtMoveFolder, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpAddFeed request and returns the reply related to it.
  A new feed will be added to the given \a parentFolder with the fiven \a feedUrl.
*/
WebServiceReplyPtr NewsBlurClient::addFeed(const QUrl &feedUrl, const QString &parentFolder)
{
    NewsBlurReplyPtr reply(new NewsBlurReply);
    reply->setRequestType(NewsBlurClient::TpAddFeed);
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_folderParameter, parentFolder);
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_urlParameter, feedUrl.toString());
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtAddFeed, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpAddFolder request and returns the reply related to it.
  A new folder with the name \a folderName will be added to an existing
  \a parentFolder.
*/
WebServiceReplyPtr NewsBlurClient::addFolder(const QString &folderName, const QString &parentFolder)
{
    NewsBlurReplyPtr reply(new NewsBlurReply);
    reply->setRequestType(NewsBlurClient::TpAddFolder);
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_folderParameter, folderName);
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_parentFolderParameter, parentFolder);
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtAddFolder, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpFindFeed request and returns the reply related to it.
  Returns the list of feed suggestions for the given \a searchTerms.
*/
FeedSearchResultsReplyPtr NewsBlurClient::findFeed(const QStringList &searchTerms)
{
    FeedSearchResultsReplyPtr reply(new FeedSearchResultsReply);
    reply->setRequestType(NewsBlurClient::TpFindFeed);
    QList<QPair<QString, QString> > parameters;
    foreach(const QString& term, searchTerms)
        parameters << qMakePair<QString, QString>(NewsBlurClient::s_termParameter, term);
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtFeedAutocomplete, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpVerifyAccount request and returns the reply related to it.
  Verifies the validity of the given \a userName and \a password pair.
*/
WebServiceReplyPtr NewsBlurClient::verifyAccount(const QString &userName, const QString &password)
{
    NewsBlurReplyPtr reply(new NewsBlurReply);
    reply->setRequestType(NewsBlurClient::TpVerifyAccount);
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_userNameParameter, userName);
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_passwordParameter, password);
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtLogin, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpRefreshUnreadCounts request and returns the reply related to it.
*/
UnreadCountsReplyPtr NewsBlurClient::updateUnreadCounts()
{
    UnreadCountsReplyPtr reply(new UnreadCountsReply);
    reply->setRequestType(NewsBlurClient::TpRefreshUnreadCounts);
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtRefreshUnreadCounts);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpFeedClassifiers request and returns the reply related to it.
  The \a feedId parameter specifies for which feed the intelligence classifiers should be
  requested.
*/
FeedClassifiersReplyPtr NewsBlurClient::feedClassifiers(int feedId)
{
    FeedClassifiersReplyPtr reply(new FeedClassifiersReply);
    reply->setRequestType(NewsBlurClient::TpFeedClassifiers);
    reply->m_feedId = feedId;
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_feedIdParameter, QString::number(feedId));
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtFeedTrainer, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Sends a NewsBlurClient::TpSaveClassifiers request and returns the reply related to it.
  Saves the intelligence \a classifiers of the feed with the id \a feedId.
  The \a storyId parameter can be optionally specified. It's unknown if the server
  is using it or not.
*/
SaveFeedClassifiersReplyPtr NewsBlurClient::saveClassifiers(const FeedClassifiers& classifiers,
                                                 int feedId, const QString& storyId)
{
    SaveFeedClassifiersReplyPtr reply(new SaveFeedClassifiersReply);
    reply->setRequestType(NewsBlurClient::TpSaveClassifiers);
    reply->m_feedId = feedId;
    reply->m_storyId = storyId;
    reply->m_feedClassifiers = classifiers;
    QList<QPair<QString, QString> > parameters;
    parameters << qMakePair<QString, QString>(NewsBlurClient::s_feedIdParameter, QString::number(feedId));
    if(!storyId.isEmpty())
        parameters << qMakePair<QString, QString>(NewsBlurClient::s_storyIdParameter, storyId);
    parameters << qMakePair<QString, QString>(classifierValueToParam(classifiers.feedClassifier(), CtFeed), QString::number(feedId));
    ClassifierHash titleClassifiers = classifiers.titleClassifiers();
    ClassifierHash authorClssifiers = classifiers.authorClassifiers();
    ClassifierHash tagClassifiers = classifiers.tagClassifiers();
    foreach(const QString& title, titleClassifiers.keys())
        parameters << qMakePair<QString, QString>(classifierValueToParam(titleClassifiers.value(title), CtTitle), title);
    foreach(const QString& author, authorClssifiers.keys())
        parameters << qMakePair<QString, QString>(classifierValueToParam(authorClssifiers.value(author), CtAuthor), author);
    foreach(const QString& tag, tagClassifiers.keys())
        parameters << qMakePair<QString, QString>(classifierValueToParam(tagClassifiers.value(tag), CtTag), tag);
    QNetworkReply* networkReply = sendRequest(NewsBlurClient::RtSaveClassifiers, QString(), parameters);
    addReply(networkReply, reply);
    return reply;
}

/*!
  Returns true if an authenticated user session has been started,
  otherwise returns false.
*/
bool NewsBlurClient::isLoggedIn() const
{
    return networkAccessManager()->cookieJar()->cookiesForUrl(QUrl(NewsBlurClient::s_serviceBaseUrl)).count();
}

/*!
  Handles the \a reply of a network request and depending on the
  type and response for a request starts new requests
  if necessary.
*/
QNetworkReply* NewsBlurClient::networkRequestFinished(QNetworkReply *networkReply,
                                                      WebServiceReplyPtr webServiceReply,
                                                      int networkRequestType)
{
    //read the contents of the network reply
    QByteArray replyContents = networkReply->readAll();
    //initialize the pointer to a new reply to 0, since it's not known yet
    //if a new request will be sent or not
    QNetworkReply* newReply = 0;

    if(networkReply->error() != QNetworkReply::NoError)
    {
        onRequestFailed(webServiceReply, NewsBlurEnum::SreNetworkError);
    }
    //if no error has occurred
    else
    {
        bool parsingOk = false;
        //parse the contents of the reply
        QVariant parsedData = Json::parse(replyContents, parsingOk);
        //if the parsing failed abort the operation
        if(!parsingOk)
        {
            onRequestFailed(webServiceReply, NewsBlurEnum::SreInvalidReplyContents);
        }
        //if result value of the response is false abort the operation
        //except if it's a TpFindFeed or a TpFeedClassifiers request
        //because replies for these requests doesn't contain the result value
        else if((parsedData.toMap().value(NewsBlurClient::s_resultValue).toString() != NewsBlurClient::s_okValue)
                && (webServiceReply->requestType() != NewsBlurClient::TpFindFeed)
                && (webServiceReply->requestType() != NewsBlurClient::TpFeedClassifiers))
        {
            onRequestFailed(webServiceReply, NewsBlurEnum::SreResponseResultFalse);
        }
        //of no error have occurred during parsing continue
        else if(webServiceReply->requestType() == NewsBlurClient::TpUpdateTree)
        {
            UpdateTreeReplyPtr updateTreeReplyPtr = webServiceReply.staticCast<UpdateTreeReply>();
            if(networkRequestType == NewsBlurClient::RtLogin)
            {
                //if login failed, abort the operation
                if(parsedData.toMap().value(NewsBlurClient::s_authenticatedValue).toString() != NewsBlurClient::s_trueValue)
                    onRequestFailed(updateTreeReplyPtr, NewsBlurEnum::SreInvalidAccount);
                else //otherwise request the feeds
                    newReply = sendRequest(NewsBlurClient::RtFeeds);
            }
            else if(networkRequestType == NewsBlurClient::RtFeeds)
            {
                //create the feed items
                QMultiHash<int, FeedItemPtr> feedItems = createFeedItems(parsedData.toMap().value(NewsBlurClient::s_feedsValue).toMap());
                QList<QPair<QString, QString> > faviconParameters;
                //create the list of favicons that should be retrieved
                foreach(int feedId, feedItems.keys())
                {
                    if(!updateTreeReplyPtr->m_ignoredFavicons.contains(QString::number(feedId)))
                        faviconParameters << qMakePair<QString, QString>(NewsBlurClient::s_feedsParameter, QString::number(feedId));
                }
                //create the folder items and add feed items to their parent folders
                QList<PageItemPtr> topLevelItems = createFolderItems(parsedData.toMap().value(NewsBlurClient::s_foldersValue).toList(),
                                                                     PageItemPtr(), &feedItems, &updateTreeReplyPtr->m_folderItems);
                updateTreeReplyPtr->m_feedItems = feedItems;
                updateTreeReplyPtr->m_rootItem->setChildItems(topLevelItems);
                updateTreeReplyPtr->m_starredStoryCount = parsedData.toMap().value(NewsBlurClient::s_starredCountValue).toInt();

                //create the feed of starred stories
                StarredGroupPtr starredGroup = StarredGroupPtr(new StarredGroup());
                starredGroup->setEnabled(true);
                starredGroup->setItemId(QString());
                starredGroup->setCanFetchMore(true);
                starredGroup->setUnreadCount(parsedData.toMap().value(NewsBlurClient::s_starredCountValue).toInt());
                starredGroup->markAsOutOfDate(false);
                updateTreeReplyPtr->m_starredGroup = starredGroup;
                updateTreeReplyPtr->m_rootItem->prependChild(starredGroup);

                //create the everything feed
                EverythingGroupPtr everythingGroup = EverythingGroupPtr(new EverythingGroup());
                everythingGroup->setEnabled(true);
                everythingGroup->setItemId(QString());
                everythingGroup->setCanFetchMore(true);
                everythingGroup->markAsOutOfDate(false);
                updateTreeReplyPtr->m_everythingGroup = everythingGroup;
                updateTreeReplyPtr->m_rootItem->prependChild(everythingGroup);

                //if some favicons are missing, retrieve them
                if(faviconParameters.count() > 0)
                    newReply = sendRequest(NewsBlurClient::RtFavicons, QString(), faviconParameters);
                else //otherwise finish the operation
                    onRequestFinished(updateTreeReplyPtr);
            }
            else if(networkRequestType == NewsBlurClient::RtFavicons)
            {
                //load the retrieved favicons and rescale them if necessary
                QVariantMap faviconMap = parsedData.toMap();
                foreach(int feedId, updateTreeReplyPtr->m_feedItems.keys())
                {
                    if(faviconMap.contains(QString::number(feedId))
                       && !updateTreeReplyPtr->m_ignoredFavicons.contains(QString::number(feedId)))
                    {
                        QString strFavicon = faviconMap.value(QString::number(feedId)).toString();
                        if(!strFavicon.isEmpty() && (strFavicon.compare(NewsBlurClient::s_nullIconContents, Qt::CaseInsensitive) != 0))
                        {
                            QByteArray baFavicon = QByteArray::fromBase64(strFavicon.toAscii());
                            QImage favicon;
                            favicon.loadFromData(baFavicon);
                            if(favicon.size() != m_preferredFaviconSize)
                                favicon = resizeFavicon(favicon);
                            updateTreeReplyPtr->m_feedFavicons.insert(QString::number(feedId)
                                                              ,favicon);
                        }
                    }
                }
                //finish the operation
                onRequestFinished(updateTreeReplyPtr);
            }
        }
        else if(webServiceReply->requestType() == NewsBlurClient::TpLoadMoreFeedStories)
        {
            FeedStoryListReplyPtr reply = webServiceReply.staticCast<FeedStoryListReply>();
            //create the story items
            QList<StoryItemPtr> stories = createStoryItems(FeedItemPtr(), parsedData.toMap().value(NewsBlurClient::s_storiesValue).toList());
            //if the feed's classifier should be loaded and they haven't been loaded yet create them
            if((reply->m_ignoredChildIds.count() == 0) && reply->m_shouldLoadClassifiers
                    && !reply->m_classifiersLoaded)
            {
                ClassifierHash tagClassifiers;
                ClassifierHash titleClassifiers;
                ClassifierHash authorClassifiers;
                NewsBlurEnum::ClassifierValue feedClassification = NewsBlurEnum::CvNeutralClassifier;
                createFeedItemClassifiers(parsedData.toMap().value(NewsBlurClient::s_classifiersValue).toMap(),
                                          &tagClassifiers,
                                          &authorClassifiers,
                                          &titleClassifiers,
                                          &feedClassification);
                reply->m_classifiers.setTagClassifiers(tagClassifiers);
                reply->m_classifiers.setTitleClassifiers(titleClassifiers);
                reply->m_classifiers.setAuthorClassifiers(authorClassifiers);
                reply->m_classifiers.setFeedClassifier(feedClassification);
                reply->m_classifiersLoaded = true;
            }
            //if some stories were recieved this is a valid page
            if(stories.count() > 0)
                ++reply->m_highestLoadedPage;
            //loop through the recieved stories
            foreach(StoryItemPtr story, stories)
            {
                //if the story isn't loaded yet add it to the reply
                if(!reply->m_ignoredChildIds.contains(story->itemId())
                   && !reply->m_storiesParent->contains(story->itemId()))
                {
                    reply->m_storiesParent->appendChild(story);
                    if(NewsBlurEnum::filterAcceptsClassification(reply->m_filter,
                                                                 NewsBlurEnum::ClassifierValue(story->itemClassification())))
                        ++reply->m_filteredStoryCount;
                }
            }
            //if less then the preferred number of stories were recieved and more stories can be loaded
            //request additional pages of stories
            if((reply->m_filteredStoryCount < reply->m_preferredStoryCount)
                 && (stories.count() > 0))
            {
                QList<QPair<QString, QString> > parameters;
                parameters << qMakePair<QString, QString>(NewsBlurClient::s_pageParameter, QString::number(reply->m_highestLoadedPage + 1));
                newReply = sendRequest(NewsBlurClient::RtFeedStories, "/" + QString::number(reply->m_feedId), parameters);
            }
            else
            {
                //finish the operation
                onRequestFinished(reply);
            }
        }
        else if(webServiceReply->requestType() == NewsBlurClient::TpLoadNewFeedStories)
        {
            FeedStoryListReplyPtr reply = webServiceReply.staticCast<FeedStoryListReply>();
            //create the story items
            QList<StoryItemPtr> stories = createStoryItems(FeedItemPtr(), parsedData.toMap().value(NewsBlurClient::s_storiesValue).toList());
            bool reachedFirstStory = false;
            //loop through the stories
            foreach(StoryItemPtr story, stories)
            {
                //if the story isn't loaded yet add it to the reply
                if(!reply->m_ignoredChildIds.contains(story->itemId())
                   && !reply->m_storiesParent->contains(story->itemId()))
                {
                    reply->m_storiesParent->appendChild(story);
                }
                //if the story is already loaded, don't add any more stories to the reply
                if(reply->m_ignoredChildIds.contains(story->itemId()))
                {
                    reachedFirstStory = true;
                    break;
                }
            }
            //if the first loaded story is reached or no more stories can be requested
            //finish the operation
            if(reachedFirstStory || (stories.count() == 0))
            {
                onRequestFinished(reply);
            }
            else //otherwise request additional pages of stories
            {
                QList<QPair<QString, QString> > parameters;
                parameters << qMakePair<QString, QString>(NewsBlurClient::s_pageParameter,
                                                          QString::number(++reply->m_lastPageRequested));
                newReply = sendRequest(NewsBlurClient::RtFeedStories, "/" + QString::number(reply->m_feedId), parameters);
            }
        }
        else if(webServiceReply->requestType() == NewsBlurClient::TpLoadMoreRiverStories)
        {
            StoryListReplyPtr reply = webServiceReply.staticCast<StoryListReply>();
            //create the story items
            QList<StoryItemPtr> stories = createStoryItems(FeedItemPtr(), parsedData.toMap().value(NewsBlurClient::s_storiesValue).toList());
            if(stories.count() > 0)
                ++reply->m_highestLoadedPage;
            //loop through the stories
            foreach(StoryItemPtr story, stories)
            {
                //if the story isn't loaded yet add it to the reply
                if(!reply->m_storiesParent->contains(story->itemId())
                        && !reply->m_ignoredChildIds.contains(story->itemId()))
                {
                    reply->m_storiesParent->appendChild(story);
                    if(NewsBlurEnum::filterAcceptsClassification(reply->m_filter,
                                                                 NewsBlurEnum::ClassifierValue(story->itemClassification())))
                        ++reply->m_filteredStoryCount;
                }
            }
            //if less then the preferred number of stories were recieved and more stories can be loaded
            //request additional pages of stories
            if((reply->m_filteredStoryCount < reply->m_preferredStoryCount)
                 && (stories.count() > 0))
            {
                QList<QPair<QString, QString> > parameters;
                parameters << qMakePair<QString, QString>(NewsBlurClient::s_pageParameter, QString::number(reply->m_highestLoadedPage + 1));
                foreach(int feedId, reply->m_feedIds)
                    parameters << qMakePair<QString, QString>(NewsBlurClient::s_feedsParameter, QString::number(feedId));
                newReply = sendRequest(NewsBlurClient::RtRiverStories, QString(), parameters);
            }
            else //otherwise finish the operation
            {
                onRequestFinished(reply);
            }
        }
        else if(webServiceReply->requestType() == NewsBlurClient::TpUpdateRiverStories)
        {
            StoryListReplyPtr reply = webServiceReply.staticCast<StoryListReply>();
            //create the story items
            QList<StoryItemPtr> stories = createStoryItems(FeedItemPtr(), parsedData.toMap().value(NewsBlurClient::s_storiesValue).toList());
            bool reachedFirstStory = false;
            //loop through the stories
            foreach(StoryItemPtr story, stories)
            {
                //if the story is already loaded, or an older story is
                //reached then the latest loaded story don't add any more stories to the reply
                if(reply->m_ignoredChildIds.contains(story->itemId())
                   || (story->date() < reply->m_latestStoryDate))
                {
                    reachedFirstStory = true;
                    break;
                }
                //if the story isn't loaded yet add it to the reply
                if(!reply->m_storiesParent->contains(story->itemId())
                   && !reply->m_ignoredChildIds.contains(story->itemId()))
                {
                    reply->m_storiesParent->appendChild(story);
                }
            }
            //if the first loaded story is reached or no more stories can be requested
            //finish the operation
            if(reachedFirstStory || (stories.count() == 0))
            {
                onRequestFinished(reply);
            }
            else //otherwise request additional pages of stories
            {
                QList<QPair<QString, QString> > parameters;
                parameters << qMakePair<QString, QString>(NewsBlurClient::s_pageParameter, QString::number(++reply->m_lastPageRequested));
                foreach(int feedId, reply->m_feedIds)
                    parameters << qMakePair<QString, QString>(NewsBlurClient::s_feedsParameter, QString::number(feedId));
                newReply = sendRequest(NewsBlurClient::RtRiverStories, QString(), parameters);
            }
        }
        else if(webServiceReply->requestType() == NewsBlurClient::TpLoadMoreStarredStories)
        {
            StoryListReplyPtr reply = webServiceReply.staticCast<StoryListReply>();
            //create the story items
            QList<StoryItemPtr> stories = createStoryItems(FeedItemPtr(), parsedData.toMap().value(NewsBlurClient::s_storiesValue).toList());
            if(stories.count() > 0)
                ++reply->m_highestLoadedPage;
            int newStoryCount = 0;
            //loop through the stories
            foreach(StoryItemPtr story, stories)
            {
                //if the story isn't loaded yet add it to the reply
                if(!reply->m_storiesParent->contains(story->itemId())
                        && !reply->m_ignoredChildIds.contains(story->itemId()))
                {
                    reply->m_storiesParent->appendChild(story);
                    ++newStoryCount;
                }
            }
            //request additional pages of stories if no older stories were retrieved
            if((newStoryCount == 0) && (stories.count() > 0))
            {
                QList<QPair<QString, QString> > parameters;
                parameters << qMakePair<QString, QString>(NewsBlurClient::s_pageParameter, QString::number(reply->m_highestLoadedPage + 1));
                newReply = sendRequest(NewsBlurClient::RtStarredStories, QString(), parameters);
            }
            else //otherwise finish the operation
            {
                onRequestFinished(reply);
            }
        }
        else if(webServiceReply->requestType() == NewsBlurClient::TpLoadNewStarredStories)
        {
            StoryListReplyPtr reply = webServiceReply.staticCast<StoryListReply>();
            //create the story items
            QList<StoryItemPtr> stories = createStoryItems(FeedItemPtr(), parsedData.toMap().value(NewsBlurClient::s_storiesValue).toList());
            bool reachedFirstStory = false;
            //loop through the stories
            foreach(StoryItemPtr story, stories)
            {
                //if the story isn't loaded yet add it to the reply
                if(!reply->m_ignoredChildIds.contains(story->itemId())
                        && !reply->m_storiesParent->contains(story->itemId()))
                {
                    reply->m_storiesParent->appendChild(story);
                }
                //if the story is already loaded don't add any more stories
                if(reply->m_ignoredChildIds.contains(story->itemId()))
                {
                    reachedFirstStory = true;
                    break;
                }
            }
            //if the first story is reached or no more stories can be requested
            //finishe the operation
            if(reachedFirstStory || (stories.count() == 0))
            {
                onRequestFinished(reply);
            }
            else //otherwise reqeust additional pages of stories
            {
                QList<QPair<QString, QString> > parameters;
                parameters << qMakePair<QString, QString>(NewsBlurClient::s_pageParameter, QString::number(++reply->m_lastPageRequested));
                newReply = sendRequest(NewsBlurClient::RtStarredStories, QString(), parameters);
            }
        }
        else if(webServiceReply->requestType() == NewsBlurClient::TpFindFeed)
        {
            FeedSearchResultsReplyPtr reply = webServiceReply.staticCast<FeedSearchResultsReply>();
            QVariantList itemList = parsedData.toList();
            foreach(const QVariant& item, itemList)
            {
                QVariantMap itemMap = item.toMap();
                reply->m_feedTitles << itemMap.value(NewsBlurClient::s_labelValue).toString();
                reply->m_urls << itemMap.value(NewsBlurClient::s_valueValue).toString();
            }
            onRequestFinished(reply);
        }
        else if(webServiceReply->requestType() == NewsBlurClient::TpRefreshUnreadCounts)
        {
            UnreadCountsReplyPtr reply = webServiceReply.staticCast<UnreadCountsReply>();
            QVariantMap feedsMap = parsedData.toMap().value(NewsBlurClient::s_feedsValue).toMap();
            foreach(const QString& feedId, feedsMap.keys())
            {
                QVariantMap unreadCountMap = feedsMap.value(feedId).toMap();
                QHash<NewsBlurEnum::ClassifierValue, int> unreadCountsPerClassification;
                unreadCountsPerClassification.insert(NewsBlurEnum::CvNeutralClassifier,
                                                     unreadCountMap.value(NewsBlurClient::s_neutralCountValue).toInt());
                unreadCountsPerClassification.insert(NewsBlurEnum::CvNegativeClassifier,
                                                     unreadCountMap.value(NewsBlurClient::s_negativeCountValue).toInt());
                unreadCountsPerClassification.insert(NewsBlurEnum::CvPositiveClassifier,
                                                     unreadCountMap.value(NewsBlurClient::s_positiveCountValue).toInt());
                reply->m_unreadCounts.insert(feedId.toInt(), unreadCountsPerClassification);
            }
            onRequestFinished(reply);
        }
        else if(webServiceReply->requestType() == NewsBlurClient::TpVerifyAccount)
        {
            NewsBlurReplyPtr reply = webServiceReply.staticCast<NewsBlurReply>();
            if(networkRequestType == NewsBlurClient::RtLogin)
            {
                //if login was successful then logout
                if(parsedData.toMap().value(NewsBlurClient::s_authenticatedValue).toString() == NewsBlurClient::s_trueValue)
                {
                    newReply = sendRequest(NewsBlurClient::RtLogout);
                }
                else
                {
                    onRequestFailed(reply, NewsBlurEnum::SreInvalidAccount);
                }
            }
            else if(networkRequestType == NewsBlurClient::RtLogout)
            {
                //clear cookies after a logout
                networkAccessManager()->setCookieJar(new QNetworkCookieJar());
                onRequestFinished(reply);
            }
        }
        else if(webServiceReply->requestType() == NewsBlurClient::TpFeedClassifiers)
        {
            FeedClassifiersReplyPtr reply = webServiceReply.staticCast<FeedClassifiersReply>();
            QVariantList classifierList = parsedData.toList();
            if(classifierList.count())
            {
                QVariantMap map = classifierList.first().toMap();
                QVariantList tagList = map.value(NewsBlurClient::s_feedTagsValue).toList();
                ClassifierHash tagClassifiers;
                ClassifierHash titleClassifiers;
                ClassifierHash authorClassifiers;
                NewsBlurEnum::ClassifierValue feedClassification = NewsBlurEnum::CvNeutralClassifier;
                foreach(const QVariant& tag, tagList)
                    tagClassifiers.insert(tag.toList().first().toString(), NewsBlurEnum::CvNeutralClassifier);
                QVariantList authorList = map.value(NewsBlurClient::s_feedAuthorsValue).toList();
                foreach(const QVariant& author, authorList)
                    authorClassifiers.insert(author.toList().first().toString(), NewsBlurEnum::CvNeutralClassifier);
                createFeedItemClassifiers(map.value(NewsBlurClient::s_classifiersValue).toMap(),
                                          &tagClassifiers,
                                          &authorClassifiers,
                                          &titleClassifiers,
                                          &feedClassification);
                reply->m_feedClassifiers.setTagClassifiers(tagClassifiers);
                reply->m_feedClassifiers.setTitleClassifiers(titleClassifiers);
                reply->m_feedClassifiers.setAuthorClassifiers(authorClassifiers);
                reply->m_feedClassifiers.setFeedClassifier(feedClassification);
            }
            onRequestFinished(reply);
        }
        else /*if((webServiceReply->requestType() == NewsBlurClient::TpMarkStoryRead)
                || (webServiceReply->requestType() == NewsBlurClient::TpMarkStoryUnread)
                || (webServiceReply->requestType() == NewsBlurClient::TpSaveStory)
                || (webServiceReply->requestType() == NewsBlurClient::TpUnsaveStory)
                || (webServiceReply->requestType() == NewsBlurClient::TpMarkFeedRead)
                || (webServiceReply->requestType() == NewsBlurClient::TpMarkFolderRead)
                || (webServiceReply->requestType() == NewsBlurClient::TpMarkEverythingRead)
                || (webServiceReply->requestType() == NewsBlurClient::TpRenameFeed)
                || (webServiceReply->requestType() == NewsBlurClient::TpRenameFolder)
                || (webServiceReply->requestType() == NewsBlurClient::TpDeleteFeed)
                || (webServiceReply->requestType() == NewsBlurClient::TpDeleteFolder)
                || (webServiceReply->requestType() == NewsBlurClient::TpMoveFeed)
                || (webServiceReply->requestType() == NewsBlurClient::TpMoveFolder)
                || (webServiceReply->requestType() == NewsBlurClient::TpAddFeed)
                || (webServiceReply->requestType() == NewsBlurClient::TpAddFolder)
                || (webServiceReply->requestType() == NewsBlurClient::TpSaveClassifiers)
                || (webServiceReply->requestType() == NewsBlurClient::TpMarkStoriesRead))*/
        {
            onRequestFinished(webServiceReply);
        }
    }
    return newReply;
}
} // namespace NewsBlur
