package com.langintro.celtx_to_odt;

import org.lobobrowser.html.parser.DocumentBuilderImpl;
import org.lobobrowser.html.parser.InputSourceImpl;
import org.lobobrowser.html.test.SimpleUserAgentContext;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
 
import org.openoffice.odf.doc.OdfDocument;
import org.openoffice.odf.doc.OdfFileDom;

import org.openoffice.odf.doc.element.office.OdfAutomaticStyles;
import org.openoffice.odf.doc.element.office.OdfMasterStyles;

import org.openoffice.odf.doc.element.style.OdfHeader;
import org.openoffice.odf.doc.element.style.OdfMasterPage;
import org.openoffice.odf.doc.element.style.OdfStyle;

import org.openoffice.odf.doc.element.table.OdfTableCell;
import org.openoffice.odf.doc.element.table.OdfTableExtra;
import org.openoffice.odf.doc.element.table.OdfTableHeaderRows;
import org.openoffice.odf.doc.element.table.OdfTableRowExtra;

import org.openoffice.odf.doc.element.text.OdfParagraphExtra;
import org.openoffice.odf.doc.element.text.OdfSpan;

import org.openoffice.odf.dom.element.OdfStylableElement;
import org.openoffice.odf.dom.OdfNamespace;
import org.openoffice.odf.dom.element.style.OdfStyleElement;
import org.openoffice.odf.dom.style.OdfStyleFamily;
import org.openoffice.odf.dom.style.props.OdfStyleProperty;
import org.openoffice.odf.dom.type.office.OdfValueType;

import org.openoffice.odf.pkg.OdfPackage;

import java.io.FileInputStream;
import java.io.InputStream;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPathConstants;

public class CeltxConverter
{
	static String COMIC_TYPE = "comic";
	static String SCREENPLAY_TYPE = "script";
	static String THEATRE_TYPE = "theatre";

	javax.swing.JFrame app;
	
	/*
		rdfDocument and htmlDocument are part of the .celtx zip file.
		outputDocument is the ODT file
	*/
	Document rdfDocument;
	Document htmlDocument;
	OdfDocument outputDocument;

	boolean outputCastOfCharacters = false;
	
	/*
		Each document needs its own XPath, because
		they all have different namespace requirements
	*/
	XPath rdfXPath;
	XPath htmlXPath;
	XPath odfXPath;

	/* Variables for information needed from the RDF file */
	static String RDF_FILE_NAME = "project.rdf";
	String htmlFileName;
	CeltxNamespaceContext rdfNamespaceContext;

	/* Variables for the HTML file */
	XPathExpression classExpression;
	XPathExpression styleExpression;

	/* Variables for the ODF file */
	OdfPackage pkg;
	static String TEMPLATE_NAME = "/resources/template.odt";
	Element officeText;		// <office:text> in content.xml
    OdfAutomaticStyles contentAutoStyles;
	OdfMasterStyles masterStyles;
    OdfAutomaticStyles styleAutoStyles;
	OdfFileDom outputDom;	// the content Document Object Model
	OdfFileDom stylesDom;	// style Document Object Model
	String outputType = SCREENPLAY_TYPE;
	
	Properties translation;
	int sceneNumber = 0;
	int shotNumber = 0;

	/* Variables for comic book output */
	OdfTableExtra comicTable = null;
	OdfTableRowExtra comicRow = null;
	OdfTableCell comicPanelCell = null;
	OdfTableCell comicDescriptionCell = null;
	OdfTableCell comicDialogCell = null;

	/* Variables for screenplay output */
	boolean firstParagraph = true;

	OdfMasterPage previousMasterPage;

	public CeltxConverter(javax.swing.JFrame app)
	{
		this.app = app;
	}

	void parseRdf(InputStream inStream) throws Exception
	{
		String uri;
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		dbf.setNamespaceAware(true);
		DocumentBuilder builder = dbf.newDocumentBuilder();
		rdfDocument = builder.parse(inStream);
		rdfNamespaceContext = new CeltxNamespaceContext();
		uri = rdfDocument.getDocumentElement().getAttributeNS(
			XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "NS1");
		rdfNamespaceContext.setNs1NamespaceUri(uri);
	}
	
