/*
 * This file is part of SubLib.
 * Copyright (C) 2005-2007 Pedro Castro
 *
 * SubLib is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * SubLib is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

using System;
using System.Collections;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;

namespace SubLib {

/// <summary>Represents the root class of all the subtitles.</summary>
/// <remarks>A <see cref="Subtitles" /> class is created using the <see cref="SubtitleFactory" />.</remarks>
public class Subtitles {
	private SubtitleCollection collection = null;
	private SubtitleProperties properties = null;
	
	/// <summary>A collection which contains the subtitles.</summary>
	public SubtitleCollection Collection {
		get { return collection; }
		set { collection = value; }
	}
	
	/// <summary>The properties of the subtitles.</summary>
	public SubtitleProperties Properties {
		get { return properties; }
		set { properties = value; }
	}
	
	/// <summary>Information about the available subtitle types.</summary>
	public static SubtitleTypeInfo[] AvailableTypes {
		get {
			SubtitleFormat[] formats = BuiltInSubtitleFormats.SubtitleFormats;
			SubtitleTypeInfo[] types = new SubtitleTypeInfo[formats.Length];
			for (int count = 0 ; count < formats.Length ; count++)
				types[count] = new SubtitleTypeInfo(formats[count]);

			return types;	
		}
	}

	/// <summary>Information about the available subtitle types, sorted by their names.</summary>
	public static SubtitleTypeInfo[] AvailableTypesSorted {
		get {
			SubtitleTypeInfo[] types = AvailableTypes;
			Array.Sort(types);
			return types;	
		}
	}
	
	/// <summary>Get information about an available subtitle type.</summary>
	/// <param name="type">The subtitle type.</param>
	/// <returns>The information about the specified subtitle type.</returns>
	public static SubtitleTypeInfo GetAvailableType (SubtitleType type) {
		SubtitleFormat format = BuiltInSubtitleFormats.GetFormat(type);
		return new SubtitleTypeInfo(format);
	}


	/// <summary>Changes the current frame rate of the subtitles.</summary>
	/// <param name="frameRate">The new frame rate to be used.</param>
	/// <remarks>This changes the frame rate currently being used with the subtitles, which is sometimes
	/// refered to as the output frame rate.</remarks>
	public void ChangeFrameRate (float frameRate) {
		float currentFrameRate = properties.CurrentFrameRate;
		if (currentFrameRate != frameRate) {
			properties.SetCurrentFrameRate(frameRate);
			UpdateFramesFromTimes(frameRate);
		}
	}
	
	/// <summary>Updates the subtitles' frames using the specified frame rate as the one they had when they were opened.</summary>
	/// <param name="frameRate">The original subtitles' frame rate.</param>
	/// <remarks>This results on having the subtitles with the frames they would have if they had been opened with this frame rate.
	/// The original frame rate is sometimes refered to as the input frame rate.</remarks>
	public void ChangeOriginalFrameRate (float frameRate) {
		float originalFrameRate = properties.OriginalFrameRate;
		float currentFrameRate = properties.CurrentFrameRate;
		if (originalFrameRate != frameRate) {
			float conversionFrameRate = currentFrameRate * originalFrameRate / frameRate;
			UpdateFramesFromTimes(conversionFrameRate);
			UpdateTimesFromFrames(currentFrameRate);
			properties.SetOriginalFrameRate(frameRate);
		}
	}
	
	/// <summary>Shifts the subtitles a specified amount of time.</summary>
	/// <param name="time">The time to use, which can be positive or negative.</param>
	public void ShiftTimings (TimeSpan time) {
		foreach (Subtitle subtitle in collection)
			subtitle.Times.Shift(time);
	}
	
	/// <summary>Shifts a range of subtitles a specified amount of time.</summary>
	/// <param name="time">The time to use, which can be positive or negative.</param>
	/// <param name="startIndex">The subtitle index the range begins with.</param>
	/// <param name="endIndex">The subtitle index the range ends with.</param>
	public void ShiftTimings (TimeSpan time, int startIndex, int endIndex) {
		if (!AreShiftTimingsArgsValid(startIndex, endIndex))
			return;

		for (int index = startIndex ; index <= endIndex ; index++) {
			Subtitle subtitle = collection.Get(index);
			subtitle.Times.Shift(time);
		}
	}
	
	/// <summary>Shifts the subtitles a specified amount of frames.</summary>
	/// <param name="frames">The frames to use, which can be positive or negative.</param>
	public void ShiftTimings (int frames) {
		foreach (Subtitle subtitle in collection)
			subtitle.Frames.Shift(frames);
	}
	
	/// <summary>Shifts a range of subtitles a specified amount of frames.</summary>
	/// <param name="frames">The frames to use, which can be positive or negative.</param>
	/// <param name="startIndex">The subtitle index the range begins with.</param>
	/// <param name="endIndex">The subtitle index the range ends with.</param>
	public void ShiftTimings (int frames, int startIndex, int endIndex) {
		if (!AreShiftTimingsArgsValid(startIndex, endIndex))
			return;

		for (int index = startIndex ; index <= endIndex ; index++) {
			Subtitle subtitle = collection.Get(index);
			subtitle.Frames.Shift(frames);
		}
	}

	/// <summary>Auto adjusts the subtitles given the correct times for the first and last subtitle.</summary>
	/// <remarks>The subtitles are first shifted to the first subtitle's correct time, and then proportionally 
	/// adjusted using the last subtitle's correct time.</remarks>
	/// <param name="startTime">The correct start time for the first subtitle.</param>
	/// <param name="endTime">The correct start time for the last subtitle.</param>
	/// <returns>Whether the subtitles could be adjusted.</returns>
	public bool AdjustTimings (TimeSpan startTime, TimeSpan endTime) {
		int startIndex = 0;
		int endIndex = collection.Count - 1;
		return AdjustTimings(startIndex, startTime, endIndex, endTime);
	}
	
	/// <summary>Auto adjusts a range of subtitles given their first and last correct times.</summary>
	/// <remarks>The subtitles are first shifted to the first subtitle's correct time, and then proportionally 
	/// adjusted using the last subtitle's correct time.</remarks>
	/// <param name="startIndex">The subtitle index to start the adjustment with.</param>
	/// <param name="startTime">The correct start time for the first subtitle.</param>
	/// <param name="endIndex">The subtitle index to end the adjustment with.</param>
	/// <param name="endTime">The correct start time for the last subtitle.</param>
	/// <returns>Whether the subtitles could be adjusted.</returns>
	public bool AdjustTimings (int startIndex, TimeSpan startTime, int endIndex, TimeSpan endTime) {
		if (!AreAdjustTimingsArgsValid(startIndex, startTime, endIndex, endTime))
			return false;

		/* Shift subtitles to the start point */
		Subtitle startSubtitle = collection.Get(startIndex);
		TimeSpan shift = startTime - startSubtitle.Times.PreciseStart;
		if (shift != TimeSpan.Zero)
			ShiftTimings(shift, startIndex, endIndex);
		
		/* Auto adjust timings with proportion */
		Subtitle endSubtitle = collection.Get(endIndex);
		double factor = (endTime - startTime).TotalMilliseconds / (endSubtitle.Times.PreciseStart - startTime).TotalMilliseconds;
		for (int index = startIndex ; index <= endIndex ; index++) {
			Subtitle subtitle = collection.Get(index);
			subtitle.Times.Scale(factor, startTime);
		}
		return true;
	}
	
	/// <summary>Auto adjusts the subtitles given the correct frames for the first and last subtitle.</summary>
	/// <remarks>The subtitles are first shifted to the first subtitle's correct frame, and then proportionally 
	/// adjusted using the last subtitle's correct frame.</remarks>
	/// <param name="startFrame">The correct start frame for the first subtitle.</param>
	/// <param name="endFrame">The correct start frame for the last subtitle.</param>
	/// <returns>Whether the subtitles could be adjusted.</returns>
	public bool AdjustTimings (int startFrame, int endFrame) {
		int startIndex = 0;
		int endIndex = collection.Count - 1;
		return AdjustTimings(startIndex, startFrame, endIndex, endFrame);
	}
	
	/// <summary>Auto adjusts a range of subtitles given their first and last correct frames.</summary>
	/// <remarks>The subtitles are first shifted to the first subtitle's correct frame, and then proportionally 
	/// adjusted using the last subtitle's correct frame.</remarks>
	/// <param name="startIndex">The subtitle index to start the adjustment with.</param>
	/// <param name="startFrame">The correct start frame for the first subtitle.</param>
	/// <param name="endIndex">The subtitle index to end the adjustment with.</param>
	/// <param name="endFrame">The correct start frame for the last subtitle.</param>
	/// <returns>Whether the subtitles could be adjusted.</returns>
	public bool AdjustTimings (int startIndex, int startFrame, int endIndex, int endFrame) {
		if (!AreAdjustTimingsArgsValid(startIndex, startFrame, endIndex, endFrame))
			return false;

		/* Shift subtitles to the start point */
		Subtitle startSubtitle = collection.Get(startIndex);
		int shift = (int)(startFrame - startSubtitle.Frames.PreciseStart);
		if (shift != 0)
			ShiftTimings(shift, startIndex, endIndex);
		
		/* Auto adjust timings with proportion */
		Subtitle endSubtitle = collection.Get(endIndex);
		double factor = (endFrame - startFrame) / (endSubtitle.Frames.PreciseStart - startFrame);
		for (int index = startIndex ; index <= endIndex ; index++) {
			Subtitle subtitle = collection.Get(index);
			subtitle.Frames.Scale(factor, startFrame);
		}
		return true;
	}

	/// <summary>Searches for text using the specified search options.</summary>
	/// <param name="options">The search options.</param>
	/// <returns>The search results, or null in case no results were found.</returns>
	public SubtitleSearchResults Find (SubtitleSearchOptions options) {
		System.Console.WriteLine("Finding " + options.Regex.ToString() + " from subtitle " + options.StartSubtitle + ", index " + options.StartIndex + ", type " + options.TextType);
		if (options.Backwards)
			return FindBackward(options);
		else
			return FindForward(options);	
	}
	
	/// <summary>Replaces all occurences of some text with the specified replacement.</summary>
	/// <param name="regex">A regular expression used to find the text to be replaced.</param>
	/// <param name="replacement">The text that will be used as a replacement.</param>
	/// <param name="replacedSubtitles">The numbers of the subtitles that were replaced.</param>
	/// <param name="oldTexts">The texts of the subtitles that were replaced, before replacement was done.</param>
	/// <remarks>The newline (\n) char is used as the line break.</remarks>
	/// <returns>The number of replaced subtitles.</returns>
	public int ReplaceAll (Regex regex, string replacement, out int[] replacedSubtitles, out string[] oldTexts) {
		return ReplaceAll(regex, replacement, "\n", out replacedSubtitles, out oldTexts);
	}

	/// <summary>Replaces all occurences of some text with the specified replacement.</summary>
	/// <param name="regex">A regular expression used to find the text to be replaced.</param>
	/// <param name="replacement">The text that will be used as a replacement.</param>
	/// <param name="lineBreak">The line break to use between multiple lines of text in each subtitle.</param>
	/// <param name="replacedSubtitles">The numbers of the subtitles that were replaced.</param>
	/// <param name="oldTexts">The texts of the subtitles that were replaced, before replacement was done.</param>
	/// <remarks>The newline (\n) char is used as the line break.</remarks>
	/// <returns>The number of replaced subtitles.</returns>
	public int ReplaceAll (Regex regex, string replacement, string lineBreak, out int[] replacedSubtitles, out string[] oldTexts) {
		ArrayList subtitles = new ArrayList();
		ArrayList texts = new ArrayList();
		MatchEvaluationCounter counter = new MatchEvaluationCounter(replacement);
		int subtitleNumber = 0;
		foreach (Subtitle subtitle in collection) {
			string oldText = subtitle.Text.Get(lineBreak);
			counter.EvaluationOccured = false;
			string newText = regex.Replace(oldText, counter.Evaluator);
			if (counter.EvaluationOccured) {
				subtitles.Add(subtitleNumber);
				texts.Add(oldText);
				subtitle.Text.Set(newText, lineBreak, false);				
				counter.EvaluationOccured = false;			
			}
			subtitleNumber++;
		}

		replacedSubtitles = (int[])subtitles.ToArray(typeof(int));
		oldTexts = (string[])texts.ToArray(typeof(string));
		return counter.Count;	
	}
	
	/// <summary>Finds the subtitle that contains the specified time position.</summary>
	/// <param name="time">The time position, in seconds.</param>
	/// <returns>The found subtitle number, or -1 if no subtitle was found.</returns>
	public int FindWithTime (float time) {
		if (collection.Count == 0)
			return -1;
		
		for (int subtitleNumber = 0 ; subtitleNumber < collection.Count ; subtitleNumber++) {
			Subtitle subtitle = collection[subtitleNumber];
			double start = subtitle.Times.Start.TotalSeconds;
			if (time < start)
				continue;
			
			double end = subtitle.Times.End.TotalSeconds;
			if (time <= end)
				return subtitleNumber;
		}
		return -1; // No subtitles were found 
	}
	
	public override string ToString(){
		return Collection.ToString() + "\n-------------------------------------------\n" + Properties.ToString();
	}
	
	/* Internal members */

	/// <summary>Initializes a new instance of the <see cref="Subtitles" /> class.</summary>
	/// <param name="collection">A collection of subtitles.</param>
	/// <param name="properties">The subtitles' properties.</param>
	internal protected Subtitles (SubtitleCollection collection, SubtitleProperties properties) {
		this.collection = collection;
		this.properties = properties;
	}
	
	internal void UpdateFramesFromTimes (float frameRate) {
		foreach (Subtitle subtitle in collection) {
			subtitle.UpdateFramesFromTimes(frameRate);
		}
	}
	
	internal void UpdateTimesFromFrames (float frameRate) {
		foreach (Subtitle subtitle in collection) {
			subtitle.UpdateTimesFromFrames(frameRate);
		}
	}

	
	/* Private members */
	
	private bool AreAdjustTimingsArgsValid (int startIndex, TimeSpan startTime, int endIndex, TimeSpan endTime) {
		if (!AreAdjustTimingsIndexesValid(startIndex, endIndex))
			return false;
		else if (!(startTime < endTime))
			return false;
		else
			return true;
	}
	
	private bool AreAdjustTimingsArgsValid (int startIndex, int startTime, int endIndex, int endTime) {
		if (!AreAdjustTimingsIndexesValid(startIndex, endIndex))
			return false;
		else if (!(startTime < endTime))
			return false;
		else
			return true;
	}
	
	private bool AreAdjustTimingsIndexesValid (int startIndex, int endIndex) {
		int subtitleCount = collection.Count;
		if (subtitleCount < 2)
			return false;
		else if (!(startIndex < endIndex))
			return false;
		else if ((startIndex < 0) || (startIndex >= (subtitleCount - 1)))
			return false;
		else if (endIndex >= subtitleCount)
			return false;
		else
			return true;
	}
	
	private bool AreShiftTimingsArgsValid (int startIndex, int endIndex) {
		int subtitleCount = collection.Count;
		if (subtitleCount == 0)
			return false;
		else if (!(startIndex <= endIndex))
			return false;
		else if ((startIndex < 0) || (startIndex >= subtitleCount))
			return false;
		else if (endIndex >= subtitleCount)
			return false;
		else
			return true;
	}
	
	/// <summary>Searches forward for text using the specified search options.</summary>
	/// <param name="options">The search options.</param>
	/// <returns>The search results, or null in case no results were found.</returns>
	private SubtitleSearchResults FindForward (SubtitleSearchOptions options) {
		if (collection.Count == 0)
			return null;
		
		/* Search the startSubtitle subtitle starting at the startIndex */
		SubtitleSearchResults results = FindInSubtitleFromIndex(options.StartSubtitle, options.LineBreak, options.Regex, options.StartIndex, options.TextType, options.Backwards);
		if (results != null)
			return results;

		/* Iterate through the rest of the collection */
		for (int subtitleNumber = options.StartSubtitle + 1 ; subtitleNumber < collection.Count ; subtitleNumber++) {
			results = FindInSubtitle(subtitleNumber, options.LineBreak, options.Regex, options.Backwards);
			if (results != null)
				return results;
		}
		
		if (options.Wrap) {
			/* Iterate from the beginning back to the subtitle */
			for (int subtitleNumber = 0 ; subtitleNumber < options.StartSubtitle ; subtitleNumber++) {
				results = FindInSubtitle(subtitleNumber, options.LineBreak, options.Regex, options.Backwards);
				if (results != null)
					return results;
			}
			/* Search the startSubtitle ending at the startIndex */
			results = FindInSubtitleTillIndex(options.StartSubtitle, options.LineBreak, options.Regex, options.StartIndex, options.TextType, options.Backwards);
			if (results != null)
				return results;
		}
		
		/* Text not found */
		return null;
	}
	
	/// <summary>Searches backward for text using the specified search options.</summary>
	/// <param name="options">The search options.</param>
	/// <returns>The search results, or null in case no results were found.</returns>
	private SubtitleSearchResults FindBackward (SubtitleSearchOptions options) {
		if (collection.Count == 0)
			return null;
		
		/* Search the subtitle starting at the startIndex */
		SubtitleSearchResults results = FindInSubtitleFromIndex(options.StartSubtitle, options.LineBreak, options.Regex, options.StartIndex, options.TextType, options.Backwards);
		if (results != null)
			return results;
		
		/* Iterate through the start of the collection */
		for (int subtitleNumber = options.StartSubtitle - 1 ; subtitleNumber > 0 ; subtitleNumber--) {
			results = FindInSubtitle(subtitleNumber, options.LineBreak, options.Regex, options.Backwards);
			if (results != null)
				return results;
		}
		
		if (options.Wrap) {
			/* Iterate from the end back to the subtitle */
			for (int subtitleNumber = collection.Count - 1 ; subtitleNumber > options.StartSubtitle ; subtitleNumber--) {
				results = FindInSubtitle(subtitleNumber, options.LineBreak, options.Regex, options.Backwards);
				if (results != null)
					return results;
			}
			/* Search the subtitle ending at the startIndex */
			results = FindInSubtitleTillIndex(options.StartSubtitle, options.LineBreak, options.Regex, options.StartIndex, options.TextType, options.Backwards);
			if (results != null)
				return results;
		}
		
		/* Text not found */
		return null;
	}
	
	/// <returns>The <see cref="SubtitleSearchResults" />, or null if the text was not found.</returns>
	private SubtitleSearchResults FindInSubtitle (int subtitleNumber, string lineBreak, Regex regex, bool backwards) {
		if (backwards) {
			/* Find first in the translation */
			SubtitleSearchResults results = FindInTextContent(subtitleNumber, lineBreak, regex, SubtitleTextType.Translation);
			if (results != null)
				return results;
			
			/* Not found in the translation, finding in the text */
			return FindInTextContent(subtitleNumber, lineBreak, regex, SubtitleTextType.Text);
		}
		else {
			/* Find first in the text */
			SubtitleSearchResults results = FindInTextContent(subtitleNumber, lineBreak, regex, SubtitleTextType.Text);
			if (results != null)
				return results;
			
			/* Not found in the text, finding in the translation */
			return FindInTextContent(subtitleNumber, lineBreak, regex, SubtitleTextType.Translation);
		}
	}
	
	/// <returns>The <see cref="SubtitleSearchResults" />, or null if the text was not found.</returns>
	private SubtitleSearchResults FindInSubtitleFromIndex (int subtitleNumber, string lineBreak, Regex regex, int startIndex, SubtitleTextType textType, bool backwards) {
		if (backwards) {
			if (textType == SubtitleTextType.Text) {
				/* Find in the text starting at the specified index */
				return FindInTextContentFromIndex(subtitleNumber, lineBreak, regex, startIndex, SubtitleTextType.Text);
			}
			else {
				/* Find first in the translation starting at the specified index */
				SubtitleSearchResults results = FindInTextContentFromIndex(subtitleNumber, lineBreak, regex, startIndex, SubtitleTextType.Translation);
				if (results != null)
					return results;
				
				/* Not found in the translation, finding in the text */
				return FindInTextContent(subtitleNumber, lineBreak, regex, SubtitleTextType.Text);
			}
		}
		else {
			if (textType == SubtitleTextType.Text) {
				/* Find first in the text starting at the specified index */
				SubtitleSearchResults results = FindInTextContentFromIndex(subtitleNumber, lineBreak, regex, startIndex, SubtitleTextType.Text);
				if (results != null)
					return results;
				
				/* Not found in the text, finding in the translation */
				return FindInTextContent(subtitleNumber, lineBreak, regex, SubtitleTextType.Translation);
			}
			else {
				/* Find in the translation starting at the specified index */
				return FindInTextContentFromIndex(subtitleNumber, lineBreak, regex, startIndex, SubtitleTextType.Translation);
			}
		}
	}
		
	/// <returns>The <see cref="SubtitleSearchResults" />, or null if the text was not found.</returns>
	private SubtitleSearchResults FindInSubtitleTillIndex (int subtitleNumber, string lineBreak, Regex regex, int endIndex, SubtitleTextType textType, bool backwards) {
		if (backwards) {
			if (textType == SubtitleTextType.Text) {
				/* Find first in the translation */
				SubtitleSearchResults results = FindInTextContent(subtitleNumber, lineBreak, regex, SubtitleTextType.Translation);
				if (results != null)
					return results;
				
				/* Not found in the text, finding in the text till the specified index */
				return FindInTextContentTillIndex(subtitleNumber, lineBreak, regex, endIndex, SubtitleTextType.Text, backwards);
			}
			else {
				/* Find in the translation till specified index */
				return FindInTextContentTillIndex(subtitleNumber, lineBreak, regex, endIndex, SubtitleTextType.Translation, backwards);
			}
		}
		else {
			if (textType == SubtitleTextType.Text) {
				/* Find in the text ending at the specified index */
				return FindInTextContentTillIndex(subtitleNumber, lineBreak, regex, endIndex, SubtitleTextType.Text, backwards);
			}
			else {
				/* Find first in the text */
				SubtitleSearchResults results = FindInTextContent(subtitleNumber, lineBreak, regex, SubtitleTextType.Text);
				if (results != null)
					return results;
				
				/* Not found in the text, finding in the translation till the specified index */
				return FindInTextContentTillIndex(subtitleNumber, lineBreak, regex, endIndex, SubtitleTextType.Translation, backwards);
			}
		}
	}
	
	private SubtitleSearchResults FindInTextContent (int subtitleNumber, string lineBreak, Regex regex, SubtitleTextType textType) {
		SubtitleText text = GetSubtitleText(subtitleNumber, textType);
		return MatchValues(regex.Match(text.Get(lineBreak)), subtitleNumber, textType);
	}
	
	private SubtitleSearchResults FindInTextContentFromIndex (int subtitleNumber, string lineBreak, Regex regex, int startIndex, SubtitleTextType textType) {
		SubtitleText text = GetSubtitleText(subtitleNumber, textType);
		return MatchValues(regex.Match(text.Get(lineBreak), startIndex), subtitleNumber, textType);
	}
	
	private SubtitleSearchResults FindInTextContentTillIndex (int subtitleNumber, string lineBreak, Regex regex, int endIndex, SubtitleTextType textType, bool backwards) {
		SubtitleText text = GetSubtitleText(subtitleNumber, textType);
		string matchText = text.Get(lineBreak);
		int startIndex = (backwards ? matchText.Length : 0);
		int length = (backwards ? matchText.Length - endIndex : endIndex);		
		return MatchValues(regex.Match(text.Get(lineBreak), startIndex, length), subtitleNumber, textType);
	}
	
	private SubtitleSearchResults MatchValues (Match match, int subtitleNumber, SubtitleTextType textType) {
		if (match.Success)
			return new SubtitleSearchResults(subtitleNumber, textType, match.Index, match.Length);
		else
			return null;
	}
	
	private SubtitleText GetSubtitleText (int subtitleNumber, SubtitleTextType textType) {
		Subtitle subtitle = collection[subtitleNumber];
		if (textType == SubtitleTextType.Text)
			return subtitle.Text;
		else
			return subtitle.Translation;	
	}
	

}

}

