#include "ASVariableFormatter.hpp"
#include <iostream>

DebugAdapterProtocol::Variable ASVariableFormatter::GetAsDAPVariable(asIScriptEngine* engine, asIScriptContext* ctx,
                                                                     AngelscriptDebugger* debugger,
                                                                     const std::string& name, int type, void* address) {
    if ((type & asTYPEID_OBJHANDLE) != 0) {
        address = *static_cast<void**>(address);
    }
    switch (type) {
        case asTYPEID_BOOL: {
            auto v = *static_cast<bool*>(address);
            return {name, v ? "true" : "false", "bool"};
        };

        case asTYPEID_INT8: {
            auto v = *static_cast<int8_t*>(address);
            return {name, std::to_string(v), "int8"};
        };
        case asTYPEID_INT16: {
            auto v = *static_cast<int16_t*>(address);
            return {name, std::to_string(v), "int16"};
        }
        case asTYPEID_INT32: {
            auto v = *static_cast<int32_t*>(address);
            return {name, std::to_string(v), "int32"};
        };
        case asTYPEID_INT64: {
            auto v = *static_cast<int64_t*>(address);
            return {name, std::to_string(v), "int64"};
        };

        case asTYPEID_UINT8: {
            auto v = *static_cast<uint8_t*>(address);
            return {name, std::to_string(v), "uint8"};
        };
        case asTYPEID_UINT16: {
            auto v = *static_cast<uint16_t*>(address);
            return {name, std::to_string(v), "uint16"};
        };
        case asTYPEID_UINT32: {
            auto v = *static_cast<uint32_t*>(address);
            return {name, std::to_string(v), "uint32"};
        };
        case asTYPEID_UINT64: {
            auto v = *static_cast<uint64_t*>(address);
            return {name, std::to_string(v), "uint64"};
        };

        case asTYPEID_FLOAT: {
            auto v = *static_cast<float*>(address);
            return {name, std::to_string(v), "float"};
        };
        case asTYPEID_DOUBLE: {
            auto v = *static_cast<double*>(address);
            return {name, std::to_string(v), "double"};
        };

        default: break;
    }
    if ((type & asTYPEID_SCRIPTOBJECT) != 0) {
        auto* obj = static_cast<asIScriptObject*>(address);
        std::string typeName = engine->GetTypeInfoById(type)->GetName();
        if (obj == nullptr) {
            return DebugAdapterProtocol::Variable::FromNull(name, typeName);
        }
        auto typeData = obj->GetObjectType();
        std::string stringified = typeName + "(*" + std::to_string((size_t)address) + ")";
        auto stringifiedFunc = typeData->GetMethodByDecl("string opImplConv()");
        if (stringifiedFunc != nullptr) {
            ctx->PushState();
            ctx->Prepare(stringifiedFunc);
            ctx->SetObject(address);
            if (ctx->Execute() == asEXECUTION_FINISHED) {
                stringified = *(std::string*)ctx->GetAddressOfReturnValue();
            }
            ctx->PopState();
        }

        size_t reference = 0;
        if (obj->GetPropertyCount() > 0) {
            std::cout << "Storing scriptObj " << name << std::endl;
            obj->AddRef();
            reference = debugger->StoreVariable(address, type, [](void* a) {
                if (a != nullptr) {
                    ((asIScriptObject*)a)->Release();
                }
            });
        }
        return DebugAdapterProtocol::Variable::FromPointer(name, typeName, stringified, reference);
    }
    // TODO: Caching of type id
    if (type == engine->GetTypeIdByDecl("string")) {
        auto* str = static_cast<std::string*>(address);
        if (str == nullptr) {
            return DebugAdapterProtocol::Variable::FromNull(name, "string");
        }
        return DebugAdapterProtocol::Variable::FromString(name, "\"" + *str + "\"");
    }
    auto* typeData = engine->GetTypeInfoById(type);
    if ((typeData->GetFlags() & asOBJ_ENUM) != 0) {
        const auto* enumValue = typeData->GetEnumValueByIndex(*static_cast<int32_t*>(address), nullptr);
        return {name, enumValue, typeData->GetName()};
    }

    if (address == nullptr) {
        return DebugAdapterProtocol::Variable::FromNull(name, typeData->GetName());
    }

    std::string stringified = std::string(typeData->GetName()) + "(*" + std::to_string((size_t)address) + ")";
    auto stringifiedFunc = typeData->GetMethodByDecl("string opImplConv()");
    if (stringifiedFunc != nullptr) {
        ctx->Prepare(stringifiedFunc);
        ctx->SetObject(*(void**)address);
        if (ctx->Execute() == asEXECUTION_FINISHED) {
            stringified = "\"" + *(std::string*)ctx->GetAddressOfReturnValue() + "\"";
        }
    }

    size_t reference = 0;
    size_t varCount = typeData->GetPropertyCount();
    for (asUINT i = 0; i < typeData->GetMethodCount(); ++i) {
        if (typeData->GetMethodByIndex(i, true)->IsProperty()) {
            varCount++;
        }
    }
    if (varCount > 0) {

        // If we want to re-use an object, we need to make sure to add a reference to it, so it doesn't get accidentally
        // cleaned up. This is currently somewhat clunky, we look up the AddRef behaviour, and execute that in a new
        // context.
        auto b = static_cast<asEBehaviours>(-1);
        asIScriptFunction* releaseFunc = nullptr;
        bool hasAddRef = false;
        for (asUINT i = 0; i < typeData->GetBehaviourCount(); ++i) {
            auto* f = typeData->GetBehaviourByIndex(i, &b);
            if (b == asBEHAVE_ADDREF) {
                // If we push state or re-use the active context, we might lose the reference to the object we have.
                // To resolve this we need a new context.
                auto* c = ctx->GetEngine()->CreateContext();
                assert(c->Prepare(f) >= 0);
                assert(c->SetObject(address) >= 0);
                assert(c->Execute() >= 0);
                c->Release();
                hasAddRef = true;
            }
            if (b == asBEHAVE_RELEASE) {
                releaseFunc = f;
            }
        }
        if (!hasAddRef) {
            releaseFunc = nullptr;
        }

        reference = debugger->StoreVariable(address, type, [engine, releaseFunc](void* a) {
            if (releaseFunc != nullptr) {
                auto* c = engine->CreateContext();
                c->Prepare(releaseFunc);
                c->SetObject(a);
                c->Execute();
                c->Release();
            }
        });
    }
    return DebugAdapterProtocol::Variable::FromPointer(name, typeData->GetName(), stringified, reference);
}

