using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.HLE.HOS.Services.SurfaceFlinger.Types;
using System;
using System.Collections.Generic;
using System.Threading;

namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
{
    class BufferQueueCore
    {
        public BufferSlotArray       Slots;
        public int                   OverrideMaxBufferCount;
        public bool                  UseAsyncBuffer;
        public bool                  DequeueBufferCannotBlock;
        public PixelFormat           DefaultBufferFormat;
        public int                   DefaultWidth;
        public int                   DefaultHeight;
        public int                   DefaultMaxBufferCount;
        public int                   MaxAcquiredBufferCount;
        public bool                  BufferHasBeenQueued;
        public ulong                 FrameCounter;
        public NativeWindowTransform TransformHint;
        public bool                  IsAbandoned;
        public NativeWindowApi       ConnectedApi;
        public bool                  IsAllocating;
        public IProducerListener     ProducerListener;
        public IConsumerListener     ConsumerListener;
        public bool                  ConsumerControlledByApp;
        public uint                  ConsumerUsageBits;
        public List<BufferItem>      Queue;
        public BufferInfo[]          BufferHistory;
        public uint                  BufferHistoryPosition;
        public bool                  EnableExternalEvent;
        public int                   MaxBufferCountCached;

        public readonly object Lock = new object();

        private KEvent _waitBufferFreeEvent;
        private KEvent _frameAvailableEvent;

        public ulong Owner { get; }

        public bool Active { get; private set; }

        public const int BufferHistoryArraySize = 8;

        public event Action BufferQueued;

        public BufferQueueCore(Switch device, ulong pid)
        {
            Slots                    = new BufferSlotArray();
            IsAbandoned              = false;
            OverrideMaxBufferCount   = 0;
            DequeueBufferCannotBlock = false;
            UseAsyncBuffer           = false;
            DefaultWidth             = 1;
            DefaultHeight            = 1;
            DefaultMaxBufferCount    = 2;
            MaxAcquiredBufferCount   = 1;
            FrameCounter             = 0;
            TransformHint            = 0;
            DefaultBufferFormat      = PixelFormat.Rgba8888;
            IsAllocating             = false;
            ProducerListener         = null;
            ConsumerListener         = null;
            ConsumerUsageBits        = 0;

            Queue = new List<BufferItem>();

            // TODO: CreateGraphicBufferAlloc?

            _waitBufferFreeEvent  = new KEvent(device.System.KernelContext);
            _frameAvailableEvent = new KEvent(device.System.KernelContext);

            Owner = pid;

            Active = true;

            BufferHistory        = new BufferInfo[BufferHistoryArraySize];
            EnableExternalEvent  = true;
            MaxBufferCountCached = 0;
        }

        public int GetMinUndequeuedBufferCountLocked(bool async)
        {
            if (!UseAsyncBuffer)
            {
                return 0;
            }

            if (DequeueBufferCannotBlock || async)
            {
                return MaxAcquiredBufferCount + 1;
            }

            return MaxAcquiredBufferCount;
        }

        public int GetMinMaxBufferCountLocked(bool async)
        {
            return GetMinUndequeuedBufferCountLocked(async);
        }

        public void UpdateMaxBufferCountCachedLocked(int slot)
        {
            if (MaxBufferCountCached <= slot)
            {
                MaxBufferCountCached = slot + 1;
            }
        }

        public int GetMaxBufferCountLocked(bool async)
        {
            int minMaxBufferCount = GetMinMaxBufferCountLocked(async);

            int maxBufferCount = Math.Max(DefaultMaxBufferCount, minMaxBufferCount);

            if (OverrideMaxBufferCount != 0)
            {
                return OverrideMaxBufferCount;
            }

            // Preserve all buffers already in control of the producer and the consumer.
            for (int slot = maxBufferCount; slot < Slots.Length; slot++)
            {
                BufferState state = Slots[slot].BufferState;

                if (state == BufferState.Queued || state == BufferState.Dequeued)
                {
                    maxBufferCount = slot + 1;
                }
            }

            return maxBufferCount;
        }

        public Status SetDefaultMaxBufferCountLocked(int count)
        {
            int minBufferCount = UseAsyncBuffer ? 2 : 1;

            if (count < minBufferCount || count > Slots.Length)
            {
                return Status.BadValue;
            }

            DefaultMaxBufferCount = count;

            SignalDequeueEvent();

            return Status.Success;
        }

        public void SignalWaitBufferFreeEvent()
        {
            if (EnableExternalEvent)
            {
                _waitBufferFreeEvent.WritableEvent.Signal();
            }
        }

        public void SignalFrameAvailableEvent()
        {
            if (EnableExternalEvent)
            {
                _frameAvailableEvent.WritableEvent.Signal();
            }
        }

        public void PrepareForExit()
        {
            lock (Lock)
            {
                Active = false;

                Monitor.PulseAll(Lock);
            }
        }

        // TODO: Find an accurate way to handle a regular condvar here as this will wake up unwanted threads in some edge cases.
        public void SignalDequeueEvent()
        {
            Monitor.PulseAll(Lock);
        }

        public void WaitDequeueEvent()
        {
            WaitForLock();
        }

        public void SignalIsAllocatingEvent()
        {
            Monitor.PulseAll(Lock);
        }

        public void WaitIsAllocatingEvent()
        {
            WaitForLock();
        }

        public void SignalQueueEvent()
        {
            BufferQueued?.Invoke();
        }

        private void WaitForLock()
        {
            if (Active)
            {
                Monitor.Wait(Lock);
            }
        }

        public void FreeBufferLocked(int slot)
        {
            Slots[slot].GraphicBuffer.Reset();

            if (Slots[slot].BufferState == BufferState.Acquired)
            {
                Slots[slot].NeedsCleanupOnRelease = true;
            }

            Slots[slot].BufferState      = BufferState.Free;
            Slots[slot].FrameNumber      = uint.MaxValue;
            Slots[slot].AcquireCalled    = false;
            Slots[slot].Fence.FenceCount = 0;
        }

        public void FreeAllBuffersLocked()
        {
            BufferHasBeenQueued = false;

            for (int slot = 0; slot < Slots.Length; slot++)
            {
                FreeBufferLocked(slot);
            }
        }

        public bool StillTracking(ref BufferItem item)
        {
            BufferSlot slot = Slots[item.Slot];

            // TODO: Check this. On Android, this checks the "handle". I assume NvMapHandle is the handle, but it might not be.
            return !slot.GraphicBuffer.IsNull && slot.GraphicBuffer.Object.Buffer.Surfaces[0].NvMapHandle == item.GraphicBuffer.Object.Buffer.Surfaces[0].NvMapHandle;
        }

        public void WaitWhileAllocatingLocked()
        {
            while (IsAllocating)
            {
                WaitIsAllocatingEvent();
            }
        }

        public void CheckSystemEventsLocked(int maxBufferCount)
        {
            if (!EnableExternalEvent)
            {
                return;
            }

            bool needBufferReleaseSignal  = false;
            bool needFrameAvailableSignal = false;

            if (maxBufferCount > 1)
            {
                for (int i = 0; i < maxBufferCount; i++)
                {
                    if (Slots[i].BufferState == BufferState.Queued)
                    {
                        needFrameAvailableSignal = true;
                    }
                    else if (Slots[i].BufferState == BufferState.Free)
                    {
                        needBufferReleaseSignal = true;
                    }
                }
            }

            if (needBufferReleaseSignal)
            {
                SignalWaitBufferFreeEvent();
            }
            else
            {
                _waitBufferFreeEvent.WritableEvent.Clear();
            }

            if (needFrameAvailableSignal)
            {
                SignalFrameAvailableEvent();
            }
            else
            {
                _frameAvailableEvent.WritableEvent.Clear();
            }
        }

        public bool IsProducerConnectedLocked()
        {
            return ConnectedApi != NativeWindowApi.NoApi;
        }

        public bool IsConsumerConnectedLocked()
        {
            return ConsumerListener != null;
        }

        public KReadableEvent GetWaitBufferFreeEvent()
        {
            lock (Lock)
            {
                return _waitBufferFreeEvent.ReadableEvent;
            }
        }

        public bool IsOwnedByConsumerLocked(int slot)
        {
            if (Slots[slot].BufferState != BufferState.Acquired)
            {
                Logger.Error?.Print(LogClass.SurfaceFlinger, $"Slot {slot} is not owned by the consumer (state = {Slots[slot].BufferState})");

                return false;
            }

            return true;
        }

        public bool IsOwnedByProducerLocked(int slot)
        {
            if (Slots[slot].BufferState != BufferState.Dequeued)
            {
                Logger.Error?.Print(LogClass.SurfaceFlinger, $"Slot {slot} is not owned by the producer (state = {Slots[slot].BufferState})");

                return false;
            }

            return true;
        }
    }
}
