/**
 * **********************************************************************
 *
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 *
 * Copyright 2008, 2010 Oracle and/or its affiliates. All rights reserved.
 *
 * Use is subject to license terms.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy
 * of the License at http://www.apache.org/licenses/LICENSE-2.0. You can also
 * obtain a copy of the License at http://odftoolkit.org/docs/license.txt
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 ************************************************************************/
package org.odftoolkit.odfdom.dom;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;


import org.odftoolkit.odfdom.dom.element.office.OfficeBodyElement;
import org.odftoolkit.odfdom.dom.element.office.OfficeMasterStylesElement;
import org.odftoolkit.odfdom.dom.element.style.StyleMasterPageElement;
import org.odftoolkit.odfdom.dom.element.table.TableTableElement;
import org.odftoolkit.odfdom.incubator.doc.office.OdfOfficeMasterStyles;
import org.odftoolkit.odfdom.incubator.doc.office.OdfOfficeStyles;
import org.odftoolkit.odfdom.pkg.OdfElement;
import org.odftoolkit.odfdom.pkg.OdfFileDom;
import org.odftoolkit.odfdom.pkg.OdfPackage;
import org.odftoolkit.odfdom.pkg.OdfPackageDocument;
import org.odftoolkit.odfdom.pkg.OdfValidationException;
import org.odftoolkit.odfdom.pkg.rdfa.Util;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;

import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;
import org.apache.jena.util.ResourceUtils;
import java.io.InputStreamReader;

/**
 * A document in ODF is from the package view a directory with a media type.
 * If the media type represents a document described by the ODF 1.2 Schema,
 * certain files are assumed within:
 * content.xml, styles.xml, metadata.xml and settings.xml.
 *
 * The class represents such a document, providing easier access to its XML files.
 */
public abstract class OdfSchemaDocument extends OdfPackageDocument {

	protected OdfContentDom mContentDom;
	protected OdfStylesDom mStylesDom;
	protected OdfMetaDom mMetaDom;
	protected OdfSettingsDom mSettingsDom;
	protected OdfOfficeStyles mDocumentStyles;

	/**
	 * Creates a new OdfSchemaDocument.
	 *
	 * @param pkg - the ODF Package that contains the document. A baseURL is being generated based on its location.
	 * @param internalPath - the directory path within the package from where the document should be loaded.
	 * @param mediaTypeString
	 *      - media type of stream. If unknown null can be used.
	 */
	protected OdfSchemaDocument(OdfPackage pkg, String internalPath, String mediaTypeString) {
		super(pkg, internalPath, mediaTypeString);
		ErrorHandler errorHandler = pkg.getErrorHandler();
		if (errorHandler != null) {
			if (pkg.getFileEntry(internalPath + "content.xml") == null && pkg.getFileEntry(internalPath + "styles.xml") == null) {
				try {
					String baseURI = pkg.getBaseURI();
					if (baseURI == null) {
						baseURI = internalPath;
					} else {
						if (!internalPath.equals(ROOT_DOCUMENT_PATH)) {
							baseURI = "/" + internalPath;
						}
					}
					errorHandler.error(new OdfValidationException(OdfSchemaConstraint.DOCUMENT_WITHOUT_CONTENT_NOR_STYLES_XML, baseURI));
				} catch (SAXException ex) {
					Logger.getLogger(OdfPackage.class.getName()).log(Level.SEVERE, null, ex);
				}
			}
			InputStream mimetypeStream = pkg.getInputStream(OdfPackage.OdfFile.MEDIA_TYPE.getPath(), true);
			if (internalPath.equals(ROOT_DOCUMENT_PATH) && mimetypeStream == null) {
				try {
					errorHandler.error(new OdfValidationException(OdfSchemaConstraint.PACKAGE_SHALL_CONTAIN_MIMETYPE, pkg.getBaseURI()));
				} catch (SAXException ex) {
					Logger.getLogger(OdfPackage.class.getName()).log(Level.SEVERE, null, ex);
				}
			}
		}
	}

	/**
	 * This enum contains all possible standardized XML ODF files of the OpenDocument document.
	 */
	public static enum OdfXMLFile {

		/** The XML file containing the content of an ODF document as specified by the ODF 1.2 specification part 1. */
		CONTENT("content.xml"),
		/** The XML file containing a predifined set of metadata related to an ODF document as specified by the ODF 1.2 specification part 1. */
		META("meta.xml"),
		/** The XML file containing the settings of an ODF document as specified by the ODF 1.2 specification part 1. */
		SETTINGS("settings.xml"),
		/** The XML file containing the styles of an ODF document as specified by the ODF 1.2 specification part 1. */
		STYLES("styles.xml");
		private final String mFileName;

