/************************************************************************
 *
 * Copyright 2010-2012 Jakob Leben (jakob.leben@gmail.com)
 *
 * This file is part of SuperCollider Qt GUI.
 *
 * 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 <http://www.gnu.org/licenses/>.
 *
 ************************************************************************/

#include "primitives.h"
#include "../QObjectProxy.h"
#include "../QcObjectFactory.h"
#include "../QcApplication.h"
#include "../Common.h"
#include "../type_codec.hpp"
#include "../metatype.hpp"
#include "../painting.h"

#include <PyrObject.h>
#include <PyrKernel.h>
#include <GC.h>
#include <SCBase.h>

#include <QMetaObject>
#include <QMetaProperty>
#include <QMetaMethod>

#if defined _WIN32
#    include "SC_Win32Utils.h"
#elif defined __FreeBSD__ || defined __OpenBSD__
#    include <stdlib.h>
#else
#    include <alloca.h>
#endif

#define IS_OBJECT_NIL(a) IsNil(slotRawObject(a)->slots)

#define QOBJECT_FROM_SLOT(s) ((QObjectProxy*)slotRawPtr(slotRawObject(s)->slots))

#define CLASS_NAME(slot) slotRawSymbol(&slotRawObject(slot)->classptr->name)->name

namespace QtCollider {

int QObject_Finalize(struct VMGlobals*, struct PyrObject*);

QC_LANG_PRIMITIVE(QObject_New, 2, PyrSlot* r, PyrSlot* a, VMGlobals* g) {
    if (NotSym(a + 0))
        return errWrongType;

    PyrObject* scObject = slotRawObject(r);
    QString qtClassName = QString(slotRawSymbol(a + 0)->name);

    qcSCObjectDebugMsg(1, scObject, QStringLiteral("CREATE: %2").arg(qtClassName));

    if (!QcApplication::compareThread())
        return QtCollider::wrongThreadError();

    QcAbstractFactory* f = QtCollider::factories().value(qtClassName);

    if (!f) {
        qcErrorMsg(QStringLiteral("Factory for class '%1' not found!").arg(qtClassName));
        return errFailed;
    }

    QObjectProxy* proxy = 0;

    MetaValue args[10];

    PyrSlot* argSlot = a + 1;
    int argc;

    if (isKindOfSlot(argSlot, class_array)) {
        PyrObject* array = slotRawObject(argSlot);
        argSlot = array->slots;
        argc = array->size;
    } else {
        argc = 1;
    }

    for (int i = 0; i < argc && i < 10; ++i) {
        PyrSlot* slot = argSlot + i;
        MetaType* type = MetaType::find(slot);
        if (type) {
            void* mem = alloca(type->size());
            Q_ASSERT(mem);
            args[i].read(mem, type, slot);
        }
    }

    proxy = f->newInstance(scObject, args);
    if (!proxy)
        return errFailed;

    SetPtr(scObject->slots, proxy);

    InstallFinalizer(g, scObject, 1, QObject_Finalize);

    return errNone;
}

QC_LANG_PRIMITIVE(QObject_Destroy, 0, PyrSlot* r, PyrSlot* a, VMGlobals* g) {
    qcSCObjectDebugMsg(1, slotRawObject(r), "DESTROY");

    QObjectProxy* proxy = QOBJECT_FROM_SLOT(r);

    if (!proxy->compareThread())
        return QtCollider::wrongThreadError();

    // Post the destruction event asynchronously;
    DestroyEvent* e = new DestroyEvent(QObjectProxy::DestroyObject);
    QApplication::postEvent(proxy, e);

    return errNone;
}

int QObject_Finalize(struct VMGlobals*, struct PyrObject* obj) {
    qcSCObjectDebugMsg(1, obj, "FINALIZE");

    QObjectProxy* proxy = (QObjectProxy*)slotRawPtr(obj->slots);

    // Invalidate proxy's SC object pointer directly.
    // Note that it is protected by language mutex;
    proxy->finalize();

    // Post the destruction event asynchronously;
    DestroyEvent* e = new DestroyEvent(QObjectProxy::DestroyProxyAndObject);
    QApplication::postEvent(proxy, e);

    return errNone;
}

QC_LANG_PRIMITIVE(QObject_SetParent, 1, PyrSlot* r, PyrSlot* a, VMGlobals* g) {
    QObjectProxy* proxy = QOBJECT_FROM_SLOT(r);

    QObjectProxy* parent = QtCollider::get(a);
    if (!parent)
        return errWrongType;

    qcSCObjectDebugMsg(1, slotRawObject(r), "SET PARENT");

    if (!proxy->compareThread())
        return QtCollider::wrongThreadError();

    bool ok = proxy->setParent(parent);

    return ok ? errNone : errFailed;
}

static void qcGetProperties(const QMetaObject* mo, PyrSlot* r, VMGlobals* g) {
    int count = mo->propertyCount();

    PyrObject* array = newPyrArray(g->gc, count, 0, true);
    SetObject(r, array);

    PyrSlot* s = array->slots;
    for (int i = 0; i < count; ++i, ++s) {
        SetSymbol(s, getsym(mo->property(i).name()));
        array->size++;
    }
}

static void qcGetMethods(const QMetaObject* mo, bool getPlain, bool getSignals, bool getSlots, PyrSlot* r,
                         VMGlobals* g) {
    int count = mo->methodCount();

    PyrObject* array = newPyrArray(g->gc, count, 0, true);
    SetObject(r, array);

    PyrSlot* s = array->slots;
    for (int i = 0; i < count; ++i) {
        QMetaMethod method = mo->method(i);
        switch (method.methodType()) {
        case QMetaMethod::Method:
            if (!getPlain || (method.access() != QMetaMethod::Public))
                continue;
            break;
        case QMetaMethod::Signal:
            if (!getSignals)
                continue;
            break;
        case QMetaMethod::Slot:
            if (!getSlots || (method.access() != QMetaMethod::Public))
                continue;
            break;
        default:
            continue;
        }
        QtCollider::set(s, QString::fromLatin1(method.methodSignature()));
        array->size++;
        g->gc->GCWrite(array, s);
        ++s;
    }
}

QC_LANG_PRIMITIVE(QMetaObject_Properties, 0, PyrSlot* r, PyrSlot* a, VMGlobals* g) {
    if (!QcApplication::compareThread())
        return QtCollider::wrongThreadError();

    PyrSlot* sClassName = slotRawObject(r)->slots + 0;
    if (NotSym(sClassName))
        return errWrongType;
    QString className(slotRawSymbol(sClassName)->name);

    QcAbstractFactory* f = QtCollider::factories().value(className);

    if (!f) {
        qcErrorMsg(QStringLiteral("Factory for class '%1' not found!").arg(className));
        return errFailed;
    }

    const QMetaObject* mo = f->metaObject();
    qcGetProperties(mo, r, g);

    return errNone;
}

QC_LANG_PRIMITIVE(QMetaObject_Methods, 3, PyrSlot* r, PyrSlot* a, VMGlobals* g) {
    if (!QcApplication::compareThread())
        return QtCollider::wrongThreadError();

    PyrSlot* sClassName = slotRawObject(r)->slots + 0;
    if (NotSym(sClassName))
        return errWrongType;
    QString className(slotRawSymbol(sClassName)->name);

    QcAbstractFactory* f = QtCollider::factories().value(className);

    if (!f) {
        qcErrorMsg(QStringLiteral("Factory for class '%1' not found!").arg(className));
        return errFailed;
    }

    bool getPlain = IsTrue(a + 0);
    bool getSignals = IsTrue(a + 1);
    bool getSlots = IsTrue(a + 2);

    const QMetaObject* mo = f->metaObject();
    qcGetMethods(mo, getPlain, getSignals, getSlots, r, g);

    return errNone;
}

QC_LANG_PRIMITIVE(QObject_GetProperties, 0, PyrSlot* r, PyrSlot* a, VMGlobals* g) {
    QObjectProxy* proxy = QOBJECT_FROM_SLOT(r);
    if (!proxy->compareThread())
        return QtCollider::wrongThreadError();

    QObject* obj = proxy->object();
    if (!obj) {
        SetNil(r);
        return errNone;
    }

    const QMetaObject* mo = obj->metaObject();
    qcGetProperties(mo, r, g);

    return errNone;
}

QC_LANG_PRIMITIVE(QObject_GetMethods, 3, PyrSlot* r, PyrSlot* a, VMGlobals* g) {
    QObjectProxy* proxy = QOBJECT_FROM_SLOT(r);
    if (!proxy->compareThread())
        return QtCollider::wrongThreadError();

    QObject* obj = proxy->object();
    if (!obj) {
        SetNil(r);
        return errNone;
    }

    bool getPlain = IsTrue(a + 0);
    bool getSignals = IsTrue(a + 1);
    bool getSlots = IsTrue(a + 2);

    const QMetaObject* mo = obj->metaObject();
    qcGetMethods(mo, getPlain, getSignals, getSlots, r, g);

    return errNone;
}

QC_LANG_PRIMITIVE(QObject_SetProperty, 3, PyrSlot* r, PyrSlot* a, VMGlobals* g) {
    QObjectProxy* proxy = QOBJECT_FROM_SLOT(r);

    if (NotSym(a))
        return errWrongType;
    PyrSymbol* property = slotRawSymbol(a);
    bool sync = IsTrue(a + 2);

    qcSCObjectDebugMsg(1, slotRawObject(r), QStringLiteral("SET: %1").arg(property->name));

    if (!proxy->compareThread())
        return QtCollider::wrongThreadError();

    QVariant val = QtCollider::get(a + 1);

    if (sync && !QtCollider::isPaintingObject(proxy)) {
        proxy->setProperty(property->name, val);
    } else {
        SetPropertyEvent* e = new SetPropertyEvent();
        e->property = property;
        e->value = val;
        QApplication::postEvent(proxy, e);
    }

    return errNone;
}

QC_LANG_PRIMITIVE(QObject_GetProperty, 1, PyrSlot* r, PyrSlot* a, VMGlobals* g) {
    QObjectProxy* proxy = QOBJECT_FROM_SLOT(r);

    if (NotSym(a))
        return errWrongType;
    PyrSymbol* symProp = slotRawSymbol(a);

    qcSCObjectDebugMsg(1, slotRawObject(r), QStringLiteral("GET: %1").arg(symProp->name));

    if (!proxy->compareThread())
        return QtCollider::wrongThreadError();

    QVariant val = proxy->property(symProp->name);
    if (!val.isValid()) {
        qcDebugMsg(1, QStringLiteral("WARNING: Invalid property '%1'").arg(symProp->name));
        SetNil(r);
        return errNone;
    }

    if (QtCollider::set(r, val))
        return errNone;
    else
        return errFailed;
}

QC_LANG_PRIMITIVE(QObject_SetEventHandler, 4, PyrSlot* r, PyrSlot* a, VMGlobals* g) {
    QObjectProxy* proxy = QOBJECT_FROM_SLOT(r);

    if (NotInt(a + 0) || NotSym(a + 1))
        return errWrongType;
    int eventType = QtCollider::get(a + 0);
    PyrSymbol* method = 0;
    slotSymbolVal(a + 1, &method);
    Synchronicity sync = IsTrue(a + 2) ? Synchronous : Asynchronous;
    bool enabled = IsTrue(a + 3);

    qcSCObjectDebugMsg(1, slotRawObject(r),
                       QStringLiteral("SET EVENT HANDLER: type %1 -> %2 [%3, %4]")
                           .arg(eventType)
                           .arg(method->name)
                           .arg(sync == Synchronous ? "SYNC" : "ASYNC")
                           .arg(enabled ? "on" : "off"));

    if (!proxy->compareThread())
        return QtCollider::wrongThreadError();

    bool ok = proxy->setEventHandler(eventType, method, sync, enabled);

    return ok ? errNone : errFailed;
}

QC_LANG_PRIMITIVE(QObject_SetEventHandlerEnabled, 2, PyrSlot* r, PyrSlot* a, VMGlobals* g) {
    if (NotInt(a + 0))
        return errWrongType;
    bool enabled = IsTrue(a + 1);
    if (!enabled && !IsFalse(a + 1))
        return errWrongType;
    int type = QtCollider::get(a + 0);

    qcSCObjectDebugMsg(1, slotRawObject(r),
                       QStringLiteral("SET EVENT HANDLER STATE: type %1 = %2").arg(type).arg(enabled ? "on" : "off"));

    QObjectProxy* proxy = QOBJECT_FROM_SLOT(r);

    if (!proxy->compareThread())
        return QtCollider::wrongThreadError();

    bool ok = proxy->setEventHandlerEnabled(type, enabled);

    return ok ? errNone : errFailed;
}

QC_LANG_PRIMITIVE(QObject_ConnectMethod, 3, PyrSlot* r, PyrSlot* a, VMGlobals* g) {
    if (NotSym(a + 0) || NotSym(a + 1))
        return errWrongType;

    PyrSymbol* signal = slotRawSymbol(a + 0);
    PyrSymbol* handler = slotRawSymbol(a + 1);
    Qt::ConnectionType ctype = IsTrue(a + 2) ? Qt::DirectConnection : Qt::QueuedConnection;

    qcSCObjectDebugMsg(1, slotRawObject(r),
                       QStringLiteral("CONNECT METHOD: %1 -> %2 [%3]")
                           .arg(signal->name)
                           .arg(handler->name)
                           .arg(IsTrue(a + 2) ? "SYNC" : "ASYNC"));

    QObjectProxy* proxy = QOBJECT_FROM_SLOT(r);

    if (!proxy->compareThread())
        return QtCollider::wrongThreadError();

    bool ok = proxy->connectMethod(signal->name, handler, ctype);

    return ok ? errNone : errFailed;
}

QC_LANG_PRIMITIVE(QObject_DisconnectMethod, 2, PyrSlot* r, PyrSlot* a, VMGlobals* g) {
    if (NotSym(a + 0) || NotSym(a + 1))
        return errWrongType;

    PyrSymbol* signal = slotRawSymbol(a + 0);
    PyrSymbol* handler = slotRawSymbol(a + 1);

    qcSCObjectDebugMsg(1, slotRawObject(r),
                       QStringLiteral("DISCONNECT METHOD: %1 -> %2").arg(signal->name).arg(handler->name));

    QObjectProxy* proxy = QOBJECT_FROM_SLOT(r);

    if (!proxy->compareThread())
        return QtCollider::wrongThreadError();

    bool ok = proxy->disconnectMethod(signal->name, handler);

    return ok ? errNone : errFailed;
}

QC_LANG_PRIMITIVE(QObject_ConnectObject, 3, PyrSlot* r, PyrSlot* a, VMGlobals* g) {
    if (NotSym(a + 0) || NotObj(a + 1))
        return errWrongType;

    PyrSymbol* signal = slotRawSymbol(a + 0);
    PyrObject* handlerObj = slotRawObject(a + 1);
    Qt::ConnectionType ctype = IsTrue(a + 2) ? Qt::DirectConnection : Qt::QueuedConnection;

    qcSCObjectDebugMsg(1, slotRawObject(r),
                       QStringLiteral("CONNECT OBJECT: %1 -> %2 [%3]")
                           .arg(signal->name)
                           .arg(slotRawSymbol(&handlerObj->classptr->name)->name)
                           .arg(IsTrue(a + 2) ? "SYNC" : "ASYNC"));

    QObjectProxy* proxy = QOBJECT_FROM_SLOT(r);

    if (!proxy->compareThread())
        return QtCollider::wrongThreadError();

    bool ok = proxy->connectObject(signal->name, handlerObj, ctype);

    return ok ? errNone : errFailed;
}

QC_LANG_PRIMITIVE(QObject_DisconnectObject, 2, PyrSlot* r, PyrSlot* a, VMGlobals* g) {
    if (NotSym(a + 0) || NotObj(a + 1))
        return errWrongType;

    PyrSymbol* signal = slotRawSymbol(a + 0);
    PyrObject* handlerObj = slotRawObject(a + 1);

    qcSCObjectDebugMsg(1, slotRawObject(r), QStringLiteral("DISCONNECT OBJECT: %1").arg(signal->name));

    QObjectProxy* proxy = QOBJECT_FROM_SLOT(r);

    if (!proxy->compareThread())
        return QtCollider::wrongThreadError();

    bool ok = proxy->disconnectObject(signal->name, handlerObj);

    return ok ? errNone : errFailed;
}

QC_LANG_PRIMITIVE(QObject_ConnectSlot, 3, PyrSlot* r, PyrSlot* a, VMGlobals* g) {
    // Args: signal, receiver, slot
    if (!isKindOfSlot(a + 1, SC_CLASS(QObject)) || NotSym(a + 0) || NotSym(a + 2))
        return errWrongType;

    PyrSymbol* symSig = slotRawSymbol(a + 0);
    PyrSymbol* symSlot = slotRawSymbol(a + 2);
    QObjectProxy* sndProxy = QOBJECT_FROM_SLOT(r);
    QObjectProxy* rcvProxy = QOBJECT_FROM_SLOT(a + 1);

    qcSCObjectDebugMsg(1, slotRawObject(r),
                       QStringLiteral("CONNECT TO SLOT: %1 -> %2").arg(symSig->name).arg(symSlot->name));

    QString strSig = QStringLiteral("2") + symSig->name;
    QString strSlot = QStringLiteral("1") + symSlot->name;

    sndProxy->lock();
    rcvProxy->lock();
    bool ok;
    if (!sndProxy->object() || !rcvProxy->object()) {
        ok = true;
    } else {
        ok = QObject::connect(sndProxy->object(), strSig.toStdString().c_str(), rcvProxy->object(),
                              strSlot.toStdString().c_str());
    }
    sndProxy->unlock();
    rcvProxy->unlock();

    if (!ok) {
        qcErrorMsg(QStringLiteral("Failed to connect %1::%2 to %3::%4!\n")
                       .arg(sndProxy->scClassName())
                       .arg(symSig->name)
                       .arg(rcvProxy->scClassName())
                       .arg(symSlot->name));
        return errFailed;
    }

    return errNone;
}

QC_LANG_PRIMITIVE(QObject_InvokeMethod, 3, PyrSlot* r, PyrSlot* a, VMGlobals* g) {
    if (NotSym(a + 0))
        return errWrongType;

    PyrSymbol* method = slotRawSymbol(a + 0);
    PyrSlot* methodArgs = a + 1;
    bool sync = !IsFalse(a + 2);

    qcSCObjectDebugMsg(1, slotRawObject(r),
                       QStringLiteral("INVOKE: '%1' [%2]").arg(method->name).arg(sync ? "SYNC" : "ASYNC"));

    QObjectProxy* proxy = QOBJECT_FROM_SLOT(r);

    PyrSlot* retSlot;
    Qt::ConnectionType cType;

    if (sync) {
        if (!proxy->compareThread())
            return QtCollider::wrongThreadError();
        retSlot = r;
        cType = Qt::DirectConnection;
    } else {
        retSlot = 0;
        cType = Qt::QueuedConnection;
    }

    bool ok = proxy->invokeMethod(method->name, retSlot, methodArgs, cType);

    return ok ? errNone : errFailed;
}

QC_LANG_PRIMITIVE(QObject_IsValid, 0, PyrSlot* r, PyrSlot* a, VMGlobals* g) {
    QObjectProxy* proxy = QOBJECT_FROM_SLOT(r);

    bool needLock = !proxy->compareThread();

    if (needLock)
        proxy->lock();

    bool hasObject = proxy->object() != 0;

    if (needLock)
        proxy->unlock();

    SetBool(r, hasObject);

    return errNone;
}

QC_LANG_PRIMITIVE(QObject_GetChildren, 1, PyrSlot* r, PyrSlot* a, VMGlobals* g) {
    if (NotSym(a) && NotNil(a))
        return errWrongType;

    QObjectProxy* proxy = QOBJECT_FROM_SLOT(r);
    PyrSymbol* className = IsSym(a) ? slotRawSymbol(a) : 0;

    qcSCObjectDebugMsg(1, slotRawObject(r),
                       QStringLiteral("GET CHILDREN: of class '%1'").arg(className ? className->name : "QObject"));

    if (!proxy->compareThread())
        return QtCollider::wrongThreadError();

    QList<PyrObject*> children = proxy->children(className);

    int count = children.count();
    PyrObject* array = newPyrArray(g->gc, count, 0, true);
    SetObject(r, array);

    PyrSlot* s = array->slots;
    Q_FOREACH (PyrObject* obj, children) {
        SetObject(s, obj);
        ++array->size;
        g->gc->GCWrite(array, s);
        ++s;
    }

    return errNone;
}

QC_LANG_PRIMITIVE(QObject_GetParent, 1, PyrSlot* r, PyrSlot* a, VMGlobals* g) {
    if (NotSym(a) && NotNil(a))
        return errWrongType;

    QObjectProxy* proxy = QOBJECT_FROM_SLOT(r);
    PyrSymbol* className = IsSym(a) ? slotRawSymbol(a) : 0;

    qcSCObjectDebugMsg(1, slotRawObject(r), QStringLiteral("GET PARENT"));

    if (!proxy->compareThread())
        return QtCollider::wrongThreadError();

    PyrObject* parent = proxy->parent(className);

    if (parent)
        SetObject(r, parent);
    else
        SetNil(r);

    return errNone;
}

void defineQObjectPrimitives() {
    LangPrimitiveDefiner definer;
    definer.define<QObject_New>();
    definer.define<QObject_Destroy>();
    definer.define<QObject_SetParent>();
    definer.define<QMetaObject_Properties>();
    definer.define<QMetaObject_Methods>();
    definer.define<QObject_GetProperties>();
    definer.define<QObject_GetMethods>();
    definer.define<QObject_SetProperty>();
    definer.define<QObject_GetProperty>();
    definer.define<QObject_SetEventHandler>();
    definer.define<QObject_SetEventHandlerEnabled>();
    definer.define<QObject_ConnectMethod>();
    definer.define<QObject_DisconnectMethod>();
    definer.define<QObject_ConnectObject>();
    definer.define<QObject_DisconnectObject>();
    definer.define<QObject_ConnectSlot>();
    definer.define<QObject_InvokeMethod>();
    definer.define<QObject_IsValid>();
    definer.define<QObject_GetChildren>();
    definer.define<QObject_GetParent>();
}

} // namespace QtCollider
