using System;
using System.Runtime.InteropServices;
using AOT;
using NUnit.Framework;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Jobs.LowLevel.Unsafe;
using Unity.Collections.Tests;

internal class CustomAllocatorTests : CollectionsTestCommonBase
{

#if !NET_DOTS
    [TestCase(1337,   1337)]
    [TestCase(0xFFFF, 0x0000)]
    [TestCase(0x0000, 0xFFFF)]
    [TestCase(0xFFFF, 0xFFFF)]
    [TestCase(0x0000, 0x0000)]
    public void AllocatorHandleToAllocatorRoundTripWorks(int i, int v)
    {
        var Index = (ushort)i;
        var Version = (ushort)v;
        AllocatorManager.AllocatorHandle srcHandle = new AllocatorManager.AllocatorHandle{ Index = Index, Version = Version };
        Allocator srcAllocator = srcHandle.ToAllocator;
        AllocatorManager.AllocatorHandle destHandle = srcAllocator;
        Assert.AreEqual(srcHandle.Index, destHandle.Index);
        Assert.AreEqual(srcHandle.Version, destHandle.Version);
        Allocator destAllocator = destHandle.ToAllocator;
        Assert.AreEqual(srcAllocator, destAllocator);
    }
#endif

    [Test]
    public void AllocatorVersioningWorks()
    {
        AllocatorManager.Initialize();
        var origin = AllocatorManager.Persistent;
        var storage = origin.AllocateBlock(default(byte), 100000); // allocate a block of bytes from Malloc.Persistent
        for(var i = 1; i <= 3; ++i)
        {
            var allocatorHelper = new AllocatorHelper<AllocatorManager.StackAllocator>(AllocatorManager.Persistent);
            ref var allocator = ref allocatorHelper.Allocator;
            allocator.Initialize(storage);
            var oldIndex = allocator.Handle.Index;
            var oldVersion = allocator.Handle.Version;
            allocator.Dispose();
            var newVersion = AllocatorManager.SharedStatics.Version.Ref.Data.ElementAt(oldIndex);
            Assert.AreEqual(oldVersion + 1, newVersion);
            allocatorHelper.Dispose();
        }
        storage.Dispose();
        AllocatorManager.Shutdown();
    }


#if !UNITY_DOTSRUNTIME
    [Test]
    public void ReleasingChildHandlesWorks()
    {
        AllocatorManager.Initialize();
        var origin = AllocatorManager.Persistent;
        var storage = origin.AllocateBlock(default(byte), 100000); // allocate a block of bytes from Malloc.Persistent
        var allocatorHelper = new AllocatorHelper<AllocatorManager.StackAllocator>(AllocatorManager.Persistent);
        ref var allocator = ref allocatorHelper.Allocator;
        allocator.Initialize(storage);
        var list = NativeList<int>.New(10, ref allocator);
        list.Add(0); // put something in the list, so it'll have a size for later
        allocator.Dispose(); // ok to tear down the storage that the stack allocator used, too.
        Assert.Throws<ObjectDisposedException>(
        () => {
            list[0] = 0; // we haven't disposed this list, but it was released automatically already. so this is an error.
        });
        storage.Dispose();
        allocatorHelper.Dispose();
        AllocatorManager.Shutdown();
    }

    [Test]
    public unsafe void ReleasingChildAllocatorsWorks()
    {
        AllocatorManager.Initialize();

        var origin = AllocatorManager.Persistent;
        var parentStorage = origin.AllocateBlock(default(byte), 100000); // allocate a block of bytes from Malloc.Persistent
        var parentHelper = new AllocatorHelper<AllocatorManager.StackAllocator>(AllocatorManager.Persistent);
        ref var parent = ref parentHelper.Allocator;

        parent.Initialize(parentStorage);  // and make a stack allocator from it

        var childStorage = parent.AllocateBlock(default(byte), 10000); // allocate some space from the parent
        var childHelper = new AllocatorHelper<AllocatorManager.StackAllocator>(AllocatorManager.Persistent);
        childHelper.Allocator.Initialize(childStorage);  // and make a stack allocator from it

        parent.Dispose(); // tear down the parent allocator

        Assert.Throws<ArgumentException>(() =>
        {
            childHelper.Allocator.Allocate(default(byte), 1000); // try to allocate from the child - it should fail.
        });

        parentStorage.Dispose();
        parentHelper.Dispose();
        childHelper.Dispose();
        AllocatorManager.Shutdown();
    }
#endif

