/*
   webpresenceplugin.cpp

   Kopete Web Presence plugin

   Copyright     2005-2006 by Tommi Rantala   <tommi.rantala@cs.helsinki.fi>
   Copyright (c) 2002,2003 by Will Stephenson <will@stevello.free-online.co.uk>

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

 *************************************************************************
 *                                                                       *
 * 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.                                   *
 *                                                                       *
 *************************************************************************
 */

#include "webpresenceplugin.h"

#include <QTimer>
#include <QFile>
#include <QTextCodec>
#include <QTextStream>
#include <QList>
#include <QDateTime>
#include <QDomDocument>
#include <KLocalizedString>

#include <kdebug.h>
#include <kpluginfactory.h>
#include <kio/job.h>
#include <kmessagebox.h>
#include <QTemporaryFile>

#include <libxml/parser.h>
#include <libxml/tree.h>

#include <libxslt/xsltconfig.h>
#include <libxslt/xsltInternals.h>
#include <libxslt/transform.h>
#include <libxslt/xsltutils.h>
#include <QStandardPaths>

#include "kopetepluginmanager.h"
#include "kopeteprotocol.h"
#include "kopeteaccountmanager.h"
#include "kopeteaccount.h"
#include "kopetecontact.h"

#include "webpresenceconfig.h"

K_PLUGIN_FACTORY(WebPresencePluginFactory, registerPlugin<WebPresencePlugin>();
                 )
K_EXPORT_PLUGIN(WebPresencePluginFactory("kopete_webpresence"))

WebPresencePlugin::WebPresencePlugin(QObject *parent, const QVariantList & /*args*/)
    : Kopete::Plugin(parent)
    , shuttingDown(false)
    , resultFormatting(WEB_HTML)
    , m_output(0)
{
    m_writeScheduler = new QTimer(this);
    connect(m_writeScheduler, &QTimer::timeout, this, &WebPresencePlugin::slotWriteFile);
    connect(Kopete::AccountManager::self(), &Kopete::AccountManager::accountRegistered, this, &WebPresencePlugin::listenToAllAccounts);
    connect(Kopete::AccountManager::self(), &Kopete::AccountManager::accountUnregistered, this, &WebPresencePlugin::listenToAllAccounts);

    connect(this, &WebPresencePlugin::settingsChanged, this, &WebPresencePlugin::slotSettingsChanged);
    slotSettingsChanged();
    listenToAllAccounts();
}

WebPresencePlugin::~WebPresencePlugin()
{
    delete m_output;
}

void WebPresencePlugin::slotSettingsChanged()
{
    // Force reading config
    WebPresenceConfig::self()->load();

    resultFormatting = WEB_UNDEFINED;

    if (WebPresenceConfig::self()->formatHTML()) {
        resultFormatting = WEB_HTML;
    } else if (WebPresenceConfig::self()->formatXHTML()) {
        resultFormatting = WEB_XHTML;
    } else if (WebPresenceConfig::self()->formatXML()) {
        resultFormatting = WEB_XML;
    } else if (WebPresenceConfig::self()->formatStylesheet()) {
        resultFormatting = WEB_CUSTOM;
        userStyleSheet = WebPresenceConfig::self()->formatStylesheetURL();
    }

    // Default to HTML, if we don't get anything useful from config file.
    if (resultFormatting == WEB_UNDEFINED) {
        resultFormatting = WEB_HTML;
    }

    // Update file
    slotWriteFile();
}

void WebPresencePlugin::listenToAllAccounts()
{
    // Connect to signals notifying of all accounts' status changes.
    ProtocolList protocols = allProtocols();

    for (ProtocolList::Iterator it = protocols.begin();
         it != protocols.end(); ++it) {
        QList<Kopete::Account *> accounts = Kopete::AccountManager::self()->accounts(*it);
        foreach (Kopete::Account *account, accounts) {
            listenToAccount(account);
        }
    }
    slotWaitMoreStatusChanges();
}

