using AOT;
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using UnityEngine.Assertions;
using Unity.Burst;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs.LowLevel.Unsafe;
using Unity.Mathematics;

namespace Unity.Collections
{
    internal struct UnmanagedArray<T> : IDisposable where T : unmanaged
    {
        IntPtr m_pointer;
        int m_length;
        public int Length => m_length;
        AllocatorManager.AllocatorHandle m_allocator;
        public UnmanagedArray(int length, AllocatorManager.AllocatorHandle allocator)
        {
            unsafe
            {
                m_pointer = (IntPtr)Memory.Unmanaged.Array.Allocate<T>(length, allocator);
            }
            m_length = length;
            m_allocator = allocator;
        }
        public void Dispose()
        {
            unsafe
            {
                Memory.Unmanaged.Free((T*)m_pointer, Allocator.Persistent);
            }
        }
        public unsafe T* GetUnsafePointer()
        {
            return (T*)m_pointer;
        }
        public ref T this[int index]
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get { unsafe { return ref ((T*)m_pointer)[index]; } }
        }
    }

    /// <summary>
    /// An allocator that is fast like a linear allocator, is threadsafe, and automatically invalidates
    /// all allocations made from it, when "rewound" by the user.
    /// </summary>
    [BurstCompile]
    public struct RewindableAllocator : AllocatorManager.IAllocator
    {
        internal struct Union
        {
            internal long m_long;

            // Number of bits used to store current position in a block to give out memory.
            // This limits the maximum block size to 1TB (2^40).
            const int currentBits = 40;
            // Offset of current position in m_long
            const int currentOffset = 0;
            // Number of bits used to store the allocation count in a block
            const long currentMask = (1L << currentBits) - 1;

            // Number of bits used to store allocation count in a block.
            // This limits the maximum number of allocations per block to 16 millions (2^24)
            const int allocCountBits = 24;
            // Offset of allocation count in m_long
            const int allocCountOffset = currentOffset + currentBits;
            const long allocCountMask = (1L << allocCountBits) - 1;

            // Current position in a block to give out memory
            internal long m_current
            {
                [MethodImpl(MethodImplOptions.AggressiveInlining)]
                get
                {
                    return (m_long >> currentOffset) & currentMask;
                }

                [MethodImpl(MethodImplOptions.AggressiveInlining)]
                set
                {
                    m_long &= ~(currentMask << currentOffset);
                    m_long |= (value & currentMask) << currentOffset;
                }
            }

            // The number of allocations in a block
            internal long m_allocCount
            {
                [MethodImpl(MethodImplOptions.AggressiveInlining)]
                get
                {
                    return (m_long >> allocCountOffset) & allocCountMask;
                }

                [MethodImpl(MethodImplOptions.AggressiveInlining)]
                set
                {
                    m_long &= ~(allocCountMask << allocCountOffset);
                    m_long |= (value & allocCountMask) << allocCountOffset;
                }
            }
        }
        [GenerateTestsForBurstCompatibility]
        internal unsafe struct MemoryBlock : IDisposable
        {
            // can't align any coarser than this many bytes
            public const int kMaximumAlignment = 16384;
            // pointer to contiguous memory
            public byte* m_pointer;
            // how many bytes of contiguous memory it points to
            public long m_bytes;
            // Union of current position to give out memory and allocation counts
            public Union m_union;

            public MemoryBlock(long bytes)
            {
                m_pointer = (byte*)Memory.Unmanaged.Allocate(bytes, kMaximumAlignment, Allocator.Persistent);
                Assert.IsTrue(m_pointer != null, "Memory block allocation failed, system out of memory");
                m_bytes = bytes;
                m_union = default;
            }

            public void Rewind()
            {
                m_union = default;
            }

            public void Dispose()
            {
                Memory.Unmanaged.Free(m_pointer, Allocator.Persistent);
                m_pointer = null;
                m_bytes = 0;
                m_union = default;
            }

            public bool Contains(IntPtr ptr)
            {
                unsafe
                {
                    void* pointer = (void*)ptr;
                    return (pointer >= m_pointer) && (pointer < m_pointer + m_union.m_current);
                }
            }
        };

        // Log2 of Maximum memory block size.  Cannot exceed MemoryBlock.Union.currentBits.
        const int kLog2MaxMemoryBlockSize = 26;

        // Maximum memory block size.  Can exceed maximum memory block size if user requested more.
        const long kMaxMemoryBlockSize = 1L << kLog2MaxMemoryBlockSize;  // 64MB

        /// Minimum memory block size, 128KB.
        const long kMinMemoryBlockSize = 128 * 1024;

        /// Maximum number of memory blocks.
        const int kMaxNumBlocks = 64;

        // Bit mask (bit 31) of the memory block busy flag indicating whether the block is busy rewinding.
        const int kBlockBusyRewindMask = 0x1 << 31;

        // Bit mask of the memory block busy flag indicating whether the block is busy allocating.
        const int kBlockBusyAllocateMask = ~kBlockBusyRewindMask;

        Spinner m_spinner;
        AllocatorManager.AllocatorHandle m_handle;
        UnmanagedArray<MemoryBlock> m_block;
        int m_last;                 // highest-index block that has memory to allocate from
        int m_used;                 // highest-index block that we actually allocated from, since last rewind
        byte m_enableBlockFree;     // flag indicating if allocator enables individual block free
        byte m_reachMaxBlockSize;   // flag indicating if reach maximum block size

        /// <summary>
        /// Initializes the allocator. Must be called before first use.
        /// </summary>
        /// <param name="initialSizeInBytes">The initial capacity of the allocator, in bytes</param>
        /// <param name="enableBlockFree">A flag indicating if allocator enables individual block free</param>
        public void Initialize(int initialSizeInBytes, bool enableBlockFree = false)
        {
            m_spinner = default;
            m_block = new UnmanagedArray<MemoryBlock>(kMaxNumBlocks, Allocator.Persistent);
            // Initial block size should be larger than min block size
            var blockSize = initialSizeInBytes > kMinMemoryBlockSize ? initialSizeInBytes : kMinMemoryBlockSize;
            m_block[0] = new MemoryBlock(blockSize);
            m_last = m_used = 0;
            m_enableBlockFree = enableBlockFree ? (byte)1 : (byte)0;
            m_reachMaxBlockSize = (initialSizeInBytes >= kMaxMemoryBlockSize) ? (byte)1 : (byte)0;
        }

        /// <summary>
        /// Property to get and set enable block free flag, a flag indicating whether the allocator should enable individual block to be freed.
        /// </summary>
        public bool EnableBlockFree
        {
            get => m_enableBlockFree != 0;
            set => m_enableBlockFree = value ? (byte)1 : (byte)0;
        }

        /// <summary>
        /// Retrieves the number of memory blocks that the allocator has requested from the system.
        /// </summary>
        public int BlocksAllocated => (int)(m_last + 1);

        /// <summary>
        /// Retrieves the size of the initial memory block, as requested in the Initialize function.
        /// </summary>
        public int InitialSizeInBytes => (int)(m_block[0].m_bytes);

        /// <summary>
        /// Retrieves the maximum memory block size.
        /// </summary>
        internal long MaxMemoryBlockSize => kMaxMemoryBlockSize;

        /// <summary>
        /// Retrieves the total bytes of the memory blocks allocated by this allocator.
        /// </summary>
        internal long BytesAllocated
        {
            get
            {
                long totalBytes = 0;
                for(int i = 0; i <= m_last; i++)
                {
                    totalBytes += m_block[i].m_bytes;
                }
                return totalBytes;
            }
        }

        /// <summary>
        /// Rewind the allocator; invalidate all allocations made from it, and potentially also free memory blocks
        /// it has allocated from the system.
        /// </summary>
        public void Rewind()
        {
            if (JobsUtility.IsExecutingJob)
                throw new InvalidOperationException("You cannot Rewind a RewindableAllocator from a Job.");
            m_handle.Rewind(); // bump the allocator handle version, invalidate all dependents
            while (m_last > m_used) // *delete* all blocks we didn't even allocate from this time around.
                m_block[m_last--].Dispose();
            while (m_used > 0) // simply *rewind* all blocks we used in this update, to avoid allocating again, every update.
                m_block[m_used--].Rewind();
            m_block[0].Rewind();
        }

        /// <summary>
        /// Dispose the allocator. This must be called to free the memory blocks that were allocated from the system.
        /// </summary>
        public void Dispose()
        {
            if (JobsUtility.IsExecutingJob)
                throw new InvalidOperationException("You cannot Dispose a RewindableAllocator from a Job.");
            m_used = 0; // so that we delete all blocks in Rewind() on the next line
            Rewind();
            m_block[0].Dispose();
            m_block.Dispose();
            m_last = m_used = 0;
        }

        /// <summary>
        /// All allocators must implement this property, in order to be installed in the custom allocator table.
        /// </summary>
        [ExcludeFromBurstCompatTesting("Uses managed delegate")]
        public AllocatorManager.TryFunction Function => Try;

        unsafe int TryAllocate(ref AllocatorManager.Block block, int startIndex, int lastIndex, long alignedSize, long alignmentMask)
        {
            for (int best = startIndex; best <= lastIndex; best++)
            {
                Union oldUnion;
                Union readUnion = default;
                long begin = 0;
                bool skip = false;
                readUnion.m_long = Interlocked.Read(ref m_block[best].m_union.m_long);
                do
                {
                    begin = (readUnion.m_current + alignmentMask) & ~alignmentMask;
                    if (begin + block.Bytes > m_block[best].m_bytes)
                    {
                        skip = true;
                        break;
                    }
                    oldUnion = readUnion;
                    Union newUnion = default;
                    newUnion.m_current = (begin + alignedSize) > m_block[best].m_bytes ? m_block[best].m_bytes : (begin + alignedSize);
                    newUnion.m_allocCount = readUnion.m_allocCount + 1;
                    readUnion.m_long = Interlocked.CompareExchange(ref m_block[best].m_union.m_long, newUnion.m_long, oldUnion.m_long);
                } while (readUnion.m_long != oldUnion.m_long);

                if(skip)
                {
                    continue;
                }

                block.Range.Pointer = (IntPtr)(m_block[best].m_pointer + begin);
                block.AllocatedItems = block.Range.Items;

                Interlocked.MemoryBarrier();
                int oldUsed;
                int readUsed;
                int newUsed;
                readUsed = m_used;
                do
                {
                    oldUsed = readUsed;
                    newUsed = best > oldUsed ? best : oldUsed;
                    readUsed = Interlocked.CompareExchange(ref m_used, newUsed, oldUsed);
                } while (newUsed != oldUsed);

                return AllocatorManager.kErrorNone;
            }

            return AllocatorManager.kErrorBufferOverflow;
        }

        /// <summary>
        /// Try to allocate, free, or reallocate a block of memory. This is an internal function, and
        /// is not generally called by the user.
        /// </summary>
        /// <param name="block">The memory block to allocate, free, or reallocate</param>
        /// <returns>0 if successful. Otherwise, returns the error code from the allocator function.</returns>
        public int Try(ref AllocatorManager.Block block)
        {
            if (block.Range.Pointer == IntPtr.Zero)
            {
                // Make the alignment multiple of cacheline size
                var alignment = math.max(JobsUtility.CacheLineSize, block.Alignment);
                var extra = alignment != JobsUtility.CacheLineSize ? 1 : 0;
                var cachelineMask = JobsUtility.CacheLineSize - 1;
                if (extra == 1)
                {
                    alignment = (alignment + cachelineMask) & ~cachelineMask;
                }

                // Adjust the size to be multiple of alignment, add extra alignment
                // to size if alignment is more than cacheline size
                var mask = alignment - 1L;
                var size = (block.Bytes + extra * alignment + mask) & ~mask;

                // Check all the blocks to see if any of them have enough memory
                var last = m_last;
                int error = TryAllocate(ref block, 0, m_last, size, mask);
                if (error == AllocatorManager.kErrorNone)
                {
                    return error;
                }

                // If that fails, allocate another block that's guaranteed big enough, and allocate from it.
                // Allocate twice as much as last time until it reaches MaxMemoryBlockSize, after that, increase
                // the block size by MaxMemoryBlockSize.
                m_spinner.Acquire();

                // After getting the lock, we must try to allocate again, because if many threads waited at
                // the lock, the first one allocates and when it unlocks, it's likely that there's space for the
                // other threads' allocations in the first thread's block.
                error = TryAllocate(ref block, last, m_last, size, mask);
                if (error == AllocatorManager.kErrorNone)
                {
                    m_spinner.Release();
                    return error;
                }

                long bytes;
                if (m_reachMaxBlockSize == 0)
                {
                    bytes = m_block[m_last].m_bytes << 1;
                }
                else
                {
                    bytes = m_block[m_last].m_bytes + kMaxMemoryBlockSize;
                }
                // if user asks more, skip smaller sizes
                bytes = math.max(bytes, size);
                m_reachMaxBlockSize = (bytes >= kMaxMemoryBlockSize) ? (byte)1 : (byte)0;
                m_block[m_last + 1] = new MemoryBlock(bytes);
                Interlocked.Increment(ref m_last);
                error = TryAllocate(ref block, m_last, m_last, size, mask);
                m_spinner.Release();
                return error;
            }

            // To free memory, no-op unless allocator enables individual block to be freed
            if (block.Range.Items == 0)
            {
                if (m_enableBlockFree != 0)
                {
                    for (int blockIndex = 0; blockIndex <= m_last; ++blockIndex)
                    {
                        if (m_block[blockIndex].Contains(block.Range.Pointer))
                        {
                            Union oldUnion;
                            Union readUnion = default;
                            readUnion.m_long = Interlocked.Read(ref m_block[blockIndex].m_union.m_long);
                            do
                            {
                                oldUnion = readUnion;
                                Union newUnion = readUnion;
                                newUnion.m_allocCount--;
                                if (newUnion.m_allocCount == 0)
                                {
                                    newUnion.m_current = 0;
                                }
                                readUnion.m_long = Interlocked.CompareExchange(ref m_block[blockIndex].m_union.m_long, newUnion.m_long, oldUnion.m_long);
                            } while (readUnion.m_long != oldUnion.m_long);
                        }
                    }
                }
                return 0; // we could check to see if the pointer belongs to us, if we want to be strict about it.
            }

            return -1;
        }

        [BurstCompile]
        [MonoPInvokeCallback(typeof(AllocatorManager.TryFunction))]
        internal static int Try(IntPtr state, ref AllocatorManager.Block block)
        {
            unsafe { return ((RewindableAllocator*)state)->Try(ref block); }
        }

        /// <summary>
        /// Retrieve the AllocatorHandle associated with this allocator. The handle is used as an index into a
        /// global table, for times when a reference to the allocator object isn't available.
        /// </summary>
        /// <value>The AllocatorHandle retrieved.</value>
        public AllocatorManager.AllocatorHandle Handle { get { return m_handle; } set { m_handle = value; } }

        /// <summary>
        /// Retrieve the Allocator associated with this allocator.
        /// </summary>
        /// <value>The Allocator retrieved.</value>
        public Allocator ToAllocator { get { return m_handle.ToAllocator; } }

        /// <summary>
        /// Check whether this AllocatorHandle is a custom allocator.
        /// </summary>
        /// <value>True if this AllocatorHandle is a custom allocator.</value>
        public bool IsCustomAllocator { get { return m_handle.IsCustomAllocator; } }

        /// <summary>
        /// Check whether this allocator will automatically dispose allocations.
        /// </summary>
        /// <remarks>Allocations made by Rewindable allocator are automatically disposed.</remarks>
        /// <value>Always true</value>
        public bool IsAutoDispose { get { return true; } }

        /// <summary>
        /// Allocate a NativeArray of type T from memory that is guaranteed to remain valid until the end of the
        /// next Update of this World. There is no need to Dispose the NativeArray so allocated. It is not possible
        /// to free the memory by Disposing it - it is automatically freed after the end of the next Update for this
        /// World.
        /// </summary>
        /// <typeparam name="T">The element type of the NativeArray to allocate.</typeparam>
        /// <param name="length">The length of the NativeArray to allocate, measured in elements.</param>
        /// <returns>The NativeArray allocated by this function.</returns>
        [GenerateTestsForBurstCompatibility(GenericTypeArguments = new[] { typeof(int) })]
        public NativeArray<T> AllocateNativeArray<T>(int length) where T : unmanaged
        {
            var container = new NativeArray<T>();
            unsafe
            {
                container.m_Buffer = this.AllocateStruct(default(T), length);
            }
            container.m_Length = length;
            container.m_AllocatorLabel = Allocator.None;
#if ENABLE_UNITY_COLLECTIONS_CHECKS
            container.m_MinIndex = 0;
            container.m_MaxIndex = length - 1;
            container.m_Safety = CollectionHelper.CreateSafetyHandle(ToAllocator);
            CollectionHelper.SetStaticSafetyId<NativeArray<T>>(ref container.m_Safety, ref NativeArrayExtensions.NativeArrayStaticId<T>.s_staticSafetyId.Data);
            Handle.AddSafetyHandle(container.m_Safety);
#endif
            return container;
        }

        /// <summary>
        /// Allocate a NativeList of type T from memory that is guaranteed to remain valid until the end of the
        /// next Update of this World. There is no need to Dispose the NativeList so allocated. It is not possible
        /// to free the memory by Disposing it - it is automatically freed after the end of the next Update for this
        /// World. The NativeList must be initialized with its maximum capacity; if it were to dynamically resize,
        /// up to 1/2 of the total final capacity would be wasted, because the memory can't be dynamically freed.
        /// </summary>
        /// <typeparam name="T">The element type of the NativeList to allocate.</typeparam>
        /// <param name="capacity">The capacity of the NativeList to allocate, measured in elements.</param>
        /// <returns>The NativeList allocated by this function.</returns>
        [GenerateTestsForBurstCompatibility(GenericTypeArguments = new[] { typeof(int) })]
        public NativeList<T> AllocateNativeList<T>(int capacity) where T : unmanaged
        {
            var container = new NativeList<T>();
            unsafe
            {
                container.m_ListData = this.Allocate(default(UnsafeList<T>), 1);
                container.m_ListData->Ptr = this.Allocate(default(T), capacity);
                container.m_ListData->m_length = 0;
                container.m_ListData->m_capacity = capacity;
                container.m_ListData->Allocator = Allocator.None;
            }
#if ENABLE_UNITY_COLLECTIONS_CHECKS
            container.m_Safety = CollectionHelper.CreateSafetyHandle(ToAllocator);
            CollectionHelper.SetStaticSafetyId<NativeList<T>>(ref container.m_Safety, ref NativeList<T>.s_staticSafetyId.Data);
            AtomicSafetyHandle.SetBumpSecondaryVersionOnScheduleWrite(container.m_Safety, true);
            Handle.AddSafetyHandle(container.m_Safety);
#endif
            return container;
        }
    }
}