//  
//   Rebex Sample Code License
// 
//   Copyright 2025, REBEX CR s.r.o.
//   All rights reserved.
//   https://www.rebex.net/
// 
//   Permission to use, copy, modify, and/or distribute this software for any
//   purpose with or without fee is hereby granted.
// 
//   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
//   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
//   OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
//   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
//   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
//   OTHER DEALINGS IN THE SOFTWARE.
// 

using System;
using System.IO;
using System.Threading;
using System.Text;

using Rebex.TerminalEmulation;

namespace Rebex.Samples
{
    /// <summary>
    /// The AnsiPlayerShellChannel class simulates the SSH server. The class implements interface IShellChannel
    /// which provides the key method and property for this purpose (Receive and Available namely).
    /// The class reads the data from the input stream and provide them to the client using the Receive method. 
    /// If the special waiting escape sequence 'ESC [ 37 ; deltaTime ]' appears in the input data
    /// it is removed and the Available property returns zero for the next deltaTime miliseconds.
    /// The input file for the AnsiPlayerShellChannel class can be made by specifying the Recorder property 
    /// at the VirtualTerminal class or the TerminalControl class or the SshTerminalControl class.
    /// For detailed sample of use look at the WinFormClient project of this solution.
    /// </summary>
    public class AnsiPlayerShellChannel : ShellChannel, IShellChannelFactory
    {
        #region Private fields

        // channel is closed
        private bool _closed = false;

        // input stream
        private Stream _inputStream;

        // buffer to store read data
        private RoundBuffer _buffer = new RoundBuffer(1024);

        // determines the time when to wake up
        private int _ticksWakeUp;

        private const int MAX_LATE_DELAY = 100;

        #endregion

        /// <summary>
        /// Initialize the instance of AnsiPlayerShellChannel.
        /// </summary>
        /// <param name="inputStream">Input stream.</param>
        public AnsiPlayerShellChannel(Stream inputStream)
        {
            if (inputStream == null)
                throw new ArgumentNullException("inputStream");

            if (!inputStream.CanRead)
                throw new ArgumentException("Specified stream is not readable.");

            _inputStream = inputStream;

            _ticksWakeUp = Environment.TickCount;
        }

        /// <summary>
        /// Gets or sets the value indicating whether the player is paused.
        /// </summary>
        public bool IsPaused
        {
            get { return _isPaused; }
            set { _isPaused = value; }
        }
        private bool _isPaused = false;


        /// <summary>
        /// Skips to next data frame in stream.
        /// </summary>
        /// <remarks>Player must be running (unavailable while the player is paused).</remarks>
        public void SeekToNextFrame()
        {
            _ticksWakeUp = Environment.TickCount;
        }

        /// <summary>
        /// Gets or sets the playback speed.
        /// </summary>
        public float Speed
        {
            get { return _speed; }
            set
            {
                if (value <= 0)
                    throw new ArgumentOutOfRangeException("Speed must be greater then zero.");

                if (value < 0.001f)
                    value = 0.001f;

                if (value > 1000f)
                    value = 1000f;

                _speed = value;
                _speedMultiplier = (int)(1024 / value);
            }
        }
        private float _speed = 1.0f;
        private long _speedMultiplier = 1024;

        /// <summary>
        /// Gets the value of 0 if waiting for 'wake up time' or 1 if data is available to be played.
        /// </summary>
        public override int Available
        {
            get
            {
                int late = Environment.TickCount - _ticksWakeUp;
                if (late < 0 || _isPaused)
                    return 0;

                if (late > MAX_LATE_DELAY)
                    _ticksWakeUp = Environment.TickCount;

                FillBufferToMax();

                if (_buffer.Length == 0)
                    return 0;

                int ticksDelta = GetWaitingSequence();
                if (ticksDelta >= 0)
                {
                    if (_speedMultiplier != 1024)
                        ticksDelta = (int)((ticksDelta * _speedMultiplier) >> 10);
                    _ticksWakeUp += ticksDelta;
                    return 0;
                }
                else
                {
                    return 1;
                }
            }
        }

        public override PollResult Poll(int microSeconds)
        {
            if (Available > 0)
                return PollResult.DataAvailable;

            if (Connected)
                return PollResult.NoData;
            else
                return PollResult.Closed;
        }

        /// <summary>
        /// Gets the ShellChannelOptions.
        /// </summary>
        public override ShellChannelOptions Options
        {
            get
            {
                // this is player, it doesn't matter what is returned
                return ShellChannelOptions.Terminal;
            }
        }

        /// <summary>
        /// Gets the value indicating whether the channel is connected.
        /// </summary>
        public override bool Connected
        {
            get
            {
                if (_closed)
                    return false;

                FillBufferToMax();
                return _buffer.Length > 0;
            }
        }

        /// <summary>
        /// Gets the terminal width.
        /// </summary>
        public override int TerminalWidth
        {
            get
            {
                throw new NotSupportedException();
            }
        }

        /// <summary>
        /// Gets the terminal height.
        /// </summary>
        public override int TerminalHeight
        {
            get
            {
                throw new NotSupportedException();
            }
        }

        /// <summary>
        /// Returns the ShellChannelState.
        /// </summary>
        /// <returns>The ShellChannelState enum.</returns>
        public override ShellChannelState GetConnectionState()
        {
            if (Connected)
                return ShellChannelState.Connected;
            else
                return ShellChannelState.NotConnected;
        }

