// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program.  If not, see .
//
using Ryujinx.Audio.Integration;
using Ryujinx.Common;
using System;
using System.Diagnostics;
namespace Ryujinx.Audio.Common
{
    /// 
    /// An audio device session.
    /// 
    class AudioDeviceSession : IDisposable
    {
        /// 
        /// The volume of the .
        /// 
        private float _volume;
        /// 
        /// The state of the .
        /// 
        private AudioDeviceState _state;
        /// 
        /// Array of all buffers currently used or released.
        /// 
        private AudioBuffer[] _buffers;
        /// 
        /// The server index inside  (appended but not queued to device driver).
        /// 
        private uint _serverBufferIndex;
        /// 
        /// The hardware index inside  (queued to device driver).
        /// 
        private uint _hardwareBufferIndex;
        /// 
        /// The released index inside  (released by the device driver).
        /// 
        private uint _releasedBufferIndex;
        /// 
        /// The count of buffer appended (server side).
        /// 
        private uint _bufferAppendedCount;
        /// 
        /// The count of buffer registered (driver side).
        /// 
        private uint _bufferRegisteredCount;
        /// 
        /// The count of buffer released (released by the driver side).
        /// 
        private uint _bufferReleasedCount;
        /// 
        /// The released buffer event.
        /// 
        private IWritableEvent _bufferEvent;
        /// 
        /// The session on the device driver.
        /// 
        private IHardwareDeviceSession _hardwareDeviceSession;
        /// 
        /// Max number of buffers that can be registered to the device driver at a time.
        /// 
        private uint _bufferRegisteredLimit;
        /// 
        /// Create a new .
        /// 
        /// The device driver session associated
        /// The release buffer event
        /// The max number of buffers that can be registered to the device driver at a time
        public AudioDeviceSession(IHardwareDeviceSession deviceSession, IWritableEvent bufferEvent, uint bufferRegisteredLimit = 4)
        {
            _bufferEvent = bufferEvent;
            _hardwareDeviceSession = deviceSession;
            _bufferRegisteredLimit = bufferRegisteredLimit;
            _buffers = new AudioBuffer[Constants.AudioDeviceBufferCountMax];
            _serverBufferIndex = 0;
            _hardwareBufferIndex = 0;
            _releasedBufferIndex = 0;
            _bufferAppendedCount = 0;
            _bufferRegisteredCount = 0;
            _bufferReleasedCount = 0;
            _volume = 1.0f;
            _state = AudioDeviceState.Stopped;
        }
        /// 
        /// Get the released buffer event.
        /// 
        /// The released buffer event
        public IWritableEvent GetBufferEvent()
        {
            return _bufferEvent;
        }
        /// 
        /// Get the state of the session.
        /// 
        /// The state of the session
        public AudioDeviceState GetState()
        {
            Debug.Assert(_state == AudioDeviceState.Started || _state == AudioDeviceState.Stopped);
            return _state;
        }
        /// 
        /// Get the total buffer count (server + driver + released).
        /// 
        /// Return the total buffer count
        private uint GetTotalBufferCount()
        {
            uint bufferCount = _bufferAppendedCount + _bufferRegisteredCount + _bufferReleasedCount;
            Debug.Assert(bufferCount <= Constants.AudioDeviceBufferCountMax);
            return bufferCount;
        }
        /// 
        /// Register a new  on the server side.
        /// 
        /// The  to register
        /// True if the operation succeeded
        private bool RegisterBuffer(AudioBuffer buffer)
        {
            if (GetTotalBufferCount() == Constants.AudioDeviceBufferCountMax)
            {
                return false;
            }
            _buffers[_serverBufferIndex] = buffer;
            _serverBufferIndex = (_serverBufferIndex + 1) % Constants.AudioDeviceBufferCountMax;
            _bufferAppendedCount++;
            return true;
        }
        /// 
        /// Flush server buffers to hardware.
        /// 
        private void FlushToHardware()
        {
            uint bufferToFlushCount = Math.Min(Math.Min(_bufferAppendedCount, 4), _bufferRegisteredLimit - _bufferRegisteredCount);
            AudioBuffer[] buffersToFlush = new AudioBuffer[bufferToFlushCount];
            uint hardwareBufferIndex = _hardwareBufferIndex;
            for (int i = 0; i < buffersToFlush.Length; i++)
            {
                buffersToFlush[i] = _buffers[_hardwareBufferIndex];
                _bufferAppendedCount--;
                _bufferRegisteredCount++;
                hardwareBufferIndex = (hardwareBufferIndex + 1) % Constants.AudioDeviceBufferCountMax;
            }
            _hardwareBufferIndex = hardwareBufferIndex;
            for (int i = 0; i < buffersToFlush.Length; i++)
            {
                _hardwareDeviceSession.QueueBuffer(buffersToFlush[i]);
            }
        }
        /// 
        /// Get the current index of the  playing on the driver side.
        /// 
        /// The output index of the  playing on the driver side
        /// True if any buffer is playing
        private bool TryGetPlayingBufferIndex(out uint playingIndex)
        {
            if (_bufferRegisteredCount > 0)
            {
                playingIndex = (_hardwareBufferIndex - _bufferRegisteredCount) % Constants.AudioDeviceBufferCountMax;
                return true;
            }
            playingIndex = 0;
            return false;
        }
        /// 
        /// Try to pop the  playing on the driver side.
        /// 
        /// The output  playing on the driver side
        /// True if any buffer is playing
        private bool TryPopPlayingBuffer(out AudioBuffer buffer)
        {
            if (_bufferRegisteredCount > 0)
            {
                uint bufferIndex = (_hardwareBufferIndex - _bufferRegisteredCount) % Constants.AudioDeviceBufferCountMax;
                buffer = _buffers[bufferIndex];
                _buffers[bufferIndex] = null;
                _bufferRegisteredCount--;
                return true;
            }
            buffer = null;
            return false;
        }
        /// 
        /// Try to pop a  released by the driver side.
        /// 
        /// The output  released by the driver side
        /// True if any buffer has been released
        public bool TryPopReleasedBuffer(out AudioBuffer buffer)
        {
            if (_bufferReleasedCount > 0)
            {
                uint bufferIndex = (_releasedBufferIndex - _bufferReleasedCount) % Constants.AudioDeviceBufferCountMax;
                buffer = _buffers[bufferIndex];
                _buffers[bufferIndex] = null;
                _bufferReleasedCount--;
                return true;
            }
            buffer = null;
            return false;
        }
        /// 
        /// Release a .
        /// 
        /// The  to release
        private void ReleaseBuffer(AudioBuffer buffer)
        {
            buffer.PlayedTimestamp = (ulong)PerformanceCounter.ElapsedNanoseconds;
            _bufferRegisteredCount--;
            _bufferReleasedCount++;
            _releasedBufferIndex = (_releasedBufferIndex + 1) % Constants.AudioDeviceBufferCountMax;
        }
        /// 
        /// Update the released buffers.
        /// 
        /// True if the session is currently stopping
        private void UpdateReleaseBuffers(bool updateForStop = false)
        {
            bool wasAnyBuffersReleased = false;
            while (TryGetPlayingBufferIndex(out uint playingIndex))
            {
                if (!updateForStop && !_hardwareDeviceSession.WasBufferFullyConsumed(_buffers[playingIndex]))
                {
                    break;
                }
                if (updateForStop)
                {
                    _hardwareDeviceSession.UnregisterBuffer(_buffers[playingIndex]);
                }
                ReleaseBuffer(_buffers[playingIndex]);
                wasAnyBuffersReleased = true;
            }
            if (wasAnyBuffersReleased)
            {
                _bufferEvent.Signal();
            }
        }
        /// 
        /// Append a new .
        /// 
        /// The  to append
        /// True if the buffer was appended
        public bool AppendBuffer(AudioBuffer buffer)
        {
            if (_hardwareDeviceSession.RegisterBuffer(buffer))
            {
                if (RegisterBuffer(buffer))
                {
                    FlushToHardware();
                    return true;
                }
                _hardwareDeviceSession.UnregisterBuffer(buffer);
            }
            return false;
        }
        public bool AppendUacBuffer(AudioBuffer buffer, uint handle)
        {
            // NOTE: On hardware, there is another RegisterBuffer method taking an handle.
            // This variant of the call always return false (stubbed?) as a result this logic will never succeed.
            return false;
        }
        /// 
        /// Start the audio session.
        /// 
        /// A  reporting an error or a success
        public ResultCode Start()
        {
            if (_state == AudioDeviceState.Started)
            {
                return ResultCode.OperationFailed;
            }
            _hardwareDeviceSession.Start();
            _state = AudioDeviceState.Started;
            FlushToHardware();
            _hardwareDeviceSession.SetVolume(_volume);
            return ResultCode.Success;
        }
        /// 
        /// Stop the audio session.
        /// 
        /// A  reporting an error or a success
        public ResultCode Stop()
        {
            if (_state == AudioDeviceState.Started)
            {
                _hardwareDeviceSession.Stop();
                UpdateReleaseBuffers(true);
                _state = AudioDeviceState.Stopped;
            }
            return ResultCode.Success;
        }
        /// 
        /// Get the volume of the session.
        /// 
        /// The volume of the session
        public float GetVolume()
        {
            return _hardwareDeviceSession.GetVolume();
        }
        /// 
        /// Set the volume of the session.
        /// 
        /// The new volume to set
        public void SetVolume(float volume)
        {
            _volume = volume;
            if (_state == AudioDeviceState.Started)
            {
                _hardwareDeviceSession.SetVolume(volume);
            }
        }
        /// 
        /// Get the count of buffer currently in use (server + driver side).
        /// 
        /// The count of buffer currently in use
        public uint GetBufferCount()
        {
            return _bufferAppendedCount + _bufferRegisteredCount;
        }
        /// 
        /// Check if a buffer is present.
        /// 
        /// The unique tag of the buffer
        /// Return true if a buffer is present
        public bool ContainsBuffer(ulong bufferTag)
        {
            uint bufferIndex = (_releasedBufferIndex - _bufferReleasedCount) % Constants.AudioDeviceBufferCountMax;
            for (int i = 0; i < GetTotalBufferCount(); i++)
            {
                if (_buffers[bufferIndex].BufferTag == bufferTag)
                {
                    return true;
                }
                bufferIndex = (bufferIndex + 1) % Constants.AudioDeviceBufferCountMax;
            }
            return false;
        }
        /// 
        /// Get the count of sample played in this session.
        /// 
        /// The count of sample played in this session
        public ulong GetPlayedSampleCount()
        {
            if (_state == AudioDeviceState.Stopped)
            {
                return 0;
            }
            else
            {
                return _hardwareDeviceSession.GetPlayedSampleCount();
            }
        }
        /// 
        /// Flush all buffers to the initial state.
        /// 
        /// True if any buffer was flushed
        public bool FlushBuffers()
        {
            if (_state == AudioDeviceState.Stopped)
            {
                return false;
            }
            uint bufferCount = GetBufferCount();
            while (TryPopReleasedBuffer(out AudioBuffer buffer))
            {
                _hardwareDeviceSession.UnregisterBuffer(buffer);
            }
            while (TryPopPlayingBuffer(out AudioBuffer buffer))
            {
                _hardwareDeviceSession.UnregisterBuffer(buffer);
            }
            if (_bufferRegisteredCount == 0 || (_bufferReleasedCount + _bufferAppendedCount) > Constants.AudioDeviceBufferCountMax)
            {
                return false;
            }
            _bufferReleasedCount += _bufferAppendedCount;
            _releasedBufferIndex = (_releasedBufferIndex + _bufferAppendedCount) % Constants.AudioDeviceBufferCountMax;
            _bufferAppendedCount = 0;
            _hardwareBufferIndex = _serverBufferIndex;
            if (bufferCount > 0)
            {
                _bufferEvent.Signal();
            }
            return true;
        }
        /// 
        /// Update the session.
        /// 
        public void Update()
        {
            if (_state == AudioDeviceState.Started)
            {
                UpdateReleaseBuffers();
                FlushToHardware();
            }
        }
        public void Dispose()
        {
            Dispose(true);
        }
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                // Tell the hardware session that we are ending.
                _hardwareDeviceSession.PrepareToClose();
                // Unregister all buffers
                while (TryPopReleasedBuffer(out AudioBuffer buffer))
                {
                    _hardwareDeviceSession.UnregisterBuffer(buffer);
                }
                while (TryPopPlayingBuffer(out AudioBuffer buffer))
                {
                    _hardwareDeviceSession.UnregisterBuffer(buffer);
                }
                // Finally dispose hardware session.
                _hardwareDeviceSession.Dispose();
                _bufferEvent.Signal();
            }
        }
    }
}