    [Test]
    public void AllocatesAndFreesFromMono()
    {
        AllocatorManager.Initialize();
        const int kLength = 100;

        var expectedAlignment = math.max(JobsUtility.CacheLineSize, UnsafeUtility.AlignOf<int>());

        for (int i = 0; i < kLength; ++i)
        {
            var allocator = AllocatorManager.Persistent;
            var block = allocator.AllocateBlock(default(int), i);
            if(i != 0)
                Assert.AreNotEqual(IntPtr.Zero, block.Range.Pointer);
            Assert.AreEqual(i, block.Range.Items);
            Assert.AreEqual(UnsafeUtility.SizeOf<int>(), block.BytesPerItem);
            Assert.AreEqual(expectedAlignment, block.Alignment);
            Assert.AreEqual(AllocatorManager.Persistent.Value, block.Range.Allocator.Value);
            allocator.FreeBlock(ref block);
        }
        AllocatorManager.Shutdown();
    }

    [BurstCompile(CompileSynchronously = true)]
    struct AllocateJob : IJobParallelFor
    {
        public NativeArray<AllocatorManager.Block> m_blocks;
        public void Execute(int index)
        {
            var allocator = AllocatorManager.Persistent;
            m_blocks[index] = allocator.AllocateBlock(default(int), index);
        }
    }

    [BurstCompile(CompileSynchronously = true)]
    struct FreeJob : IJobParallelFor
    {
        public NativeArray<AllocatorManager.Block> m_blocks;
        public void Execute(int index)
        {
            var temp = m_blocks[index];
            temp.Free();
            m_blocks[index] = temp;
        }
    }

    [Test]
    public void AllocatesAndFreesFromBurst()
    {
        AllocatorManager.Initialize();

        const int kLength = 100;
        var blocks = new NativeArray<AllocatorManager.Block>(kLength, Allocator.Persistent);
        var allocateJob = new AllocateJob();
        allocateJob.m_blocks = blocks;
        allocateJob.Schedule(kLength, 1).Complete();

        var expectedAlignment = math.max(JobsUtility.CacheLineSize, UnsafeUtility.AlignOf<int>());

        for (int i = 0; i < kLength; ++i)
        {
            var block = allocateJob.m_blocks[i];
            if(i != 0)
                Assert.AreNotEqual(IntPtr.Zero, block.Range.Pointer);
            Assert.AreEqual(i, block.Range.Items);
            Assert.AreEqual(UnsafeUtility.SizeOf<int>(), block.BytesPerItem);
            Assert.AreEqual(expectedAlignment, block.Alignment);
            Assert.AreEqual(AllocatorManager.Persistent.Value, block.Range.Allocator.Value);
        }

        var freeJob = new FreeJob();
        freeJob.m_blocks = blocks;
        freeJob.Schedule(kLength, 1).Complete();

        for (int i = 0; i < kLength; ++i)
        {
            var block = allocateJob.m_blocks[i];
            Assert.AreEqual(IntPtr.Zero, block.Range.Pointer);
            Assert.AreEqual(0, block.Range.Items);
            Assert.AreEqual(UnsafeUtility.SizeOf<int>(), block.BytesPerItem);
            Assert.AreEqual(expectedAlignment, block.Alignment);
            Assert.AreEqual(AllocatorManager.Persistent.Value, block.Range.Allocator.Value);
        }
        blocks.Dispose();
        AllocatorManager.Shutdown();
    }

    // This allocator wraps UnsafeUtility.Malloc, but also initializes memory to some constant value after allocating.
    [BurstCompile(CompileSynchronously = true)]
    struct ClearToValueAllocator : AllocatorManager.IAllocator
    {
        public AllocatorManager.AllocatorHandle Handle { get { return m_handle; } set { m_handle = value; } }

