/*
    kopeteaccount.cpp - Kopete Account

    Copyright (c) 2003-2005 by Olivier Goffart       <ogoffart@kde.org>
    Copyright (c) 2003-2004 by Martijn Klingens      <klingens@kde.org>
    Copyright (c) 2004      by Richard Smith         <kde@metafoo.co.uk>
    Copyright (c) 2007         Will Stephenson       <wstephenson@kde.org>

    Kopete    (c) 2002-2005 by the Kopete developers <kopete-devel@kde.org>

    *************************************************************************
    *                                                                       *
    * This library is free software; you can redistribute it and/or         *
    * modify it under the terms of the GNU Lesser General Public            *
    * License as published by the Free Software Foundation; either          *
    * version 2 of the License, or (at your option) any later version.      *
    *                                                                       *
    *************************************************************************
*/

#include "kopeteaccount.h"

#include <QWidget>
#include <QTimer>
#include <QPixmap>
#include <QIcon>
#include <QPointer>
#include <QDialog>
#include <QAction>
#include <QDialogButtonBox>
#include <QPushButton>
#include <QVBoxLayout>
#include <QMenu>

#include <kconfig.h>

#include <kiconeffect.h>
#include <kmessagebox.h>
#include <kmessagebox_queued.h>
#include <KSharedConfig>
#include <kactionmenu.h>
#include <kconfiggroup.h>
#include <KConfigGroup>

#include "kopeteidentity.h"
#include "kopeteidentitymanager.h"
#include "kabcpersistence.h"
#include "kopetecontactlist.h"
#include "kopeteaccountmanager.h"
#include "kopetecontact.h"
#include "kopetemetacontact.h"
#include "kopeteprotocol.h"
#include "kopetepluginmanager.h"
#include "kopetegroup.h"
#include "kopetebehaviorsettings.h"
#include "kopeteutils.h"
#include "kopeteuiglobal.h"
#include "kopeteblacklister.h"
#include "kopeteonlinestatusmanager.h"
#include "editaccountwidget.h"

namespace Kopete {
class Account::Private
{
public:
    Private(Protocol *protocol, const QString &accountId)
        : protocol(protocol)
        , id(accountId)
        , excludeconnect(true)
        , priority(0)
        , connectionTry(0)
        , identity(0)
        , myself(0)
        , suppressStatusTimer(0)
        , suppressStatusNotification(false)
        , blackList(new Kopete::BlackLister(protocol->pluginId(), accountId))
    {
    }

    ~Private()
    {
        delete blackList;
    }

