/*******************************************************************************
 * Copyright (c) 2004, 2009 Tasktop Technologies and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Tasktop Technologies - initial API and implementation
 *******************************************************************************/

package org.eclipse.mylyn.internal.context.core;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

import org.apache.commons.io.IOUtils;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;
import org.eclipse.mylyn.commons.core.StatusHandler;
import org.eclipse.mylyn.context.core.IContextContributor;
import org.eclipse.mylyn.context.core.IInteractionContext;
import org.eclipse.mylyn.context.core.IInteractionContextScaling;

/**
 * @author Mik Kersten
 * @author Jevgeni Holodkov
 */
public class InteractionContextExternalizer {

	public static final String ELMNT_INTERACTION_HISTORY_OLD = "interactionEvent"; //$NON-NLS-1$

	public static final String ELMNT_INTERACTION_HISTORY = "InteractionHistory"; //$NON-NLS-1$

	public static final String ATR_STRUCTURE_KIND = "StructureKind"; //$NON-NLS-1$

	public static final String ATR_STRUCTURE_HANDLE = "StructureHandle"; //$NON-NLS-1$

	public static final String ATR_START_DATE = "StartDate"; //$NON-NLS-1$

	public static final String ATR_ORIGIN_ID = "OriginId"; //$NON-NLS-1$

	public static final String ATR_NAVIGATION = "Navigation"; //$NON-NLS-1$

	public static final String ATR_KIND = "Kind"; //$NON-NLS-1$

	public static final String ATR_INTEREST = "Interest"; //$NON-NLS-1$

	public static final String ATR_NUM_EVENTS = "NumEvents"; //$NON-NLS-1$

	public static final String ATR_CREATION_COUNT = "CreationCount"; //$NON-NLS-1$

	public static final String ATR_DELTA = "Delta"; //$NON-NLS-1$

	public static final String ATR_END_DATE = "EndDate"; //$NON-NLS-1$

	public static final String ATR_ID = "Id"; //$NON-NLS-1$

	public static final String ATR_VERSION = "Version"; //$NON-NLS-1$

	static final String DATE_FORMAT_STRING = "yyyy-MM-dd HH:mm:ss.S z"; //$NON-NLS-1$

	static String getFirstContextHandle(File sourceFile) throws CoreException {
		try {
			ZipFile zipFile = new ZipFile(sourceFile);
			try {
				for (Enumeration<?> e = zipFile.entries(); e.hasMoreElements();) {
					ZipEntry entry = (ZipEntry) e.nextElement();
					String name = entry.getName();
					if (name.endsWith(InteractionContextManager.CONTEXT_FILE_EXTENSION_OLD)) {
						try {
							String decodedName = URLDecoder.decode(name,
									InteractionContextManager.CONTEXT_FILENAME_ENCODING);
							if (decodedName.length() > InteractionContextManager.CONTEXT_FILE_EXTENSION_OLD.length()) {
								return decodedName.substring(0, decodedName.length()
										- InteractionContextManager.CONTEXT_FILE_EXTENSION_OLD.length());
							}
						} catch (IllegalArgumentException ignored) {
							// not a valid context entry
						}
					}
				}
				return null;
			} finally {
				zipFile.close();
			}
		} catch (IOException e) {
			throw new CoreException(new Status(IStatus.ERROR, ContextCorePlugin.ID_PLUGIN,
					"Could not get context handle from " + sourceFile, e)); //$NON-NLS-1$
		}
	}

	public void writeContextToXml(IInteractionContext context, File file) throws IOException {
		writeContextToXml(context, file, new SaxContextWriter());
	}

	/**
	 * Public for testing.
	 * 
	 * @throws IOException
	 *             if writing of context fails
	 */
	public void writeContextToXml(IInteractionContext context, File file, IInteractionContextWriter writer)
			throws IOException {
		if (context.getInteractionHistory().isEmpty()) {
			return;
		}

		FileOutputStream fileOutputStream = new FileOutputStream(file);
		try {
			ZipOutputStream outputStream = new ZipOutputStream(fileOutputStream);
			try {
				writeContext(context, outputStream, writer);
			} finally {
				outputStream.close();
			}
		} finally {
			fileOutputStream.close();
		}
	}

	public void writeContext(IInteractionContext context, ZipOutputStream outputStream) throws IOException {
		writeContext(context, outputStream, new SaxContextWriter());
	}