void WebPresencePlugin::listenToAccount(Kopete::Account *account)
{
    if (account && account->myself()) {
        // Connect to the account's status changed signal
        // because we can't know if the account has already connected
        QObject::disconnect(account->myself(), &Kopete::Contact::onlineStatusChanged, this, &WebPresencePlugin::slotRecieveStatusUpdate);
        QObject::connect(account->myself(), &Kopete::Contact::onlineStatusChanged, this, &WebPresencePlugin::slotRecieveStatusUpdate);
    }
}

void WebPresencePlugin::slotRecieveStatusUpdate(Kopete::Contact * /* args */, const Kopete::OnlineStatus& /* args */, const Kopete::OnlineStatus& /* args */)
{
    slotWaitMoreStatusChanges();
}

void WebPresencePlugin::slotWaitMoreStatusChanges()
{
    if (!m_writeScheduler->isActive()) {
        m_writeScheduler->start(WebPresenceConfig::self()->uploadFrequency() * 1000);
    }
}

void WebPresencePlugin::slotWriteFile()
{
    m_writeScheduler->stop();

    // generate the (temporary) XML file representing the current contact list
    const QUrl dest = QUrl::fromLocalFile(WebPresenceConfig::self()->uploadURL().path());
    if (dest.isEmpty() || !dest.isValid()) {
        kDebug(14309) << "url is empty or not valid. NOT UPDATING!";
        return;
    }

    QTemporaryFile *xml = generateFile();
    xml->setAutoRemove(true);

    switch (resultFormatting) {
    case WEB_XML:
        m_output = xml;
        xml = nullptr;
        break;
    case WEB_HTML:
    case WEB_XHTML:
    case WEB_CUSTOM:
        m_output = new QTemporaryFile();
        m_output->open();

        if (!transform(xml, m_output)) {
            //TODO: give some error to user, even better if shown only once
            delete m_output;
            m_output = nullptr;

            delete xml;
            return;
        }

        delete xml; // might make debugging harder!
        break;
    default:
        return;
    }

    // upload it to the specified URL
    QUrl src = QUrl::fromLocalFile(m_output->fileName());
    KIO::FileCopyJob *job = KIO::file_move(src, dest, -1, KIO::Overwrite | KIO::HideProgressInfo);
    connect(job, &KIO::FileCopyJob::result, this, &WebPresencePlugin::slotUploadJobResult);
}

void WebPresencePlugin::slotUploadJobResult(KJob *job)
{
    if (job->error()) {
        kDebug(14309) << "Error uploading presence info.";
        KMessageBox::detailedError(0, i18n("An error occurred when uploading your presence page.\nCheck the path and write permissions of the destination."), 0, displayName());
        delete m_output;
        m_output = nullptr;
    }
}