template <typename T, int (asIScriptContext::*SetArg)(asUINT, T), T (asIScriptContext::*GetLength)()>
void ASVariableFormatter::FormatArrayLike(std::vector<DebugAdapterProtocol::Variable>& vars, asIScriptEngine* engine,
                                          asIScriptContext* ctx, void* address, AngelscriptDebugger* debugger,
                                          asIScriptFunction* indexFunc) {
    auto length = (ctx->*GetLength)();
    for (T i = 0; i < length; ++i) {
        assert(ctx->Prepare(indexFunc) >= 0);
        ctx->SetObject(address);
        (ctx->*SetArg)(0, i);
        ctx->Execute();
        auto name = std::to_string(i);
        vars.push_back(GetAsDAPVariable(engine, ctx, debugger, name, indexFunc->GetReturnTypeId(),
                                        ctx->GetAddressOfReturnValue()));
    }
}

void ASVariableFormatter::GetChildDAPVariables(std::vector<DebugAdapterProtocol::Variable>& vars,
                                               asIScriptEngine* engine, asIScriptContext* ctx,
                                               AngelscriptDebugger* debugger, int type, void* address) {
    if (address == nullptr) {
        return;
    }

    if ((type & asTYPEID_SCRIPTOBJECT) != 0) {
        auto* obj = static_cast<asIScriptObject*>(address);
        for (asUINT i = 0; i < obj->GetPropertyCount(); ++i) {
            const auto* propertyName = obj->GetPropertyName(i);
            auto propertyType = obj->GetPropertyTypeId(i);
            auto* propertyAddress = obj->GetAddressOfProperty(i);
            vars.push_back(GetAsDAPVariable(engine, ctx, debugger, propertyName, propertyType, propertyAddress));
        }
        return;
    }

    auto* typeData = engine->GetTypeInfoById(type);
    for (asUINT i = 0; i < typeData->GetPropertyCount(); ++i) {
        const char* propertyName = nullptr;
        int propertyType = 0;
        int offset = 0;

        typeData->GetProperty(i, &propertyName, &propertyType, nullptr, nullptr, &offset);
        vars.push_back(
            GetAsDAPVariable(engine, ctx, debugger, propertyName, propertyType, (void*)((size_t)address + offset)));
    }
    for (asUINT i = 0; i < typeData->GetMethodCount(); ++i) {
        auto func = typeData->GetMethodByIndex(i, true);
        if (func->IsProperty()) {
            assert(ctx->Prepare(func) >= 0);
            ctx->SetObject(address);
            auto state = ctx->Execute();
            if (state == asEXECUTION_FINISHED) {
                auto name = std::string(func->GetName()).substr(4);
                vars.push_back(GetAsDAPVariable(engine, ctx, debugger, name, func->GetReturnTypeId(),
                                                ctx->GetAddressOfReturnValue()));
            }
        }
    }

    auto indexFunc = typeData->GetMethodByName("get_opIndex", true);
    if (indexFunc != nullptr && indexFunc->GetParamCount() == 1) {
        auto lengthFunc = typeData->GetMethodByName("get_Length", true);
        int t;
        indexFunc->GetParam(0, &t);
        if (lengthFunc != nullptr && lengthFunc->GetParamCount() == 0 && lengthFunc->GetReturnTypeId() == t) {
            assert(ctx->Prepare(lengthFunc) >= 0);
            ctx->SetObject(address);
            ctx->Execute();
            if (t == asTYPEID_UINT64) {
                FormatArrayLike<uint64_t, &asIScriptContext::SetArgQWord, &asIScriptContext::GetReturnQWord>(
                    vars, engine, ctx, address, debugger, indexFunc);
            }
            else if (t == asTYPEID_INT32){
                FormatArrayLike<uint32_t, &asIScriptContext::SetArgDWord, &asIScriptContext::GetReturnDWord>(
                    vars, engine, ctx, address, debugger, indexFunc);
            }
        }
        // vars.push_back(DebugAdapterProtocol::Variable("has_indexer", "true", "string", {}));
    }
}
