#include "il2cpp-config.h"
#include "gc/gc_wrapper.h"
#include "gc/GarbageCollector.h"
#include "vm/Array.h"
#include "vm/Class.h"
#include "vm/Exception.h"
#include "vm/Object.h"
#include "vm/Profiler.h"
#include "il2cpp-class-internals.h"
#include "il2cpp-object-internals.h"
#include <memory>

namespace il2cpp
{
namespace vm
{
    Il2CppArray* Array::Clone(Il2CppArray* arr)
    {
        Il2CppClass *typeInfo = arr->klass;
        const uint32_t elem_size = il2cpp::vm::Array::GetElementSize(typeInfo);

        if (arr->bounds == NULL)
        {
            il2cpp_array_size_t len = il2cpp::vm::Array::GetLength(arr);
            Il2CppArray *clone = (Il2CppArray*)il2cpp::vm::Array::NewFull(typeInfo, &len, NULL);
            memcpy(il2cpp::vm::Array::GetFirstElementAddress(clone), il2cpp::vm::Array::GetFirstElementAddress(arr), elem_size * len);

            gc::GarbageCollector::SetWriteBarrier((void**)il2cpp::vm::Array::GetFirstElementAddress(clone), elem_size * len);

            return clone;
        }

        il2cpp_array_size_t size = elem_size;
        std::vector<il2cpp_array_size_t> lengths(typeInfo->rank);
        std::vector<il2cpp_array_size_t> lowerBounds(typeInfo->rank);

        for (int i = 0; i < typeInfo->rank; ++i)
        {
            lengths[i] = arr->bounds[i].length;
            size *= arr->bounds[i].length;
            lowerBounds[i] = arr->bounds[i].lower_bound;
        }

        Il2CppArray* clone = il2cpp::vm::Array::NewFull(typeInfo, &lengths[0], &lowerBounds[0]);
        memcpy(il2cpp::vm::Array::GetFirstElementAddress(clone), il2cpp::vm::Array::GetFirstElementAddress(arr), size);

        gc::GarbageCollector::SetWriteBarrier((void**)il2cpp::vm::Array::GetFirstElementAddress(clone), size);

        return clone;
    }

    int32_t Array::GetElementSize(const Il2CppClass *klass)
    {
        IL2CPP_ASSERT(klass->rank);
        return klass->element_size;
    }

    uint32_t Array::GetLength(Il2CppArray* array)
    {
        return ARRAY_LENGTH_AS_INT32(array->max_length);
    }

    uint32_t Array::GetByteLength(Il2CppArray* array)
    {
        Il2CppClass *klass;
        il2cpp_array_size_t length;
        int i;

        klass = array->klass;

        if (array->bounds == NULL)
            length = array->max_length;
        else
        {
            length = 1;
            for (i = 0; i < klass->rank; ++i)
                length *= array->bounds[i].length;
        }

        return ARRAY_LENGTH_AS_INT32(length * GetElementSize(klass));
    }

    Il2CppArray* Array::New(Il2CppClass *elementTypeInfo, il2cpp_array_size_t length)
    {
        return NewSpecific(Class::GetArrayClass(elementTypeInfo, 1), length);
    }

    static void RaiseOverflowException()
    {
        vm::Exception::Raise(vm::Exception::GetOverflowException("Arithmetic operation resulted in an overflow."));
    }