		/**
		 * @return the file name of xml files contained in odf packages.
		 */
		public String getFileName() {
			return mFileName;
		}

		OdfXMLFile(String fileName) {
			this.mFileName = fileName;
		}
	}

	/**
	 * Gets the ODF content.xml file as stream.
	 *
	 * @return - a stream of the ODF content 'content.xml' file
	 * @throws java.lang.Exception - if the stream can not be extracted
	 */
	public InputStream getContentStream() throws Exception {
		String path = getXMLFilePath(OdfXMLFile.CONTENT);
		return mPackage.getInputStream(path);
	}

	/**
	 * Gets the ODF style.xml file as stream.
	 *
	 * @return - a stream of the ODF style 'styles.xml' file
	 * @throws java.lang.Exception - if the stream can not be extracted
	 */
	public InputStream getStylesStream() throws Exception {
		return mPackage.getInputStream(getXMLFilePath(OdfXMLFile.STYLES));
	}

	/**
	 * Gets the ODF settings.xml file as stream.
	 *
	 * @return - a stream of the ODF settings 'setting.xml' file
	 * @throws java.lang.Exception - if the stream can not be extracted
	 */
	public InputStream getSettingsStream() throws Exception {
		return mPackage.getInputStream(getXMLFilePath(OdfXMLFile.SETTINGS));
	}

	/**
	 * Gets the ODF metadata.xml file as stream.
	 *
	 * @return - a stream of the ODF metadata 'meta.xml' file
	 * @throws java.lang.Exception - if the stream can not be extracted
	 */
	public InputStream getMetaStream() throws Exception {
		return mPackage.getInputStream(getXMLFilePath(OdfXMLFile.META));
	}

	/**
	 * Get the relative path for an embedded ODF document including its file name.
	 * @param file represents one of the standardized XML ODF files.
	 * @return path to embedded ODF XML file relative to ODF package root.
	 */
	protected String getXMLFilePath(OdfXMLFile file) {
		return file.mFileName;
	}

	/**
	 * Get the URI, where this ODF document is stored.
	 * @return the URI to the ODF document. Returns null if document is not stored yet.
	 */
	public String getBaseURI() {
		return mPackage.getBaseURI();


	}

	/**
	 * Return the ODF type-based content DOM of the content.xml
	 *
	 * @return ODF type-based content DOM or null if no content.xml exists.
	 * @throws Exception if content DOM could not be initialized
	 */
	public OdfContentDom getContentDom() throws Exception {
		if (mContentDom == null) {
			mContentDom = (OdfContentDom) getFileDom(OdfXMLFile.CONTENT);
		}
		return mContentDom;
	}

	/**
	 * Return the ODF type-based styles DOM of the styles.xml
	 *
	 * @return ODF type-based styles DOM or null if no styles.xml exists.
	 * @throws Exception if styles DOM could not be initialized
	 */
	public OdfStylesDom getStylesDom() throws Exception {
		if (mStylesDom == null) {
			mStylesDom = (OdfStylesDom) getFileDom(OdfXMLFile.STYLES);
		}
		return mStylesDom;
	}

	/**
	 * Return the ODF type-based metadata DOM of the meta.xml
	 *
	 * @return ODF type-based meta DOM or null if no meta.xml exists.
	 * @throws Exception if meta DOM could not be initialized
	 */
	public OdfMetaDom getMetaDom() throws Exception {
		if (mMetaDom == null) {
			mMetaDom = (OdfMetaDom) getFileDom(OdfXMLFile.META);
		}
		return mMetaDom;
	}

	/**
	 * Return the ODF type-based settings DOM of the settings.xml
	 *
	 * @return ODF type-based settings DOM or null if no settings.xml exists.
	 * @throws Exception if settings DOM could not be initialized
	 */
	public OdfSettingsDom getSettingsDom() throws Exception {
		if (mSettingsDom == null) {
			mSettingsDom = (OdfSettingsDom) getFileDom(OdfXMLFile.SETTINGS);
		}
		return mSettingsDom;
	}

	/**
	 *
	 * @return the office:styles element from the styles dom or null if there is
	 * no such element.
	 */
	public OdfOfficeStyles getDocumentStyles() {
		if (mDocumentStyles == null) {
			try {
				OdfFileDom stylesDom = getStylesDom();
				if (stylesDom != null) {
					mDocumentStyles = OdfElement.findFirstChildNode(OdfOfficeStyles.class, stylesDom.getFirstChild());
				} else {
					return null;
				}
			} catch (Exception ex) {
				Logger.getLogger(OdfSchemaDocument.class.getName()).log(Level.SEVERE, null, ex);
			}
		}
		return mDocumentStyles;
	}

