/**
 * Class that represents the .ASS and .SSA subtitle file format
 *
 * <br><br>
 * Copyright (c) 2012 J. David Requejo <br>
 * j[dot]david[dot]requejo[at] Gmail
 * <br><br>
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
 * and associated documentation files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute,
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
 * is furnished to do so, subject to the following conditions:
 * <br><br>
 * The above copyright notice and this permission notice shall be included in all copies
 * or substantial portions of the Software.
 * <br><br>
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
 * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 * @author J. David REQUEJO
 *
 */

package com.github.tvbox.osc.subtitle.format;


import com.github.tvbox.osc.subtitle.model.Style;
import com.github.tvbox.osc.subtitle.model.Subtitle;
import com.github.tvbox.osc.subtitle.model.Time;
import com.github.tvbox.osc.subtitle.model.TimedTextObject;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Iterator;

public class FormatASS implements TimedTextFileFormat {

	public TimedTextObject parseFile(String fileName, InputStream is) throws IOException {

		TimedTextObject tto = new TimedTextObject();
		tto.fileName = fileName;

		Subtitle caption = new Subtitle();
		Style style;

		//for the clock timer
		float timer = 100;

		//if the file is .SSA or .ASS
		boolean isASS = false;

		//variables to store the formats
		String[] styleFormat;
		String[] dialogueFormat;

		//first lets load the file
		InputStreamReader in= new InputStreamReader(is);
		BufferedReader br = new BufferedReader(in);

		String line;
		int lineCounter = 0;
		try {
			//we scour the file
			line=br.readLine();
			lineCounter++;
			while (line!=null){
				line = line.trim();
				//we skip any line until we find a section [section name]
				if(line.startsWith("[")){
					//now we must identify the section
					if(line.equalsIgnoreCase("[Script info]")){
						//its the script info section section
						lineCounter++;
						line=br.readLine().trim();
						//Each line is scanned for useful info until a new section is detected
						while (!line.startsWith("[")){
							if(line.startsWith("Title:")) { //标题信息非必要
								String[] titleArr = line.split(":");
								//We have found the title
								tto.title = titleArr.length > 1 ? titleArr[1].trim() : "";
							} else if (line.startsWith("Original Script:")) { //作者信息非必要
								String[] authorArr = line.split(":");
								//We have found the author
								tto.author = authorArr.length > 1 ? authorArr[1].trim() : "";
							} else if (line.startsWith("Script Type:")){
								//we have found the version
								if(line.split(":")[1].trim().equalsIgnoreCase("v4.00+"))isASS = true;
									//we check the type to set isASS or to warn if it comes from an older version than the studied specs
								else if(!line.split(":")[1].trim().equalsIgnoreCase("v4.00"))
									tto.warnings+="Script version is older than 4.00, it may produce parsing errors.";
							} else if (line.startsWith("Timer:"))
								//We have found the timer
								timer = Float.parseFloat(line.split(":")[1].trim().replace(',','.'));
							//we go to the next line
							lineCounter++;
							line=br.readLine().trim();
						}

					} else if (line.equalsIgnoreCase("[v4 Styles]")
							|| line.equalsIgnoreCase("[v4 Styles+]")
							|| line.equalsIgnoreCase("[v4+ Styles]")){
						//its the Styles description section
						if(line.contains("+")&&isASS==false){
							//its ASS and it had not been noted
							isASS=true;
							tto.warnings+="ScriptType should be set to v4:00+ in the [Script Info] section.\n\n";
						}
						lineCounter++;
						line=br.readLine();
						//the first line should define the format
						if(!line.startsWith("Format:")){
							//if not, we scan for the format.
							tto.warnings+="Format: (format definition) expected at line "+line+" for the styles section\n\n";
							while (!line.startsWith("Format:")){
								lineCounter++;
								line=br.readLine();
							}
						}
						// we recover the format's fields
						styleFormat = line.split(":")[1].trim().split(",");
						lineCounter++;
						line=br.readLine();
						// we parse each style until we reach a new section
						while (!line.startsWith("Style:")){
							tto.warnings+="Style: (format definition) expected at line "+line+" for the styles section\n\n";
							//next line
							lineCounter++;
							line=br.readLine();
						}
						//we parse the style
						style = parseStyleForASS(line.split(":")[1].trim().split(","),styleFormat,lineCounter,isASS,tto.warnings);
						//and save the style
						tto.styling.put(style.iD, style);

					} else if (line.trim().equalsIgnoreCase("[Events]")){
						//its the events specification section
						lineCounter++;
						line=br.readLine();
						tto.warnings+="Only dialogue events are considered, all other events are ignored.\n\n";
						//the first line should define the format of the dialogues
						if(!line.startsWith("Format:")){
							//if not, we scan for the format.
							tto.warnings+="Format: (format definition) expected at line "+line+" for the events section\n\n";
							while (!line.startsWith("Format:")){
								lineCounter++;
								line=br.readLine();
							}
						}
						// we recover the format's fields
						dialogueFormat = line.split(":")[1].trim().split(",");
						//next line
						lineCounter++;
						line=br.readLine();
						// we parse each style until we reach a new section
						while (!line.startsWith("[")){
							//we check it is a dialogue
							//WARNING: all other events are ignored.
							if (line.startsWith("Dialogue:")){
								//we parse the dialogue
								caption = parseDialogueForASS(line.split(":",2)[1].trim().split(",",10),dialogueFormat,timer, tto);
								//and save the caption
								int key = caption.start.mseconds;
								//in case the key is already there, we increase it by a millisecond, since no duplicates are allowed
								while (tto.captions.containsKey(key)) key++;
								tto.captions.put(key, caption);
							}
							//next line
							lineCounter++;
							line=br.readLine();
						}

					} else if (line.trim().equalsIgnoreCase("[Fonts]") || line.trim().equalsIgnoreCase("[Graphics]")){
						//its the custom fonts or embedded graphics section
						//these are not supported
						tto.warnings+= "The section "+line.trim()+" is not supported for conversion, all information there will be lost.\n\n";
					} else {
						tto.warnings+= "Unrecognized section: "+line.trim()+" all information there is ignored.";
					}
				}
				line = br.readLine();
				lineCounter++;
			}
			// parsed styles that are not used should be eliminated
			tto.cleanUnusedStyles();

		}  catch (NullPointerException e){
			tto.warnings+= "unexpected end of file, maybe last caption is not complete.\n\n";
		} finally{
			//we close the reader
			is.close();
		}

		tto.built = true;
		return tto;
	}


