package org.bodington.server.resources;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
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.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipOutputStream;

import org.apache.log4j.Logger;
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.servlet.Request;
import org.bodington.servlet.facilities.DocumentFacility;
import org.bodington.util.BodingtonURL;

public class ContentZipper {

	private Request request;
	private ZipOutputStream zipOutputStream;
	protected List errors;
	protected List ignoreStrings;
	protected List fileList;
	private boolean recursive;
	private boolean includeDescriptions;
	private boolean includeIntroductions;
	private boolean includeQuickLinks;
	private boolean includeInternalQuickLinks;
	private boolean collateDocumentSections;
	private boolean includeContentListing;
	private boolean combineImageBlock;

	protected static final String HTML_FILE_SUFFIX=".html";
	protected static final String INTRODUCTION_FILE_SUFFIX="-intro.html";
	protected static final String DESCRIPTION_FILE_SUFFIX="-descrip.html";
	protected static final String QUICKLINK_FILE_SUFFIX = ".url";

	private Logger logger = Logger.getLogger(ContentZipper.class);
	
	/**
	 * Class to record metadata regarding the file/s generated for inclusion in the zip archive.
	 * As more user options are included in the export functionality, so the files and names
	 *  actually included in the zip archive get too complex to track easily.
	 *  Used by subclass which generates IMS manifest file of metadata.
	 * @author sers0022
	 *
	 */
	protected class FileData
    {
		protected PrimaryKey identifier;
		protected PrimaryKey parentID;
		protected PrimaryKey resourceID; // used when object is UploadedFile.
		protected String path; // path to resource/folder/file, includes filename.
		protected String filename;
		protected String fileSuffix; // to differentiate between intro/descrip, used by subclass creating XML manifest.
		protected String title;
		protected boolean isFolder;
        
		/**
         * Constructor for Uploaded Files. 
         * @param id
         * @param path
         * @param filename
         */
        private FileData (UploadedFile ufile, String path)
        {
        	this.identifier = ufile.getUploadedFileId();
        	this.parentID = ufile.getParentUploadedFileId();
        	this.resourceID = ufile.getResourceId();
			this.filename = ufile.getName();
			this.fileSuffix = "";
			this.title = ufile.getName(); // same as filename
			this.isFolder = ufile.isFolder();
        	try {
				this.path = path + ufile.getURLInResource();
			} catch (BuildingServerException e) {}
        }
        
        /**
         * Constructor for files generated from resource content 
         * @param id
         * @param path
         * @param fileName
         */
        private FileData(PrimaryKey id, String path, String fileName, String fileSuffix)
        {
        	this.identifier = id;
//        	this.parentID needs to be null, uses resource id
        	this.resourceID = id;
            // need suffix to differentiate between intro and descrip:
            this.fileSuffix = fileSuffix;
            this.filename = fileName + fileSuffix;
        	this.path = path + fileName + fileSuffix;
            this.title = fileName + fileSuffix;
			this.isFolder = false;
        }

        public String toString()
        {
        	return "identifier: "+identifier
        	+" parentID: " + parentID
        	+" resourceID: " + resourceID
        	+" path: " + path
            +" fileName: " + filename
            +" fileSuffix: " + fileSuffix
            +" title: " + title
        	+" isFolder: " + isFolder;
        } 
    }
	
	/**
	 * Set export options from the request params.
	 * @param request
	 */
	public void setRequestOptions()
	{
		Enumeration param_names = request.getParameterNames();
		while (param_names.hasMoreElements()) {
			String param_name = (String) param_names.nextElement();
			String param_value = request.getParameter(param_name);
			logger.debug(param_name + ": " + param_value);
		}

		this.recursive = parseParam("recursive");
		this.includeDescriptions = parseParam("includeDescriptions");
		this.includeIntroductions = parseParam("includeIntroductions");
		this.includeQuickLinks = parseParam("includeQuickLinks");
		this.includeInternalQuickLinks = parseParam("includeInternalQuickLinks");
		this.collateDocumentSections = parseParam("collateDocumentSections");
		this.includeContentListing = parseParam("includeContentListing");
		this.combineImageBlock = parseParam("combineImageBlock");
	}
	
