/************************************************************************
 *
 *  Converter.java
 *
 *  Copyright: 2002-2015 by Henrik Just
 *
 *  This file is part of Writer2LaTeX.
 *  
 *  Writer2LaTeX 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 3 of the License, or
 *  (at your option) any later version.
 *  
 *  Writer2LaTeX 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 Writer2LaTeX.  If not, see <http://www.gnu.org/licenses/>.
 * 
 *  Version 1.6 (2015-06-11)
 *
 */

package writer2latex.xhtml;

import java.io.File;
import java.io.FileInputStream;
import java.util.HashSet;
import java.util.ListIterator;
import java.util.LinkedList;
import java.util.Set;
import java.util.Stack;
import java.util.Vector;
import java.util.Hashtable;
import java.util.Iterator;

import java.io.InputStream;
import java.io.IOException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.Text;

import writer2latex.api.Config;
import writer2latex.api.ContentEntry;
import writer2latex.api.ConverterFactory;
import writer2latex.api.OutputFile;
import writer2latex.base.ContentEntryImpl;
import writer2latex.base.BasicConverter;
import writer2latex.office.MIMETypes;
import writer2latex.office.OfficeReader;
import writer2latex.office.StyleWithProperties;
import writer2latex.office.XMLString;
import writer2latex.util.ExportNameCollection;
import writer2latex.util.Misc;
import writer2latex.xhtml.content.DrawParser;
import writer2latex.xhtml.content.MathParser;
import writer2latex.xhtml.content.PageContainer;
import writer2latex.xhtml.content.TableParser;
import writer2latex.xhtml.content.TextParser;
import writer2latex.xhtml.l10n.L10n;
import writer2latex.xhtml.style.Styles;

/**
 * <p>This class converts an OpenDocument file to an XHTML(+MathML) or EPUB document.</p>
 *
 */
public class Converter extends BasicConverter {
	private static final String EPUB_STYLES_FOLDER = "styles/";
	private static final String EPUB_STYLESHEET = "styles/styles1.css";
	private static final String EPUB_CUSTOM_STYLESHEET = "styles/styles.css";
	
    // Config
    private XhtmlConfig config;
	
    public Config getConfig() { return config; }
    
    protected XhtmlConfig getXhtmlConfig() { return config; }

    // The locale
    private L10n l10n;
                        
    // The helpers
    private Styles styles;
    private TextParser textParser;
    private TableParser tableParser;
    private DrawParser drawParser;
    private MathParser mathParser;
	
    // The template
    private XhtmlDocument template = null;
    
    // The included style sheet and associated resources
    private CssDocument styleSheet = null;
    private Set<ResourceDocument> resources = new HashSet<ResourceDocument>();
    
    // The xhtml output file(s)
    public int nType = XhtmlDocument.XHTML10; // the doctype
    private boolean bOPS = false; // Do we need to be OPS conforming?
    Vector<XhtmlDocument> outFiles;
    private int outFileIndex;
    private XhtmlDocument htmlDoc; // current outfile
    private Document htmlDOM; // current DOM, usually within htmlDoc
    public PageContainer pageContainer = null;
    //private int nTocFileIndex = -1;
    //private int nAlphabeticalIndex = -1;

    // Hyperlinks
    Hashtable<String, Integer> targets = new Hashtable<String, Integer>();
    LinkedList<LinkDescriptor> links = new LinkedList<LinkDescriptor>();
    // Strip illegal characters from internal hyperlink targets
    private ExportNameCollection targetNames = new ExportNameCollection(true);
    
    // The current context (currently we only track the content width, but this might be expanded with formatting
    // attributes - at least background color and font size later)
    private Stack<String> contentWidth = new Stack<String>();
    
    // Constructor setting the DOCTYPE
    public Converter(int nType) {
        super();
        config = new XhtmlConfig();
        this.nType = nType;
        pageContainer = new PageContainer();

    }

    // override methods to read templates, style sheets and resources
    @Override public void readTemplate(InputStream is) throws IOException {
        template = new XhtmlDocument("Template",nType);
        template.read(is);
    }
	
    @Override public void readTemplate(File file) throws IOException {
        readTemplate(new FileInputStream(file));
    }

    @Override public void readStyleSheet(InputStream is) throws IOException {
    	if (styleSheet==null) {
    		styleSheet = new CssDocument(EPUB_CUSTOM_STYLESHEET);
    	}
    	styleSheet.read(is);
    }
	
