package org.bodington.server.resources;

import java.io.IOException;
import java.io.OutputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.zip.ZipEntry;

import org.apache.log4j.Logger;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Comment;
import org.w3c.dom.Element;
import org.w3c.dom.Document;

import org.bodington.database.PrimaryKey;
import org.bodington.server.BuildingServerException;
import org.bodington.server.BuildingSession;
import org.bodington.server.BuildingSessionManagerImpl;
import org.bodington.server.realm.Permission;
import org.bodington.servlet.Request;
import org.bodington.xml.DOMFactory;
import org.bodington.xml.XMLMetadataUtils;
import org.bodington.xml.XMLSerializer;

public class ContentPackager extends ContentZipper {
	
	private Logger logger = Logger.getLogger(ContentPackager.class);

	/**default namespace and metadata namespace*/
	private static String DEFAULT_NAMESPACE_URI = "http://www.imsglobal.org/xsd/imscp_v1p1";
	private static String DEFAULT_NAMESPACE_SCHEMA_LOCATION = "http://www.imsglobal.org/xsd/imscp_v1p1.xsd";
	
	private static String IMSMD_NAMESPACE_URI = "http://www.imsglobal.org/xsd/imsmd_v1p2";
	private static String IMSMD_NAMESPACE_SCHEMA_LOCATION = "http://www.imsglobal.org/xsd/imsmd_v1p2p4.xsd";
	
	private Document document;
	// Need 'organization' element to attach 'item' elements to (hierarchical):
	private Element organizationsElement, organizationElement;
	// Hashtable to keep track of 'item' elements representing resources/folders in order to attach nested file/folder
	// elements to correct parents:
	private Hashtable itemElementsTable;
	// Need  a 'resources' element; holds all child 'resource' elements:
	private Element resourcesElement;
	// Hashtable of 'resource' elements, in order to attach 'file' elements for each Bod resource.
	// (Necessary because processing of metadata for child and parent can be interleaved.)
	private Hashtable resourceElementsTable;
	
	// Base URL for creation of source URLs of original material location in metadata:
	private String baseURL;

	protected void init( Request request, OutputStream outputStream )
	{
		super.init(request, outputStream);
		itemElementsTable = new Hashtable();
		resourceElementsTable = new Hashtable();

		String host = request.getServerName();
		String contextpath = request.getContextPath();
		String servletpath = request.getServletPath();
		baseURL = "http://" + host + contextpath + servletpath + "/";
	}
	
	public void packageResource( Request request, OutputStream outputStream ) throws BuildingServerException {

		try {
			init(request, outputStream);
			Resource resource = request.getResource();

			if ( resource.checkPermission( Permission.ARCHIVE ) ||
							resource.checkPermission( Permission.MANAGE ) )
			{
				// Need to create upper level metadata elements:
				Element manifestElement = createManifestElement(resource);
				organizationsElement = createOrganizationsElement(resource);
				manifestElement.appendChild(organizationsElement);
				// Need organization element to attach item elements to (hierarchical):
				organizationElement = createOrganizationElement(resource);
				organizationsElement.appendChild(organizationElement);
				// Need resources element; holds all resources:
				resourcesElement = createDefaultNSElement("resources");

				// create and append nested item elements, and resource elements:
				processResource(resource, "");

				manifestElement.appendChild(resourcesElement);

				if ( !errors.isEmpty() )
					writeErrorFileToZip(formatErrorMessages());

				writeManifestToZip(manifestElement, "imsmanifest.xml");

				finishProcessing();
			}
			else
			{
				String message = "You need Manage or Archive access to use the Export tool";
				throw new BuildingServerException(message);
			}
		} catch (IOException e) {
			throw new BuildingServerException(e.getMessage());
		}
	}

