package aQute.bnd.maven.support;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

import aQute.lib.hex.Hex;
import aQute.lib.io.IO;
import aQute.lib.utf8properties.UTF8Properties;
import aQute.libg.filelock.DirectoryLock;

/**
 * An entry (a group/artifact) in the maven cache in the .m2/repository
 * directory. It provides methods to get the pom and the artifact.
 */
public class MavenEntry implements Closeable {
	final Maven					maven;
	final File					root;
	final File					dir;
	final String				path;
	final DirectoryLock			lock;
	final Map<URI,CachedPom>	poms	= new HashMap<URI,CachedPom>();
	final File					pomFile;
	final File					artifactFile;
	final String				pomPath;
	final File					propertiesFile;
	UTF8Properties				properties;
	private boolean				propertiesChanged;
	FutureTask<File>			artifact;
	String						artifactPath;

	/**
	 * Constructor.
	 * 
	 * @param maven
	 * @param path
	 */
	MavenEntry(Maven maven, String path) {
		this.root = maven.repository;
		this.maven = maven;
		this.path = path;
		this.pomPath = path + ".pom";
		this.artifactPath = path + ".jar";
		this.dir = IO.getFile(maven.repository, path).getParentFile();
		try {
			IO.mkdirs(this.dir);
		} catch (IOException e) {
			throw new ExceptionInInitializerError(e);
		}
		this.pomFile = new File(maven.repository, pomPath);
		this.artifactFile = new File(maven.repository, artifactPath);
		this.propertiesFile = new File(dir, "bnd.properties");
		this.lock = new DirectoryLock(dir, 5 * 60000); // 5 mins
	}

	public File getArtifactFile() {
		return artifactFile;
	}

	/**
	 * This is the method to get the POM for a cached entry.
	 * 
	 * @param urls The allowed URLs
	 * @return a CachedPom for this maven entry
	 * @throws Exception If something goes haywire
	 */
	public CachedPom getPom(URI[] urls) throws Exception {

		// First check if we have the pom cached in memory
		synchronized (this) {
			// Try to find it in the in-memory cache
			for (URI url : urls) {
				CachedPom pom = poms.get(url);
				if (pom != null)
					return pom;
			}
		}

		// Ok, we need to see if it exists on disk

		// lock.lock();
		try {

			if (isValid()) {
				// Check if one of our repos had the correct file.
				for (URI url : urls) {
					String valid = getProperty(url.toASCIIString());
					if (valid != null)
						return createPom(url);
				}

				// we have the info, but have to verify that it
				// exists in one of our repos but we do not have
				// to download it as our cache is already ok.
				for (URI url : urls) {
					if (verify(url, pomPath)) {
						return createPom(url);
					}
				}

				// It does not exist in out repo
				// so we have to fail even though we do have
				// the file.

			} else {
				IO.mkdirs(dir);
				// We really do not have the file
				// so we have to find out who has it.
				for (final URI url : urls) {

					if (download(url, pomPath)) {
						if (verify(url, pomPath)) {
							artifact = new FutureTask<File>(new Callable<File>() {

								public File call() throws Exception {
									if (download(url, artifactPath)) {
										verify(url, artifactPath);
									}
									return artifactFile;
								}

							});
							maven.executor.execute(artifact);
							return createPom(url);
						}
					}
				}
			}
			return null;
		} finally {
			saveProperties();
			// lock.release();
		}
	}

	/**
	 * Download a resource from the given repo.
	 * 
	 * @param url The base url for the repo
	 * @param path The path part
	 * @throws MalformedURLException
	 */
	boolean download(URI repo, String path) throws MalformedURLException {
		try {
			URL url = toURL(repo, path);
			System.err.println("Downloading " + repo + " path " + path + " url " + url);
			File file = new File(root, path);
			IO.copy(url.openStream(), file);
			System.err.println("Downloaded " + url);
			return true;
		} catch (Exception e) {
			System.err.println("debug: " + e);
			return false;
		}
	}