    @Override public void readStyleSheet(File file) throws IOException {
        readStyleSheet(new FileInputStream(file));
    }
    
    @Override public void readResource(InputStream is, String sFileName, String sMediaType) throws IOException {
    	if (sMediaType==null) {
    		// Guess the media type from the file extension
    		sMediaType="";
    		String sfilename = sFileName.toLowerCase();
    		// PNG, JPEG and GIF are the basic raster image formats that must be supported by EPUB readers
    		if (sfilename.endsWith(MIMETypes.PNG_EXT)) { sMediaType = MIMETypes.PNG; }
    		else if (sfilename.endsWith(MIMETypes.JPEG_EXT)) { sMediaType = MIMETypes.JPEG; }
    		else if (sfilename.endsWith(".jpeg")) { sMediaType = MIMETypes.JPEG; }
    		else if (sfilename.endsWith(MIMETypes.GIF_EXT)) { sMediaType = MIMETypes.GIF; }
    		// The OPS 2.0.1 specification recommends (and only mentions) OpenType with this media type
    		else if (sfilename.endsWith(".otf")) { sMediaType = "application/vnd.ms-opentype"; }
    		// For convenience we also include a media type for true type fonts (the most common, it seems)
    		else if (sfilename.endsWith(".ttf")) { sMediaType = "application/x-font-ttf"; }
    	}
    	ResourceDocument doc = new ResourceDocument(EPUB_STYLES_FOLDER+sFileName, sMediaType);
    	doc.read(is);
    	resources.add(doc);
    }
    
    @Override public void readResource(File file, String sFileName, String sMediaType) throws IOException {
    	readResource(new FileInputStream(file), sFileName, sMediaType);
    }    
    
    public String getContentWidth() {
    	return contentWidth.peek();
    }
    
    public String pushContentWidth(String sWidth) {
    	return contentWidth.push(sWidth);
    }
    
    public void popContentWidth() {
    	contentWidth.pop();
    }
    
    public boolean isTopLevel() {
    	return contentWidth.size()==1;
    }

    public Styles getStylesParser() { return styles; }
	
    public TextParser getTextParser() { return textParser; }
	
    public TableParser getTableParser() { return tableParser; }

    public DrawParser getDrawParser() { return drawParser; }

    public MathParser getMathParser() { return mathParser; }
	
    public int getType() { return nType; }
    
    public boolean isHTML5() { return nType==XhtmlDocument.HTML5; }
	
    public int getOutFileIndex() { return outFileIndex; }
    
    public void addContentEntry(String sTitle, int nLevel, String sTarget) {
    	converterResult.addContentEntry(new ContentEntryImpl(sTitle,nLevel,htmlDoc,sTarget));
    }
    
    public void setTocFile(String sTarget) {
    	converterResult.setTocFile(new ContentEntryImpl(l10n.get(L10n.CONTENTS),1,htmlDoc,sTarget));
    	//nTocFileIndex = nOutFileIndex;
    }
	
    protected void setLofFile(String sTarget) {
    	converterResult.setLofFile(new ContentEntryImpl("Figures",1,htmlDoc,sTarget));
    }
	
    protected void setLotFile(String sTarget) {
    	converterResult.setLotFile(new ContentEntryImpl("Tables",1,htmlDoc,sTarget));
    }
	
    public void setIndexFile(String sTarget) {
    	converterResult.setIndexFile(new ContentEntryImpl(l10n.get(L10n.INDEX),1,htmlDoc,sTarget));
    	//nAlphabeticalIndex = nOutFileIndex;
    }
    
    public void setCoverFile(String sTarget) {
    	converterResult.setCoverFile(new ContentEntryImpl("Cover",0,htmlDoc,sTarget));
    }
	
    public void setCoverImageFile(OutputFile file, String sTarget) {
    	converterResult.setCoverImageFile(new ContentEntryImpl("Cover image",0,file,sTarget));
    }
	
    public Element createElement(String s) { return htmlDOM.createElement(s); }
	
    public Text createTextNode(String s) { return htmlDOM.createTextNode(s); }
	
    public Node importNode(Node node, boolean bDeep) { return htmlDOM.importNode(node,bDeep); }
	
    public L10n getL10n() { return l10n; }
    
    public void setOPS(boolean b) { bOPS = true; }
    
    public boolean isOPS() { return bOPS; }
    
