/*
 * contactlistmodel.cpp - model of contact list
 * Copyright (C) 2008-2010  Yandex LLC (Michail Pishchagin)
 *
 * 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, see <https://www.gnu.org/licenses/>.
 *
 */

#include "avatars.h"
#include "contactlistitem.h"
#include "contactlistmodel_p.h"
#include "debug.h"
#include "psiaccount.h"
#include "psicontactlist.h"
#include "psioptions.h"
#include "userlist.h"

#include <QColor>
#include <QIcon>
#include <QMessageBox>
#include <QModelIndex>
#include <QTextDocument>
#include <QVariant>

#define MAX_COMMIT_DELAY 30 /* seconds */
#define COMMIT_INTERVAL 100 /* msecs */
#define COLLAPSED_OPTIONS "options.contactlist.group-state.collapsed"
#define HIDDEN_OPTIONS "options.contactlist.group-state.hidden"

/*****************************/
/* ContactListModel::Private */
/*****************************/

ContactListModel::Private::Private(ContactListModel *parent) :
    QObject(), q(parent), groupsEnabled(false), accountsEnabled(false), contactList(nullptr),
    commitTimer(new QTimer(this))
{
    connect(commitTimer, SIGNAL(timeout()), SLOT(commit()));
    commitTimer->setSingleShot(true);
    commitTimer->setInterval(COMMIT_INTERVAL);

    collapsed = PsiOptions::instance()->getOption(COLLAPSED_OPTIONS, QStringList()).toStringList();
    hidden    = PsiOptions::instance()->getOption(HIDDEN_OPTIONS, QStringList()).toStringList();
}

ContactListModel::Private::~Private() { }

void ContactListModel::Private::realAddContact(PsiContact *contact)
{
    ContactListItem *root = static_cast<ContactListItem *>(q->root());
    ;
    if (accountsEnabled) {
        PsiAccount      *account     = contact->account();
        ContactListItem *accountItem = root->findAccount(account);

        if (!accountItem) {
            accountItem = new ContactListItem(q, ContactListItem::Type::AccountType);
            accountItem->setAccount(account);
            accountItem->setExpanded(!collapsed.contains(accountItem->internalName()));

            connect(account, SIGNAL(accountDestroyed()), SLOT(onAccountDestroyed()));
            connect(account, SIGNAL(updatedAccount()), SLOT(updateAccount()));

            root->appendChild(accountItem);
        }
        root = accountItem;
    }

    if (!contact->isSelf() && groupsEnabled) {
        ContactListItem::SpecialGroupType specialGroupType = specialGroupFor(contact);
        QStringList groups = specialGroupType == ContactListItem::SpecialGroupType::NoneSpecialGroupType
            ? contact->groups()
            : QStringList { QString() };

        for (const QString &groupName : groups) {

            ContactListItem *groupItem = nullptr;
            if (specialGroupType == ContactListItem::SpecialGroupType::NoneSpecialGroupType)
                groupItem = root->findGroup(groupName);
            else
                groupItem = root->findGroup(specialGroupType);

            // No duplicates
            if (groupItem && groupItem->findContact(contact)) {
                continue;
            }

            ContactListItem *item = new ContactListItem(q, ContactListItem::Type::ContactType);
            item->setContact(contact);

            if (!groupItem) {
                groupItem = new ContactListItem(q, ContactListItem::Type::GroupType, specialGroupType);
                if (specialGroupType == ContactListItem::SpecialGroupType::NoneSpecialGroupType)
                    groupItem->setName(groupName);

                root->appendChild(groupItem);
                groupItem->setExpanded(!collapsed.contains(groupItem->internalName()));
                groupItem->setHidden(hidden.contains(groupItem->internalName()), false);
            }
            groupItem->appendChild(item);

            monitoredContacts.insert(contact, q->toModelIndex(item));
        }
    } else {
        ContactListItem *item = new ContactListItem(q, ContactListItem::Type::ContactType);
        item->setContact(contact);
        root->appendChild(item);
        monitoredContacts.insert(contact, q->toModelIndex(item));
    }

    connect(contact, &PsiContact::destroyed, this, &Private::removeContact);
    connect(contact, &PsiContact::groupsChanged, this, &Private::contactGroupsChanged);
    connect(contact, &PsiContact::updated, this, &Private::contactUpdated);
    connect(contact, &PsiContact::alert, this, &Private::contactUpdated);
    connect(contact, &PsiContact::anim, this, &Private::contactUpdated);
}