        /// <summary>
        /// Sends the data to the server using this channel. Actually this is player, no data will be sent.
        /// </summary>
        /// <returns>The count parameter is always returned.</returns>
        public override int Send(byte[] buffer, int offset, int count)
        {
            // this is only a player, do nothing
            return count;
        }

        /// <summary>
        /// Receive the data from the server. In this case reads data from the 
        /// input file and stores it to the specified buffer.
        /// </summary>
        /// <param name="buffer">
        /// An array of bytes. When this method returns, the buffer contains 
        /// the specified byte array with the values between offset and (offset + count - 1) 
        /// replaced by the bytes read from the input file.
        /// </param>
        /// <param name="offset">
        /// The zero-based byte offset in buffer at which to begin storing 
        /// the data read from the input file.
        /// </param>
        /// <param name="count">
        /// The maximum number of bytes to be read from the input file.
        /// </param>
        /// <returns>
        /// The total number of bytes read into the buffer. This can be less than the
        /// number of bytes requested if that many bytes are not currently available,
        /// or zero (0) if the EOF is reached.
        /// </returns>
        public override int Receive(byte[] buffer, int offset, int count)
        {
            if (buffer == null)
                throw new ArgumentNullException("buffer");

            if (offset + count > buffer.Length)
                throw new ArgumentOutOfRangeException("bytes", "Offset and count must refer to a location within the buffer.");

            if (!Connected)
                throw new InvalidOperationException("Channel is not connected.");

            int total = 0;

            while (count > 0)
            {
                while (Available == 0)
                {
                    if (total > 0)
                        return total;

                    Thread.Sleep(1);
                }

                lock (_buffer)
                {
                    int bytesToCopy = Math.Min(count, GetCountToCopy());
                    int bytesRead = _buffer.Read(buffer, offset, bytesToCopy);
                    total += bytesRead;
                    offset += bytesRead;
                    count -= bytesRead;
                }
            }

            return total;
        }

        /// <summary>
        /// Informs the server about the screen size changing of the client.
        /// Actually this is only a player with data store in a file, so nothing will be done.
        /// </summary>
        public override void SetTerminalSize(int width, int height)
        {
            // this is only a player, do nothing
        }

        /// <summary>
        /// Closes the channel.
        /// </summary>
        public override void Close()
        {
            lock (_buffer)
            {
                if (_inputStream != null)
                    _inputStream.Close();

                _inputStream = null;
                _closed = true;
            }
        }

        /// <summary>
        /// Creates the new instance of the IShellChannel.
        /// </summary>
        /// <param name="options">Terminal options.</param>
        /// <param name="width">Terminal screen width.</param>
        /// <param name="height">Terminal screen height.</param>
        /// <returns>New instance of the IShellChannel.</returns>
        public ShellChannel CreateShellChannel(TerminalOptions options, int width, int height)
        {
            return this;
        }

        /// <summary>
        /// Finds the position of the first occurance of the ESC. 
        /// If the ESC is not found the Length of the buffer is returned.
        /// </summary>
        /// <returns>Position of the first occurance of the ESC (Lenght of the buffer if the ESC is not found).</returns>
        private int GetCountToCopy()
        {
            for (int i = 1; i < _buffer.Length; i++)
            {
                if (_buffer[i] == 0x1B) // ESC
                    return i;
            }
            return _buffer.Length;
        }

        /// <summary>
        /// Reads as many bytes as possible from the input stream and add it to the buffer.
        /// The stream will be closed if the EOF is reached.
        /// </summary>
        private void FillBufferToMax()
        {
            lock (_buffer)
            {
                while (_buffer.Length < _buffer.MaximumCapacity)
                {
                    if (_inputStream == null)
                        return;

                    int byteRead = _buffer.CopyFrom(_inputStream, _buffer.MaximumCapacity - _buffer.Length);
                    if (byteRead == 0)
                    {
                        _inputStream.Close();
                        _inputStream = null;
                        return;
                    }
                }
            }
        }

        /// <summary>
        /// Returns deltaTime if the buffer starts with the waiting sequence 'ESC [ 37 ; deltaTime ]'; returns -1 otherwise.
        /// </summary>
        /// <returns>DeltaTime if the buffer starts with waiting sequence; -1 otherwise.</returns>
        private int GetWaitingSequence()
        {
            if (_buffer.Length < 7 || _buffer[0] != 0x1B || _buffer[1] != '[' || _buffer[2] != '3' || _buffer[3] != '7' || _buffer[4] != ';')
                return -1;

            for (int i = 5; i < 12 && i < _buffer.Length; i++)
            {
                if (_buffer[i] >= '0' && _buffer[i] <= '9')
                    continue;

                if (_buffer[i] != ']')
                    return -1;

                if (i == 5)
                    return -1;

                byte[] buffer = new byte[7];
                _buffer.Read(buffer, 0, 5);
                _buffer.Read(buffer, 0, i - 5);
                string delta = Encoding.ASCII.GetString(buffer, 0, i - 5);
                _buffer.Read(buffer, 0, 1);

                return int.Parse(delta);
            }
            return -1;
        }
    }
}