	void parseScriptFile(InputStream inStream, String fileName) throws Exception
	{
		SimpleUserAgentContext context = new SimpleUserAgentContext();
		context.setExternalCSSEnabled(false);
		DocumentBuilderImpl builder = new DocumentBuilderImpl(context);
		htmlDocument = builder.parse(new InputSourceImpl(inStream,
			fileName,"utf-8"));
	}
	
	void processHtmlFile( ) throws Exception
	{
		InputStream in;
		NodeList paragraphs;
		
		htmlXPath = XPathFactory.newInstance().newXPath();
		classExpression = htmlXPath.compile("./@class");
		styleExpression = htmlXPath.compile("./@style");

		in = OdfDocument.class.getResourceAsStream(TEMPLATE_NAME);
		pkg = OdfPackage.loadPackage(in);
		outputDocument = OdfDocument.loadDocument(pkg);
		outputDom = outputDocument.getContentDom();
		stylesDom = outputDocument.getStylesDom();

		CeltxNamespaceAdder.addOdfNamespaces(outputDom.getDocumentElement());
		CeltxNamespaceAdder.addOdfNamespaces(stylesDom.getDocumentElement());

		odfXPath = outputDocument.getXPath();
		odfXPath.setNamespaceContext(new OdfNamespace());
		officeText = (Element) odfXPath.evaluate( "office:text",
			outputDocument.getOfficeBody(), XPathConstants.NODE);
        contentAutoStyles = outputDom.getAutomaticStyles();
		masterStyles = outputDocument.getOfficeMasterStyles();
        styleAutoStyles = stylesDom.getAutomaticStyles();
		
		paragraphs = (NodeList) htmlXPath.evaluate("/html/body/p",
				htmlDocument, XPathConstants.NODESET);

		emptyComicCells(); // not needed for screenplay, but do it anyway
		
		new CeltxTitlePage(this).insertTitlePage();

		if (outputCastOfCharacters)
		{
			new CeltxCastOfCharacters(this).outputCastOfCharacters();
		}

		if (outputType.equals(SCREENPLAY_TYPE))
		{
			officeText.appendChild(new OdfParagraphExtra(outputDom,
				"CxScreenplayStart"));
		}

		for (int i = 0; i < paragraphs.getLength(); i++)
		{
			if (outputType.equals(COMIC_TYPE))
			{
				processComicParagraph(paragraphs.item(i));
			}
			else
			{
				processScreenplayParagraph(paragraphs.item(i));
			}
		}
		if (outputType.equals(COMIC_TYPE))
		{
			if (comicTable == null)
			{
				startComicTable(null);
			}
			endComicTable();
		}
	}
	
	void processComicParagraph(Node htmlParagraph) throws Exception
	{
		String classSpec = classExpression.evaluate(htmlParagraph);
		String styleSpec = styleExpression.evaluate(htmlParagraph);
		OdfParagraphExtra outputPara;
		OdfSpan outputSpan;

		if (classSpec.equals("sceneheading"))
		{
			endComicTable();
			sceneNumber++;
			shotNumber = 0;
			startComicTable(htmlParagraph);
		}
		else if (classSpec.equals("shot"))
		{
			endComicRow();
			shotNumber++;
			startComicRow();
			comicPanelCell.appendChild( new OdfParagraphExtra(
				outputDom, sceneNumber + "." + shotNumber, null));
			outputPara = new OdfParagraphExtra(outputDom, "CxComicDescription");
			outputSpan = new OdfSpan(outputDom);
			addStyles(outputSpan, styleSpec);
			processHtmlElement(outputDom, htmlParagraph, outputSpan);
			outputPara.appendChild(outputSpan);
			comicDescriptionCell.appendChild(outputPara);
		}
		else if (classSpec.equals("character") || classSpec.equals("caption") ||
			classSpec.equals("dialog") || classSpec.equals("parenthetical"))
		{
			outputPara = new OdfParagraphExtra(outputDom,
				"CxComic" + ucfirst(classSpec));
			if (classSpec.equals("caption"))
			{
				outputPara.addContent(translation.getProperty("CAPTION") +
					": ");
			}
			outputSpan = new OdfSpan(outputDom);
			addStyles(outputSpan, styleSpec);
			processHtmlElement(outputDom, htmlParagraph, outputSpan);
			outputPara.appendChild(outputSpan);
			comicDialogCell.appendChild(outputPara);
		}
	}

