#include "il2cpp-config.h"
#include "il2cpp-object-internals.h"
#include "il2cpp-class-internals.h"
#include "vm/Object.h"
#include "vm/CCW.h"
#include "vm/Class.h"
#include "vm/CachedCCWBase.h"
#include "vm/Exception.h"
#include "vm/MetadataCache.h"
#include "vm/Method.h"
#include "vm/RCW.h"
#include "vm/Runtime.h"
#include "vm/ScopedThreadAttacher.h"
#include "vm/String.h"

namespace il2cpp
{
namespace vm
{
    struct ManagedObject : CachedCCWBase<ManagedObject>
    {
        inline ManagedObject(Il2CppObject* obj) :
            CachedCCWBase<ManagedObject>(obj)
        {
        }

        virtual il2cpp_hresult_t STDCALL QueryInterface(const Il2CppGuid& iid, void** object) IL2CPP_OVERRIDE
        {
            if (::memcmp(&iid, &Il2CppIUnknown::IID, sizeof(Il2CppGuid)) == 0
                || ::memcmp(&iid, &Il2CppIInspectable::IID, sizeof(Il2CppGuid)) == 0
                || ::memcmp(&iid, &Il2CppIAgileObject::IID, sizeof(Il2CppGuid)) == 0)
            {
                *object = GetIdentity();
                AddRefImpl();
                return IL2CPP_S_OK;
            }

            if (::memcmp(&iid, &Il2CppIManagedObjectHolder::IID, sizeof(Il2CppGuid)) == 0)
            {
                *object = static_cast<Il2CppIManagedObjectHolder*>(this);
                AddRefImpl();
                return IL2CPP_S_OK;
            }

            if (::memcmp(&iid, &Il2CppIMarshal::IID, sizeof(Il2CppGuid)) == 0)
            {
                *object = static_cast<Il2CppIMarshal*>(this);
                AddRefImpl();
                return IL2CPP_S_OK;
            }

            if (::memcmp(&iid, &Il2CppIWeakReferenceSource::IID, sizeof(Il2CppGuid)) == 0)
            {
                *object = static_cast<Il2CppIWeakReferenceSource*>(this);
                AddRefImpl();
                return IL2CPP_S_OK;
            }

            *object = NULL;
            return IL2CPP_E_NOINTERFACE;
        }
    };

    Il2CppIUnknown* CCW::CreateCCW(Il2CppObject* obj)
    {
        // check for ccw create function, which is implemented by objects that implement COM or Windows Runtime interfaces
        const Il2CppInteropData* interopData = obj->klass->interopData;
        if (interopData != NULL)
        {
            const CreateCCWFunc createCcw = interopData->createCCWFunction;

            if (createCcw != NULL)
                return createCcw(obj);
        }

        // otherwise create generic ccw object that "only" implements IUnknown, IMarshal, IInspectable, IAgileObject and IManagedObjectHolder interfaces
        void* memory = utils::Memory::Malloc(sizeof(ManagedObject));
        if (memory == NULL)
            Exception::RaiseOutOfMemoryException();
        return static_cast<Il2CppIManagedObjectHolder*>(new(memory) ManagedObject(obj));
    }

    Il2CppObject* CCW::Unpack(Il2CppIUnknown* unknown)
    {
        Il2CppIManagedObjectHolder* managedHolder;
        il2cpp_hresult_t hr = unknown->QueryInterface(Il2CppIManagedObjectHolder::IID, reinterpret_cast<void**>(&managedHolder));
        Exception::RaiseIfFailed(hr, true);

        Il2CppObject* instance = managedHolder->GetManagedObject();
        managedHolder->Release();

        IL2CPP_ASSERT(instance);
        return instance;
    }

    static Il2CppString* ValueToStringFallbackToEmpty(Il2CppObject* value)
    {
        Il2CppClass* klass = il2cpp::vm::Object::GetClass(value);
        const MethodInfo* toStringMethod = il2cpp::vm::Class::GetMethodFromName(klass, "ToString", 0);
        // check if the method we are to call is expecting a value type 'this' parameter or not.
        // handles edge case of enums where the 'value' is SomeEnum but the ToString method is Enum::ToString.
        // In this case, a boxed 'this' parameter is expected even though the object value is a valuetype.
        bool isValueTypeMethod = il2cpp::vm::Class::IsValuetype(il2cpp::vm::Method::GetClass(toStringMethod));

        Il2CppException* exception = NULL;
        Il2CppString* result = (Il2CppString*)il2cpp::vm::Runtime::Invoke(toStringMethod, isValueTypeMethod ? Object::GetRawData(value) : value, NULL, &exception);
        if (exception != NULL)
            return String::Empty();

        return result;
    }

