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

public class ContentZipper {

	private ZipOutputStream zipOutputStream;
	protected List errors;
	protected List ignoreStrings;
	private boolean recursive;
	private boolean includeDescriptions;
	private boolean includeIntroductions;
	private boolean includeQuickLinks;

	protected static final String INTRODUCTION_FILENAME="introduction.html";
	protected static final String DESCRIPTION_FILENAME="description.html";
	protected static final String EASYWRITER_FILENAME="easywriter.html";
	protected static final String TEXTBLOCK_FILENAME="textblock.html";
	protected static final String QUICKLINK_FILENAME = "link.url";

	private Logger logger = Logger.getLogger(ContentZipper.class);
	
	protected void init(OutputStream outputStream) {
		setZipOutputStream(new ZipOutputStream( outputStream ));
		errors = new ArrayList();
		ignoreStrings = Arrays.asList(new String[] {"", "<!-- -->", "<!>", "<!-- No Introduction -->", "<!-- No Description -->"});
		
	}
	
	public void packageResource( Resource resource, OutputStream outputStream ) {
		
		init( outputStream );
		
		if ( !resource.checkPermission( Permission.MANAGE ) )
			return;
		
		processResource(resource, "");
		
		if ( !errors.isEmpty() )
		{
			writeErrorFileToZip(formatErrorMessages());
		}
			
		finishProcessing();
	}

	/**
	 * Creates XML elements and adds content to archive for a given resource.
	 * Is called recursively as it walks down the tree
	 * @param resource
	 * @param path
	 * @param containingElement
	 * @return
	 * @throws BuildingServerException
	 */
	public void processResource( Resource resource, String path )
	{
		if ( !resource.checkPermission( Permission.MANAGE ) )
			return;

		if (includeResource(resource))
		{
			// do summat with intro and description:
			processResourceIntroduction(resource, path);
			processResourceDescription(resource, path);

			// add uploaded files to zip:
			processUploadedFiles(resource,path);

			processContent(resource, path);

			ExportEvent event = new ExportEvent(ExportEvent.EVENT_ADD_RESOURCE_TO_ZIP, resource);
			event.save();
		}
		
		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 exporting child resource content: " + e.getMessage();
				errors.add(message);
				logger.info(message, e);
			}
		}

	}

	private boolean includeResource(Resource resource)
	{
		return includeQuickLinks || !(resource instanceof QuickLink);
	}

	private void processContent(Resource resource, String path)
	{
		if (resource instanceof QuickLink)
		{
			QuickLink quickLink = (QuickLink)resource;
			if (!quickLink.isBodURL() && quickLink.getURL() != null)
			{
				try
				{
					writeToZip(new ByteArrayInputStream(quickLink.getURL().getBytes("UTF-8")),
						path+QUICKLINK_FILENAME);
				}
				catch (Exception e)
				{
					String message = "Failed to add quicklink: "+ e.getMessage();
					errors.add(message);
					logger.info(message, e);
				}
			}
		}
	}

    /**
	 * Create HTML file from resource introduction. </br>
	 * In the case of EasyWriter documents, creation is mandatory, and appropriate filename used. </br>
	 * (Returns boolean as ContentPackager subclass needs to know whether to add entry to manifest.)
	 * @param resource
	 * @param path path to add to in zip archive.
	 * @return true if file was created.
	 */
	
	protected boolean processResourceIntroduction( Resource resource, String path )
	{
		try {
			// EasyWriter uses the introduction field for content, so need to override includeIntroductions in that case:
			boolean isEasyWriter = (resource.getResourceType() == Resource.RESOURCE_EASYEDIT);

			// if not EasyWriter && include is false, then return:
			if ( !(isEasyWriter) && !isIncludeIntroductions())
				return false;

			// if empty intro, then return:
			String intro = resource.getIntroduction();
			if ( ignoreStrings.contains(intro))
				return false;
			
			if ( isEasyWriter)
				createHTMLFile(resource, intro, path, EASYWRITER_FILENAME);
			else
				createHTMLFile(resource, intro, path, INTRODUCTION_FILENAME);

		} catch (Exception e) {
			String message = "Error in exporting introduction as file: " + e.getMessage();
			errors.add(message);
			logger.info(message, e);
			return false;
		}
		return true;
	}
	
	/**
	 * Create HTML file from resource description. </br>
	 * In the case of TextBlocks, creation is mandatory, and appropriate filename used.</br>
	 * (Returns boolean as ContentPackager subclass needs to know whether to add entry to manifest.)
	 * @param resource
	 * @param path path to add to in zip archive.
	 * @return true if file was created.
	 */
	
	protected boolean processResourceDescription( Resource resource, String path )
	{
		try {
			// TextBlock uses the description field for content, so need to override includeDescriptions in that case:
			boolean isTextBlock = (resource.getResourceType() == Resource.RESOURCE_TEXTBLOCK);
			
			// if not TextBlock && include is false, then return:
			if ( !(isTextBlock) && !isIncludeDescriptions())
				return false;
			
			// if empty description, then return:
			String description = resource.getDescription();
			if ( ignoreStrings.contains(description))
				return false;
			
			if (isTextBlock)
				createHTMLFile(resource, description, path, TEXTBLOCK_FILENAME);
			else
				createHTMLFile(resource, description, path, DESCRIPTION_FILENAME);

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

	private void createHTMLFile(Resource resource, String content, String path, String filename)
			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);
		tempfile.delete();
	}

	protected 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, so no files.

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

    protected 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()) 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());
			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);
		}
    }

	protected 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 ZipOutputStream getZipOutputStream() {
		return zipOutputStream;
	}

	protected void setZipOutputStream(ZipOutputStream zipOutputStream) {
		this.zipOutputStream = zipOutputStream;
	}
	
	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();
	}
	
	private boolean isRecursive() {
		return recursive;
	}

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

	public boolean isIncludeDescriptions() {
		return includeDescriptions;
	}

	public void setIncludeDescriptions(boolean includeDescriptions) {
		this.includeDescriptions = includeDescriptions;
	}

	public boolean isIncludeIntroductions() {
		return includeIntroductions;
	}

	public void setIncludeIntroductions(boolean includeIntroductions) {
		this.includeIntroductions = includeIntroductions;
	}

	public boolean isIncludeQuickLinks()
	{
		return includeQuickLinks;
	}

	public void setIncludeQuickLinks(boolean includeQuickLinks)
	{
		this.includeQuickLinks = includeQuickLinks;
	}

}