	/*
	 * Look in the style specification for the keywors
	 * bold (from font-weight: bold), italic (font-style:italic),
	 * and underline (text-decoration:underline), and paste their
	 * names together. If any of these exist, prefix a "Cx" --
	 * that's the name of the ODT style to apply from the template.
	 */
	void addStyles(OdfStylableElement element, String styleSpec)
	{
		String[] styleNames ={"bold", "italic", "underline"};
		String resultStyle = "";
		for (int i = 0; i < styleNames.length; i++)
		{
			if (styleSpec.indexOf(styleNames[i]) >= 0)
			{
				resultStyle += ucfirst(styleNames[i]);
			}
		}
		if (!resultStyle.isEmpty())
		{
			element.setStyleName("Cx" + resultStyle);
		}
	}

	/**
	 * Recursively descend through the given input HTML node and
	 * add its text to the given ODF outputNode, creating the appropriate
	 * spans for styling as you proceed.
	 * @outputDom the DOM where the result goes
	 * @param inputNode an HTML span or paragraph element
	 * @param outputNode an ODFParagraphExtra element
	 */
	void processHtmlElement(OdfFileDom outputDom, Node inputNode,
		Node outputNode)
		throws Exception
	{
		Node node = inputNode.getFirstChild();
		while (node != null)
		{
			if (node.getNodeType() == Node.TEXT_NODE)
			{
				outputNode.appendChild(outputDom.
					createTextNode(node.getNodeValue()));
			}
			else if (node.getNodeType() == Node.ELEMENT_NODE)
			{
				if (((Element) node).getLocalName().equals("span"))
				{
					OdfSpan officeSpan = new OdfSpan(outputDom);
					String styleSpec = styleExpression.evaluate(node);
					addStyles(officeSpan, styleSpec);
					processHtmlElement(outputDom, node, officeSpan);
					outputNode.appendChild(officeSpan);
				}
			}
			node = node.getNextSibling();
		}
	}

	void startComicTable(Node htmlParagraph) throws Exception
	{
        OdfStyle originalTableStyle;
        OdfStyleElement newTableStyle;
        Map<OdfStyleProperty,String> properties;

        OdfTableHeaderRows headerRows;
		OdfTableRowExtra row;

        List<String> colList = Arrays.asList(
			"ComicTable.A", "ComicTable.B", "ComicTable.C");
// System.err.println("Start table; scene " + sceneNumber +" shot " + shotNumber);

		/*
		 * Prepare a master page for this table
		 */
		previousMasterPage = startComicPage(htmlParagraph);
		
		/*
         * Create a style that duplicates the "ComicTable" style,
         * and set it master-page-name attribute to the master page
         * created by startComicPage()
         */
        originalTableStyle = contentAutoStyles.getStyle("ComicTable",
            OdfStyleFamily.Table);
        newTableStyle = contentAutoStyles.createStyle(OdfStyleFamily.Table);
        newTableStyle.setProperties(originalTableStyle.getStyleProperties());
        newTableStyle.setName("ComicTable" + sceneNumber);
        newTableStyle.setMasterPageName("CxComicPage" + sceneNumber);

		/*
         * Create the table and set its columns
         */
		comicTable = new OdfTableExtra(outputDom);
		comicTable.setName("ComicTable" + sceneNumber);
		comicTable.setStyleName("ComicTable" + sceneNumber);
		comicTable.setColumnList(
			comicTable.createColumnStyleList(colList));

        /* Add the header row */
		headerRows = new OdfTableHeaderRows(outputDom);
		row = new OdfTableRowExtra(outputDom);
		row.populateStrings(
			Arrays.asList(translation.getProperty("PANEL"),
				translation.getProperty("DESCRIPTION"),
				translation.getProperty("DIALOG")),
			Arrays.asList("ComicTable.A1", "ComicTable.A1",
				"ComicTable.C1"),
			Arrays.asList("Table_20_Heading")
		);
		headerRows.appendChild(row);
		comicTable.appendChild(headerRows);
	}