    @Override public void convertInner() throws IOException {      
        sTargetFileName = Misc.trimDocumentName(sTargetFileName,XhtmlDocument.getExtension(nType));
		
        outFiles = new Vector<XhtmlDocument>();
        outFileIndex = -1;

        l10n = new L10n();
        
        if (isOPS()) {
        	imageConverter.setBaseFileName("image");
        	imageConverter.setUseSubdir("images");
        }
        else { 
        	imageConverter.setBaseFileName(sTargetFileName+"-img");
        	if (config.saveImagesInSubdir()) {
        		imageConverter.setUseSubdir(sTargetFileName+"-img");        	
        	}
        }

        imageConverter.setDefaultFormat(MIMETypes.PNG);
        imageConverter.addAcceptedFormat(MIMETypes.JPEG);
        imageConverter.addAcceptedFormat(MIMETypes.GIF);
        
        if (nType==XhtmlDocument.HTML5) { // HTML supports SVG as well
        	imageConverter.setDefaultVectorFormat(MIMETypes.SVG);
        }

        styles = new Styles(ofr,config,this,nType);
        textParser = new TextParser(ofr,config,this);
        tableParser = new TableParser(ofr,config,this);
        drawParser = new DrawParser(ofr,config,this);
        mathParser = new MathParser(ofr,config,this,nType!=XhtmlDocument.XHTML10 && nType!=XhtmlDocument.XHTML11);

        // Set locale to document language
        StyleWithProperties style = ofr.isSpreadsheet() ? ofr.getDefaultCellStyle() : ofr.getDefaultParStyle();
        if (style!=null) {
        	// The only CTL language recognized currently is farsi
        	if ("fa".equals(style.getProperty(XMLString.STYLE_LANGUAGE_COMPLEX))) {
        		l10n.setLocale("fa", "IR");
        	}
        	else {
        		l10n.setLocale(style.getProperty(XMLString.FO_LANGUAGE), style.getProperty(XMLString.FO_COUNTRY));
        	}
        }
        
        // Set the main content width
        pushContentWidth(getStylesParser().getPageSc().getTextWidth());

        // Traverse the body
        Element body = ofr.getContent();
        if (ofr.isSpreadsheet()) { tableParser.convertTableContent(body); }
        else if (ofr.isPresentation()) { drawParser.convertDrawContent(body); }
        else { textParser.convertDocumentContent(body); }
		
        // Set the title page and text page entries
        if (converterResult.getContent().isEmpty()) {
        	// No headings in the document: There is no title page and the text page is the first page
        	converterResult.setTextFile(new ContentEntryImpl("Text", 1, outFiles.get(0), null));
        	// We also have to add a toc entry (the ncx file cannot be empty)
        	converterResult.addContentEntry(new ContentEntryImpl("Text", 1, outFiles.get(0), null));
        }
        else {
        	ContentEntry firstHeading = converterResult.getContent().get(0);
        	// The title page is the first page after the cover, unless the first page starts with a heading
        	int nFirstPage = converterResult.getCoverFile()!=null ? 1 : 0;
        	if (outFiles.get(nFirstPage)!=firstHeading.getFile() || firstHeading.getTarget()!=null) {
        		converterResult.setTitlePageFile(new ContentEntryImpl("Title page", 1, outFiles.get(nFirstPage), null));
        	}
        	// The text page is the one containing the first heading
        	converterResult.setTextFile(new ContentEntryImpl("Text", 1, firstHeading.getFile(), firstHeading.getTarget()));
        }

        // Resolve links
        ListIterator<LinkDescriptor> iter = links.listIterator();
        while (iter.hasNext()) {
            LinkDescriptor ld = iter.next();
            Integer targetIndex = targets.get(ld.sId);
            if (targetIndex!=null) {
                int nTargetIndex = targetIndex.intValue();
                targetNames.addName(ld.sId);
                if (nTargetIndex == ld.nIndex) { // same file
                	
                  ld.element.setAttribute("href","#"+targetNames.getName(ld.sId));
                }
                else {
                  ld.element.setAttribute("href",getOutFileName(nTargetIndex,true)
                                            +"#"+targetNames.getName(ld.sId));
                }
            }
        }

        // Add included style sheet, if any - and we are creating OPS content
        if (bOPS && styleSheet!=null) {
        	converterResult.addDocument(styleSheet);
        	for (ResourceDocument doc : resources) {
        		converterResult.addDocument(doc);
        	}
        }
        
        // Export styles (XHTML)
        if (!isOPS() && !config.separateStylesheet()) {
        	for (int i=0; i<=outFileIndex; i++) {
        		Element head = outFiles.get(i).getHeadNode();
        		if (head!=null) {
        			Node styleNode = styles.getStylesNode(outFiles.get(i).getContentDOM());
        			if (styleNode!=null) {
        				head.appendChild(styleNode);
        			}
        		}
        	}
        }
        
        // Load MathJax
        // TODO: Should we support different configurations of MathJax?
        if ((nType==XhtmlDocument.HTML5 || nType==XhtmlDocument.XHTML_MATHML) && config.useMathJax()) {
        	for (int i=0; i<=outFileIndex; i++) {
        		if (outFiles.get(i).hasMath()) {
        			XhtmlDocument doc = outFiles.get(i);
        			Element head = doc.getHeadNode();
        			if (head!=null) {
        				Element script = doc.getContentDOM().createElement("script");
        				head.appendChild(script);
        				script.setAttribute("type", "text/javascript");
        				script.setAttribute("src", "http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML");
        			}
        		}
        	}
        }
        
        // Create headers & footers (if nodes are available)
        if (ofr.isSpreadsheet()) {
            for (int i=0; i<=outFileIndex; i++) {

                XhtmlDocument doc = outFiles.get(i);
                Document dom = doc.getContentDOM();
                Element header = doc.getHeaderNode();
                Element footer = doc.getFooterNode();
                Element headerPar = dom.createElement("p");
                Element footerPar = dom.createElement("p");
                footerPar.setAttribute("style","clear:both"); // no floats may pass!

                // Add uplink
                if (config.getXhtmlUplink().length()>0) {
                    Element a = dom.createElement("a");
                    a.setAttribute("href",config.getXhtmlUplink());
                    a.appendChild(dom.createTextNode(l10n.get(L10n.UP)));
                    headerPar.appendChild(a);
                    headerPar.appendChild(dom.createTextNode(" "));
                    a = dom.createElement("a");
                    a.setAttribute("href",config.getXhtmlUplink());
                    a.appendChild(dom.createTextNode(l10n.get(L10n.UP)));
                    footerPar.appendChild(a);
                    footerPar.appendChild(dom.createTextNode(" "));
                }
                // Add links to all sheets:
                int nSheets = tableParser.sheetNames.size();
                for (int j=0; j<nSheets; j++) {
                    if (config.xhtmlCalcSplit()) {
                        addNavigationLink(dom,headerPar,tableParser.sheetNames.get(j),j);
                        addNavigationLink(dom,footerPar,tableParser.sheetNames.get(j),j);
                    }
                    else {
                        addInternalNavigationLink(dom,headerPar,tableParser.sheetNames.get(j),"tableheading"+j);
                        addInternalNavigationLink(dom,footerPar,tableParser.sheetNames.get(j),"tableheading"+j);
	                }
                }
                
                if (header!=null) { header.appendChild(headerPar); }
                if (footer!=null) { footer.appendChild(footerPar); }
            }
        }
        else if (outFileIndex>0) {
            for (int i=0; i<=outFileIndex; i++) {
                XhtmlDocument doc = outFiles.get(i);
                Document dom = doc.getContentDOM();
                //Element content = doc.getContentNode();

                // Header links
                Element header = doc.getHeaderNode();
                if (header!=null) {
                    if (ofr.isPresentation()) {
                        // Absolute placement in presentations (quick and dirty solution)
                        header.setAttribute("style","position:absolute;top:0;left:0");
                    }
                    if (config.getXhtmlUplink().length()>0) {
                        Element a = dom.createElement("a");
                        a.setAttribute("href",config.getXhtmlUplink());
                        a.appendChild(dom.createTextNode(l10n.get(L10n.UP)));
                        header.appendChild(a);
                        header.appendChild(dom.createTextNode(" "));
                    }
                    addNavigationLink(dom,header,l10n.get(L10n.FIRST),0);
                    addNavigationLink(dom,header,l10n.get(L10n.PREVIOUS),i-1);
                    addNavigationLink(dom,header,l10n.get(L10n.NEXT),i+1);
                    addNavigationLink(dom,header,l10n.get(L10n.LAST),outFileIndex);
                    if (textParser.getTocIndex()>=0) {
                        addNavigationLink(dom,header,l10n.get(L10n.CONTENTS),textParser.getTocIndex());
                    }
                    if (textParser.getAlphabeticalIndex()>=0) {
                        addNavigationLink(dom,header,l10n.get(L10n.INDEX),textParser.getAlphabeticalIndex());
                    }
                }

                // Footer links
                Element footer = doc.getFooterNode();
                if (footer!=null && !ofr.isPresentation()) {
                    // No footer in presentations
                    if (config.getXhtmlUplink().length()>0) {
                        Element a = dom.createElement("a");
                        a.setAttribute("href",config.getXhtmlUplink());
                        a.appendChild(dom.createTextNode(l10n.get(L10n.UP)));
                        footer.appendChild(a);
                        footer.appendChild(dom.createTextNode(" "));
                    }
                    addNavigationLink(dom,footer,l10n.get(L10n.FIRST),0);
                    addNavigationLink(dom,footer,l10n.get(L10n.PREVIOUS),i-1);
                    addNavigationLink(dom,footer,l10n.get(L10n.NEXT),i+1);
                    addNavigationLink(dom,footer,l10n.get(L10n.LAST),outFileIndex);
                    if (textParser.getTocIndex()>=0) {
                        addNavigationLink(dom,footer,l10n.get(L10n.CONTENTS),textParser.getTocIndex());
                    }
                    if (textParser.getAlphabeticalIndex()>=0) {
                        addNavigationLink(dom,footer,l10n.get(L10n.INDEX),textParser.getAlphabeticalIndex());
                    }
                }
            }
        }
        else if (config.getXhtmlUplink().length()>0) {
            for (int i=0; i<=outFileIndex; i++) {
                XhtmlDocument doc = outFiles.get(i);
                Document dom = doc.getContentDOM();
                //Element content = doc.getContentNode();

                Element header = doc.getHeaderNode();
                if (header!=null) {
                    Element a = dom.createElement("a");
                    a.setAttribute("href",config.getXhtmlUplink());
                    a.appendChild(dom.createTextNode(l10n.get(L10n.UP)));
                    header.appendChild(a);
                    header.appendChild(dom.createTextNode(" "));
                }

                Element footer = doc.getFooterNode();
                if (footer!=null) {
                    Element a = dom.createElement("a");
                    a.setAttribute("href",config.getXhtmlUplink());
                    a.appendChild(dom.createTextNode(l10n.get(L10n.UP)));
                    footer.appendChild(a);
                    footer.appendChild(dom.createTextNode(" "));
                }
            }
        }
        
        // Export styles
        if (config.xhtmlFormatting()>XhtmlConfig.IGNORE_STYLES) {
        	if (isOPS()) { // EPUB
        		CssDocument cssDoc = new CssDocument(EPUB_STYLESHEET);
        		cssDoc.read(styles.allStyleSelectors(false));
        		converterResult.addDocument(cssDoc);
        	}
        	else if (config.separateStylesheet()) { // XHTML
        		CssDocument cssDoc = new CssDocument(sTargetFileName+"-styles.css");
        		cssDoc.read(styles.allStyleSelectors(false));
        		converterResult.addDocument(cssDoc);
        	}
        }
        
    }
	
