/*******************************************************************************
 * Copyright (c) 2007, 2018 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.artifact.optimizers.jardelta;

import java.io.*;
import java.util.*;
import java.util.zip.*;

public class DeltaComputer {
	private final File target;
	private final File base;
	private final File destination;
	private ZipFile baseJar;
	private ZipFile targetJar;
	private Set<String> baseEntries;
	private ArrayList<ZipEntry> additions;
	private ArrayList<ZipEntry> changes;
	private ZipFile manifestJar = null;

	public DeltaComputer(File base, File target, File destination) {
		this.base = base;
		this.target = target;
		this.destination = destination;
	}

	public void run() throws IOException {
		try {
			if (!openJars())
				return;
			computeDelta();
			writeDelta();
		} finally {
			closeJars();
		}
	}

	private void writeDelta() {
		try {
			try (ZipOutputStream result = new ZipOutputStream(new FileOutputStream(destination))) {

				// if the delta includes the manifest, be sure to write it first
				if (manifestJar != null)
					writeEntry(result, manifestJar.getEntry("META-INF/MANIFEST.MF"), manifestJar, true);
				// write out the removals. These are all the entries left in the baseEntries
				// since they were not seen in the targetJar. Here just write out an empty
				// entry with a name that signals the delta processor to delete.
				for (String baseEntry : baseEntries)
					writeEntry(result, new ZipEntry(baseEntry + ".delete"), null, false);
				// write out the additions.
				for (ZipEntry entry : additions)
					writeEntry(result, entry, targetJar, false);
				// write out the changes.
				for (ZipEntry entry : changes)
					writeEntry(result, entry, targetJar, false);
			}
		} catch (IOException e) {
			e.printStackTrace();
			return;
		}
	}

	private void writeEntry(ZipOutputStream result, ZipEntry entry, ZipFile sourceJar, boolean manifest)
			throws IOException {
		if (!manifest && entry.getName().equalsIgnoreCase("META-INF/MANIFEST.MF"))
			return;
		// add the entry
		result.putNextEntry(entry);
		try {
			// if there is a sourceJar copy over the content for the entry into the result
			if (sourceJar != null) {

				try (InputStream contents = sourceJar.getInputStream(entry)) {
					transferStreams(contents, result);
				}
			}
		} finally {
			result.closeEntry();
		}
	}

	/**
	 * Transfers all available bytes from the given input stream to the given output
	 * stream. Does not close either stream.
	 *
	 * @param source
	 * @param destination
	 * @throws IOException
	 */
	public static void transferStreams(InputStream source, OutputStream destination) throws IOException {
		source = new BufferedInputStream(source);
		destination = new BufferedOutputStream(destination);
		try {
			byte[] buffer = new byte[8192];
			while (true) {
				int bytesRead = -1;
				if ((bytesRead = source.read(buffer)) == -1)
					break;
				destination.write(buffer, 0, bytesRead);
			}
		} finally {
			destination.flush();
		}
	}

	private void computeDelta() throws IOException {
		changes = new ArrayList<>();
		additions = new ArrayList<>();
		// start out assuming that all the base entries are being removed
		baseEntries = getEntries(baseJar);
		for (Enumeration<? extends ZipEntry> e = targetJar.entries(); e.hasMoreElements();)
			check(e.nextElement(), targetJar);
	}

	private boolean openJars() {
		try {
			baseJar = new ZipFile(base);
			targetJar = new ZipFile(target);
		} catch (IOException e) {
			return false;
		}
		return true;
	}

	private void closeJars() {
		if (baseJar != null)
			try {
				baseJar.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		if (targetJar != null)
			try {
				targetJar.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
	}

	/**
	 * Compare the given entry against the base JAR to see if/how it differs. Update
	 * the appropriate set based on the discovered difference.
	 *
	 * @param entry the entry to test
	 * @throws IOException
	 */
	private void check(ZipEntry entry, ZipFile file) throws IOException {
		ZipEntry baseEntry = baseJar.getEntry(entry.getName());

		// remember the manifest if we see it
		checkForManifest(entry, file);
		// if there is no entry then this is an addition. remember the addition and
		// return;
		if (baseEntry == null) {
			additions.add(entry);
			return;
		}
		// now we know each JAR has an entry for the name, compare and see how/if they
		// differ
		boolean changed = !equals(entry, baseEntry);
		if (changed)
			changes.add(entry);
		baseEntries.remove(baseEntry.getName());
	}

	// compare the two entries. We already know that they have the same name.
	private boolean equals(ZipEntry entry, ZipEntry baseEntry) {
		if (entry.getSize() != baseEntry.getSize())
			return false;
		// make sure the entries are of the same type
		if (entry.isDirectory() != baseEntry.isDirectory())
			return false;
		// if the entries are files then compare the times.
		if (!entry.isDirectory())
			if (entry.getTime() != baseEntry.getTime())
				return false;
		return true;
	}

	private Set<String> getEntries(ZipFile jar) {
		HashSet<String> result = new HashSet<>(jar.size());
		for (Enumeration<? extends ZipEntry> e = jar.entries(); e.hasMoreElements();) {
			ZipEntry entry = e.nextElement();
			checkForManifest(entry, jar);
			result.add(entry.getName());
		}
		return result;
	}

	/**
	 * Check to see if the given entry is the manifest. If so, remember it for use
	 * when writing the resultant JAR.
	 *
	 * @param entry
	 * @param jar
	 */
	private void checkForManifest(ZipEntry entry, ZipFile jar) {
		if (entry.getName().equalsIgnoreCase("META-INF/MANIFEST.MF"))
			manifestJar = jar;
	}
}
