/*
 *   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 "minorfeed.h"


MinorFeed::MinorFeed(PumpController::requestTypes minorFeedType,
                     PumpController *pumpController,
                     GlobalObject *globalObject,
                     FilterChecker *filterChecker,
                     QWidget *parent) : QFrame(parent)
{
    m_feedType = minorFeedType;

    m_pumpController = pumpController;
    connect(m_pumpController, &PumpController::minorFeedFailed,
            this, &MinorFeed::onUpdateFailed);

    m_globalObject = globalObject;

    m_filterChecker = filterChecker;


    this->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken);
    this->setContentsMargins(0, 0, 0, 0);

    // Layout for the items
    m_itemsLayout = new QVBoxLayout();
    m_itemsLayout->setContentsMargins(0, 0, 0, 0);
    m_itemsLayout->setSpacing(1); // Very small spacing

    // Separator frame, to mark where new items 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();

    // Button to get newer items (pending stuff)
    m_getPendingButton = new QPushButton(QIcon::fromTheme("view-refresh",
                                                          QIcon(":/images/menu-refresh.png")),
                                         QStringLiteral("*get pending activities*"),
                                         this);
    m_getPendingButton->setFlat(true);
    connect(m_getPendingButton, &QAbstractButton::clicked,
            this, &MinorFeed::updateFeed);
    m_getPendingButton->hide();


    // Button to get more items (older stuff)
    m_getOlderButton = new QPushButton(QIcon::fromTheme("list-add",
                                                       QIcon(":/images/list-add.png")),
                                       tr("Older Activities"),
                                       this);
    m_getOlderButton->setFlat(true);
    m_getOlderButton->setToolTip("<b></b>"
                                 + tr("Get previous minor activities"));
    connect(m_getOlderButton, &QAbstractButton::clicked,
            this, &MinorFeed::getMoreActivities);

    // Set disabled initially; will be enabled when contents are set
    m_getOlderButton->setDisabled(true);


    // Main layout
    m_mainLayout = new QVBoxLayout();
    m_mainLayout->setContentsMargins(1, 1, 1, 1);
    m_mainLayout->addWidget(m_getPendingButton);
    m_mainLayout->addLayout(m_itemsLayout, 1);
    m_mainLayout->addSpacing(8);
    m_mainLayout->addStretch();
    m_mainLayout->addWidget(m_getOlderButton);
    this->setLayout(m_mainLayout);



    // Demo activity, stating that there's nothing to show
    QVariantMap demoGenerator;
    demoGenerator.insert("displayName", QStringLiteral("Dianara"));

    QVariantMap demoActivityMap;
    demoActivityMap.insert("published", QDateTime::currentDateTimeUtc()
                                        .toString(Qt::ISODate));
    demoActivityMap.insert("generator", demoGenerator);
    demoActivityMap.insert("content",   tr("There are no activities to show yet."));

    ASActivity *demoActivity = new ASActivity(demoActivityMap, QString(), this);

    MinorFeedItem *demoFeedItem = new MinorFeedItem(demoActivity,
                                                    false, // Not highlighted by filter
                                                    m_pumpController,
                                                    m_globalObject,
                                                    this);
    demoFeedItem->setItemAsNew(false,
                               false); // Do not inform the feed
    m_itemsLayout->addWidget(demoFeedItem);
    m_itemsInFeed.append(demoFeedItem);

    m_firstLoad = true;
    m_gettingNew = true; // First time should be true
    m_unreadItemsCount = 0;
    m_highlightedItemsCount = 0;
    m_pendingToReceiveNextTime = 0;

    // Remember what was the newest activity last time
    QSettings settings;
    settings.beginGroup("MinorFeedStates");
    switch (m_feedType)
    {
    case PumpController::MinorFeedMainRequest:
        m_previousNewestActivityId = settings.value("previousNewestItemIdMain")
                                             .toString();
        m_fullFeedItemCount = settings.value("totalItemsMain").toInt();
        m_feedBatchItemCount = 50;
        break;

    case PumpController::MinorFeedDirectRequest:
        m_previousNewestActivityId = settings.value("previousNewestItemIdDirect")
                                             .toString();
        m_fullFeedItemCount = settings.value("totalItemsDirect").toInt();
        m_feedBatchItemCount = 20;
        break;

    case PumpController::MinorFeedActivityRequest:
        m_previousNewestActivityId = settings.value("previousNewestItemIdActivity")
                                             .toString();
        m_fullFeedItemCount = settings.value("totalItemsActivity").toInt();
        m_feedBatchItemCount = 20;
        break;

    default:
        qDebug() << "MinorFeed created with wrong type:" << m_feedType;
    }
    settings.endGroup();


    // Sync all avatar's follow state when there are changes in the Following list
    connect(m_pumpController, &PumpController::followingListChanged,
            this, &MinorFeed::updateAvatarFollowStates);

    qDebug() << "MinorFeed created";
}


MinorFeed::~MinorFeed()
{
    QSettings settings;
    settings.beginGroup("MinorFeedStates");

    switch (m_feedType)
    {
    case PumpController::MinorFeedMainRequest:
        settings.setValue("previousNewestItemIdMain", m_previousNewestActivityId);
        settings.setValue("totalItemsMain", m_fullFeedItemCount);
        break;

    case PumpController::MinorFeedDirectRequest:
        settings.setValue("previousNewestItemIdDirect", m_previousNewestActivityId);
        settings.setValue("totalItemsDirect", m_fullFeedItemCount);
        break;

    case PumpController::MinorFeedActivityRequest:
        settings.setValue("previousNewestItemIdActivity", m_previousNewestActivityId);
        settings.setValue("totalItemsActivity", m_fullFeedItemCount);
        break;

    default:
        qDebug() << "MinorFeed destructor: feed type was wrong";
    }
    settings.endGroup();

    qDebug() << "MinorFeed destroyed; Type:" << m_feedType;
}



void MinorFeed::clearContents()
{
    qDebug() << "MinorFeed::clearContents()";
    foreach (MinorFeedItem *feedItem, m_itemsInFeed)
    {
        m_itemsLayout->removeWidget(feedItem);
        delete feedItem;
    }

    m_itemsInFeed.clear();

    m_itemsLayout->removeWidget(m_separatorFrame);
    m_separatorFrame->hide();
}


/*
 * Remove oldest items, to avoid ever-increasing memory usage
 * Called after updating the feed, only when getting newer items
 *
 * At the very least, keep as many items as were received in last update
 *
 */
