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

Application::Application(Core::Squawk* p_core):
    QObject(),
    availability(Shared::Availability::offline),
    core(p_core),
    squawk(nullptr),
    notifications("org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications"),
    roster(),
    conversations(),
    dialogueQueue(roster),
    nowQuitting(false),
    destroyingSquawk(false),
    storage(),
    trayIcon(nullptr),
    actionQuit(Shared::icon("application-exit"), tr("Quit")),
    actionToggle(QApplication::windowIcon(), tr("Minimize to tray")),
    expandedPaths()
{
    connect(&actionQuit, &QAction::triggered, this, &Application::quit);
    connect(&actionToggle, &QAction::triggered, this, &Application::toggleSquawk);

    connect(&roster, &Models::Roster::unnoticedMessage, this, &Application::notify);
    connect(&roster, &Models::Roster::unreadMessagesCountChanged, this, &Application::unreadMessagesCountChanged);
    connect(&roster, &Models::Roster::addedElement, this, &Application::onAddedElement);


    //connecting myself to the backend
    connect(this, &Application::changeState, core, &Core::Squawk::changeState);
    connect(this, &Application::setRoomJoined, core, &Core::Squawk::setRoomJoined);
    connect(this, &Application::setRoomAutoJoin, core, &Core::Squawk::setRoomAutoJoin);
    connect(this, &Application::subscribeContact, core, &Core::Squawk::subscribeContact);
    connect(this, &Application::unsubscribeContact, core, &Core::Squawk::unsubscribeContact);
    connect(this, &Application::replaceMessage, core, &Core::Squawk::replaceMessage);
    connect(this, &Application::sendMessage, core, &Core::Squawk::sendMessage);
    connect(this, &Application::resendMessage, core, &Core::Squawk::resendMessage);
    connect(&roster, &Models::Roster::requestArchive,
            std::bind(&Core::Squawk::requestArchive, core, std::placeholders::_1, std::placeholders::_2, 20, std::placeholders::_3));

    connect(&dialogueQueue, &DialogQueue::modifyAccountRequest, core, &Core::Squawk::modifyAccountRequest);
    connect(&dialogueQueue, &DialogQueue::responsePassword, core, &Core::Squawk::responsePassword);
    connect(&dialogueQueue, &DialogQueue::disconnectAccount, core, &Core::Squawk::disconnectAccount);

    connect(&roster, &Models::Roster::fileDownloadRequest, core, &Core::Squawk::fileDownloadRequest);
    connect(&roster, &Models::Roster::localPathInvalid, core, &Core::Squawk::onLocalPathInvalid);


    //coonecting backend to myself
    connect(core, &Core::Squawk::stateChanged, this, &Application::stateChanged);

    connect(core, &Core::Squawk::accountMessage, &roster, &Models::Roster::addMessage);
    connect(core, &Core::Squawk::responseArchive, &roster, &Models::Roster::responseArchive);
    connect(core, &Core::Squawk::changeMessage, &roster, &Models::Roster::changeMessage);

    connect(core, &Core::Squawk::newAccount, &roster, &Models::Roster::addAccount);
    connect(core, &Core::Squawk::changeAccount, this, &Application::changeAccount);
    connect(core, &Core::Squawk::removeAccount, this, &Application::removeAccount);

    connect(core, &Core::Squawk::addContact, &roster, &Models::Roster::addContact);
    connect(core, &Core::Squawk::addGroup, this, &Application::addGroup);
    connect(core, &Core::Squawk::removeGroup, &roster, &Models::Roster::removeGroup);
    connect(core, qOverload<const QString&, const QString&>(&Core::Squawk::removeContact),
            &roster, qOverload<const QString&, const QString&>(&Models::Roster::removeContact));
    connect(core, qOverload<const QString&, const QString&, const QString&>(&Core::Squawk::removeContact),
            &roster, qOverload<const QString&, const QString&, const QString&>(&Models::Roster::removeContact));
    connect(core, &Core::Squawk::changeContact, &roster, &Models::Roster::changeContact);
    connect(core, &Core::Squawk::addPresence, &roster, &Models::Roster::addPresence);
    connect(core, &Core::Squawk::removePresence, &roster, &Models::Roster::removePresence);

    connect(core, &Core::Squawk::addRoom, &roster, &Models::Roster::addRoom);
    connect(core, &Core::Squawk::changeRoom, &roster, &Models::Roster::changeRoom);
    connect(core, &Core::Squawk::removeRoom, &roster, &Models::Roster::removeRoom);
    connect(core, &Core::Squawk::addRoomParticipant, &roster, &Models::Roster::addRoomParticipant);
    connect(core, &Core::Squawk::changeRoomParticipant, &roster, &Models::Roster::changeRoomParticipant);
    connect(core, &Core::Squawk::removeRoomParticipant, &roster, &Models::Roster::removeRoomParticipant);


    connect(core, &Core::Squawk::fileDownloadComplete, std::bind(&Models::Roster::fileComplete, &roster, std::placeholders::_1, false));
    connect(core, &Core::Squawk::fileUploadComplete, std::bind(&Models::Roster::fileComplete, &roster, std::placeholders::_1, true));
    connect(core, &Core::Squawk::fileProgress, &roster, &Models::Roster::fileProgress);
    connect(core, &Core::Squawk::fileError, &roster, &Models::Roster::fileError);

    connect(core, &Core::Squawk::requestPassword, this, &Application::requestPassword);
    connect(core, &Core::Squawk::ready, this, &Application::readSettings);

    QDBusConnection sys = QDBusConnection::sessionBus();
    sys.connect(
        "org.freedesktop.Notifications",
        "/org/freedesktop/Notifications",
        "org.freedesktop.Notifications",
        "NotificationClosed",
        this,
        SLOT(onNotificationClosed(quint32, quint32))
    );
    sys.connect(
        "org.freedesktop.Notifications",
        "/org/freedesktop/Notifications",
        "org.freedesktop.Notifications",
        "ActionInvoked",
        this,
        SLOT(onNotificationInvoked(quint32, const QString&))
    );
}