        public Allocator ToAllocator { get { return m_handle.ToAllocator; } }

        public bool IsCustomAllocator { get { return m_handle.IsCustomAllocator; } }

        internal AllocatorManager.AllocatorHandle m_handle;
        internal AllocatorManager.AllocatorHandle m_parent;

        public byte m_clearValue;

        public void Initialize<T>(byte ClearValue, ref T parent) where T : unmanaged, AllocatorManager.IAllocator
        {
            m_parent = parent.Handle;
            m_clearValue = ClearValue;
#if ENABLE_UNITY_COLLECTIONS_CHECKS
            parent.Handle.AddChildAllocator(m_handle);
#endif
        }

        public unsafe int Try(ref AllocatorManager.Block block)
        {
            var temp = block.Range.Allocator;
            block.Range.Allocator = m_parent;
            var error = AllocatorManager.Try(ref block);
            block.Range.Allocator = temp;
            if (error != 0)
                return error;
            if (block.Range.Pointer != IntPtr.Zero) // if we allocated or reallocated...
                UnsafeUtility.MemSet((void*)block.Range.Pointer, m_clearValue, block.Bytes); // clear to a value.
            return 0;
        }

        [BurstCompile(CompileSynchronously = true)]
		[MonoPInvokeCallback(typeof(AllocatorManager.TryFunction))]
        public static unsafe int Try(IntPtr state, ref AllocatorManager.Block block)
        {
            return ((ClearToValueAllocator*)state)->Try(ref block);
        }

        public AllocatorManager.TryFunction Function => Try;
        public void Dispose()
        {
            m_handle.Dispose();
        }
    }

    [BurstCompile(CompileSynchronously = true)]
    internal struct CountingAllocator : AllocatorManager.IAllocator
    {
        public AllocatorManager.AllocatorHandle Handle { get { return m_handle; } set { m_handle = value; } }

        public Allocator ToAllocator { get { return m_handle.ToAllocator; } }

        public bool IsCustomAllocator { get { return m_handle.IsCustomAllocator; } }
        public AllocatorManager.AllocatorHandle m_handle;
        public int AllocationCount;
        public bool WasUsed => AllocationCount > 0;
        public void Initialize()
        {
            AllocationCount = 0;
#if ENABLE_UNITY_ALLOCATIONS_CHECKS
            AllocatorManager.Persistent.Handle.AddChildAllocator(m_handle);
#endif
        }

        public int Try(ref AllocatorManager.Block block)
        {
            var temp = block.Range.Allocator;
            block.Range.Allocator = AllocatorManager.Persistent;
            var error = AllocatorManager.Try(ref block);
            block.Range.Allocator = temp;
            if (error != 0)
                return error;
            if (block.Range.Pointer != IntPtr.Zero) // if we allocated or reallocated...
                ++AllocationCount;
            return 0;
        }

        [BurstCompile(CompileSynchronously = true)]
        [MonoPInvokeCallback(typeof(AllocatorManager.TryFunction))]
        public static unsafe int Try(IntPtr state, ref AllocatorManager.Block block)
        {
            return ((CountingAllocator*)state)->Try(ref block);
        }

        public AllocatorManager.TryFunction Function => Try;
        public void Dispose()
        {
            m_handle.Dispose();
        }
    }

    [Test]
    public void UserDefinedAllocatorWorks()
    {
        AllocatorManager.Initialize();
        var parent = AllocatorManager.Persistent;
        var allocatorHelper = new AllocatorHelper<ClearToValueAllocator>(AllocatorManager.Persistent);
        ref var allocator = ref allocatorHelper.Allocator;
        allocator.Initialize(0, ref parent);

        var expectedAlignment = math.max(JobsUtility.CacheLineSize, UnsafeUtility.AlignOf<int>());

        for (byte ClearValue = 0; ClearValue < 0xF; ++ClearValue)
        {
            allocator.m_clearValue = ClearValue;
            const int kLength = 100;
            for (int i = 1; i < kLength; ++i)
            {
                var block = allocator.AllocateBlock(default(int), i);
                Assert.AreNotEqual(IntPtr.Zero, block.Range.Pointer);
                Assert.AreEqual(i, block.Range.Items);
                Assert.AreEqual(UnsafeUtility.SizeOf<int>(), block.BytesPerItem);
                Assert.AreEqual(expectedAlignment, block.Alignment);
                allocator.FreeBlock(ref block);
            }
        }
        allocator.Dispose();
        allocatorHelper.Dispose();
        AllocatorManager.Shutdown();
    }