	public String[] toFile(TimedTextObject tto) {

		//first we check if the TimedTextObject had been built, otherwise...
		if(!tto.built)
			return null;

		//we will write the lines in an ArrayList 
		int index = 0;
		//the minimum size of the file is the number of captions and styles + lines for sections and formats and the script info, so we'll take some extra space.
		ArrayList<String> file = new ArrayList<String>(30+tto.styling.size()+tto.captions.size());

		//header is placed
		file.add(index++,"[Script Info]");
		//title next
		String title = "Title: ";
		if (tto.title == null || tto.title.isEmpty())
			title += tto.fileName;
		else title += tto.title;
		file.add(index++,title);
		//author next
		String author = "Original Script: ";
		if (tto.author == null || tto.author.isEmpty())
			author += "Unknown";
		else author += tto.author;
		file.add(index++,author);
		//additional info
		if (tto.copyrigth != null && !tto.copyrigth.isEmpty())
			file.add(index++,"; "+tto.copyrigth);
		if (tto.description != null && !tto.description.isEmpty())
			file.add(index++,"; "+tto.description);
		file.add(index++,"; Converted by the Online Subtitle Converter developed by J. David Requejo");
		//mandatory info
		if (tto.useASSInsteadOfSSA)
			file.add(index++,"Script Type: V4.00+");
		else file.add(index++,"Script Type: V4.00");
		file.add(index++,"Collisions: Normal");
		file.add(index++,"Timer: 100,0000");
		if (tto.useASSInsteadOfSSA)
			file.add(index++,"WrapStyle: 1");
		//an empty line is added
		file.add(index++,"");

		//Styles section
		if (tto.useASSInsteadOfSSA)
			file.add(index++,"[V4+ Styles]");
		else file.add(index++,"[V4 Styles]");
		//define the format
		if (tto.useASSInsteadOfSSA)
			file.add(index++,"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding");
		else file.add(index++,"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding");
		//Next we iterate over the styles
		Iterator<Style> itrS = tto.styling.values().iterator();
		while(itrS.hasNext()){
			String styleLine = "Style: ";
			//new style
			Style current = itrS.next();
			//name
			styleLine+= current.iD+",";
			styleLine+= current.font+",";
			styleLine+= current.fontSize+",";
			styleLine+= getColorsForASS(tto.useASSInsteadOfSSA, current);
			styleLine+= getOptionsForASS(tto.useASSInsteadOfSSA, current);
			//BorderStyle, Outline, Shadow
			styleLine+= "1,2,2,";
			styleLine+= getAlignForASS(tto.useASSInsteadOfSSA, current.textAlign);
			//MarginL, MarginR, MarginV
			styleLine+= ",0,0,0,";
			//AlphaLevel
			if(!tto.useASSInsteadOfSSA)styleLine+= "0,";
			//Encoding
			styleLine+= "0";

			//and we add the style definition line
			file.add(index++,styleLine);
		}
		//an empty line is added
		file.add(index++,"");

		//Events section
		file.add(index++,"[Events]");
		//define the format
		if (tto.useASSInsteadOfSSA)
			file.add(index++,"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text");
		else file.add(index++,"Format: Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text");
		//Next we iterate over the captions
		Iterator<Subtitle> itrC = tto.captions.values().iterator();
		while(itrC.hasNext()){
			//for each caption
			String line = "Dialogue: 0,";
			//new caption
			Subtitle current = itrC.next();
			//offset is applied
			if(tto.offset != 0){
				current.start.mseconds += tto.offset;
				current.end.mseconds += tto.offset;
			}
			//start time
			line+= current.start.getTime("h:mm:ss.cs")+",";
			//end time
			line+= current.end.getTime("h:mm:ss.cs")+",";
			//offset is undone
			if(tto.offset != 0){
				current.start.mseconds -= tto.offset;
				current.end.mseconds -= tto.offset;
			}
			//style
			if (current.style != null)
				line+= current.style.iD;
			else
				line+="Default";
			//default margins are used, no name or effect is recognized
			line+=",,0000,0000,0000,,";

			//we add the caption text with \N as line breaks  and clean of XML
			line +=  current.content.replaceAll("<br />","�N").replaceAll("\\<.*?\\>", "").replace('�', '\\');
			//and we add the caption line
			file.add(index++,line);
		}
		//an empty line is added
		file.add(index++,"");

		//we return the expected file as an array of String
		String[] toReturn = new String[file.size()];
		for (int i = 0; i < toReturn.length; i++) {
			toReturn[i] = file.get(i);
		}
		return toReturn;
	}

