/*
 * Squawk messenger. 
 * Copyright (C) 2019  Yury Gubich <blue@macaw.me>
 * 
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 */

#include "messagefeed.h"

#include <ui/models/element.h>
#include <ui/models/room.h>

#include <QDebug>

const QHash<int, QByteArray> Models::MessageFeed::roles = {
    {Text, "text"},
    {Sender, "sender"},
    {Date, "date"},
    {DeliveryState, "deliveryState"},
    {Correction, "correction"},
    {SentByMe,"sentByMe"},
    {Avatar, "avatar"},
    {Attach, "attach"},
    {Id, "id"},
    {Error, "error"},
    {Bulk, "bulk"}
};

Models::MessageFeed::MessageFeed(const Element* ri, QObject* parent):
    QAbstractListModel(parent),
    storage(),
    indexById(storage.get<id>()),
    indexByTime(storage.get<time>()),
    rosterItem(ri),
    syncState(incomplete),
    uploads(),
    downloads(),
    failedDownloads(),
    failedUploads(),
    unreadMessages(new std::set<QString>()),
    observersAmount(0)
{
}

Models::MessageFeed::~MessageFeed()
{
    delete unreadMessages;
    
    for (Shared::Message* message : storage) {
        delete message;
    }
}

void Models::MessageFeed::addMessage(const Shared::Message& msg)
{
    QString id = msg.getId();
    StorageById::const_iterator itr = indexById.find(id);
    if (itr != indexById.end()) {
        qDebug() << "received more then one message with the same id, skipping yet the new one";
        return;
    }
    
    Shared::Message* copy = new Shared::Message(msg);
    StorageByTime::const_iterator tItr = indexByTime.upper_bound(msg.getTime());
    int position;
    if (tItr == indexByTime.end()) {
        position = storage.size();
    } else {
        position = indexByTime.rank(tItr);
    }
    beginInsertRows(QModelIndex(), position, position);
    storage.insert(copy);
    endInsertRows();
    
    emit newMessage(msg);
    
    if (observersAmount == 0 && !msg.getForwarded()) {      //not to notify when the message is delivered by the carbon copy
        unreadMessages->insert(id);                         //cuz it could be my own one or the one I read on another device
        emit unreadMessagesCountChanged();
        emit unnoticedMessage(msg);
    }
}

void Models::MessageFeed::changeMessage(const QString& id, const QMap<QString, QVariant>& data)
{
    StorageById::iterator itr = indexById.find(id);
    if (itr == indexById.end()) {
        qDebug() << "received a command to change a message, but the message couldn't be found, skipping";
        return;
    }
    
    Shared::Message* msg = *itr;
    std::set<MessageRoles> changeRoles = detectChanges(*msg, data);
    QModelIndex index = modelIndexByTime(id, msg->getTime());
    Shared::Message::Change functor(data);
    bool success = indexById.modify(itr, functor);
    if (!success) {
        qDebug() << "received a command to change a message, but something went wrong modifying message in the feed, throwing error";
        throw 872;
    }
    
    if (functor.hasIdBeenModified()) {
        changeRoles.insert(MessageRoles::Id);
        std::set<QString>::const_iterator umi = unreadMessages->find(id);
        if (umi != unreadMessages->end()) {
            unreadMessages->erase(umi);
            unreadMessages->insert(msg->getId());
        }
    }
    
    if (changeRoles.size() > 0) {
        //change message is a final event in download/upload event train
        //only after changeMessage we can consider the download is done
        Progress::const_iterator dItr = downloads.find(id);
        bool attachOrError = changeRoles.count(MessageRoles::Attach) > 0 || changeRoles.count(MessageRoles::Error);
        if (dItr != downloads.end()) {
            if (attachOrError) {
                downloads.erase(dItr);
            } else if (changeRoles.count(MessageRoles::Id) > 0) {
                qreal progress = dItr->second;
                downloads.erase(dItr);
                downloads.insert(std::make_pair(msg->getId(), progress));
            }
        } else {
            dItr = uploads.find(id);
            if (dItr != uploads.end()) {
                if (attachOrError) {
                    uploads.erase(dItr);
                } else if (changeRoles.count(MessageRoles::Id) > 0) {
                    qreal progress = dItr->second;
                    uploads.erase(dItr);
                    uploads.insert(std::make_pair(msg->getId(), progress));
                }
            }
        }
        
        Err::const_iterator eitr = failedDownloads.find(id);
        if (eitr != failedDownloads.end()) {
            failedDownloads.erase(eitr);
            changeRoles.insert(MessageRoles::Attach);
        } else {
            eitr = failedUploads.find(id);
            if (eitr != failedUploads.end()) {
                failedUploads.erase(eitr);
                changeRoles.insert(MessageRoles::Attach);
            }
        }
        
        QVector<int> cr;
        for (MessageRoles role : changeRoles) {
            cr.push_back(role);
        }
        
        emit dataChanged(index, index, cr);

        if (observersAmount == 0 && !msg->getForwarded() && changeRoles.count(MessageRoles::Text) > 0) {
            unreadMessages->insert(id);
            emit unreadMessagesCountChanged();
            emit unnoticedMessage(*msg);
        }
    }
}