	public void endComicTable() throws Exception
	{
// System.err.println("End table; scene " + sceneNumber +" shot " + shotNumber);
		if (comicTable != null)
		{
			endComicRow();
			officeText.appendChild(comicTable);
			comicRow = null;
		}
	}

	public void emptyComicCells()
	{
		comicPanelCell = new OdfTableCell(outputDom);
		comicPanelCell.setValueType(OdfValueType.STRING);
		comicPanelCell.setStyleName("ComicTable.A2");

		comicDescriptionCell = new OdfTableCell(outputDom);
		comicDescriptionCell.setValueType(OdfValueType.STRING);
		comicDescriptionCell.setStyleName("ComicTable.A2");

		comicDialogCell = new OdfTableCell(outputDom);
		comicDialogCell.setValueType(OdfValueType.STRING);
		comicDialogCell.setStyleName("ComicTable.C1");
	}

	public void startComicRow()
	{
// System.err.println("Start row; scene " + sceneNumber + " shot " + shotNumber);
		comicRow = new OdfTableRowExtra(outputDom);
		emptyComicCells();
	}

	public void endComicRow() throws Exception
	{
// System.err.println("End row; scene " + sceneNumber + " shot " + shotNumber);
		if (comicRow != null)
		{
			comicRow.appendCell(comicPanelCell);
			comicRow.appendCell(comicDescriptionCell);
			comicRow.appendCell(comicDialogCell);
			if (comicTable == null)
			{
				startComicTable(null);
			}
			comicTable.appendRow(comicRow);
		}
	}

    /**
     * Create a master page to correspond to a comic page.
     * Each "page" of the comic may have a description stretching across
     * several physical printed pages. Thus, each comic page gets its own
     * master page, with a name of cxComicPage<em>n</em>, where <em>n</em>
     * is the comic page number.
     *
	 * @htmlParagraph the paragraph containing the scene name, or null if none
     * @return an OdfMasterPage specification.
     * 
     */
	OdfMasterPage startComicPage(Node htmlParagraph) throws Exception
	{
		OdfHeader styleHeader = new OdfHeader(stylesDom);
		OdfMasterPage masterPage = new OdfMasterPage(stylesDom);
		OdfParagraphExtra odfParagraph = new OdfParagraphExtra(stylesDom,
				"Header");

		odfParagraph.addContent(translation.getProperty("PAGE") + " "
			+ sceneNumber + " ");
		if (htmlParagraph != null)
		{
			processHtmlElement(stylesDom, htmlParagraph, odfParagraph);
		}
		styleHeader.appendChild(odfParagraph);
		masterPage.appendChild(styleHeader);
		masterPage.setDisplayName("CxComicPage" + sceneNumber);
		masterPage.setName("CxComicPage" + sceneNumber);
		masterPage.setPageLayoutName("Mpm1");
		masterStyles.appendChild(masterPage);

		return masterPage;
	}

	void processScreenplayParagraph( Node htmlParagraph ) throws Exception
	{
		String classSpec = classExpression.evaluate(htmlParagraph);
		String styleSpec = styleExpression.evaluate(htmlParagraph);
		OdfParagraphExtra outputPara = new OdfParagraphExtra(outputDom,
				"Cx" + ucfirst(classSpec));
// System.err.println("Processing screenplay paragraph of type " + classSpec);
		OdfSpan outputSpan = new OdfSpan(outputDom);
		addStyles(outputSpan, styleSpec);
		processHtmlElement(outputDom, htmlParagraph, outputSpan);
		outputPara.appendChild(outputSpan);
		officeText.appendChild(outputPara);
	}

