/*
 *   This file is part of Dianara
 *   Copyright 2012-2019  JanKusanagi JRR <jancoding@gmx.com>
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the
 *   Free Software Foundation, Inc.,
 *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA .
 */

#include "timeline.h"

TimeLine::TimeLine(PumpController::requestTypes timelineType,
                   PumpController *pumpController,
                   GlobalObject *globalObject,
                   FilterChecker *filterChecker,
                   QWidget *parent) :  QWidget(parent)
{
    m_pumpController = pumpController;
    connect(m_pumpController, &PumpController::timelineFailed,
            this, &TimeLine::onUpdateFailed);
    m_globalObject = globalObject;
    m_filterChecker = filterChecker;

    m_timelineType = timelineType;
    m_isFavoritesTimeline = false; // Initialize

    this->setMinimumSize(180, 180); // Ensure something's always visible


    // Simulated data for demo posts
    QVariantMap demoLocationData;
    demoLocationData.insert("displayName",  "Demoville");

    QVariantMap demoAuthorData;
    demoAuthorData.insert("displayName",    "Demo User");
    demoAuthorData.insert("id",             "demo@somepump.example");
    demoAuthorData.insert("url",            "https://jancoding.wordpress.com/dianara");
    demoAuthorData.insert("location",       demoLocationData);
    demoAuthorData.insert("summary",        "I am not a real user");

    QVariantMap demoGeneratorData;
    demoGeneratorData.insert("displayName", "Dianara");

    QVariantMap demoObjectData;
    demoObjectData.insert("objectType",     "note");
    demoObjectData.insert("id",             "demo-post-id");
#if 0 // =1 for quick ImageViewer tests
    demoObjectData.insert("objectType",     "image");
    QVariantMap demoImageData;
    demoImageData.insert("url", "http://dianara.nongnu.org/images/mageia.jpg");
    demoObjectData.insert("fullImage", demoImageData);
#endif

    // Show date/time when latest stable version was released
    demoObjectData.insert("published",      "2019-12-27T20:00:00Z");


    QSettings settings; // FIXME: kinda tmp, until posts have "unread" status, etc.
    settings.beginGroup("TimelineStates");

    // Demo post content depends on timeline type; also, restore some feed values
    switch (m_timelineType)
    {
    case PumpController::MainTimelineRequest:
        demoObjectData.insert("displayName", tr("Welcome to Dianara"));
        demoObjectData.insert("content",
                              tr("Dianara is a <b>Pump.io</b> client.")
                              + "<br>"

                              + tr("If you don't have a Pump account yet, you can get one "
                                   "at the following address, for instance:")
                              + "<br>"
                                "<a href=\"http://pump.io/tryit.html\">"
                                "http://pump.io/tryit.html</a>"
                                "<br><br>"

                              + tr("Press <b>F1</b> if you want to open the Help window.")
                              + "<br><br>"

                              + tr("First, configure your account from the "
                                   "<b>Settings - Account</b> menu.")
                              + " "
                              + tr("After the process is done, your profile "
                                   "and timelines should update automatically.")
                              + "<br><br>"

                              + tr("Take a moment to look around the menus and "
                                   "the Configuration window.")
                              + "<br><br>"

                              + tr("You can also set your profile data and picture from "
                                   "the <b>Settings - Edit Profile</b> menu.")
                              + "<br><br>"

                              + tr("There are tooltips everywhere, so if you "
                                   "hover over a button or a text field with "
                                   "your mouse, you'll probably see some "
                                   "extra information.")
                              + "<br><br>"

                              + "<a href=\"https://jancoding.wordpress.com/dianara\">"
                              + tr("Dianara's blog") + "</a><br><br>"
                                "<a href=\"https://pumpio.readthedocs.io/en/latest/userguide.html\">"
                              + tr("Pump.io User Guide")
                              + "</a>"
                              + "<br><br>");

        m_previousNewestPostId = settings.value("previousNewestPostIdMain")
                                         .toString();
        m_fullTimelinePostCount = settings.value("totalPostsMain").toInt();
        break;


    case PumpController::DirectTimelineRequest:
        demoObjectData.insert("displayName",  tr("Direct Messages Timeline"));
        demoObjectData.insert("content",      tr("Here, you'll see posts "
                                                 "specifically directed to you.")
                                              + "<br><br><br>");
        m_previousNewestPostId = settings.value("previousNewestPostIdDirect")
                                         .toString();
        m_fullTimelinePostCount = settings.value("totalPostsDirect").toInt();
        break;


    case PumpController::ActivityTimelineRequest:
        demoObjectData.insert("displayName", tr("Activity Timeline"));
        demoObjectData.insert("content",     tr("You'll see your own posts here.")
                                             + "<br><br><br>");
        m_previousNewestPostId = settings.value("previousNewestPostIdActivity")
                                         .toString();
        m_fullTimelinePostCount = settings.value("totalPostsActivity").toInt();
        break;


    case PumpController::FavoritesTimelineRequest:
        demoObjectData.insert("displayName", tr("Favorites Timeline"));
        demoObjectData.insert("content",     tr("Posts and comments you've liked.")
                                             + "<br><br><br>");
        m_previousNewestPostId = settings.value("previousNewestPostIdFavorites")
                                         .toString();
        m_fullTimelinePostCount = settings.value("totalPostsFavorites").toInt();

        m_isFavoritesTimeline = true;
        break;



    default:
        demoObjectData.insert("content", "<h2>Empty timeline</h2>");

    }
    settings.endGroup();


    QVariantMap demoPostData;
    demoPostData.insert("actor",          demoAuthorData);
    demoPostData.insert("generator",      demoGeneratorData);
    demoPostData.insert("object",         demoObjectData);
    demoPostData.insert("id",             "demo-activity-id");


    m_firstLoad = true;
    m_gettingNew = true; // First time should be true

    m_unreadPostsCount = 0;
    m_timelineOffset = 0;
    m_oldTimelineOffset = 0;
    m_wasOnFirstPage = true;
    m_postsPendingForNextTime = 0;

    this->syncPostsPerPage();


    // Separator frame, to mark where new posts from the last batch end
    m_separatorFrame = new QFrame(this);
    m_separatorFrame->setFrameStyle(QFrame::HLine);
    m_separatorFrame->setMinimumHeight(28);
    m_separatorFrame->setContentsMargins(0, 8, 0, 8);
    m_separatorFrame->hide();


    // Info label, shown when there are no posts, or to indicate loading and such
    m_infoLabel = new QLabel(this);
    m_infoLabel->setAlignment(Qt::AlignCenter);
    m_infoLabel->setWordWrap(true);
    m_infoLabel->setSizePolicy(QSizePolicy::MinimumExpanding,
                               QSizePolicy::Expanding);
    m_infoLabel->hide();


    m_getNewPendingButton = new QPushButton(QIcon::fromTheme("view-refresh",
                                                             QIcon(":/images/menu-refresh.png")),
                                            "*get more pending messages*",
                                            this);
    m_getNewPendingButton->setFlat(true);
    connect(m_getNewPendingButton, &QAbstractButton::clicked,
            this, &TimeLine::getNewPending);
    m_getNewPendingButton->hide();


    // This will hold the posts and be hidden or disabled when needed
    m_postsWidget = new QWidget(this);
    m_postsWidget->setContentsMargins(0, 0, 0, 0);
    /*
     * Allow focus in the post holder itself, to avoid focus going to
     * the pagination buttons when cancelling post creation in the publisher
     *
     */
    m_postsWidget->setFocusPolicy(Qt::StrongFocus);

    if (m_timelineType == PumpController::UserTimelineRequest)
    {
        m_postsWidget->hide(); // Will be shown when ready
        // Hiding it now ensures the infoLabel messages appear well centered
    }


    m_firstPageButton = new QPushButton(QIcon::fromTheme("go-first",
                                                         QIcon(":/images/button-previous.png")),
                                        tr("Newest"),
                                        this);
    m_firstPageButton->setFocusPolicy(Qt::ClickFocus);
    connect(m_firstPageButton, &QAbstractButton::clicked,
            this, &TimeLine::goToFirstPage);


    m_previousPageButton = new QPushButton(QIcon::fromTheme("go-previous",
                                                            QIcon(":/images/button-previous.png")),
                                           tr("Newer"),
                                           this);
    m_previousPageButton->setFocusPolicy(Qt::ClickFocus);
    connect(m_previousPageButton, &QAbstractButton::clicked,
            this, &TimeLine::goToPreviousPage);


    m_pageSelector = new PageSelector(this);
    connect(m_pageSelector, &PageSelector::pageJumpRequested,
            this, &TimeLine::goToSpecificPage);

    m_currentPageButton = new QPushButton(QIcon::fromTheme("go-next-view-page"),
                                          "1 / 1",  // Correct value will be set on real update
                                          this);
    m_currentPageButton->setSizePolicy(QSizePolicy::MinimumExpanding,
                                       QSizePolicy::Maximum);
    m_currentPageButton->setFocusPolicy(Qt::ClickFocus);
    connect(m_currentPageButton, &QAbstractButton::clicked,
            this, &TimeLine::showPageSelector);


    m_nextPageButton = new QPushButton(QIcon::fromTheme("go-next",
                                                        QIcon(":/images/button-next.png")),
                                       tr("Older"),
                                       this);
    m_nextPageButton->setFocusPolicy(Qt::ClickFocus);
    connect(m_nextPageButton, &QAbstractButton::clicked,
            this, &TimeLine::goToNextPage);


    // Set reversed icons for bottom buttons if RTL language causes reversed layout
    if (qApp->layoutDirection() == Qt::RightToLeft)
    {
        m_firstPageButton->setIcon(QIcon::fromTheme("go-last",
                                                    QIcon(":/images/button-next.png")));
        m_previousPageButton->setIcon(QIcon::fromTheme("go-next",
                                                       QIcon(":/images/button-next.png")));
        m_nextPageButton->setIcon(QIcon::fromTheme("go-previous",
                                                   QIcon(":/images/button-previous.png")));
    }




    ///// Layout
    m_postsLayout = new QVBoxLayout();
    m_postsLayout->setContentsMargins(0, 0, 0, 0);
    m_postsWidget->setLayout(m_postsLayout);
    // Setting alignment of this layout to AlignTop caused all posts to be
    // compressed at the top when there were a lot of them; removed



    m_bottomLayout = new QHBoxLayout();
    m_bottomLayout->addSpacing(2);
    m_bottomLayout->addWidget(m_firstPageButton,    3);
    m_bottomLayout->addSpacing(2);
    m_bottomLayout->addStretch(1);
    m_bottomLayout->addSpacing(2);
    m_bottomLayout->addWidget(m_previousPageButton, 3);
    m_bottomLayout->addWidget(m_currentPageButton,  1);
    m_bottomLayout->addWidget(m_nextPageButton,     3);
    m_bottomLayout->addSpacing(2);


    m_mainLayout = new QVBoxLayout();
    m_mainLayout->setContentsMargins(0, 0, 0, 0);
    m_mainLayout->addWidget(m_getNewPendingButton);
    m_mainLayout->addWidget(m_infoLabel,   2);
    m_mainLayout->addWidget(m_postsWidget, 1);
    m_mainLayout->addStretch(0); // Ensure buttons are always at the bottom
    m_mainLayout->addSpacing(2);              // 2 pixel separation
    m_mainLayout->addLayout(m_bottomLayout, 0);

    this->setLayout(m_mainLayout);


    this->createKeyboardActions();


    // Add the default "demo" post
    if (timelineType != PumpController::UserTimelineRequest)
    {
        ASActivity *demoActivity = new ASActivity(demoPostData, QString(), this);
        Post *demoPost = new Post(demoActivity,
                                  false, // Not highlighted
                                  false, // Not standalone
                                  m_pumpController,
                                  m_globalObject,
                                  this);
        m_postsInTimeline.append(demoPost);
        m_postsLayout->addWidget(demoPost);
    }
    else
    {
        this->showMessage(tr("Requesting..."));
    }



    // Sync avatar's follow state for every post when there are changes in the Following list
    connect(m_pumpController, &PumpController::followingListChanged,
            this, &TimeLine::updateAvatarFollowStates);
    // Also, sort the Following list completion model

    // Disable buttons initially, until something is received
    this->disablePaginationButtons();

    qDebug() << "TimeLine created";
}