    static il2cpp_hresult_t HandleInvalidIPropertyConversionImpl(const std::string& exceptionMessage)
    {
        ScopedThreadAttacher scopedThreadAttacher; // Make sure we're attached before we create exceptions (aka allocate managed memory)

        Il2CppException* exception = Exception::GetInvalidCastException(exceptionMessage.c_str());
        Exception::PrepareExceptionForThrow(exception);
        Exception::StoreExceptionInfo(exception, ValueToStringFallbackToEmpty(exception));
        return exception->hresult;
    }

    il2cpp_hresult_t CCW::HandleInvalidIPropertyConversion(const char* fromType, const char* toType)
    {
        std::string message = il2cpp::utils::StringUtils::Printf("Object in an IPropertyValue is of type '%s', which cannot be converted to a '%s'.", fromType, toType);
        return HandleInvalidIPropertyConversionImpl(message);
    }

    il2cpp_hresult_t CCW::HandleInvalidIPropertyConversion(Il2CppObject* value, const char* fromType, const char* toType)
    {
        Il2CppString* valueStr = ValueToStringFallbackToEmpty(value);
        std::string message = il2cpp::utils::StringUtils::Printf(
            "Object in an IPropertyValue is of type '%s' with value '%s', which cannot be converted to a '%s'.",
            fromType,
            utils::StringUtils::Utf16ToUtf8(valueStr->chars, valueStr->length).c_str(),
            toType);
        return HandleInvalidIPropertyConversionImpl(message);
    }

    il2cpp_hresult_t CCW::HandleInvalidIPropertyArrayConversion(const char* fromArrayType, const char* fromElementType, const char* toElementType, il2cpp_array_size_t index)
    {
        std::string message = il2cpp::utils::StringUtils::Printf(
            "Object in an IPropertyValue is of type '%s' which cannot be converted to a '%s[]' due to array element '%d': Object in an IPropertyValue is of type '%s', which cannot be converted to a '%s'.",
            fromArrayType,
            toElementType,
            static_cast<int>(index),
            fromElementType,
            toElementType);
        return HandleInvalidIPropertyConversionImpl(message);
    }

    il2cpp_hresult_t CCW::HandleInvalidIPropertyArrayConversion(Il2CppObject* value, const char* fromArrayType, const char* fromElementType, const char* toElementType, il2cpp_array_size_t index)
    {
        Il2CppString* valueStr = ValueToStringFallbackToEmpty(value);
        std::string message = il2cpp::utils::StringUtils::Printf(
            "Object in an IPropertyValue is of type '%s' which cannot be converted to a '%s[]' due to array element '%d': Object in an IPropertyValue is of type '%s' with value '%s', which cannot be converted to a '%s'.",
            fromArrayType,
            toElementType,
            static_cast<int>(index),
            fromElementType,
            utils::StringUtils::Utf16ToUtf8(valueStr->chars, valueStr->length).c_str(),
            toElementType);
        return HandleInvalidIPropertyConversionImpl(message);
    }

    Il2CppIUnknown* CCW::GetCCW(Il2CppObject* o, Il2CppReflectionType* type)
    {
        if (o == NULL)
            vm::Exception::Raise(vm::Exception::GetArgumentNullException("o"));

        if (type == NULL)
            vm::Exception::Raise(vm::Exception::GetArgumentNullException("type"));

        Il2CppClass* klass = vm::Class::FromIl2CppType(type->type);

        if (!vm::Class::IsInterface(klass))
        {
            DECLARE_NATIVE_C_STRING_AS_STRING_VIEW_OF_IL2CPP_CHARS(parameter, IL2CPP_NATIVE_STRING("type"));
            DECLARE_NATIVE_C_STRING_AS_STRING_VIEW_OF_IL2CPP_CHARS(message, IL2CPP_NATIVE_STRING("The type parameter is not an interface."));
            vm::Exception::Raise(vm::Exception::GetArgumentException(parameter, message));
        }

        if (vm::Class::IsGeneric(klass))
        {
            DECLARE_NATIVE_C_STRING_AS_STRING_VIEW_OF_IL2CPP_CHARS(parameter, IL2CPP_NATIVE_STRING("type"));
            DECLARE_NATIVE_C_STRING_AS_STRING_VIEW_OF_IL2CPP_CHARS(message, IL2CPP_NATIVE_STRING("The type parameter is a generic type."));
            vm::Exception::Raise(vm::Exception::GetArgumentException(parameter, message));
        }

        const Il2CppInteropData* interopData = klass->interopData;
        if (interopData == NULL || interopData->guid == NULL)
        {
            DECLARE_NATIVE_C_STRING_AS_STRING_VIEW_OF_IL2CPP_CHARS(parameter, IL2CPP_NATIVE_STRING("type"));
            DECLARE_NATIVE_C_STRING_AS_STRING_VIEW_OF_IL2CPP_CHARS(message, IL2CPP_NATIVE_STRING("The specified type must be visible from COM."));
            vm::Exception::Raise(vm::Exception::GetArgumentException(parameter, message));
        }

        return vm::CCW::GetOrCreate(o, *interopData->guid);
    }
} /* namespace vm */
} /* namespace il2cpp */
