/*
    Copyright 2012 Arno Rehn <arno@arnorehn.de>

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 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
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library.  If not, see <http://www.gnu.org/licenses/>.
*/

#ifndef LAMBDACONNECTION_H
#define LAMBDACONNECTION_H

#include <QByteArray>
#include <QObject>
#include <QVariant>

#include <ffi.h>

namespace {

template<typename T>
struct cleanup_handler;

struct any;

template<>
struct cleanup_handler<any> {
    virtual ~cleanup_handler() {}
};

template<typename T>
struct cleanup_handler : public cleanup_handler<any> {
    cleanup_handler(T* t) : t(t) {}

    virtual ~cleanup_handler() {
        delete t;
    }

    T* t;
};

template<typename T>
inline cleanup_handler<T> *create_cleanup_handler(T* t) {
    return new cleanup_handler<T>(t);
}

template<class R, class T, class... Args>
constexpr unsigned int arity(R (T::*)(Args...) const) {
    return sizeof...(Args);
}

template<class R, class T, class... Args>
constexpr unsigned int arity(R (T::*)(Args...)) {
    return sizeof...(Args);
}

template<class R, class... Args>
constexpr unsigned int arity(R(*)(Args...)) {
    return sizeof...(Args);
}

template<class T>
constexpr unsigned int arity(const T&) {
    return arity(&T::operator());
}

// used to determine the std::function template parameters for a lambda
template<class R, class T, class... Args>
inline std::function<R(Args...)> get_functional_type(R (T::*fun)(Args...) const) {
    return std::function<R(Args...)>();
}

// used to determine the std::function template parameters for a lambda
template<class R, class T, class... Args>
inline std::function<R(Args...)> get_functional_type(R (T::*fun)(Args...)) {
    return std::function<R(Args...)>();
}

// creates an std::function from any lambda
template<class T>
inline decltype(get_functional_type(&T::operator())) *get_functional(const T& t) {
    return new decltype(get_functional_type(&T::operator()))(t);
}

// creates an std::function from any function pointer
template<class R, class... Args>
inline std::function<R(Args...)> *get_functional(R (*fun)(Args...)) {
    return new std::function<R(Args...)>(fun);
}

ffi_type* ffiTypeFromTypeName(const QByteArray& typeName) {
    if (typeName.isEmpty() || typeName == "void") return &ffi_type_void;

    switch (QVariant::nameToType(typeName)) {
        case QVariant::Bool: return &ffi_type_uint8;
        case QVariant::Int: return &ffi_type_sint32;
        case QVariant::UInt: return &ffi_type_uint32;
        case QVariant::LongLong: return &ffi_type_sint64;
        case QVariant::ULongLong: return &ffi_type_uint64;
        case QVariant::Double: return &ffi_type_double;

        default: return &ffi_type_pointer;
    }
}

void ffiTypesFromMetaMethod(ffi_type ** args, uint nArgs, const QMetaMethod& method) {
    QList<QByteArray> types = method.parameterTypes();

    for (int i = 0; i < nArgs; ++i) {
        args[i] = ffiTypeFromTypeName(types.at(i));
    }
}

// one function to call them all...
template<class R, class... Args>
void callFunctional(const std::function<R(Args...)>& f, Args... args) {
    f(args...);
}

class LambdaConnection : public QObject {
public:
    template<class T>
    LambdaConnection(QObject *sender, const char *signal, const T& f)
        : QObject(sender), _arity(arity(f)), functor(0)
    {
        QByteArray ba = QMetaObject::normalizedSignature(signal + 1);
        int sigIndex = sender->metaObject()->indexOfMethod(ba.constData());
        if (sigIndex < 0) {
            qWarning("LambdaConnection: No such signal: '%s'", ba.constData());
            return;
        }

        static const int offset = staticMetaObject.methodCount();

        // create a copy of f and transform it to an std::function on the heap
        auto copy = get_functional(f);
        cleanup = create_cleanup_handler(copy); // don't leak memory
        functor = copy;

        opPtr = getFuncPointer(copy);

        args = new ffi_type*[_arity + 1];
        args[0] = &ffi_type_pointer;
        ffiTypesFromMetaMethod(args + 1, _arity, sender->metaObject()->method(sigIndex));

        if (ffi_prep_cif(&cif, FFI_DEFAULT_ABI, _arity + 1, &ffi_type_void, args) != FFI_OK) {
            qWarning("FFI preparation failed for signal '%s'", ba.constData());
        }

        QMetaObject::connect(sender, sigIndex, this, offset, Qt::DirectConnection, 0);

        values = new void*[_arity + 1];
        values[0] = &functor;
    }

    virtual ~LambdaConnection() {
        delete cleanup;
        delete args;
        delete values;
    }

    template<typename T>
    static bool connect(QObject *source, const char *signal, const T& lambda) {
        auto connection = new LambdaConnection(source, signal, lambda);
        return connection->functor;
    }

    int qt_metacall(QMetaObject::Call c, int id, void **a)
    {
        id = QObject::qt_metacall(c, id, a);
        if (id < 0)
            return id;

        if (c == QMetaObject::InvokeMetaMethod && id == 0) {
            memcpy(values + 1, a + 1, sizeof(void*) * _arity);
            ffi_call(&cif, opPtr, 0, values);
            id--;
        }

        return id;
    }

private:
    typedef void(*VoidFunc)();

    template<class R, class... Args>
    constexpr VoidFunc getFuncPointer(std::function<R(Args...)>*) {
        auto ptr = &callFunctional<R, Args...>;
        return reinterpret_cast<VoidFunc>(ptr);
    }

    const unsigned int _arity;
    cleanup_handler<any> *cleanup;
    void *functor;
    VoidFunc opPtr;

    ffi_cif cif;
    ffi_type **args;
    void **values;
};

}

#endif