void MinorFeed::removeOldItems(int minimumToKeep)
{
    int maxItems = qMax(m_feedBatchItemCount * 2, // TMP FIXME
                        minimumToKeep);

    if (m_itemsInFeed.count() <= maxItems)
    {
        // Not too many items yet
        return;
    }

    int itemCounter = 0;
    foreach (MinorFeedItem *feedItem, m_itemsInFeed)
    {
        if (itemCounter >= maxItems)
        {
            if (!feedItem->isNew()) // Don't remove if it's unread (optional?)
            {
                m_itemsLayout->removeWidget(feedItem);
                m_itemsInFeed.removeOne(feedItem);
                delete feedItem;
            }
        }

        ++itemCounter;
    }

    // Update "next" link manually, based on the last item present in the feed
    QByteArray lastItemId = m_itemsInFeed.last()->getActivityId().toLocal8Bit();
    lastItemId = lastItemId.toPercentEncoding(); // Needs to be percent-encoded

    m_nextLink = m_pumpController->getFeedApiUrl(m_feedType)
                 + "?before=" + lastItemId;
}


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


void MinorFeed::markAllAsRead()
{
    foreach (MinorFeedItem *feedItem, m_itemsInFeed)
    {
        feedItem->setItemAsNew(false,   // Mark as not new
                               false);  // Don't inform the feed
    }

    m_unreadItemsCount = 0;
    m_highlightedItemsCount = 0;
}

/*
 * Update fuzzy timestamps in all items
 *
 */
void MinorFeed::updateFuzzyTimestamps()
{
    foreach (MinorFeedItem *feedItem, m_itemsInFeed)
    {
        feedItem->setFuzzyTimeStamp();
    }
}


void MinorFeed::syncActivityWithTimelines(ASActivity *activity)
{
    //////////////////////////////////////////////////////////// An edited post
    if (activity->getVerb() == "update")
    {
        if (ASObject::canDisplayObject(activity->object()->getType()))
        {
            emit objectUpdated(activity->object());
        }
    }    


    //////////////////////////////////////////// A new comment posted to a post
    if (activity->getVerb() == "post")
    {
        ASObject *activityObject = activity->object();

        // Check if the object has proper author info; if not, use activity's author
        if (activityObject->author()->getId().isEmpty())
        {
            activityObject->updateAuthorFromPerson(activity->author());
        }
        // FIXME: this needs some checking...
        // v1.3.0b1+11: seems OK

        emit objectReplyAdded(activityObject);
    }


    ////////////////////////////////////////////////////////////// A liked post
    if (activity->getVerb() == "favorite"
     || activity->getVerb() == "like")
    {
        // FIXME: handle liking of comments (1.4.x)

        if (ASObject::canDisplayObject(activity->object()->getType()))
        {
            emit objectLiked(activity->object()->getId(),
                             activity->object()->getType(),
                             activity->author()->getId(),
                             activity->author()->getNameWithFallback(),
                             activity->author()->getUrl());
        }
    }

    /////////////////////////////////////////////////////////// An unliked post
    if (activity->getVerb() == "unfavorite"
     || activity->getVerb() == "unlike")
    {
        // FIXME: handle unliking of comments (1.4.x)

        if (ASObject::canDisplayObject(activity->object()->getType()))
        {
            emit objectUnliked(activity->object()->getId(),
                               activity->object()->getType(),
                               activity->author()->getId());
        }
    }


    //////////////////////////////////////////////////////////// A deleted post
    if (activity->getVerb() == "delete")
    {
        if (ASObject::canDisplayObject(activity->object()->getType()))
        {
            emit objectDeleted(activity->object());
        }
    }

    // Sync "follow" and "stop-following"
}


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