QTemporaryFile *WebPresencePlugin::generateFile()
{
    // generate the (temporary) XML file representing the current contact list
    kDebug(14309);
    QString notKnown = i18n("Not yet known");

    QDomDocument doc;

    doc.appendChild(doc.createProcessingInstruction(QStringLiteral("xml"),
                                                    QStringLiteral("version=\"1.0\" encoding=\"UTF-8\"")));

    QDomElement root = doc.createElement(QStringLiteral("webpresence"));
    doc.appendChild(root);

    // insert the current date/time
    QDomElement date = doc.createElement(QStringLiteral("listdate"));
    QDomText t = doc.createTextNode(QLocale().toString(QDateTime::currentDateTime()));
    date.appendChild(t);
    root.appendChild(date);

    // insert the user's name
    QDomElement name = doc.createElement(QStringLiteral("name"));
    QDomText nameText;
    const QString userName = WebPresenceConfig::self()->showThisName();
    if (!WebPresenceConfig::self()->showName() && !userName.isEmpty()) {
        nameText = doc.createTextNode(userName);
    } else {
        nameText = doc.createTextNode(notKnown);
    }
    name.appendChild(nameText);
    root.appendChild(name);

    // insert the list of the user's accounts
    QDomElement accounts = doc.createElement(QStringLiteral("accounts"));
    root.appendChild(accounts);

    QList<Kopete::Account *> list = Kopete::AccountManager::self()->accounts();
    // If no accounts, stop here
    if (!list.isEmpty()) {
        foreach (Kopete::Account *account, list) {
            QDomElement acc = doc.createElement(QStringLiteral("account"));
            //output += h.openTag( "account" );

            QDomElement protoName = doc.createElement(QStringLiteral("protocol"));
            QDomText protoNameText = doc.createTextNode(
                account->protocol()->pluginId());
            protoName.appendChild(protoNameText);
            acc.appendChild(protoName);

            Kopete::Contact *me = account->myself();
            QString displayName = me->displayName();
            QDomElement accName = doc.createElement(QStringLiteral("accountname"));
            QDomText accNameText = doc.createTextNode((me)
                                                      ? displayName
                                                      : notKnown);
            accName.appendChild(accNameText);
            acc.appendChild(accName);

            QDomElement accStatus = doc.createElement(QStringLiteral("accountstatus"));
            QDomText statusText = doc.createTextNode((me)
                                                     ? statusAsString(me->onlineStatus())
                                                     : notKnown);
            accStatus.appendChild(statusText);

            // Do not add these if we're shutting down, because the result
            // would be quite weird.
            if (!shuttingDown) {
                // Add away message as an attribute, if one exists.
                if ((me->onlineStatus().status() == Kopete::OnlineStatus::Away
                     || me->onlineStatus().status() == Kopete::OnlineStatus::Busy)
                    && !me->property(QStringLiteral("awayMessage")).value().toString().isEmpty()) {
                    accStatus.setAttribute(QStringLiteral("awayreason"),
                                           me->property(QStringLiteral("awayMessage")).value().toString());
                }

                // Add the online status description as an attribute, if one exits.
                if (!me->onlineStatus().description().isEmpty()) {
                    accStatus.setAttribute(QStringLiteral("statusdescription"),
                                           me->onlineStatus().description());
                }
            }
            acc.appendChild(accStatus);

            if (WebPresenceConfig::self()->includeIMAddress()) {
                QDomElement accAddress = doc.createElement(QStringLiteral("accountaddress"));
                QDomText addressText = doc.createTextNode((me)
                                                          ? me->contactId()
                                                          : notKnown);
                accAddress.appendChild(addressText);
                acc.appendChild(accAddress);
            }

            accounts.appendChild(acc);
        }
    }

    // write the XML to a temporary file
    QTemporaryFile *file = new QTemporaryFile();
    file->setAutoRemove(false);
    file->open();

    QString buf;
    QTextStream stream(&buf, QIODevice::WriteOnly);
    stream.setCodec("UTF-16");   // QtXML works only with UTF-16
    doc.documentElement().save(stream, 4);   // QDomDocument::save() override stream codec to UTF-8
    file->write(buf.toUtf8());
    file->flush();
    return file;
}