Application::~Application() {}

void Application::quit()
{
    if (!nowQuitting) {
        nowQuitting = true;
        emit quitting();

        writeSettings();
        unreadMessagesCountChanged(0);      //this notification persist in the desktop, for now I'll zero it on quit not to confuse people
        for (Conversations::const_iterator itr = conversations.begin(), end = conversations.end(); itr != end; ++itr) {
            disconnect(itr->second, &Conversation::destroyed, this, &Application::onConversationClosed);
            itr->second->close();
        }
        conversations.clear();
        dialogueQueue.quit();

        if (squawk != nullptr) {
            squawk->close();
        }

        if (trayIcon != nullptr) {
            trayIcon->deleteLater();
            trayIcon = nullptr;
        }

        if (!destroyingSquawk) {
            checkForTheLastWindow();
        }
    }
}

void Application::checkForTheLastWindow()
{
    if (QApplication::topLevelWidgets().size() > 0) {
        emit readyToQuit();
    } else {
        connect(qApp, &QApplication::lastWindowClosed, this, &Application::readyToQuit);
    }
}

void Application::createMainWindow()
{
    if (squawk == nullptr) {
        squawk = new Squawk(roster);

        connect(squawk, &Squawk::notify, this, &Application::notify);
        connect(squawk, &Squawk::changeSubscription, this, &Application::changeSubscription);
        connect(squawk, &Squawk::openedConversation, this, &Application::onSquawkOpenedConversation);
        connect(squawk, &Squawk::openConversation, this, &Application::openConversation);
        connect(squawk, &Squawk::changeState, this, &Application::setState);
        connect(squawk, &Squawk::changeTray, this, &Application::onChangeTray);
        connect(squawk, &Squawk::itemExpanded, this, &Application::onItemExpanded);
        connect(squawk, &Squawk::itemCollapsed, this, &Application::onItemCollapsed);
        connect(squawk, &Squawk::quit, this, &Application::quit);
        connect(squawk, &Squawk::closing, this, &Application::onSquawkClosing);

        connect(squawk, &Squawk::modifyAccountRequest, core, &Core::Squawk::modifyAccountRequest);
        connect(squawk, &Squawk::newAccountRequest, core, &Core::Squawk::newAccountRequest);
        connect(squawk, &Squawk::removeAccountRequest, core, &Core::Squawk::removeAccountRequest);
        connect(squawk, &Squawk::connectAccount, core, &Core::Squawk::connectAccount);
        connect(squawk, &Squawk::disconnectAccount, core, &Core::Squawk::disconnectAccount);

        connect(squawk, &Squawk::addContactRequest, core, &Core::Squawk::addContactRequest);
        connect(squawk, &Squawk::removeContactRequest, core, &Core::Squawk::removeContactRequest);
        connect(squawk, &Squawk::removeRoomRequest, core, &Core::Squawk::removeRoomRequest);
        connect(squawk, &Squawk::addRoomRequest, core, &Core::Squawk::addRoomRequest);
        connect(squawk, &Squawk::addContactToGroupRequest, core, &Core::Squawk::addContactToGroupRequest);
        connect(squawk, &Squawk::removeContactFromGroupRequest, core, &Core::Squawk::removeContactFromGroupRequest);
        connect(squawk, &Squawk::renameContactRequest, core, &Core::Squawk::renameContactRequest);
        connect(squawk, &Squawk::requestVCard, core, &Core::Squawk::requestVCard);
        connect(squawk, &Squawk::uploadVCard, core, &Core::Squawk::uploadVCard);
        connect(squawk, &Squawk::changeDownloadsPath, core, &Core::Squawk::changeDownloadsPath);

        connect(core, &Core::Squawk::responseVCard, squawk, &Squawk::responseVCard);

        dialogueQueue.setParentWidnow(squawk);
        squawk->stateChanged(availability);
        squawk->raise();
        squawk->show();
        squawk->activateWindow();

        for (const std::list<QString>& entry : expandedPaths) {
            QModelIndex ind = roster.getIndexByPath(entry);
            if (ind.isValid()) {
                squawk->expand(ind);
            }
        }

        connect(squawk, &Squawk::itemExpanded, this, &Application::onItemExpanded);
        connect(squawk, &Squawk::itemCollapsed, this, &Application::onItemCollapsed);
    }
}