std::set<Models::MessageFeed::MessageRoles> Models::MessageFeed::detectChanges(const Shared::Message& msg, const QMap<QString, QVariant>& data) const
{
    std::set<MessageRoles> roles;
    Shared::Message::State state = msg.getState();
    QMap<QString, QVariant>::const_iterator itr = data.find("state");
    if (itr != data.end() && static_cast<Shared::Message::State>(itr.value().toUInt()) != state) {
        roles.insert(MessageRoles::DeliveryState);
    }
    
    itr = data.find("outOfBandUrl");
    bool att = false;
    if (itr != data.end() && itr.value().toString() != msg.getOutOfBandUrl()) {
        roles.insert(MessageRoles::Attach);
        att = true;
    }
    
    if (!att) {
        itr = data.find("attachPath");
        if (itr != data.end() && itr.value().toString() != msg.getAttachPath()) {
            roles.insert(MessageRoles::Attach);
        }
    }
    
    if (state == Shared::Message::State::error) {
        itr = data.find("errorText");
        if (itr != data.end() && itr.value().toString() != msg.getErrorText()) {
            roles.insert(MessageRoles::Error);
        }
    }
    
    itr = data.find("body");
    if (itr != data.end() && itr.value().toString() != msg.getBody()) {
        QMap<QString, QVariant>::const_iterator dItr = data.find("stamp");
        QDateTime correctionDate;
        if (dItr != data.end()) {
            correctionDate = dItr.value().toDateTime();
        } else {
            correctionDate = QDateTime::currentDateTimeUtc();      //in case there is no information about time of this correction it's applied
        }
        if (!msg.getEdited() || msg.getLastModified() < correctionDate) {
            roles.insert(MessageRoles::Text);
            roles.insert(MessageRoles::Correction);
        }
    } else {
        QMap<QString, QVariant>::const_iterator dItr = data.find("stamp");
        if (dItr != data.end()) {
            QDateTime ntime = dItr.value().toDateTime();
            if (msg.getTime() != ntime) {
                roles.insert(MessageRoles::Date);
            }
        }
    }
    
    return roles;
}

void Models::MessageFeed::removeMessage(const QString& id)
{
    //todo;
}

Shared::Message Models::MessageFeed::getMessage(const QString& id)
{
    StorageById::iterator itr = indexById.find(id);
    if (itr == indexById.end()) {
        throw NotFound(id.toStdString(), rosterItem->getJid().toStdString(), rosterItem->getAccountName().toStdString());
    }

    return **itr;
}


