/*
 * captchaformsplugin.cpp - plugin
 * Copyright (C) 2010  Evgeny Khryukin
 *
 * 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 library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 */

#include <QDomElement>

#include "captchadialog.h"
#include "loader.h"
#include "ui_options.h"

#include "psiplugin.h"
#include "optionaccessor.h"
#include "optionaccessinghost.h"
#include "plugininfoprovider.h"
#include "eventcreatinghost.h"
#include "eventcreator.h"
#include "stanzafilter.h"
#include "stanzasender.h"
#include "stanzasendinghost.h"
#include "accountinfoaccessinghost.h"
#include "accountinfoaccessor.h"
#include "applicationinfoaccessinghost.h"
#include "applicationinfoaccessor.h"


#define constVersion "0.1.1"
#define constProxyHost "host"
#define constProxyPort "port"
#define constProxyUser "user"
#define constProxyPass "pass"
#define constAutopopup "autopopup"
#define constUseProxy "useproxy"

static const QStringList methods = QStringList() << "qa" << "ocr" << "picture_q" << "picture_recog";

class CaptchaFormsPlugin : public QObject, public PsiPlugin, public OptionAccessor, public PluginInfoProvider, public EventCreator,
                           public StanzaFilter, public StanzaSender, public AccountInfoAccessor, public ApplicationInfoAccessor
{
    Q_OBJECT
#ifdef HAVE_QT5
    Q_PLUGIN_METADATA(IID "com.psi-plus.CaptchaFormsPlugin")
#endif
    Q_INTERFACES(PsiPlugin OptionAccessor PluginInfoProvider EventCreator StanzaFilter StanzaSender AccountInfoAccessor ApplicationInfoAccessor)
    public:
    CaptchaFormsPlugin();
    virtual QString name() const;
    virtual QString shortName() const;
    virtual QString version() const;
    virtual QWidget* options();
    virtual bool enable();
    virtual bool disable();
    virtual void optionChanged(const QString&){};
    virtual void applyOptions();
    virtual void restoreOptions();
    virtual void setOptionAccessingHost(OptionAccessingHost* host);
    virtual void setEventCreatingHost(EventCreatingHost* host);
    virtual bool incomingStanza(int account, const QDomElement& xml);
    virtual bool outgoingStanza(int account, QDomElement& xml);
    virtual void setStanzaSendingHost(StanzaSendingHost *host);
    virtual void setAccountInfoAccessingHost(AccountInfoAccessingHost* host);
    virtual void setApplicationInfoAccessingHost(ApplicationInfoAccessingHost* host);
    virtual QString pluginInfo();
    virtual QPixmap icon() const;

private:
    OptionAccessingHost *psiOptions;
    EventCreatingHost *psiEvent;
    StanzaSendingHost *stanzaSender;
    AccountInfoAccessingHost* accInfo;
    ApplicationInfoAccessingHost* appInfo;
    bool enabled;
    int id;
    bool autopopup, useProxy;
    QList< QHash<QString, QString> > challenges_;
    QHash< QString, QPointer<CaptchaDialog> > dialogs_;
    Ui::Options ui_;


    bool isValidChallenge(const QDomElement& xml, QHash<QString, QString>& dataFields) const;
    int findChalleng(const QString& field, const QString& value);

private slots:
    void eventActivated(const QString&);
    void submitChallenge(const QString&, const QString&);
    void cancelChallenge(const QString&);
    void loaderData(const QString& id, const QByteArray& data);
    void loaderError(const QString& id);
};

#ifndef HAVE_QT5
Q_EXPORT_PLUGIN(CaptchaFormsPlugin);
#endif

CaptchaFormsPlugin::CaptchaFormsPlugin()
    : psiOptions(0)
    , psiEvent(0)
    , stanzaSender(0)
    , accInfo(0)
    , appInfo(0)
    , enabled(false)
    , id(111)
    , autopopup(true)
    , useProxy(false)

{
}

QString CaptchaFormsPlugin::name() const {
    return "Captcha Forms Plugin";
}

QString CaptchaFormsPlugin::shortName() const {
    return "captcha";
}

QString CaptchaFormsPlugin::version() const {
    return constVersion;
}

bool CaptchaFormsPlugin::enable()
{
    if(psiOptions) {
        enabled = true;
        id = 111;
        useProxy = psiOptions->getPluginOption(constUseProxy, QVariant(useProxy)).toBool();
        autopopup = psiOptions->getPluginOption(constAutopopup, QVariant(autopopup)).toBool();

        if(!useProxy)
            appInfo->getProxyFor(name()); //register proxy
    }
    return enabled;
}

bool CaptchaFormsPlugin::disable()
{
    enabled = false;
    return true;
}

QWidget* CaptchaFormsPlugin::options()
{
    if (!enabled) {
        return 0;
    }
    QWidget *options = new QWidget();
    ui_.setupUi(options);

    restoreOptions();
    return options;
}

void CaptchaFormsPlugin::applyOptions()
{
    useProxy = ui_.cb_use_proxy->isChecked();
    psiOptions->setPluginOption(constUseProxy, QVariant(useProxy));
    autopopup = ui_.cb_autopopup->isChecked();
    psiOptions->setPluginOption(constAutopopup, QVariant(autopopup));

    if(!useProxy)
        appInfo->getProxyFor(name()); //register proxy
}