    Il2CppArray* Array::NewSpecific(Il2CppClass *klass, il2cpp_array_size_t n)
    {
        Il2CppObject *o;
        Il2CppArray *ao;
        uint32_t elem_size;
        il2cpp_array_size_t byte_len;

        ClassInlines::InitFromCodegen(klass);
        IL2CPP_ASSERT(klass->rank);
        IL2CPP_ASSERT(klass->initialized);
        IL2CPP_ASSERT(klass->element_class->initialized);
        IL2CPP_ASSERT(klass->byval_arg.type == IL2CPP_TYPE_SZARRAY);

        IL2CPP_NOT_IMPLEMENTED_NO_ASSERT(Array::NewSpecific, "Not checking for overflow");
        IL2CPP_NOT_IMPLEMENTED_NO_ASSERT(Array::NewSpecific, "Handle allocations with a GC descriptor");

        if (n > IL2CPP_ARRAY_MAX_INDEX)
        {
            RaiseOverflowException();
            return NULL;
        }

        elem_size = il2cpp_array_element_size(klass);
        //if (CHECK_MUL_OVERFLOW_UN (n, elem_size)) {
        //  mono_gc_out_of_memory (MONO_ARRAY_MAX_SIZE);
        //  return NULL;
        //}
        byte_len = n * elem_size;
        //if (CHECK_ADD_OVERFLOW_UN (byte_len, sizeof (MonoArray))) {
        //  mono_gc_out_of_memory (MONO_ARRAY_MAX_SIZE);
        //  return NULL;
        //}
        byte_len += kIl2CppSizeOfArray;
        if (!klass->has_references)
        {
            o = Object::AllocatePtrFree(byte_len, klass);
#if NEED_TO_ZERO_PTRFREE
            ((Il2CppArray*)o)->bounds = NULL;
            memset(vm::Object::GetRawData(o), 0, byte_len - sizeof(Il2CppObject));
#endif
        }
        else if (klass->element_class->byval_arg.valuetype &&
                 ((GC_descr)klass->element_class->gc_desc & GC_DS_TAGS) == GC_DS_BITMAP)
        {
            o = (Il2CppObject*)GC_gcj_vector_malloc(byte_len, klass);
        }
#if IL2CPP_HAS_GC_DESCRIPTORS
        else if (klass->gc_desc != GC_NO_DESCRIPTOR)
        {
            o = Object::AllocateSpec(byte_len, klass);
        }
#endif
        else
        {
            o = Object::Allocate(byte_len, klass);
        }

        ao = (Il2CppArray*)o;
        ao->max_length = n;

#if IL2CPP_ENABLE_PROFILER
        if (Profiler::ProfileAllocations())
            Profiler::Allocation(o, klass);
#endif

        return ao;
    }