    private void addNavigationLink(Document dom, Node node, String s, int nIndex) {
        if (nIndex>=0 && nIndex<=outFileIndex) {
            Element a = dom.createElement("a");
            a.setAttribute("href",Misc.makeHref(getOutFileName(nIndex,true)));
            a.appendChild(dom.createTextNode(s));
            //node.appendChild(dom.createTextNode("["));
            node.appendChild(a);
            node.appendChild(dom.createTextNode(" "));
            //node.appendChild(dom.createTextNode("] "));
        }
        else {
            Element span = dom.createElement("span");
            span.setAttribute("class","nolink");
            node.appendChild(span);
            span.appendChild(dom.createTextNode(s));
            node.appendChild(dom.createTextNode(" "));
            //node.appendChild(dom.createTextNode("["+s+"] "));
        }
    }
	
    private void addInternalNavigationLink(Document dom, Node node, String s, String sLink) {
        Element a = dom.createElement("a");
        a.setAttribute("href","#"+sLink);
        a.appendChild(dom.createTextNode(s));
        //node.appendChild(dom.createTextNode("["));
        node.appendChild(a);
        node.appendChild(dom.createTextNode(" "));
        //node.appendChild(dom.createTextNode("] "));
    }
    
    /* get inline text, ignoring any draw objects, footnotes, formatting and hyperlinks */
    public String getPlainInlineText(Node node) {
    	StringBuilder buf = new StringBuilder();
        Node child = node.getFirstChild();
        while (child!=null) {
            short nodeType = child.getNodeType();
               
            switch (nodeType) {
                case Node.TEXT_NODE:
                    buf.append(child.getNodeValue());
                    break;
                        
                case Node.ELEMENT_NODE:
                    String sName = child.getNodeName();
                    if (sName.equals(XMLString.TEXT_S)) {
                           buf.append(" ");
                    }
                    else if (sName.equals(XMLString.TEXT_LINE_BREAK) || sName.equals(XMLString.TEXT_TAB_STOP) || sName.equals(XMLString.TEXT_TAB)) { // text:tab in oasis
                        buf.append(" ");
                    }
                    else if (OfficeReader.isNoteElement(child)) {
                        // ignore
                    }
                    else if (OfficeReader.isTextElement(child)) {
                    	buf.append(getPlainInlineText(child));
                    }
                    break;
                default:
                    // Do nothing
            }
            child = child.getNextSibling();
        }
        return buf.toString();
    }