/*
 * Destructor stores timeline states in the settings
 *
 */
TimeLine::~TimeLine()
{
    QSettings settings;
    settings.beginGroup("TimelineStates");

    switch (m_timelineType)
    {
    case PumpController::MainTimelineRequest:
        settings.setValue("previousNewestPostIdMain",
                          m_previousNewestPostId);
        settings.setValue("totalPostsMain",
                          m_fullTimelinePostCount);
        break;

    case PumpController::DirectTimelineRequest:
        settings.setValue("previousNewestPostIdDirect",
                          m_previousNewestPostId);
        settings.setValue("totalPostsDirect",
                          m_fullTimelinePostCount);
        break;

    case PumpController::ActivityTimelineRequest:
        settings.setValue("previousNewestPostIdActivity",
                          m_previousNewestPostId);
        settings.setValue("totalPostsActivity",
                          m_fullTimelinePostCount);
        break;

    case PumpController::FavoritesTimelineRequest:
        settings.setValue("previousNewestPostIdFavorites",
                          m_previousNewestPostId);
        settings.setValue("totalPostsFavorites",
                          m_fullTimelinePostCount);
        break;

    case PumpController::UserTimelineRequest:
        break;

    default:
        qDebug() << "Timeline destructor: timelineType is invalid!";
    }
    settings.endGroup();


    qDebug() << "TimeLine destroyed; Type:" << m_timelineType;
}


