#include "il2cpp-config.h"
#include "il2cpp-object-internals.h"
#include "il2cpp-class-internals.h"
#include "icalls/mscorlib/System/Enum.h"
#include "vm/Class.h"
#include "vm/Object.h"
#include "vm/Exception.h"
#include "vm/Reflection.h"
#include "vm/Enum.h"

namespace il2cpp
{
namespace icalls
{
namespace mscorlib
{
namespace System
{
    Il2CppObject * Enum::get_value(Il2CppObject *thisPtr)
    {
        if (!thisPtr)
            return NULL;

        IL2CPP_ASSERT(thisPtr->klass->enumtype);

        Il2CppClass* enumClass = vm::Class::FromIl2CppType(vm::Class::GetEnumBaseType(thisPtr->klass));
        Il2CppObject* res = vm::Object::New(enumClass);
        void* dst = (char*)res + sizeof(Il2CppObject);
        void* src = (char*)thisPtr + sizeof(Il2CppObject);
        int32_t size = vm::Class::GetValueSize(enumClass, NULL);

        memcpy(dst, src, size);

        return res;

        return il2cpp::vm::Object::Box(thisPtr->klass->element_class, (char*)thisPtr + sizeof(Il2CppObject));
    }

    int Enum::compare_value_to(Il2CppObject * thisPtr, Il2CppObject * other)
    {
        void* tdata = (char*)thisPtr + sizeof(Il2CppObject);
        void* odata = (char*)other + sizeof(Il2CppObject);
        const Il2CppType *basetype = vm::Class::GetEnumBaseType(vm::Object::GetClass(thisPtr));
        IL2CPP_ASSERT(basetype);

#define COMPARE_ENUM_VALUES(ENUM_TYPE) do { \
        ENUM_TYPE me = *((ENUM_TYPE*)tdata); \
        ENUM_TYPE other = *((ENUM_TYPE*)odata); \
        if (me == other) \
            return 0; \
        return me > other ? 1 : -1; \
    } while (0)

#define COMPARE_ENUM_VALUES_RANGE(ENUM_TYPE) do { \
        ENUM_TYPE me = *((ENUM_TYPE*)tdata); \
        ENUM_TYPE other = *((ENUM_TYPE*)odata); \
        if (me == other) \
            return 0; \
        return me - other; \
    } while (0)

        switch (basetype->type)
        {
            case IL2CPP_TYPE_U1:
                COMPARE_ENUM_VALUES(uint8_t);
            case IL2CPP_TYPE_I1:
                COMPARE_ENUM_VALUES(int8_t);
            case IL2CPP_TYPE_CHAR:
                COMPARE_ENUM_VALUES_RANGE(Il2CppChar);
            case IL2CPP_TYPE_U2:
                COMPARE_ENUM_VALUES_RANGE(uint16_t);
            case IL2CPP_TYPE_I2:
                COMPARE_ENUM_VALUES(int16_t);
            case IL2CPP_TYPE_U4:
                COMPARE_ENUM_VALUES(uint32_t);
            case IL2CPP_TYPE_I4:
                COMPARE_ENUM_VALUES(int32_t);
            case IL2CPP_TYPE_U8:
                COMPARE_ENUM_VALUES(uint64_t);
            case IL2CPP_TYPE_I8:
                COMPARE_ENUM_VALUES(int64_t);
            default:
                IL2CPP_ASSERT(false && "Implement type 0x%02x in compare_value_to");
        }

#undef COMPARE_ENUM_VALUES_RANGE
#undef COMPARE_ENUM_VALUES
        return 0;
    }

    int32_t Enum::get_hashcode(Il2CppObject * thisPtr)
    {
        void* data = (char*)thisPtr + sizeof(Il2CppObject);
        Il2CppClass *basetype = thisPtr->klass->element_class;
        IL2CPP_ASSERT(basetype);

        if (basetype == il2cpp_defaults.sbyte_class)
            return *((int8_t*)data);
        if (basetype == il2cpp_defaults.byte_class)
            return *((uint8_t*)data);
        if (basetype == il2cpp_defaults.char_class)
            return *((Il2CppChar*)data);
        if (basetype == il2cpp_defaults.uint16_class)
            return *((uint16_t*)data);
        if (basetype == il2cpp_defaults.int16_class)
            return *((uint16_t*)data);
        if (basetype == il2cpp_defaults.uint32_class)
            return *((uint32_t*)data);
        if (basetype == il2cpp_defaults.int32_class)
            return *((int32_t*)data);
        if (basetype == il2cpp_defaults.uint64_class || basetype == il2cpp_defaults.int64_class)
        {
            int64_t value = *((int64_t*)data);
            return (int32_t)(value & 0xffffffff) ^ (int32_t)(value >> 32);
        }

        IL2CPP_ASSERT(0 && "System_Enum_get_hashcode_icall");
        return 0;
    }

    static uint64_t
    read_enum_value(char *mem, Il2CppClass* type)
    {
        if (type == il2cpp_defaults.byte_class)
            return *(int8_t*)mem;
        if (type == il2cpp_defaults.sbyte_class)
            return *(uint8_t*)mem;
        if (type == il2cpp_defaults.uint16_class)
            return *(uint16_t*)mem;
        if (type == il2cpp_defaults.int16_class)
            return *(int16_t*)mem;
        if (type == il2cpp_defaults.uint32_class)
            return *(uint32_t*)mem;
        if (type == il2cpp_defaults.int32_class)
            return *(int32_t*)mem;
        if (type == il2cpp_defaults.uint64_class)
            return *(uint64_t*)mem;
        if (type == il2cpp_defaults.int64_class)
            return *(int64_t*)mem;

        IL2CPP_ASSERT(0);

        return 0;
    }

