/*
 * xdata_widget.cpp - a class for displaying jabber:x:data forms
 * Copyright (C) 2003-2004  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 "xdata_widget.h"

#include "desktoputil.h"
#include "iris/xmpp_client.h"
#include "iris/xmpp_tasks.h"
#include "iris/xmpp_xdata.h"
#include "networkaccessmanager.h"
#include "psicon.h"

#include <QCheckBox>
#include <QComboBox>
#include <QGridLayout>
#include <QLabel>
#include <QLayout>
#include <QLineEdit>
#include <QListWidget>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QObject>
#include <QSpacerItem>
#include <QTextEdit>
#include <QUrl>

using namespace XMPP;

class XDataMediaWidget : public QLabel {
    Q_OBJECT

public:
    XDataMediaWidget(XData::Field::MediaUri uri, QSize s, Jid j, XDataWidget *xdw) :
        QLabel(xdw), _xdWidget(xdw), _size(s), _type(uri.mimeType)
    {
        Q_UNUSED(_xdWidget)
        if (uri.uri.startsWith("cid:")) {
            JT_BitsOfBinary *task = new JT_BitsOfBinary(xdw->client()->rootTask());
            connect(task, SIGNAL(finished()), SLOT(bobReceived()));
            task->get(j, uri.uri.mid(4));
            task->go(true);
        } else {
            auto           nam   = xdw->psi()->networkAccessManager();
            QNetworkReply *reply = nam->get(QNetworkRequest(QUrl(uri.uri)));
            connect(reply, SIGNAL(finished()), SLOT(oobReceived()));
        }
    }

    static QList<XDataMediaWidget *> fromMediaElement(XData::Field::MediaElement m, Jid j, XDataWidget *xdw)
    {
        QList<XDataMediaWidget *> result;
        // simple image filter
        // TODO add support for other formats
        for (const XData::Field::MediaUri &uri : m) {
            if (uri.mimeType.startsWith("image")) {
                XDataMediaWidget *mw = new XDataMediaWidget(uri, m.mediaSize(), j, xdw);
                result.append(mw);
            }
        }
        return result;
    }

    static QStringList supportedMedia()
    {
        static QStringList wildcards;
        if (wildcards.isEmpty()) {
            wildcards << "image/*";
        }
        return wildcards;
    }

private:
    void onDataReceived(const QByteArray &data)
    {
        if (!data.isNull()) {
            QPixmap mpix;
            mpix.loadFromData(data);
            if (_size.isEmpty()) {
                setPixmap(mpix);
            } else {
                setPixmap(mpix.scaled(_size));
            }
        }
    }

private slots:
    void bobReceived()
    {
        BoBData &bob = static_cast<JT_BitsOfBinary *>(sender())->data();
        onDataReceived(bob.data());
    }

    void oobReceived()
    {
        QNetworkReply *reply = dynamic_cast<QNetworkReply *>(sender());
        onDataReceived(reply->readAll());
        reply->deleteLater();
    }

private:
    XDataWidget *_xdWidget;
    QSize        _size;
    QString      _type;
};

//----------------------------------------------------------------------------
// XDataField
//----------------------------------------------------------------------------
class XDataField {
public:
    XDataField(XData::Field f, XDataWidget *w = nullptr)
    {
        _field    = f;
        _xdWidget = w;
    }
    virtual ~XDataField() { }

    virtual XData::Field field() const { return _field; }

    virtual QWidget *focusProxy() const { return nullptr; }

    QString labelText(QString str = ": ") const
    {
        QString text = _field.label();
        if (text.isEmpty())
            text = _field.var();
        return text + str;
    }

    QString reqText() const
    {
        QString req;
        if (_field.required())
            req = "*";
        if (!_field.desc().isEmpty()) {
            if (!req.isEmpty())
                req += ' ';
            req += "(?)";
        }
        return req;
    }

    virtual bool isValid() const { return field().isValid(); }

private:
    XData::Field _field;

protected:
    XDataWidget *_xdWidget;
};

////////////////////////////////////////

class XDataField_Hidden : public XDataField {
public:
    XDataField_Hidden(XData::Field f, XDataWidget *w) : XDataField(f, w) { }
};

////////////////////////////////////////

class XDataField_Boolean : public XDataField {
public:
    XDataField_Boolean(XData::Field f, QGridLayout *grid, XDataWidget *xdw) : XDataField(f, xdw)
    {
        int  row     = grid->rowCount();
        bool checked = false;
        if (f.value().count()) {
            QString s = f.value().constFirst();
            if (s == "1" || s == "true" || s == "yes")
                checked = true;
        }

        QHBoxLayout *layout     = new QHBoxLayout;
        QSpacerItem *spacerItem = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum);

        check = new QCheckBox(xdw);
        check->setChecked(checked);
        layout->addWidget(check);

        QLabel *label = new QLabel(labelText(""), xdw);
        layout->addWidget(label);
        layout->addSpacerItem(spacerItem);

        grid->addLayout(layout, row, 0);

        QLabel *req = new QLabel(reqText(), xdw);
        grid->addWidget(req, row, 1);

        if (!f.desc().isEmpty()) {
            label->setToolTip(f.desc());
            check->setToolTip(f.desc());
            req->setToolTip(f.desc());
        }
    }

    XData::Field field() const
    {
        XData::Field f = XDataField::field();
        QStringList  val;
        val << QString(check->isChecked() ? "1" : "0");
        f.setValue(val);
        return f;
    }

private:
    QCheckBox *check;
};

////////////////////////////////////////

class XDataField_Fixed : public XDataField {
public:
    XDataField_Fixed(XData::Field f, QGridLayout *grid, XDataWidget *xdw) : XDataField(f, xdw)
    {
        int                   row = grid->rowCount();
        QString               text;
        QStringList           val = f.value();
        QStringList::Iterator it  = val.begin();
        for (; it != val.end(); ++it) {
            if (!text.isEmpty())
                text += "<br>";
            text += *it;
        }

        QLabel *fixed = new QLabel("<qt>" + text + "<br/></qt>", xdw);
        fixed->setWordWrap(true);
        grid->addWidget(fixed, row, 0);

        if (!f.desc().isEmpty()) {
            fixed->setToolTip(f.desc());
        }
    }
};

////////////////////////////////////////

class XDataField_TextSingle : public XDataField {
public:
    XDataField_TextSingle(XData::Field f, QGridLayout *grid, XDataWidget *xdw) : XDataField(f, xdw)
    {
        int                        row = grid->rowCount();
        XData::Field::MediaElement me  = f.mediaElement();
        if (!me.isEmpty()) {
            Jid j = xdw->owner();

            QList<XDataMediaWidget *> mediaWidgets = XDataMediaWidget::fromMediaElement(me, j, _xdWidget);
            for (XDataMediaWidget *w : mediaWidgets) {
                grid->addWidget(w, row, 0, 1, 3, Qt::AlignCenter);
                row++;
            }
        }

        QString text;
        if (f.value().count())
            text = f.value().constFirst();
        QVBoxLayout *layout = new QVBoxLayout;

        QLabel *label = new QLabel(labelText(), xdw);
        label->setWordWrap(true);
        layout->addWidget(label);

        edit = new QLineEdit(xdw);
        edit->setText(text);
        layout->addWidget(edit);

        grid->addLayout(layout, row, 0);

        QLabel *req = new QLabel(reqText(), xdw);
        grid->addWidget(req, row, 1);

        if (!f.desc().isEmpty()) {
            label->setToolTip(f.desc());
            edit->setToolTip(f.desc());
            req->setToolTip(f.desc());
        }
    }

    XData::Field field() const
    {
        XData::Field f = XDataField::field();
        f.setMediaElement(XData::Field::MediaElement());
        QStringList val;
        val << edit->text();
        f.setValue(val);
        return f;
    }

    QWidget *focusProxy() const { return edit; }

protected:
    QLineEdit *edit;
};

////////////////////////////////////////

class XDataField_TextPrivate : public XDataField_TextSingle {
public:
    XDataField_TextPrivate(XData::Field f, QGridLayout *grid, XDataWidget *xdw) : XDataField_TextSingle(f, grid, xdw)
    {
        edit->setEchoMode(QLineEdit::Password);
    }
};

////////////////////////////////////////

class XDataField_JidSingle : public XDataField_TextSingle {
public:
    XDataField_JidSingle(XData::Field f, QGridLayout *grid, XDataWidget *w) : XDataField_TextSingle(f, grid, w)
    {
        // TODO: add proper validation
    }
};

////////////////////////////////////////

class XDataField_ListSingle : public XDataField {
public:
    XDataField_ListSingle(XData::Field f, QGridLayout *grid, XDataWidget *xdw) : XDataField(f, xdw)
    {
        QHBoxLayout *layout = new QHBoxLayout;

        int     row   = grid->rowCount();
        QLabel *label = new QLabel(labelText(), xdw);
        label->setWordWrap(true);
        layout->addWidget(label);

        combo = new QComboBox(xdw);
        layout->addWidget(combo);
        combo->setInsertPolicy(QComboBox::NoInsert);

        grid->addLayout(layout, row, 0);

        QString sel;
        if (!f.value().isEmpty())
            sel = f.value().constFirst();

        XData::Field::OptionList           opts = f.options();
        XData::Field::OptionList::Iterator it   = opts.begin();
        for (; it != opts.end(); ++it) {
            QString lbl = (*it).label;
            if (lbl.isEmpty())
                lbl = (*it).value;

            combo->addItem(lbl);
            if ((*it).value == sel)
                combo->setCurrentIndex(combo->count() - 1);
        }

        QLabel *req = new QLabel(reqText(), xdw);
        grid->addWidget(req, row, 1);

        if (!f.desc().isEmpty()) {
            label->setToolTip(f.desc());
            combo->setToolTip(f.desc());
            req->setToolTip(f.desc());
        }
    }

    XData::Field field() const
    {
        QString lbl = combo->currentText();

        XData::Field f = XDataField::field();
        QStringList  val;

        XData::Field::OptionList           opts = f.options();
        XData::Field::OptionList::Iterator it   = opts.begin();
        for (; it != opts.end(); ++it) {
            if ((*it).label == lbl || (*it).value == lbl) {
                val << (*it).value;
                break;
            }
        }

        f.setValue(val);
        return f;
    }

    QWidget *focusProxy() const { return combo; }

private:
    QComboBox *combo;
};

////////////////////////////////////////

class XDataField_ListMulti : public XDataField {
public:
    XDataField_ListMulti(XData::Field f, QGridLayout *grid, XDataWidget *xdw) : XDataField(f, xdw)
    {
        int     row   = grid->rowCount();
        QLabel *label = new QLabel(labelText(), xdw);
        label->setWordWrap(true);
        grid->addWidget(label, row, 0);

        list = new QListWidget(xdw);
        grid->addWidget(list, row, 1);
        list->setSelectionMode(QAbstractItemView::MultiSelection);

        XData::Field::OptionList           opts = f.options();
        XData::Field::OptionList::Iterator it   = opts.begin();
        for (; it != opts.end(); ++it) {
            QString lbl = (*it).label;
            if (lbl.isEmpty())
                lbl = (*it).value;

            QListWidgetItem *item = new QListWidgetItem(lbl, list);

            QStringList           val = f.value();
            QStringList::Iterator sit = val.begin();
            for (; sit != val.end(); ++sit)
                if ((*it).label == *sit || (*it).value == *sit)
                    item->setSelected(true);
        }

        QLabel *req = new QLabel(reqText(), xdw);
        grid->addWidget(req, row, 2);

        if (!f.desc().isEmpty()) {
            label->setToolTip(f.desc());
            list->setToolTip(f.desc());
            req->setToolTip(f.desc());
        }
    }

    XData::Field field() const
    {
        XData::Field f = XDataField::field();
        QStringList  val;

        for (int i = 0; i < list->count(); i++) {
            QListWidgetItem *item = list->item(i);
            if (item->isSelected()) {
                QString                            lbl  = item->text();
                XData::Field::OptionList           opts = f.options();
                XData::Field::OptionList::Iterator it   = opts.begin();
                for (; it != opts.end(); ++it) {
                    if ((*it).label == lbl || (*it).value == lbl) {
                        val << (*it).value;
                        break;
                    }
                }
            }
        }

        f.setValue(val);
        return f;
    }

    QWidget *focusProxy() const { return list; }

private:
    QListWidget *list;
};

////////////////////////////////////////

class XDataField_TextMulti : public XDataField {
public:
    XDataField_TextMulti(XData::Field f, QGridLayout *grid, XDataWidget *xdw) : XDataField(f, xdw)
    {
        int          row    = grid->rowCount();
        QHBoxLayout *layout = new QHBoxLayout;

        QLabel *label = new QLabel(labelText(), xdw);
        label->setWordWrap(true);
        layout->addWidget(label);

        edit = new QTextEdit(xdw);
        layout->addWidget(edit);

        QString               text;
        QStringList           val = f.value();
        QStringList::Iterator it  = val.begin();
        for (; it != val.end(); ++it) {
            if (!text.isEmpty())
                text += '\n';
            text += *it;
        }
        edit->setText(text);

        grid->addLayout(layout, row, 0);

        QLabel *req = new QLabel(reqText(), xdw);
        grid->addWidget(req, row, 1);

        if (!f.desc().isEmpty()) {
            label->setToolTip(f.desc());
            edit->setToolTip(f.desc());
            req->setToolTip(f.desc());
        }
    }

    XData::Field field() const
    {
        XData::Field f = XDataField::field();
        f.setValue(edit->toPlainText().split("\n"));
        return f;
    }

    QWidget *focusProxy() const { return edit; }

private:
    QTextEdit *edit;
};

////////////////////////////////////////

class XDataField_JidMulti : public XDataField_TextMulti {
public:
    XDataField_JidMulti(XData::Field f, QGridLayout *grid, XDataWidget *xdw) : XDataField_TextMulti(f, grid, xdw)
    {
        // TODO: improve validation
    }
};

//----------------------------------------------------------------------------
// XDataWidget
//----------------------------------------------------------------------------

XDataWidget::XDataWidget(PsiCon *psi, QWidget *parent, XMPP::Client *client, XMPP::Jid owner) :
    QWidget(parent), psi_(psi), client_(client), consistent_(true)
{
    owner_  = owner;
    layout_ = new QVBoxLayout(this);
    layout_->setContentsMargins(0, 0, 0, 0);
}

XDataWidget::~XDataWidget() { qDeleteAll(fields_); }

PsiCon *XDataWidget::psi() const { return psi_; }

XMPP::Client *XDataWidget::client() const { return client_; }

QString XDataWidget::registrarType() const { return registrarType_; }

XMPP::Jid XDataWidget::owner() const { return owner_; }

XMPP::Stanza::Error XDataWidget::consistencyError() const { return consistencyError_; }

void XDataWidget::setInstructions(const QString &instructions)
{
    if (!instructions.isEmpty()) {
        QLabel *l = new QLabel(instructions, this);
        l->setWordWrap(true);
        l->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse);
        connect(l, SIGNAL(linkActivated(const QString &)), SLOT(linkActivated(const QString &)));
        layout_->addWidget(l);
    }
}

void XDataWidget::setForm(const XMPP::XData &d, bool withInstructions)
{
    qDeleteAll(fields_);
    fields_.clear();

    QLayoutItem *child;
    while ((child = layout_->takeAt(0)) != nullptr) {
        delete child->widget();
        delete child;
    }

    registrarType_ = d.registrarType();
    XData::FieldList fields;
    if (registrarType_ == "urn:xmpp:captcha") {
        QStringList supportedMedia = XDataMediaWidget::supportedMedia();
        QStringList mediaVars;
        mediaVars << "audio_recog" << "ocr" << "picture_q" << "picture_recog" << "speech_q" << "speech_recog"
                  << "video_q" << "video_recog";
        short maxAnswers       = 0;
        short requestedAnswers = 0;
        Jid   from;
        for (const XData::Field &field : d.fields()) {
            if (!field.var().isEmpty()) {
                if (field.var() == "answers") {
                    requestedAnswers = field.value().value(0).toShort();
                }
                if (field.var() == "from") {
                    from = field.value().value(0);
                }
                if (field.var() == "SHA-256") {
                    if (field.required()) {
                        consistent_ = false; // sha-256 is not supported atm
                        break;
                    }
                    continue; // unlikely, but who knows
                }
                bool isMedia = mediaVars.contains(field.var());
                if (isMedia || field.var() == "qa") {
                    if (isMedia) {
                        if (!field.mediaElement().checkSupport(supportedMedia)) {
                            if (field.required()) {
                                consistent_ = false;
                                break;
                            }
                            continue;
                        }
                    }
                    maxAnswers++;
                }
            }
            fields.append(field);
        }
        if (requestedAnswers > maxAnswers) {
            consistent_ = false;
        }
        if (owner_.domain() != from.domain() || (!owner_.node().isEmpty() && owner_.node() != from.node())) {
            consistent_ = false;
        }
        if (!consistent_) {
            consistencyError_
                = Stanza::Error(Stanza::Error::ErrorType::Modify, Stanza::Error::ErrorCond::NotAcceptable);
        }
        // TODO check if captcha was sent too late (more than 2 minutes)
    } else {
        fields = d.fields();
    }
    if (withInstructions) {
        setInstructions(d.instructions());
    }
    setFields(fields);
}

XData::FieldList XDataWidget::fields() const
{
    XData::FieldList f;

    for (const auto &field : fields_) {
        f.append(field->field());
    }

    return f;
}

void XDataWidget::setFields(const XData::FieldList &f)
{
    QWidget *fields = new QWidget(this);
    layout_->addWidget(fields);
    if (f.count()) {
        // FIXME
        QGridLayout *grid = new QGridLayout(fields);
        grid->setSpacing(3);
        bool focusSet = false;

        XData::FieldList::ConstIterator it = f.begin();
        for (; it != f.end(); ++it) {
            XDataField *f;
            switch ((*it).type()) {
            case XData::Field::Field_Boolean:
                f = new XDataField_Boolean(*it, grid, this);
                break;
            case XData::Field::Field_Fixed:
                f = new XDataField_Fixed(*it, grid, this);
                break;
            case XData::Field::Field_Hidden:
                f = new XDataField_Hidden(*it, this);
                break;
            case XData::Field::Field_JidSingle:
                f = new XDataField_JidSingle(*it, grid, this);
                break;
            case XData::Field::Field_ListMulti:
                f = new XDataField_ListMulti(*it, grid, this);
                break;
            case XData::Field::Field_ListSingle:
                f = new XDataField_ListSingle(*it, grid, this);
                break;
            case XData::Field::Field_TextMulti:
                f = new XDataField_TextMulti(*it, grid, this);
                break;
            case XData::Field::Field_JidMulti:
                f = new XDataField_JidMulti(*it, grid, this);
                break;
            case XData::Field::Field_TextPrivate:
                f = new XDataField_TextPrivate(*it, grid, this);
                break;

            default:
                f = new XDataField_TextSingle(*it, grid, this);
            }
            fields_.append(f);
            auto proxy = f->focusProxy();
            if (proxy) {
                if (!focusSet) {
                    setFocusProxy(proxy);
                    focusSet = true;
                }
                lastFocusableWidget_ = proxy;
            }
        }
    }
}

XDataField *XDataWidget::fieldByVar(const QString &var) const
{
    for (XDataField *field : fields_) {
        if (field->field().var() == var) {
            return field;
        }
    }
    return nullptr;
}

void XDataWidget::linkActivated(const QString &link) { DesktopUtil::openUrl(link); }

#include "xdata_widget.moc"
