/***************************************************************************
 * script.cpp
 * This file is part of the KDE project
 * copyright (C)2007-2008 by Sebastian Sauer (mail@dipe.org)
 *
 * This program 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 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
 * Library General Public License for more details.
 * You should have received a copy of the GNU Library General Public License
 * along with this program; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 ***************************************************************************/

#include "script.h"
#include "kross_qtscript_debug.h"

#include <QMetaObject>
#include <QMetaMethod>
#include <QScriptEngine>
#include <QScriptValueIterator>

using namespace Kross;

namespace Kross
{

/// \internal private d-pointer class.
class EcmaScript::Private
{
public:
    EcmaScript *m_script;
    QScriptEngine *m_engine;
    QScriptValue m_kross;
    QScriptValue m_self;

    explicit Private(EcmaScript *script) : m_script(script), m_engine(nullptr) {}
    ~Private()
    {
        delete m_engine;
    }

    bool init()
    {
        if (m_script->action()->hadError()) {
            m_script->action()->clearError();
        }

        delete m_engine;
        m_engine = new QScriptEngine();
        m_engine->installTranslatorFunctions();

        // load the Kross QScriptExtensionPlugin plugin that provides
        // us a bridge between Kross and QtScript. See here plugin.h
        m_engine->importExtension("kross");
        if (m_engine->hasUncaughtException()) {
            handleException();
            delete m_engine;
            m_engine = nullptr;
            return false;
        }

        // the Kross QScriptExtensionPlugin exports the "Kross" property.
        QScriptValue global = m_engine->globalObject();
        m_kross = global.property("Kross");
        Q_ASSERT(m_kross.isQObject());
        Q_ASSERT(! m_engine->hasUncaughtException());

        // Attach our Kross::Action instance to be able to access it in
        // scripts. Just like at the Kjs-backend we publish our own
        // action as "self".
        m_self = m_engine->newQObject(m_script->action());
        global.setProperty("self", m_self, QScriptValue::ReadOnly | QScriptValue::Undeletable);

        {
            // publish the global objects.
            QHash< QString, QObject * > objects = Manager::self().objects();
            QHash< QString, QObject * >::Iterator it(objects.begin()), end(objects.end());
            for (; it != end; ++it) {
                global.setProperty(it.key(), m_engine->newQObject(it.value()));
            }
        }

        {
            // publish the local objects.
            QHash< QString, QObject * > objects = m_script->action()->objects();
            QHash< QString, QObject * >::Iterator it(objects.begin()), end(objects.end());
            for (; it != end; ++it) {
                copyEnumsToProperties(it.value());
                global.setProperty(it.key(), m_engine->newQObject(it.value()));
            }
        }

        return ! m_engine->hasUncaughtException();
    }

    void copyEnumsToProperties(QObject *object)
    {
        const QMetaObject *meta = object->metaObject();
        for (int i = 0; i < meta->enumeratorCount(); ++i) {
            QMetaEnum metaenum = meta->enumerator(i);
            for (int j = 0; j < metaenum.keyCount(); ++j) {
                object->setProperty(metaenum.key(j), metaenum.value(j));
            }
        }
    }

    void handleException()
    {
        Q_ASSERT(m_engine);
        Q_ASSERT(m_engine->hasUncaughtException());
        const QString err = m_engine->uncaughtException().toString();
        const int linenr = m_engine->uncaughtExceptionLineNumber();
        const QString trace = m_engine->uncaughtExceptionBacktrace().join("\n");
        qCDebug(KROSS_QTSCRIPT_LOG) << QStringLiteral("%1, line:%2, backtrace:\n%3").arg(err).arg(linenr).arg(trace);
        m_script->action()->setError(err, trace, linenr);
        m_engine->clearExceptions();
    }

    void addObject(QObject *object, const QString &name = QString())
    {
        Q_ASSERT(m_engine);
        Q_ASSERT(! m_engine->hasUncaughtException());
        QScriptValue global = m_engine->globalObject();
        QScriptValue value = m_engine->newQObject(object);
        global.setProperty(name.isEmpty() ? object->objectName() : name, value);
    }