/*
 * QActions with shourtcuts, for better keyboard control
 *
 */
void TimeLine::createKeyboardActions()
{
    // Single step
    m_scrollUpAction = new QAction(this);
    m_scrollUpAction->setShortcut(QKeySequence("Ctrl+Up"));
    connect(m_scrollUpAction, &QAction::triggered,
            this, &TimeLine::scrollUp);
    this->addAction(m_scrollUpAction);

    m_scrollDownAction = new QAction(this);
    m_scrollDownAction->setShortcut(QKeySequence("Ctrl+Down"));
    connect(m_scrollDownAction, &QAction::triggered,
            this, &TimeLine::scrollDown);
    this->addAction(m_scrollDownAction);

    // Pages
    m_scrollPageUpAction = new QAction(this);
    m_scrollPageUpAction->setShortcut(QKeySequence("Ctrl+PgUp"));
    connect(m_scrollPageUpAction, &QAction::triggered,
            this, &TimeLine::scrollPageUp);
    this->addAction(m_scrollPageUpAction);

    m_scrollPageDownAction = new QAction(this);
    m_scrollPageDownAction->setShortcut(QKeySequence("Ctrl+PgDown"));
    connect(m_scrollPageDownAction, &QAction::triggered,
            this, &TimeLine::scrollPageDown);
    this->addAction(m_scrollPageDownAction);

    // Top / Bottom
    m_scrollTopAction = new QAction(this);
    m_scrollTopAction->setShortcut(QKeySequence("Ctrl+Home"));
    connect(m_scrollTopAction, &QAction::triggered,
            this, &TimeLine::scrollToTop);
    this->addAction(m_scrollTopAction);

    m_scrollBottomAction = new QAction(this);
    m_scrollBottomAction->setShortcut(QKeySequence("Ctrl+End"));
    connect(m_scrollBottomAction, &QAction::triggered,
            this, &TimeLine::scrollToBottom);
    this->addAction(m_scrollBottomAction);


    // Previous/Next page in timeline
    m_previousPageAction = new QAction(this);
    m_previousPageAction->setShortcut(QKeySequence("Ctrl+Left"));
    connect(m_previousPageAction, &QAction::triggered,
            m_previousPageButton, &QAbstractButton::click);
    m_previousPageButton->setToolTip(m_previousPageAction->shortcut()
                                                          .toString(QKeySequence::NativeText));
    this->addAction(m_previousPageAction);

    m_nextPageAction = new QAction(this);
    m_nextPageAction->setShortcut(QKeySequence("Ctrl+Right"));
    connect(m_nextPageAction, &QAction::triggered,
            m_nextPageButton, &QAbstractButton::click);
    m_nextPageButton->setToolTip(m_nextPageAction->shortcut() // FIXME: maybe add a clearer message
                                                  .toString(QKeySequence::NativeText));
    this->addAction(m_nextPageAction);

    // It's safer to use these QActions than setting shortcuts to buttons
    // The latter gets messed up when built with Qt 5 and run under Plasma 5.x
    m_showPageSelectorAction = new QAction(this);
    m_showPageSelectorAction->setShortcut(QKeySequence("Ctrl+G"));
    connect(m_showPageSelectorAction, &QAction::triggered,
            m_currentPageButton, &QAbstractButton::click);
    this->addAction(m_showPageSelectorAction);
}



void TimeLine::setCustomUrl(QString url)
{
    m_customUrl = url;
}



/*
 * Remove all widgets (Post *) from the timeline
 *
 */