QVariant Models::MessageFeed::data(const QModelIndex& index, int role) const
{
    int i = index.row();
    QVariant answer;
    
    StorageByTime::const_iterator itr = indexByTime.nth(i);
    if (itr != indexByTime.end()) {
        const Shared::Message* msg = *itr;
        
        switch (role) {
            case Qt::DisplayRole:
            case Text: {
                QString body = msg->getBody();
                if (body != msg->getOutOfBandUrl()) {
                    answer = body;
                }
            }
                break;
            case Sender: 
                if (sentByMe(*msg)) {
                    answer = rosterItem->getAccountName();
                } else {
                    if (rosterItem->isRoom()) {
                        answer = msg->getFromResource();
                    } else {
                        answer = rosterItem->getDisplayedName();
                    }
                }
                break;
            case Date: 
                answer = msg->getTime();
                break;
            case DeliveryState: 
                answer = static_cast<unsigned int>(msg->getState());
                break;
            case Correction: 
                answer.setValue(fillCorrection(*msg));;
                break;
            case SentByMe: 
                answer = sentByMe(*msg);
                break;
            case Avatar: {
                QString path;
                if (sentByMe(*msg)) {
                    path = rosterItem->getAccountAvatarPath();
                } else if (!rosterItem->isRoom()) {
                    if (rosterItem->getAvatarState() != Shared::Avatar::empty) {
                        path = rosterItem->getAvatarPath();
                    }
                } else {
                    const Room* room = static_cast<const Room*>(rosterItem);
                    path = room->getParticipantIconPath(msg->getFromResource());
                }
                
                if (path.size() == 0) {
                    answer = Shared::iconPath("user", true);
                } else {
                    answer = path;
                }
            }
                break;
            case Attach: 
                answer.setValue(fillAttach(*msg));
                break;
            case Id: 
                answer.setValue(msg->getId());
                break;
                break;
            case Error: 
                answer.setValue(msg->getErrorText());
                break;
            case Bulk: {
                FeedItem item;
                item.id = msg->getId();
                markMessageAsRead(item.id);
                
                item.sentByMe = sentByMe(*msg);
                item.date = msg->getTime();
                item.state = msg->getState();
                item.error = msg->getErrorText();
                item.correction = fillCorrection(*msg);
                
                QString body = msg->getBody();
                if (body != msg->getOutOfBandUrl()) {
                    item.text = body;
                }
                
                item.avatar.clear();
                if (item.sentByMe) {
                    item.sender = rosterItem->getAccountName();
                    item.avatar = rosterItem->getAccountAvatarPath();
                } else {
                    if (rosterItem->isRoom()) {
                        item.sender = msg->getFromResource();
                        const Room* room = static_cast<const Room*>(rosterItem);
                        item.avatar = room->getParticipantIconPath(msg->getFromResource());
                    } else {
                        item.sender = rosterItem->getDisplayedName();
                        if (rosterItem->getAvatarState() != Shared::Avatar::empty) {
                            item.avatar = rosterItem->getAvatarPath();
                        }
                    }
                }
                
                if (item.avatar.size() == 0) {
                    item.avatar = Shared::iconPath("user", true);
                }
                item.attach = fillAttach(*msg);
                answer.setValue(item);
            }
                break;
            default:
                break;
        }
    }
    
    return answer;
}

int Models::MessageFeed::rowCount(const QModelIndex& parent) const
{
    return storage.size();
}

bool Models::MessageFeed::markMessageAsRead(const QString& id) const
{
    std::set<QString>::const_iterator umi = unreadMessages->find(id);
    if (umi != unreadMessages->end()) {
        unreadMessages->erase(umi);
        emit unreadMessagesCountChanged();
        return true;
    }
    return false;
}

unsigned int Models::MessageFeed::unreadMessagesCount() const
{
    return unreadMessages->size();
}

bool Models::MessageFeed::canFetchMore(const QModelIndex& parent) const
{
    return syncState == incomplete;
}

void Models::MessageFeed::fetchMore(const QModelIndex& parent)
{
    if (syncState == incomplete) {
        syncState = syncing;
        emit syncStateChange(syncState);
        
        if (storage.size() == 0) {
            emit requestArchive("");
        } else {
            emit requestArchive((*indexByTime.rbegin())->getId());
        }
    }
}