	/**
	 * Converts a repo + path to a URL..
	 * 
	 * @param base The base repo
	 * @param path The path in the directory + url
	 * @return a URL that points to the file in the repo
	 * @throws MalformedURLException
	 */
	URL toURL(URI base, String path) throws MalformedURLException {
		StringBuilder r = new StringBuilder();
		r.append(base.toString());
		if (r.charAt(r.length() - 1) != '/')
			r.append('/');
		r.append(path);
		return new URL(r.toString());
	}

	/**
	 * Check if this is a valid cache directory, might probably need some more
	 * stuff.
	 * 
	 * @return true if valid
	 */
	private boolean isValid() {
		return pomFile.isFile() && pomFile.length() > 100 && artifactFile.isFile() && artifactFile.length() > 100;
	}

	/**
	 * We maintain a set of bnd properties in the cache directory.
	 * 
	 * @param key The key for the property
	 * @param value The value for the property
	 */
	private void setProperty(String key, String value) {
		Properties properties = getProperties();
		properties.setProperty(key, value);
		propertiesChanged = true;
	}

	/**
	 * Answer the properties, loading if needed.
	 */
	protected Properties getProperties() {
		if (properties == null) {
			properties = new UTF8Properties();
			File props = new File(dir, "bnd.properties");
			if (props.exists()) {
				try {
					properties.load(props, null);
				} catch (Exception e) {
					// we ignore for now, will handle it on safe
				}
			}
		}
		return properties;
	}

	/**
	 * Answer a property.
	 * 
	 * @param key The key
	 * @return The value
	 */
	private String getProperty(String key) {
		Properties properties = getProperties();
		return properties.getProperty(key);
	}

	private void saveProperties() throws IOException {
		if (propertiesChanged) {
			try (OutputStreamWriter osw = new OutputStreamWriter(IO.outputStream(propertiesFile))) {
				properties.store(osw, "");
			} finally {
				properties = null;
				propertiesChanged = false;
			}
		}
	}

	/**
	 * Help function to create the POM and record its source.
	 * 
	 * @param url the repo from which it was constructed
	 * @return the new pom
	 * @throws Exception
	 */
	private CachedPom createPom(URI url) throws Exception {
		CachedPom pom = new CachedPom(this, url);
		pom.parse();
		poms.put(url, pom);
		setProperty(url.toASCIIString(), "true");
		return pom;
	}

	/**
	 * Verify that the repo has a checksum file for the given path and that this
	 * checksum matchs.
	 * 
	 * @param repo The repo
	 * @param path The file id
	 * @return true if there is a digest and it matches one of the algorithms
	 * @throws Exception
	 */
	boolean verify(URI repo, String path) throws Exception {
		for (String algorithm : Maven.ALGORITHMS) {
			if (verify(repo, path, algorithm))
				return true;
		}
		return false;
	}

	/**
	 * Verify the path against its digest for the given algorithm.
	 * 
	 * @param repo
	 * @param path
	 * @param algorithm
	 * @throws Exception
	 */
	private boolean verify(URI repo, String path, String algorithm) throws Exception {
		String digestPath = path + "." + algorithm;
		File actualFile = new File(root, path);

		if (download(repo, digestPath)) {
			File digestFile = new File(root, digestPath);
			final MessageDigest md = MessageDigest.getInstance(algorithm);
			IO.copy(actualFile, md);
			byte[] digest = md.digest();
			String source = IO.collect(digestFile).toUpperCase();
			String hex = Hex.toHexString(digest).toUpperCase();
			if (source.startsWith(hex)) {
				System.err.println("Verified ok " + actualFile + " digest " + algorithm);
				return true;
			}
		}
		System.err.println("Failed to verify " + actualFile + " for digest " + algorithm);
		return false;
	}

	public File getArtifact() throws Exception {
		if (artifact == null)
			return artifactFile;
		return artifact.get();
	}

	public File getPomFile() {
		return pomFile;
	}

	public void close() throws IOException {

	}

	public void remove() {
		if (dir.getParentFile() != null) {
			IO.delete(dir);
		}
	}

}