	/* PRIVATEMETHODS */

	/**
	 * This methods transforms a format line from ASS according to a format definition into an Style object.
	 *
	 * @param line the format line without its declaration
	 * @param styleFormat the list of attributes in this format line
	 * @return a new Style object.
	 */
	private Style parseStyleForASS(String[] line, String[] styleFormat, int index, boolean isASS, String warnings) {

		Style newStyle = new Style(Style.defaultID());
		if (line.length != styleFormat.length){
			//both should have the same size
			warnings+="incorrectly formated line at "+index+"\n\n";
		} else {
			for (int i = 0; i < styleFormat.length; i++) {
				//we go through every format parameter and save the interesting values
				if (styleFormat[i].trim().equalsIgnoreCase("Name")){
					//we save the name
					newStyle.iD=line[i].trim();
				} else if (styleFormat[i].trim().equalsIgnoreCase("Fontname")){
					//we save the font
					newStyle.font=line[i].trim();
				} else if (styleFormat[i].trim().equalsIgnoreCase("Fontsize")){
					//we save the size
					newStyle.fontSize=line[i].trim();
				}else if (styleFormat[i].trim().equalsIgnoreCase("PrimaryColour")){
					//we save the color
					String color =line[i].trim();
					if(isASS){
						if(color.startsWith("&H")) newStyle.color=Style.getRGBValue("&HAABBGGRR", color);
						else  newStyle.color=Style.getRGBValue("decimalCodedAABBGGRR", color);
					} else {
						if(color.startsWith("&H")) newStyle.color=Style.getRGBValue("&HBBGGRR", color);
						else  newStyle.color=Style.getRGBValue("decimalCodedBBGGRR", color);
					}
				}else if (styleFormat[i].trim().equalsIgnoreCase("BackColour")){
					//we save the background color
					String color =line[i].trim();
					if(isASS){
						if(color.startsWith("&H")) newStyle.backgroundColor=Style.getRGBValue("&HAABBGGRR", color);
						else  newStyle.backgroundColor=Style.getRGBValue("decimalCodedAABBGGRR", color);
					} else {
						if(color.startsWith("&H")) newStyle.backgroundColor=Style.getRGBValue("&HBBGGRR", color);
						else  newStyle.backgroundColor=Style.getRGBValue("decimalCodedBBGGRR", color);
					}
				}else if (styleFormat[i].trim().equalsIgnoreCase("Bold")){
					//we save if bold
					newStyle.bold= Boolean.parseBoolean(line[i].trim());
				}else if (styleFormat[i].trim().equalsIgnoreCase("Italic")){
					//we save if italic
					newStyle.italic= Boolean.parseBoolean(line[i].trim());
				}else if (styleFormat[i].trim().equalsIgnoreCase("Underline")){
					//we save if underlined
					newStyle.underline= Boolean.parseBoolean(line[i].trim());
				}else if (styleFormat[i].trim().equalsIgnoreCase("Alignment")){
					//we save the alignment
					int placement = Integer.parseInt(line[i].trim());
					if (isASS){
						switch(placement){
							case 1:
								newStyle.textAlign="bottom-left";
								break;
							case 2:
								newStyle.textAlign="bottom-center";
								break;
							case 3:
								newStyle.textAlign="bottom-right";
								break;
							case 4:
								newStyle.textAlign="mid-left";
								break;
							case 5:
								newStyle.textAlign="mid-center";
								break;
							case 6:
								newStyle.textAlign="mid-right";
								break;
							case 7:
								newStyle.textAlign="top-left";
								break;
							case 8:
								newStyle.textAlign="top-center";
								break;
							case 9:
								newStyle.textAlign="top-right";
								break;
							default:
								warnings+="undefined alignment for style at line "+index+"\n\n";
						}
					} else {
						switch(placement){
							case 9:
								newStyle.textAlign="bottom-left";
								break;
							case 10:
								newStyle.textAlign="bottom-center";
								break;
							case 11:
								newStyle.textAlign="bottom-right";
								break;
							case 1:
								newStyle.textAlign="mid-left";
								break;
							case 2:
								newStyle.textAlign="mid-center";
								break;
							case 3:
								newStyle.textAlign="mid-right";
								break;
							case 5:
								newStyle.textAlign="top-left";
								break;
							case 6:
								newStyle.textAlign="top-center";
								break;
							case 7:
								newStyle.textAlign="top-right";
								break;
							default:
								warnings+="undefined alignment for style at line "+index+"\n\n";
						}
					}
				}

			}
		}

		return newStyle;
	}