void TimeLine::clearTimeLineContents(bool showMessage)
{
    foreach (Post *oldPost, m_postsInTimeline)
    {
        m_postsLayout->removeWidget(oldPost);
        delete oldPost;
    }
    m_postsInTimeline.clear();
    m_objectsIdList.clear();

    m_postsPendingForNextTime = 0;

    m_postsLayout->removeWidget(m_separatorFrame);
    m_separatorFrame->hide();

    if (showMessage)
    {
        this->showMessage(tr("Loading..."));
    }

    qApp->processEvents(); // So GUI gets updated
}


/*
 * Remove oldest posts from current page, to avoid ever-increasing memory usage.
 * Called after updating the timeline, only when getting newer posts on the
 * first page.
 *
 * At the very least, keep as many posts as were received in last update.
 *
 */
void TimeLine::removeOldPosts(int minimumToKeep)
{
    int maxPosts = qMax(m_postsPerPage * 2, // TMP FIXME
                        minimumToKeep);

    if (m_postsInTimeline.count() <= maxPosts)
    {
        // Not too many posts yet, so do nothing
        return;
    }

    int postCounter = 0;
    foreach (Post *post, m_postsInTimeline)
    {
        if (postCounter >= maxPosts)
        {
            if (!post->isNew()             // Don't remove if it's unread
             && !post->isBeingCommented()) // or currently being commented on
            {
                m_postsLayout->removeWidget(post);
                m_postsInTimeline.removeOne(post);
                m_objectsIdList.removeAt(postCounter);
                delete post;
            }
        }

        ++postCounter;
    }

    // Update "next" link manually, based on the last post present in the page
    QByteArray lastPostId = m_postsInTimeline.last()->getActivityId().toLocal8Bit();
    lastPostId = lastPostId.toPercentEncoding(); // Needs to be percent-encoded

    m_nextPageLink = m_pumpController->getFeedApiUrl(m_timelineType)
                   + "?before=" + lastPostId;
}



void TimeLine::insertSeparator(int position)
{
    m_postsLayout->insertWidget(position, m_separatorFrame);
    m_separatorFrame->show();
}



int TimeLine::getCurrentPage()
{
    if (m_postsPerPage == 0)
    {
        m_postsPerPage = 1;
    }

    return (m_timelineOffset / m_postsPerPage) + 1;
}


int TimeLine::getTotalPages()
{
    if (m_postsPerPage == 0)
    {
        m_postsPerPage = 1;
    }

    int totalPages = qCeil(m_fullTimelinePostCount / (float)m_postsPerPage);

    return qMax(totalPages, 1); // 1 is the minimum
}


int TimeLine::getTotalPosts()
{
    return m_fullTimelinePostCount;
}


/*
 *  Update the button at the bottom of the page, indicating current "page"
 *
 */
void TimeLine::updateCurrentPageNumber()
{
    const int currentPage = this->getCurrentPage();
    const int totalPages = this->getTotalPages();
    const QString currentPageString = QLocale::system().toString(currentPage);
    const QString totalPagesString = QLocale::system().toString(totalPages);
    const QString totalPostsString = QLocale::system()
                                             .toString(m_fullTimelinePostCount);

    m_currentPageButton->setText(QString("%1 / %2")
                                 .arg(currentPageString)
                                 .arg(totalPagesString));

    m_currentPageTotalString = tr("Page %1 of %2.").arg(currentPageString)
                                                   .arg(totalPagesString);
    m_currentPageButton->setToolTip(m_currentPageTotalString
                                    + "<br>"
                                    + tr("Showing %1 posts per page.")
                                      .arg(m_postsPerPage)
                                    + "<br>"
                                    + tr("%1 posts in total.")
                                      .arg(totalPostsString)
                                    + "<hr>"
                                      "<b><i>"
                                    + tr("Click here or press Control+G to "
                                         "jump to a specific page")
                                    + "</i></b>");

    m_previousPageButton->setDisabled(currentPage == 1); // Disabled on 1st page
    m_nextPageButton->setDisabled(currentPage == totalPages); // Disabled on last page
}



void TimeLine::syncPostsPerPage()
{
    if (m_timelineType == PumpController::MainTimelineRequest
     || m_timelineType == PumpController::UserTimelineRequest)
    {
        m_postsPerPage = m_globalObject->getPostsPerPageMain();
    }
    else
    {
        m_postsPerPage = m_globalObject->getPostsPerPageOther();
    }
}


void TimeLine::enablePaginationButtons()
{
    m_firstPageButton->setEnabled(true);
    m_currentPageButton->setEnabled(true);
    this->updateCurrentPageNumber(); // Will re-enable prev/next buttons as needed
}


void TimeLine::disablePaginationButtons()
{
    m_getNewPendingButton->setDisabled(true);

    m_firstPageButton->setDisabled(true);
    m_previousPageButton->setDisabled(true);
    m_currentPageButton->setDisabled(true);
    m_nextPageButton->setDisabled(true);
}




/*
 * Resize all posts in timeline
 *
 */
void TimeLine::resizePosts(QList<Post *> postsToResize, bool resizeAll)
{
    if (resizeAll)
    {
        postsToResize = m_postsInTimeline;
    }

    foreach (Post *post, postsToResize)
    {
        // Call setPostContents() and setPostHeight()

        // New method, disabled for 1.3.1; has some drawbacks
        //post->onResizeOrShow();

        /* -- Old method, forcing a resize */
        post->resize(post->width() - 1,
                     post->height() - 1);
        /* re-enabled for 1.3.1 */
    }
}

void TimeLine::markPostsAsRead()
{
    foreach (Post *post, m_postsInTimeline)
    {
        // Mark post as read without informing the timeline
        post->setPostAsRead(false);
    }

    m_unreadPostsCount = 0;
    m_highlightedPostsCount = 0;

    emit unreadPostsCountChanged(m_timelineType,
                                 m_unreadPostsCount,
                                 m_highlightedPostsCount,
                                 m_fullTimelinePostCount);
}


void TimeLine::updateFuzzyTimestamps()
{
    foreach (Post *post, m_postsInTimeline)
    {
        post->setFuzzyTimestamps();
    }
}


