using System;
using System.Text;

/*
    Copyright (c) 2011 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>
    /// Represents a timecode with a resolution based on milliseconds.
    /// </summary>
    sealed public class Timecode
    {
        #region Fields

        private Int32 _milliseconds = 0;
        private const Int32 _maximumMilliseconds = 359999000; // 99:59:59.999
        
        #endregion

        #region Properties

        /// <summary>
        /// Gets the hours portion of the timecode. The maximum value is 99 hours.
        /// </summary>
        public Int32 Hours
        {
            get
            {
                return ConvertToHours();
            }
        }
        /// <summary>
        /// Gets the maximum number of milliseconds that the timecode can hold.
        /// </summary>
        public Int64 MaximumMilliseconds
        {
            get
            {
                return _maximumMilliseconds;
            }
        }
        /// <summary>
        /// Gets the milliseconds portion of the timecode.
        /// </summary>
        public Int32 Milliseconds
        {
            get
            {
                return ConvertToMilliseconds();
            }
        }
        /// <summary>
        /// Gets the minutes portion of the timecode.
        /// </summary>
        public Int32 Minutes
        {
            get
            {
                return ConvertToMinutes();
            }
        }
        /// <summary>
        /// Gets the seconds portion of the timecode.
        /// </summary>
        public Int32 Seconds
        {
            get
            {
                return ConvertToSeconds();
            }
        }
        /// <summary>
        /// Gets the total number of hours.
        /// </summary>
        public Int32 TotalHours
        {
            get
            {
                return Convert.ToInt32(_milliseconds / 3600000);
            }
        }
        /// <summary>
        /// Gets the total number of milliseconds.
        /// </summary>
        public Int32 TotalMilliseconds
        {
            get
            {
                return _milliseconds;
            }
        }
        /// <summary>
        /// Gets the total number of minutes.
        /// </summary>
        public Int32 TotalMinutes
        {
            get
            {
                return Convert.ToInt32(_milliseconds / 60000);
            }
        }
        /// <summary>
        /// Gets the total number of seconds.
        /// </summary>
        public Int32 TotalSeconds
        {
            get
            {
                return Convert.ToInt32(_milliseconds / 1000);
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Creates a new instance of the Timecode class initialized to zero milliseconds.
        /// </summary>
        public Timecode()
        {
            _milliseconds = 0;
        }
   
        /// <summary>
        /// Creates a new instance of the Timecode class initialized from a byte array
        /// created by the ToBinary method.
        /// </summary>
        /// <param name="bytes">The bytes containing the encoded value for the timecode.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when bytes is null.</exception>
        /// <exception cref="System.ArgumentNullException">Thrown when bytes length is not four bytes.</exception>
        /// <exception cref="System.ArgumentException">Thrown when total milliseconds is less than '00:00:00:00'.</exception>
        /// <exception cref="System.ArgumentException">Thrown when total milliseconds is greater than '99:59:59:59'.</exception>
        public Timecode(byte[] bytes)
        {
            if (bytes == null)
            {
                throw new ArgumentNullException("bytes");
            }
            if (bytes.Length != 4)
            {
                throw new ArgumentException("bytes length must be four bytes.");
            }

            _milliseconds = (Int32)(bytes[0] | bytes[1] << 8 | bytes[2] << 16 | bytes[3] << 24);

            if (_milliseconds < 0)
            {
                throw new ArgumentException("total milliseconds is less than '00:00:00:00'");
            }
            if (_milliseconds > _maximumMilliseconds)
            {
                throw new ArgumentException("total milliseconds is greater than '99:59:59:59'");
            }
        }

        /// <summary>
        /// Creates a new instance of the Timecode class initialized to the specified values.
        /// </summary>
        /// <param name="hours">The number of hours.</param>
        /// <param name="minutes">The number of minutes.</param>
        /// <param name="seconds">The number of seconds.</param>
        /// <param name="milliseconds">The number of milliseconds.</param>
        /// <exception cref="System.ArgumentException">Thrown when hours is less than zero.</exception>
        /// <exception cref="System.ArgumentException">Thrown when minutes is less than zero.</exception>
        /// <exception cref="System.ArgumentException">Thrown when seconds is less than zero.</exception>
        /// <exception cref="System.ArgumentException">Thrown when milliseconds is less than zero.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown when the total milliseconds causes an
        /// overflow because the result is too long.</exception>
        /// <exception cref="System.ArgumentException">Thrown when total milliseconds is less than '00:00:00:00'.</exception>
        /// <exception cref="System.ArgumentException">Thrown when total milliseconds is greater than '99:59:59:59'.</exception>
        public Timecode(Int32 hours, Int32 minutes, Int32 seconds, Int32 milliseconds)
        {
            if (hours < 0)
            {
                throw new Exception("hours less than zero.");
            }
            if (minutes < 0)
            {
                throw new Exception("minutes less than zero.");
            }
            if (seconds < 0)
            {
                throw new Exception("seconds less than zero.");
            }
            if (milliseconds < 0)
            {
                throw new Exception("milliseconds less than zero.");
            }

            Int32 totalMilliseconds = ConvertToTotalMilliseconds(hours, minutes, seconds, milliseconds);

            if (totalMilliseconds < Int32.MinValue || totalMilliseconds > Int32.MaxValue)
            {
                throw new ArgumentOutOfRangeException("total milliseconds", "Overflow - result too long.");
            }
            if (totalMilliseconds < 0)
            {
                throw new ArgumentException("total milliseconds is less than '00:00:00:00'");
            }
            if (totalMilliseconds > _maximumMilliseconds)
            {
                throw new ArgumentException("total milliseconds is greater than '99:59:59:59'");
            }
        
            _milliseconds = totalMilliseconds;
        }

        /// <summary>
        /// Creates a new instance of the Timecode class initialized to the value of the
        /// specified Timecode.
        /// </summary>
        /// <param name="value">The Timecode used to initialize this instance.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when value is null.</exception>
        /// <exception cref="System.ArgumentException">Thrown when value.Milliseconds is less than '00:00:00:00'.</exception>
        /// <exception cref="System.ArgumentException">Thrown when value.Milliseconds is greater than '99:59:59:59'.</exception>
        public Timecode(Timecode value)
        {
            if (value._milliseconds < 0)
            {
                throw new ArgumentException("value.Milliseconds is less than '00:00:00:00'");
            }
            if (value._milliseconds > _maximumMilliseconds)
            {
                throw new ArgumentException("value.Milliseconds is greater than '99:59:59:59'");
            }
            
            _milliseconds = value._milliseconds;
        }

        /// <summary>
        /// Adds the specifed hours, minutes, seconds, and milliseconds to the current value.
        /// </summary>
        /// <param name="hours">The number of hours to add.</param>
        /// <param name="minutes">The number of minutes to add.</param>
        /// <param name="seconds">The number of seconds to add.</param>
        /// <param name="milliseconds">The number of milliseconds to add.</param>
        /// <exception cref="System.ArgumentException">Thrown when hours is less than zero.</exception>
        /// <exception cref="System.ArgumentException">Thrown when minutes is less than zero.</exception>
        /// <exception cref="System.ArgumentException">Thrown when seconds is less than zero.</exception>
        /// <exception cref="System.ArgumentException">Thrown when milliseconds is less than zero.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown when the total milliseconds causes an
        /// overflow because the result is too long.</exception>
        /// <exception cref="System.ArgumentException">Thrown when total milliseconds is less than '00:00:00:00'.</exception>
        /// <exception cref="System.ArgumentException">Thrown when total milliseconds is greater than '99:59:59:59'.</exception>
        public void Add(Int32 hours, Int32 minutes, Int32 seconds, Int32 milliseconds)
        {
            if (hours < 0)
            {
                throw new Exception("hours less than zero.");
            }
            if (minutes < 0)
            {
                throw new Exception("minutes less than zero.");
            }
            if (seconds < 0)
            {
                throw new Exception("seconds less than zero.");
            }
            if (milliseconds < 0)
            {
                throw new Exception("milliseconds less than zero.");
            }

            Int32 totalMilliseconds = ConvertToTotalMilliseconds(hours, minutes, seconds, milliseconds);

            if (_milliseconds + totalMilliseconds < Int32.MinValue || _milliseconds + totalMilliseconds > Int32.MaxValue)
            {
                throw new ArgumentOutOfRangeException("total milliseconds", "Overflow - result too long.");
            }
            if (_milliseconds + totalMilliseconds < 0)
            {
                throw new ArgumentException("total milliseconds is less than '00:00:00:00'");
            }
            if (_milliseconds + totalMilliseconds > _maximumMilliseconds)
            {
                throw new ArgumentException("total milliseconds is greater than '99:59:59:59'");
            }
        
            _milliseconds = _milliseconds + totalMilliseconds;
        }

        /// <summary>
        /// Adds the value of a Timecode instance to this instance.
        /// </summary>
        /// <param name="value">The Timecode to add.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when value is null.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown when value causes an overflow
        /// because the result is too long.</exception>
        /// <exception cref="System.ArgumentException">Thrown when value.Milliseconds is less than '00:00:00:00'.</exception>
        /// <exception cref="System.ArgumentException">Thrown when value.Milliseconds is greater than '99:59:59:59'.</exception>
        public void Add(Timecode value)
        {
            if (_milliseconds + value._milliseconds < Int32.MinValue || _milliseconds + value._milliseconds > Int32.MaxValue)
            {
                throw new ArgumentOutOfRangeException("value", "Overflow - result too long.");
            }
            if (_milliseconds + value._milliseconds < 0)
            {
                throw new ArgumentException("total milliseconds is less than '00:00:00:00'");
            }
            if (_milliseconds + value._milliseconds > _maximumMilliseconds)
            {
                throw new ArgumentException("total milliseconds is greater than '99:59:59:59'");
            }

            _milliseconds = _milliseconds + value._milliseconds;
        }

        /// <summary>
        /// Compares the specified Timecode to this instance and returns a value indicating
        /// the relationship between the values.
        /// </summary>
        /// <param name="value">The Timecode to compare to this instance.</param>
        /// <returns>Less than zero if this Timecode is less than value; zero if the Timecodes
        /// are identical; and greater than zero if this Timecode is greater than value.</returns>
        /// <exception cref="System.ArgumentNullException">Thrown when value is null.</exception>
        public int Compare(Timecode value)
        {
            if (_milliseconds > value._milliseconds)
            {
                return 1;
            }
            if (_milliseconds < value._milliseconds)
            {
                return -1;
            }

            return 0;
        }

        /// <summary>
        /// Converts total milliseconds to a timecode and returns the hours portion.
        /// </summary>
        /// <returns>THe hours as an Int32.</returns>
        private Int32 ConvertToHours()
        {
            return Convert.ToInt32((_milliseconds / 3600000) % 100);
        }

        /// <summary>
        /// Converts total milliseconds to a timecode and returns the milliseconds portion.
        /// </summary>
        /// <returns>The milliseconds as an Int32.</returns>
        private Int32 ConvertToMilliseconds()
        {
            return Convert.ToInt32(_milliseconds % 1000);
        }

        /// <summary>
        /// Converts total milliseconds to a timecode and returns the minutes portion.
        /// </summary>
        /// <returns>The minutes as an Int32.</returns>
        private Int32 ConvertToMinutes()
        {
            return Convert.ToInt32((_milliseconds / 60000) % 60);
        }

        /// <summary>
        /// Converts total milliseconds to a timecode and returns the seconds portion.
        /// </summary>
        /// <returns>The seconds as an Int32.</returns>
        private Int32 ConvertToSeconds()
        {
            return Convert.ToInt32((_milliseconds / 1000) % 60);
        }

        /// <summary>
        /// Converts the supplied interval values to to a total milliseconds value. 
        /// </summary>
        /// <param name="hours">The hours to convert.</param>
        /// <param name="minutes">The minutes to convert.</param>
        /// <param name="seconds">The seconds to convert.</param>
        /// <param name="milliseconds">The milliseconds to convert.</param>
        /// <returns>The total milliseconds as an Int32.</returns>
        private Int32 ConvertToTotalMilliseconds(Int32 hours, Int32 minutes, Int32 seconds, Int32 milliseconds)
        {
            return (hours * 3600000) + (minutes * 60000) + (seconds * 1000) + milliseconds;
        }

        /// <summary>
        /// Subtracts the specifed hours, minutes, seconds, and milliseconds from the current value.
        /// </summary>
        /// <param name="hours">The number of hours to subtract.</param>
        /// <param name="minutes">The number of minutes to subtract.</param>
        /// <param name="seconds">The number of seconds to subtract.</param>
        /// <param name="milliseconds">The number of milliseconds to subtract.</param>
        /// <exception cref="System.ArgumentException">Thrown when hours is less than zero.</exception>
        /// <exception cref="System.ArgumentException">Thrown when minutes is less than zero.</exception>
        /// <exception cref="System.ArgumentException">Thrown when seconds is less than zero.</exception>
        /// <exception cref="System.ArgumentException">Thrown when milliseconds is less than zero.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown when the total milliseconds causes an
        /// overflow because the result is too long.</exception>
        /// <remarks>
        /// If the result is less than zero, the timecode will be zero.
        /// </remarks>
        /// <exception cref="System.ArgumentException">Thrown when total milliseconds is less than '00:00:00:00'.</exception>
        /// <exception cref="System.ArgumentException">Thrown when total milliseconds is greater than '99:59:59:59'.</exception>
        public void Subtract(Int32 hours, Int32 minutes, Int32 seconds, Int32 milliseconds)
        {
            if (hours < 0)
            {
                throw new Exception("hours less than zero.");
            }
            if (minutes < 0)
            {
                throw new Exception("minutes less than zero.");
            }
            if (seconds < 0)
            {
                throw new Exception("seconds less than zero.");
            }
            if (milliseconds < 0)
            {
                throw new Exception("milliseconds less than zero.");
            }

            Int32 totalMilliseconds = ConvertToTotalMilliseconds(hours, minutes, seconds, milliseconds);

            if (_milliseconds - totalMilliseconds < Int32.MinValue || _milliseconds - totalMilliseconds > Int32.MaxValue)
            {
                throw new ArgumentOutOfRangeException("total milliseconds", "Overflow - result too long.");
            }
            if (_milliseconds + totalMilliseconds < 0)
            {
                throw new ArgumentException("total milliseconds is less than '00:00:00:00'");
            }
            if (_milliseconds + totalMilliseconds > _maximumMilliseconds)
            {
                throw new ArgumentException("total milliseconds is greater than '99:59:59:59'");
            }

            _milliseconds = _milliseconds - totalMilliseconds;

            if (_milliseconds < 0)
            {
                _milliseconds = 0;
            }
        }

        /// <summary>
        /// Subtracts the value of a Timecode instance from this instance.
        /// </summary>
        /// <param name="value">The Timecode to subtract.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when value is null.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown when value causes an overflow
        /// because the result is too long.</exception>
        /// <exception cref="System.ArgumentException">Thrown when total milliseconds is less than '00:00:00:00'.</exception>
        /// <exception cref="System.ArgumentException">Thrown when total milliseconds is greater than '99:59:59:59'.</exception>
        public void Subtract(Timecode value)
        {
            if (_milliseconds - value._milliseconds < Int32.MinValue || _milliseconds - value._milliseconds > Int32.MaxValue)
            {
                throw new ArgumentOutOfRangeException("value", "Overflow - result too long.");
            }
            if (_milliseconds - value._milliseconds < 0)
            {
                throw new ArgumentException("total milliseconds is less than '00:00:00:00'");
            }
            if (_milliseconds - value._milliseconds > _maximumMilliseconds)
            {
                throw new ArgumentException("total milliseconds is greater than '99:59:59:59'");
            }

            _milliseconds = _milliseconds - value._milliseconds;
        }

        /// <summary>
        /// Returns the timecode as a byte array.
        /// </summary>
        /// <returns>A byte array with a length of eight bytes.</returns>
        public byte[] ToBinary()
        {
            byte[] result = new byte[4];

            result[0] = (byte)_milliseconds;
            result[1] = (byte)(_milliseconds >> 8);
            result[2] = (byte)(_milliseconds >> 16);
            result[3] = (byte)(_milliseconds >> 24);
 
            return result;
        }

        /// <summary>
        /// Returns the timecode as a  formatted string ('hh:mm:ss.fff').
        /// </summary>
        /// <returns>A string.</returns>
        public override string ToString()
        {
            return ConvertToHours().ToString("00") + ":" + ConvertToMinutes().ToString("00") + ":" +
                ConvertToSeconds().ToString("00") + "." + ConvertToMilliseconds().ToString("000");
        }

        /// <summary>
        /// Returns the timecode as a custom formatted string.
        /// </summary>
        /// <param name="format">A format string.</param>
        /// <returns>A string.</returns>
        /// <exception cref="System.ArgumentException">Thrown when format is null or empty.</exception>
        /// <remarks>
        /// <list type="table">
        /// <listheader>
        /// <term>Format Specifier</term>
        /// <term>Description</term>
        /// </listheader>
        /// <item>
        ///   <term>f</term>
        ///   <description>Displays the number of frames. If less than ten, only a sigle digit
        ///   is displayed.</description>
        /// </item>
        /// <item>
        ///   <term>ff</term>
        ///   <description>Displays the number of frames as a two-digit value from 0 to the framerate
        ///    minus one.</description>
        /// </item>
        /// <item>
        ///   <term>h</term>
        ///   <description>Displays the number of hours from 0 to 99. If less than ten, only a sigle digit
        ///   is displayed.</description>
        /// </item>
        /// <item>
        ///   <term>hh</term>
        ///   <description>Displays the number of hours as a two-digit value from 0 to 99.</description>
        /// </item>
        /// <item>
        ///   <term>m</term>
        ///   <description>Displays the number of minutes. If less than ten, only a sigle digit is displayed.</description>
        /// </item>
        /// <item>
        ///   <term>mm</term>
        ///   <description>Displays the number of minutes.</description>
        /// </item>
        /// <item>
        ///   <term>s</term>
        ///   <description>Displays the number of seconds. If less than ten, only a sigle digit is displayed.</description>
        /// </item>
        /// <item>
        ///   <term>ss</term>
        ///   <description>Displays the number of seconds.</description>
        /// </item>
        /// <item>
        ///   <term>\</term>
        ///   <description>Escapes the following character.</description>
        /// </item>
        /// </list>
        /// </remarks>
        public string ToString(string format)
        {
            if (string.IsNullOrEmpty(format))
            {
                throw new ArgumentException("format is null or empty.");
            }

            StringBuilder sb = new StringBuilder();
            Int32 i = 0;
            string tempChars = string.Empty;

            while (i < format.Length)
            {
                // Get current character.
                tempChars = format.Substring(i, 1);

                // Escape character? Output it and next character. Otherwise is last character
                // so fallthrough and loop code will output the backslash.
                if (tempChars == "\\" && i < format.Length - 1)
                {
                    tempChars = format.Substring(i, 2);
                    i++; // Two chars pulled, must increment here to make i=i+2
                }

                // Hours?
                if (tempChars == "h")
                {
                    // If not at end of string AND next char is 'h', update tempChars to 'hh'.
                    if (i < format.Length - 1 && format[i + 1] == 'h')
                    {
                        tempChars = format.Substring(i, 2);
                    }

                    // Display single digit hours?
                    if (tempChars == "h")
                    {
                        tempChars = ConvertToHours().ToString();
                    }

                    // Display two-digit hours?
                    if (tempChars == "hh")
                    {
                        tempChars = ConvertToHours().ToString("00");
                        i++; // Two chars pulled, must increment here to make i=i+2
                    }
                }

                // Minutes?
                if (tempChars == "m")
                {
                    // If not at end of string AND next char is 'm', update tempChars to 'mm'.
                    if (i < format.Length - 1 && format[i + 1] == 'm')
                    {
                        tempChars = format.Substring(i, 2);
                    }

                    // Display single digit minutes?
                    if (tempChars == "m")
                    {
                        tempChars = ConvertToMinutes().ToString();
                    }

                    // Display two-digit minutes?
                    if (tempChars == "mm")
                    {
                        tempChars = ConvertToMinutes().ToString("00");
                        i++; // Two chars pulled, must increment here to make i=i+2
                    }
                }

                // Seconds?
                if (tempChars == "s")
                {
                    // If not at end of string AND next char is 's', update tempChars to 'ss'.
                    if (i < format.Length - 1 && format[i + 1] == 's')
                    {
                        tempChars = format.Substring(i, 2);
                    }

                    // Display single digit seconds?
                    if (tempChars == "s")
                    {
                        tempChars = ConvertToSeconds().ToString();
                    }

                    // Display two-digit seconds?
                    if (tempChars == "ss")
                    {
                        tempChars = ConvertToSeconds().ToString("00");
                        i++; // Two chars pulled, must increment here to make i=i+2
                    }
                }

                // Milliseconds?
                if (tempChars == "f")
                {
                    // If not at end of string AND next char is 'f', update tempChars to 'ff'.
                    if (i < format.Length - 1 && format[i + 1] == 'f')
                    {
                        tempChars = format.Substring(i, 2);
                    }
                    // If not at end of string AND next two chars are 'f', update tempChars to 'fff'.
                    if (i < format.Length - 2 && format[i + 1] == 'f' && format[i + 2] == 'f')
                    {
                        tempChars = format.Substring(i, 3);
                    }

                    // Display single digit milliseconds?
                    if (tempChars == "f")
                    {
                        tempChars = ConvertToMilliseconds().ToString();
                    }

                    // Display two-digit milliseconds?
                    if (tempChars == "ff")
                    {
                        tempChars = ConvertToMilliseconds().ToString("00");
                        i++; // Two chars pulled, must increment here to make i=i+2
                    }

                    // Display two-digit milliseconds?
                    if (tempChars == "fff")
                    {
                        tempChars = ConvertToMilliseconds().ToString("000");
                        i = i + 2; // Three chars pulled, must increment here to make i=i+3
                    }
                }

                sb.Append(tempChars);
                i++;
            }

            return sb.ToString();
        }

        #endregion
    }
}