    // this is testing for the case where we want+ to install a stack allocator that itself allocates from a big hunk
    // of memory provided by the default Persistent allocator, and then make allocations on the stack allocator.
    [Test]
    public void StackAllocatorWorks()
    {
        AllocatorManager.Initialize();
        var origin = AllocatorManager.Persistent;
        var backingStorage = origin.AllocateBlock(default(byte), 100000); // allocate a block of bytes from Malloc.Persistent
        var allocatorHelper = new AllocatorHelper<AllocatorManager.StackAllocator>(AllocatorManager.Persistent);
        ref var allocator = ref allocatorHelper.Allocator;
        allocator.Initialize(backingStorage);

        var expectedAlignment = math.max(JobsUtility.CacheLineSize, UnsafeUtility.AlignOf<int>());

        const int kLength = 100;
        for (int i = 1; i < kLength; ++i)
        {
            var block = allocator.AllocateBlock(default(int), i);
            Assert.AreNotEqual(IntPtr.Zero, block.Range.Pointer);
            Assert.AreEqual(i, block.Range.Items);
            Assert.AreEqual(UnsafeUtility.SizeOf<int>(), block.BytesPerItem);
            Assert.AreEqual(expectedAlignment, block.Alignment);
            allocator.FreeBlock(ref block);
        }
        allocator.Dispose();
        backingStorage.Dispose();
        allocatorHelper.Dispose();
        AllocatorManager.Shutdown();
    }

    [Test]
    public void CustomAllocatorNativeListWorksWithoutHandles()
    {
        AllocatorManager.Initialize();
        var allocator = AllocatorManager.Persistent;
        var list = NativeList<byte>.New(100, ref allocator);
        list.Dispose(ref allocator);
        AllocatorManager.Shutdown();
    }

#if !UNITY_DOTSRUNTIME
    [Test]
    public void CustomAllocatorNativeListThrowsWhenAllocatorIsWrong()
    {
        AllocatorManager.Initialize();
        var allocator0 = AllocatorManager.Persistent;
        var list = NativeList<byte>.New(100, ref allocator0);
        Assert.Throws<ArgumentOutOfRangeException>(() =>
        {
            list.Dispose(ref CommonRwdAllocatorHelper.Allocator);
        });
        list.Dispose(ref allocator0);
        AllocatorManager.Shutdown();
    }
#endif

    // this is testing for the case where we want to install a custom allocator that clears memory to a constant
    // byte value, and then have an UnsafeList use that custom allocator.
    [Test]
    public void CustomAllocatorUnsafeListWorks()
    {
        AllocatorManager.Initialize();
        var parent = AllocatorManager.Persistent;
        var allocatorHelper = new AllocatorHelper<ClearToValueAllocator>(AllocatorManager.Persistent);
        ref var allocator = ref allocatorHelper.Allocator;
        allocator.Initialize(0xFE, ref parent);
        for (byte ClearValue = 0; ClearValue < 0xF; ++ClearValue)
        {
            allocator.m_clearValue = ClearValue;
            var unsafelist = new UnsafeList<byte>(1, allocator.Handle);
            const int kLength = 100;
            unsafelist.Resize(kLength);
            for (int i = 0; i < kLength; ++i)
                Assert.AreEqual(ClearValue, unsafelist[i]);
            unsafelist.Dispose();
        }
        allocator.Dispose();
        allocatorHelper.Dispose();
        AllocatorManager.Shutdown();
    }