	/**
	 * For testing
	 */
	public void writeContext(IInteractionContext context, ZipOutputStream outputStream, IInteractionContextWriter writer)
			throws IOException {
		String handleIdentifier = context.getHandleIdentifier();
		String encoded = URLEncoder.encode(handleIdentifier, InteractionContextManager.CONTEXT_FILENAME_ENCODING);
		ZipEntry zipEntry = new ZipEntry(encoded + InteractionContextManager.CONTEXT_FILE_EXTENSION_OLD);
		outputStream.putNextEntry(zipEntry);
		outputStream.setMethod(ZipOutputStream.DEFLATED);

		writer.setOutputStream(outputStream);
		writer.writeContextToStream(context);
		outputStream.flush();
		outputStream.closeEntry();

		addAdditionalInformation(context, outputStream);
	}

	private void addAdditionalInformation(final IInteractionContext context, final ZipOutputStream outputStream)
			throws IOException {
		for (final IContextContributor contributor : getContextContributor()) {
			SafeRunner.run(new ISafeRunnable() {
				public void handleException(Throwable e) {
					StatusHandler.log(new Status(IStatus.WARNING, ContextCorePlugin.ID_PLUGIN,
							"Context contribution failed: " //$NON-NLS-1$
									+ contributor.getClass(), e));
				}

				public void run() throws Exception {
					InputStream additionalContextInformation = contributor.getDataAsStream(context);
					if (additionalContextInformation != null) {
						try {
							String encoded = URLEncoder.encode(contributor.getIdentifier(),
									InteractionContextManager.CONTEXT_FILENAME_ENCODING);
							ZipEntry zipEntry = new ZipEntry(encoded);
							outputStream.putNextEntry(zipEntry);
							IOUtils.copy(additionalContextInformation, outputStream);
							outputStream.flush();
							outputStream.closeEntry();
						} finally {
							additionalContextInformation.close();
						}
					}
				}
			});
		}
	}

	public InputStream getAdditionalInformation(File file, String contributorIdentifier) throws IOException {
		if (!file.exists()) {
			return null;
		}
		final ZipFile zipFile = new ZipFile(file);
		ZipEntry entry = findFileInZip(zipFile, contributorIdentifier);
		if (entry == null) {
			return null;
		}

		return new FilterInputStream(zipFile.getInputStream(entry)) {
			@Override
			public void close() throws IOException {
				super.close();
				zipFile.close();
			}
		};
	}

	private ZipEntry findFileInZip(ZipFile zipFile, String identifier) throws UnsupportedEncodingException {
		String encoded = URLEncoder.encode(identifier, InteractionContextManager.CONTEXT_FILENAME_ENCODING);
		for (Enumeration<?> e = zipFile.entries(); e.hasMoreElements();) {
			ZipEntry entry = (ZipEntry) e.nextElement();
			if (entry.getName().equals(encoded)) {
				return entry;
			}
		}
		return null;
	}

	private List<IContextContributor> getContextContributor() {
		return ContextCorePlugin.getDefault().getContextContributor();
	}

	public IInteractionContext readContextFromXml(String handleIdentifier, File fromFile,
			IInteractionContextScaling scaling) {
		return readContextFromXml(handleIdentifier, fromFile, new SaxContextReader(), scaling);
	}

	/**
	 * Public for testing
	 */
	public IInteractionContext readContextFromXml(String handleIdentifier, File fromFile,
			IInteractionContextReader reader, IInteractionContextScaling scaling) {
		try {
			if (!fromFile.exists()) {
				return null;
			} else {
				if (reader instanceof SaxContextReader) {
					((SaxContextReader) reader).setContextScaling(scaling);
				}

				InteractionContext context = reader.readContext(handleIdentifier, fromFile);
				if (context == null) {
					String firstHandle = getFirstContextHandle(fromFile);
					if (firstHandle != null && !firstHandle.equals(handleIdentifier)) {
						context = reader.readContext(firstHandle, fromFile);
						if (context != null) {
							context.setHandleIdentifier(handleIdentifier);
						}
					}
				}
				return context;
			}
		} catch (Exception e) {
			// TODO: propagate exception instead?
			StatusHandler.log(new Status(IStatus.ERROR, ContextCorePlugin.ID_PLUGIN, "Could not read: " //$NON-NLS-1$
					+ fromFile.getAbsolutePath(), e));
		}
		return null;
	}
}