void CaptchaFormsPlugin::restoreOptions()
{
    ui_.cb_autopopup->setChecked(autopopup);
    ui_.cb_use_proxy->setChecked(useProxy);
}

void CaptchaFormsPlugin::setOptionAccessingHost(OptionAccessingHost *host)
{
    psiOptions = host;
}

void CaptchaFormsPlugin::setEventCreatingHost(EventCreatingHost *host)
{
    psiEvent = host;
}

void CaptchaFormsPlugin::setAccountInfoAccessingHost(AccountInfoAccessingHost *host)
{
    accInfo = host;
}

void CaptchaFormsPlugin::setApplicationInfoAccessingHost(ApplicationInfoAccessingHost *host)
{
    appInfo = host;
}

void CaptchaFormsPlugin::setStanzaSendingHost(StanzaSendingHost *host)
{
    stanzaSender = host;
}

bool CaptchaFormsPlugin::incomingStanza(int account, const QDomElement &xml)
{
    if(enabled) {
        QHash<QString, QString> dataFields;
        if(!isValidChallenge(xml, dataFields) )
            return false;
        else {
            dataFields["id"] = QString::number(id++);
            dataFields["account"] = QString::number(account);
            challenges_.push_back(dataFields);
            if(autopopup)
                eventActivated(dataFields.value("sender"));
            else
                psiEvent->createNewEvent(account, dataFields.value("sender"), tr("Captcha Plugin: CAPTCHA from %1")
                                         .arg(dataFields.value("sender")), this, SLOT(eventActivated(QString)));

            return true;
        }
    }
    return false;
}

bool CaptchaFormsPlugin::outgoingStanza(int /*account*/, QDomElement& /*xml*/)
{
    return false;
}

void CaptchaFormsPlugin::eventActivated(const QString& from)
{
    int index = findChalleng("sender", from);
    if(index == -1)
        return;

    QHash<QString, QString> dataFields = challenges_.at(index);
    QString id = dataFields.value("id");
    QPointer<CaptchaDialog> cd = new CaptchaDialog(id);
    cd->setBody(dataFields.value("body"));
    cd->setQuestion(dataFields.value("label"));
    connect(cd, SIGNAL(ok(QString, QString)), this, SLOT(submitChallenge(QString, QString)));
    connect(cd, SIGNAL(cancel(QString)), this, SLOT(cancelChallenge(QString)));
    dialogs_[id] = cd;


    if(dataFields.contains("data")) {
        QByteArray ba;
        ba.append(dataFields.value("data"));
        QPixmap pix = QPixmap::fromImage(QImage::fromData(QByteArray::fromBase64(ba)));
        cd->setPixmap(pix);
    }
    else {
        Loader *ld = new Loader(id, this);
        if(useProxy) {
            int acc = dataFields.value("account").toInt();
            QString host_ = accInfo->proxyHost(acc);
            if(!host_.isEmpty()) {
                ld->setProxy(host_, accInfo->proxyPort(acc), accInfo->proxyUser(acc), accInfo->proxyPassword(acc));
            }
        }
        else {
            Proxy p = appInfo->getProxyFor(name());
            ld->setProxy(p.host, p.port, p.user, p.pass);
        }
        QString url = dataFields.value("uri");
        if(url.isEmpty()) {
            QString str = dataFields.value("body");
            QRegExp re(".*(http://[^\"]+).*");
            if(re.indexIn(str) != -1)
                url = re.cap(1) + "/image";
        }
        ld->start(url);
        connect(ld, SIGNAL(data(QString,QByteArray)), SLOT(loaderData(QString, QByteArray)));
        connect(ld, SIGNAL(error(QString)), SLOT(loaderError(QString)));
    }
    cd->show();
}

void CaptchaFormsPlugin::loaderError(const QString& id)
{
    if(!dialogs_.contains(id))
        return;

    QPointer<CaptchaDialog> cd = dialogs_.value(id);
    if(cd) {
        cd->setText(tr("Error! Image can not be loaded."));
    }
    else
        dialogs_.remove(id);
}

void CaptchaFormsPlugin::loaderData(const QString& id, const QByteArray& data)
{
    if(!dialogs_.contains(id))
        return;

    QPointer<CaptchaDialog> cd = dialogs_.value(id);
    if(cd) {
        cd->setPixmap(QPixmap::fromImage(QImage::fromData(data)));
    }
    else
        dialogs_.remove(id);
}

int CaptchaFormsPlugin::findChalleng(const QString& field, const QString& value)
{
    int index = -1;
    for(int i = 0; i < challenges_.size(); i++) {
        QHash<QString, QString> dataFields = challenges_.at(i);
        if(dataFields.contains(field) && dataFields.value(field) == value) {
            index = i;
            break;
        }
    }
    return index;
}

