/*
 * Copyright (c) 2013 Red Hat, Inc. and/or its affiliates.
 *  
 *  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:
 *      Brad Davis - bradsdavis@gmail.com - Initial API and implementation
*/
package org.jboss.windup.util;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.jboss.windup.metadata.type.archive.ZipMetadata;


/**
 *  Custom implementation of zip archive extraction.
 */
public class RecursiveZipMetaFactory {
	private static final Logger LOG = LoggerFactory.getLogger(RecursiveZipMetaFactory.class);

	private static final int BUFFER = 2048;

    /** The directory to extract to. */
	private File startLocation;

	public RecursiveZipMetaFactory(File startLocation, Set<String> extensions) {
		UUID uuidKey = UUID.randomUUID();
		safeExtractKey = "_" + StringUtils.substring(StringUtils.remove(uuidKey.toString(), "-"), 0, 6);

		this.startLocation = new File(startLocation.getAbsolutePath() + File.separator + "jboss_windup" + safeExtractKey);
		this.kae = extensions;
	}

	private final String safeExtractKey;
	private Set<String> kae = new HashSet<String>();

	public void releaseTempFiles() {
		LOG.debug("Cleaning up: " + this.startLocation.getAbsolutePath());
		FileUtils.deleteQuietly(this.startLocation);
	}

	public ZipMetadata recursivelyExtract(ZipFile zip) {

		if (LOG.isDebugEnabled()) {
			LOG.debug(zip.getName() + ": " + this.startLocation.getAbsolutePath());
		}
		ZipMetadata archive = generateArchive(null, new File(zip.getName()));
		recursivelyExtract(archive, zip, this.startLocation);

		return archive;
	}

	protected void recursivelyExtract(ZipMetadata parent, ZipFile zip, File outputDirectory) {
		String fileName = StringUtils.substringAfterLast(zip.getName(), File.separator);
		File subOutputDir = new File(outputDirectory.getAbsolutePath() + File.separator + fileName);
		ZipEntry entry;

		Enumeration<?> e = zip.entries();
		while (e.hasMoreElements()) {
			entry = (ZipEntry) e.nextElement();
			// must be a candidate file.
			if (!entry.isDirectory()) {
				if (archiveEndInEntryOfInterest(entry.getName())) {
					try {
						File extracted = unzipEntry(parent, entry, zip, subOutputDir);
						ZipFile zf = new ZipFile(extracted);

						// we should know it is a valid zip here..
						ZipMetadata arch = generateArchive(parent, extracted);
						LOG.info("Prepared ZipMetadata: " + arch.getRelativePath());
						recursivelyExtract(arch, zf, new File(StringUtils.substringBeforeLast(zf.getName(), File.separator)));
					}
					catch (FileNotFoundException e1) {
						LOG.warn("Skipping invalid zip entry: " + entry);
					}
					catch (IOException e1) {
						LOG.warn("Skipping invalid zip entry: " + entry);
					}
				}
			}
		}
		try {
			zip.close();
		}
		catch (IOException e1) {
			LOG.error("Exception closing zip.", e1);
		}
	}

	protected File unzipEntry(ZipMetadata parent, ZipEntry entry, ZipFile zipfile, File archiveOutputDirectory) {
		BufferedOutputStream dest = null;
		BufferedInputStream is = null;
		String pathOutput = null;
		if (StringUtils.contains(entry.toString(), "/")) {
			pathOutput = StringUtils.substringBeforeLast(entry.toString(), "/");
		}
		else {
			pathOutput = File.separator;
		}
		File entryPathOutput = new File(archiveOutputDirectory.getAbsolutePath() + safeExtractKey + File.separator + pathOutput);
		File entryOutput = new File(archiveOutputDirectory.getAbsolutePath() + safeExtractKey + File.separator + entry);
		if (!entryOutput.exists()) {
			try {
				FileUtils.forceMkdir(entryPathOutput);
				is = new BufferedInputStream(zipfile.getInputStream(entry));
				if (LOG.isDebugEnabled()) {
					LOG.debug("Unzipping: " + entryOutput.getAbsolutePath());
				}

				int count;
				byte data[] = new byte[BUFFER];
				FileOutputStream fos = new FileOutputStream(entryOutput);
				dest = new BufferedOutputStream(fos, BUFFER);

				while ((count = is.read(data, 0, BUFFER)) != -1) {
					dest.write(data, 0, count);
				}
				dest.flush();

				return entryOutput;
			}
			catch (IOException e) {
				LOG.error("IOException: " + e.getMessage(), e);
			}
			finally {
				IOUtils.closeQuietly(is);
				IOUtils.closeQuietly(dest);
			}
		}
		else {
			LOG.warn("Entry: " + entry.toString() + " is a duplicate.  Returning the first entry.");
			return entryOutput;
		}

		return null;

	}

	private boolean archiveEndInEntryOfInterest(String entryName) {
		for (String extension : kae) {
			if (StringUtils.endsWith(entryName, extension)) {
				return true;
			}
		}
		return false;
	}

	private ZipMetadata generateArchive(ZipMetadata parent, File entryOutput) {
		String relativePath = StringUtils.removeStart(entryOutput.getAbsolutePath(), this.startLocation.getAbsolutePath().toString());

		if (LOG.isTraceEnabled()) {
			LOG.trace("RE Relative Path: " + relativePath);
			LOG.trace("SafeKey: " + safeExtractKey);
		}

		relativePath = StringUtils.replace(relativePath, "\\", "/");
		relativePath = StringUtils.removeStart(relativePath, "/");

		if (StringUtils.contains(relativePath, this.safeExtractKey)) {
			// all subarchives of the target archive will get copied to a location with the safeExtractKey.
			relativePath = StringUtils.remove(relativePath, this.safeExtractKey);
		}
		else {
			// otherwise, we know it is the original file...
			relativePath = StringUtils.substringAfterLast(relativePath, "/");
		}

		String archiveName = relativePath;
		if (StringUtils.contains(archiveName, "/")) {
			archiveName = StringUtils.substringAfterLast(relativePath, "/");
		}

		ZipMetadata archive = new ZipMetadata();
		archive.setName(archiveName);
		archive.setFilePointer(entryOutput);
		archive.setRelativePath(relativePath);

		if (parent != null) {
			parent.getNestedArchives().add(archive);
		}
		
		archive.setArchiveMeta(parent);

		if (LOG.isTraceEnabled()) {
			LOG.trace("Created archive: " + archive.toString());
		}
		return archive;
	}
}