void Application::onSquawkClosing()
{
    dialogueQueue.setParentWidnow(nullptr);

    if (!nowQuitting) {
        disconnect(core, &Core::Squawk::responseVCard, squawk, &Squawk::responseVCard);
    }

    destroyingSquawk = true;
    squawk->deleteLater();
    squawk = nullptr;

    QSettings settings;
    if (!nowQuitting && QSystemTrayIcon::isSystemTrayAvailable() && settings.value("tray", false).toBool()) {
        if (settings.value("hideTray", false).toBool()) {
            createTrayIcon();
        }
        actionToggle.setText(tr("Show Squawk"));
    } else {
        quit();
    }
}

void Application::onSquawkDestroyed() {
    destroyingSquawk = false;
    if (nowQuitting) {
        checkForTheLastWindow();
    }
}

void Application::onChangeTray(bool enabled, bool hide)
{
    if (enabled) {
        if (trayIcon == nullptr) {
            if (!hide || squawk == nullptr) {
                createTrayIcon();
            }
        } else {
            if (hide && squawk != nullptr) {
                trayIcon->deleteLater();
                trayIcon = nullptr;
            }
        }
    } else if (trayIcon == nullptr) {
        trayIcon->deleteLater();
        trayIcon = nullptr;
    }
}

void Application::createTrayIcon()
{
    trayIcon = new QSystemTrayIcon();

    QMenu* trayIconMenu = new QMenu();
    trayIconMenu->addAction(&actionToggle);
    trayIconMenu->addAction(&actionQuit);

    trayIcon->setContextMenu(trayIconMenu);
    trayIcon->setIcon(QApplication::windowIcon().pixmap(32, 32));
    trayIcon->setToolTip(QApplication::applicationDisplayName());

    connect(trayIcon, &QSystemTrayIcon::activated, this, &Application::trayClicked);
    connect(trayIcon, &QSystemTrayIcon::destroyed, trayIconMenu, &QMenu::deleteLater);

    trayIcon->show();
}

