/***************************************************************************
 *   Copyright (c) 2004 Werner Mayer <wmayer[at]users.sourceforge.net>     *
 *                                                                         *
 *   This file is part of the FreeCAD CAx development system.              *
 *                                                                         *
 *   This library is free software; you can redistribute it and/or         *
 *   modify it under the terms of the GNU Library General Public           *
 *   License as published by the Free Software Foundation; either          *
 *   version 2 of the License, or (at your option) any later version.      *
 *                                                                         *
 *   This library  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 Library General Public License for more details.                  *
 *                                                                         *
 *   You should have received a copy of the GNU Library General Public     *
 *   License along with this library; see the file COPYING.LIB. If not,    *
 *   write to the Free Software Foundation, Inc., 59 Temple Place,         *
 *   Suite 330, Boston, MA  02111-1307, USA                                *
 *                                                                         *
 ***************************************************************************/


#include <QApplication>
#include <QVBoxLayout>

#include <FCConfig.h>

#ifdef FC_OS_WIN32
# undef max
# undef min
# ifdef _MSC_VER
#  pragma warning(disable : 4099)
#  pragma warning(disable : 4522)
# endif
#endif

#include <App/Application.h>
#include <Base/Console.h>
#include <Base/Exception.h>
#include <Base/Interpreter.h>

#include "WidgetFactory.h"
#include "PrefWidgets.h"
#include "PythonWrapper.h"
#include "UiLoader.h"


using namespace Gui;

Gui::WidgetFactoryInst* Gui::WidgetFactoryInst::_pcSingleton = nullptr;

WidgetFactoryInst& WidgetFactoryInst::instance()
{
    if (!_pcSingleton) {
        _pcSingleton = new WidgetFactoryInst;
    }
    return *_pcSingleton;
}

void WidgetFactoryInst::destruct()
{
    if (_pcSingleton) {
        delete _pcSingleton;
    }
    _pcSingleton = nullptr;
}

/**
 * Creates a widget with the name \a sName which is a child of \a parent.
 * To create an instance of this widget once it must has been registered.
 * If there is no appropriate widget registered nullptr is returned.
 */
QWidget* WidgetFactoryInst::createWidget(const char* sName, QWidget* parent) const
{
    auto w = static_cast<QWidget*>(Produce(sName));

    // this widget class is not registered
    if (!w) {
#ifdef FC_DEBUG
        Base::Console().warning("\"%s\" is not registered\n", sName);
#else
        Base::Console().log("\"%s\" is not registered\n", sName);
#endif
        return nullptr;
    }

    try {
#ifdef FC_DEBUG
        const char* cName = qobject_cast<QWidget*>(w)->metaObject()->className();
        Base::Console().log("Widget of type '%s' created.\n", cName);
#endif
    }
    catch (...) {
#ifdef FC_DEBUG
        Base::Console().error("%s does not inherit from \"QWidget\"\n", sName);
#else
        Base::Console().log("%s does not inherit from \"QWidget\"\n", sName);
#endif
        delete w;
        return nullptr;
    }

    // set the parent to the widget
    if (parent) {
        w->setParent(parent);
    }

    return w;
}

/**
 * Creates a widget with the name \a sName which is a child of \a parent.
 * To create an instance of this widget once it must has been registered.
 * If there is no appropriate widget registered nullptr is returned.
 */
Gui::Dialog::PreferencePage* WidgetFactoryInst::createPreferencePage(
    const char* sName,
    QWidget* parent
) const
{
    auto w = (Gui::Dialog::PreferencePage*)Produce(sName);

    // this widget class is not registered
    if (!w) {
#ifdef FC_DEBUG
        Base::Console().warning("Cannot create an instance of \"%s\"\n", sName);
#else
        Base::Console().log("Cannot create an instance of \"%s\"\n", sName);
#endif
        return nullptr;
    }

    if (qobject_cast<Gui::Dialog::PreferencePage*>(w)) {
#ifdef FC_DEBUG
        Base::Console().log("Preference page of type '%s' created.\n", w->metaObject()->className());
#endif
    }
    else {
#ifdef FC_DEBUG
        Base::Console().error("%s does not inherit from 'Gui::Dialog::PreferencePage'\n", sName);
#endif
        delete w;
        return nullptr;
    }

    // set the parent to the widget
    if (parent) {
        w->setParent(parent);
    }

    return w;
}