bool WebPresencePlugin::transform(QTemporaryFile *src, QTemporaryFile *dest)
{
    bool retval = true;
    xmlSubstituteEntitiesDefault(1);
    xmlLoadExtDtdDefaultValue = 1;

    QFile sheet;

    switch (resultFormatting) {
    case WEB_XML:
        // Oops! We tried to call transform() but XML was requested.
        return false;
    case WEB_HTML:
        if (WebPresenceConfig::self()->useImagesHTML()) {
            sheet.setFileName(QStandardPaths::locate(QStandardPaths::DataLocation, QStringLiteral("/webpresence/webpresence_html_images.xsl")));
        } else {
            sheet.setFileName(QStandardPaths::locate(QStandardPaths::DataLocation, QStringLiteral("/webpresence/webpresence_html.xsl")));
        }
        break;
    case WEB_XHTML:
        if (WebPresenceConfig::self()->useImagesHTML()) {
            sheet.setFileName(QStandardPaths::locate(QStandardPaths::DataLocation, QStringLiteral("/webpresence/webpresence_xhtml_images.xsl")));
        } else {
            sheet.setFileName(QStandardPaths::locate(QStandardPaths::DataLocation, QStringLiteral("/webpresence/webpresence_xhtml.xsl")));
        }
        break;
    case WEB_CUSTOM:
        sheet.setFileName(userStyleSheet.path());
        break;
    default:
        // Shouldn't ever reach here.
        return false;
    }

    // TODO: auto / smart pointers would be useful here
    xsltStylesheetPtr cur = 0;
    xmlDocPtr doc = 0;
    xmlDocPtr res = 0;

    if (!sheet.exists()) {
        kDebug(14309) << "ERROR: Style sheet not found";
        retval = false;
        goto end;
    }

    // is the cast safe?
    cur = xsltParseStylesheetFile((const xmlChar *)sheet.fileName().toLatin1().data());
    if (!cur) {
        kDebug(14309) << "ERROR: Style sheet parsing failed";
        retval = false;
        goto end;
    }

    doc = xmlParseFile(QFile::encodeName(src->fileName()));
    if (!doc) {
        kDebug(14309) << "ERROR: XML parsing failed";
        retval = false;
        goto end;
    }

    res = xsltApplyStylesheet(cur, doc, 0);
    if (!res) {
        kDebug(14309) << "ERROR: Style sheet apply failed";
        retval = false;
        goto end;
    }

    if (xsltSaveResultToFd(dest->handle(), res, cur) == -1) {
        kDebug(14309) << "ERROR: Style sheet apply failed";
        retval = false;
        goto end;
    }

    // then it all worked!

end:
    xsltCleanupGlobals();
    xmlCleanupParser();
    if (doc) {
        xmlFreeDoc(doc);
    }
    if (res) {
        xmlFreeDoc(res);
    }
    if (cur) {
        xsltFreeStylesheet(cur);
    }

    return retval;
}

ProtocolList WebPresencePlugin::allProtocols()
{
    kDebug(14309);

    Kopete::PluginList plugins = Kopete::PluginManager::self()->loadedPlugins(QStringLiteral("Protocols"));
    Kopete::PluginList::ConstIterator it;

    ProtocolList result;

    for (it = plugins.constBegin(); it != plugins.constEnd(); ++it) {
        result.append(static_cast<Kopete::Protocol *>(*it));
    }

    return result;
}

QString WebPresencePlugin::statusAsString(const Kopete::OnlineStatus &newStatus)
{
    if (shuttingDown) {
        return QStringLiteral("OFFLINE");
    }

    QString status;
    switch (newStatus.status()) {
    case Kopete::OnlineStatus::Online:
        status = QStringLiteral("ONLINE");
        break;
    case Kopete::OnlineStatus::Away:
        status = QStringLiteral("AWAY");
        break;
    case Kopete::OnlineStatus::Busy:
        status = QStringLiteral("BUSY");
        break;
    case Kopete::OnlineStatus::Offline:
    case Kopete::OnlineStatus::Invisible:
        status = QStringLiteral("OFFLINE");
        break;
    default:
        status = QStringLiteral("UNKNOWN");
    }

    return status;
}

void WebPresencePlugin::aboutToUnload()
{
    // Stop timer. Do not need it anymore.
    m_writeScheduler->stop();

    // Force statusAsString() report all accounts as OFFLINE.
    shuttingDown = true;

    // Do final update of webpresence file.
    slotWriteFile();

    emit readyForUnload();
}

// vim: set noet ts=4 sts=4 sw=4:
#include "webpresenceplugin.moc"