    QPointer <Protocol> protocol;
    QString id;
    QString accountLabel;
    bool excludeconnect;
    uint priority;
    QHash<QString, Contact *> contacts;
    QColor color;
    uint connectionTry;
    QPointer <Identity> identity;
    QPointer <Contact> myself;
    QTimer suppressStatusTimer;
    QTimer reconnectTimer;
    bool reconnectOnNetworkIsOnline;
    bool suppressStatusNotification;
    Kopete::BlackLister *blackList;
    KConfigGroup *configGroup;
    QString customIcon;
    Kopete::OnlineStatus restoreStatus;
    Kopete::StatusMessage restoreMessage;
    QDateTime lastLoginTime;
    bool suspended;
    Kopete::OnlineStatus suspendStatus;
};

Account::Account(Protocol *parent, const QString &accountId)
    : QObject(parent)
    , d(new Private(parent, accountId))
{
    d->configGroup = new KConfigGroup(KSharedConfig::openConfig(), QStringLiteral("Account_%1_%2").arg(d->protocol->pluginId(), d->id));

    d->excludeconnect = d->configGroup->readEntry("ExcludeConnect", false);
    d->color = d->configGroup->readEntry("Color", QColor());
    d->customIcon = d->configGroup->readEntry("Icon", QString());
    d->priority = d->configGroup->readEntry("Priority", 0);

    d->restoreStatus = Kopete::OnlineStatus::Online;
    d->restoreMessage = Kopete::StatusMessage();
    d->reconnectOnNetworkIsOnline = false;

    d->reconnectTimer.setSingleShot(true);
    QObject::connect(&d->reconnectTimer, SIGNAL(timeout()),
                     this, SLOT(reconnect()));

    QObject::connect(Solid::Networking::notifier(), SIGNAL(statusChanged(Solid::Networking::Status)), this, SLOT(networkingStatusChanged(Solid::Networking::Status)));

    QObject::connect(&d->suppressStatusTimer, SIGNAL(timeout()),
                     this, SLOT(slotStopSuppression()));

    d->suspended = false;
    d->suspendStatus = Kopete::OnlineStatus::Offline;
}

Account::~Account()
{
    // Delete all registered child contacts first
    foreach (Contact *c, d->contacts) {
        QObject::disconnect(c, SIGNAL(contactDestroyed(Kopete::Contact*)), this, 0);
    }
    qDeleteAll(d->contacts);
    d->contacts.clear();

    qCDebug(LIBKOPETE_LOG) << " account '" << d->id << "' about to emit accountDestroyed ";
    emit accountDestroyed(this);

    delete d->myself;
    delete d->configGroup;
    delete d;
}

void Account::reconnect()
{
    if (isConnected()) {
        return; // Already connected
    }
    qCDebug(LIBKOPETE_LOG) << "account " << d->id << " restoreStatus " << d->restoreStatus.status()
                           << " restoreTitle " << d->restoreMessage.title()
                           << " restoreMessage " << d->restoreMessage.message();
    setOnlineStatus(d->restoreStatus, d->restoreMessage);
}

void Account::networkingStatusChanged(const Solid::Networking::Status status)
{
    switch (status) {
    case Solid::Networking::Connected:
        if (d->reconnectOnNetworkIsOnline && !excludeConnect()) {
            reconnect();
        }
        break;

    case Solid::Networking::Unconnected:
    case Solid::Networking::Disconnecting:
        setOnlineStatus(OnlineStatus::Offline);
        if (Kopete::BehaviorSettings::self()->reconnectOnDisconnect()) {
            d->reconnectOnNetworkIsOnline = true;
        }
        break;

    case Solid::Networking::Unknown:
    case Solid::Networking::Connecting:
        break;
    }
}

void Account::disconnected(DisconnectReason reason)
{
    qCDebug(LIBKOPETE_LOG) << reason;
    //reconnect if needed
    if (reason == BadPassword) {
        d->reconnectTimer.start(0);
    } else if (Kopete::BehaviorSettings::self()->reconnectOnDisconnect() == true && reason > Manual) {
        bool networkAvailable = (Solid::Networking::status() == Solid::Networking::Unknown || Solid::Networking::status() == Solid::Networking::Connected);
        if (reason == ConnectionReset && !networkAvailable) {
            d->reconnectOnNetworkIsOnline = true;
            d->reconnectTimer.stop();
        } else {
            if (d->reconnectTimer.isActive()) {
                return; // In case protocol calls disconnected more than one time on disconnect.
            }
            d->connectionTry++;
            //use a timer to allow the plugins to clean up after return
            if (d->connectionTry < 3) {
                d->reconnectTimer.start(10000);   // wait 10 seconds before reconnect
            } else if (d->connectionTry <= 10) {
                d->reconnectTimer.start(((2 * (d->connectionTry - 2)) - 1) * 60000);   // wait 1,3,5...15 minutes => stops after 64 min
            }
        }
    } else {
        d->reconnectOnNetworkIsOnline = false;
        d->reconnectTimer.stop();
    }

    if (reason == OtherClient) {
        Kopete::Utils::notifyConnectionLost(this, i18n("You have been disconnected"), i18n("You have connected from another client or computer to the account '%1'", d->id),
                                            i18n(
                                                "Most proprietary Instant Messaging services do not allow you to connect from more than one location. Check that nobody is using your account without your permission. If you need a service that supports connection from various locations at the same time, use the Jabber protocol."));
    }
}

Protocol *Account::protocol() const
{
    return d->protocol;
}

QString Account::accountId() const
{
    return d->id;
}

const QColor Account::color() const
{
    return d->color;
}

void Account::setColor(const QColor &color)
{
    d->color = color;
    if (d->color.isValid()) {
        d->configGroup->writeEntry("Color", d->color);
    } else {
        d->configGroup->deleteEntry("Color");
    }
    emit colorChanged(color);
}

void Account::setPriority(uint priority)
{
    d->priority = priority;
    d->configGroup->writeEntry("Priority", d->priority);
}

uint Account::priority() const
{
    return d->priority;
}

QPixmap Account::accountIcon(const int size) const
{
    QString icon = d->customIcon.isEmpty() ? d->protocol->pluginIcon() : d->customIcon;

    // FIXME: this code is duplicated with OnlineStatus, can we merge it somehow?
    QPixmap base = KIconLoader::global()->loadIcon(
        icon, KIconLoader::Small, size);

    if (d->color.isValid()) {
        KIconEffect effect;
        base = effect.apply(base, KIconEffect::Colorize, 1, d->color, 0);
    }

    if (size > 0 && base.width() != size) {
        base.scaled(size, size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
    }

    return base;
}

QString Account::accountIconPath(const KIconLoader::Group size) const
{
    return KIconLoader::global()->iconPath(d->customIcon.isEmpty()
                                           ? d->protocol->pluginIcon()
                                           : d->customIcon, size);
}

KConfigGroup *Kopete::Account::configGroup() const
{
    return d->configGroup;
}

void Account::setAccountLabel(const QString &label)
{
    d->accountLabel = label;
}

QString Account::accountLabel() const
{
    if (d->accountLabel.isNull()) {
        return d->id;
    }
    return d->accountLabel;
}

void Account::setExcludeConnect(bool b)
{
    d->excludeconnect = b;
    d->configGroup->writeEntry("ExcludeConnect", d->excludeconnect);
}

bool Account::excludeConnect() const
{
    return d->excludeconnect;
}

bool Account::registerContact(Contact *c)
{
    Q_ASSERT(c->metaContact() != Kopete::ContactList::self()->myself());

    if (d->contacts.value(c->contactId())) {
        qCWarning(LIBKOPETE_LOG) << "Contact already exists!!! accountId: " << c->account() << " contactId: " << c->contactId();
        return false;
    }

    d->contacts.insert(c->contactId(), c);
    QObject::connect(c, SIGNAL(contactDestroyed(Kopete::Contact*)),
                     SLOT(contactDestroyed(Kopete::Contact*)));

    return true;
}

void Account::contactDestroyed(Contact *c)
{
    d->contacts.remove(c->contactId());
}

const QHash<QString, Contact *> &Account::contacts()
{
    return d->contacts;
}

Kopete::MetaContact *Account::addContact(const QString &contactId, const QString &displayName, Group *group, AddMode mode)
{
    if (!protocol()->canAddMyself() && contactId == d->myself->contactId()) {
        if (isConnected() && d->lastLoginTime.secsTo(QDateTime::currentDateTime()) > 30) {
            KMessageBox::queuedMessageBox(Kopete::UI::Global::mainWidget(), KMessageBox::Error,
                                          i18n("You are not allowed to add yourself to the contact list. The addition of \"%1\" to account \"%2\" will not take place.", contactId, accountId()),
                                          i18n("Error Creating Contact")
                                          );
        } else {
            qCWarning(LIBKOPETE_LOG) << "You are not allowed to add yourself to the contact list. The addition of" << contactId
                                     << "to account" << accountId() << "will not take place.";
        }
        return 0;
    }

    bool isTemporary = (mode == Temporary);

    Contact *c = d->contacts.value(contactId);

    if (!group) {
        group = Group::topLevel();
    }

    if (c && c->metaContact()) {
        if (c->metaContact()->isTemporary() && !isTemporary) {
            qCDebug(LIBKOPETE_LOG) <<  " You are trying to add an existing temporary contact. Just add it on the list";

            c->metaContact()->setTemporary(false, group);
            ContactList::self()->addMetaContact(c->metaContact());
        } else {
            // should we here add the contact to the parentContact if any?
            qCDebug(LIBKOPETE_LOG) << "Contact already exists";
        }
        return c->metaContact();
    }

    MetaContact *parentContact = new MetaContact();
    if (!displayName.isEmpty()) {
        parentContact->setDisplayName(displayName);
    }

    //Set it as a temporary contact if requested
    if (isTemporary) {
        parentContact->setTemporary(true);
    } else {
        parentContact->addToGroup(group);
    }

    if (c) {
        c->setMetaContact(parentContact);
        if (mode == ChangeKABC) {
            qCDebug(LIBKOPETE_LOG) << " changing KABC";
            KABCPersistence::self()->write(parentContact);
        }
    } else {
        if (!createContact(contactId, parentContact)) {
            delete parentContact;
            return nullptr;
        }
    }

    ContactList::self()->addMetaContact(parentContact);
    return parentContact;
}

bool Account::addContact(const QString &contactId, MetaContact *parent, AddMode mode)
{
    if (!protocol()->canAddMyself() && contactId == myself()->contactId()) {
        if (isConnected() && d->lastLoginTime.secsTo(QDateTime::currentDateTime()) > 30) {
            KMessageBox::queuedMessageBox(Kopete::UI::Global::mainWidget(), KMessageBox::Error,
                                          i18n("You are not allowed to add yourself to the contact list. The addition of \"%1\" to account \"%2\" will not take place.", contactId, accountId()),
                                          i18n("Error Creating Contact")
                                          );
        } else {
            qCWarning(LIBKOPETE_LOG) << "You are not allowed to add yourself to the contact list. The addition of" << contactId
                                     << "to account" << accountId() << "will not take place.";
        }

        return false;
    }

    const bool isTemporary = parent->isTemporary();
    Contact *c = d->contacts.value(contactId);
    if (c && c->metaContact()) {
        if (c->metaContact()->isTemporary() && !isTemporary) {
            qCDebug(LIBKOPETE_LOG)
                <<"Account::addContact: You are trying to add an existing temporary contact. Just add it on the list" << endl;

            //setMetaContact ill take care about the deletion of the old contact
            c->setMetaContact(parent);
            return true;
        } else {
            // should we here add the contact to the parentContact if any?
            qCDebug(LIBKOPETE_LOG) << "Account::addContact: Contact already exists";
        }
        return false; //(the contact is not in the correct metacontact, so false)
    }

    const bool success = createContact(contactId, parent);

    if (success && mode == ChangeKABC) {
        qCDebug(LIBKOPETE_LOG) << " changing KABC";
        KABCPersistence::self()->write(parent);
    }

    return success;
}

void Account::fillActionMenu(KActionMenu *actionMenu)
{
    //default implementation
    KActionMenu *menu = new KActionMenu( QIcon(myself()->onlineStatus().iconFor( this )), accountId(), this);
#ifdef __GNUC__
#warning No icon shown, we should go away from QPixmap genered icons with overlays.
#endif
    QString nick;
    if (identity()->hasProperty(Kopete::Global::Properties::self()->nickName().key())) {
        nick = identity()->property(Kopete::Global::Properties::self()->nickName()).value().toString();
    } else {
        nick = myself()->displayName();
    }

    // Always add title at the beginning of actionMenu
    QAction *before = actionMenu->menu()->actions().value(0, 0);
    actionMenu->menu()->setTitle(nick.isNull() ? accountLabel() : i18n("%2 <%1>", accountLabel(), nick));
    actionMenu->menu()->setIcon(myself()->onlineStatus().iconFor(myself()));
    actionMenu->menu()->setDefaultAction(before);
    actionMenu->menu()->addSeparator();

    QAction *propertiesAction = new QAction(i18n("Properties"), actionMenu);
    QObject::connect(propertiesAction, SIGNAL(triggered(bool)), this, SLOT(editAccount()));
    actionMenu->addAction(propertiesAction);
}

bool Account::hasCustomStatusMenu() const
{
    return false;
}

bool Account::isConnected() const
{
    return myself() && myself()->isOnline();
}

bool Account::isAway() const
{
    //We might want to change the method name here.
    return d->myself && (d->myself->onlineStatus().status() == Kopete::OnlineStatus::Away
                         || d->myself->onlineStatus().status() == Kopete::OnlineStatus::Busy);
}

bool Account::isBusy() const
{
    return d->myself && (d->myself->onlineStatus().status() == Kopete::OnlineStatus::Busy);
}

Identity *Account::identity() const
{
    return d->identity;
}

bool Account::setIdentity(Identity *ident)
{
    if (d->identity == ident) {
        return false;
    }

    if (d->identity) {
        d->identity->removeAccount(this);
    }

    ident->addAccount(this);
    d->identity = ident;
    d->configGroup->writeEntry("Identity", ident->id());
    return true;
}

Contact *Account::myself() const
{
    return d->myself;
}

void Account::setMyself(Contact *myself)
{
    Q_ASSERT(!d->myself);

    d->myself = myself;

    QObject::connect(d->myself, SIGNAL(onlineStatusChanged(Kopete::Contact*,Kopete::OnlineStatus,Kopete::OnlineStatus)),
                     this, SLOT(slotOnlineStatusChanged(Kopete::Contact*,Kopete::OnlineStatus,Kopete::OnlineStatus)));
    QObject::connect(d->myself, SIGNAL(propertyChanged(Kopete::PropertyContainer*,QString,QVariant,QVariant)),
                     this, SLOT(slotContactPropertyChanged(Kopete::PropertyContainer*,QString,QVariant,QVariant)));

    if (isConnected()) {
        emit isConnectedChanged();
    }
}

void Account::slotOnlineStatusChanged(Contact * /* contact */, const OnlineStatus &newStatus, const OnlineStatus &oldStatus)
{
    const bool wasOffline = !oldStatus.isDefinitelyOnline();
    const bool isOffline = !newStatus.isDefinitelyOnline();
    d->suspended = false;

    if (wasOffline && !isOffline) {
        d->lastLoginTime = QDateTime::currentDateTime();
    }

    // If we went offline we have to ensure that all of our contacts
    // are online too. Otherwise the "Last Seen" tooltip won't work
    // properly. See bug 266580.
    if (!wasOffline && isOffline) {
        setAllContactsStatus(Kopete::OnlineStatus::Offline);
    }

    if (wasOffline || newStatus.status() == OnlineStatus::Offline) {
        // Wait for twenty seconds until we treat status notifications for contacts
        // as unrelated to our own status change.
        // Twenty seconds may seem like a long time, but just after your own
        // connection it's basically neglectible, and depending on your own
        // contact list's size, the protocol you are using, your internet
        // connection's speed and your computer's speed you *will* need it.
        d->suppressStatusNotification = true;
        d->suppressStatusTimer.setSingleShot(true);
        d->suppressStatusTimer.start(20000);
        //the timer is also used to reset the d->connectionTry
    }

    if (!isOffline) {
        d->restoreStatus = newStatus;
        d->restoreMessage.setTitle(myself()->property(Kopete::Global::Properties::self()->statusTitle()).value().toString());
        d->restoreMessage.setMessage(myself()->property(Kopete::Global::Properties::self()->statusMessage()).value().toString());
    }

/*	qCDebug(LIBKOPETE_LOG) << "account " << d->id << " changed status. was "
                   << Kopete::OnlineStatus::statusTypeToString(oldStatus.status()) << ", is "
                   << Kopete::OnlineStatus::statusTypeToString(newStatus.status()) << endl;*/
    if (wasOffline != isOffline) {
        emit isConnectedChanged();
    }
}

bool Account::suspend(const Kopete::StatusMessage &reason)
{
    if (d->suspended) {
        return false;
    }

    d->suspendStatus = myself()->onlineStatus();
    if (myself()->onlineStatus().status() == OnlineStatus::Connecting) {
        d->suspendStatus = d->restoreStatus;
    }
    setOnlineStatus(OnlineStatus::Offline, reason);
    d->suspended = true;
    return true;
}

bool Account::resume()
{
    if (!d->suspended) {
        return false;
    }

    if (d->suspendStatus != Kopete::OnlineStatus::Offline) {
        setOnlineStatus(d->suspendStatus, d->restoreMessage, Kopete::Account::None);
    }

    return true;
}

void Account::setAllContactsStatus(const Kopete::OnlineStatus &status)
{
    d->suppressStatusNotification = true;
    d->suppressStatusTimer.setSingleShot(true);
    d->suppressStatusTimer.start(20000);

    QHashIterator<QString, Contact *> it(d->contacts);
    for (; it.hasNext();) {
        it.next();

        Contact *c = it.value();
        if (c) {
            c->setOnlineStatus(status);
        }
    }
}

void Account::slotContactPropertyChanged(PropertyContainer * /* contact */, const QString &key, const QVariant &old, const QVariant &newVal)
{
    if (key == Kopete::Global::Properties::self()->statusTitle().key() && old != newVal && isConnected()) {
        d->restoreMessage.setTitle(newVal.toString());
    } else if (key == Kopete::Global::Properties::self()->statusMessage().key() && old != newVal && isConnected()) {
        d->restoreMessage.setMessage(newVal.toString());
    }
}

void Account::slotStopSuppression()
{
    d->suppressStatusNotification = false;
    if (isConnected()) {
        d->connectionTry = 0;
    }
}

bool Account::suppressStatusNotification() const
{
    return d->suppressStatusNotification;
}

bool Account::removeAccount()
{
    //default implementation
    return true;
}

BlackLister *Account::blackLister()
{
    return d->blackList;
}

void Account::block(const QString &contactId)
{
    d->blackList->addContact(contactId);
}

void Account::unblock(const QString &contactId)
{
    d->blackList->removeContact(contactId);
}

bool Account::isBlocked(const QString &contactId)
{
    return d->blackList->isBlocked(contactId);
}

void Account::editAccount(QWidget *parent)
{
    QPointer<QDialog> editDialog = new QDialog(parent);
    editDialog->setWindowTitle(i18n("Edit Account"));
    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel);
    QVBoxLayout *mainLayout = new QVBoxLayout;
    editDialog->setLayout(mainLayout);

    KopeteEditAccountWidget *m_accountWidget = protocol()->createEditAccountWidget(this, editDialog);
    // FIXME: Why the #### is EditAccountWidget not a QWidget?!? This sideways casting
    //        is braindead and error-prone. Looking at MSN the only reason I can see is
    //        because it allows direct subclassing of designer widgets. But what is
    //        wrong with embedding the designer widget in an empty QWidget instead?
    //        Also, if this REALLY has to be a pure class and not a widget, then the
    //        class should at least be renamed to EditAccountIface instead - Martijn
    QWidget *w = dynamic_cast<QWidget *>(m_accountWidget);
    if (!w) {
        delete editDialog;
        return;
    }
    mainLayout->addWidget(w);
    QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
    okButton->setDefault(true);
    okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
    QObject::connect(buttonBox, SIGNAL(accepted()), editDialog, SLOT(accept()));
    QObject::connect(buttonBox, SIGNAL(rejected()), editDialog, SLOT(reject()));
    mainLayout->addWidget(buttonBox);

    if (editDialog->exec() == QDialog::Accepted) {
        if (m_accountWidget->validateData()) {
            m_accountWidget->apply();
        }
    }

    delete editDialog;
}

void Account::setCustomIcon(const QString &i)
{
    d->customIcon = i;
    if (!i.isEmpty()) {
        d->configGroup->writeEntry("Icon", i);
    } else {
        d->configGroup->deleteEntry("Icon");
    }
    emit colorChanged(color());
}

QString Account::customIcon()  const
{
    return d->customIcon;
}
} // END namespace Kopete