	/**
	 * This methods transforms a dialogue line from ASS according to a format definition into an Caption object.
	 *
	 * @param line the dialogue line without its declaration
	 * @param dialogueFormat the list of attributes in this dialogue line
	 * @param timer % to speed or slow the clock, above 100% span of the subtitles is reduced.
	 * @return a new Caption object
	 */
	private Subtitle parseDialogueForASS(String[] line, String[] dialogueFormat, float timer, TimedTextObject tto) {

		Subtitle newCaption = new Subtitle();

		//all information from fields 10 onwards are the caption text therefore needn't be split
		String captionText = line[9];
		//text is cleaned before being inserted into the caption
		newCaption.content = captionText.replaceAll("\\{.*?\\}", "").replace("\n", "<br />").replace("\\N", "<br />");

		for (int i = 0; i < dialogueFormat.length; i++) {
			//we go through every format parameter and save the interesting values
			if (dialogueFormat[i].trim().equalsIgnoreCase("Style")){
				//we save the style
				Style s =  tto.styling.get(line[i].trim());
				if (s!=null)
					newCaption.style= s;
				else
					tto.warnings+="undefined style: "+line[i].trim()+"\n\n";
			} else if (dialogueFormat[i].trim().equalsIgnoreCase("Start")){
				//we save the starting time
				newCaption.start=new Time("h:mm:ss.cs",line[i].trim());
			} else if (dialogueFormat[i].trim().equalsIgnoreCase("End")){
				//we save the starting time
				newCaption.end=new Time("h:mm:ss.cs",line[i].trim());
			}
		}

		//timer is applied
		if (timer != 100){
			newCaption.start.mseconds /= (timer/100);
			newCaption.end.mseconds /= (timer/100);
		}
		return newCaption;
	}