    Il2CppArray* Array::NewFull(Il2CppClass *array_class, il2cpp_array_size_t *lengths, il2cpp_array_size_t *lower_bounds)
    {
        il2cpp_array_size_t byte_len, len, bounds_size;
        Il2CppObject *o;
        Il2CppArray *array;
        int i;

        Class::Init(array_class);
        IL2CPP_ASSERT(array_class->rank);
        IL2CPP_ASSERT(array_class->initialized);
        IL2CPP_ASSERT(array_class->element_class->initialized);

        IL2CPP_NOT_IMPLEMENTED_NO_ASSERT(Array::NewFull, "IGNORING non-zero based arrays!");
        IL2CPP_NOT_IMPLEMENTED_NO_ASSERT(Array::NewFull, "Handle allocations with a GC descriptor");

        byte_len = il2cpp_array_element_size(array_class);
        len = 1;

        /* A single dimensional array with a 0 lower bound is the same as an szarray */
        if (array_class->rank == 1 && ((array_class->byval_arg.type == IL2CPP_TYPE_SZARRAY) || (lower_bounds && lower_bounds[0] == 0)))
        {
            /* A single dimensional array with a 0 lower bound should be an szarray */
            /* but the caller asked for an IL2CPP_TYPE_ARRAY, which insn't correct */
            IL2CPP_ASSERT(array_class->byval_arg.type == IL2CPP_TYPE_SZARRAY);

            len = lengths[0];
            if (len > IL2CPP_ARRAY_MAX_INDEX) //MONO_ARRAY_MAX_INDEX
                RaiseOverflowException();
            bounds_size = 0;
        }
        else
        {
            IL2CPP_ASSERT(array_class->byval_arg.type == IL2CPP_TYPE_ARRAY);

            bounds_size = sizeof(Il2CppArrayBounds) * array_class->rank;

            for (i = 0; i < array_class->rank; ++i)
            {
                if (lengths[i] > IL2CPP_ARRAY_MAX_INDEX)  //MONO_ARRAY_MAX_INDEX
                    RaiseOverflowException();
                //if (CHECK_MUL_OVERFLOW_UN (len, lengths [i]))
                //  mono_gc_out_of_memory (MONO_ARRAY_MAX_SIZE);
                len *= lengths[i];
            }
        }

        //if (CHECK_MUL_OVERFLOW_UN (byte_len, len))
        //  mono_gc_out_of_memory (MONO_ARRAY_MAX_SIZE);
        byte_len *= len;
        //if (CHECK_ADD_OVERFLOW_UN (byte_len, sizeof (MonoArray)))
        //  mono_gc_out_of_memory (MONO_ARRAY_MAX_SIZE);
        byte_len += kIl2CppSizeOfArray;
        if (bounds_size)
        {
            /* align */
            //if (CHECK_ADD_OVERFLOW_UN (byte_len, 3))
            //  mono_gc_out_of_memory (MONO_ARRAY_MAX_SIZE);
            byte_len = (byte_len + (IL2CPP_SIZEOF_VOID_P - 1)) & ~(IL2CPP_SIZEOF_VOID_P - 1);
            //if (CHECK_ADD_OVERFLOW_UN (byte_len, bounds_size))
            //  mono_gc_out_of_memory (MONO_ARRAY_MAX_SIZE);
            byte_len += bounds_size;
        }
        /*
         * Following three lines almost taken from mono_object_new ():
         * they need to be kept in sync.
         */
        if (!array_class->has_references)
        {
            o = Object::AllocatePtrFree(byte_len, array_class);
#if NEED_TO_ZERO_PTRFREE
            memset(vm::Object::GetRawData(o), 0, byte_len - sizeof(Il2CppObject));
#endif
        }
#if IL2CPP_HAS_GC_DESCRIPTORS
        else if (array_class->gc_desc != GC_NO_DESCRIPTOR)
        {
            o = Object::AllocateSpec(byte_len, array_class);
        }
#endif
        else
        {
            o = Object::Allocate(byte_len, array_class);
        }

        array = (Il2CppArray*)o;
        array->max_length = len;

        if (bounds_size)
        {
            Il2CppArrayBounds *bounds = (Il2CppArrayBounds*)((char*)array + byte_len - bounds_size);
            array->bounds = bounds;
            for (i = 0; i < array_class->rank; ++i)
            {
                bounds[i].length = lengths[i];
                if (lower_bounds)
                    bounds[i].lower_bound = ARRAY_LENGTH_AS_INT32(lower_bounds[i]);
            }
        }

#if IL2CPP_ENABLE_PROFILER
        if (Profiler::ProfileAllocations())
            Profiler::Allocation(o, array_class);
#endif

        return array;
    }

    char* Array::GetFirstElementAddress(Il2CppArray *array)
    {
        return reinterpret_cast<char*>(array) + kIl2CppSizeOfArray;
    }

    il2cpp_array_size_t Array::IndexFromIndices(Il2CppArray* thisPtr, int32_t const * indices)
    {
        int32_t i;
        il2cpp_array_size_t pos;
        Il2CppClass* ac;
        ac = thisPtr->klass;

        pos = indices[0] - thisPtr->bounds[0].lower_bound;
        for (i = 1; i < ac->rank; i++)
            pos = pos * thisPtr->bounds[i].length + indices[i] -
                thisPtr->bounds[i].lower_bound;

        return pos;
    }
} /* namespace vm */
} /* namespace il2cpp */

LIBIL2CPP_CODEGEN_API int32_t
il2cpp_array_element_size(Il2CppClass *ac)
{
    return il2cpp::vm::Array::GetElementSize(ac);
}
