'  
'   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.
' 

Imports System
Imports System.IO
Imports System.Threading
Imports System.Text

Imports Rebex.TerminalEmulation

''' <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
    Inherits ShellChannel
    Implements IShellChannelFactory
#Region "Private fields"

    ' channel is closed
    Private _closed As Boolean = False

    ' input stream
    Private _inputStream As Stream

    ' buffer to store read data
    Private _buffer As New RoundBuffer(1024)

    ' determines the time when to wake up
    Private _ticksWakeUp As Integer

    Private Const MAX_LATE_DELAY As Integer = 100

#End Region

    ''' <summary>
    ''' Initialize the instance of AnsiPlayerShellChannel.
    ''' </summary>
    ''' <param name="inputStream">Input stream.</param>
    Public Sub New(ByVal inputStream As Stream)
        If inputStream Is Nothing Then
            Throw New ArgumentNullException("inputStream")
        End If

        If Not inputStream.CanRead Then
            Throw New ArgumentException("Specified stream is not readable.")
        End If

        _inputStream = inputStream

        _ticksWakeUp = Environment.TickCount
    End Sub

    ''' <summary>
    ''' Gets or sets the value indicating whether the player is paused.
    ''' </summary>
    Public Property IsPaused() As Boolean
        Get
            Return _isPaused
        End Get
        Set(ByVal Value As Boolean)
            _isPaused = Value
        End Set
    End Property
    Private _isPaused As Boolean = False


    ''' <summary>
    ''' Skips to next data frame in stream.
    ''' </summary>
    ''' <remarks>Player must be running (unavailable while the player is paused).</remarks>
    Public Sub SeekToNextFrame()
        _ticksWakeUp = Environment.TickCount
    End Sub

    ''' <summary>
    ''' Gets or sets the playback speed.
    ''' </summary>
    Public Property Speed() As Single
        Get
            Return _speed
        End Get
        Set(ByVal Value As Single)
            If Value <= 0 Then
                Throw New ArgumentOutOfRangeException("Speed must be greater then zero.")
            End If

            If (Value < 0.001F) Then Value = 0.001F

            If (Value > 1000.0F) Then Value = 1000.0F

            _speed = Value
            _speedMultiplier = CInt((1024 / Value))
        End Set
    End Property
    Private _speed As Single = 1.0F
    Private _speedMultiplier As Long = 1024

    ''' <summary>
    ''' Gets the value of 0 if waiting for 'wake up time' or 1 if data is available to be played.
    ''' </summary>
    Public Overrides ReadOnly Property Available() As Integer
        Get
            Dim late As Integer = Environment.TickCount - _ticksWakeUp
            If late < 0 OrElse _isPaused Then
                Return 0
            End If

            If late > MAX_LATE_DELAY Then
                _ticksWakeUp = Environment.TickCount
            End If

            FillBufferToMax()

            If _buffer.Length = 0 Then
                Return 0
            End If

            Dim ticksDelta As Integer = GetWaitingSequence()
            If ticksDelta >= 0 Then
                If _speedMultiplier <> 1024 Then
                    ticksDelta = (ticksDelta * _speedMultiplier) >> 10
                End If
                _ticksWakeUp += ticksDelta
                Return 0
            Else
                Return 1
            End If
        End Get
    End Property

    Public Overrides Function Poll(ByVal microSeconds As Integer) As PollResult
        If (Available > 0) Then Return PollResult.DataAvailable

        If (Connected) Then
            Return PollResult.NoData
        Else
            Return PollResult.Closed
        End If
    End Function

    ''' <summary>
    ''' Gets the ShellChannelOptions.
    ''' </summary>
    Public Overrides ReadOnly Property Options() As ShellChannelOptions
        Get
            ' this is player, it doesn't matter what is returned
            Return ShellChannelOptions.Terminal
        End Get
    End Property

    ''' <summary>
    ''' Gets the value indicating whether the channel is connected.
    ''' </summary>
    Public Overrides ReadOnly Property Connected() As Boolean
        Get
            If _closed Then
                Return False
            End If

            FillBufferToMax()
            Return _buffer.Length > 0
        End Get
    End Property

    ''' <summary>
    ''' Gets the terminal width.
    ''' </summary>
    Public Overrides ReadOnly Property TerminalWidth() As Int32
        Get
            Throw New NotSupportedException
        End Get
    End Property

    ''' <summary>
    ''' Gets the terminal height.
    ''' </summary>
    Public Overrides ReadOnly Property TerminalHeight() As Int32
        Get
            Throw New NotSupportedException
        End Get
    End Property

    ''' <summary>
    ''' Returns the ShellChannelState.
    ''' </summary>
    ''' <returns>The ShellChannelState enum.</returns>
    Public Overrides Function GetConnectionState() As ShellChannelState
        If Connected Then
            Return ShellChannelState.Connected
        Else
            Return ShellChannelState.NotConnected
        End If
    End Function

    ''' <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 Overrides Function Send(ByVal buffer As Byte(), ByVal offset As Integer, ByVal count As Integer) As Integer
        ' this is only a player, do nothing
        Return count
    End Function

    ''' <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 Overrides Function Receive(ByVal buffer As Byte(), ByVal offset As Integer, ByVal count As Integer) As Integer
        If buffer Is Nothing Then
            Throw New ArgumentNullException("buffer")
        End If

        If offset + count > buffer.Length Then
            Throw New ArgumentOutOfRangeException("bytes", "Offset and count must refer to a location within the buffer.")
        End If

        If Not Connected Then
            Throw New InvalidOperationException("Channel is not connected.")
        End If

        Dim total As Integer = 0

        While count > 0
            While Available = 0
                If total > 0 Then
                    Return total
                End If

                Thread.Sleep(1)
            End While

            SyncLock _buffer
                Dim bytesToCopy As Integer = Math.Min(count, GetCountToCopy())
                Dim bytesRead As Integer = _buffer.Read(buffer, offset, bytesToCopy)
                total += bytesRead
                offset += bytesRead
                count -= bytesRead
            End SyncLock
        End While

        Return total
    End Function

    ''' <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 Overrides Sub SetTerminalSize(ByVal width As Integer, ByVal height As Integer)
        ' this is only a player, do nothing
    End Sub

    ''' <summary>
    ''' Closes the channel.
    ''' </summary>
    Public Overrides Sub Close()
        SyncLock _buffer
            If Not _inputStream Is Nothing Then
                _inputStream.Close()
            End If

            _inputStream = Nothing
            _closed = True
        End SyncLock
    End Sub

    ''' <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 Function CreateShellChannel(ByVal options As TerminalOptions, ByVal width As Integer, ByVal height As Integer) As ShellChannel Implements IShellChannelFactory.CreateShellChannel
        Return Me
    End Function

    ''' <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 Function GetCountToCopy() As Integer
        Dim i As Integer
        For i = 1 To _buffer.Length - 1
            ' ESC
            If _buffer(i) = 27 Then
                Return i
            End If
        Next
        Return _buffer.Length
    End Function

    ''' <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 Sub FillBufferToMax()
        SyncLock _buffer
            While _buffer.Length < _buffer.MaximumCapacity
                If _inputStream Is Nothing Then Return

                Dim byteRead As Integer = _buffer.CopyFrom(_inputStream, _buffer.MaximumCapacity - _buffer.Length)
                If byteRead = 0 Then
                    _inputStream.Close()
                    _inputStream = Nothing
                    Return
                End If
            End While
        End SyncLock
    End Sub

    ''' <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 Function GetWaitingSequence() As Integer
        If _buffer.Length < 7 OrElse _
         _buffer(0) <> 27 OrElse _
         Convert.ToChar(_buffer(1)) <> "["c OrElse _
         Convert.ToChar(_buffer(2)) <> "3"c OrElse _
         Convert.ToChar(_buffer(3)) <> "7"c OrElse _
         Convert.ToChar(_buffer(4)) <> ";"c Then
            Return -1
        End If

        Dim i As Integer = 5
        While i < 12 AndAlso i < _buffer.Length
            If Not (Convert.ToChar(_buffer(i)) >= "0"c AndAlso Convert.ToChar(_buffer(i)) <= "9"c) Then
                If Convert.ToChar(_buffer(i)) <> "]"c Then
                    Return -1
                End If

                If i = 5 Then
                    Return -1
                End If

                Dim buffer As Byte() = New Byte(6) {}
                _buffer.Read(buffer, 0, 5)
                _buffer.Read(buffer, 0, i - 5)
                Dim delta As String = Encoding.ASCII.GetString(buffer, 0, i - 5)
                _buffer.Read(buffer, 0, 1)

                Return Integer.Parse(delta)
            End If
            i += 1
        End While
        Return -1
    End Function
End Class
