/*
 * 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 "squawk.h"
#include "ui_squawk.h"
#include <QDebug>
#include <QIcon>

Squawk::Squawk(Models::Roster& p_rosterModel, QWidget *parent) :
    QMainWindow(parent),
    m_ui(new Ui::Squawk),
    accounts(nullptr),
    preferences(nullptr),
    about(nullptr),
    rosterModel(p_rosterModel),
    contextMenu(new QMenu()),
    vCards(),
    currentConversation(nullptr),
    restoreSelection(),
    needToRestore(false)
{
    m_ui->setupUi(this);
    m_ui->roster->setModel(&rosterModel);
    m_ui->roster->setContextMenuPolicy(Qt::CustomContextMenu);
    if (QApplication::style()->styleHint(QStyle::SH_ScrollBar_Transient) == 1) {
        m_ui->roster->setColumnWidth(1, 52);
    } else {
        m_ui->roster->setColumnWidth(1, 26);
    }
    m_ui->roster->setIconSize(QSize(20, 20));
    m_ui->roster->header()->setStretchLastSection(false);
    m_ui->roster->header()->setSectionResizeMode(0, QHeaderView::Stretch);
    
    for (int i = static_cast<int>(Shared::AvailabilityLowest); i < static_cast<int>(Shared::AvailabilityHighest) + 1; ++i) {
        Shared::Availability av = static_cast<Shared::Availability>(i);
        m_ui->comboBox->addItem(Shared::availabilityIcon(av), Shared::Global::getName(av));
    }
    m_ui->comboBox->setCurrentIndex(static_cast<int>(Shared::Availability::offline));
    
    connect(m_ui->actionAccounts, &QAction::triggered, this, &Squawk::onAccounts);
    connect(m_ui->actionPreferences, &QAction::triggered, this, &Squawk::onPreferences);
    connect(m_ui->actionAddContact, &QAction::triggered, this, &Squawk::onNewContact);
    connect(m_ui->actionAddConference, &QAction::triggered, this, &Squawk::onNewConference);
    connect(m_ui->actionQuit, &QAction::triggered, this, &Squawk::close);
    connect(m_ui->comboBox, qOverload<int>(&QComboBox::activated), this, &Squawk::onComboboxActivated);
    //connect(m_ui->roster, &QTreeView::doubleClicked, this, &Squawk::onRosterItemDoubleClicked);
    connect(m_ui->roster, &QTreeView::customContextMenuRequested, this, &Squawk::onRosterContextMenu);
    connect(m_ui->roster, &QTreeView::collapsed, this, &Squawk::onItemCollepsed);
    connect(m_ui->roster->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &Squawk::onRosterSelectionChanged);
    
    connect(rosterModel.accountsModel, &Models::Accounts::sizeChanged, this, &Squawk::onAccountsSizeChanged);
    connect(contextMenu, &QMenu::aboutToHide, this, &Squawk::onContextAboutToHide);
    connect(m_ui->actionAboutSquawk, &QAction::triggered, this, &Squawk::onAboutSquawkCalled);
    //m_ui->mainToolBar->addWidget(m_ui->comboBox);
    
    if (testAttribute(Qt::WA_TranslucentBackground)) {
        m_ui->roster->viewport()->setAutoFillBackground(false);
    }
    
    QSettings settings;
    settings.beginGroup("ui");
    settings.beginGroup("window");
    if (settings.contains("geometry")) {
        restoreGeometry(settings.value("geometry").toByteArray());
    }
    if (settings.contains("state")) {
        restoreState(settings.value("state").toByteArray());
    }
    settings.endGroup();
    
    if (settings.contains("splitter")) {
        m_ui->splitter->restoreState(settings.value("splitter").toByteArray());
    }
    settings.endGroup();
}

Squawk::~Squawk() {
    delete contextMenu;
}

void Squawk::onAccounts()
{
    if (accounts == nullptr) {
        accounts = new Accounts(rosterModel.accountsModel);
        accounts->setAttribute(Qt::WA_DeleteOnClose);
        connect(accounts, &Accounts::destroyed, this, &Squawk::onAccountsClosed);
        connect(accounts, &Accounts::newAccount, this, &Squawk::newAccountRequest);
        connect(accounts, &Accounts::changeAccount, this, &Squawk::modifyAccountRequest);
        connect(accounts, &Accounts::connectAccount, this, &Squawk::connectAccount);
        connect(accounts, &Accounts::disconnectAccount, this, &Squawk::disconnectAccount);
        connect(accounts, &Accounts::removeAccount, this, &Squawk::removeAccountRequest);
        
        accounts->show();
    } else {
        accounts->show();
        accounts->raise();
        accounts->activateWindow();
    }
}

void Squawk::onPreferences()
{
    if (preferences == nullptr) {
        preferences = new Settings();
        preferences->setAttribute(Qt::WA_DeleteOnClose);
        connect(preferences, &Settings::destroyed, this, &Squawk::onPreferencesClosed);
        connect(preferences, &Settings::changeDownloadsPath, this, &Squawk::changeDownloadsPath);

        preferences->show();
    } else {
        preferences->show();
        preferences->raise();
        preferences->activateWindow();
    }
}


void Squawk::onAccountsSizeChanged(unsigned int size)
{
    if (size > 0) {
        m_ui->actionAddContact->setEnabled(true);
        m_ui->actionAddConference->setEnabled(true);
    } else {
        m_ui->actionAddContact->setEnabled(false);
        m_ui->actionAddConference->setEnabled(false);
    }
}

void Squawk::onNewContact()
{
    NewContact* nc = new NewContact(rosterModel.accountsModel, this);
    
    connect(nc, &NewContact::accepted, this, &Squawk::onNewContactAccepted);
    connect(nc, &NewContact::rejected, nc, &NewContact::deleteLater);
    
    nc->exec();
}

void Squawk::onNewConference()
{
    JoinConference* jc = new JoinConference(rosterModel.accountsModel, this);
    
    connect(jc, &JoinConference::accepted, this, &Squawk::onJoinConferenceAccepted);
    connect(jc, &JoinConference::rejected, jc, &JoinConference::deleteLater);
    
    jc->exec();
}

void Squawk::onNewContactAccepted()
{
    NewContact* nc = static_cast<NewContact*>(sender());
    NewContact::Data value = nc->value();
    
    emit addContactRequest(value.account, value.jid, value.name, value.groups);
    
    nc->deleteLater();
}

void Squawk::onJoinConferenceAccepted()
{
    JoinConference* jc = static_cast<JoinConference*>(sender());
    JoinConference::Data value = jc->value();
    
    emit addRoomRequest(value.account, value.jid, value.nick, value.password, value.autoJoin);
    
    jc->deleteLater();
}

void Squawk::closeEvent(QCloseEvent* event)
{
    if (accounts != nullptr) {
        accounts->close();
    }
    if (preferences != nullptr) {
        preferences->close();
    }
    if (about != nullptr) {
        about->close();
    }
    
    for (std::map<QString, VCard*>::const_iterator itr = vCards.begin(), end = vCards.end(); itr != end; ++itr) {
        disconnect(itr->second, &VCard::destroyed, this, &Squawk::onVCardClosed);
        itr->second->close();
    }
    vCards.clear();
    writeSettings();
    emit closing();;

    QMainWindow::closeEvent(event);
}

void Squawk::onAccountsClosed() {
    accounts = nullptr;}

void Squawk::onPreferencesClosed() {
    preferences = nullptr;}

void Squawk::onAboutSquawkClosed() {
    about = nullptr;}

void Squawk::onComboboxActivated(int index)
{
    Shared::Availability av = Shared::Global::fromInt<Shared::Availability>(index);
    emit changeState(av);
}

void Squawk::expand(const QModelIndex& index) {
    m_ui->roster->expand(index);}

void Squawk::stateChanged(Shared::Availability state) {
    m_ui->comboBox->setCurrentIndex(static_cast<int>(state));}

void Squawk::onRosterItemDoubleClicked(const QModelIndex& item)
{
    if (item.isValid()) {
        Models::Item* node = static_cast<Models::Item*>(item.internalPointer());
        if (node->type == Models::Item::reference) {
            node = static_cast<Models::Reference*>(node)->dereference();
        }
        Models::Contact* contact = nullptr;
        Models::Room* room = nullptr;
        switch (node->type) {
            case Models::Item::contact:
                contact = static_cast<Models::Contact*>(node);
                emit openConversation(Models::Roster::ElId(contact->getAccountName(), contact->getJid()));
                break;
            case Models::Item::presence:
                contact = static_cast<Models::Contact*>(node->parentItem());
                emit openConversation(Models::Roster::ElId(contact->getAccountName(), contact->getJid()), node->getName());
                break;
            case Models::Item::room:
                room = static_cast<Models::Room*>(node);
                emit openConversation(Models::Roster::ElId(room->getAccountName(), room->getJid()));
                break;
            default:
                m_ui->roster->expand(item);
                break;
        }
    }
}

void Squawk::closeCurrentConversation()
{
    if (currentConversation != nullptr) {
        currentConversation->deleteLater();
        currentConversation = nullptr;
        m_ui->filler->show();
    }
}

void Squawk::onRosterContextMenu(const QPoint& point)
{
    QModelIndex index = m_ui->roster->indexAt(point);
    if (index.isValid()) {
        Models::Item* item = static_cast<Models::Item*>(index.internalPointer());
        if (item->type == Models::Item::reference) {
            item = static_cast<Models::Reference*>(item)->dereference();
        }
        contextMenu->clear();
        bool hasMenu = false;
        bool active = item->getAccountConnectionState() == Shared::ConnectionState::connected;
        switch (item->type) {
            case Models::Item::account: {
                Models::Account* acc = static_cast<Models::Account*>(item);
                hasMenu = true;
                QString name = acc->getName();
                
                if (acc->getActive()) {
                    QAction* con = contextMenu->addAction(Shared::icon("network-disconnect"), tr("Deactivate"));
                    connect(con, &QAction::triggered, std::bind(&Squawk::disconnectAccount, this, name));
                } else {
                    QAction* con = contextMenu->addAction(Shared::icon("network-connect"), tr("Activate"));
                    connect(con, &QAction::triggered, std::bind(&Squawk::connectAccount, this, name));
                }
                
                QAction* card = contextMenu->addAction(Shared::icon("user-properties"), tr("VCard"));
                card->setEnabled(active);
                connect(card, &QAction::triggered, std::bind(&Squawk::onActivateVCard, this, name, acc->getBareJid(), true));
                
                QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove"));
                connect(remove, &QAction::triggered, std::bind(&Squawk::removeAccountRequest, this, name));
            }
                break;
            case Models::Item::contact: {
                Models::Contact* cnt = static_cast<Models::Contact*>(item);
                Models::Roster::ElId id(cnt->getAccountName(), cnt->getJid());
                QString cntName = cnt->getName();
                hasMenu = true;
                
                QAction* dialog = contextMenu->addAction(Shared::icon("mail-message"), tr("Open dialog"));
                dialog->setEnabled(active);
                connect(dialog, &QAction::triggered, std::bind(&Squawk::onRosterItemDoubleClicked, this, index));
                
                Shared::SubscriptionState state = cnt->getState();
                switch (state) {
                    case Shared::SubscriptionState::both:
                    case Shared::SubscriptionState::to: {
                        QAction* unsub = contextMenu->addAction(Shared::icon("news-unsubscribe"), tr("Unsubscribe"));
                        unsub->setEnabled(active);
                        connect(unsub, &QAction::triggered, std::bind(&Squawk::changeSubscription, this, id, false));
                    }
                    break;
                    case Shared::SubscriptionState::from:
                    case Shared::SubscriptionState::unknown:
                    case Shared::SubscriptionState::none: {
                        QAction* sub = contextMenu->addAction(Shared::icon("news-subscribe"), tr("Subscribe"));
                        sub->setEnabled(active);
                        connect(sub, &QAction::triggered, std::bind(&Squawk::changeSubscription, this, id, true));
                    }    
                }
                
                QAction* rename = contextMenu->addAction(Shared::icon("edit-rename"), tr("Rename"));
                rename->setEnabled(active);
                connect(rename, &QAction::triggered, [this, cntName, id]() {
                    QInputDialog* dialog = new QInputDialog(this);
                    connect(dialog, &QDialog::accepted, [this, dialog, cntName, id]() {
                        QString newName = dialog->textValue();
                        if (newName != cntName) {
                            emit renameContactRequest(id.account, id.name, newName);
                        }
                        dialog->deleteLater();
                    });
                    connect(dialog, &QDialog::rejected, dialog, &QObject::deleteLater);
                    dialog->setInputMode(QInputDialog::TextInput);
                    dialog->setLabelText(tr("Input new name for %1\nor leave it empty for the contact \nto be displayed as %1").arg(id.name));
                    dialog->setWindowTitle(tr("Renaming %1").arg(id.name));
                    dialog->setTextValue(cntName);
                    dialog->exec();
                });
                
                
                QMenu* groupsMenu = contextMenu->addMenu(Shared::icon("group"), tr("Groups"));
                std::deque<QString> groupList = rosterModel.groupList(id.account);
                for (QString groupName : groupList) {
                    QAction* gr = groupsMenu->addAction(groupName);
                    gr->setCheckable(true);
                    gr->setChecked(rosterModel.groupHasContact(id.account, groupName, id.name));
                    gr->setEnabled(active);
                    connect(gr, &QAction::toggled, [this,  groupName, id](bool checked) {
                        if (checked) {
                            emit addContactToGroupRequest(id.account, id.name, groupName);
                        } else {
                            emit removeContactFromGroupRequest(id.account, id.name, groupName);
                        }
                    });
                }
                QAction* newGroup = groupsMenu->addAction(Shared::icon("group-new"), tr("New group"));
                newGroup->setEnabled(active);
                connect(newGroup, &QAction::triggered, [this, id]() {
                    QInputDialog* dialog = new QInputDialog(this);
                    connect(dialog, &QDialog::accepted, [this, dialog, id]() {
                        emit addContactToGroupRequest(id.account, id.name, dialog->textValue());
                        dialog->deleteLater();
                    });
                    connect(dialog, &QDialog::rejected, dialog, &QObject::deleteLater);
                    dialog->setInputMode(QInputDialog::TextInput);
                    dialog->setLabelText(tr("New group name"));
                    dialog->setWindowTitle(tr("Add %1 to a new group").arg(id.name));
                    dialog->exec();
                });
                
                
                QAction* card = contextMenu->addAction(Shared::icon("user-properties"), tr("VCard"));
                card->setEnabled(active);
                connect(card, &QAction::triggered, std::bind(&Squawk::onActivateVCard, this, id.account, id.name, false));
                
                QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove"));
                remove->setEnabled(active);
                connect(remove, &QAction::triggered, std::bind(&Squawk::removeContactRequest, this, id.account, id.name));
                
            }
                break;
            case Models::Item::room: {
                Models::Room* room = static_cast<Models::Room*>(item);
                hasMenu = true;
                
                QAction* dialog = contextMenu->addAction(Shared::icon("mail-message"), tr("Open conversation"));
                dialog->setEnabled(active);
                connect(dialog, &QAction::triggered, [this, index]() {
                    onRosterItemDoubleClicked(index);
                });
                
                
                Models::Roster::ElId id(room->getAccountName(), room->getJid());
                if (room->getAutoJoin()) {
                    QAction* unsub = contextMenu->addAction(Shared::icon("news-unsubscribe"), tr("Unsubscribe"));
                    unsub->setEnabled(active);
                    connect(unsub, &QAction::triggered, std::bind(&Squawk::changeSubscription, this, id, false));
                } else {
                    QAction* sub = contextMenu->addAction(Shared::icon("news-subscribe"), tr("Subscribe"));
                    sub->setEnabled(active);
                    connect(sub, &QAction::triggered, std::bind(&Squawk::changeSubscription, this, id, true));
                }
                
                QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove"));
                remove->setEnabled(active);
                connect(remove, &QAction::triggered, std::bind(&Squawk::removeRoomRequest, this, id.account, id.name));
            }
                break;
            default:
                break;
        }
        if (hasMenu) {
            contextMenu->popup(m_ui->roster->viewport()->mapToGlobal(point));
        }
    }    
}

void Squawk::responseVCard(const QString& jid, const Shared::VCard& card)
{
    std::map<QString, VCard*>::const_iterator itr = vCards.find(jid);
    if (itr != vCards.end()) {
        itr->second->setVCard(card);
        itr->second->hideProgress();
    }
}

void Squawk::onVCardClosed()
{
    VCard* vCard = static_cast<VCard*>(sender());
    
    std::map<QString, VCard*>::const_iterator itr = vCards.find(vCard->getJid());
    if (itr == vCards.end()) {
        qDebug() << "VCard has been closed but can not be found among other opened vCards, application is most probably going to crash";
        return;
    }
    vCards.erase(itr);
}

void Squawk::onActivateVCard(const QString& account, const QString& jid, bool edition)
{
    std::map<QString, VCard*>::const_iterator itr = vCards.find(jid);
    VCard* card;
    if (itr != vCards.end()) {
        card = itr->second;
    } else {
        card = new VCard(jid, edition);
        if (edition) {
            card->setWindowTitle(tr("%1 account card").arg(account));
        } else {
            card->setWindowTitle(tr("%1 contact card").arg(jid));
        }
        card->setAttribute(Qt::WA_DeleteOnClose);
        vCards.insert(std::make_pair(jid, card));
        
        connect(card, &VCard::destroyed, this, &Squawk::onVCardClosed);
        connect(card, &VCard::saveVCard, std::bind( &Squawk::onVCardSave, this, std::placeholders::_1, account));
    }
    
    card->show();
    card->raise();
    card->activateWindow();
    card->showProgress(tr("Downloading vCard"));
    
    emit requestVCard(account, jid);
}

void Squawk::onVCardSave(const Shared::VCard& card, const QString& account)
{
    VCard* widget = static_cast<VCard*>(sender());
    emit uploadVCard(account, card);
    
    widget->deleteLater();
}

void Squawk::writeSettings()
{
    QSettings settings;
    settings.beginGroup("ui");
        settings.beginGroup("window");
            settings.setValue("geometry", saveGeometry());
            settings.setValue("state", saveState());
        settings.endGroup();
    
        settings.setValue("splitter", m_ui->splitter->saveState());
        settings.remove("roster");
        settings.beginGroup("roster");
            int size = rosterModel.accountsModel->rowCount(QModelIndex());
            for (int i = 0; i < size; ++i) {
                QModelIndex acc = rosterModel.index(i, 0, QModelIndex());
                Models::Account* account = rosterModel.accountsModel->getAccount(i);
                QString accName = account->getName();
                settings.beginGroup(accName);

                    settings.setValue("expanded", m_ui->roster->isExpanded(acc));
                    std::deque<QString> groups = rosterModel.groupList(accName);
                    for (const QString& groupName : groups) {
                        settings.beginGroup(groupName);
                            QModelIndex gIndex = rosterModel.getGroupIndex(accName, groupName);
                            settings.setValue("expanded", m_ui->roster->isExpanded(gIndex));
                        settings.endGroup();
                    }

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

    settings.sync();
}

void Squawk::onItemCollepsed(const QModelIndex& index)
{
    QSettings settings;
    Models::Item* item = static_cast<Models::Item*>(index.internalPointer());
    switch (item->type) {
        case Models::Item::account:
            settings.setValue("ui/roster/" + item->getName() + "/expanded", false);
            break;
        case Models::Item::group: {
            QModelIndex accInd = rosterModel.parent(index);
            Models::Account* account = rosterModel.accountsModel->getAccount(accInd.row());
            settings.setValue("ui/roster/" + account->getName() + "/" + item->getName() + "/expanded", false);
        }
            break;
        default:
            break;
    }
}

void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIndex& previous)
{   
    if (restoreSelection.isValid() && restoreSelection == current) {
        restoreSelection = QModelIndex();
        return;
    }
    
    if (current.isValid()) {
        Models::Item* node = static_cast<Models::Item*>(current.internalPointer());
        if (node->type == Models::Item::reference) {
            node = static_cast<Models::Reference*>(node)->dereference();
        }
        Models::Contact* contact = nullptr;
        Models::Room* room = nullptr;
        QString res;
        Models::Roster::ElId* id = nullptr;
        bool hasContext = true;
        switch (node->type) {
            case Models::Item::contact:
                contact = static_cast<Models::Contact*>(node);
                id = new Models::Roster::ElId(contact->getAccountName(), contact->getJid());
                break;
            case Models::Item::presence:
                contact = static_cast<Models::Contact*>(node->parentItem());
                id = new Models::Roster::ElId(contact->getAccountName(), contact->getJid());
                res = node->getName();
                hasContext = false;
                break;
            case Models::Item::room:
                room = static_cast<Models::Room*>(node);
                id = new Models::Roster::ElId(room->getAccountName(), room->getJid());
                break;
            case Models::Item::participant:
                room = static_cast<Models::Room*>(node->parentItem());
                id = new Models::Roster::ElId(room->getAccountName(), room->getJid());
                hasContext = false;
                break;
            case Models::Item::group:
                hasContext = false;
            default:
                break;
        }
        
        if (hasContext && QGuiApplication::mouseButtons() & Qt::RightButton) {
            if (id != nullptr) {
                delete id;
            }
            needToRestore = true;
            restoreSelection = previous;
            return;
        }
        
        if (id != nullptr) {
            if (currentConversation != nullptr) {
                if (currentConversation->getId() == *id) {
                    if (contact != nullptr) {
                        currentConversation->setPalResource(res);
                    }
                    return;
                } else {
                    currentConversation->deleteLater();
                }
            } else {
                m_ui->filler->hide();
            }
            
            Models::Account* acc = rosterModel.getAccount(id->account);
            if (contact != nullptr) {
                currentConversation = new Chat(acc, contact);
            } else if (room != nullptr) {
                currentConversation = new Room(acc, room);
            }
            if (!testAttribute(Qt::WA_TranslucentBackground)) {
                currentConversation->setFeedFrames(true, false, true, true);
            }
            
            emit openedConversation();
            
            if (res.size() > 0) {
                currentConversation->setPalResource(res);
            }
            
            m_ui->splitter->insertWidget(1, currentConversation);
            
            delete id;
        } else {
            closeCurrentConversation();
        }
    } else {
        closeCurrentConversation();
    }
}

void Squawk::onContextAboutToHide()
{
    if (needToRestore) {
        needToRestore = false;
        m_ui->roster->selectionModel()->setCurrentIndex(restoreSelection, QItemSelectionModel::ClearAndSelect);
    }
}

void Squawk::onAboutSquawkCalled()
{
    if (about == nullptr) {
        about = new About();
        about->setAttribute(Qt::WA_DeleteOnClose);
        connect(about, &Settings::destroyed, this, &Squawk::onAboutSquawkClosed);
    } else {
        about->raise();
        about->activateWindow();
    }
    about->show();
}

Models::Roster::ElId Squawk::currentConversationId() const
{
    if (currentConversation == nullptr) {
        return Models::Roster::ElId();
    } else {
        return Models::Roster::ElId(currentConversation->getAccount(), currentConversation->getJid());
    }
}

void Squawk::select(QModelIndex index)
{
    m_ui->roster->scrollTo(index, QAbstractItemView::EnsureVisible);
    m_ui->roster->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect);
}