void Application::trayClicked(QSystemTrayIcon::ActivationReason reason)
{
    switch (reason) {
        case QSystemTrayIcon::Trigger:
        case QSystemTrayIcon::DoubleClick:
        case QSystemTrayIcon::MiddleClick:
            toggleSquawk();
            break;
        default:
            break;
    }
}

void Application::toggleSquawk()
{
    QSettings settings;
    if (squawk == nullptr) {
        createMainWindow();
        if (settings.value("hideTray", false).toBool()) {
            trayIcon->deleteLater();
            trayIcon = nullptr;
        }

        actionToggle.setText(tr("Minimize to tray"));
    } else {
        squawk->close();
    }
}

void Application::onItemCollapsed(const QModelIndex& index)
{
    std::list<QString> address = roster.getItemPath(index);
    if (address.size() > 0) {
        expandedPaths.erase(address);
    }
}

void Application::onItemExpanded(const QModelIndex& index)
{
    std::list<QString> address = roster.getItemPath(index);
    if (address.size() > 0) {
        expandedPaths.insert(address);
    }
}

void Application::onAddedElement(const std::list<QString>& path)
{
    if (squawk != nullptr && expandedPaths.count(path) > 0) {
        QModelIndex index = roster.getIndexByPath(path);
        if (index.isValid()) {
            squawk->expand(index);
        }
    }
}

void Application::notify(const QString& account, const Shared::Message& msg)
{
    QString jid = msg.getPenPalJid();
    QString name = QString(roster.getContactName(account, jid));
    QString path = QString(roster.getContactIconPath(account, jid, msg.getPenPalResource()));
    QVariantList args;
    args << QString();

    uint32_t notificationId = qHash(msg.getId());
    args << notificationId;
    if (path.size() > 0) {
        args << path;
    } else {
        args << QString("mail-message");    //TODO should here better be unknown user icon?
    }
    if (msg.getType() == Shared::Message::groupChat) {
        args << msg.getFromResource() + tr(" from ") + name;
    } else {
        args << name;
    }

    QString body(msg.getBody());
    QString oob(msg.getOutOfBandUrl());
    if (body == oob) {
        body = tr("Attached file");
    }

    args << body;
    args << QStringList({
        "markAsRead", tr("Mark as Read"),
        "openConversation", tr("Open conversation")
    });
    args << QVariantMap({
        {"desktop-entry", qApp->desktopFileName()},
        {"category", QString("message")},
        {"urgency", 1},
       // {"sound-file", "/path/to/macaw/squawk"},
        {"sound-name", QString("message-new-instant")}
    });
    args << -1;
    notifications.callWithArgumentList(QDBus::AutoDetect, "Notify", args);

    storage.insert(std::make_pair(notificationId, std::make_pair(Models::Roster::ElId(account, name), msg.getId())));

    if (squawk != nullptr) {
        QApplication::alert(squawk);
    }
}

void Application::onNotificationClosed(quint32 id, quint32 reason)
{
    Notifications::const_iterator itr = storage.find(id);
    if (itr != storage.end()) {
        if (reason == 2) {  //dissmissed by user (https://specifications.freedesktop.org/notification-spec/latest/ar01s09.html)
            //TODO may ba also mark as read?
        }
        if (reason != 1) {  //just expired, can be activated again from history, so no removing for now
            storage.erase(id);
            qDebug() << "Notification" << id << "was closed";
        }
    }
}

void Application::onNotificationInvoked(quint32 id, const QString& action)
{
    qDebug() << "Notification" << id << action << "request";
    Notifications::const_iterator itr = storage.find(id);
    if (itr != storage.end()) {
        if (action == "markAsRead") {
            roster.markMessageAsRead(itr->second.first, itr->second.second);
        } else if (action == "openConversation") {
            focusConversation(itr->second.first, "", itr->second.second);
        }
    }
}

void Application::unreadMessagesCountChanged(int count)
{
    QDBusMessage signal = QDBusMessage::createSignal("/", "com.canonical.Unity.LauncherEntry", "Update");
    signal << qApp->desktopFileName() + QLatin1String(".desktop");
    signal << QVariantMap ({
        {"count-visible", count != 0},
        {"count", count}
    });
    QDBusConnection::sessionBus().send(signal);
}

