// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.

#include "DebugLog.h"
#include "Engine/Core/Log.h"
#include "Engine/Scripting/InternalCalls.h"
#include "Engine/Scripting/ScriptingObject.h"
#include "Engine/Scripting/MException.h"
#include "Engine/Scripting/Scripting.h"
#include "Engine/Scripting/ScriptingType.h"
#include "Engine/Scripting/BinaryModule.h"
#include "Engine/Scripting/MainThreadManagedInvokeAction.h"
#include "Engine/Scripting/ManagedCLR/MDomain.h"
#include "Engine/Scripting/ManagedCLR/MAssembly.h"
#include "Engine/Scripting/ManagedCLR/MClass.h"
#include "FlaxEngine.Gen.h"
#include <ThirdParty/mono-2.0/mono/metadata/exception.h>
#include <ThirdParty/mono-2.0/mono/metadata/appdomain.h>

namespace DebugLogHandlerInternal
{
    void LogWrite(LogType level, MonoString* msgObj)
    {
        StringView msg;
        MUtils::ToString(msgObj, msg);
        Log::Logger::Write(level, msg);
    }

    void Log(LogType level, MonoString* msgObj, ScriptingObject* obj, MonoString* stackTrace)
    {
        if (msgObj == nullptr)
            return;

        // Get info
        StringView msg;
        MUtils::ToString(msgObj, msg);
        //const String objName = obj ? obj->ToString() : String::Empty;

        // Send event
        // TODO: maybe option for build to threat warnings and errors as fatal errors?
        //const String logMessage = String::Format(TEXT("Debug:{1} {2}"), objName, *msg);
        Log::Logger::Write(level, msg);
    }

    void LogException(MonoException* exception, ScriptingObject* obj)
    {
        if (exception == nullptr)
            return;

        // Get info
        MException ex(exception);
        String objName = obj ? obj->ToString() : String::Empty;

        // Print exception including inner exceptions
        // TODO: maybe option for build to threat warnings and errors as fatal errors?
        ex.Log(LogType::Warning, objName.GetText());
    }
}

namespace FlaxLogWriterInternal
{
    void WriteStringToLog(MonoString* msg)
    {
        if (msg == nullptr)
            return;

        auto msgStr = (Char*)mono_string_to_utf16(msg);
        LOG_STR(Info, msgStr);
    }
}

void DebugLog::RegisterInternalCalls()
{
    // DebugLogHandler
    ADD_INTERNAL_CALL("FlaxEngine.DebugLogHandler::Internal_LogWrite", &DebugLogHandlerInternal::LogWrite);
    ADD_INTERNAL_CALL("FlaxEngine.DebugLogHandler::Internal_Log", &DebugLogHandlerInternal::Log);
    ADD_INTERNAL_CALL("FlaxEngine.DebugLogHandler::Internal_LogException", &DebugLogHandlerInternal::LogException);

    // FlaxLogWriter
    ADD_INTERNAL_CALL("FlaxEngine.FlaxLogWriter::Internal_WriteStringToLog", &FlaxLogWriterInternal::WriteStringToLog);
}

namespace DebugLogImpl
{
    MMethod* Internal_SendLog = nullptr;
    MMethod* Internal_SendLogException = nullptr;
    MMethod* Internal_GetStackTrace = nullptr;
    CriticalSection Locker;
}

using namespace DebugLogImpl;

void ClearMethods(MAssembly*)
{
    Internal_SendLog = nullptr;
    Internal_SendLogException = nullptr;
    Internal_GetStackTrace = nullptr;
}

bool CacheMethods()
{
    if (Internal_SendLog && Internal_SendLogException && Internal_GetStackTrace)
        return false;
    ScopeLock lock(Locker);
    if (Internal_SendLog && Internal_SendLogException && Internal_GetStackTrace)
        return false;

    auto engine = GetBinaryModuleFlaxEngine()->Assembly;
    if (engine == nullptr || !engine->IsLoaded())
        return true;
    auto debugLogHandlerClass = engine->GetClass("FlaxEngine.DebugLogHandler");
    if (!debugLogHandlerClass)
        return false;

    Internal_SendLog = debugLogHandlerClass->GetMethod("Internal_SendLog", 2);
    if (!Internal_SendLog)
        return false;

    Internal_SendLogException = debugLogHandlerClass->GetMethod("Internal_SendLogException", 1);
    if (!Internal_SendLogException)
        return false;

    Internal_GetStackTrace = debugLogHandlerClass->GetMethod("Internal_GetStackTrace");
    if (!Internal_GetStackTrace)
        return false;

    engine->Unloading.Bind<ClearMethods>();

    return false;
}

void DebugLog::Log(LogType type, const String& message)
{
    if (CacheMethods())
        return;

    auto scriptsDomain = Scripting::GetScriptsDomain();
    MainThreadManagedInvokeAction::ParamsBuilder params;
    params.AddParam(type);
    params.AddParam(message, scriptsDomain->GetNative());
    MainThreadManagedInvokeAction::Invoke(Internal_SendLog, params);
}

void DebugLog::LogException(MonoObject* exceptionObject)
{
    if (exceptionObject == nullptr)
        return;

    if (CacheMethods())
        return;

    MainThreadManagedInvokeAction::ParamsBuilder params;
    params.AddParam(exceptionObject);
    MainThreadManagedInvokeAction::Invoke(Internal_SendLogException, params);
}

String DebugLog::GetStackTrace()
{
    String result;
    if (!CacheMethods())
    {
        auto stackTraceObj = Internal_GetStackTrace->Invoke(nullptr, nullptr, nullptr);
        MUtils::ToString((MonoString*)stackTraceObj, result);
    }
    return result;
}

void DebugLog::ThrowException(const char* msg)
{
    // Throw exception to the C# world
    auto ex = mono_exception_from_name_msg(mono_get_corlib(), "System", "Exception", msg);
    mono_raise_exception(ex);
}

void DebugLog::ThrowNullReference()
{
    //LOG(Warning, "Invalid null reference.");
    //LOG_STR(Warning, DebugLog::GetStackTrace());

    // Throw exception to the C# world
    auto ex = mono_get_exception_null_reference();
    mono_raise_exception(ex);
}

void DebugLog::ThrowArgument(const char* arg, const char* msg)
{
    // Throw exception to the C# world
    auto ex = mono_get_exception_argument(arg, msg);
    mono_raise_exception(ex);
}

void DebugLog::ThrowArgumentNull(const char* arg)
{
    // Throw exception to the C# world
    auto ex = mono_get_exception_argument_null(arg);
    mono_raise_exception(ex);
}

void DebugLog::ThrowArgumentOutOfRange(const char* arg)
{
    // Throw exception to the C# world
    auto ex = mono_get_exception_argument_out_of_range(arg);
    mono_raise_exception(ex);
}

void DebugLog::ThrowNotSupported(const char* msg)
{
    // Throw exception to the C# world
    auto ex = mono_get_exception_not_supported(msg);
    mono_raise_exception(ex);
}