    public void handleOfficeAnnotation(Node onode, Node hnode) {
        if (config.xhtmlNotes()) {
            // Extract the text from the paragraphs, separate paragraphs with newline
        	StringBuilder buf = new StringBuilder();
        	Element creator = null;
        	Element date = null;
        	Node child = onode.getFirstChild();
        	while (child!=null) {
        		if (Misc.isElement(child, XMLString.TEXT_P)) {
        			if (buf.length()>0) { buf.append('\n'); }
        			buf.append(getPlainInlineText(child));
        		}
        		else if (Misc.isElement(child, XMLString.DC_CREATOR)) {
        			creator = (Element) child;
        		}
        		else if (Misc.isElement(child, XMLString.DC_DATE)) {
        			date = (Element) child;
        		}
        		child = child.getNextSibling();
        	}
        	if (creator!=null) {
    			if (buf.length()>0) { buf.append('\n'); }
    			buf.append(getPlainInlineText(creator));        		
        	}
        	if (date!=null) {
    			if (buf.length()>0) { buf.append('\n'); }
    			buf.append(Misc.formatDate(OfficeReader.getTextContent(date), l10n.getLocale().getLanguage(), l10n.getLocale().getCountry()));
        	}
            Node commentNode = htmlDOM.createComment(buf.toString());
            hnode.appendChild(commentNode);
        }
    }
	
