/*****************************************************************************
* luaEngine Lua Engine for Qt
* Copyright (C) 2018-2021 Syping
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/

#define LUA_LIB
#include "LuaEngine.h"
#include "LuaEngineRegistry.h"
#include <QCoreApplication>
#include <QTextStream>
#include <QMetaMethod>
LuaEngine::LuaEngine(QObject *parent, bool loadBaseLibraries) : QObject(parent)
{
    LuaEngine(LuaEngineType::UnknownEngineType, parent, loadBaseLibraries);
}

LuaEngine::LuaEngine(LuaEngineType engineType, QObject *parent, bool loadBaseLibraries) : QObject(parent), p_engineType(engineType)
{
    L = luaL_newstate();
    if (loadBaseLibraries)
        luaL_openlibs(L);
    engineRegistry->registerEngine((void*)L, (void*)this);

    pushVariant("DeleteInstant", 0);
    pushVariant("DeleteLater", 1);
    pushVariant("CliExecuted", "CliExecuted");
    pushFunction("delete", luaObjectDelete_p);
    pushFunction("connect", luaTriggerConnect_p);
    pushFunction("disconnect", luaTriggerDisconnect_p);
    pushFunction("getObjectParent", luaObjectGetParent_p);
    pushFunction("setObjectParent", luaObjectSetParent_p);
    pushFunction("luaEngineQuit", luaEngineQuit_p);
    pushFunction("luaEngineVersion", luaEngineVersion_p);
    pushFunction("luaEnginePlatform", luaEnginePlatform_p);
}

LuaEngine::~LuaEngine()
{
    engineRegistry->unregisterEngine(L);
    lua_close(L);
}

lua_State* LuaEngine::luaState()
{
    return L;
}

LuaEngine::LuaEngineType LuaEngine::engineType()
{
    return p_engineType;
}

void LuaEngine::loadBaseLibraries()
{
    luaL_openlibs(L);
}

int LuaEngine::luaEngineWriter_p(lua_State *L_p, const void *buffer, size_t size, void *array)
{
    Q_UNUSED(L_p)
    ((QByteArray*)array)->append(QByteArray(static_cast<const char*>(buffer), (int)size));
    return 0;
}

int LuaEngine::luaEngineQuit_p(lua_State *L_p)
{
    int argumentCount = getArgumentCount(L_p);
    if (argumentCount == 1) {
        bool ok;
        int retcode = getVariant(L_p, 1).toInt(&ok);
        if (ok) {
            QCoreApplication::exit(retcode);
            return 0;
        }
    }
    QCoreApplication::quit();
    return 0;
}

int LuaEngine::luaEngineVersion_p(lua_State *L_p)
{
    pushVariant(L_p, "0.1");
    return 1;
}

int LuaEngine::luaEnginePlatform_p(lua_State *L_p)
{
#ifdef Q_OS_ANDROID
    pushVariant(L_p, "Android");
#elif defined(Q_OS_LINUX)
    pushVariant(L_p, "Linux");
#elif defined(Q_OS_FREEBSD)
    pushVariant(L_p, "FreeBSD");
#elif defined(Q_OS_OPENBSD)
    pushVariant(L_p, "OpenBSD");
#elif defined(Q_OS_MACOS)
    pushVariant(L_p, "macOS");
#elif defined(Q_OS_DARWIN)
    pushVariant(L_p, "Darwin");
#elif defined(Q_OS_UNIX)
    pushVariant(L_p, "Unix");
#elif defined(Q_OS_WIN)
    pushVariant(L_p, "Windows");
#else
    pushVariant(L_p, "Unknown");
#endif
    return 1;
}

QByteArray LuaEngine::dumpLuaScript()
{
    QByteArray array;
    lua_lock(L);
    lua_dump(L, luaEngineWriter_p, (void*)&array, 1);
    lua_unlock(L);
    return array;
}

bool LuaEngine::loadLuaScript(const QByteArray &data)
{
    int result = luaL_loadbuffer(L, data.data(), data.size(), "script");
    return (result == 0) ? true : false;
}

bool LuaEngine::loadLuaScript(QIODevice *device, bool closeDevice)
{
    QByteArray data;
    if (!device->isOpen()) {
        if (device->open(QIODevice::ReadOnly)) {
            data = device->readAll();
            if (closeDevice)
                device->close();
            return loadLuaScript(data);
        }
    }
    else {
        data = device->readAll();
        if (closeDevice)
            device->close();
        return loadLuaScript(data);
    }
    return false;
}

bool LuaEngine::executeLuaScript(const QByteArray &data)
{
    if (loadLuaScript(data))
        return (lua_pcall(L, 0, LUA_MULTRET, 0) == 0);
    return false;
}

bool LuaEngine::executeLuaScript(QIODevice *device, bool closeDevice)
{
    QByteArray data;
    if (!device->isOpen()) {
        if (device->open(QIODevice::ReadOnly)) {
            data = device->readAll();
            if (closeDevice)
                device->close();
            return executeLuaScript(data);
        }
    }
    else {
        data = device->readAll();
        if (closeDevice)
            device->close();
        return executeLuaScript(data);
    }
    return false;
}

bool LuaEngine::executeLuaFunction(const char *name, bool requireReturn)
{
    return executeLuaFunction(L, name, requireReturn);
}

bool LuaEngine::executeLuaFunction(lua_State *L_p, const char *name, bool requireReturn)
{
    int returnCount = (requireReturn) ? LUA_MULTRET : 0;
    lua_getglobal(L_p, name);
    return (lua_pcall(L_p, 0, returnCount, 0) == 0);
}

bool LuaEngine::executeLuaFunction(const char *name, const QVariant &argument, bool requireReturn)
{
    return executeLuaFunction(L, name, argument, requireReturn);
}

bool LuaEngine::executeLuaFunction(lua_State *L_p, const char *name, const QVariant &argument, bool requireReturn)
{
    int returnCount = (requireReturn) ? LUA_MULTRET : 0;
    lua_getglobal(L_p, name);
    pushVariant(L_p, argument);
    return (lua_pcall(L_p, 1, returnCount, 0) == 0);
}

bool LuaEngine::executeLuaFunction(const char *name, const QVariantList &args, bool requireReturn)
{
    return executeLuaFunction(L, name, args, requireReturn);
}

bool LuaEngine::executeLuaFunction(lua_State *L_p, const char *name, const QVariantList &args, bool requireReturn)
{
    int returnCount = (requireReturn) ? LUA_MULTRET : 0;
    lua_getglobal(L_p, name);
    for (const QVariant &argument : args) {
        pushVariant(L_p, argument);
    }
    return (lua_pcall(L_p, args.count(), returnCount, 0) == 0);
}

void LuaEngine::pushFunction(const char *name, lua_CFunction function)
{
    pushFunction(L, name, function);
}

void LuaEngine::pushFunction(lua_State *L_p, const char *name, lua_CFunction function)
{
    lua_pushcfunction(L_p, function);
    lua_setglobal(L_p, name);
}

void LuaEngine::pushPointer(const char *name, void *pointer)
{
    pushPointer(L, name, pointer);
}

void LuaEngine::pushPointer(lua_State *L_p, const char *name, void *pointer)
{
    pushPointer(L_p, pointer);
    lua_setglobal(L_p, name);
}

void LuaEngine::pushPointer(void *pointer)
{
    pushPointer(L, pointer);
}

void LuaEngine::pushPointer(lua_State *L_p, void *pointer)
{
    lua_pushlightuserdata(L_p, pointer);
}

void LuaEngine::pushVariant(const char *name, const QVariant &variant)
{
    pushVariant(L, name, variant);
}

void LuaEngine::pushVariant(lua_State *L_p, const char *name, const QVariant &variant)
{
    pushVariant(L_p, variant);
    lua_setglobal(L_p, name);
}

void LuaEngine::pushVariant(const QVariant &variant)
{
    pushVariant(L, variant);
}

void LuaEngine::pushVariant(lua_State *L_p, const QVariant &variant)
{
    if (variant.type() == QVariant::Bool) {
        lua_pushboolean(L_p, (int)variant.toBool());
    }
    else if (variant.type() == QVariant::Int) {
        lua_pushinteger(L_p, variant.toInt());
    }
    else if (variant.type() == QVariant::Double) {
        lua_pushnumber(L_p, variant.toDouble());
    }
    else if (variant.type() == QVariant::String) {
        lua_pushstring(L_p, variant.toString().toUtf8().data());
    }
    else if (variant.type() == QVariant::StringList) {
        QStringList stringList = variant.toStringList();
        lua_createtable(L_p, 0, stringList.count());
        int currentId = 1;
        for (const QString &string : qAsConst(stringList)) {
            lua_pushinteger(L_p, currentId);
            lua_pushstring(L_p, string.toUtf8().data());
            lua_settable(L_p, -3);
            currentId++;
        }
    }
    else if ((QMetaType::Type)variant.type() == QMetaType::QVariantList) {
        QVariantList variantList = variant.toList();
        lua_createtable(L_p, 0, variantList.count());
        int currentId = 1;
        for (const QVariant &variant : qAsConst(variantList)) {
            lua_pushinteger(L_p, currentId);
            pushVariant(L_p, variant);
            lua_settable(L_p, -3);
            currentId++;
        }
    }
    else if ((QMetaType::Type)variant.type() == QMetaType::QVariantMap) {
        QVariantMap variantMap = variant.toMap();
        lua_createtable(L_p, 0, variantMap.count());
        for (auto it = variantMap.constBegin(); it != variantMap.constEnd(); it++) {
            lua_pushstring(L_p, it.key().toUtf8().data());
            pushVariant(L_p, it.value());
            lua_settable(L_p, -3);
        }
    }
    else if ((QMetaType::Type)variant.type() == QMetaType::Void || (QMetaType::Type)variant.type() == QMetaType::VoidStar) {
        lua_pushlightuserdata(L_p, variant.value<void*>());
    }
    else {
        lua_pushnil(L_p);
    }
}

QVariant LuaEngine::getVariant(const char *name)
{
    lua_getglobal(L, name);
    return returnVariant();
}

QVariant LuaEngine::getVariant(lua_State *L_p, const char *name)
{
    lua_getglobal(L_p, name);
    return returnVariant(L_p);
}

QVariant LuaEngine::getVariant(int index)
{
    return getVariant(L, index);
}

QVariant LuaEngine::getVariant(lua_State *L_p, int index)
{
    if (lua_isboolean(L_p, index)) {
        return QVariant::fromValue((bool)lua_toboolean(L_p, index));
    }
    else if (lua_isinteger(L_p, index)) {
        return QVariant::fromValue(lua_tointeger(L_p, index));
    }
    else if (lua_isnumber(L_p, index)) {
        return QVariant::fromValue(lua_tonumber(L_p, index));
    }
    else if (lua_isstring(L_p, index)) {
        return QVariant::fromValue(QString(lua_tostring(L_p, index)));
    }
    else if (lua_istable(L_p, index)) {
        QVariantMap variantMap;
        lua_pushvalue(L_p, index);
        lua_pushnil(L_p);
        while (lua_next(L_p, -2) != 0) {
            lua_pushvalue(L_p, -2);
            QString key = QString(lua_tostring(L_p, -1));
            QVariant value = getVariant(L_p, -2);
            variantMap.insert(key, value);
            lua_pop(L_p, 2);
        }
        lua_pop(L_p, 1);
        return QVariant::fromValue(variantMap);
    }
    else if (lua_isuserdata(L_p, index)) {
        return QVariant::fromValue(lua_touserdata(L_p, index));
    }
    else if (lua_isnoneornil(L_p, index)) {
        return QVariant();
    }
    QTextStream(stderr) << "Warning: Didn't catch lua_isnoneornil before empty QVariant got returned" << Qt::endl;
    return QVariant();
}

void* LuaEngine::returnPointer()
{
    return returnPointer(L);
}

void* LuaEngine::returnPointer(lua_State *L_p)
{
    return getPointer(L_p, -1);
}

void* LuaEngine::getPointer(const char *name)
{
    lua_getglobal(L, name);
    return returnPointer();
}

void* LuaEngine::getPointer(lua_State *L_p, const char *name)
{
    lua_getglobal(L_p, name);
    return returnPointer(L_p);
}

void* LuaEngine::getPointer(int index)
{
    return getPointer(L, index);
}

void* LuaEngine::getPointer(lua_State *L_p, int index)
{
    return lua_touserdata(L_p, index);
}

QVariant LuaEngine::returnVariant()
{
    return returnVariant(L);
}

QVariant LuaEngine::returnVariant(lua_State *L_p)
{
    return getVariant(L_p, -1);
}

QVariantList LuaEngine::getArguments(lua_State *L_p)
{
    QVariantList arguments;
    int argumentCount = getArgumentCount(L_p);
    for (int i = 1; i < (argumentCount + 1); i++) {
        arguments << getVariant(L_p, i);
    }
    return arguments;
}

int LuaEngine::getArgumentCount(lua_State *L_p)
{
    return lua_gettop(L_p);
}

int LuaEngine::luaObjectDelete_p(lua_State *L_p)
{
    if (getArgumentCount(L_p) >= 1) {
        void *pointer = getPointer(L_p, 1);
        if (pointer != NULL) {
            switch (getVariant(L_p, 2).toInt()) {
            case 1:
                ((QObject*)pointer)->deleteLater();
                break;
            default:
                delete ((QObject*)pointer);
            }
        }
    }
    return 0;
}

int LuaEngine::luaObjectGetParent_p(lua_State *L_p)
{
    if (getArgumentCount(L_p) >= 1) {
        void *pointer = getPointer(L_p, 1);
        if (pointer != NULL) {
            pushPointer(L_p, ((QObject*)pointer)->parent());
            return 1;
        }
    }
    return 0;
}

int LuaEngine::luaObjectSetParent_p(lua_State *L_p)
{
    if (getArgumentCount(L_p) >= 2) {
        void *o_pointer = getPointer(L_p, 1);
        void *p_pointer = getPointer(L_p, 2);
        if (o_pointer != NULL && p_pointer != NULL && ((QObject*)o_pointer)->inherits("QObject") && ((QObject*)p_pointer)->inherits("QObject")) {
            ((QObject*)o_pointer)->setParent((QObject*)p_pointer);
        }
    }
    return 0;
}

int LuaEngine::luaTriggerConnect_p(lua_State *L_p)
{
    if (getArgumentCount(L_p) >= 3) {
        void *pointer = getPointer(L_p, 1);
        if (pointer != NULL) {
            QObject *object = (QObject*)pointer;
            QString signalString = getVariant(L_p, 2).toString();
            int signalIndex = object->metaObject()->indexOfSignal(signalString.toUtf8().data());
            if (signalIndex != -1) {
                LuaEngine *engine = (LuaEngine*)engineRegistry->getEngine(L_p);
                int slotIndex = engine->metaObject()->indexOfSlot("luaTriggerSlot_p()");
                if (slotIndex != -1) {
                    QMetaMethod signal = object->metaObject()->method(signalIndex);
                    QMetaMethod slot = engine->metaObject()->method(slotIndex);
                    QString funcStorage;
                    QTextStream(&funcStorage) << "__ConnectFunc_" << object << "_" << signal.name();
                    engine->setProperty(funcStorage.toUtf8().data(), getVariant(L_p, 3));
                    QObject::connect(object, signal, engine, slot);
                }
            }
        }
    }
    return 0;
}

int LuaEngine::luaTriggerDisconnect_p(lua_State *L_p)
{
    if (getArgumentCount(L_p) >= 2) {
        void *pointer = getPointer(L_p, 1);
        if (pointer != NULL) {
            QObject *object = (QObject*)pointer;
            QString signalString = getVariant(L_p, 2).toString();
            int signalIndex = object->metaObject()->indexOfSignal(signalString.toUtf8().data());
            if (signalIndex != -1) {
                LuaEngine *engine = (LuaEngine*)engineRegistry->getEngine(L_p);
                int slotIndex = engine->metaObject()->indexOfSlot("luaTriggerSlot_p()");
                if (slotIndex != -1) {
                    QMetaMethod signal = object->metaObject()->method(signalIndex);
                    QMetaMethod slot = engine->metaObject()->method(slotIndex);
                    QString funcStorage;
                    QTextStream(&funcStorage) << "__ConnectFunc_" << object << "_" << signal.name();
                    engine->setProperty(funcStorage.toUtf8().data(), QVariant());
                    QObject::disconnect(object, signal, engine, slot);
                }
            }
        }
    }
    return 0;
}

void LuaEngine::luaTriggerSlot_p()
{
    QMetaMethod signal = sender()->metaObject()->method(senderSignalIndex());
    QString funcStorage;
    QTextStream(&funcStorage) << "__ConnectFunc_" << sender() << "_" << signal.name();
    QString luaConnectFunc = property(funcStorage.toUtf8().data()).toString();
    executeLuaFunction(luaConnectFunc.toUtf8().data(), QVariant::fromValue((void*)sender()));
}