/**
 * Creates a preference widget with the name \a sName and the preference name \a sPref
 * which is a child of \a parent.
 * To create an instance of this widget once it must has been registered.
 * If there is no appropriate widget registered nullptr is returned.
 * After creation of this widget its recent preferences are restored automatically.
 */
QWidget* WidgetFactoryInst::createPrefWidget(const char* sName, QWidget* parent, const char* sPref)
{
    QWidget* w = createWidget(sName);
    // this widget class is not registered
    if (!w) {
        return nullptr;  // no valid QWidget object
    }

    // set the parent to the widget
    w->setParent(parent);

    try {
        auto pw = dynamic_cast<PrefWidget*>(w);
        if (pw) {
            pw->setEntryName(sPref);
            pw->restorePreferences();
        }
    }
    catch (...) {
#ifdef FC_DEBUG
        Base::Console().error("%s does not inherit from \"PrefWidget\"\n", w->metaObject()->className());
#endif
        delete w;
        return nullptr;
    }

    return w;
}

// ----------------------------------------------------

WidgetFactorySupplier* WidgetFactorySupplier::_pcSingleton = nullptr;

WidgetFactorySupplier& WidgetFactorySupplier::instance()
{
    // not initialized?
    if (!_pcSingleton) {
        _pcSingleton = new WidgetFactorySupplier;
    }
    return *_pcSingleton;
}

void WidgetFactorySupplier::destruct()
{
    // delete the widget factory and all its producers first
    WidgetFactoryInst::destruct();
    delete _pcSingleton;
    _pcSingleton = nullptr;
}

// ----------------------------------------------------

PrefPageUiProducer::PrefPageUiProducer(const char* filename, const char* group)
    : fn(QString::fromUtf8(filename))
{
    WidgetFactoryInst::instance().AddProducer(filename, this);
    Gui::Dialog::DlgPreferencesImp::addPage(filename, group);
}

PrefPageUiProducer::~PrefPageUiProducer() = default;

void* PrefPageUiProducer::Produce() const
{
    QWidget* page = new Gui::Dialog::PreferenceUiForm(fn);
    return static_cast<void*>(page);
}

// ----------------------------------------------------

PrefPagePyProducer::PrefPagePyProducer(const Py::Object& p, const char* group)
    : type(p)
{
    std::string str;
    Base::PyGILStateLocker lock;
    if (type.hasAttr("__name__")) {
        str = static_cast<std::string>(Py::String(type.getAttr("__name__")));
    }

    WidgetFactoryInst::instance().AddProducer(str.c_str(), this);
    Gui::Dialog::DlgPreferencesImp::addPage(str, group);
}

PrefPagePyProducer::~PrefPagePyProducer()
{
    Base::PyGILStateLocker lock;
    type = Py::None();
}

void* PrefPagePyProducer::Produce() const
{
    Base::PyGILStateLocker lock;
    try {
        Py::Callable method(type);
        Py::Tuple args;
        Py::Object page = method.apply(args);
        QWidget* widget = new Gui::Dialog::PreferencePagePython(page);
        if (!widget->layout()) {
            delete widget;
            widget = nullptr;
        }
        return widget;
    }
    catch (Py::Exception&) {
        PyErr_Print();
        return nullptr;
    }
}

// ----------------------------------------------------

using namespace Gui::Dialog;

PreferencePagePython::PreferencePagePython(const Py::Object& p, QWidget* parent)
    : PreferencePage(parent)
    , page(p)
{
    Base::PyGILStateLocker lock;
    Gui::PythonWrapper wrap;
    if (wrap.loadCoreModule()) {

        // old style class must have a form attribute while
        // new style classes can be the widget itself
        Py::Object widget;
        if (page.hasAttr(std::string("form"))) {
            widget = page.getAttr(std::string("form"));
        }
        else {
            widget = page;
        }

        QObject* object = wrap.toQObject(widget);
        if (object) {
            QWidget* form = qobject_cast<QWidget*>(object);
            if (form) {
                this->setWindowTitle(form->windowTitle());
                auto layout = new QVBoxLayout;
                layout->addWidget(form);
                setLayout(layout);
            }
        }
    }
}