	private Element createManifestElement( Resource bodingtonResource )
	{
		String identifier = bodingtonResource.getResourceId().toString();
		Element manifest = createDefaultNSElement("manifest");
		manifest.setAttribute("identifier", "Manifest-" + identifier );
		manifest.setAttribute("version", "IMS CP 1.1.4");
		manifest.setAttribute("xmlns", DEFAULT_NAMESPACE_URI);
		manifest.setAttribute("xmlns:imsmd", IMSMD_NAMESPACE_URI);
		manifest.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
		manifest.setAttribute("xsi:schemaLocation",
				DEFAULT_NAMESPACE_URI + " " + DEFAULT_NAMESPACE_SCHEMA_LOCATION + " "
				+ IMSMD_NAMESPACE_URI  + " " + IMSMD_NAMESPACE_SCHEMA_LOCATION);
		
		// create metadata element:
		Element metadata = createDefaultNSElement("metadata");
		// append IMS CP schema element:
		metadata.appendChild(createDefaultNSElement("schema", "IMS Content"));
		// append IMS CP schema version element:
		metadata.appendChild(createDefaultNSElement("schemaversion", "1.1.4"));
		
		// date generated:
		DateFormat format = new SimpleDateFormat("dd MMM yyyy HH:mm:ss");
		Element datetime = createLOMElement("datetime", format.format(new Date()));
		metadata.appendChild(datetime);
		
		manifest.appendChild(metadata);
		
		return manifest;
	}

	private Element createOrganizationsElement( Resource bodingtonResource )
	{
		String identifier = bodingtonResource.getResourceId().toString();
		Element organizations = createDefaultNSElement("organizations");
		organizations.setAttributeNS(DEFAULT_NAMESPACE_URI, "default", "TOC-" + identifier);
		
		return organizations;
	}
	
	private Element createOrganizationElement( Resource bodingtonResource )
	{
		String identifier = bodingtonResource.getResourceId().toString();
		Element organization = createDefaultNSElement("organization"); 
		organization.setAttributeNS(DEFAULT_NAMESPACE_URI, "identifier", "TOC-" + identifier);
		organization.setAttributeNS(DEFAULT_NAMESPACE_URI, "structure", "hierarchical");
		
		String note = "To satisfy the IMS CP schema, additional metadata for each item is appended after any child item elements.";
		organization.appendChild(createCommentElement(note));

		return organization;
	}

	/**
	 * Creates XML elements and adds content to archive for a given resource.
	 * (Calls superclass to add content to zip, then creates XML metadata.)
	 * @param resource
	 * @param path
	 * @param containingElement
	 * @return
	 * @throws IOException
	 */
	public void processResource( Resource resource, String path ) throws IOException
	{
		// TODO: should be able to remove checks for isIncludedResourceType() and checkPermission().
		// Checks are also in superclass. If they are removed, ensure XML elements don't get added to manifest.
		if ( isIncludedResourceType(resource, path))
		{
			if ( !( resource.checkPermission( Permission.MANAGE ) ||
					resource.checkPermission(Permission.ARCHIVE) ) )
			{
				errors.add("Resource not added, you need Manage or Archive access for " + path);
				return;
			}

			PrimaryKey identifier = resource.getResourceId();
			// create 'item' element representing this resource:
			Element itemElement = createItemElement(resource, errors);
			// put it in the table in order to attach child resources later:
			itemElementsTable.put(identifier, itemElement);

			// create 'resource' element which will have 'file' elements appended
			// for any content in files (existing and generated from description, etc):
			Element resourceElement = createResourceElement(resource, path);
			resourceElementsTable.put(identifier, resourceElement);
			// append all 'resource' elements to 'resources' element: 
			resourcesElement.appendChild(resourceElement);

			// call superclass to produce zip archive:
			super.processResource(resource, path);

			generateFileMetadata();

			// determine which element to append the 'item' element to, and append it:
			PrimaryKey parentID = resource.getParentResourceId();
			if (itemElementsTable.containsKey(parentID))
			{
				Element parent = (Element)itemElementsTable.get(parentID);
				parent.appendChild(itemElement);
			}
			else
				organizationElement.appendChild(itemElement);

			// (need to append other metadata elements after child item elements in order to satisfy schema.)
			// add imsmd:location element (with original URL in Bod):
			String resource_path = path;
			if (resource_path.equals(""))
				resource_path = resource.getName() + "/";
			Element location = createLOMElement("location", baseURL + resource_path);
			location.setAttribute("type", "URI");
			itemElement.appendChild(location);
			// add LOM metadata:
			appendLOMMetadataToElement(resource, itemElement, errors);

		}
		else
			processChildren(resource, path); // necessary for bulk export of QTI from children only.
	}

