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

/*
    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>
    /// Top-level class that parses basic information about a DVD. The class requires the 'VIDEO_TS.IFO' and
    /// individual title set IFOs be present for parsing.
    /// </summary>
    sealed public class IfoReader
    {
        #region Fields

        private bool _isClosed = false;
        private Int32 _mainProgramChainId = 0;
        private Int32 _mainTitleSetId = 0;
        private string _path = string.Empty;
        private bool[] _regions = new bool[8];
        private TitleSet[] _titleSets = null;

        #endregion

        #region Properties

        /// <summary>
        /// Gets the total number of checked program chains.
        /// </summary>
        public Int32 CheckedProgramsCount
        {
            get
            {
                Int32 result = 0;

                if (!_isClosed)
                {
                    for (int i = 0; i < _titleSets.Length; i++)
                    {
                        for (int j = 0; j < _titleSets[i].ProgramChains.Length; j++)
                        {
                            if (_titleSets[i].ProgramChains[j].Checked)
                            {
                                result++;
                            }
                        }
                    }
                }

                return result;
            }
        }
        /// <summary>
        /// Gets the approximate total length in bytes of all checked program chains.
        /// </summary>
        /// <remarks>
        /// The total length is calculated by adding together the lengths of all selected program chains.
        /// The length of any unchecked program chain streams are not subtracted from the total, so the
        /// actual length may be less than the returned result.
        /// </remarks>
        public Int64 CheckedProgramsLength
        {
            get
            {
                Int64 result = 0;

                if (!_isClosed)
                {
                    for (int i = 0; i < _titleSets.Length; i++)
                    {
                        for (int j = 0; j < _titleSets[i].ProgramChains.Length; j++)
                        {
                            if (_titleSets[i].ProgramChains[j].Checked)
                            {
                                result = result + _titleSets[i].ProgramChains[j].Length;
                            }
                        }
                    }
                }

                return result;
            }
        }
        /// <summary>
        /// Gets a value indicating whether the dvd is playable in all regions (all flags set).
        /// </summary>
        public bool IsRegionAll
        {
            get
            {
                bool result = true;
                for (int i = 0; i < _regions.Length; i++)
                {
                    if (_regions[i] == false)
                    {
                        result = false;
                        break;
                    }
                }

                return result;
            }
        }
        /// <summary>
        /// Gets a value indicating whether the dvd is playable in all regions (all flags not set).
        /// </summary>
        public bool IsRegionZero
        {
            get
            {
                bool result = true;
                for (int i = 0; i < _regions.Length; i++)
                {
                    if (_regions[i] == true)
                    {
                        result = false;
                        break;
                    }
                }

                return result;
            }
        }
        /// <summary>
        /// Gets the id of the largest program chain by comparing fiilesizes.
        /// </summary>
        public Int32 MainProgramChainId
        {
            get
            {
                return _mainProgramChainId;
            }
        }
        /// <summary>
        /// Gets the id of the largest title set by comparing fiilesizes.
        /// </summary>
        public Int32 MainTitleSetId
        {
            get
            {
                return _mainTitleSetId;
            }
        }
        /// <summary>
        /// Gets the drive or folder path for the DVD.
        /// </summary>
        public string Path
        {
            get
            {
                return _path;
            }
        }
        /// <summary>
        /// Gets the regions for the dvd as a an array with the index + 1 as the region number.
        /// </summary>
        public bool[] Regions
        {
            get
            {
                return _regions;
            }
        }
        /// <summary>
        /// Gets the regions for the dvd as a string containing CSV values.
        /// </summary>
        public string RegionString
        {
            get
            {
                StringBuilder sb = new StringBuilder();

                if (IsRegionZero)
                {
                    sb.Append("0, ");
                }

                for (int i = 0; i < _regions.Length; i++)
                {
                    sb.Append((i + 1).ToString() + ", ");
                }

                // Remove final comma and space chars.
                if (sb.Length > 2)
                {
                    sb.Remove(sb.Length - 2, 2);
                }

                return sb.ToString();
            }
        }
        /// <summary>
        /// Gets and sets the title sets for the dvd.
        /// </summary>
        public TitleSet[] TitleSets
        {
            get
            {
                return _titleSets;
            }
            internal set
            {
                _titleSets = value;
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Creats a new instance of the IfoReader class.
        /// </summary>
        /// <param name="path">The drive or folder path which contains the DVD IFOs.</param>
        /// <exception cref="System.ArgumentException">Thrown when path is null or empty.</exception>
        /// <exception cref="System.IO.DirectoryNotFoundException">Thrown when path is not found.</exception>
        /// <exception cref="System.IO.FileNotFoundException">Thrown when VIDEO_TS.IFO is not found.</exception>
        public IfoReader(string path)
        {
            if (string.IsNullOrEmpty(path))
            {
                throw new ArgumentException("path is null or empty.");
            }
            if (!System.IO.Directory.Exists(path))
            {
                throw new System.IO.DirectoryNotFoundException("'" + path + "' not found.");
            }
            if (!System.IO.File.Exists(path + "\\VIDEO_TS.IFO"))
            {
                throw new System.IO.FileNotFoundException("VIDEO_TS.IFO not found.");
            }

            _path = path;

            ParseMainIfo();
        }

        /// <summary>
        /// Closes the IfoReader and release all resources.
        /// </summary>
        public void Close()
        {
            _isClosed = true;
            _mainProgramChainId = 0;
            _mainTitleSetId = 0;
            _path = string.Empty;
            _regions = null;
            _titleSets = null;
        }

        /// <summary>
        /// Gets information about the DVD by parsing the IFOs.
        /// </summary>
        private void ParseMainIfo()
        {
            // Get all ifos in the folder.
            string[] titleSetFiles = Directory.GetFiles(_path, "VTS*.IFO");
            string[] vobFiles = Directory.GetFiles(_path, "VTS*.VOB");
            
            if (titleSetFiles.Length == 0 || vobFiles.Length == 0)
            {
                return;
            }

            // Dimension the title sets array to the number of IFOs in the folder.
            _titleSets = new TitleSet[titleSetFiles.Length];

            // Process vob file info.            
            for (int i = 0; i < vobFiles.Length; i++)
            {
                // Get the id of the title set we are working with.
                Int32 vtsId = Dvd.Utility.GetVideoTitleSetID(vobFiles[i]);

                // Existing title set? Update values.
                if (_titleSets[vtsId -1] != null)
                {
                    Int32 part = Dvd.Utility.GetVideoTitleSetPartNumber(vobFiles[i]);

                    // Only update if not the first part for the title set.
                    if (part > 0)
                    {
                        // Increment the number of parts found so far for the title set.
                        _titleSets[vtsId - 1].Parts = _titleSets[vtsId -1].Parts + 1;

                        // Increment the total length for the title set by the size of this part.
                        _titleSets[vtsId - 1].Length = _titleSets[vtsId -1].Length + (new FileInfo(vobFiles[i]).Length);
                    }
                }

                // New title set? Add it.
                if (_titleSets[vtsId - 1] == null)
                {
                    _titleSets[vtsId - 1] = new TitleSet(vtsId);
                    _titleSets[vtsId - 1].VobFilename = vobFiles[i];
                    _titleSets[vtsId - 1].Parts = 1;
                    _titleSets[vtsId - 1].Length = (new FileInfo(vobFiles[i]).Length);
                }
            }

            // Parse all title set ifos.
            for (int i = 0; i < titleSetFiles.Length; i++)
            {
                ParseTitleSetIfo(titleSetFiles[i]);
            }

            // Get DVD info.
            Int32 value = 0;
            using (FileStream reader = new FileStream(_path + "VIDEO_TS.IFO", FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                //
                // Get DVS regions.
                //
                reader.Seek(0x23, SeekOrigin.Begin);
                value = reader.ReadByte();
                // Instead of checking each bit, loop and shift them. A value of zero means the region is present.
                for (int i = 0; i < _regions.Length; i++)
                {
                    if (((value >> i) & 0x01) == 0)
                    {
                        _regions[i] = true;
                    }
                }

                //
                // Get PGC angles count. Some PGCs might not have angles data.
                //
                reader.Seek(0xc4, SeekOrigin.Begin);
                Int64 index = ((reader.ReadByte() * 0x1000000) + (reader.ReadByte() * 0x10000) + (reader.ReadByte()
                    * 0x100) + reader.ReadByte()) * 2048;
                reader.Seek(index, SeekOrigin.Begin);

                Int32 numberOfTitles = (reader.ReadByte() * 0x100) + reader.ReadByte();
                reader.Seek(6, SeekOrigin.Current);

                byte[] b = new byte[12];
                Int32 titleSetId = 1;
                Int32 programChainId = 1;
                for (int i = 0; i < numberOfTitles; i++)
                {
                    if (reader.Read(b, 0, b.Length) != -1)
                    {
                        titleSetId = b[6];
                        programChainId = b[7];
                        
                        _titleSets[titleSetId -1].ProgramChains[programChainId -1].AnglesCount = b[1];
                        
                        titleSetId = 1;
                        programChainId = 1;
                    }
                }
            }

            // Determine the main PGC by filesize.
            Int64 size = 0;
            for (int i = 0; i < _titleSets.Length; i++)
            {
                for (int j = 0; j < _titleSets[i].ProgramChains.Length; j++)
                {
                    if (_titleSets[i].ProgramChains[j].Length > size)
                    {
                        size = _titleSets[i].ProgramChains[j].Length;
                        _mainTitleSetId = i + 1;
                        _mainProgramChainId = j + 1;
                    }
                }
            }
        }

        /// <summary>
        /// Parses an IFO file.
        /// </summary>
        /// <param name="filename">The filename of the IFO to parse.</param>
        /// <exception cref="System.ArgumentException">Thrown when filename is null or empty.</exception>
        /// <exception cref="System.IO.FileNotFoundException">Thrown when filename is not found.</exception>
        private void ParseTitleSetIfo(string filename)
        {
            if (string.IsNullOrEmpty(filename))
            {
                throw new ArgumentException("filename is null or empty.");
            }
            if (!File.Exists(filename))
            {
                throw new FileNotFoundException("'" + filename + "' not found.");
            }

            // Get video title set id and filename.
            Int32 vtsId = Dvd.Utility.GetVideoTitleSetID(filename);
            _titleSets[vtsId -1].IfoFilename = filename;

            // Title set exists?
            if (_titleSets[vtsId - 1] == null)
            {
                return;
            }

            // Load the entire IFO file. Load the title set.
            byte[] buffer = System.IO.File.ReadAllBytes(filename);
            TitleSet vts = _titleSets[vtsId - 1];

            // Parse data in the IFO.
            vts.ProgramCainsTableOffset = (((buffer[0xcc] * 0x1000000) + (buffer[0xcd] * 0x10000)) + (buffer[0xce] * 0x100)) + buffer[0xcf];

            byte[] b = new byte[2];
            b[0] = buffer[0x200];
            b[1] = buffer[0x201];
            vts.SetVideoAttributes(b);
            b = null;

            // Load audio streams info.
            vts.AudioStreams = new AudioStream[(buffer[0x202] * 0x100) + buffer[0x203]];
            if (vts.AudioStreams.Length != 0)
            {
                for (int i = 0; i < vts.AudioStreams.Length; i++)
                {
                    b = new byte[8]; // audio attributes
                    byte[] xb = new byte[24]; // multi-channel extension - karaoke mode
                    Array.Copy(buffer, 0x204 + (i * b.Length), b, 0, b.Length);
                    Array.Copy(buffer, 0x318 + (i * b.Length), xb, 0, xb.Length);
                    vts.AudioStreams[i] = new AudioStream(b, xb);
                    b = null;
                }
            }

            // Load subpicture streams info.
            vts.Subpictures = new Subpicture[(buffer[0x254] * 0x100) + buffer[0x255]];
            if (vts.Subpictures.Length != 0)
            {
                for (int i = 0; i < vts.Subpictures.Length; i++)
                {
                    b = new byte[6]; // subpicture attributes
                    Array.Copy(buffer, 0x256 + (i * b.Length), b, 0, b.Length);
                    vts.Subpictures[i] = new Subpicture(i + 1, b);
                    b = null;
                }
            }

            // Load program chains info.
            vts.ProgramChains = new ProgramChain[(buffer[vts.ProgramCainsTableOffset * 0x800] * 0x100) +
                buffer[(vts.ProgramCainsTableOffset * 0x800) + 1]];

            if (vts.ProgramChains.Length != 0)
            {
                for (int i = 0; i < vts.ProgramChains.Length; i++)
                {
                    // Calculate offsets.
                    b = new byte[4];
                    Array.Copy(buffer, ((vts.ProgramCainsTableOffset * 0x800) + (i * 8)) + 0xc, b, 0, b.Length);
                    Int32 pgcOffset = (b[0] * 0x1000000) + (b[1] * 0x10000) + (b[2] * 0x100) + b[3];
                    pgcOffset += vts.ProgramCainsTableOffset * 0x800;

                    b = new byte[6];
                    Array.Copy(buffer, pgcOffset + 2, b, 0, b.Length);
                    vts.ProgramChains[i] = new ProgramChain(i + 1, vts, b, buffer[pgcOffset + 0xa3]);
                    b = null;

                    // Load audio stream ids.
                    vts.ProgramChains[i].AudioStreamRefs = new AudioStreamRef[vts.AudioStreams.Length];
                    for (int j = 0; j < vts.ProgramChains[i].AudioStreamRefs.Length; j++)
                    {
                        Int32 value = buffer[(pgcOffset + 0xc) + (2 * j)];
                        if ((value & 0x7) < vts.AudioStreams.Length)
                        {
                            vts.ProgramChains[i].AudioStreamRefs[j] = new AudioStreamRef(j,
                                Dvd.Utility.AudioStreamCodingModePrefixes[vts.AudioStreams[
                                (value & 0x7)].CodingModeValue] + (value & 0x7),
                                (((value >> 7) & 1) == 1));
                        }
                    }

                    // Load subpicture ids.
                    vts.ProgramChains[i].SubpictureRefs = new SubpictureStreamRef[vts.Subpictures.Length];
                    for (int j = 0; j < vts.ProgramChains[i].SubpictureRefs.Length; j++)
                    {
                        b = new byte[4];
                        Array.Copy(buffer, (pgcOffset + 0x1c) + (b.Length * j), b, 0, b.Length);
                        vts.ProgramChains[i].SubpictureRefs[j] = new SubpictureStreamRef(j, b);
                        b = null;
                    }

                    // Load cell playback and position data.
                    // Cells array already created when PGC created, that is why a new instance is not crated here.
                    for (int j = 0; j < vts.ProgramChains[i].Cells.Length; j++)
                    {
                        b = new byte[24];
                        byte[] b2 = new byte[4];
                        Array.Copy(buffer, (pgcOffset + (buffer[pgcOffset + 0xe8] * 0x100) + buffer[pgcOffset + 0xe9] + (b.Length * j)), b, 0, b.Length);
                        Array.Copy(buffer, (pgcOffset + (buffer[pgcOffset + 0xea] * 0x100) + buffer[pgcOffset + 0xeb] + (b2.Length * j)), b2, 0, b2.Length);
                        vts.ProgramChains[i].Cells[j] = new Cell(b, b2);
                        b = null;
                        b2 = null;
                    }

                    // Load program data.
                    // Program array already created when PGC created, that is why a new instance is not crated here.
                    for (int j = 0; j < vts.ProgramChains[i].Programs.Length; j++)
                    {
                        vts.ProgramChains[i].Programs[j] = new Program(j + 1);
                        vts.ProgramChains[i].Programs[j].StartCell = buffer[pgcOffset + (buffer[pgcOffset + 0xe6]
                            * 0x100) + buffer[pgcOffset + 0xe7] + j];
                    }

                    // Done adding cells, have the program chain to update its' internal values.
                    vts.ProgramChains[i].Update();
                }
            }
        }

        #endregion
    }
}