void CaptchaFormsPlugin::submitChallenge(const QString& id, const QString& text)
{
    int index = findChalleng("id", id);
    if(index == -1)
        return;

    QHash<QString, QString> dataFields = challenges_.at(index);
    QString mes = QString("<iq type=\"set\" from=\"%1\" to=\"%2\" xml:lang=\"en\" id=\"%3\">"
                          "<captcha xmlns=\"urn:xmpp:captcha\"><x xmlns=\"jabber:x:data\" type=\"submit\">"
                          "<field var=\"FORM_TYPE\"><value>urn:xmpp:captcha</value></field>"
                          "<field var=\"from\"><value>%4</value></field>"
                          "<field var=\"challenge\"><value>%5</value></field>"
                          "<field var=\"%6\"><value>%7</value></field>"
                          "</x></captcha></iq>")
                          .arg(stanzaSender->escape(dataFields.value("to")), stanzaSender->escape(dataFields.value("sender")), stanzaSender->escape(dataFields.value("id")))
                          .arg(stanzaSender->escape(dataFields.value("from")), stanzaSender->escape(dataFields.value("challenge")))
                          .arg(stanzaSender->escape(dataFields.value("method")), text);

    stanzaSender->sendStanza(dataFields.value("account").toInt(), mes);

    challenges_.removeAt(index);
}

void CaptchaFormsPlugin::cancelChallenge(const QString& id)
{
    int index = findChalleng("id", id);
    if(index == -1)
        return;

    QHash<QString, QString> dataFields = challenges_.at(index);

    QString mes = QString("<message type=\"error\" to=\"%1\" xml:lang=\"en\" id=\"%2\">"
                          "<error xmlns=\"urn:ietf:params:xml:ns:xmpp-stanzas\" type=\"modify\">"
                          "<not-acceptable xmlns=\"urn:ietf:params:xml:ns:xmpp-stanzas\"/></error></message>")
                          //.arg(dataFields.value("to"))
                          .arg(stanzaSender->escape(dataFields.value("sender")))
                          .arg(stanzaSender->escape(dataFields.value("challenge")));
                          stanzaSender->sendStanza(dataFields.value("account").toInt(), mes);

    challenges_.removeAt(index);
}

bool CaptchaFormsPlugin::isValidChallenge(const QDomElement& stanza, QHash<QString, QString>& dataFields) const
{
    if(stanza.tagName() != "message")
        return false;

    QDomElement captcha = stanza.firstChildElement("captcha");
    if(captcha.attribute("xmlns") != "urn:xmpp:captcha")
        return false;

    bool find = false;
    QDomElement formElem = captcha.firstChildElement("x");
    while (!formElem.isNull() ) {
        if(formElem.attribute("xmlns") == "jabber:x:data") {
            find = true;
            break;
        }
        formElem = formElem.nextSiblingElement("x");
    }

    if(!find)
        return false;

    dataFields["sender"] = stanza.attribute("from");
    dataFields["body"] = stanza.firstChildElement("body").text();
    dataFields["to"] = stanza.attribute("to");

    find = false;
    QString id = stanza.attribute("id");

    QDomNode field = formElem.firstChild();
    while(!field.isNull()) {
        QDomElement tmp = field.toElement();
        if(tmp.tagName() != "field")
            field = field.nextSibling();

        QString var = tmp.attribute("var");

        if(var == "FORM_TYPE") {
            if(tmp.firstChildElement("value").text() == "urn:xmpp:captcha") {
                find = true;
            }
        }
        else if(var == "challenge") {
            if(tmp.firstChildElement("value").text() == id) {
                dataFields["challenge"] = id;  // id запроса
            }
        }
        else if(var == "from") {
            QString from = tmp.firstChildElement("value").text();
            if(!from.isEmpty()) {
                dataFields["from"] = from;
            }
        }
        else if(tmp.attribute("type") != "hidden") {
            if(!var.isEmpty() && methods.contains(var)) {
                dataFields["label"] = tmp.attribute("label");  //вопрос
                dataFields["method"] = var; // метод
                QDomNodeList uriList = tmp.elementsByTagName("uri");
                for(int i = 0; i < uriList.size(); i++) {
                    QDomElement uriElem = uriList.at(i).toElement();
                    QString link = uriElem.text();
                    if(uriElem.attribute("type").left(5) == "image"
                       && link.left(4) == "http") {
                        dataFields["uri"] = link;  // ссылка на контент в интернете
                        dataFields["uri_type"] = uriElem.attribute("type"); // тип изображения
                        break;
                    }
                }
            }
        }
        field = field.nextSibling();
    }

    QDomElement data = stanza.firstChildElement("data");
    if(!data.isNull()) {
        dataFields["data"] = data.text();
    }

    QStringList f = dataFields.keys();
    if( !find || !f.contains("challenge") || !f.contains("from") )
        return false;

    return true;
}

QString CaptchaFormsPlugin::pluginInfo()
{
    return tr("Author: ") +     "Dealer_WeARE\n"
         + tr("Email: ") + "wadealer@gmail.com\n\n"
         + trUtf8("This plugin is designed to pass of captcha directly from the Psi+.");
}

QPixmap CaptchaFormsPlugin::icon() const
{
    return QPixmap(":/icons/captcha.png");
}

#include "captchaformsplugin.moc"