void ContactListModel::Private::addContacts(const QList<PsiContact *> &contacts)
{
    SLOW_TIMER(100);

    if (contacts.isEmpty())
        return;

    emit q->layoutAboutToBeChanged();
    for (auto *contact : contacts) {
        realAddContact(contact);
    }
    emit q->layoutChanged();
}

void ContactListModel::Private::updateContacts(const QList<PsiContact *> &contacts)
{
    SLOW_TIMER(100);

    if (contacts.isEmpty())
        return;

    // prepare ranges for updating to reduce 'emit dataChanged' invoking
    QHash<QModelIndex, QPair<int, int>> ranges;
    QModelIndexList                     indexes;
    for (const PsiContact *contact : contacts) {
        QModelIndexList indexes2 = q->indexesFor(contact);
        indexes += indexes2;

        for (const QModelIndex &index : std::as_const(indexes2)) {
            QModelIndex parent = index.parent();
            int         row    = index.row();
            if (ranges.contains(parent)) {
                if (index.row() < ranges.value(parent).first)
                    ranges[parent].first = row;
                else if (index.row() > ranges.value(parent).second)
                    ranges[parent].second = row;
            } else {
                ranges.insert(parent, QPair<int, int>(row, row));
            }
        }
    }

    QHashIterator<QModelIndex, QPair<int, int>> it(ranges);
    while (it.hasNext()) {
        it.next();
        int         row1  = it.value().first;
        int         row2  = it.value().second;
        QModelIndex index = it.key();

        // update contact
        emit q->dataChanged(q->index(row1, 0, index), q->index(row2, 0, index));

        // Update group
        emit q->dataChanged(index, index);
    }
}

void ContactListModel::Private::addOperation(PsiContact *contact, ContactListModel::Private::Operation operation)
{
    if (operation == AddContact && monitoredContacts.contains(contact)) {
        Q_ASSERT(false);
    }

    if (!operationQueue.contains(contact)) {
        operationQueue[contact] = operation;
    } else {
        operationQueue[contact] |= operation;
    }

    if (commitTimerStartTime.isNull())
        commitTimerStartTime = QDateTime::currentDateTime();

    if (commitTimerStartTime.secsTo(QDateTime::currentDateTime()) > MAX_COMMIT_DELAY)
        commit();
    else
        commitTimer->start();
}

int ContactListModel::Private::simplifiedOperationList(int operations) const
{
    return (operations & AddContact) ? AddContact : operations;
}

// Detect special group type for contact
ContactListItem::SpecialGroupType ContactListModel::Private::specialGroupFor(PsiContact *contact)
{
    ContactListItem::SpecialGroupType type = ContactListItem::SpecialGroupType::NoneSpecialGroupType;

    if (contact->isPrivate()) {
        type = ContactListItem::SpecialGroupType::MucPrivateChatsSpecialGroupType;
    } else if (!contact->inList()) {
        type = ContactListItem::SpecialGroupType::NotInListSpecialGroupType;
    } else if (contact->isAgent()) {
        type = ContactListItem::SpecialGroupType::TransportsSpecialGroupType;
    } else if (contact->isConference()) {
        type = ContactListItem::SpecialGroupType::ConferenceSpecialGroupType;
    } else if (contact->noGroups()) {
        type = ContactListItem::SpecialGroupType::GeneralSpecialGroupType;
    }

    return type;
}

void ContactListModel::Private::commit()
{
    SLOW_TIMER(100);

    commitTimerStartTime = QDateTime();

    commitTimer->stop();
    if (operationQueue.isEmpty())
        return;

    QHashIterator<PsiContact *, int> it(operationQueue);

    QList<PsiContact *> contactsForAdding;
    QList<PsiContact *> contactsForUpdate;

    while (it.hasNext()) {
        it.next();
        PsiContact *contact = it.key();

        int operations = simplifiedOperationList(it.value());
        if (operations & AddContact) {
            Q_ASSERT(!monitoredContacts.contains(contact));
            if (monitoredContacts.contains(contact))
                continue;

            contactsForAdding << contact;
        }
        if (operations & RemoveContact)
            Q_ASSERT(false);
        if (operations & UpdateContact)
            contactsForUpdate << contact;
        if (operations & ContactGroupsChanged) {
            removeContact(contact);
            if (!contactsForAdding.contains(contact)) {
                contactsForAdding << contact;
            }
        }
    }

    addContacts(contactsForAdding);
    updateContacts(contactsForUpdate);
    operationQueue.clear();
}