/*
 * Get the latest activities
 *
 */
void MinorFeed::updateFeed()
{
    m_gettingNew = true;
    m_getOlderButton->setDisabled(true);

    m_pumpController->getFeed(m_feedType,
                              200, // Maximum item count allowed by API
                              m_prevLink);
}

/*
 * Get additional older activities
 *
 */
void MinorFeed::getMoreActivities()
{
    m_gettingNew = false;
    m_getOlderButton->setDisabled(true);

    m_pumpController->getFeed(m_feedType,
                              m_feedBatchItemCount,
                              m_nextLink);
}



void MinorFeed::setFeedContents(QVariantList activitiesList,
                                QString previous, QString next,
                                int totalItemCount)
{
    if (m_firstLoad)
    {
        m_prevLink = previous;
        m_nextLink = next;

        this->clearContents();
    }
    else
    {
        if (m_gettingNew)
        {
            if (!previous.isEmpty())
            {
                m_prevLink = previous;
            }
        }
        else
        {
            if (!next.isEmpty())
            {
                m_nextLink = next;
            }
        }
    }

    qDebug() << "Current MinorFeed prev/next links:" << m_prevLink << m_nextLink;


    const int activitiesListSize = activitiesList.size();
    int totalItemDifference = -1; // TMP FIXME; for cases where it's unknown
    if (m_gettingNew)
    {
        // Check how many new items we should we expecting
        totalItemDifference = totalItemCount - m_fullFeedItemCount;
        m_fullFeedItemCount = totalItemCount;
        qDebug() << "MinorFeed::setFeedContents(); Should load"
                 << totalItemDifference << "items this time ###";

        // Check how many more items need to be received, if more than max are pending
        m_pendingToReceiveNextTime += totalItemDifference;
        m_pendingToReceiveNextTime -= activitiesListSize;
        if (m_pendingToReceiveNextTime > 0)
        {
            if (m_firstLoad)
            {
                // The difference in pending items is in the older activities, so doesn't count
                m_pendingToReceiveNextTime = 0;

                // TODO: On first load, could display the "pending" number at the "older" button
            }
            else
            {
                // Button at the top to fetch the pending activities, even more new stuff
                m_getPendingButton->setText(tr("Get %1 newer",
                                               "As in: Get 3 newer (activities)")
                                            .arg(m_pendingToReceiveNextTime));
                m_getPendingButton->show();
            }
        }
        else
        {
            m_pendingToReceiveNextTime = 0; // In case it was less than 0
            m_getPendingButton->hide();
        }

        // Hide "Get newer" button also if for some reason it's present and we
        if (activitiesListSize == 0) // confirm there's nothing else to receive
        {
            m_pendingToReceiveNextTime = 0;
            m_getPendingButton->hide();
        }
    }

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

    int newItemsCount = 0;
    int newHighlightedItemsCount = 0;
    int newFilteredItemsCount = 0;

    bool itemIsNew;
    bool itemHighlightedByFilter;

    bool allNewItemsCounted = false;
    QString newestActivityId; // To store the activity ID for the newest item in the feed
                              // so we can know how many new items we receive next time

    int insertedItemsCount = 0;
    bool needToInsertSeparator = false;

    QList<ASActivity *> activitiesToSync;
    QList<ASActivity *> allProcessedActivities;

    foreach (const QVariant activityVariant, activitiesList)
    {
        itemIsNew = false;

        ASActivity *activity = new ASActivity(activityVariant.toMap(),
                                              m_pumpController->currentUserId(),
                                              this);
        allProcessedActivities.append(activity);


        const int filtered = m_filterChecker->validateActivity(activity);

        // If there is no reason to filter out the item, add it to the feed
        if (filtered != FilterChecker::FilterOut)
        {
            // Store activities in reverse order, for later processing (sync with TL)
            activitiesToSync.prepend(activity);

            // Determine which activities are new
            if (newestActivityId.isEmpty()) // Only first time, for newest item
            {
                if (m_gettingNew)
                {
                    newestActivityId = activity->getId();
                }
                else
                {
                    newestActivityId = m_previousNewestActivityId;
                    allNewItemsCounted = true;
                }
            }

            // Determine if this item is new, or if not anymore
            if (!allNewItemsCounted)
            {
                if (activity->getId() == m_previousNewestActivityId)
                {
                    allNewItemsCounted = true;
                    if (newItemsCount > 0)
                    {
                        needToInsertSeparator = true;
                    }
                }
                else
                {
                    // If activity is not ours, add it to the count
                    if (activity->author()->getId() != m_pumpController->currentUserId())
                    {
                        ++newItemsCount;
                        itemIsNew = true;
                    }
                }
            }


            if (filtered == FilterChecker::Highlight) // kinda TMP
            {
                itemHighlightedByFilter = true;
            }
            else
            {
                itemHighlightedByFilter = false;
            }

            MinorFeedItem *newFeedItem = new MinorFeedItem(activity,
                                                           itemHighlightedByFilter,
                                                           m_pumpController,
                                                           m_globalObject,
                                                           this);
            newFeedItem->setItemAsNew(itemIsNew,
                                      false); // Don't inform the feed
            if (itemIsNew)
            {
                connect(newFeedItem, &MinorFeedItem::itemRead,
                        this, &MinorFeed::decreaseNewItemsCount);

                if (newFeedItem->getItemHighlightType() != -1)
                {
                    ++newHighlightedItemsCount;
                }
            }

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

                m_itemsLayout->insertWidget(insertedItemsCount, newFeedItem);
                m_itemsInFeed.insert(insertedItemsCount, newFeedItem);
                ++insertedItemsCount;
            }
            else // Not new, getting 'older', so add at the bottom
            {
                m_itemsLayout->addWidget(newFeedItem);
                m_itemsInFeed.append(newFeedItem);
            }
        }
        else
        {
            ++newFilteredItemsCount;

            // Since the item is not added to the feed, we need to delete the activity
            delete activity; // FIXME: should not delete, just hide
        }
    } // End foreach


    // The first time stuff is received from the server, there's no need to sync
    if (!m_firstLoad)
    {
        // Activities in activitiesToSync are stored in reverse
        foreach (ASActivity *activity, activitiesToSync)
        {
            // Send notifications to update objects in the timelines
            this->syncActivityWithTimelines(activity);
        }
    }

    // Cleanup all ASActivity objects, no longer needed
    foreach (ASActivity *activity, allProcessedActivities)
    {
        delete activity;
    }


    // If there were new items, and not already added, add separator: -------
    if (newItemsCount > 0 && m_separatorFrame->isHidden())
    {
        this->insertSeparator(insertedItemsCount);
    }


    if (!newestActivityId.isEmpty()) // If some items were received should be valid
    {
        m_previousNewestActivityId = newestActivityId;
    }

    m_unreadItemsCount += newItemsCount;
    m_highlightedItemsCount += newHighlightedItemsCount;

    qDebug() << "Minor feed updated";
    if (m_gettingNew) // not when getting more, older ones
    {
        emit newItemsCountChanged(m_unreadItemsCount,
                                  m_highlightedItemsCount);
        emit newItemsReceived(m_feedType,
                              newItemsCount,
                              newHighlightedItemsCount,
                              newFilteredItemsCount,
                              m_pendingToReceiveNextTime);
        qDebug() << "New items:" << newItemsCount
                 << "; New highlighted:" << newHighlightedItemsCount;


        // Clean up, keeping at least the ones that were just received
        if (activitiesListSize > 0) // but only if something was received
        {
            this->removeOldItems(activitiesListSize);
        }
    }
    else
    {
        emit newItemsReceived(m_feedType,
                              activitiesListSize, // Actual number of received items
                              -1, -1, -1); // -1 highlighted, filtered and pending
                                           // means these are old items
    }

    m_getOlderButton->setEnabled(true);
    m_firstLoad = false;
}


void MinorFeed::onUpdateFailed(int requestType)
{
    if (requestType == m_feedType)
    {
        m_getOlderButton->setEnabled(true);
    }
}


void MinorFeed::decreaseNewItemsCount(bool wasHighlighted)
{
    --m_unreadItemsCount;
    if (wasHighlighted)
    {
        --m_highlightedItemsCount;
    }

    emit newItemsCountChanged(m_unreadItemsCount, m_highlightedItemsCount);
}


void MinorFeed::updateAvatarFollowStates()
{
    foreach (MinorFeedItem *feedItem, m_itemsInFeed)
    {
        feedItem->syncAvatarFollowState();
    }
}