PreferencePagePython::~PreferencePagePython()
{
    Base::PyGILStateLocker lock;
    page = Py::None();
}

void PreferencePagePython::changeEvent(QEvent* e)
{
    QWidget::changeEvent(e);
}

void PreferencePagePython::loadSettings()
{
    Base::PyGILStateLocker lock;
    try {
        if (page.hasAttr(std::string("loadSettings"))) {
            Py::Callable method(page.getAttr(std::string("loadSettings")));
            Py::Tuple args;
            method.apply(args);
        }
    }
    catch (Py::Exception&) {
        Base::PyException e;  // extract the Python error text
        e.reportException();
    }
}

void PreferencePagePython::saveSettings()
{
    Base::PyGILStateLocker lock;
    try {
        if (page.hasAttr(std::string("saveSettings"))) {
            Py::Callable method(page.getAttr(std::string("saveSettings")));
            Py::Tuple args;
            method.apply(args);
        }
    }
    catch (Py::Exception&) {
        Base::PyException e;  // extract the Python error text
        e.reportException();
    }
}

// ----------------------------------------------------

/* TRANSLATOR Gui::ContainerDialog */

/**
 *  Constructs a ContainerDialog which embeds the child \a templChild.
 *  The dialog will be modal.
 */
ContainerDialog::ContainerDialog(QWidget* templChild)
    : QDialog(QApplication::activeWindow())
{
    setModal(true);
    setWindowTitle(templChild->objectName());
    setObjectName(templChild->objectName());

    setSizeGripEnabled(true);
    MyDialogLayout = new QGridLayout(this);

    buttonOk = new QPushButton(this);
    buttonOk->setObjectName(QLatin1String("buttonOK"));
    buttonOk->setText(tr("&OK"));
    buttonOk->setAutoDefault(true);
    buttonOk->setDefault(true);

    MyDialogLayout->addWidget(buttonOk, 1, 0);
    auto spacer = new QSpacerItem(210, 20, QSizePolicy::Expanding, QSizePolicy::Minimum);
    MyDialogLayout->addItem(spacer, 1, 1);

    buttonCancel = new QPushButton(this);
    buttonCancel->setObjectName(QLatin1String("buttonCancel"));
    buttonCancel->setText(tr("&Cancel"));
    buttonCancel->setAutoDefault(true);

    MyDialogLayout->addWidget(buttonCancel, 1, 2);

    templChild->setParent(this);

    MyDialogLayout->addWidget(templChild, 0, 0, 0, 2);
    resize(QSize(307, 197).expandedTo(minimumSizeHint()));

    // signals and slots connections
    connect(buttonOk, &QPushButton::clicked, this, &QDialog::accept);
    connect(buttonCancel, &QPushButton::clicked, this, &QDialog::reject);
}

/** Destroys the object and frees any allocated resources */
ContainerDialog::~ContainerDialog() = default;

// ----------------------------------------------------

void PyResource::init_type()
{
    behaviors().name("PyResource");
    behaviors().doc("PyResource");
    // you must have overwritten the virtual functions
    behaviors().supportRepr();
    behaviors().supportGetattr();
    behaviors().supportSetattr();
    add_varargs_method("value", &PyResource::value);
    add_varargs_method("setValue", &PyResource::setValue);
    add_varargs_method("show", &PyResource::show);
    add_varargs_method("connect", &PyResource::connect);
}

PyResource::PyResource()
    : myDlg(nullptr)
{}

PyResource::~PyResource()
{
    delete myDlg;
    for (auto it : mySignals) {
        SignalConnect* sc = it;
        delete sc;
    }
}

/**
 * Loads an .ui file with the name \a name. If the .ui file cannot be found or the QWidgetFactory
 * cannot create an instance an exception is thrown. If the created resource does not inherit from
 * QDialog an instance of ContainerDialog is created to embed it.
 */