	/**
	 * return the office:master-styles element of this document.
	 *
	 * @return the office:master-styles element
	 */
	public OdfOfficeMasterStyles getOfficeMasterStyles() {
		try {
			OdfFileDom fileDom = getStylesDom();
			if (fileDom != null) {
				return OdfElement.findFirstChildNode(OdfOfficeMasterStyles.class, fileDom.getFirstChild());
			}
		} catch (Exception ex) {
			Logger.getLogger(OdfSchemaDocument.class.getName()).log(Level.SEVERE, null, ex);
		}
		return null;
	}

	/**
	 *
	 * @return the office:styles element from the styles dom. If there is not
	 *         yet such an element, it is created.
	 */
	public OdfOfficeStyles getOrCreateDocumentStyles() {
		if (mDocumentStyles == null) {
			try {
				OdfFileDom stylesDom = getStylesDom();
				Node parent = stylesDom != null ? stylesDom.getFirstChild() : null;
				if (parent != null) {
					mDocumentStyles = OdfElement.findFirstChildNode(OdfOfficeStyles.class, parent);
					if (mDocumentStyles == null) {
						mDocumentStyles = stylesDom.newOdfElement(OdfOfficeStyles.class);
						parent.insertBefore(mDocumentStyles, parent.getFirstChild());
					}
				}
			} catch (Exception ex) {
				Logger.getLogger(OdfSchemaDocument.class.getName()).log(Level.SEVERE, null, ex);
			}
		}
		return mDocumentStyles;
	}

	/**
	 * Return a list of table features in this document.
	 *
	 * @return a list of table features in this document.
	 */
	// ToDo: Instead of a method to receive all possible feature/components on the document, there might be a generic or one each element?
	public List<TableTableElement> getTables() {
		List<TableTableElement> tableList = new ArrayList<TableTableElement>();
		try {
			// find tables from content.xml
			OfficeBodyElement officeBody = OdfElement.findFirstChildNode(OfficeBodyElement.class, getContentDom().getRootElement());
			OdfElement contentRoot = OdfElement.findFirstChildNode(OdfElement.class, officeBody);
			tableList = fillTableList(contentRoot, tableList);

			// find tables from styles.xml (header & footer)
			Map<String, StyleMasterPageElement> masterPages = getMasterPages();
			StyleMasterPageElement defaultMasterPage = masterPages.get("Standard");
			if (defaultMasterPage != null) {
				tableList = fillTableList(defaultMasterPage, tableList);
			}
		} catch (Exception ex) {
			Logger.getLogger(OdfSchemaDocument.class.getName()).log(Level.SEVERE, null, ex);
		}
		return tableList;
	}

	// Only tables being on root level are being considered
	private List<TableTableElement> fillTableList(Element startElement, List<TableTableElement> tableList) {
		NodeList childList = startElement.getChildNodes();
		for (int i = 0;
				i < childList.getLength();
				i++) {
			Node childNode = childList.item(i);
			if (childNode instanceof Element) {
				if(childNode instanceof TableTableElement){
					tableList.add((TableTableElement) childList.item(i));
				}else{
					fillTableList((Element) childNode, tableList);
				}
			}
		}
		return tableList;
	}

	/**
	 * ToDo: Instead of adding all elements using an index to the document, we
	 * might add a pattern to the code generation to create a HashMap either on
	 * demand (whenever such a structure is required from the user) or by
	 * default
	 *
	 * @deprecated This method will be moved to the generated sources as soon
	 *              code generation was improved!
	 *
	 */
	public Map<String, StyleMasterPageElement> getMasterPages() throws Exception {

		// get original values:
		OdfStylesDom stylesDoc = getStylesDom();
		OfficeMasterStylesElement masterStyles = OdfElement.findFirstChildNode(OfficeMasterStylesElement.class, stylesDoc.getRootElement());
		Map<String, StyleMasterPageElement> masterPages = null;
		if (masterStyles != null) {
			NodeList lstMasterPages = stylesDoc.getElementsByTagNameNS(OdfDocumentNamespace.STYLE.getUri(), "master-page");
			if (lstMasterPages != null && lstMasterPages.getLength() > 0) {
				masterPages = new HashMap();
				for (int i = 0; i < lstMasterPages.getLength(); i++) {
					StyleMasterPageElement masterPage = (StyleMasterPageElement) lstMasterPages.item(i); //Take the node from the list
					//ToDo: Drop Attribute Suffix for methods returning String values and NOT Attributes
					String styleName = masterPage.getStyleNameAttribute();
					masterPages.put(styleName, masterPage);
				}
			}
		}
		return masterPages;
	}