bool TimeLine::commentingOnAnyPost()
{
    foreach (Post *post, m_postsInTimeline)
    {
        if (post->isBeingCommented())
        {
            return true;
        }
    }

    return false;
}


void TimeLine::notifyBlockedUpdates()
{
    const QString tlName = PumpController::getFeedNameAndPath(m_timelineType).first();
    m_globalObject->setStatusMessage(tr("'%1' cannot be updated "
                                        "because a comment is currently "
                                        "being composed.",
                                        "%1 = feed's name").arg(tlName));
}


/*
 * Return list of pointers to Post() objects currently in the timeline
 *
 */
QList<Post *> TimeLine::getPostsInTimeline()
{
    return m_postsInTimeline;
}


QFrame *TimeLine::getSeparatorWidget()
{
    return m_separatorFrame;
}


void TimeLine::showMessage(QString message)
{
    m_infoLabel->setText("<big><b>" + message + "</b></big>");
    m_infoLabel->show();
}



/*****************************************************************************/
/*****************************************************************************/
/********************************** SLOTS ************************************/
/*****************************************************************************/
/*****************************************************************************/


void TimeLine::setTimeLineContents(QVariantList postList, QString previousLink,
                                   QString nextLink, int totalItems)
{
    qDebug() << "TimeLine::setTimeLineContents()";
    if (this->commentingOnAnyPost())
    {
        // Extra protection, see https://gitlab.com/dianara/dianara-dev/issues/35
        qDebug() << "Aborting timeline update due to comment in progress";
        this->notifyBlockedUpdates();
        return;
    }

    int postListSize = postList.size();

    // Disable to avoid clicks to posts (which would mark them as read)
    if (postListSize > 0)   // until fully updated
    {
        this->setDisabled(true);
    }

    // Remove all previous posts in timeline, when switching pages
    if (m_firstLoad || !m_wasOnFirstPage
     || m_isFavoritesTimeline || m_timelineOffset > 0)
    {
        // Hide the posts while TL reloads; helps performance a lot
        m_postsWidget->hide();

        qDebug() << "Removing previous posts from timeline";
        this->clearTimeLineContents();
        m_unreadPostsCount = 0;
        m_highlightedPostsCount = 0;

        // Ask mainWindow to scroll the QScrollArea containing the timeline to the top
        //  emit scrollTo(QAbstractSlider::SliderToMinimum);
        ////////// TMP FIXME: don't scroll to top; make it optional

        m_previousPageLink = previousLink;
        m_nextPageLink = nextLink;
        qDebug() << "Prev/Next links:" << m_previousPageLink << m_nextPageLink;
    }
    else
    {
        if (!previousLink.isEmpty())
        {
            m_previousPageLink = previousLink;
            // Just the previousLink; don't store nextLink, keep the old one
        }
    }


    int totalPostDifference = totalItems - m_fullTimelinePostCount;
    m_fullTimelinePostCount = totalItems;


    // Check how many more posts need to be received, if more than max are pending
    m_postsPendingForNextTime += totalPostDifference;
    m_postsPendingForNextTime -= postListSize;
    if (m_postsPendingForNextTime > 0)
    {
        if (m_firstLoad)
        {
            // The difference in pending posts is in the older pages, so doesn't count
            m_postsPendingForNextTime = 0;

            // FIXME 1.4.x: On first load, should display the "pending" number
            // at the bottom or at the "older" button
        }
        else
        {
            // Button at the top to fetch the pending messages, even more new stuff
            m_getNewPendingButton->setText(tr("%1 more posts pending for "
                                              "next update.")
                                           .arg(m_postsPendingForNextTime)
                                           + "   " // 3 spaces, then an alarm clock
                                           + QString::fromUtf8("\342\217\260")
                                           + "\n"
                                           + tr("Click here to receive "
                                                "them now."));
            m_getNewPendingButton->setEnabled(true);
            m_getNewPendingButton->show();
        }
    }
    else
    {
        m_postsPendingForNextTime = 0; // In case it was less than 0
        m_getNewPendingButton->hide();
    }

    // Remove the current separator line
    m_postsLayout->removeWidget(m_separatorFrame);
    m_separatorFrame->hide();


    ////////////////////////////////////// Start adding content to the timeline

    int newPostCount = 0;
    int newHighlightedPostsCount = 0;
    int newDirectPostsCount = 0;
    int newHLByFilterPostsCount = 0;
    int newDeletedPostsCount = 0;
    int newFilteredPostsCount = 0;
    bool allNewPostsCounted = false;
    int insertedPosts = 0;
    bool needToInsertSeparator = false;

    // Here we'll store the post ID for the first (newest) post in the timeline
    QString newestPostId; // (actually activity ID)
    // With it, we can know how many new posts (if any) we receive next time

    QList<Post *> postsInsertedThisTime;

    // Fill timeline with new contents
    foreach (const QVariant singlePost, postList)
    {
        if (singlePost.type() == QVariant::Map)
        {
            bool postIsNew = false;

            QVariantMap activityMap;
            // Since "Favorites" is a collection of objects, not activities,
            // we need to put "Favorites" posts into fake activities
            if (!m_isFavoritesTimeline)
            {
                // Data is already an activity
                activityMap = singlePost.toMap();
            }
            else
            {
                // Put object into the empty/fake VariantMap for the activity
                activityMap.insert("object", singlePost.toMap());
                activityMap.insert("actor",  singlePost.toMap()
                                                       .value("author").toMap());
                activityMap.insert("id",     singlePost.toMap()
                                                       .value("id").toString());
            }

            ASActivity *activity = new ASActivity(activityMap,
                                                  m_pumpController->currentUserId(),
                                                  this);

            // See if it's deleted
            QString postDeletedTime = activity->object()->getDeletedTime();

            // See if we have to filter it out (or highlight it)
            int filtered = m_filterChecker->validateActivity(activity);


            // See if we hide the post if a copy is already visible in the timeline
            bool postIsDuplicated = false;
            if (m_globalObject->getHideDuplicates()) // Depending on the setting
            {
                if (m_objectsIdList.contains(activity->object()->getId()))
                {
                    postIsDuplicated = true;
                }
            }

            if (newestPostId.isEmpty()) // only first time, for newest post
            {
                if (m_gettingNew)
                {
                    newestPostId = activity->getId();
                }
                else
                {
                    newestPostId = m_previousNewestPostId;
                    allNewPostsCounted = true;
                }
            }


            if (!allNewPostsCounted)
            {
                if (activity->getId() == m_previousNewestPostId)
                {
                    allNewPostsCounted = true;
                    if (newPostCount > 0)
                    {
                        needToInsertSeparator = true;
                    }
                }
                else
                {
                    // If post is NOT deleted or filtered, not ours, and
                    // this is not the Favorites timeline, add it to the count
                    if (postDeletedTime.isEmpty()
                        && filtered != FilterChecker::FilterOut
                        && !postIsDuplicated
                        && activity->author()->getId() != m_pumpController->currentUserId()
                        && activity->object()->author()->getId() != m_pumpController->currentUserId()
                        && !m_isFavoritesTimeline
                        && m_timelineType != PumpController::UserTimelineRequest)
                    {
                        ++newPostCount;

                        // Mark current post as new
                        postIsNew = true;
                    }
                    else
                    {
                        if (!postDeletedTime.isEmpty())
                        {
                            ++newDeletedPostsCount;
                        }
                        else if (filtered == FilterChecker::FilterOut
                              || postIsDuplicated)
                        {
                            ++newFilteredPostsCount;
                        }
                    }
                }
            }



            bool highlightedByFilter = false;
            if (filtered == FilterChecker::Highlight)
            {
                highlightedByFilter = true;
            }

            Post *newPost = new Post(activity,
                                     highlightedByFilter,
                                     false,  // NOT standalone
                                     m_pumpController,
                                     m_globalObject,
                                     this);
            if (postIsNew)
            {
                newPost->setPostAsNew();
                connect(newPost, &Post::postRead,
                        this, &TimeLine::decreaseUnreadPostsCount);

                int highlightType = newPost->getHighlightType();
                if (highlightType != Post::NoHighlight)
                {
                    ++newHighlightedPostsCount;

                    if (highlightType == Post::MessageForUserHighlight)
                    {
                        ++newDirectPostsCount;
                    }

                    if (highlightType == Post::FilterRulesHighlight)
                    {
                        ++newHLByFilterPostsCount;
                    }
                }
            }


            if (needToInsertSeparator)  // -------
            {
                this->insertSeparator(insertedPosts);
                ++insertedPosts;
                needToInsertSeparator = false;
            }


            m_objectsIdList.insert(insertedPosts, newPost->getObjectId());
            postsInsertedThisTime.append(newPost);
            m_postsLayout->insertWidget(insertedPosts, newPost);
            m_postsInTimeline.insert(insertedPosts, newPost);
            ++insertedPosts;

            // Bounce Post() signal to MainWindow
            connect(newPost, &Post::commentingOnPost,
                    this, &TimeLine::commentingOnPost);


            // If post has been filtered out or hidden because it's a duplicate
            if (filtered == FilterChecker::FilterOut || postIsDuplicated)
            {
                newPost->hide(); // For now; maybe make it so that it can be clicked to show - FIXME
                qDebug() << "Post filtered out or hidden because it's a duplicate\n"
                         << "Filter action:" << filtered
                         << "(0=filter out; 1=highlight, 999=no filtering)\n"
                         << "Duplicated:" << postIsDuplicated;
            }
        }
        else  // singlePost.type() is not a QVariant::Map
        {
            qDebug() << "Expected a Map, got something else";
            qDebug() << postList;
        }
    } // end foreach

    qApp->processEvents(); // pre-resize posts

    m_firstLoad = false;


    // If there were new posts, and separator not already added, add it: -----
    if (newPostCount > 0 && m_separatorFrame->isHidden())
    {
        this->insertSeparator(insertedPosts);
    }

    if (!newestPostId.isEmpty())
    {
        m_previousNewestPostId = newestPostId;
    }

    m_unreadPostsCount += newPostCount;
    m_highlightedPostsCount += newHighlightedPostsCount;
    qDebug() << "-----------\nNew posts:" << newPostCount
             << "\nActual total new from previous update:" << totalPostDifference
             << "\nNewest post ID:" << m_previousNewestPostId
             << "\nNew highlighted:" << newHighlightedPostsCount
             << "\n------ New direct:" << newDirectPostsCount
             << "\n------ New HL by filter:" << newHLByFilterPostsCount
             << "\nNew deleted: " << newDeletedPostsCount
             << "\nNew filtered out: " << newFilteredPostsCount
             << "\nTotal posts:" << m_fullTimelinePostCount
             << "\nTotal currently loaded posts:" << m_postsInTimeline.size();


    if (postListSize > 0)
    {
        // Resize the posts, but only the ones added in this update
        this->resizePosts(postsInsertedThisTime);
    }

    if (m_gettingNew)
    {
        emit timelineRendered(m_timelineType,
                              newPostCount, newHighlightedPostsCount,
                              newDirectPostsCount, newHLByFilterPostsCount,
                              newDeletedPostsCount, newFilteredPostsCount,
                              m_postsPendingForNextTime,
                              QString()); // Current page doesn't apply

        emit unreadPostsCountChanged(m_timelineType,
                                     m_unreadPostsCount,
                                     m_highlightedPostsCount,
                                     m_fullTimelinePostCount);

        // Clean up, keeping at least the posts that were just received
        if (postListSize > 0)  // but only if there was _something_
        {
            this->removeOldPosts(postListSize);
        }
    }
    else
    {
        this->updateCurrentPageNumber();
        emit timelineRendered(m_timelineType,
                              postListSize, -1, // Highlighted counts (direct and by filter)
                              -1, -1,           // are irrelevant in this case
                              newDeletedPostsCount, newFilteredPostsCount,
                              -1,               // And so is the "pending for next time"
                              m_currentPageTotalString);

        emit unreadPostsCountChanged(m_timelineType,
                                     0, 0,
                                     m_fullTimelinePostCount);
    }


    if (m_postsInTimeline.length() == 0)
    {
        this->showMessage(tr("There are no posts"));
        m_postsWidget->hide();
    }
    else
    {
        m_infoLabel->hide();
        m_postsWidget->show(); // Show posts again
    }

    // Enable timeline again, since everything is added and drawn
    this->setEnabled(true);
    this->enablePaginationButtons();

    qDebug() << "setTimeLineContents() /END";
}