void PyResource::load(const char* name)
{
    QString fn = QString::fromUtf8(name);
    QFileInfo fi(fn);

    // checks whether it's a relative path
    if (fi.isRelative()) {
        QString cwd = QDir::currentPath();
        QString home = QDir(QString::fromStdString(App::Application::getHomePath())).path();

        // search in cwd and home path for the file
        //
        // file does not reside in cwd, check home path now
        if (!fi.exists()) {
            if (cwd == home) {
                QString what = QObject::tr("Cannot find file %1").arg(fi.absoluteFilePath());
                throw Base::FileSystemError(what.toUtf8().constData());
            }
            else {
                fi.setFile(QDir(home), fn);

                if (!fi.exists()) {
                    QString what = QObject::tr("Cannot find file %1 neither in %2 nor in %3")
                                       .arg(fn, cwd, home);
                    throw Base::FileSystemError(what.toUtf8().constData());
                }
                else {
                    fn = fi.absoluteFilePath();  // file resides in FreeCAD's home directory
                }
            }
        }
    }
    else {
        if (!fi.exists()) {
            QString what = QObject::tr("Cannot find file %1").arg(fn);
            throw Base::FileSystemError(what.toUtf8().constData());
        }
    }

    QWidget* w = nullptr;
    try {
        auto loader = UiLoader::newInstance();
        QFile file(fn);
        if (file.open(QFile::ReadOnly)) {
            w = loader->load(&file, QApplication::activeWindow());
        }
        file.close();
    }
    catch (...) {
        throw Base::RuntimeError("Cannot create resource");
    }

    if (!w) {
        throw Base::ValueError("Invalid widget.");
    }

    if (w->inherits("QDialog")) {
        myDlg = static_cast<QDialog*>(w);
    }
    else {
        myDlg = new ContainerDialog(w);
    }
}

/**
 * Makes a connection between the sender widget \a sender and its signal \a signal
 * of the created resource and Python callback function \a cb.
 * If the sender widget does not exist or no resource has been loaded this method returns false,
 * otherwise it returns true.
 */
bool PyResource::connect(const char* sender, const char* signal, PyObject* cb)
{
    if (!myDlg) {
        return false;
    }

    QObject* objS = nullptr;
    QList<QWidget*> list = myDlg->findChildren<QWidget*>();
    QList<QWidget*>::const_iterator it = list.cbegin();
    QObject* obj;
    QString sigStr = QStringLiteral("2%1").arg(QString::fromLatin1(signal));

    while (it != list.cend()) {
        obj = *it;
        ++it;
        if (obj->objectName() == QLatin1String(sender)) {
            objS = obj;
            break;
        }
    }

    if (objS) {
        auto sc = new SignalConnect(this, cb);
        mySignals.push_back(sc);
        return QObject::connect(objS, sigStr.toLatin1(), sc, SLOT(onExecute()));
    }
    else {
        qWarning("'%s' does not exist.\n", sender);
    }

    return false;
}

Py::Object PyResource::repr()
{
    return Py::String("Resource object");
}

/**
 * Searches for a widget and its value in the argument object \a args
 * to returns its value as Python object.
 * In the case it fails nullptr is returned.
 */
Py::Object PyResource::value(const Py::Tuple& args)
{
    char* psName;
    char* psProperty;
    if (!PyArg_ParseTuple(args.ptr(), "ss", &psName, &psProperty)) {
        throw Py::Exception();
    }

    QVariant v;
    if (myDlg) {
        QList<QWidget*> list = myDlg->findChildren<QWidget*>();
        QList<QWidget*>::const_iterator it = list.cbegin();
        QObject* obj;

        bool fnd = false;
        while (it != list.cend()) {
            obj = *it;
            ++it;
            if (obj->objectName() == QLatin1String(psName)) {
                fnd = true;
                v = obj->property(psProperty);
                break;
            }
        }

        if (!fnd) {
            qWarning("'%s' not found.\n", psName);
        }
    }

    Py::Object item = Py::None();
    switch (v.userType()) {
        case QMetaType::QStringList: {
            QStringList str = v.toStringList();
            int nSize = str.count();
            Py::List slist(nSize);
            for (int i = 0; i < nSize; ++i) {
                slist.setItem(i, Py::String(str[i].toLatin1()));
            }
            item = slist;
        } break;
        case QMetaType::QByteArray:
            break;
        case QMetaType::QString:
            item = Py::String(v.toString().toLatin1());
            break;
        case QMetaType::Double:
            item = Py::Float(v.toDouble());
            break;
        case QMetaType::Bool:
            item = Py::Boolean(v.toBool());
            break;
        case QMetaType::UInt:
            item = Py::Long(static_cast<unsigned long>(v.toUInt()));
            break;
        case QMetaType::Int:
            item = Py::Long(v.toInt());
            break;
        default:
            item = Py::String("");
            break;
    }

    return item;
}

