package org.bodington.server.resources;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipOutputStream;

import org.apache.log4j.Logger;
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.xml.DOMFactory;
import org.bodington.xml.XMLMetadataUtils;
import org.bodington.xml.XMLSerializer;

public class ContentPackager {
	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 includeIMSManifest;
	private boolean recursive;
	private Document document;
	private ZipOutputStream zipOutputStream;
	
	public void packageResource( Resource bodingtonResource, OutputStream outputStream ) {
		
		setZipOutputStream(new ZipOutputStream( outputStream ));

		// XML for manifest is generated whether user wants it included in zip archive
		// or not, as it makes the code simpler...
		
		if ( !bodingtonResource.checkPermission( Permission.MANAGE ) )
			return;
		
		List errors = new ArrayList();

		// Need to create upper level metadata elements:
		Element manifestElement = createManifestElement(bodingtonResource);
		// Need organizations element:
		Element organizationsElement = createOrganizationsElement(bodingtonResource);
		manifestElement.appendChild(organizationsElement);
		// Need organization element to attach item elements to (hierarchical):
		Element organizationElement = createOrganizationElement(bodingtonResource);
		organizationsElement.appendChild(organizationElement);
		// Need resources element; holds all resources (i.e flat):
		Element resourcesElement = createDefaultNSElement("resources");
		
		try {
			// create and append nested item elements, and resource elements:
			createItemandResourceElements(bodingtonResource, "", organizationElement, resourcesElement, errors);
			manifestElement.appendChild(resourcesElement);
		} catch (BuildingServerException e) {
			String message = "Error in accessing data in order to build archive: " + e.getMessage();
			errors.add(message);
			logger.info(message, e);
		}
		
		try {
			if ( !errors.isEmpty() )
			{
				String message = formatErrorMessage(errors);
				writeErrorFileToZip(message);
			}
			if ( isIncludeIMSManifest() ) {
				writeManifestToZip(manifestElement, "imsmanifest.xml");
			}
		} catch (Exception e) {
			logger.info("Failed to write manifest/error file to content package.", e);
		}
		
		ExportEvent event = new ExportEvent(ExportEvent.EVENT_ADD_RESOURCE_TO_ZIP, bodingtonResource);
        event.save();
        
		closeZipOutputStream();
	}

	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 bodResource
	 * @param path
	 * @param containingElement
	 * @return
	 * @throws BuildingServerException
	 */
	public void createItemandResourceElements( Resource bodResource, String path, Element containingElement, Element resourcesElement, List errors )
	  throws BuildingServerException
	{
		if ( !bodResource.checkPermission( Permission.MANAGE ) )
			return;

		// create item element representing this resource:
		Element itemElement = createItemElement(bodResource, errors);
		containingElement.appendChild(itemElement);

		// Obtain a list of entries in the root directory and its descendants (exclude deleted files/folders):
		BuildingSession session = BuildingSessionManagerImpl.getSession( bodResource );
		UploadedFileSummary[] summaries = session.getUploadedFileSession().getFileAndDescendentSummaries( "/", true );
		// Obtain the location of the filestore for the entries
		File source_filestore = bodResource.getWebPublishFolder();

		// Files and folders are represented as a flat summaries array.
		// Hashtable keeps track of XML elements representing folders in order to attach nested file/folder
		// elements to correct parent element:
		Hashtable folderElements = new Hashtable();

		for ( int summaryIndex = 0; summaryIndex < summaries.length; summaryIndex++ )
		{
			PrimaryKey fileID = summaries[summaryIndex].getUploadedFileId();

			if ( fileID == null ) continue; //no root folder, so no files.

			UploadedFile f = UploadedFile.findUploadedFile( fileID );
			PrimaryKey parentID = f.getParentUploadedFileId();

			if ( parentID == null ) continue; // root folder

			// create item element for file/folder:
			Element item = createItemElement(f);

			if ( folderElements.containsKey( parentID ))
			{ // it belongs in a folder:
				Element parent = (Element)folderElements.get( parentID );
				parent.appendChild( item );
			}
			else // must be attached to root, so add it to itemElement
			{ 
				itemElement.appendChild(item);
			}

			if ( f.isFolder() )
			{ // need to keep track of it to use when we find subfolders/files:
				folderElements.put(fileID, 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);
				
				try {
					// add file to zip archive:
					File source = new File(source_filestore, f.getRealNameInResource());
					FileInputStream inputStream = new FileInputStream( source );
					writeToZip(inputStream, path + f.getNameInResource());
					logger.debug("Added to zip: " +path + f.getNameInResource());
				} catch (Exception e) {
					String message = "Error in adding file to zip archive: " + e.getMessage();
					errors.add(message);
					logger.info(message, e);
				}
			}
		}

		if ( isRecursive() ) {
			try {
				//recursively get child resources:
				Enumeration children = bodResource.findChildren();
				while (children.hasMoreElements()) {
					Resource child = (Resource) children.nextElement();
					createItemandResourceElements(child, path + child.getName() + "/", itemElement, resourcesElement, errors);
					ExportEvent event = new ExportEvent(ExportEvent.EVENT_ADD_RESOURCE_TO_ZIP, child);
			        event.save();
				}
			} catch (BuildingServerException e) {
				String message = "Error in exporting child resource content: " + e.getMessage();
				errors.add(message);
				logger.info(message, e);
			}
		}
		// need to append LOM metadata after child item elements in order to satisfy schema:
		appendLOMMetadataToElement(bodResource, itemElement, errors);
	}
	
	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();
		// Create item element, metadata, append to organization element:
		Element item = createDefaultNSElement("item");
		item.setAttributeNS(DEFAULT_NAMESPACE_URI, "identifier", "ITEM-"+ identifier);
		if ( !file.isFolder() )
			item.setAttributeNS(DEFAULT_NAMESPACE_URI, "identifierref", "RESOURCE-"+ identifier);
		Element title = createDefaultNSElement("title");
		setNodeValue(title, file.getName());
		item.appendChild(title);
		
		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 ) throws BuildingServerException
	{
		//TODO Should there be resource elements for folders?
		String identifier = file.getUploadedFileId().toString();
		Element resourceElement = createDefaultNSElement("resource");
		resourceElement.setAttributeNS(DEFAULT_NAMESPACE_URI, "identifier", "RESOURCE-"+ identifier);
		if (file.isFolder())
			resourceElement.setAttributeNS(DEFAULT_NAMESPACE_URI, "type","folder");
		else
			resourceElement.setAttributeNS(DEFAULT_NAMESPACE_URI, "type","file");
			
		String href = path + file.getNameInResource();
		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 writeToZip( InputStream inputStream, String zipEntryName ) throws IOException
    {

		try {
			//create and add zip entry
			ZipEntry zipEntry = new ZipEntry( zipEntryName );
			getZipOutputStream().putNextEntry( zipEntry );
			//write  bytes from the inputStream
			byte[] readBuffer = new byte[8192];
			int bytesIn = 0;
			while( ( bytesIn = inputStream.read( readBuffer ) ) != -1 ) {
				getZipOutputStream().write( readBuffer, 0, bytesIn );
			}
		} finally {
			//close the stream and zipEntry
			inputStream.close();
			getZipOutputStream().closeEntry();
		}
    }
	
	private void writeManifestToZip( Element element, String zipEntryName ) throws Exception
	{
		//create and add zip entry
		ZipEntry zipEntry = new ZipEntry( zipEntryName );
		getZipOutputStream().putNextEntry( zipEntry );
		XMLSerializer.out(element, "xml", getZipOutputStream());

		//close the zipEntry
		getZipOutputStream().closeEntry();
    }
	
	private void writeErrorFileToZip( String message ) throws IOException
	{
		ByteArrayInputStream inputStream = new ByteArrayInputStream(message.getBytes());
		writeToZip(inputStream, "errors.txt");
	}

	private boolean isIncludeIMSManifest() {
		return includeIMSManifest;
	}

	public void setIncludeIMSManifest( boolean includeIMSManifest ) {
		this.includeIMSManifest = includeIMSManifest;
	}

	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;
	}

	private ZipOutputStream getZipOutputStream() {
		return zipOutputStream;
	}

	private void setZipOutputStream(ZipOutputStream zipOutputStream) {
		this.zipOutputStream = zipOutputStream;
	}
	
	private void closeZipOutputStream()
	{
		try {
			try {
				getZipOutputStream().close();
			} catch (ZipException ze) {
				// probably because there was no content to zip up...
				writeErrorFileToZip("Error occurred in creating archive, possibly because no content was found.");
				getZipOutputStream().close();
			}
		} catch (IOException e) {
			logger.info("Error in attempting to close ZipOutputStream.", e);
		}		
	}
	
	private String formatErrorMessage(List errors) {

		StringWriter swriter = new StringWriter();
		PrintWriter writer = new PrintWriter(swriter);
		for (Iterator iterator = errors.iterator(); iterator.hasNext();) {
			writer.println((String) iterator.next());
		}
		return swriter.toString();
	}
}
