using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Nez.Unmanaged.Collections
{
    [DebuggerDisplay("Length = {" + nameof(Length) + "}")]
    [DebuggerTypeProxy(typeof(NativeArrayDebugView<>))]
    public unsafe struct NativeArray<T> : IDisposable, IEnumerable<T>, IEnumerable where T : unmanaged
    {
        public struct Enumerator : IEnumerator<T>, IEnumerator, IDisposable
        {
            private NativeArray<T> _array;
            private int _index;

            object IEnumerator.Current => Current;

            public T Current => _array[_index];

            public Enumerator(ref NativeArray<T> array)
            {
                _array = array;
                _index = -1;
            }

            public void Dispose()
            { }

            public bool MoveNext()
            {
                _index++;
                return _index < _array.Length;
            }

            public void Reset() => _index = -1;
        }

        public int Length => _length;

        internal unsafe void* _buffer;
        internal int _length;
        internal Allocator _allocator;
        internal DisposeSentinel _DisposeSentinel;


        public unsafe T this[int index]
        {
            get => UnsafeUtility.ReadArrayElement<T>(_buffer, index);
            set => UnsafeUtility.WriteArrayElement<T>(_buffer, index, value);
        }

        public unsafe NativeArray(int length, Allocator allocator = Allocator.Persistent, NativeArrayOptions options = NativeArrayOptions.ClearMemory)
        {
            var bufferSize = Marshal.SizeOf<T>() * length;
            _buffer = (void*)UnsafeUtility.Alloc(bufferSize, allocator);

            if (options == NativeArrayOptions.ClearMemory)
                UnsafeUtility.Memset(_buffer, 0, bufferSize);

            _length = length;
            _allocator = allocator;
            DisposeSentinel.Create(out _DisposeSentinel, allocator);
        }

        public unsafe void* GetUnsafePtr() => _buffer;

        public void Dispose()
        {
            DisposeSentinel.Dispose(ref _DisposeSentinel);
            UnsafeUtility.Free((IntPtr)_buffer);
            _buffer = null;
            _length = 0;
        }

        public T[] ToArray()
        {
            var array = new T[Length];
            Copy(this, array, Length);
            return array;
        }

        #region IEnumerable

        public Enumerator GetEnumerator() => new Enumerator(ref this);

        IEnumerator<T> IEnumerable<T>.GetEnumerator() => new Enumerator(ref this);

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

        #endregion

        #region Equals

        public static bool operator ==(NativeArray<T> left, NativeArray<T> right) => left.Equals(right);

        public static bool operator !=(NativeArray<T> left, NativeArray<T> right) => !left.Equals(right);

        public bool Equals(NativeArray<T> other)
        {
            return _buffer == other._buffer && _length == other._length;
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj))
                return false;
            return obj is NativeArray<T> && Equals((NativeArray<T>)obj);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                return ((int)_buffer * 397) ^ _length;
            }
        }

        #endregion

        #region Copying

        public static void Copy(T[] src, NativeArray<T> dst)
        {
            if (src.Length != dst.Length)
                throw new ArgumentException("source and destination length must be the same");

            Copy(src, 0, dst, 0, src.Length);
        }

        public static void Copy(NativeArray<T> src, T[] dst)
        {
            if (src.Length != dst.Length)
                throw new ArgumentException("source and destination length must be the same");

            Copy(src, 0, dst, 0, src.Length);
        }

        public static void Copy(NativeArray<T> src, NativeArray<T> dst)
        {
            if (src.Length != dst.Length)
                throw new ArgumentException("source and destination length must be the same");

            Copy(src, 0, dst, 0, src.Length);
        }

        public static void Copy(NativeArray<T> src, T[] dst, int length)
        {
            Copy(src, 0, dst, 0, length);
        }

        public static void Copy(NativeArray<T> src, NativeArray<T> dst, int length)
        {
            Copy(src, 0, dst, 0, length);
        }

        public static void Copy(NativeArray<T> src, int srcIndex, NativeArray<T> dst, int dstIndex, int length)
        {
            var size = Unsafe.SizeOf<T>();
            var destAddr = (byte*)dst._buffer + dstIndex * size;
            var srcAddr = (byte*)src._buffer + srcIndex * size;
            Unsafe.CopyBlock(destAddr, srcAddr, (uint)(length * size));
        }

        public static void Copy(T[] src, int srcIndex, NativeArray<T> dst, int dstIndex, int length)
        {
            var handle = GCHandle.Alloc(src, GCHandleType.Pinned);
            var addr = handle.AddrOfPinnedObject();

            var size = Unsafe.SizeOf<T>();
            var destAddr = (byte*)dst._buffer + dstIndex * size;
            var srcAddr = (byte*)addr + srcIndex * size;
            Unsafe.CopyBlock(destAddr, srcAddr, (uint)(length * size));

            handle.Free();
        }

        public static void Copy(NativeArray<T> src, int srcIndex, T[] dst, int dstIndex, int length)
        {
            var handle = GCHandle.Alloc(dst, GCHandleType.Pinned);
            var addr = handle.AddrOfPinnedObject();

            var size = Unsafe.SizeOf<T>();
            var srcAddr = (byte*)src._buffer + srcIndex * size;
            var dstAddr = (byte*)addr + dstIndex * size;
            Unsafe.CopyBlock(dstAddr, srcAddr, (uint)(length * size));

            handle.Free();
        }

        #endregion

        internal sealed class NativeArrayDebugView<TU> where TU : unmanaged
        {
            private NativeArray<TU> _array;

            public NativeArrayDebugView(NativeArray<TU> array) => _array = array;

            public TU[] Items => _array.ToArray();
        }

    }

}