    [Test]
    public unsafe void SlabAllocatorWorks()
    {
        var SlabSizeInBytes = 256;
        var SlabSizeInInts = SlabSizeInBytes / sizeof(int);
        var Slabs = 256;
        AllocatorManager.Initialize();
        var origin = AllocatorManager.Persistent;
        var backingStorage = origin.AllocateBlock(default(byte), Slabs * SlabSizeInBytes); // allocate a block of bytes from Malloc.Persistent
        var allocatorHelper = new AllocatorHelper<AllocatorManager.SlabAllocator>(AllocatorManager.Persistent);
        ref var allocator = ref allocatorHelper.Allocator;
        allocator.Initialize(backingStorage, SlabSizeInBytes, Slabs * SlabSizeInBytes);

        var expectedAlignment = math.max(JobsUtility.CacheLineSize, UnsafeUtility.AlignOf<int>());

        var block0 = allocator.AllocateBlock(default(int), SlabSizeInInts);
        Assert.AreNotEqual(IntPtr.Zero, block0.Range.Pointer);
        Assert.AreEqual(SlabSizeInInts, block0.Range.Items);
        Assert.AreEqual(UnsafeUtility.SizeOf<int>(), block0.BytesPerItem);
        Assert.AreEqual(expectedAlignment, block0.Alignment);
        Assert.AreEqual(1, allocator.Occupied[0]);

        var block1 = allocator.AllocateBlock(default(int), SlabSizeInInts - 1);
        Assert.AreNotEqual(IntPtr.Zero, block1.Range.Pointer);
        Assert.AreEqual(SlabSizeInInts - 1, block1.Range.Items);
        Assert.AreEqual(UnsafeUtility.SizeOf<int>(), block1.BytesPerItem);
        Assert.AreEqual(expectedAlignment, block1.Alignment);
        Assert.AreEqual(3, allocator.Occupied[0]);

        allocator.FreeBlock(ref block0);
        Assert.AreEqual(2, allocator.Occupied[0]);
        allocator.FreeBlock(ref block1);
        Assert.AreEqual(0, allocator.Occupied[0]);

        Assert.Throws<ArgumentException>(() =>
        {
            allocatorHelper.Allocator.AllocateBlock(default(int), 65);
        });

        allocator.Dispose();
        backingStorage.Dispose();
        allocatorHelper.Dispose();
        AllocatorManager.Shutdown();
    }

    [Test]
    public unsafe void CollectionHelper_IsAligned()
    {
        Assert.Throws<ArgumentException>(() => CollectionHelper.IsAligned((void*)0x0, 0)); // value is 0
        Assert.Throws<ArgumentException>(() => CollectionHelper.IsAligned((void*)0x1000, 0)); // alignment is 0
        Assert.Throws<ArgumentException>(() => CollectionHelper.IsAligned((void*)0x1000, 3)); // alignment is not pow2

        for (var i = 0; i < 31; ++i)
        {
            Assert.IsTrue(CollectionHelper.IsAligned((void*)0x80000000, 1<<i));
        }
    }

    [Test]
    public void AllocatorManager_AllocateBlock_UsesAlignmentArgument()
    {
        int sizeOf = 1;
        int alignOf = 4096;
        int items = 1;
        var temp = AllocatorManager.Temp;
        var block = temp.AllocateBlock(sizeOf, alignOf, items);
        Assert.AreNotEqual(IntPtr.Zero, block.Range.Pointer);

        unsafe
        {
            Assert.IsTrue(CollectionHelper.IsAligned((void*)block.Range.Pointer, alignOf));
        }
    }

    [Test]
    public void AllocatorManager_AllocateBlock_AlwaysCacheLineAligned()
    {
        int sizeOf = 1;
        int items = 1;
        var temp = AllocatorManager.Temp;

        for (int alignment = 1; alignment < 256; ++alignment)
        {
            var block = temp.AllocateBlock(sizeOf, alignment, items);
            Assert.AreNotEqual(IntPtr.Zero, block.Range.Pointer);

            unsafe
            {
                Assert.IsTrue(CollectionHelper.IsAligned((void*)block.Range.Pointer, CollectionHelper.CacheLineSize));
            }
        }
    }
}