    static void
    write_enum_value(char *mem, Il2CppClass* type, uint64_t value)
    {
        if (type == il2cpp_defaults.byte_class || type == il2cpp_defaults.sbyte_class)
        {
            uint8_t *p = (uint8_t*)mem;
            *p = (uint8_t)value;
        }
        else if (type == il2cpp_defaults.uint16_class || type == il2cpp_defaults.int16_class)
        {
            uint16_t *p = (uint16_t*)mem;
            *p = (uint16_t)value;
        }
        else if (type == il2cpp_defaults.uint32_class || type == il2cpp_defaults.int32_class)
        {
            uint32_t *p = (uint32_t*)mem;
            *p = (uint32_t)value;
        }
        else if (type == il2cpp_defaults.uint64_class || type == il2cpp_defaults.int64_class)
        {
            uint64_t *p = (uint64_t*)mem;
            *p = value;
        }
        else
        {
            IL2CPP_ASSERT(0);
        }
    }

    Il2CppObject * Enum::ToObject(Il2CppReflectionType * enumType, Il2CppObject * value)
    {
        //MonoDomain *domain;
        Il2CppClass *enumc, *objc;
        Il2CppObject *res;
        Il2CppClass *etype;
        uint64_t val;

        IL2CPP_CHECK_ARG_NULL(enumType);
        IL2CPP_CHECK_ARG_NULL(value);

        //domain = mono_object_domain (enumType);
        enumc = vm::Class::FromIl2CppType(enumType->type);
        objc = vm::Object::GetClass(value);

        //if (!enumc->enumtype)
        //  mono_raise_exception (mono_get_exception_argument ("enumType", "Type provided must be an Enum."));
        //if (!((objc->enumtype) || (objc->byval_arg.type >= MONO_TYPE_I1 && objc->byval_arg.type <= MONO_TYPE_U8)))
        //  mono_raise_exception (mono_get_exception_argument ("value", "The value passed in must be an enum base or an underlying type for an enum, such as an Int32."));

        etype = vm::Class::GetElementClass(enumc);
        if (!etype)
            /* MS throws this for typebuilders */
            il2cpp::vm::Exception::Raise(il2cpp::vm::Exception::GetArgumentException("Type must be a type provided by the runtime.", "enumType"));

        res = (Il2CppObject*)il2cpp::vm::Object::New(enumc);
        //val = read_enum_value ((char *)value + sizeof (mscorlib_System_Object), objc->enumtype? mono_class_enum_basetype (objc)->type: objc->byval_arg.type);
        val = read_enum_value((char*)value + sizeof(Il2CppObject), objc->enumtype ? objc->element_class : objc);
        write_enum_value((char*)res + sizeof(Il2CppObject), etype, val);

        return res;
    }

    Il2CppReflectionType * Enum::get_underlying_type(Il2CppReflectionType *type)
    {
        const Il2CppType *etype;

        etype = vm::Class::GetEnumBaseType(vm::Class::FromIl2CppType(type->type));
        if (!etype)
            /* MS throws this for typebuilders */
            il2cpp::vm::Exception::Raise(il2cpp::vm::Exception::GetArgumentException("Type must be a type provided by the runtime.", "enumType"));

        return il2cpp::vm::Reflection::GetTypeObject(etype);
    }

#if NET_4_0
    bool Enum::GetEnumValuesAndNames(Il2CppReflectionRuntimeType* enumType, Il2CppArray** values, Il2CppArray** names)
    {
        return vm::Enum::GetEnumValuesAndNames(vm::Class::FromIl2CppType(enumType->type.type), values, names);
    }

    bool Enum::InternalHasFlag(Il2CppObject* thisPtr, Il2CppObject* flags)
    {
        Il2CppClass* enumClass = vm::Class::FromIl2CppType(vm::Class::GetEnumBaseType(thisPtr->klass));
        int32_t size = vm::Class::GetValueSize(enumClass, NULL);
        uint64_t a_val = 0, b_val = 0;

        memcpy(&a_val, vm::Object::Unbox(thisPtr), size);
        memcpy(&b_val, vm::Object::Unbox(flags), size);

        return (a_val & b_val) == b_val;
    }

    int32_t Enum::InternalCompareTo(Il2CppObject* o1, Il2CppObject* o2)
    {
        return compare_value_to(o1, o2);
    }

    Il2CppObject* Enum::InternalBoxEnum(Il2CppReflectionRuntimeType* enumType, int64_t value)
    {
        return vm::Object::Box(vm::Class::FromIl2CppType(enumType->type.type), &value);
    }

    Il2CppReflectionRuntimeType* Enum::InternalGetUnderlyingType(Il2CppReflectionRuntimeType* enumType)
    {
        return reinterpret_cast<Il2CppReflectionRuntimeType*>(get_underlying_type(&enumType->type));
    }

#endif
} /* namespace System */
} /* namespace mscorlib */
} /* namespace icalls */
} /* namespace il2cpp */