	protected void generateFileMetadata()
	{
		for (Iterator iterator = fileList.iterator(); iterator.hasNext();)
		{
			FileData fd = (FileData) iterator.next();
			PrimaryKey containingResourceID = fd.resourceID; // ID of resource containing this file
			String path = fd.path;
			
			// Uploaded file: create file element, find parent resource element and append
			//  ditto for: intro, descrip, easywriter, textblock, imageblock, quicklink, collated files
			
			// create resource file element:
			Element fileElement = createFileElement(path);
			// find correct 'resource' item to append it to:
			Element resourceElement = (Element)resourceElementsTable.get(containingResourceID);
			resourceElement.appendChild(fileElement);
		}
		
		fileList.clear();
	}
	
	public Element createItemElement( Resource resource, List errors )
	{
		String identifier = resource.getResourceId().toString();
		Element item = createDefaultNSElement("item");
		item.setAttributeNS(DEFAULT_NAMESPACE_URI, "identifier", "ITEM-"+ identifier);
		item.setAttributeNS(DEFAULT_NAMESPACE_URI, "identifierref", "RESOURCE-"+ identifier);
		try {
			Element title = createDefaultNSElement("title", resource.getTitle());
			item.appendChild(title);
		} catch (Exception e) {
			String message = "Error adding title to metadata. " + e.getMessage();
			logger.warn(message);
			errors.add(message);
		}
		return item;
	}
	
	public void appendLOMMetadataToElement( Resource bodResource, Element parent, List errors )
	{
		// imsmd:lom
		Element imsmdlom = createLOMElement("lom");
		// imsmd:general
		Element imsmdgeneral = createLOMElement("general");
		try {
			// imsmd:title
			Element imsmdtitle = createLOMTitleElement( bodResource.getTitle() );
			imsmdgeneral.appendChild(imsmdtitle);
			
			// If user has not chosen to include description as a standalone HTML file,
			// then include it here in the manifest metadata:
			
			if ( !super.includeDescriptions ) {
				String description = bodResource.getDescription();
				if (!ignoreStrings.contains(description)) {
					// imsmd:description:
					Element imsmdesc = createLOMDescriptionElement(description);
					imsmdgeneral.appendChild(imsmdesc);
				}
			}
			
		} catch (BuildingServerException e) {
			String message = "Error adding title/description to metadata. " + e.getMessage();
			logger.warn(message);
			errors.add(message);
			return; // i.e give up altogether
		}
		//imsmd:keyword:
		try {
			BuildingSession session = BuildingSessionManagerImpl.getSession( bodResource );
			String[] keywords = session.getFieldValuesFromMetadata( "keywords" );
			if (keywords.length > 0)
			{
				String string = XMLMetadataUtils.getKeywordStringFromArray( keywords );
				Element keyword = createLOMKeywordElement(string);
				imsmdgeneral.appendChild(keyword);
			}
		} catch (BuildingServerException e) {
			String message = "Error adding keywords to metadata. " + e.getMessage();
			logger.warn(message);
			errors.add(message);
		}
		imsmdlom.appendChild(imsmdgeneral);
		parent.appendChild(imsmdlom);
		
	}
	
//	public void appendLOMMetadataToElement( UploadedFile ufile, Element parent )
//	{
//		// Method not currently used, may be able to add more fields to make it more useful...
//		// imsmd:lom
//		Element imsmdlom = createLOMElement("lom");
//		// imsmd:general
//		Element imsmdgeneral = createLOMElement("general");
//		// imsmd:title
//		Element imsmdtitle = createLOMTitleElement( ufile.getName() );
//		imsmdgeneral.appendChild(imsmdtitle);
//		
//		imsmdlom.appendChild(imsmdgeneral);
//		parent.appendChild(imsmdlom);
//		
//	}