    void connectFunctions(ChildrenInterface *children)
    {
        Q_ASSERT(m_engine);
        Q_ASSERT(! m_engine->hasUncaughtException());
        QString eval;
        QScriptValue global = m_engine->globalObject();
        QHashIterator< QString, ChildrenInterface::Options > it(children->objectOptions());
        while (it.hasNext()) {
            it.next();
            if (it.value() & ChildrenInterface::AutoConnectSignals) {
                QObject *sender = children->object(it.key());
                if (! sender) {
                    continue;
                }
                QScriptValue obj = m_engine->globalObject().property(it.key());
                if (! obj.isQObject()) {
                    continue;
                }
                const QMetaObject *mo = sender->metaObject();
                const int count = mo->methodCount();
                for (int i = 0; i < count; ++i) {
                    QMetaMethod mm = mo->method(i);
                    const QString signature = mm.methodSignature();
                    const QString name = signature.left(signature.indexOf('('));
                    if (mm.methodType() == QMetaMethod::Signal) {
                        QScriptValue func = global.property(name);
                        if (! func.isFunction()) {
                            //krossdebug( QString("EcmaScript::connectFunctions No function to connect with %1.%2").arg(it.key()).arg(name) );
                            continue;
                        }
                        qCDebug(KROSS_QTSCRIPT_LOG) << "EcmaScript::connectFunctions Connecting with " <<
                            it.key() << "." << name;
                        eval += QString("try { %1.%2.connect(%3); } catch(e) { print(e); }\n").arg(it.key()).arg(name).arg(name);
                    }
                }
            }
        }
        Q_ASSERT(! m_engine->hasUncaughtException());
        if (! eval.isNull()) {
            m_engine->evaluate(eval);
            Q_ASSERT(! m_engine->hasUncaughtException());
        }
    }

};

}

EcmaScript::EcmaScript(Interpreter *interpreter, Action *action) : Script(interpreter, action), d(new Private(this))
{
    //krossdebug( QString("EcmaScript::EcmaScript") );
}

EcmaScript::~EcmaScript()
{
    //krossdebug( QString("EcmaScript::~EcmaScript") );
    delete d;
}

void EcmaScript::execute()
{
    if (! d->init()) {
        d->handleException();
        return;
    }

    QString scriptCode = action()->code();
    if (scriptCode.startsWith(QLatin1String("#!"))) { // remove optional shebang-line
        scriptCode.remove(0, scriptCode.indexOf('\n'));
    }

    const QString fileName = action()->file().isEmpty() ? action()->name() : action()->file();

    //krossdebug( QString("EcmaScript::execute fileName=%1 scriptCode=\n%2").arg(fileName).arg(scriptCode) );

    Q_ASSERT(d->m_engine);

    if (d->m_engine->hasUncaughtException()) {
        d->m_engine->clearExceptions();
    }

    d->m_engine->evaluate(scriptCode, fileName);

    if (d->m_engine->hasUncaughtException()) {
        d->handleException();
        return;
    }

    //d->connectFunctions( &Manager::self() );
    d->connectFunctions(action());
}

QStringList EcmaScript::functionNames()
{
    if (! d->m_engine && ! d->init()) {
        d->handleException();
        return QStringList();
    }
    QStringList names;
    QScriptValueIterator it(d->m_engine->globalObject());
    while (it.hasNext()) {
        it.next();
        if (it.value().isFunction()) {
            names << it.name();
        }
    }
    return names;
}

QVariant EcmaScript::callFunction(const QString &name, const QVariantList &args)
{
    if (! d->m_engine && ! d->init()) {
        d->handleException();
        return QVariant();
    }

    QScriptValue obj = d->m_engine->globalObject();
    QScriptValue function = obj.property(name);
    if (! function.isFunction()) {
        QString err = QString("No such function '%1'").arg(name);
        qCWarning(KROSS_QTSCRIPT_LOG) << "EcmaScript::callFunction " << err;
        setError(err);
        return QVariant();
    }

    QScriptValueList arguments;
    foreach (const QVariant &v, args) {
        arguments << d->m_engine->toScriptValue(v);
    }
    QScriptValue result = function.call(obj, arguments);
    if (d->m_engine->hasUncaughtException()) {
        d->handleException();
        return QVariant();
    }
    return result.toVariant();
}

QVariant EcmaScript::evaluate(const QByteArray &code)
{
    if (! d->m_engine && ! d->init()) {
        d->handleException();
        return QVariant();
    }

    QScriptValue result = d->m_engine->evaluate(code);
    if (d->m_engine->hasUncaughtException()) {
        d->handleException();
        return QVariant();
    }
    return result.toVariant();
}

QObject *EcmaScript::engine() const
{
    return d->m_engine;
}