	void run(String inputFileName, String outputFileName, String outputType,
		boolean showCast)
	{
		/* Get file name and output type if given */
		int i;
		int nFileNames = 0;
		String docTypePath;
		String urlPrefix = "http://celtx.com/NS/v1/";

		this.outputType = lcfirst(outputType);
		outputCastOfCharacters = showCast;
		
		try {
			ZipInputStream zipStream =
				new ZipInputStream(new FileInputStream(inputFileName));
			ZipEntry zipEntry;
			while (rdfDocument == null &&
				(zipEntry = zipStream.getNextEntry() ) != null)
			{
				if (zipEntry.getName().equals(RDF_FILE_NAME))
				{
					parseRdf(zipStream);
				}
			}
			zipStream.close();

			/*
				Retrieve the  name by finding it
				in the RDF file
			*/
			rdfXPath = XPathFactory.newInstance().newXPath();
			rdfXPath.setNamespaceContext(rdfNamespaceContext);
			docTypePath =
				"/RDF:RDF/cx:Document[cx:doctype/@RDF:resource='"
				+ urlPrefix + ucfirst(outputType) +
				"Document']/@cx:localFile";
			htmlFileName = rdfXPath.evaluate(docTypePath, rdfDocument);

			if (!htmlFileName.equals(""))
			{
				zipStream = new ZipInputStream(new
					FileInputStream(inputFileName));
				while (htmlDocument == null &&
					(zipEntry = zipStream.getNextEntry() ) != null)
				{
					if (zipEntry.getName().equals(htmlFileName))
					{
						parseScriptFile(zipStream, "file://" +
							htmlFileName);
					}
				}
				zipStream.close();

				translation = new Properties();
				InputStream confFile = this.getClass().getClassLoader().
						getResourceAsStream("com/langintro/celtx_to_odt/converter.properties");
				if (confFile == null)
				{
					javax.swing.JOptionPane.showMessageDialog(app,
					"Cannot open converter properties file", "Error",
					javax.swing.JOptionPane.ERROR_MESSAGE);
				}
				translation.load(confFile);
				confFile.close();

				processHtmlFile();
				pkg.insert(outputDom, "content.xml");
				pkg.insert(stylesDom, "styles.xml");
				pkg.save(outputFileName);
			}
			else
			{
				javax.swing.JOptionPane.showMessageDialog(app,
					"Unable to open internal HTML file." + "\n" +
					"Have you specified the correct  type?", "Error",
					javax.swing.JOptionPane.ERROR_MESSAGE);
			}
		}
		catch (Exception e)
		{
			javax.swing.JOptionPane.showMessageDialog(app,
				e.getMessage(), "Error",
				javax.swing.JOptionPane.ERROR_MESSAGE);
			e.printStackTrace();
		}
		
	}
	
	private String ucfirst(String str)
	{
		String result;
		if (str != null && str.length() > 0)
		{
			result = str.substring(0,1).toUpperCase() + str.substring(1);
		}
		else
		{
			result = str;
		}
		return result;
	}

	private String lcfirst(String str)
	{
		String result;
		if (str != null && str.length() > 0)
		{
			result = str.substring(0,1).toLowerCase() + str.substring(1);
		}
		else
		{
			result = str;
		}
		return result;
	}

	/**
	 * Returns the value of rdfDocument.
	 */
	public Document getRdfDocument()
	{
		return rdfDocument;
	}
	
	public Document getHtmlDocument()
	{
		return htmlDocument;
	}
	
	public OdfDocument getOutputDocument()
	{
		return outputDocument;
	}
	
	public Element getOfficeText()
	{
		return officeText;
	}
	
	public OdfMasterStyles getOfficeStyles()
	{
		return masterStyles;
	}

    public OdfAutomaticStyles getContentAutoStyles()
    {
        return contentAutoStyles;
    }

    public OdfAutomaticStyles getStyleAutoStyles()
    {
        return styleAutoStyles;
    }

	public OdfFileDom getOutputDom()
	{
		return outputDom;
	}
	
	public OdfFileDom getStylesDom()
	{
		return stylesDom;
	}

	public XPath getRdfXPath()
	{
		return rdfXPath;
	}
	
	public XPath getHtmlXPath()
	{
		return htmlXPath;
	}
	
	public XPath getOdfXPath()
	{
		return odfXPath;
	}

	public OdfTableExtra getComicTable()
	{
		return comicTable;
	}
	
	public Properties getTranslation()
	{
		return translation;
	}

	public CeltxNamespaceContext getCeltxNamespaceContext()
	{
		return rdfNamespaceContext;
	}
	
}

