package org.bodington.server.resources;

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.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_v1p2.xsd";
	
	private Document document;
	// Need organization element to attach item elements to (hierarchical):
	private Element organizationsElement, organizationElement;
	// Need resources element; holds all resources (i.e flat):
	private Element resourcesElement;
	// Hashtable to keep track of XML elements representing resources/folders in order to attach nested file/folder
	// elements to correct parent element:
	private Hashtable elementTable;
	// Hashtable for filename to suffix mappings:
	private static Hashtable suffixes;

	protected void init( Request request, OutputStream outputStream )
	{
		super.init(request, outputStream);
		elementTable = new Hashtable();
		suffixes = new Hashtable();
		// need suffix to append to resource identifier to get unique id in manifest:
		suffixes.put(DESCRIPTION_FILE_SUFFIX, "-descrip");
		suffixes.put(INTRODUCTION_FILE_SUFFIX, "-intro");
		suffixes.put(HTML_FILE_SUFFIX, "-content");
		suffixes.put(QUICKLINK_FILE_SUFFIX, "-link");
	}
	
	public void packageResource( Request request, OutputStream outputStream ) {
		
		init(request, outputStream);
		
		Resource resource = request.getResource();
		
		if ( !resource.checkPermission( Permission.MANAGE ) )
		{
			errors.add("Resource not added, you need Manage access to use the Export tool");
			writeErrorFileToZip(formatErrorMessages());
			finishProcessing();
			return;
		}

		// 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 (i.e flat):
		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();
	}

	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 manifest metadata:
		Element metadata = createDefaultNSElement("metadata");
		//schema element
		Element schema = createDefaultNSElement("schema");
		schema.setTextContent("IMS Content");
		metadata.appendChild(schema);
		//schema version element
		Element schemaVersion = createDefaultNSElement("schemaversion");
		schemaVersion.setTextContent("1.1.4");
		metadata.appendChild(schemaVersion);
		// date generated
		Element date = createLOMElement("date");
		Element datetime = createLOMElement("datetime");
		DateFormat format = new SimpleDateFormat("dd MMM yyyy HH:mm:ss");
		datetime.setTextContent(format.format(new Date()));
		date.appendChild(datetime);
		metadata.appendChild(date);
		
		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");

		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 BuildingServerException
	 */
	public void processResource( Resource resource, String path )
	{
		if ( !resource.checkPermission( Permission.MANAGE ) )
		{
			errors.add("Resource not added, you need Manage access for " + path);
			return;
		}
		
		//TODO: check what happens with manifest when suite is empty at top level, etc, when no folder is generated?
		
		// create item element representing this resource:
		Element itemElement = createItemElement(resource, errors);
		// put it in the table in order to attach files/folders/child resources later:
		elementTable.put(resource.getResourceId(), itemElement);
		
		// call superclass to produce zip archive:
		super.processResource(resource, path);
		
		generateFileMetadata();
		

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

		// need to append LOM metadata after child item elements in order to satisfy schema:
		appendLOMMetadataToElement(resource, itemElement, errors);
	}

	protected void generateFileMetadata()
	{
		for (Iterator iterator = fileList.iterator(); iterator.hasNext();)
		{
			FileData fd = (FileData) iterator.next();
			PrimaryKey identifier = fd.identifier;
			String title = fd.title;
			String path = fd.path;
			boolean isFolder = fd.isFolder;
			
			// need a suffix in the identifier and identifierref attributes to differentiate
			// between intro/descrip, etc files associated with single resource:
			String suffix = "";
			if (null != suffixes.get(fd.fileSuffix))
				suffix = (String)suffixes.get(fd.fileSuffix);
			
			// 3 cases:
			// Resource?
			// Uploaded folder: create item, add to table, don't append yet
			// Uploaded file: create item, find parent/s and append, create resource element
			//  ditto for: intro, descrip, easywriter, textblock, imageblock, quicklink, collated files
			

			// create item element:
			Element itemElement = createItemElement(identifier + suffix, title);
			
			// if folder, put in item element table:
			if (fd.isFolder)
			{
				elementTable.put(identifier, itemElement);
				continue;
			}
			
			// if file, attach to parent/s:
			attachFileItemElementToParents(itemElement, fd);
			
			// create resource (and file) element:
			Element resourceElement = createResourceElement(identifier + suffix, path, isFolder);
			resourcesElement.appendChild(resourceElement);
		}
		
		fileList.clear();
	}
	
	/**
	 * Find all parent elements (resource or series of folders) for a file, and attach them all correctly.
	 * It's necessary to retrospectively append parents to each other in order to avoid adding
	 *  metadata entries for empty folders. 
	 * @param file Uploaded File being processed
	 * @param Element Metadata element representing the file
	 */
	
	private void attachFileItemElementToParents(Element itemElement, FileData filedata)
	{
		Element child = itemElement;
		PrimaryKey parentID = filedata.parentID;

		try {
			while (null != parentID && elementTable.containsKey(parentID)) // i.e parent is a folder, root folders not included in elementTable.
			{
				Element parent = (Element)elementTable.get( parentID );
				if (!(child.getParentNode() == parent)) // to avoid removing and re-appending
					parent.appendChild( child );
				// find parent UploadedFile in order to get it's parent ID:
				UploadedFile parentFile = UploadedFile.findUploadedFile( parentID );
				parentID = parentFile.getParentUploadedFileId();
				//now view current parent as a child:
				child = parent;
			}

			//append file or topmost folder to resource
//			parentID = filedata.resourceID;// not same as UploadedFile ID...
			Element resource = (Element)elementTable.get( filedata.resourceID );
			resource.appendChild( child );

		} catch (Exception e) {
			String message = "Error adding folder details to metadata. " + e.getMessage();
			logger.warn(message);
			errors.add(message);
		}	

	}
	
	//TODO: tidy up these two seemingly unrelated createItemElement methods:
	
	public Element createItemElement( Resource resource, List errors )
	{
		String identifier = resource.getResourceId().toString();
		Element item = createDefaultNSElement("item");
		item.setAttributeNS(DEFAULT_NAMESPACE_URI, "identifier", "ITEM-"+ identifier);
		try {
			Element title = createDefaultNSElement("title");
			title.setTextContent(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 Element createItemElement( String identifier, String title )
	{
		Element item = createDefaultNSElement("item");
		item.setAttributeNS(DEFAULT_NAMESPACE_URI, "identifier", "ITEM-"+ identifier);
//		if ( folder )
		item.setAttributeNS(DEFAULT_NAMESPACE_URI, "identifierref", "RESOURCE-"+ identifier);
		Element titleElement = createDefaultNSElement("title");
		titleElement.setTextContent(title);
		item.appendChild(titleElement);
		
		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);
			// imsmd:description:
			String description = bodResource.getDescription();
			if (!ignoreStrings.contains(description))
			{
				Element imsmdesc = createLOMDescriptionElement(description);
				imsmdgeneral.appendChild(imsmdesc);
			}
			// imsmd:source (i.e URL in Bod)
			String originalpath = bodResource.getFullName();
			Element source = createLOMSourceElement(originalpath);
			imsmdgeneral.appendChild(source);
			
		} 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( String path, UploadedFile file )
	{
		String identifier = file.getUploadedFileId().toString();
		String href = "";
		try {
			href = path + file.getNameInResource();
		} catch (Exception e) {
			String message = "Error adding href to resource element in metadata. " + e.getMessage();
			logger.warn(message);
			errors.add(message);
		}

		return createResourceElement(identifier, href, file.isFolder());
	}
	
	public Element createResourceElement(String identifier, String href, boolean folder )
	{
		Element resourceElement = createDefaultNSElement("resource");
		resourceElement.setAttributeNS(DEFAULT_NAMESPACE_URI, "identifier", "RESOURCE-"+ identifier);
		if (folder)
			resourceElement.setAttributeNS(DEFAULT_NAMESPACE_URI, "type","folder");
		else
			resourceElement.setAttributeNS(DEFAULT_NAMESPACE_URI, "type","file");

		resourceElement.setAttributeNS(DEFAULT_NAMESPACE_URI, "href", href);

		Element fileElement = createDefaultNSElement("file");
		fileElement.setAttributeNS(DEFAULT_NAMESPACE_URI, "href", href);
		resourceElement.appendChild(fileElement);

		return resourceElement;
	}
	
	/**
	 * creates the default namespace element
	 * @param elename - element name
	 * @return - returns the default namespace element
	 */
	private Element createDefaultNSElement(String elename) {

		return getDocument().createElementNS(DEFAULT_NAMESPACE_URI, elename);
	}

	private Element createLOMElement(String elename) {

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

		return imsmdlom;
	}
	
	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 createLOMSourceElement(String source) {
		
		return createLOMElementWithLangstring("source", source, 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 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;
	}
}