/**
 * Searches for a widget, its value name and the new value in the argument object \a args
 * to set even this new value.
 * In the case it fails nullptr is returned.
 */
Py::Object PyResource::setValue(const Py::Tuple& args)
{
    char* psName;
    char* psProperty;
    PyObject* psValue;
    if (!PyArg_ParseTuple(args.ptr(), "ssO", &psName, &psProperty, &psValue)) {
        throw Py::Exception();
    }

    QVariant v;
    if (PyUnicode_Check(psValue)) {
        v = QString::fromUtf8(PyUnicode_AsUTF8(psValue));
    }
    else if (PyLong_Check(psValue)) {
        unsigned int val = PyLong_AsLong(psValue);
        v = val;
    }
    else if (PyFloat_Check(psValue)) {
        v = PyFloat_AsDouble(psValue);
    }
    else if (PyList_Check(psValue)) {
        QStringList str;
        int nSize = PyList_Size(psValue);
        for (int i = 0; i < nSize; ++i) {
            PyObject* item = PyList_GetItem(psValue, i);
            if (!PyUnicode_Check(item)) {
                continue;
            }
            const char* pItem = PyUnicode_AsUTF8(item);
            str.append(QString::fromUtf8(pItem));
        }

        v = str;
    }
    else {
        throw Py::TypeError("Unsupported type");
    }

    if (myDlg) {
        QList<QWidget*> list = myDlg->findChildren<QWidget*>();
        QList<QWidget*>::const_iterator it = list.cbegin();
        QObject* obj;

        bool fnd = false;
        while (it != list.cend()) {
            obj = *it;
            ++it;
            if (obj->objectName() == QLatin1String(psName)) {
                fnd = true;
                obj->setProperty(psProperty, v);
                break;
            }
        }

        if (!fnd) {
            qWarning("'%s' not found.\n", psName);
        }
    }

    return Py::None();
}

/**
 * If any resource has been loaded this methods shows it as a modal dialog.
 */
Py::Object PyResource::show(const Py::Tuple&)
{
    if (myDlg) {
        // small trick to get focus
        myDlg->showMinimized();

#ifdef Q_WS_X11
        // On X11 this may not work. For further information see QWidget::showMaximized
        //
        // workaround for X11
        myDlg->hide();
        myDlg->show();
#endif

        myDlg->showNormal();
        myDlg->exec();
    }

    return Py::None();
}

/**
 * Searches for the sender, the signal and the callback function to connect with
 * in the argument object \a args. In the case it fails nullptr is returned.
 */
Py::Object PyResource::connect(const Py::Tuple& args)
{
    char* psSender;
    char* psSignal;

    PyObject* temp;

    if (PyArg_ParseTuple(args.ptr(), "ssO", &psSender, &psSignal, &temp)) {
        if (!PyCallable_Check(temp)) {
            PyErr_SetString(PyExc_TypeError, "parameter must be callable");
            throw Py::Exception();
        }

        Py_XINCREF(temp); /* Add a reference to new callback */
        std::string sSender = psSender;
        std::string sSignal = psSignal;

        if (!connect(psSender, psSignal, temp)) {
            // no signal object found => dispose the callback object
            Py_XDECREF(temp); /* Dispose of callback */
        }

        return Py::None();
    }

    // error set by PyArg_ParseTuple
    throw Py::Exception();
}

// ----------------------------------------------------

SignalConnect::SignalConnect(PyObject* res, PyObject* cb)
    : myResource(res)
    , myCallback(cb)
{}

SignalConnect::~SignalConnect()
{
    Base::PyGILStateLocker lock;
    Py_XDECREF(myCallback); /* Dispose of callback */
}

/**
 * Calls the callback function of the connected Python object.
 */
void SignalConnect::onExecute()
{
    PyObject* arglist;
    PyObject* result;

    /* Time to call the callback */
    arglist = Py_BuildValue("(O)", myResource);
    result = PyObject_CallObject(myCallback, arglist);
    Py_XDECREF(result);
    Py_DECREF(arglist);
}

#include "moc_WidgetFactory.cpp"