	/**
	 * Close the OdfPackage and release all temporary created data.
	 * Acter execution of this method, this class is no longer usable.
	 * Do this as the last action to free resources.
	 * Closing an already closed document has no effect.
	 * Note that this will not close any cached documents.
	 */
	@Override
	public void close() {
		mContentDom = null;
		mStylesDom = null;
		mMetaDom = null;
		mSettingsDom = null;
		mDocumentStyles = null;
		super.close();
	}

	public OdfFileDom getFileDom(OdfXMLFile file) throws Exception {
		return getFileDom(getXMLFilePath(file));
	}

	/**
	 * Get all two types of RDF Metadata through GRDDL XSLT:
	 * http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#__RefHeading__1415068_253892949
	 */
	public Model getRDFMetadata() throws Exception {
		Model m = getInContentMetadata().union(this.getManifestRDFMetadata());
		return m;
	}

	/**
	 * Get In Content RDF Metadata through GRDDL XSLT
	 * http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#__RefHeading__1415070_253892949
	 */
	public Model getInContentMetadata() throws Exception {
		Model documentRDFModel = ModelFactory.createDefaultModel();
		Model fileRDFModel = null;
		for (String internalPath : this.getPackage().getFilePaths()) {
			for (OdfXMLFile file : OdfXMLFile.values()) {
				if (Util.isSubPathOf(internalPath, this.getDocumentPath())
						&& internalPath.endsWith(file.getFileName())) {
					fileRDFModel = getXMLFileMetadata(internalPath);
					if (fileRDFModel.size() > 0) {
						documentRDFModel = documentRDFModel.union(fileRDFModel);
					}
					break;
				}
			}
		}
		if (fileRDFModel.size() > 0) {
			documentRDFModel = documentRDFModel.union(fileRDFModel);
		}
		return documentRDFModel;
	}

	/**
	 * Get in-content metadata cache model
	 *
	 * @return The in-content metadata cache model
	 * @throws Exception
	 */
	public Model getInContentMetadataFromCache() throws Exception {
		Model m = ModelFactory.createDefaultModel();
		// find and merge the RDF triples cache from the OdfXMLFile files
		for (OdfXMLFile file : OdfXMLFile.values()) {
			for (Model m1 : this.getFileDom(file).getInContentMetadataCache().values()) {
				m = m.union(m1);
			}
		}
		return m;
	}

	/**
	 * Get RDF metadata from manifest.rdf and those rdf files registered in the
	 * manifest.xml as "application/rdf+xml" through GRDDL XSLT
	 * http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#__RefHeading__1415072_253892949
	 */
	public Model getManifestRDFMetadata() throws Exception {
		Model m = ModelFactory.createDefaultModel();
		for (String internalPath : this.getPackage().getFilePaths()) {
			if (Util.isSubPathOf(internalPath, this.getDocumentPath()) && this.getPackage().getMediaTypeString(internalPath).endsWith("application/rdf+xml")) {
				Model m1 = ModelFactory.createDefaultModel();
				String RDFBaseUri = Util.getRDFBaseUri(this.getPackage().getBaseURI(), internalPath);
				m1.read(new InputStreamReader(this.getPackage().getInputStream(internalPath), "utf-8"), RDFBaseUri);
				// remove the last SLASH at the end of the RDFBaseUri:
				// test_rdfmeta.odt/ --> test_rdfmeta.odt
				ResourceUtils.renameResource(m1.getResource(RDFBaseUri), RDFBaseUri.substring(0, RDFBaseUri.length() - 1));
				if (m1.size() > 0) {
					m = m.union(m1);
				}
			}
		}
		return m;
	}

	/**
	 * Get in-content metadata model of bookmarks
	 *
	 * @return The in-content metadata model of bookmarks
	 * @throws Exception
	 */
	public Model getBookmarkRDFMetadata() throws Exception {
		Model m = ModelFactory.createDefaultModel();
		for (OdfXMLFile file : OdfXMLFile.values()) {
			OdfFileDom dom = getFileDom(file);
			m = m.union(dom.getBookmarkRDFMetadata());
		}
		return m;
	}
}