	/**
	 * returns a string with the correctly formated colors
	 * @param useASSInsteadOfSSA true if formated for ASS
	 * @return the colors in the decimal format
	 */
	private String getColorsForASS(boolean useASSInsteadOfSSA, Style style) {
		String colors;
		if(useASSInsteadOfSSA)
			//primary color(BBGGRR) with Alpha level (00) in front + 00FFFFFF + 00000000 + background color(BBGGRR) with Alpha level (80) in front
			colors= Integer.parseInt("00"+ style.color.substring(4,6)+style.color.substring(2, 4)+style.color.substring(0, 2), 16)+",16777215,0,"+ Long.parseLong("80"+ style.backgroundColor.substring(4,6)+style.backgroundColor.substring(2, 4)+style.backgroundColor.substring(0, 2), 16)+",";
		else {
			//primary color(BBGGRR) + FFFFFF + 000000 + background color(BBGGRR)
			String color = style.color.substring(4,6)+style.color.substring(2, 4)+style.color.substring(0, 2);
			String bgcolor = style.backgroundColor.substring(4,6)+style.backgroundColor.substring(2, 4)+style.backgroundColor.substring(0, 2);
			colors= Long.parseLong(color, 16)+",16777215,0,"+ Long.parseLong(bgcolor, 16)+",";
		}
		return colors;
	}

	/**
	 * returns a string with the correctly formated options
	 * @param useASSInsteadOfSSA
	 * @return
	 */
	private String getOptionsForASS(boolean useASSInsteadOfSSA, Style style) {
		String options;
		if (style.bold)
			options="-1,";
		else
			options="0,";
		if (style.italic)
			options+="-1,";
		else
			options+="0,";
		if(useASSInsteadOfSSA){
			if (style.underline)
				options+="-1,";
			else
				options+="0,";
			options+="0,100,100,0,0,";
		}
		return options;
	}

	/**
	 * converts the string explaining the alignment into the ASS equivalent integer offering bottom-center as default value
	 * @param useASSInsteadOfSSA
	 * @param align
	 * @return
	 */
	private int getAlignForASS(boolean useASSInsteadOfSSA, String align) {
		if (useASSInsteadOfSSA){
			int placement = 2;
			if ("bottom-left".equals(align))
				placement = 1;
			else if ("bottom-center".equals(align))
				placement = 2;
			else if ("bottom-right".equals(align))
				placement = 3;
			else if ("mid-left".equals(align))
				placement = 4;
			else if ("mid-center".equals(align))
				placement = 5;
			else if ("mid-right".equals(align))
				placement = 6;
			else if ("top-left".equals(align))
				placement = 7;
			else if ("top-center".equals(align))
				placement = 8;
			else if ("top-right".equals(align))
				placement = 9;

			return placement;
		} else {

			int placement = 10;
			if ("bottom-left".equals(align))
				placement = 9;
			else if ("bottom-center".equals(align))
				placement = 10;
			else if ("bottom-right".equals(align))
				placement = 11;
			else if ("mid-left".equals(align))
				placement = 1;
			else if ("mid-center".equals(align))
				placement = 2;
			else if ("mid-right".equals(align))
				placement = 3;
			else if ("top-left".equals(align))
				placement = 5;
			else if ("top-center".equals(align))
				placement = 6;
			else if ("top-right".equals(align))
				placement = 7;

			return placement;
		}
	}

}