void TimeLine::onUpdateFailed(int requestType)
{
    if (requestType == m_timelineType)
    {
        this->setEnabled(true); // Just in case

        m_timelineOffset = m_oldTimelineOffset;
        this->enablePaginationButtons();
    }
}


/*
 * Update data in all currently-visible posts matching the object ID sent
 * by the minor feed
 *
 */
void TimeLine::updatePostsFromMinorFeed(ASObject *object)
{
    /* FIXME: This should handle cases where a comment might be visible as a
     * post in the timeline (shared) _and_ be a comment in a visible post
     *
     * Also, something could be a note, therefore visible in the timeline,
     * but also be in reply to something else.
     *
     */

    if (object->getInReplyToId().isEmpty()) // Parent object
    {
        foreach (Post *post, m_postsInTimeline)
        {
            if (post->getObjectId() == object->getId())
            {
                post->updateDataFromObject(object);
            }
        }
    }
    else                                    // Reply to something
    {
        foreach (Post *post, m_postsInTimeline)
        {
            if (post->getObjectId() == object->getInReplyToId())
            {
                post->updateCommentFromObject(object);
            }
        }
    }
}



void TimeLine::addLikesFromMinorFeed(QString objectId, QString objectType,
                                     QString actorId, QString actorName,
                                     QString actorUrl)
{
    // FIXME 1.4.x: handle updating likes in comments
    foreach (Post *post, m_postsInTimeline)
    {
        if (post->getObjectId() == objectId)
        {
            post->appendLike(actorId, actorName, actorUrl);
        }
    }
}