void ContactListModel::Private::clear()
{
    q->beginResetModel();

    // disconnect accounts. they have nothing to update here after clear
    const auto &children = q->root()->children();
    for (const auto &child : children) {
        ContactListItem *item = static_cast<ContactListItem *>(child);
        if (item->isAccount()) {
            cleanUpAccount(item->account());
        }
    }

    qDeleteAll(q->root()->children());

    operationQueue.clear();

#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    QHashIterator<PsiContact *, QPersistentModelIndex> it(monitoredContacts);
#else
    QMultiHashIterator<PsiContact *, QPersistentModelIndex> it(monitoredContacts);
#endif
    while (it.hasNext()) {
        it.next();
        disconnect(it.key(), nullptr, this, nullptr);
    }
    monitoredContacts.clear();

    q->endResetModel();
}

void ContactListModel::Private::addContact(PsiContact *contact)
{
    Q_ASSERT(!monitoredContacts.contains(contact));
    if (monitoredContacts.contains(contact))
        return;

    addOperation(contact, AddContact);
}

/*!
 * removeContact() could be called directly by PsiContactList when account is disabled, or
 * by contact's destructor. We just ensure that the calls are balanced.
 */

void ContactListModel::Private::removeContact(PsiContact *contact)
{
    Q_ASSERT(monitoredContacts.contains(contact));
    if (!monitoredContacts.contains(contact))
        return;

    while (monitoredContacts.contains(contact)) {
        QModelIndex index = monitoredContacts.take(contact);
        if (!index.isValid()) {
            continue;
        }

        q->beginRemoveRows(index.parent(), index.row(), index.row());
        ContactListItem *item  = q->toItem(index);
        ContactListItem *group = item->parent();

        delete item;
        q->endRemoveRows();

        index = q->toModelIndex(group);
        // Delete empty group
        if (group->isGroup() && !group->childCount()) {
            q->beginRemoveRows(index.parent(), index.row(), index.row());
            delete group;
            q->endRemoveRows();

        } else {
            // Update group
            emit q->dataChanged(index, index);
        }
    }
    disconnect(contact, nullptr, this, nullptr);
    operationQueue.remove(contact);
}

void ContactListModel::Private::contactUpdated()
{
    PsiContact *contact = qobject_cast<PsiContact *>(sender());
    Q_ASSERT(monitoredContacts.contains(contact));
    if (!monitoredContacts.contains(contact))
        return;

    // Check for groups changing
    // Maybe very difficult and should be simplified?
    QList<ContactListItem *> groupItems;
    const auto              &indexes = monitoredContacts.values(contact);
    for (const QPersistentModelIndex &index : indexes) {
        ContactListItem *item   = q->toItem(index);
        ContactListItem *parent = item->parent();
        if (parent && parent->isGroup()) {
            groupItems << parent;
        }
    }

    Operation                         operation        = Operation::UpdateContact;
    ContactListItem::SpecialGroupType specialGroupType = specialGroupFor(contact);
    if (specialGroupType == ContactListItem::SpecialGroupType::NoneSpecialGroupType) {
        QStringList groups1;
        for (ContactListItem *item : std::as_const(groupItems)) {
            groups1 << item->name();
        }
        groups1.sort();
        QStringList groups2 = contact->groups();
        groups2.sort();
        if (groups1 != groups2) {
            operation = Operation::ContactGroupsChanged;
        }
    } else if (groupItems.size() > 1
               || (!groupItems.isEmpty() && groupItems.first()->specialGroupType() != specialGroupType)) {
        operation = Operation::ContactGroupsChanged;
    }

    addOperation(contact, operation);
}

void ContactListModel::Private::contactGroupsChanged()
{
    PsiContact *contact = qobject_cast<PsiContact *>(sender());
    Q_ASSERT(monitoredContacts.contains(contact));
    if (!monitoredContacts.contains(contact))
        return;

    addOperation(contact, ContactGroupsChanged);
}

void ContactListModel::Private::onAccountDestroyed()
{
    PsiAccount *account = qobject_cast<PsiAccount *>(sender());
    cleanUpAccount(account);
}

void ContactListModel::Private::updateAccount()
{
    PsiAccount *account = qobject_cast<PsiAccount *>(sender());
    if (account->enabled()) {
        ContactListItem *root = static_cast<ContactListItem *>(q->root());
        ;
        ContactListItem *accountItem = root->findAccount(account);
        Q_ASSERT(accountItem);
        q->updateItem(accountItem);
    } else {
        cleanUpAccount(account);
    }
}