void Models::MessageFeed::responseArchive(const std::list<Shared::Message> list, bool last)
{
    Storage::size_type size = storage.size();
    
    beginInsertRows(QModelIndex(), size, size + list.size() - 1);
    for (const Shared::Message& msg : list) {
        Shared::Message* copy = new Shared::Message(msg);
        storage.insert(copy);
    }
    endInsertRows();
    
    if (syncState == syncing) {
        if (last) {
            syncState = complete;
        } else {
            syncState = incomplete;
        }
        emit syncStateChange(syncState);
    }
}

QModelIndex Models::MessageFeed::index(int row, int column, const QModelIndex& parent) const
{
    if (!hasIndex(row, column, parent)) {
        return QModelIndex();
    }
    
    StorageByTime::iterator itr = indexByTime.nth(row);
    if (itr != indexByTime.end()) {
        Shared::Message* msg = *itr;
        
        return createIndex(row, column, msg);
    } else {
        return QModelIndex();
    }
}

QHash<int, QByteArray> Models::MessageFeed::roleNames() const
{
    return roles;
}

bool Models::MessageFeed::sentByMe(const Shared::Message& msg) const
{
    if (rosterItem->isRoom()) {
        const Room* room = static_cast<const Room*>(rosterItem);
        return room->getNick().toLower() == msg.getFromResource().toLower();
    } else {
        return msg.getOutgoing();
    }
}

Models::Attachment Models::MessageFeed::fillAttach(const Shared::Message& msg) const
{
    ::Models::Attachment att;
    QString id = msg.getId();
    
    att.localPath = msg.getAttachPath();
    att.remotePath = msg.getOutOfBandUrl();
    
    if (att.remotePath.size() == 0) {
        if (att.localPath.size() == 0) {
            att.state = none;
        } else {
            Err::const_iterator eitr = failedUploads.find(id);
            if (eitr != failedUploads.end()) {
                att.state = errorUpload;
                att.error = eitr->second;
            } else {
                Progress::const_iterator itr = uploads.find(id);
                if (itr == uploads.end()) {
                    att.state = local;
                } else {
                    att.state = uploading;
                    att.progress = itr->second;
                }
            }
        }
    } else {
        if (att.localPath.size() == 0) {
            Err::const_iterator eitr = failedDownloads.find(id);
            if (eitr != failedDownloads.end()) {
                att.state = errorDownload;
                att.error = eitr->second;
            } else {
                Progress::const_iterator itr = downloads.find(id);
                if (itr == downloads.end()) {
                    att.state = remote;
                } else {
                    att.state = downloading;
                    att.progress = itr->second;
                }
            }
        } else {
            att.state = ready;
        }
    }
    
    return att;
}

Models::Edition Models::MessageFeed::fillCorrection(const Shared::Message& msg) const
{
    ::Models::Edition ed({msg.getEdited(), msg.getOriginalBody(), msg.getLastModified()});
    
    return ed;
}


void Models::MessageFeed::downloadAttachment(const QString& messageId)
{
    bool notify = false;
    Err::const_iterator eitr = failedDownloads.find(messageId);
    if (eitr != failedDownloads.end()) {
        failedDownloads.erase(eitr);
        notify = true;
    }
    
    QModelIndex ind = modelIndexById(messageId);
    if (ind.isValid()) {
        std::pair<Progress::iterator, bool> progressPair = downloads.insert(std::make_pair(messageId, 0));
        if (progressPair.second) {     //Only to take action if we weren't already downloading it
            Shared::Message* msg = static_cast<Shared::Message*>(ind.internalPointer());
            notify = true;
            emit fileDownloadRequest(msg->getOutOfBandUrl());
        } else {
            qDebug() << "Attachment download for message with id" << messageId << "is already in progress, skipping";
        }
    } else {
        qDebug() << "An attempt to download an attachment for the message that doesn't exist. ID:" << messageId;
    }
    
    if (notify) {
        emit dataChanged(ind, ind, {MessageRoles::Attach});
    }
}