void TimeLine::removeLikesFromMinorFeed(QString objectId, QString objectType,
                                        QString actorId)
{
    // FIXME 1.4.x: handle updating likes in comments
    foreach (Post *post, m_postsInTimeline)
    {
        if (post->getObjectId() == objectId)
        {
            post->removeLike(actorId);
        }
    }
}


/*
 * Add one single comment read from a minor feed, to the
 * corresponding parent post in this timeline
 *
 */
void TimeLine::addReplyFromMinorFeed(ASObject *object)
{
    QString parentPostId = object->getInReplyToId();

    foreach (Post *post, m_postsInTimeline)
    {
        if (post->getObjectId() == parentPostId)
        {
            post->appendComment(object);
        }
    }
}

void TimeLine::setPostsDeletedFromMinorFeed(ASObject *object)
{
    foreach (Post *post, m_postsInTimeline)
    {
        if (post->getObjectId() == object->getId())
        {
            post->setPostAsRead(true); // Just in case, and notifying timeline
            post->setPostDeleted(object->getDeletedOnString());
        }
    }

    // If the object has a parent, find it in the comments
    if (!object->getInReplyToId().isEmpty())
    {
        foreach (Post *post, m_postsInTimeline)
        {
            if (post->getObjectId() == object->getInReplyToId())
            {
                post->setCommentDeletedFromObject(object);
            }
        }
    }
}





/*
 * Add the full list of likes to a post
 *
 */
void TimeLine::setLikesInPost(QVariantList likesList, QString originatingPostUrl)
{
    // FIXME: should get proper post ID from the JSON,
    //        instead of using and splitting the replyUrl
    const QString cleanUrl = originatingPostUrl.split("?").first();
    //qDebug() << "Originating post URL:" << cleanUrl;

    // Look for the originating Post() object
    foreach (Post *post, m_postsInTimeline)
    {
        if (post->likesUrl() == cleanUrl)
        {
            qDebug() << "Found originating Post; setting likes on it...";
            post->setLikes(likesList);
            /*
             * Don't break, so likes get set in other
             * visible copies of the post too
             */
        }
    }
}


/*
 * Add the full list of comments to a post
 *
 */
void TimeLine::setCommentsInPost(QVariantList commentsList,
                                 QString originatingPostUrl)
{
    // FIXME: should get proper post ID from the JSON,
    //        instead of using and splitting the replyUrl
    const QString cleanUrl = originatingPostUrl.split("?").first();
    //qDebug() << "Originating post URL:" << cleanUrl;

    // Look for the originating Post() object
    foreach (Post *post, m_postsInTimeline)
    {
        if (post->commentsUrl() == cleanUrl)
        {
            qDebug() << "Found originating Post; setting comments on it...";
            post->setComments(commentsList);

            /* Don't break, so comments get set in copies of the post too,
               like if JohnDoe posted something and JaneDoe shared it soon
               after, so both the original post and its shared copy are visible
               in the timeline. */
        }
    }
}