void ContactListModel::Private::cleanUpAccount(PsiAccount *account)
{
    disconnect(account, nullptr, this, nullptr);
    ContactListItem *root = static_cast<ContactListItem *>(q->root());
    ContactListItem *item = root->findAccount(account);
    if (!item) {
        qCritical("BUG: account was already removed from the list");
        return;
    }
    QModelIndex index = q->toModelIndex(item);
    q->beginRemoveRows(index.parent(), index.row(), index.row());
    delete item;
    q->endRemoveRows();
}

/********************/
/* ContactListModel */
/********************/

ContactListModel::ContactListModel(PsiContactList *contactList, QObject *parent) :
    AbstractTreeModel(new ContactListItem(this, ContactListItem::Type::RootType), parent), d(new Private(this))
{
    d->contactList = contactList;

    connect(contactList, SIGNAL(addedContact(PsiContact *)), d, SLOT(addContact(PsiContact *)));
    connect(contactList, SIGNAL(removedContact(PsiContact *)), d, SLOT(removeContact(PsiContact *)));

    connect(d->contactList, SIGNAL(destroying()), SLOT(destroyingContactList()));
    connect(d->contactList, SIGNAL(showOfflineChanged(bool)), SIGNAL(showOfflineChanged()));
    connect(d->contactList, SIGNAL(showHiddenChanged(bool)), SIGNAL(showHiddenChanged()));
    connect(d->contactList, SIGNAL(showSelfChanged(bool)), SIGNAL(showSelfChanged()));
    connect(d->contactList, SIGNAL(showAgentsChanged(bool)), SIGNAL(showTransportsChanged()));
    connect(d->contactList, SIGNAL(rosterRequestFinished()), SLOT(rosterRequestFinished()));
    connect(d->contactList, SIGNAL(contactSortStyleChanged(QString)), SIGNAL(contactSortStyleChanged()));
}

ContactListModel::~ContactListModel() { delete d; }

void ContactListModel::destroyingContactList()
{
    d->commit();

    // Save groups state
    d->collapsed.clear();
    QList<ContactListItem *> list = static_cast<ContactListItem *>(root())->allChildren();

    for (const auto *item : list) {
        if (!item->expanded())
            d->collapsed << item->internalName();

        if (item->isHidden())
            d->hidden << item->internalName();
    }

    PsiOptions::instance()->setOption(COLLAPSED_OPTIONS, d->collapsed);
    PsiOptions::instance()->setOption(HIDDEN_OPTIONS, d->hidden);

    d->clear();

    d->contactList = nullptr;
    invalidateLayout();
}

bool ContactListModel::groupsEnabled() const { return d->groupsEnabled; }

void ContactListModel::setGroupsEnabled(bool enabled)
{
    if (d->groupsEnabled != enabled) {
        d->groupsEnabled = enabled;
        invalidateLayout();
    }
}

bool ContactListModel::accountsEnabled() const { return d->accountsEnabled; }

void ContactListModel::setAccountsEnabled(bool enabled)
{
    if (d->accountsEnabled != enabled) {
        d->accountsEnabled = enabled;
        invalidateLayout();
    }
}

void ContactListModel::rosterRequestFinished()
{
    if (rowCount(QModelIndex()) == 0) {
        beginResetModel();
        endResetModel();

        emit layoutAboutToBeChanged();
        emit layoutChanged();
    }
}

void ContactListModel::invalidateLayout()
{
    d->clear();

    if (!d->contactList)
        return;

    emit layoutAboutToBeChanged();

    for (PsiContact *contact : d->contactList->contacts()) {
        Q_ASSERT(!d->monitoredContacts.contains(contact));
        if (d->monitoredContacts.contains(contact))
            continue;

        d->realAddContact(contact);
    }
    emit layoutChanged();
}

PsiContact *ContactListModel::contactFor(const QModelIndex &index) const
{
    ContactListItem *item = static_cast<ContactListItem *>(index.internalPointer());
    if (item->type() != ContactListItem::Type::ContactType)
        return nullptr;

    return item->contact();
}

QModelIndexList ContactListModel::indexesFor(const PsiContact *contact) const
{
    Q_ASSERT(contact);
    QModelIndexList result;
    const auto     &indexes = d->monitoredContacts.values(const_cast<PsiContact *>(contact));
    for (const auto &index : indexes) {
        result << index;
    }
    return result;
}