	/**
	 * Get boolean from request param.
	 * @param request
	 * @param param_name
	 * @return boolean
	 */
	private boolean parseParam(String param_name)
	{
		boolean value = Boolean.parseBoolean(request.getParameter(param_name));
		logger.debug(param_name + ": " + value);
		return value;
	}
	
	protected void init(Request request, OutputStream outputStream ) {
		
		setRequest(request);
		setRequestOptions();
		setZipOutputStream(new ZipOutputStream( outputStream ));
		errors = new ArrayList();
		fileList = new Vector();
		ignoreStrings = Arrays.asList(new String[] {"", "<!-- -->", "<!>", "<p>&nbsp;</p>", "<!-- No Introduction -->", "<!-- No Description -->"});
	}
	
	public void packageResource( Request request, OutputStream outputStream ) {
		
		init( request, outputStream );
		
		Resource resource = request.getResource();
		
		if ( resource.checkPermission( Permission.MANAGE ) )
			processResource(resource, "");
		else
			errors.add("Resource not added, you need Manage access to use the Export tool");
		
		if ( !errors.isEmpty() )
			writeErrorFileToZip(formatErrorMessages());
			
		finishProcessing();
	}

	/**
	 * Adds content to archive for a given resource.
	 * Is called recursively as it walks down the tree.<br/>
	 * @param resource
	 * @param path
	 * @param containingElement
	 * @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;
		}
		
		// handle special cases (non-generic behaviour) here:
		if (resource instanceof QuickLink)
			processQuickLink((QuickLink)resource, path);
		else if (resource instanceof TextBlockResource)
			processTextBlock((TextBlockResource)resource, path);
		else if (resource instanceof ImageBlockResource)
			processImageBlock((ImageBlockResource)resource, path);
		else if (resource instanceof EasyEditResource)
			processEasyWriter((EasyEditResource)resource, path);
		else if (resource.getResourceType() == Resource.RESOURCE_DOCUMENT)
			processStructuredDocument(resource, path);
		else
			processGenericResource(resource, path);
		
		ExportEvent event = new ExportEvent(ExportEvent.EVENT_ADD_RESOURCE_TO_ZIP, resource);
		event.save();
		
		if ( recursive ) {
			processChildren(resource, path);
		}
	}

	/**
	 * Separated processing of children in order to provide override point in subclass.
	 * @param resource
	 * @param path
	 */
	protected void processChildren(Resource resource, String path) {
		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 exporting child resource content: " + e.getMessage();
			errors.add(message);
			logger.info(message, e);
		}
	}

	/**
	 * Generic behaviour: include uploaded files, and descrip/intro if option set.
	 * @param resource
	 * @param path
	 */
	private void processGenericResource(Resource resource, String path)
	{
		String name = resource.getName();
		if (includeDescriptions)
			processResourceDescription(resource, path, name, DESCRIPTION_FILE_SUFFIX);
		if (includeIntroductions)
			processResourceIntroduction(resource, path, name, INTRODUCTION_FILE_SUFFIX);

		// uploaded files:
		processUploadedFiles(resource,path);
	}
	
	/**
	 * Include target URL as .url file, uploaded files, descrip & intro.
	 * @param quicklink
	 * @param path
	 */
	private void processQuickLink(QuickLink quicklink, String path)
	{
		// if flag set to exclude all, return:
		if (!includeQuickLinks)
			return;
		
		// if url hasn't been set: (exclude internal links, where url is null)
		if (quicklink.getURL() == null && !quicklink.isBodURL())
			return;

		// is link is internal and flag to include internal links is not set, return:
		if (quicklink.isBodURL() && !includeInternalQuickLinks)
			return;
		
		// Generate description, intro and any files:
		//TODO: Handle case where link is to uploaded file in QuickLink itself?
		processGenericResource(quicklink, path);
		
		String url;
		if (quicklink.isBodURL())
			url = new BodingtonURL(getRequest()).getResourceUrl(quicklink.getResourceIdLink(), true);
		else
			url = quicklink.getURL();

		try
		{
			String filename = quicklink.getName();
			writeToZip(new ByteArrayInputStream(url.getBytes("UTF-8")), path+filename+QUICKLINK_FILE_SUFFIX);
			fileList.add(new FileData(quicklink.getResourceId(), path, filename, QUICKLINK_FILE_SUFFIX));
		}
		catch (Exception e)
		{
			String message = "Failed to add quicklink: "+ e.getMessage();
			errors.add(message);
			logger.info(message, e);
		}	
		
	}
	
	/**
	 * EasyWriter content is held in intro, always included regardless of intro flag, uses custom filename.
	 * @param resource
	 * @param path
	 */
	private void processEasyWriter(EasyEditResource resource, String path)
	{
		String name = resource.getName();
		// do summat with intro and description:
		if (includeDescriptions)
			processResourceDescription(resource, path, name, DESCRIPTION_FILE_SUFFIX);
		// always include intro regardless of flag, use custom filename:
		processResourceIntroduction(resource, path, name, HTML_FILE_SUFFIX);

		// uploaded files:
		processUploadedFiles(resource,path);
		
	}

	/**
	 * Only processes description field, always included regardless of flag, uses custom filename.
	 * @param resource
	 * @param path
	 */
	private void processTextBlock(TextBlockResource resource, String path)
	{	
		String name = resource.getName();
		// do summat with description only:
		processResourceDescription(resource, path, name, HTML_FILE_SUFFIX);
	}
	

	/**
	 * Combine title (if option set), image and description into single HTML file.
	 * Also adds image properties. If combine option not set, processes generically.
	 * @param resource
	 * @param path
	 */
	private void processImageBlock(ImageBlockResource imageblock, String path)
	{	
		String name = imageblock.getName();
		
		//TODO writeHTMLFile() uses resource title for title tag regardless of displaytitle setting.
		if (combineImageBlock)
		{
			try {
				StringBuffer buf = new StringBuffer();
				if (imageblock.isDisplayTitle())
					buf.append("<h3>" + imageblock.getTitle() + "</h3>\n");
				
				String url = imageblock.getSourceURL();
				// imageblock uses relative urls from parent, and include resource name:
				url = url.replace("./"+name+"/", "");
				String height = imageblock.getImageHeight();
				String width = imageblock.getImageWidth();
				String alttext = imageblock.getAltTagText();
				String linkHREF = imageblock.getLinkHREF();
				if (null != linkHREF && !linkHREF.equals(""))
					buf.append("<a href=\""+linkHREF+"\">\n");
				buf.append("<img src=\""+url+"\" alt=\""+ alttext+"\" height=\""+height+"\" width=\""+width+"\">\n");
				if (null != linkHREF && !linkHREF.equals(""))
					buf.append("</a>\n");
				
				if (imageblock.isDisplayDescription())
					buf.append(imageblock.getDescription());
				
				createHTMLFile(imageblock, buf.toString(), path, name, HTML_FILE_SUFFIX);
				
				processUploadedFiles(imageblock, path);
				
			} catch (Exception e) {
				String message = "Error in combining imageblock components into single file: " + e.getMessage();
				errors.add(message);
				logger.info(message, e);
				// processgenericResource()?
			}
		}
		else
			processGenericResource(imageblock, path);

	}
	
	/**
	 * Check option to include sections (uploaded files) in a single file.
	 * If set, collate all document sections into a single HTML file.
	 * Also need to add all non-section related files & folders to archive.
	 * @param resource
	 * @param path
	 */

	private void processStructuredDocument(Resource structdoc, String path)
	{
		//exclude hidden section files; provide parameter?
		// allow contents listing in separate HTML file if not collated?
		//TODO: metadata should include section titles?
		
		String name = structdoc.getName();
		
		if (collateDocumentSections)
		{
			// intro and description first, will appear first in metadata
			if (includeDescriptions)
				processResourceDescription(structdoc, path, name, DESCRIPTION_FILE_SUFFIX);
			if (includeIntroductions)
				processResourceIntroduction(structdoc, path, name, INTRODUCTION_FILE_SUFFIX);

			DocumentFacility facility = (DocumentFacility) ResourceUtils.getFacility(structdoc);
			createCollatedSectionsFile(structdoc, facility, path);
			//process files that aren't sections:
			processStructuredDocumentFiles(structdoc, facility, path);
		}
		else
			processGenericResource(structdoc, path);
	}

	/**
	 * Collate all document sections into a single HTML file.
	 * @param structdoc
	 * @param path
	 */
	
	protected void createCollatedSectionsFile(Resource structdoc, DocumentFacility facility, String path)
	{
		String name = structdoc.getName();
		StringWriter swrite = new StringWriter();
		// writer to write document section content:
		PrintWriter writer = new PrintWriter(swrite);
		// buffer to hold index listing
		StringBuffer buf = new StringBuffer();
		//TODO: find better way with Writers and buffer, etc?
        
		try {
			// output content of sections, and get set of HTML links for contents listing:
			List titleLinks = facility.outputSections(writer, BuildingSessionManagerImpl.getSession(structdoc));
			if (titleLinks.size() == 0)
				return;
			
			if (includeContentListing && titleLinks.size() > 1) {
				buf.append("<h3>Contents</h3>\n");
				buf.append("<ul>\n");
				for (Iterator iterator = titleLinks.iterator(); iterator.hasNext();)
					buf.append("<li>" + iterator.next() + "</li>\n");
				buf.append("</ul>\n");
			}
			buf.append(swrite.toString());
			createHTMLFile(structdoc, buf.toString(), path, name, HTML_FILE_SUFFIX);
		} catch (Exception e) {
			String message = "Error in collating document sections into single file: " + e.getMessage();
			errors.add(message);
			logger.info(message, e);
		}
		writer.close();

	}

	/**
	 * Add files that aren't document sections to zip archive.
	 * There may be additional images, etc that are used in document content.
	 * @param resource
	 * @param path
	 */
	private void processStructuredDocumentFiles(Resource resource, DocumentFacility facility, String path)
	{
		try {
			// Obtain a list of entries in the root directory and its descendants (exclude deleted files/folders):
			BuildingSession session = BuildingSessionManagerImpl.getSession( resource );
			// Files and folders are represented as a flat summaries array:
			UploadedFileSummary[] summaries = session.getUploadedFileSession().getFileAndDescendentSummaries( "/", true );
			// Obtain the location of the filestore for the entries
			File source_filestore = resource.getWebPublishFolder();
			for ( int summaryIndex = 0; summaryIndex < summaries.length; summaryIndex++ )
			{
				UploadedFileSummary summary = summaries[summaryIndex];
				if (summary.getParentUploadedFileId() == null) // root folder
					continue;
				if (null == facility.parseFileName(summary)) // check whether file represents numbered section content
				{
					UploadedFile f = UploadedFile.findUploadedFile( summary.getUploadedFileId() );
					processUploadedFile(f, path, source_filestore);
				}
			}
		} catch (Exception e) {
			String message = "Error adding additional files in Structured Document: " + e.getMessage();
			errors.add(message);
			logger.info(message, e);
		}
	}
	
	/**
	 * Create HTML file from resource description. </br>
	 * @param resource
     * @param path path to add to in zip archive.
     * @param filename
	 */
	
	private void processResourceDescription( Resource resource, String path, String filename, String fileSuffix )
	{
		try {
			// if empty description, then return:
			String description = resource.getDescription();
			if ( ignoreStrings.contains(description))
				return;

			createHTMLFile(resource, description, path, filename, fileSuffix);

		} catch (Exception e) {
			String message = "Error in exporting description as file: " + e.getMessage();
			errors.add(message);
			logger.info(message, e);
		}
	}
	
	/**
	 * Create HTML file from resource introduction. </br>
	 * @param resource
     * @param path path to add to in zip archive.
     * @param filename
	 */
	
	private void processResourceIntroduction( Resource resource, String path, String filename, String fileSuffix )
	{
		try {
			// if empty intro, then return:
			String intro = resource.getIntroduction();
			if ( ignoreStrings.contains(intro))
				return;

			createHTMLFile(resource, intro, path, filename, fileSuffix);

		} catch (Exception e) {
			String message = "Error in exporting introduction as file: " + e.getMessage();
			errors.add(message);
			logger.info(message, e);
		}
	}

	private void createHTMLFile(Resource resource, String content, String path, String filename, String fileSuffix)
	throws IOException, BuildingServerException, FileNotFoundException
	{
		File tempfile = File.createTempFile(filename, null);
		PrintWriter writer = new PrintWriter(new FileWriter(tempfile));
		writer.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">");
		writer.println("<html>");
		writer.println("<head>");
		writer.println("<title>"+ resource.getTitle() +"</title>");
		writer.println("<meta name=bod_path value=\"" + resource.getFullName() + "\" >");
		writer.println("<meta name=bod_resourceid value=\"" + resource.getResourceId() + "\" >");
		writer.println("</head>");
		writer.println("<body>");
		writer.println(content);
		writer.println("</body>");
		writer.println("</html>");
		writer.close();
		writeToZip(new FileInputStream( tempfile ), path + filename + fileSuffix);
		tempfile.delete();

		fileList.add(new FileData(resource.getResourceId(), path, filename, fileSuffix));
	}

	private void processUploadedFiles( Resource resource, String path )
	{
		try {
			// Obtain a list of entries in the root directory and its descendants (exclude deleted files/folders):
			BuildingSession session = BuildingSessionManagerImpl.getSession( resource );
			// Files and folders are represented as a flat summaries array:
			UploadedFileSummary[] summaries = session.getUploadedFileSession().getFileAndDescendentSummaries( "/", true );
			// Obtain the location of the filestore for the entries
			File source_filestore = resource.getWebPublishFolder();

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

				if ( fileID == null ) continue; //no root folder

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

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

				processUploadedFile(f, path, source_filestore);

			}
		} catch (BuildingServerException e) {
			String message = "Error in processing uploaded files: " + e.getMessage();
			errors.add(message);
			logger.info(message, e);
		}
	}

    private void processUploadedFile( UploadedFile f, String path, File source_filestore )
    {
    	// don't need to do anything with folders, get containing folder name/s from files instead...
    	if (f.isFolder())
    	{
    		fileList.add(new FileData(f, path));
    		return;
    	}
		
		try {
			// add file to zip archive:
			File source = new File(source_filestore, f.getRealNameInResource());
			FileInputStream inputStream = new FileInputStream( source );
			writeToZip(inputStream, path + f.getNameInResource());
			fileList.add(new FileData(f, path));
			
		} catch (Exception e) {
			String message = "Error in adding file to zip archive: " + e.getMessage();
			errors.add(message);
			logger.info(message, e);
		}
    }

	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();
		}
    }
	
	protected void finishProcessing()
	{
		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);
		}		
	}
	
	protected void writeErrorFileToZip( String message )
	{
		try {
			ByteArrayInputStream inputStream = new ByteArrayInputStream(message.getBytes());
			writeToZip(inputStream, "errors.txt");
		} catch (IOException e) {
			logger.info("Failed to write error file to content package.", e);
		}
	}
	
	protected String formatErrorMessages()
	{
		StringWriter swriter = new StringWriter();
		PrintWriter writer = new PrintWriter(swriter);
		for (Iterator iterator = errors.iterator(); iterator.hasNext();) {
			writer.println((String) iterator.next());
		}
		return swriter.toString();
	}
	
	protected ZipOutputStream getZipOutputStream()
	{
		return zipOutputStream;
	}

	protected void setZipOutputStream(ZipOutputStream zipOutputStream)
	{
		this.zipOutputStream = zipOutputStream;
	}

	protected void setRequest(Request request) {
		this.request = request;
	}
	
	protected Request getRequest() {
		return request;
	}
	
}