    /////////////////////////////////////////////////////////////////////////
    // UTILITY METHODS
	
    // Create output file name (docname.html, docname1.html, docname2.html etc.)
    public String getOutFileName(int nIndex, boolean bWithExt) {
        return sTargetFileName + (nIndex>0 ? Integer.toString(nIndex) : "") 
                               + (bWithExt ? htmlDoc.getFileExtension() : "");
    }
	
    // Return true if the current outfile has a non-empty body
    public boolean outFileHasContent() {
        return htmlDoc.getContentNode().hasChildNodes();
    }
    
    // Use another document. TODO: This is very ugly; clean it up!!!
    public void changeOutFile(int nIndex) {
        outFileIndex = nIndex;
        htmlDoc = outFiles.get(nIndex);
        htmlDOM = htmlDoc.getContentDOM();
    }
	
    public Element getPanelNode() {
        return htmlDoc.getPanelNode();
    }
	public String getTitle(){
		String title = metaData.getTitle();
    	if (title==null) { 
    		// use filename as fallback
    		title = htmlDoc.getFileName();
    	}
		return title;
		
	}
    // Prepare next output file
    public Element nextOutFile() {
        htmlDoc = new XhtmlDocument(getOutFileName(++outFileIndex,false),nType);
        htmlDoc.setConfig(config);
        if (template!=null) { htmlDoc.readFromTemplate(template); }
        outFiles.add(outFileIndex,htmlDoc);
        converterResult.addDocument(htmlDoc);
        Element rootElement = createHeadAndBody();
        addEpubNs(rootElement);
        applyWritingDirection();
        addTitle();
        addHeadData();
        return htmlDoc.getContentNode();
    }

		private void addTitle() {
			Element title = htmlDoc.getTitleNode();
			if (title!=null) {
				String sTitle = metaData.getTitle();
				if (sTitle==null) { // use filename as fallback
					sTitle = htmlDoc.getFileName();
				}
				title.appendChild( htmlDOM.createTextNode(sTitle) );
			}
		}

		private Element createHeadAndBody() {
			htmlDOM = htmlDoc.getContentDOM();
			Element rootElement = htmlDOM.getDocumentElement();
			styles.applyDefaultLanguage(rootElement);
			return rootElement;
		}

