using System;
using System.Collections.Generic;

/*
    Copyright (c) 2010 flixmonk. (http://DVDSwarm.codeplex.com)
    All Rights Reserved.
        
    Redistribution and use in source and binary forms, with or without modification,
    are permitted provided that the following conditions are met:
        
    * Redistributions of source code must retain the above copyright notice, this list
      of conditions and the following disclaimer.
        
    * Redistributions in binary form must reproduce the above copyright notice, this
    list of conditions and the following disclaimer in the documentation and/or other
    materials provided with the distribution. 
        
    * Neither the name of The Flixmonk nor the names of its contributors may be used to
    endorse or promote products derived from this software without specific prior written
    permission.
        
    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
    EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
    OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
    SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
    OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
    HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
    TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
namespace DVDSwarm.Dvd
{
    /// <summary>
    /// Contains info about the trnsport packet. Some properties only have valid values for
    /// certain packet types.
    /// </summary>
    sealed public class TransportPacket
    {
        #region Fields

        private AudioTransportPacketFormat _audioFormat = AudioTransportPacketFormat.Unknown;

        // Lookup tables for transposing bytes from DVD LPCM to standard little-endian LPCM.
        // Five channels is the maximum supoorted by the DVD spec.
        byte[][] _audioLpcm24BitChannelByteOrder = new byte[5][]
        {
            // 1 Channel
            new byte[6]  { 4, 1, 0, 5, 3, 2 },
            
            // 2 Channel
            new byte[12] { 8, 1, 0, 9, 3, 2, 10, 5, 4, 11, 7, 6 },
            
            // 3 Channel
            new byte[18] { 12, 1, 0, 13, 3, 2, 14, 5, 4, 15, 7, 6, 16, 9, 8, 17, 11, 10 },
            
            // 4 Channel
            new byte[24] { 16, 1, 0, 17, 3, 2, 18, 5, 4, 19, 7, 6, 20, 9, 8, 21, 11, 10, 22, 13, 12, 23, 15, 14 },
            
            // 5 Channel
            new byte[30] { 20, 1, 0, 21, 3, 2, 22, 5, 4, 23, 7, 6, 24, 9, 8, 25, 11, 10, 26, 13, 12, 27, 15, 14, 28, 17, 16, 29, 19, 18 }
        };

        private byte[] _buffer = null;
        private Boolean _canDemux = true;
        private Int64 _discontinuity = 0;
        private Int32 _id = 0;
        private Int32 _length = 0;
        private Int32 _navCellId = 0;
        private Int64 _navPresentationStartTimeStamp = 0;
        private Int64 _navPresentationEndTimeStamp = 0;
        private Int32 _navVobId = 0;
        private TransportPacketType _packetType = TransportPacketType.Unknown;
        private Int32 _payloadLength = 0;
        private Int32 _payloadOffset = 0;
        private Int32 _pesHeaderLength = 0;
        private Int64 _presentationTimeStamp = 0;
        private Int32 _startCode = 0;
        private Int64 _timeStampOffset = 0;

        #endregion

        #region Properties

        /// <summary>
        /// [Audio] Gets the audio format.
        /// </summary>
        public AudioTransportPacketFormat AudioFormat
        {
            get
            {
                return _audioFormat;
            }
        }
        /// <summary>
        /// Gets a value indicating whether the stream can be demuxed.
        /// </summary>
        public Boolean CanDemux
        {
            get
            {
                return _canDemux;
            }
        }
        /// <summary>
        /// Gets a value indicating whether the audio, subpicture, or video packet has a presentation
        /// time stamp.
        /// </summary>
        public Boolean HasPresentationTimeStamp
        {
            get
            {
                return (_presentationTimeStamp > 0);
            }
        }
        /// <summary>
        /// Gets the stream id if present, otherwise zero is returned.
        /// </summary>
        public Int32 Id
        {
            get
            {
                return _id;
            }
        }
        /// <summary>
        /// Gets the stream length in bytes, including the PES header.
        /// </summary>
        public Int32 Length
        {
            get
            {
                return _length;
            }
        }
        /// <summary>
        /// [Navigation] Gets the cell ID.
        /// </summary>
        public Int32 NavigationCellId
        {
            get
            {
                return _navCellId;
            }
        }
        /// <summary>
        /// Gets the navigation packet presentation end time stamp.
        /// </summary>
        public Int64 NavigationPresentationEndTimeStamp
        {
            get
            {
                return _navPresentationEndTimeStamp;
            }
        }
        /// <summary>
        /// Gets the navigation packet presentation start time stamp.
        /// </summary>
        public Int64 NavigationPresentationStartTimeStamp
        {
            get
            {
                return _navPresentationStartTimeStamp;
            }
        }
        /// <summary>
        /// [Navigation] Gets the vob ID.
        /// </summary>
        public Int32 NavigationVobId
        {
            get
            {
                return _navVobId;
            }
        }
        /// <summary>
        /// Gets the transport packet type.
        /// </summary>
        public TransportPacketType PacketType
        {
            get
            {
                return _packetType;
            }
        }
        /// <summary>
        /// Gets the stream length in bytes, excluding the PES header.
        /// </summary>
        public Int32 PayloadLength
        {
            get
            {
                return _payloadLength;
            }
        }
        /// <summary>
        /// Gets the absolute payload offset from the start of the packet.
        /// </summary>
        public Int32 PayloadOffset
        {
            get
            {
                return _payloadOffset;
            }
        }
        /// <summary>
        /// Gets the Packetized Elementary Stream (PES) header length.
        /// </summary>
        public Int32 PesHeaderLength
        {
            get
            {
                return _pesHeaderLength;
            }
        }
        /// <summary>
        /// Gets the presentation time stamp for an audio, subpicture or video packet.
        /// </summary>
        public Int64 PresentationTimeStamp
        {
            get
            {
                return _presentationTimeStamp;
            }
        }
        /// <summary>
        /// Gets the packet start code.
        /// </summary>
        public Int32 StartCode
        {
            get
            {
                return _startCode;
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Creates a new instance of the TransportPacket class initialized with the
        /// values contained in the specifed transport packet.
        /// </summary>
        /// <param name="buffer">The transport packet to parse.</param>
        /// <param name="timeStampOffset">The initial navigation time stamp for the vobu plus the discontinuity.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when buffer is null.</exception>
        /// <exception cref="System.ArgumentException">Thrown when buffer length is not 2048 bytes.</exception>
        public TransportPacket(byte[] buffer, Int64 timeStampOffset)
        {
            if (buffer == null)
            {
                throw new ArgumentNullException("buffer");
            }
            if (buffer.Length != 2048)
            {
                throw new ArgumentOutOfRangeException("buffer Length must be 2048 bytes.");
            }
   
            _buffer = buffer;
            _timeStampOffset = timeStampOffset;
            _startCode = (_buffer[0xe] << 24) + (_buffer[0xf] << 16) + (_buffer[0x10] << 8) + _buffer[0x11];
            _length = (_buffer[0x12] << 8) + _buffer[0x13];
            _pesHeaderLength = _buffer[0x16];
            _payloadLength = _length - _pesHeaderLength - 3;
            _payloadOffset = _buffer[0x16] + 0x17;
            _id = _buffer[_buffer[0x16] + 0x17];

            //
            // Audio packet
            // 
            if ((_startCode == 0x01bd || (_startCode >= 0x01c0 && _startCode <= 0x01df)) && _id >= 0x80 && _id < 0xa7)
            {
                _packetType = TransportPacketType.Audio;

                if (StartCode >= 0x01C0 && StartCode <= 0x01df)
                {
                    // MPEG-1/MPEG-2 audio.
                    _id = _buffer[0x11];
                }

                // Formats.
                if (_id >= 0x80 && _id <= 0x87)
                {
                    _audioFormat = AudioTransportPacketFormat.Ac3;
                }
                if (_id >= 0x88 && _id <= 0x8f)
                {
                    _audioFormat = AudioTransportPacketFormat.Dts;
                }
                if (_id >= 0xa0 && _id <= 0xa7)
                {
                    _audioFormat = AudioTransportPacketFormat.Lpcm;
                }
                if (_id >= 0xc0 && _id <= 0xc7)
                {
                    _audioFormat = AudioTransportPacketFormat.Mpeg1;
                }
                if (_id >= 0xd0 && _id <= 0xd7)
                {
                    _audioFormat = AudioTransportPacketFormat.Mpeg2;
                }

                // Known audio format with length greater than zero? Allow demux.
                if (_audioFormat == AudioTransportPacketFormat.Unknown)
                {
                    _canDemux = false;
                }
            }

            //
            // Navigation packet
            // 
            if (_startCode == 0x01bb)
            {
                _packetType = TransportPacketType.Navigation;

                _canDemux = false;
                _navCellId = buffer[0x422];
                _navVobId = (buffer[0x41f] << 8) + buffer[0x420];

                _navPresentationStartTimeStamp = (buffer[0x39] << 24) + (buffer[0x3a] << 16) + (buffer[0x3b] << 8) + buffer[0x3c];
                _navPresentationEndTimeStamp = (buffer[0x3d] << 24) + (buffer[0x3e] << 16) + (buffer[0x3f] << 8) + buffer[0x40];
            }

            //
            // Padding packet
            // 
            if (_startCode == 0x1be)
            {
                _canDemux = false;
                _packetType = TransportPacketType.Padding;
            }

            //
            // Subpicture packet
            // 
            if (_startCode == 0x01bd && _id > 0x1f && _id < 0x40)
            {
                _packetType = TransportPacketType.Subpicture;

                // Skip the stream ID.
                _payloadLength--;
                _payloadOffset++;
            }

            //
            // Video packet
            // 
            if (_startCode == 0x01e0)
            {
                _packetType = TransportPacketType.Video;
                _id = 0xe0;
            }

            // Audio, subpicture, or video packet? Header offset greater than zero? Has a presentation time stamp?
            if ((_packetType == TransportPacketType.Audio || _packetType == TransportPacketType.Subpicture ||
                _packetType == TransportPacketType.Video) && _buffer[0x16] > 0 &&
                (_buffer[0x15] >> 6) == 2 || (_buffer[0x15] >> 6) == 3)
            {
                _presentationTimeStamp = (((Int64)_buffer[0x17] & 0x0e) << 29) + ((Int64)_buffer[0x18] << 22) +
                (((Int64)_buffer[0x19] & 0xfe) << 14) + ((Int64)_buffer[0x1a] << 7) +
                (((Int64)_buffer[0x1b] & 0xfe) >> 1);
            }

            if (_length == 0)
            {
                _canDemux = false;
            }
        }

        /// <summary>
        /// Demuxes the transport packet to the specified file stream if demuxing is supported
        /// for the packet type.
        /// </summary>
        /// <param name="output">The FileStream to demux the packet data to.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when output is null.</exception>
        public void Demux(System.IO.FileStream output)
        {
            if (output == null)
            {
                throw new ArgumentNullException("output");
            }

            if (!_canDemux)
            {
                return;
            }

            if (_packetType == TransportPacketType.Audio)
            {
                if (_audioFormat == AudioTransportPacketFormat.Lpcm)
                {
                    DemuxLpcmAudio(output);
                    return;
                }

                if (_audioFormat == AudioTransportPacketFormat.Ac3 || _audioFormat == AudioTransportPacketFormat.Dts)
                {
                    _payloadOffset += 4;
                    _payloadLength -= 4;
                }
            }

            // Subpicture? Has a PES header and a Presentation Time Stamp?
            if (_packetType == TransportPacketType.Subpicture && PesHeaderLength > 0 && HasPresentationTimeStamp)
            {
                Int32 value = Convert.ToInt32(PresentationTimeStamp - _timeStampOffset);

                // Write subpicture header and time stamp.
                output.WriteByte(0x53);
                output.WriteByte(0x50);
                output.WriteByte((byte)(value & 0xff));
                output.WriteByte((byte)((value >> 8) & 0xff));
                output.WriteByte((byte)((value >> 16) & 0xff));
                output.WriteByte((byte)((value >> 24) & 0xff));
                output.WriteByte(0);
                output.WriteByte(0);
                output.WriteByte(0);
                output.WriteByte(0);
            }

            //
            // Standard write bytes for audio, subpicture and video packets.
            //
            output.Write(_buffer, _payloadOffset, _payloadLength);
        }

        /// <summary>
        /// Demuxes LPCM audio.
        /// </summary>
        /// <param name="output">The FileStream to demux the audio data to.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when output is null.</exception>
        private void DemuxLpcmAudio(System.IO.FileStream output)
        {
            if (output == null)
            {
                throw new ArgumentNullException("output");
            }

            Int32 channels = (_buffer[_buffer[0x16] + 0x17 + 5] & 7) + 1;

            Int32 sampleRate = ((_buffer[_buffer[0x16] + 0x17 + 5] >> 4) & 3) * 48000;
            if (sampleRate > 96000)
            {
                sampleRate = 48000;
            }

            Int32 bitsPerSample = ((_buffer[_buffer[0x16] + 0x17 + 5] >> 6) & 3) * 4 + 16;
            if (bitsPerSample > 24)
            {
                bitsPerSample = 16;
            }
            _payloadOffset += 3;

            // Copy bytes.
            if (bitsPerSample == 16)
            {
                if ((_length - _payloadOffset) % 2 != 0)
                {
                    System.Windows.Forms.MessageBox.Show("Invalid sample count: LPCM 16-bit.");
                    return;
                }

                for (Int32 k = _payloadOffset; k < _length - 1; k += 2)
                {
                    output.WriteByte(_buffer[k + 1]);
                    output.WriteByte(_buffer[k]);
                }
            }

            // Copy bytes.
            if (bitsPerSample == 24)
            {
                if ((_length - _payloadOffset) % (6 * channels) != 0)
                {
                    System.Windows.Forms.MessageBox.Show("Invalid sample count: LPCM 24-bit.");
                }

                for (Int32 i = _payloadOffset; i < (_length - 6 * channels + 1); i += 6 * channels)
                {
                    for (Int32 j = 0; j < _audioLpcm24BitChannelByteOrder[channels - 1].Length; j++)
                    {
                        output.WriteByte(_buffer[_payloadOffset + _audioLpcm24BitChannelByteOrder[channels - 1][j]]);
                    }
                }

            }
        }

        #endregion
    }
}