void TimeLine::goToFirstPage()
{
    qDebug() << "TimeLine::goToFirstPage()";

    if (this->commentingOnAnyPost())
    {
        // Update is blocked because a post is being commented on
        this->notifyBlockedUpdates();
        return;
    }

    this->syncPostsPerPage();

    m_gettingNew = true;
    m_oldTimelineOffset = m_timelineOffset;

    if (m_timelineOffset == 0) // On page 1
    {
        m_wasOnFirstPage = true;
    }
    else
    {
        m_wasOnFirstPage = false;

        m_previousPageLink.clear();  // Full reload of newest stuff
        m_timelineOffset = 0;

        this->setDisabled(true); // Disable soon, to avoid wrong clicks
    }

    // Disable pagination buttons to avoid double-clicking problems, in case
    // the whole timeline isn't disabled (whenever we're in the first page)
    this->disablePaginationButtons();
    // The ones that make sense will be re-enabled later

    /*
     * If this is the favorites timeline, previousPageLink will be empty,
     * which means the first posts at offset 0 will be loaded anyway
     *
     */
    m_pumpController->getFeed(m_timelineType,
                              m_postsPerPage,
                              m_previousPageLink);
}



void TimeLine::goToPreviousPage()
{
    qDebug() << "TimeLine::goToPreviousPage()";
    if (this->commentingOnAnyPost())
    {
        this->notifyBlockedUpdates();
        return;
    }

    this->syncPostsPerPage();

    m_gettingNew = false;

    this->setDisabled(true); // Disable soon, to avoid wrong clicks

    m_oldTimelineOffset = m_timelineOffset;
    m_timelineOffset -= m_postsPerPage;
    if (m_timelineOffset < 0)
    {
        m_timelineOffset = 0;
    }

    if (!m_isFavoritesTimeline) // Not favorites, use PreviousLink
    {
        m_pumpController->getFeed(m_timelineType,
                                  m_postsPerPage,
                                  m_previousPageLink);
    }
    else
    {
        m_pumpController->getFeed(m_timelineType,
                                  m_postsPerPage,
                                  QString(),
                                  m_timelineOffset);
    }
}



void TimeLine::goToNextPage()
{
    qDebug() << "TimeLine::goToNextPage()";
    if (this->commentingOnAnyPost())
    {
        this->notifyBlockedUpdates();
        return;
    }

    this->syncPostsPerPage();

    m_gettingNew = false;

    this->setDisabled(true); // Disable soon, to avoid wrong clicks

    m_oldTimelineOffset = m_timelineOffset;
    m_timelineOffset += m_postsPerPage;

    if (!m_isFavoritesTimeline) // Not favorites, use NextLink
    {
        m_pumpController->getFeed(m_timelineType,
                                  m_postsPerPage,
                                  m_nextPageLink);
    }
    else // Use offset
    {
        m_pumpController->getFeed(m_timelineType,
                                  m_postsPerPage,
                                  QString(),
                                  m_timelineOffset);
    }
}


void TimeLine::goToSpecificPage(int pageNumber)
{
    qDebug() << "TimeLine::goToSpecificPage(): " << pageNumber;
    if (this->commentingOnAnyPost())
    {
        this->notifyBlockedUpdates();
        return;
    }

    this->syncPostsPerPage();

    m_gettingNew = false; // FIXME: should be true if page=1

    this->setDisabled(true); // Disable soon, to avoid wrong clicks

    m_oldTimelineOffset = m_timelineOffset;
    m_timelineOffset = (pageNumber - 1) * m_postsPerPage;

    /***************************************************************************
     * Workaround for Pump.io core bug:
     *
     *    Ensure that timelineOffset is NOT greater
     *    than (fullTimelinePostCount - postsPerPage)
     *
     * Otherwise, when requesting the last page, the server might return
     * ALL posts, even beyond API limits, instead of the last (oldest) few
     * posts.
     *
     * https://github.com/pump-io/pump.io/issues/1087
     *
     */
    if (m_timelineType == PumpController::UserTimelineRequest)
    {
        int maxTimelineOffset = m_fullTimelinePostCount - m_postsPerPage;
        m_timelineOffset = qMin(m_timelineOffset, maxTimelineOffset);
    }

    m_pumpController->getFeed(m_timelineType,
                              m_postsPerPage,
                              m_customUrl, // No prev/next links, so use possible customUrl, or empty
                              m_timelineOffset); // And use offset instead
}

/*
 * Get newest posts that were pending, explicitly from the "get pending" button.
 *
 * Needed to set focus on another widget first.
 *
 */
void TimeLine::getNewPending()
{
    // Avoid annoying focus changes when 'get pending' button is disabled
    m_postsWidget->setFocus();

    this->goToFirstPage();
}


void TimeLine::showPageSelector()
{
    m_pageSelector->showForPage(this->getCurrentPage(),
                                this->getTotalPages());
}



void TimeLine::scrollUp()
{
    emit scrollTo(QAbstractSlider::SliderSingleStepSub);
}

void TimeLine::scrollDown()
{
    emit scrollTo(QAbstractSlider::SliderSingleStepAdd);
}

void TimeLine::scrollPageUp()
{
    emit scrollTo(QAbstractSlider::SliderPageStepSub);
}

void TimeLine::scrollPageDown()
{
    emit scrollTo(QAbstractSlider::SliderPageStepAdd);
}

void TimeLine::scrollToTop()
{
    emit scrollTo(QAbstractSlider::SliderToMinimum);
}

void TimeLine::scrollToBottom()
{
    emit scrollTo(QAbstractSlider::SliderToMaximum);
}



/*
 * Decrease internal counter of unread posts (by 1), and inform
 * the parent window, so it can update its tab titles
 *
 */
void TimeLine::decreaseUnreadPostsCount(bool wasHighlighted)
{
    --m_unreadPostsCount;

    if (wasHighlighted)
    {
        --m_highlightedPostsCount;
    }

    emit unreadPostsCountChanged(m_timelineType,
                                 m_unreadPostsCount,
                                 m_highlightedPostsCount,
                                 m_fullTimelinePostCount);
}

void TimeLine::updateAvatarFollowStates()
{
    foreach (Post *post, m_postsInTimeline)
    {
        post->syncAvatarFollowState();
    }

    // Since this happens when the Following list changes, sort its model
    m_globalObject->sortNickCompletionModel();
}

