using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.Xml;

/*
    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>
    /// DVD program chain information.
    /// </summary>
    sealed public class ProgramChain
    {
        #region Fields

        private Int32 _anglesCount = 1;
        private AudioStreamRef[] _audioStreamRefs = null;
        private Cell[] _cells = null;
        private SmpteTimecode _duration = new SmpteTimecode(VideoFramerate.Fps30, true);
        private VideoFramerate _framerate = VideoFramerate.Fps30;
        private string _framerateString = string.Empty;
        private Int32 _id = 0;
        private Int64 _length = 0;
        private Dvd.ProgramChainPlaybackMode _playbackMode = ProgramChainPlaybackMode.Sequential;
        private Program[] _programs = null;
        private SubpictureStreamRef[] _subpictureRefs = null;
        private Dvd.Subtitle[] _subtitles = null;
        private TitleSet _titleset = null;
        private Boolean _videoChecked = true;

        #endregion

        #region Properties

        /// <summary>
        /// Gets and sets the number of angles for the program chain.
        /// </summary>
        public Int32 AnglesCount
        {
            get
            {
                return _anglesCount;
            }
            internal set
            {
                _anglesCount = value;
            }
        }
        /// <summary>
        /// Gets and sets the audio stream references for the program chain.
        /// </summary>
        public AudioStreamRef[] AudioStreamRefs
        {
            get
            {
                return _audioStreamRefs;
            }
            internal set
            {
                _audioStreamRefs = value;
            }
        }
        /// <summary>
        /// Gets and sets the cells for all programs in the program chain.
        /// </summary>
        public Cell[] Cells
        {
            get
            {
                return _cells;
            }
            internal set
            {
                _cells = value;
            }
        }
        /// <summary>
        /// Gets the chapter end frames for the program chain as a an array.
        /// </summary>
        public string[] ChapterEndFrames
        {
            get
            {
                StringBuilder sb = new StringBuilder();

                VideoFramerate framerate = VideoFramerate.Fps30;
                if (_framerateString == Dvd.Utility.ProgramChainFramerateNames[1])
                {
                    framerate = VideoFramerate.Fps25;
                }
                SmpteTimecode totalDuration = new SmpteTimecode(framerate, false);

                string[] result = new string[_programs.Length];
                for (Int32 i = 0; i < _programs.Length; i++)
                {
                    totalDuration.Add(_programs[i].Duration);

                    result[i] = totalDuration.TotalFrames.ToString();
                }

                return result;
            }
        }
        /// <summary>
        /// Gets the chapters (programs) for the program chain as a string. Each chapter is a seperate line with
        /// the chapter values comma seperated.
        /// </summary>
        /// <example>
        /// <para>
        /// Chapter,Duration,ChapterFrames,ChapterEndFrame,TotalDuration
        /// </para>
        /// <para>
        /// 2,00:03:13:00,5809,5809,00:06:21:19
        /// </para>
        /// </example>
        public string Chapters
        {
            get
            {
                StringBuilder sb = new StringBuilder();

                VideoFramerate framerate = VideoFramerate.Fps30;
                if (_framerateString == Dvd.Utility.ProgramChainFramerateNames[1])
                {
                    framerate = VideoFramerate.Fps25;
                }
                SmpteTimecode totalDuration = new SmpteTimecode(framerate, false);

                for (Int32 i = 0; i < _programs.Length; i++)
                {
                    totalDuration.Add(_programs[i].Duration);

                    sb.Append(_programs[i].ID.ToString() + ",");
                    sb.Append(_programs[i].Duration.ToString() + ",");
                    sb.Append(_programs[i].Frames.ToString() + ",");
                    sb.Append(totalDuration.TotalFrames.ToString() + ",");
                    sb.AppendLine(totalDuration.ToString());
                }

                return sb.ToString();
            }
        }
        /// <summary>
        /// Gets the chapter timecodes for the program chain as a an array.
        /// </summary>
        public string[] ChapterTimecodes
        {
            get
            {
                StringBuilder sb = new StringBuilder();

                VideoFramerate framerate = VideoFramerate.Fps30;
                if (_framerateString == Dvd.Utility.ProgramChainFramerateNames[1])
                {
                    framerate = VideoFramerate.Fps25;
                }
                SmpteTimecode totalDuration = new SmpteTimecode(framerate, false);

                string[] result = new string[_programs.Length];
                for (Int32 i = 0; i < _programs.Length; i++)
                {
                    result[i] = _programs[i].Duration.ToString();
                }

                return result;
            }
        }
        /// <summary>
        /// Gets whether the program chain is selected. It is considered selected if at
        /// least one audio, subpicture, or video stream is selected.
        /// </summary>
        public Boolean Checked
        {
            get
            {
                return (_length > 0 && _duration.TotalFrames > 0 && (_videoChecked || CheckedAudioStreamsCount > 0
                    || CheckedSubtitlesCount > 0));
            }
        }
        /// <summary>
        /// Gets the checked audio stream output filenames as an array. If no streams are
        /// selected, an empty array is returned. The delay is left as 'DELAY 0ms' and
        /// should be changed by the demuxer to the appropriate value.
        /// </summary>
        public string[] CheckedAudioOutputFilenames
        {
            get
            {
                List<string> result = new List<string>();

                for (Int32 i = 0; i < _audioStreamRefs.Length; i++)
                {
                    if (_audioStreamRefs[i].Checked)
                    {
                        result.Add(FormatAudioOutputFilename(_audioStreamRefs[i].TrackId));
                    }
                }

                return result.ToArray();
            }
        }
        /// <summary>
        /// Gets the number of checked audio streams.
        /// </summary>
        public Int32 CheckedAudioStreamsCount
        {
            get
            {
                Int32 result = 0;

                for (Int32 i = 0; i < _audioStreamRefs.Length; i++)
                {
                    if (_audioStreamRefs[i].Checked)
                    {
                        result++;
                    }
                }

                return result;
            }
        }
        /// <summary>
        /// Gets the checked audio stream track numbers as an array. If no streams are
        /// selected, an empty array is returned.
        /// </summary>
        public string[] CheckedAudioTrackIds
        {
            get
            {
                List<string> result = new List<string>();

                for (Int32 i = 0; i < _audioStreamRefs.Length; i++)
                {
                    if (_audioStreamRefs[i].Checked)
                    {
                        result.Add(_audioStreamRefs[i].TrackId.ToString("X2"));
                    }
                }

                return result.ToArray();
            }
        }
        /// <summary>
        /// Gets the number of checked programs.
        /// </summary>
        public Int32 CheckedProgramsCount
        {
            get
            {
                Int32 result = 0;

                for (Int32 i = 0; i < _programs.Length; i++)
                {
                    if (_programs[i].Checked)
                    {
                        result++;
                    }
                }

                return result;
            }
        }
        /// <summary>
        /// Gets the total length in bytes of checked programs. The individual lengths of video, audio,
        /// and subpictures is unavailable.
        /// </summary>
        public Int64 CheckedProgramsLength
        {
            get
            {
                Int64 result = 0;

                for (Int32 i = 0; i < _programs.Length; i++)
                {
                    if (_programs[i].Checked)
                    {
                        for (Int32 j = _programs[i].StartCell - 1; j < _programs[i].EndCell; j++)
                        {
                            result += _cells[j].Length;
                        }
                    }
                }

                return result;
            }
        }
        /// <summary>
        /// Gets the checked subtitle stream output filenames as an array. If no streams are
        /// selected, an empty array is returned. The delay is left as 'DELAY 0ms' and
        /// should be changed by the demuxer to the appropriate value.
        /// </summary>
        public string[] CheckedSubtitleOutputFilenames
        {
            get
            {
                List<string> result = new List<string>();

                for (Int32 i = 0; i < _subtitles.Length; i++)
                {
                    if (_subtitles[i].Checked)
                    {
                        result.Add(FormatSubtitleOutputFilename(_subtitles[i].TrackId));
                    }
                }

                return result.ToArray();
            }
        }
        /// <summary>
        /// Gets the number of checked subtitles.
        /// </summary>
        public Int32 CheckedSubtitlesCount
        {
            get
            {
                Int32 result = 0;

                for (Int32 i = 0; i < _subtitles.Length; i++)
                {
                    if (_subtitles[i].Checked)
                    {
                        result++;
                    }
                }

                return result;
            }
        }
        /// <summary>
        /// Gets the checked subtitle streams as an array. If no streams are
        /// selected, an empty array is returned.
        /// </summary>
        public string[] CheckedSubtitleTrackIds
        {
            get
            {
                List<string> result = new List<string>();

                for (Int32 i = 0; i < _subtitles.Length; i++)
                {
                    if (_subtitles[i].Checked)
                    {
                        result.Add(_subtitles[i].TrackId.ToString("X2"));
                    }
                }

                return result.ToArray();
            }
        }
        /// <summary>
        /// Gets the duration.
        /// </summary>
        public SmpteTimecode Duration
        {
            get
            {
                return _duration;
            }
        }
        /// <summary>
        /// Gets and sets the playback framerate for the programs. Only Fps30 and Fps25 are valid values.
        /// </summary>
        public VideoFramerate Framerate
        {
            get
            {
                return _framerate;
            }
            internal set
            {
                _framerate = value;
            }
        }
        /// <summary>
        /// Gets and sets the playback framerate for the programs as a string. Only 30fps and 25fps are valid values.
        /// </summary>
        public string FramerateString
        {
            get
            {
                return _framerateString;
            }
            internal set
            {
                _framerateString = value;
            }
        }
        /// <summary>
        /// Gets the program chain id. Values start at one.
        /// </summary>
        public Int32 Id
        {
            get
            {
                return _id;
            }
        }
        /// <summary>
        /// Gets and sets the length in bytes for the program chain.
        /// </summary>
        public Int64 Length
        {
            get
            {
                return _length;
            }
            internal set
            {
                _length = value;
            }
        }
        /// <summary>
        /// Gets and sets the playback mode for the program chain.
        /// </summary>
        public Dvd.ProgramChainPlaybackMode PlaybackMode
        {
            get
            {
                return _playbackMode;
            }
            internal set
            {
                _playbackMode = value;
            }
        }
        /// <summary>
        /// Gets and sets the programs for the program chain.
        /// </summary>
        public Program[] Programs
        {
            get
            {
                return _programs;
            }
            internal set
            {
                _programs = value;
            }
        }
        /// <summary>
        /// Gets and sets the subpicture stream references for the program chain.
        /// </summary>
        public SubpictureStreamRef[] SubpictureRefs
        {
            get
            {
                return _subpictureRefs;
            }
            internal set
            {
                _subpictureRefs = value;
            }
        }
        /// <summary>
        /// Gets and sets the subtitles for the program chain.
        /// </summary>
        public Dvd.Subtitle[] Subtitles
        {
            get
            {
                return _subtitles;
            }
            internal set
            {
                _subtitles = value;
            }
        }
        /// <summary>
        /// Gets and sets whether the video stream is selected.
        /// </summary>
        public Boolean VideoChecked
        {
            get
            {
                return _videoChecked;
            }
            set
            {
                _videoChecked = value;
            }
        }
        /// <summary>
        /// Gets the video stream number as a formatted hexadecimal string.
        /// </summary>
        public string VideoIDString
        {
            get
            {
                return "E0";
            }
        }
        /// <summary>
        /// Gets the output filename for the video without the path.
        /// </summary>
        public string VideoOutputFilename
        {
            get
            {
                StringBuilder sb = new StringBuilder();

                // VTS_02_PGC_01_1 - 0xE0 - VIDEO - MPEG-2 (NTSC) - 720x480 - 4~3 Fullscreen.m2v

                sb.Append("VTS_" + _titleset.Id.ToString("00") + "_PGC_" + _id.ToString("00"));
                sb.Append(" - 0xE0 - VIDEO");
                sb.Append(" - " + _titleset.VideoCodingMode);
                sb.Append(" - " + _titleset.VideoResolution + " (" + _titleset.VideoStandard + ")");
                if (_titleset.VideoAspectRatio == VideoAspectRatio.Aspect_1_33)
                {
                    sb.Append(" - 4~3");
                }
                if (_titleset.VideoAspectRatio == VideoAspectRatio.Aspect_1_78)
                {
                    sb.Append(" - 16~9");
                }
                if ((_titleset.VideoAutoLetterbox || _titleset.VideoIsLetterBoxed) && !_titleset.VideoAutoPanScan)
                {
                    sb.Append(" (Letterboxed)");
                }
                if (!(_titleset.VideoAutoLetterbox || _titleset.VideoIsLetterBoxed) && _titleset.VideoAutoPanScan)
                {
                    sb.Append(" (PanScan)");
                }
                if ((_titleset.VideoAutoLetterbox || _titleset.VideoIsLetterBoxed) && _titleset.VideoAutoPanScan)
                {
                    sb.Append(" (Letterboxed-PanScan)");
                }
                sb.Append(".M2V");

                return sb.ToString();
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Creates a new instance of the ProgramChain class.
        /// </summary>
        /// <param name="value">The id for the program chain. Values start at one.</param>
        /// <param name="titleset">The parent titleset.</param>
        /// <param name="bytes">A byte array containing bytes to parse.</param>
        /// <param name="playbackMode">A byte containing the encoded playback mode.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when titleset is null.</exception>
        /// <exception cref="System.ArgumentNullException">Thrown when bytes is null.</exception>
        /// <exception cref="System.ArgumentException">Thrown when bytes length is not six bytes.</exception>
        public ProgramChain(Int32 value, TitleSet titleset, byte[] bytes, byte playbackMode)
        {
            if (titleset == null)
            {
                throw new ArgumentNullException("titleset");
            }
            if (bytes == null)
            {
                throw new ArgumentNullException("bytes");
            }
            if (bytes.Length != 6)
            {
                throw new ArgumentException("bytes length must be six bytes.");
            }

            _id = value;
            _programs = new Program[bytes[0]];
            _cells = new Cell[bytes[1]];
            _framerate = Dvd.Utility.ProgramChainFramerates[(bytes[5] >> 6) & 3];
            _framerateString = Dvd.Utility.ProgramChainFramerateNames[(bytes[5] >> 6) & 3];
            _titleset = titleset;

            if (playbackMode == 0)
            {
                _playbackMode = ProgramChainPlaybackMode.Sequential;
            }
            else
            {
                if (((playbackMode >> 7) & 1) == 0)
                {
                    _playbackMode = ProgramChainPlaybackMode.Random;
                }
                if (((playbackMode >> 7) & 1) == 1)
                {
                    _playbackMode = ProgramChainPlaybackMode.Shuffle;
                }
            }

            byte[] b = new byte[4];
            Array.Copy(bytes, 2, b, 0, b.Length);
            if (_framerateString == Dvd.Utility.ProgramChainFramerateNames[1])
            {
                _duration = new SmpteTimecode(b, VideoFramerate.Fps25, false);
            }
            if (_framerateString == Dvd.Utility.ProgramChainFramerateNames[3])
            {
                _duration = new SmpteTimecode(b, VideoFramerate.Fps30, true);
            }
            b = null;
        }

        /// <summary>
        /// Gets the output filename for the specified audio stream. The delay is left as
        /// 'DELAY 0ms' and should be changed by the demuxer to the appropriate value.
        /// </summary>
        /// <param name="value">Either the trackId or the index for the audio stream.</param>
        public string FormatAudioOutputFilename(Int32 value)
        {
            // Accept either track id or index.
            Int32 index = value;
            if (value > 7)
            {
                value = value & 7;
            }
            
            // VTS_02_PGC_01_1 - 0x80 - AUDIO - AC3 - 2ch - 48kHz - DRC - English -Director's Comments - DELAY 0ms.AC3
            StringBuilder result = new StringBuilder();
            Dvd.AudioStream audio = _titleset.AudioStreams[_audioStreamRefs[value].Index];

            result.Append("VTS_" + _titleset.Id.ToString("00") + "_PGC_" + _id.ToString("00"));
            result.Append(" - 0x" + _audioStreamRefs[value].TrackId.ToString("X2"));
            result.Append(" - AUDIO - " + audio.CodingMode);
            result.Append(" - " + audio.ChannelCount.ToString() + "ch");
            result.Append(" - " + audio.SampleRate);
            result.Append(" - " + audio.Quantization);
            if (audio.Language != Dvd.Utility.UnspecifiedString)
            {
                result.Append(" - " + audio.Language);
            }
            if (audio.CodeExtension != Dvd.Utility.UnspecifiedString)
            {
                result.Append(" - " + audio.CodeExtension);
            }
            result.Append(" - DELAY 0ms" + audio.FileExtension);

            return result.ToString();
        }

        /// <summary>
        /// Returns the output filename for the specified subtitle. The delay is left as
        /// 'DELAY 0ms' and should be changed by the demuxer to the appropriate value.
        /// </summary>
        /// <param name="value">>Either the trackId or the index for the subtitle.</param>
        public string FormatSubtitleOutputFilename(Int32 value)
        {
            // Accept either track id or index.
            Int32 index = value;
            if (value > 0x1f)
            {
                value = value & 0x1f;
            }
            
            // VTS_02_PGC_01_1 - 0x20 - SUBTITLE - Widescreen - English - Director's Comments - DELAY 0ms.SUP
            StringBuilder result = new StringBuilder();

            result.Append("VTS_" + _titleset.Id.ToString("00") + "_PGC_" + _id.ToString("00"));
            result.Append(" - 0x" + _subtitles[value].TrackId.ToString("X2"));
            result.Append(" - SUBTITLE - " + Dvd.Utility.ConvertSubtitleAspectToName(_subtitles[value].Aspect));
            if (_subtitles[(value & 0x1f)].Language != Dvd.Utility.UnspecifiedString)
            {
                result.Append(" - " + _subtitles[value].Language);
            }
            if (_subtitles[value].Description != Dvd.Utility.UnspecifiedString &&
                _subtitles[value].Description != Dvd.Utility.UnknownString)
            {
                result.Append(" - " + _subtitles[value].Description);
            }
            result.Append(" - DELAY 0ms.SUP");

            return result.ToString();
        }

        /// <summary>
        /// Updates internal values after all cells have been added.
        /// </summary>
        public void Update()
        {
            // Update program properties.
            Int32 endCell = _cells.Length;
            for (Int32 i = _programs.Length - 1; i >= 0; i--)
            {
                // Cells must start at one!
                if (_programs[i].StartCell > 0)
                {
                    _programs[i].EndCell = endCell;
                    _programs[i].CellCount = endCell - _programs[i].StartCell + 1;
                    endCell = _programs[i].StartCell - 1;

                    for (Int32 j = 0; j < _programs[i].CellCount; j++)
                    {
                        _programs[i].Duration.Add(_cells[_programs[i].StartCell + j - 1].Duration);
                        _programs[i].Length = _programs[i].Length + _cells[_programs[i].StartCell + j - 1].Length;
                    }
                }
            }

            // Update program chain length by adding all cell lengths.
            _length = 0;
            for (Int32 i = 0; i < _cells.Length; i++)
            {
                _length = _length + _cells[i].Length;
            }

            // Update subtitles.
            List<Dvd.Subtitle> tempSubtitles = new List<Subtitle>();
            for (Int32 i = 0; i < _subpictureRefs.Length; i++)
            {
                Dvd.SubpictureStreamRef subpictureRef = _subpictureRefs[i];

                if (subpictureRef.IsPresent)
                {
                    Dvd.Subpicture subpicture = _titleset.Subpictures[subpictureRef.Index];

                    // Title set aspect is fullscreen?
                    if (_titleset.VideoAspectRatio == VideoAspectRatio.Aspect_1_33)
                    {
                        if (subpictureRef.FullscreenTrackId == subpictureRef.PanScanTrackId)
                        {
                            tempSubtitles.Add(new Subtitle(subpictureRef.FullscreenTrackId, subpictureRef.Index,
                                subpicture.Language, _titleset.Subpictures[subpictureRef.Index].CodeExtension,
                                SubtitleAspect.FullscreenPanScan));
                        }
                        else
                        {
                            // FullscreenID for the first subpicture must be zero. After that,
                            // any FullscreenId or PanScanID with a value of zero is treated as
                            // though it is not present.

                            if ((subpictureRef.FullscreenTrackId == 0 && i == 0) || (subpictureRef.FullscreenTrackId > 0))
                            {
                                tempSubtitles.Add(new Subtitle(subpictureRef.FullscreenTrackId, subpictureRef.Index,
                                    subpicture.Language, _titleset.Subpictures[subpictureRef.Index].CodeExtension,
                                    SubtitleAspect.Fullscreen));
                            }
                            if (subpictureRef.PanScanTrackId > 0)
                            {
                                tempSubtitles.Add(new Subtitle(subpictureRef.PanScanTrackId, subpictureRef.Index,
                                    subpicture.Language, _titleset.Subpictures[subpictureRef.Index].CodeExtension,
                                    SubtitleAspect.PanScan));
                            }
                        }
                    }

                    // Title set aspect is widescreen?
                    if (_titleset.VideoAspectRatio == VideoAspectRatio.Aspect_1_78)
                    {
                        if (subpictureRef.WidescreenTrackId == subpictureRef.LetterboxTrackId)
                        {
                            tempSubtitles.Add(new Subtitle(subpictureRef.WidescreenTrackId, subpictureRef.Index,
                                subpicture.Language, _titleset.Subpictures[subpictureRef.Index].CodeExtension,
                                SubtitleAspect.WidescreenLetterbox));
                        }
                        else
                        {
                            // WidescreenID for the first subpicture must be zero. After that,
                            // any WidescreenId or LetterboxID with a value of zero is treated as
                            // though it is not present.

                            if ((subpictureRef.WidescreenTrackId == 0 && i == 0) || (subpictureRef.WidescreenTrackId > 0))
                            {
                                tempSubtitles.Add(new Subtitle(subpictureRef.WidescreenTrackId, subpictureRef.Index,
                                    subpicture.Language, _titleset.Subpictures[subpictureRef.Index].CodeExtension,
                                    SubtitleAspect.Widescreen));
                            }
                            if (subpictureRef.LetterboxTrackId > 0)
                            {
                                tempSubtitles.Add(new Subtitle(subpictureRef.LetterboxTrackId, subpictureRef.Index,
                                    subpicture.Language, _titleset.Subpictures[subpictureRef.Index].CodeExtension,
                                    SubtitleAspect.Letterbox));
                            }
                        }
                    }
                }
            }

            // Initialize subtitles array and copy values.
            _subtitles = tempSubtitles.ToArray();
        }

        #endregion
    }
}