ContactListItem *ContactListModel::toItem(const QModelIndex &index) const
{
    Q_ASSERT(!index.isValid() || index.model() == this);

    ContactListItem *item = nullptr;
    bool             b    = index.isValid();
    if (!b) {
        item = static_cast<ContactListItem *>(root());
    } else {
        item = static_cast<ContactListItem *>(index.internalPointer());
    }

    return item;
}

QModelIndex ContactListModel::toModelIndex(ContactListItem *item) const
{
    if (!item || item == root())
        return QModelIndex();
    else
        return createIndex(item->row(), 0, item);
}

QVariant ContactListModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();

    ContactListItem *item = toItem(index);

    if (role == ContactListItemRole)
        return QVariant::fromValue(item);
    else
        return item->value(role);
}

bool ContactListModel::setData(const QModelIndex &index, const QVariant &data, int role)
{
    if (!index.isValid())
        return false;

    ContactListItem *item = toItem(index);
    if (!item)
        return false;
    PsiContact *contact = item->isContact() ? item->contact() : nullptr;

    if (role == ActivateRole) {
        if (!contact)
            return false;

        contact->activate();
        return true;
    } else if (role == Qt::EditRole) {
        QString name = data.toString();
        if (contact) {
            item->setName(name);
            emit dataChanged(index, index);
        } else if (item->isGroup() && !name.isEmpty()) {
            QString             oldName = item->name();
            QList<PsiContact *> contacts;
            for (int i = 0; i < item->childCount(); ++i) {
                if (item->child(i)->isContact())
                    contacts << item->child(i)->contact();
            }

            for (PsiContact *contact : std::as_const(contacts)) {
                QStringList groups = contact->groups();
                groups.removeOne(oldName);
                groups << name;
                contact->setGroups(groups);
            }
        }

        return true;
    } else if (role == ExpandedRole) {
        if (!item->isContact()) {
            item->setExpanded(data.toBool());
        }
    }

    return true;
}

/**
 * Returns the number of columns in the \param parent model item.
 */
int ContactListModel::columnCount(const QModelIndex &parent) const
{
    if (parent.column() > 0)
        return 0;

    return 1;
}

/**
 * Returns the item flags for the given \param index in the model.
 */
Qt::ItemFlags ContactListModel::flags(const QModelIndex &index) const
{
    if (!index.isValid())
        return Qt::ItemIsDropEnabled;

    Qt::ItemFlags f = QAbstractItemModel::flags(index);
    f |= Qt::ItemIsSelectable | Qt::ItemIsEnabled;

    ContactListItem *item = toItem(index);
    if (item) {
        if ((index.column() == NameColumn) && item->isEditable())
            f |= Qt::ItemIsEditable;

        if (!item->isExpandable())
            f |= Qt::ItemNeverHasChildren;
    }
    return f;
}

/**
 * Call this slot to notify \param index that it's now displayed in the 'expanded' state.
 */
void ContactListModel::expanded(const QModelIndex &index) { setData(index, QVariant(true), ExpandedRole); }

/**
 * Call this slot to notify \param index that it's now displayed in the 'collapsed' state.
 */
void ContactListModel::collapsed(const QModelIndex &index) { setData(index, QVariant(false), ExpandedRole); }

PsiContactList *ContactListModel::contactList() const { return d->contactList; }

void ContactListModel::renameSelectedItem() { emit inPlaceRename(); }

void ContactListModel::updateItem(ContactListItem *item, bool notifyModel)
{
    Q_ASSERT(item);

    QModelIndex index = toModelIndex(item);
    if (notifyModel)
        emit dataChanged(index, index);
}

bool ContactListModel::showOffline() const
{
    if (!d->contactList)
        return false;
    return d->contactList->showOffline();
}

bool ContactListModel::showSelf() const
{
    if (!d->contactList)
        return false;
    return d->contactList->showSelf();
}

bool ContactListModel::showTransports() const
{
    if (!d->contactList)
        return false;
    return d->contactList->showAgents();
}

bool ContactListModel::showHidden() const
{
    if (!d->contactList)
        return false;
    return d->contactList->showHidden();
}

QString ContactListModel::contactSortStyle() const
{
    if (!d->contactList)
        return QString("alpha");
    return d->contactList->contactSortStyle();
}