	public Element createResourceElement(Resource resource, String href )
	{
		String identifier = resource.getPrimaryKey().toString();
		Element resourceElement = createDefaultNSElement("resource");
		resourceElement.setAttributeNS(DEFAULT_NAMESPACE_URI, "identifier", "RESOURCE-"+ identifier);
		// set standard type attribute for content intended for viewing in a browser: 
		resourceElement.setAttributeNS(DEFAULT_NAMESPACE_URI, "type","webcontent");
		if (href.length() > 1 && href.endsWith("/"))
			href = href.substring(0, href.length()-1);
		resourceElement.setAttributeNS(DEFAULT_NAMESPACE_URI, "href", href);

		return resourceElement;
	}

	private Element createFileElement(String href)
	{
		Element fileElement = createDefaultNSElement("file");
		fileElement.setAttributeNS(DEFAULT_NAMESPACE_URI, "href", href);
		
		return fileElement;
	}
	
	/**
	 * creates the default namespace element
	 * @param elementName - element name
	 * @return - returns the default namespace element
	 */
	private Element createDefaultNSElement(String elementName) {

		return getDocument().createElementNS(DEFAULT_NAMESPACE_URI, elementName);
	}
	
	private Element createDefaultNSElement(String elementName, String text) {
		
		Element element = createDefaultNSElement(elementName);
		element.setTextContent(text);
		
		return element;
	}

	private Element createLOMElement(String elementName) {

		Element imsmdlom = getDocument().createElementNS(IMSMD_NAMESPACE_URI, elementName);
		imsmdlom.setPrefix("imsmd");

		return imsmdlom;
	}
	
	private Element createLOMElement(String elementName, String text) {
		
		Element element = createLOMElement(elementName);
		element.setTextContent(text);
		
		return element;
	}
	
	private Element createLOMTitleElement(String title) {
		
		return createLOMElementWithLangstring("title", title, false);
	}
	
	private Element createLOMDescriptionElement(String description) {

		return createLOMElementWithLangstring("description", description, true);
	}

	private Element createLOMKeywordElement(String keyword) {

		return createLOMElementWithLangstring("keyword", keyword, false);
	}
	
	private Element createLOMElementWithLangstring(String elementName, String text, boolean wrapInCData) {
		
		Element element = createLOMElement(elementName);
		//imsmd:langstring
		Element imsmdlangstring = createLOMElement("langstring");
		//mdLangString.addAttribute("xml:lang", "en-US");
		if (wrapInCData) {
			CDATASection cdata = getDocument().createCDATASection(text);
			imsmdlangstring.appendChild(cdata);
		}
		else imsmdlangstring.setTextContent(text);
		
		element.appendChild(imsmdlangstring);

		return element;
	}
	
	private Comment createCommentElement( String text ) {
		
		return getDocument().createComment(" " + text + " ");
	}
	
	private void writeManifestToZip( Element element, String zipEntryName )
	{
		try {
			//create and add zip entry
			ZipEntry zipEntry = new ZipEntry( zipEntryName );
			getZipOutputStream().putNextEntry( zipEntry );
			XMLSerializer.out(element, "xml", getZipOutputStream());

			//close the zipEntry
			getZipOutputStream().closeEntry();
		} catch (Exception e) {
			String message = "Error adding IMS manifest to zip: " + e.getMessage();
			logger.warn(message);
			errors.add(message);
		}
    }

	private Document getDocument() {
		if (document != null)
			return document;
		else
			document = DOMFactory.newDocument();
		return document;
	}
}
