package org.bodington.server.resources;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.zip.ZipEntry;

import org.apache.log4j.Logger;
import org.w3c.dom.DOMException;
import org.w3c.dom.Element;
import org.w3c.dom.Document;
import org.w3c.dom.Text;

import org.bodington.database.PrimaryKey;
import org.bodington.server.BuildingServerException;
import org.bodington.server.BuildingSession;
import org.bodington.server.BuildingSessionManagerImpl;
import org.bodington.server.events.ExportEvent;
import org.bodington.server.realm.Permission;
import org.bodington.util.Properties;
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 boolean recursive;
	private Document document;
	// Need organization element to attach item elements to (hierarchical):
	Element organizationsElement, organizationElement;
	// Need resources element; holds all resources (i.e flat):
	Element resourcesElement;
	// Hashtable to keep track of XML elements representing resources/folders in order to attach nested file/folder
	// elements to correct parent element:
	Hashtable elementTable;

	protected void init(OutputStream outputStream) {
		super.init(outputStream);
		elementTable = new Hashtable();
	}
	
	public void packageResource( Resource resource, OutputStream outputStream ) {
		
		init(outputStream);
		
		if ( !resource.checkPermission( Permission.MANAGE ) )
			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);
		// need to reference it later:
		elementTable.put("organization", organizationElement);
		organizationsElement.appendChild(organizationElement);
		// Need resources element; holds all resources (i.e flat):
		resourcesElement = createDefaultNSElement("resources");
		// need to reference it later:
		elementTable.put("resources", resourcesElement);
		
		// 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");
		setNodeValue(schema, "IMS Content");
		metadata.appendChild(schema);
		//schema version element
		Element schemaVersion = createDefaultNSElement("schemaversion");
		setNodeValue(schemaVersion, "1.1.4");
		metadata.appendChild(schemaVersion);
		
		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.
	 * @param resource
	 * @param path
	 * @param containingElement
	 * @return
	 * @throws BuildingServerException
	 */
	public void processResource( Resource resource, String path )
	{
		if ( !resource.checkPermission( Permission.MANAGE ) )
			return;
		
		// 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);
		
		// 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);
		
		// call superclass to produce zip archive:
		super.processResource(resource, path);

		if ( isRecursive() ) {
			try {
				//recursively get child resources to process:
				Enumeration children = resource.findChildren();
				while (children.hasMoreElements()) {
					Resource child = (Resource) children.nextElement();
					processResource(child, path + child.getName() + "/");
				}
			} catch (BuildingServerException e) {
				String message = "Error in generating child resource metadata: " + e.getMessage();
				errors.add(message);
				logger.info(message, e);
			}
		}
		
		// need to append LOM metadata after child item elements in order to satisfy schema:
		appendLOMMetadataToElement(resource, itemElement, errors);
	}
	
	protected boolean processResourceIntroduction(Resource resource, String path) {

		if (super.processResourceIntroduction(resource, path)) // ie. introduction.html has been appended to zip.
		{
			PrimaryKey resourceID = resource.getResourceId();
			String identifier = resourceID.toString() + "-intro";
			// create item element for introduction:
			Element item = createItemElement(identifier, "Introduction", false);
			Element parent = (Element)elementTable.get(resourceID);
			parent.appendChild(item);

			// create resource (and file) element for introduction.html:
			Element resourceElement = createResourceElement(path, identifier, path + INTRODUCTION_FILENAME, false);
			resourcesElement.appendChild(resourceElement);
		}

		return true;
	}
	
	protected boolean processResourceDescription(Resource resource, String path) {

		// (description is also added to manifest, but not really much use there.)
		if (super.processResourceDescription(resource, path)) // ie. description.html has been appended to zip.
		{
			PrimaryKey resourceID = resource.getResourceId();
			String identifier = resourceID.toString() + "-descrip";
			// create item element for description:
			Element item = createItemElement(identifier, "Description", false);
			Element parent = (Element)elementTable.get(resourceID);
			parent.appendChild(item);

			// create resource (and file) element for description.html:
			Element resourceElement = createResourceElement(path, identifier, path + DESCRIPTION_FILENAME, false);
			resourcesElement.appendChild(resourceElement);
		}

		return true;
	}
	
	protected void processUploadedFiles(Resource resource, String path) {

		// call superclass to produce zip archive:
		super.processUploadedFiles(resource, path);
		// method in superclass calls processUploadedFle() for each file/folder, overridden in this class...
	}
	
	protected void processUploadedFile( UploadedFile f, String path, File source_filestore ) {
		
		// create item element for file/folder:
		Element item = createItemElement(f);

		// find whether parent element is a resource or folder???
		// in both cases there will already be an element:
		PrimaryKey parentID = f.getParentUploadedFileId();

		if ( !elementTable.containsKey( parentID )) // no containing folder item element
		{
			parentID = f.getResourceId(); // get resource item element instead
		}
		
		Element parent = (Element)elementTable.get( parentID );
		parent.appendChild( item );

		if ( f.isFolder() )
		{ // need to keep track of it to use when we find subfolders/files:
			elementTable.put(f.getUploadedFileId(), item);
		}
		else // it's a file, it also needs a resource element:
		{
			// create resource (and file) element for file:
			Element resourceElement = createResourceElement(path, f);
			resourcesElement.appendChild(resourceElement);
		}
		
		// call superclass to produce zip archive:
		super.processUploadedFile(f, path, source_filestore);

	}
	
	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");
			setNodeValue(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 Element createItemElement( UploadedFile file )
	{
		String identifier = file.getUploadedFileId().toString();
		return createItemElement(identifier, file.getName(), file.isFolder());
	}
	
	public Element createItemElement( String identifier, String title, boolean folder )
	{
		// Create item element, metadata, append to organization element:
		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");
		setNodeValue(titleElement, 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:		
			Element imsmdesc = createLOMDescriptionElement(bodResource.getDescription());
			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" );
			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(path, identifier, href, file.isFolder());
	}
	
	public Element createResourceElement( String path, 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);
	}
	
	private Element createLOMDescriptionElement(String description) {

		return createLOMElementWithLangstring("description", description);
	}

	private Element createLOMKeywordElement(String keyword) {

		return createLOMElementWithLangstring("keyword", keyword);
	}
	
	private Element createLOMElementWithLangstring(String elementName, String text) {
		
		Element element = createLOMElement(elementName);
		//imsmd:langstring
		Element imsmdlangstring = createLOMElement("langstring");
		//mdLangString.addAttribute("xml:lang", "en-US");
		setNodeValue(imsmdlangstring, text);		
		element.appendChild(imsmdlangstring);

		return element;
	}
	
	private void setNodeValue( Element parent, String data ) {
		// org.w3c.dom.Element setNodeValue() doesn't seem to work?
		Text textNode = getDocument().createTextNode( data );
		parent.appendChild( textNode );
	}
	
	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 boolean isRecursive() {
		return recursive;
	}

	public void setRecursive( boolean recursive ) {
		this.recursive = recursive;
	}

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