void Application::focusConversation(const Models::Roster::ElId& id, const QString& resource, const QString& messageId)
{
    if (squawk != nullptr) {
        if (squawk->currentConversationId() != id) {
            QModelIndex index = roster.getContactIndex(id.account, id.name, resource);
            squawk->select(index);
        }

        if (squawk->isMinimized()) {
            squawk->showNormal();
        } else {
            squawk->show();
        }
        squawk->raise();
        squawk->activateWindow();
    } else {
        openConversation(id, resource);
    }

    //TODO focus messageId;
}

void Application::setState(Shared::Availability p_availability)
{
    if (availability != p_availability) {
        availability = p_availability;
        emit changeState(availability);
    }
}

void Application::stateChanged(Shared::Availability state)
{
    availability = state;
    if (squawk != nullptr) {
        squawk->stateChanged(state);
    }
}

void Application::readSettings()
{
    QSettings settings;
    settings.beginGroup("ui");
    int avail;
    if (settings.contains("availability")) {
        avail = settings.value("availability").toInt();
    } else {
        avail = static_cast<int>(Shared::Availability::online);
    }

    settings.beginGroup("roster");
    QStringList entries = settings.allKeys();
    for (const QString& entry : entries) {
        QStringList p = entry.split("/");
        if (p.last() == "expanded" && settings.value(entry, false).toBool()) {
            p.pop_back();
            expandedPaths.emplace(p.begin(), p.end());
        }
    }

    settings.endGroup();
    settings.endGroup();

    setState(Shared::Global::fromInt<Shared::Availability>(avail));
    createMainWindow();

    if (settings.value("tray", false).toBool() && !settings.value("hideTray", false).toBool()) {
        createTrayIcon();
    }
}

void Application::writeSettings()
{
    QSettings settings;
    settings.beginGroup("ui");
        settings.setValue("availability", static_cast<int>(availability));

        settings.remove("roster");
        settings.beginGroup("roster");
            for (const std::list<QString>& address : expandedPaths) {
                QString path = "";
                for (const QString& hop : address) {
                    path += hop + "/";
                }
                path += "expanded";
                settings.setValue(path, true);
            }

        settings.endGroup();
    settings.endGroup();
}

void Application::requestPassword(const QString& account, bool authenticationError) {
    if (authenticationError) {
        dialogueQueue.addAction(account, DialogQueue::askCredentials);
    } else {
        dialogueQueue.addAction(account, DialogQueue::askPassword);
    }

}
void Application::onConversationClosed()
{
    Conversation* conv = static_cast<Conversation*>(sender());
    Models::Roster::ElId id(conv->getAccount(), conv->getJid());
    Conversations::const_iterator itr = conversations.find(id);
    if (itr != conversations.end()) {
        conversations.erase(itr);
    }
    if (conv->isMuc) {
        Room* room = static_cast<Room*>(conv);
        if (!room->autoJoined()) {
            emit setRoomJoined(id.account, id.name, false);
        }
    }
}

void Application::changeSubscription(const Models::Roster::ElId& id, bool subscribe)
{
    Models::Item::Type type = roster.getContactType(id);

    switch (type) {
        case Models::Item::contact:
            if (subscribe) {
                emit subscribeContact(id.account, id.name, "");
            } else {
                emit unsubscribeContact(id.account, id.name, "");
            }
            break;
        case Models::Item::room:
            setRoomAutoJoin(id.account, id.name, subscribe);
            if (!isConverstationOpened(id)) {
                emit setRoomJoined(id.account, id.name, subscribe);
            }
            break;
        default:
            break;
    }
}

void Application::subscribeConversation(Conversation* conv)
{
    connect(conv, &Conversation::destroyed, this, &Application::onConversationClosed);
    connect(conv, &Conversation::sendMessage, this, &Application::onConversationMessage);
    connect(conv, &Conversation::replaceMessage, this, &Application::onConversationReplaceMessage);
    connect(conv, &Conversation::resendMessage, this, &Application::onConversationResend);
    connect(conv, &Conversation::notifyableMessage, this, &Application::notify);
}