bool Models::MessageFeed::registerUpload(const QString& messageId)
{
    bool success = uploads.insert(std::make_pair(messageId, 0)).second; 
    
    QVector<int> roles({});
    Err::const_iterator eitr = failedUploads.find(messageId);
    if (eitr != failedUploads.end()) {
        failedUploads.erase(eitr);
        roles.push_back(MessageRoles::Attach);
    } else if (success) {
        roles.push_back(MessageRoles::Attach);
    }
    
    QModelIndex ind = modelIndexById(messageId);
    emit dataChanged(ind, ind, roles);
    
    return success;
}

void Models::MessageFeed::fileProgress(const QString& messageId, qreal value, bool up)
{
    Progress* pr = 0;
    Err* err = 0;
    if (up) {
        pr = &uploads;
        err = &failedUploads;
    } else {
        pr = &downloads;
        err = &failedDownloads;
    }
    
    QVector<int> roles({});
    Err::const_iterator eitr = err->find(messageId);
    if (eitr != err->end() && value != 1) {         //like I want to clear this state when the download is started anew
        err->erase(eitr);
        roles.push_back(MessageRoles::Attach);
    }
    
    Progress::iterator itr = pr->find(messageId);
    if (itr != pr->end()) {
        itr->second = value;
        QModelIndex ind = modelIndexById(messageId);
        emit dataChanged(ind, ind, roles);
    }
}

void Models::MessageFeed::fileComplete(const QString& messageId, bool up)
{
    fileProgress(messageId, 1, up);
}

void Models::MessageFeed::fileError(const QString& messageId, const QString& error, bool up)
{
    Err* failed;
    Progress* loads;
    if (up) {
        failed = &failedUploads;
        loads = &uploads;
    } else {
        failed = &failedDownloads;
        loads = &downloads;
    }
    
    Progress::iterator pitr = loads->find(messageId);
    if (pitr != loads->end()) {
        loads->erase(pitr);
    }
    
    std::pair<Err::iterator, bool> pair = failed->insert(std::make_pair(messageId, error));
    if (!pair.second) {
        pair.first->second = error;
    }
    QModelIndex ind = modelIndexById(messageId);
    if (ind.isValid()) {
        emit dataChanged(ind, ind, {MessageRoles::Attach});
    }
}

void Models::MessageFeed::incrementObservers()
{
    ++observersAmount;
}

void Models::MessageFeed::decrementObservers()
{
    --observersAmount;
}


QModelIndex Models::MessageFeed::modelIndexById(const QString& id) const
{
    StorageById::const_iterator itr = indexById.find(id);
    if (itr != indexById.end()) {
        Shared::Message* msg = *itr;
        return modelIndexByTime(id, msg->getTime());
    }
    
    return QModelIndex();
}

QModelIndex Models::MessageFeed::modelIndexByTime(const QString& id, const QDateTime& time) const
{
    if (indexByTime.size() > 0) {
        StorageByTime::const_iterator tItr = indexByTime.lower_bound(time);
        StorageByTime::const_iterator tEnd = indexByTime.upper_bound(time);
        bool found = false;
        while (tItr != tEnd) {
            if (id == (*tItr)->getId()) {
                found = true;
                break;
            }
            ++tItr;
        }
        
        if (found) {
            int position = indexByTime.rank(tItr);
            return createIndex(position, 0, *tItr);
        }
    }
    
    return QModelIndex();
}

void Models::MessageFeed::reportLocalPathInvalid(const QString& messageId)
{
    StorageById::iterator itr = indexById.find(messageId);
    if (itr == indexById.end()) {
        qDebug() << "received a command to change a message, but the message couldn't be found, skipping";
        return;
    }
    
    Shared::Message* msg = *itr;
    
    emit localPathInvalid(msg->getAttachPath());
    
    //gonna change the message in current model right away, to prevent spam on each attempt to draw element
    QModelIndex index = modelIndexByTime(messageId, msg->getTime());
    msg->setAttachPath("");
    
    emit dataChanged(index, index, {MessageRoles::Attach});
}

Models::MessageFeed::SyncState Models::MessageFeed::getSyncState() const
{
    return syncState;
}

void Models::MessageFeed::requestLatestMessages()
{
    if (syncState != syncing) {
        syncState = syncing;
        emit syncStateChange(syncState);
        
        emit requestArchive("");
    }
}