		private void applyWritingDirection() {
			if (!ofr.isPresentation()) {
			    StyleInfo pageInfo = new StyleInfo();
			    styles.getPageSc().applyDefaultWritingDirection(pageInfo);
			    styles.getPageSc().writeStyle(pageInfo,htmlDoc.getContentNode());
			}
		}

		private void addHeadData() {
			Element head = htmlDoc.getHeadNode();
			if (head!=null) {
				// Declare charset (we need this for XHTML 1.0 strict and HTML5 because we have no <?xml ... ?>)
				if (nType==XhtmlDocument.XHTML10) {
					Element meta = htmlDOM.createElement("meta");
					meta.setAttribute("http-equiv","Content-Type");
					meta.setAttribute("content","text/html; charset="+htmlDoc.getEncoding().toLowerCase());
					head.appendChild(meta);
				}
				else if (nType==XhtmlDocument.HTML5) {
					// The encoding should be UTF-8, but we still respect the user's setting
					Element meta = htmlDOM.createElement("meta");
					meta.setAttribute("charset",htmlDoc.getEncoding().toUpperCase());
					head.appendChild(meta);        		
				}

				// Add meta data (for EPUB the meta data belongs to the .opf file)
				if (!bOPS) {
					// "Traditional" meta data
					//createMeta("generator","Writer2LaTeX "+Misc.VERSION);
					createMeta(head,"description",metaData.getDescription());
					createMeta(head,"keywords",metaData.getKeywords());

					// Dublin core meta data (optional)
					// Format as recommended on dublincore.org (http://dublincore.org/documents/dc-html/)
					// Declare meta data profile
					if (config.xhtmlUseDublinCore()) {
						head.setAttribute("profile","http://dublincore.org/documents/2008/08/04/dc-html/");
						// Add link to declare namespace
						Element dclink = htmlDOM.createElement("link");
						dclink.setAttribute("rel","schema.DC");
						dclink.setAttribute("href","http://purl.org/dc/elements/1.1/");
						head.appendChild(dclink);
						// Insert the actual meta data
						createMeta(head,"DC.title",metaData.getTitle());
						// DC.subject actually contains subject+keywords, so we merge them
						String sDCSubject = "";
						if (metaData.getSubject()!=null && metaData.getSubject().length()>0) {
							sDCSubject = metaData.getSubject();
						}
						if (metaData.getKeywords()!=null && metaData.getKeywords().length()>0) {
							if (sDCSubject.length()>0) { sDCSubject+=", "; }
							sDCSubject += metaData.getKeywords();
						}
						createMeta(head,"DC.subject",sDCSubject);
						createMeta(head,"DC.description",metaData.getDescription());
						createMeta(head,"DC.creator",metaData.getCreator());
						createMeta(head,"DC.date",metaData.getDate());
						createMeta(head,"DC.language",metaData.getLanguage());
					}
				}

				// Add link to custom stylesheet, if producing normal XHTML
				if (!bOPS && config.xhtmlCustomStylesheet().length()>0) {
					Element htmlStyle = htmlDOM.createElement("link");
					htmlStyle.setAttribute("rel","stylesheet");
					htmlStyle.setAttribute("type","text/css");
					htmlStyle.setAttribute("media","all");
					htmlStyle.setAttribute("href",config.xhtmlCustomStylesheet());
					head.appendChild(htmlStyle);
				}
				
				// Add link to generated stylesheet if producing normal XHTML and the user wants separate css
				if (!bOPS && config.separateStylesheet() && config.xhtmlFormatting()>XhtmlConfig.IGNORE_STYLES) {
					Element htmlStyle = htmlDOM.createElement("link");
					htmlStyle.setAttribute("rel","stylesheet");
					htmlStyle.setAttribute("type","text/css");
					htmlStyle.setAttribute("media","all");
					htmlStyle.setAttribute("href",sTargetFileName+"-styles.css");
					head.appendChild(htmlStyle);
				}

				// Add link to included style sheet if producing OPS content
				if (bOPS && styleSheet!=null) {
					Element sty = htmlDOM.createElement("link");
					sty.setAttribute("rel", "stylesheet");
					sty.setAttribute("type", "text/css");
					sty.setAttribute("href", EPUB_CUSTOM_STYLESHEET);
					head.appendChild(sty);
				}

				// Add link to generated stylesheet if producing OPS content
				if (isOPS() && config.xhtmlFormatting()>XhtmlConfig.IGNORE_STYLES) {
					Element htmlStyle = htmlDOM.createElement("link");
					htmlStyle.setAttribute("rel","stylesheet");
					htmlStyle.setAttribute("type","text/css");
					htmlStyle.setAttribute("href",EPUB_STYLESHEET);
					head.appendChild(htmlStyle);
				}

			}
		}
    