void Application::openConversation(const Models::Roster::ElId& id, const QString& resource)
{
    Conversations::const_iterator itr = conversations.find(id);
    Models::Account* acc = roster.getAccount(id.account);
    Conversation* conv = nullptr;
    bool created = false;
    if (itr != conversations.end()) {
        conv = itr->second;
    } else {
        Models::Element* el = roster.getElement(id);
        if (el != nullptr) {
            if (el->type == Models::Item::room) {
                created = true;
                Models::Room* room = static_cast<Models::Room*>(el);
                conv = new Room(acc, room);
                if (!room->getJoined()) {
                    emit setRoomJoined(id.account, id.name, true);
                }
            } else if (el->type == Models::Item::contact) {
                created = true;
                conv = new Chat(acc, static_cast<Models::Contact*>(el));
            }
        }
    }

    if (conv != nullptr) {
        if (created) {
            conv->setAttribute(Qt::WA_DeleteOnClose);
            subscribeConversation(conv);
            conversations.insert(std::make_pair(id, conv));
        }

        conv->show();
        conv->raise();
        conv->activateWindow();

        if (resource.size() > 0) {
            conv->setPalResource(resource);
        }
    }
}

void Application::onConversationMessage(const Shared::Message& msg)
{
    Conversation* conv = static_cast<Conversation*>(sender());
    QString acc = conv->getAccount();

    roster.addMessage(acc, msg);
    emit sendMessage(acc, msg);
}

void Application::onConversationReplaceMessage(const QString& originalId, const Shared::Message& msg)
{
    Conversation* conv = static_cast<Conversation*>(sender());
    QString acc = conv->getAccount();

    roster.changeMessage(acc, msg.getPenPalJid(), originalId, {
        {"state", static_cast<uint>(Shared::Message::State::pending)}
    });
    emit replaceMessage(acc, originalId, msg);
}

void Application::onConversationResend(const QString& id)
{
    Conversation* conv = static_cast<Conversation*>(sender());
    QString acc = conv->getAccount();
    QString jid = conv->getJid();

    emit resendMessage(acc, jid, id);
}

void Application::onSquawkOpenedConversation() {
    subscribeConversation(squawk->currentConversation);
    Models::Roster::ElId id = squawk->currentConversationId();

    const Models::Element* el = roster.getElementConst(id);
    if (el != nullptr && el->isRoom() && !static_cast<const Models::Room*>(el)->getJoined()) {
        emit setRoomJoined(id.account, id.name, true);
    }
}

void Application::removeAccount(const QString& account)
{
    Conversations::const_iterator itr = conversations.begin();
    while (itr != conversations.end()) {
        if (itr->first.account == account) {
            Conversations::const_iterator lItr = itr;
            ++itr;
            Conversation* conv = lItr->second;
            disconnect(conv, &Conversation::destroyed, this, &Application::onConversationClosed);
            conv->close();
            conversations.erase(lItr);
        } else {
            ++itr;
        }
    }

    if (squawk != nullptr && squawk->currentConversationId().account == account) {
        squawk->closeCurrentConversation();
    }

    roster.removeAccount(account);
}

void Application::changeAccount(const QString& account, const QMap<QString, QVariant>& data)
{
    for (QMap<QString, QVariant>::const_iterator itr = data.begin(), end = data.end(); itr != end; ++itr) {
        QString attr = itr.key();
        roster.updateAccount(account, attr, *itr);
    }
}

void Application::addGroup(const QString& account, const QString& name)
{
    roster.addGroup(account, name);

    if (squawk != nullptr) {
        QSettings settings;
        settings.beginGroup("ui");
        settings.beginGroup("roster");
        settings.beginGroup(account);
        if (settings.value("expanded", false).toBool()) {
            QModelIndex ind = roster.getAccountIndex(account);
            squawk->expand(ind);
            if (settings.value(name + "/expanded", false).toBool()) {
                squawk->expand(roster.getGroupIndex(account, name));
            }
        }
        settings.endGroup();
        settings.endGroup();
        settings.endGroup();
    }
}

bool Application::isConverstationOpened(const Models::Roster::ElId& id) const {
    return (conversations.count(id) > 0) || (squawk != nullptr && squawk->currentConversationId() == id);}