    // Add epub namespace for the purpose of semantic inflection in EPUB 3
    public void addEpubNs(Element elm) {
    	if (bOPS && nType==XhtmlDocument.HTML5) {
           	elm.setAttribute("xmlns:epub", "http://www.idpf.org/2007/ops");
    	}    	
    }
    
	// Add a type from the structural semantics vocabulary of EPUB 3
    public void addEpubType(Element elm, String sType) {
    	if (bOPS && nType==XhtmlDocument.HTML5 && sType!=null) {
    		elm.setAttribute("epub:type", sType);
    	}
    }
	
    // create a target
    public Element createTarget(String sId) {
        Element a = htmlDOM.createElement("a");
    		targetNames.addName(sId);
        a.setAttribute("id",targetNames.getName(sId));
        targets.put(sId, new Integer(outFileIndex));
        return a;
    }
	
    // put a target id on an existing element
    public void addTarget(Element node,String sId) {
    		targetNames.addName(sId);
        node.setAttribute("id",targetNames.getName(sId));
        targets.put(sId, new Integer(outFileIndex));
    }

    // create an internal link
    public Element createLink(String sId) {
        Element a = htmlDOM.createElement("a");
        LinkDescriptor ld = new LinkDescriptor();
        ld.element = a; ld.sId = sId; ld.nIndex = outFileIndex;
        links.add(ld);
        return a;
    }

    // create a link
    public Element createLink(Element onode) {
        // First create the anchor
        String sHref = onode.getAttribute(XMLString.XLINK_HREF);
        Element anchor;
        if (sHref.startsWith("#")) { // internal link
            anchor = createLink(sHref.substring(1));
        }
        else { // external link
            anchor = htmlDOM.createElement("a");
            
            sHref = ofr.fixRelativeLink(sHref);
	
            // Workaround for an OOo problem:
            if (sHref.indexOf("?")==-1) { // No question mark
                int n3F=sHref.indexOf("%3F");
                if (n3F>0) { // encoded question mark
                    sHref = sHref.substring(0,n3F)+"?"+sHref.substring(n3F+3);
                }
            }
            anchor.setAttribute("href",sHref);
            String sName = Misc.getAttribute(onode,XMLString.OFFICE_NAME);
            if (sName!=null) {
            	// The name attribute is rarely use (usually the user will insert a bookmark)
            	// Hence we (mis)use it to set additional attributes that are not supported by OOo
            	if (sName.indexOf(";")==-1 && sName.indexOf("=")==-1) {
            		// Simple case, use the value to set name and title
                    anchor.setAttribute("name",sName);
                    anchor.setAttribute("title",sName);
            	}
            	else {
                	// Complex case - using the syntax: name=value;name=value;...
            		String[] sElements = sName.split(";");
            		for (String sElement : sElements) {
            			String[] sNameVal = sElement.split("=");
            			if (sNameVal.length>=2) {
            				anchor.setAttribute(sNameVal[0].trim(),sNameVal[1].trim());
            			}
            		}
            	}
            }
            // TODO: target has been deprecated in xhtml 1.0 strict
            String sTarget = Misc.getAttribute(onode,XMLString.OFFICE_TARGET_FRAME_NAME);
            if (sTarget!=null) { anchor.setAttribute("target",sTarget); }
        }

        // Then style it
        String sStyleName = onode.getAttribute(XMLString.TEXT_STYLE_NAME);
        String sVisitedStyleName = onode.getAttribute(XMLString.TEXT_VISITED_STYLE_NAME);
        StyleInfo anchorInfo = new StyleInfo();
        styles.getTextSP().applyAnchorStyle(sStyleName,sVisitedStyleName,anchorInfo);
        styles.getTextSP().writeStyle(anchorInfo,anchor);

        return anchor;
    }

	
    private void createMeta(Element head, String sName, String sValue) {
        if (sValue==null) { return; }
        Element meta = htmlDOM.createElement("meta");
        meta.setAttribute("name",sName);
        meta.setAttribute("content",sValue);
        head.appendChild(meta);
    }
	
		
}