package aQute.bnd.repository.p2.provider;

import static aQute.bnd.osgi.resource.ResourceUtils.toVersion;
import static java.util.Collections.singleton;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.concurrent.TimeUnit;

import org.osgi.resource.Capability;
import org.osgi.resource.Requirement;
import org.osgi.resource.Resource;
import org.osgi.service.repository.Repository;
import org.osgi.util.function.Function;
import org.osgi.util.promise.Promise;
import org.osgi.util.promise.Promises;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import aQute.bnd.http.HttpClient;
import aQute.bnd.osgi.Processor;
import aQute.bnd.osgi.repository.BridgeRepository;
import aQute.bnd.osgi.repository.ResourcesRepository;
import aQute.bnd.osgi.repository.XMLResourceGenerator;
import aQute.bnd.osgi.repository.XMLResourceParser;
import aQute.bnd.osgi.resource.CapabilityBuilder;
import aQute.bnd.osgi.resource.FilterBuilder;
import aQute.bnd.osgi.resource.RequirementBuilder;
import aQute.bnd.osgi.resource.ResourceBuilder;
import aQute.bnd.osgi.resource.ResourceUtils;
import aQute.bnd.osgi.resource.ResourceUtils.ContentCapability;
import aQute.bnd.osgi.resource.ResourceUtils.IdentityCapability;
import aQute.bnd.service.RepositoryPlugin.DownloadListener;
import aQute.bnd.util.repository.DownloadListenerPromise;
import aQute.bnd.version.Version;
import aQute.lib.io.IO;
import aQute.p2.api.Artifact;
import aQute.p2.provider.P2Impl;
import aQute.service.reporter.Reporter;

class P2Indexer implements Closeable {
	private final static Logger			logger		= LoggerFactory.getLogger(P2Indexer.class);
	private static final long	MAX_STALE	= TimeUnit.DAYS.toMillis(100);
	private final Reporter					reporter;
	final File								location;
	private final HttpClient				client;
	private final URI						url;
	private final String					name;
	private final File						indexFile;
	private volatile BridgeRepository		bridge;
	private static final Resource		RECOVERY	= new ResourceBuilder().build();
	private static final String			P2_CAPABILITY_NAMESPACE	= "bnd.p2";
	private static final String			MD5_ATTRIBUTE			= "md5";
	private static final Requirement	MD5_REQUIREMENT			= new RequirementBuilder(P2_CAPABILITY_NAMESPACE)
			.addFilter(new FilterBuilder().isPresent(MD5_ATTRIBUTE))
			.buildSyntheticRequirement();

	P2Indexer(Reporter reporter, File location, HttpClient client, URI url, String name) throws Exception {
		this.reporter = reporter;
		this.location = location;
		this.indexFile = new File(location, "index.xml.gz");
		this.client = client;
		this.url = url;
		this.name = name;
		IO.mkdirs(this.location);

		validate();

		Repository r;
		if (this.indexFile.isFile())
			r = readFile();
		else {
			r = readRepository();
			save(r);
		}

		bridge = new BridgeRepository(r);
	}

	private void validate() {
		if (!this.location.isDirectory())
			throw new IllegalArgumentException("%s cannot be made a directory" + this.location);
	}

	File get(String bsn, Version version, Map<String,String> properties, DownloadListener... listeners)
			throws Exception {
		Resource resource = getBridge().get(bsn, version);
		if (resource == null)
			return null;

		ContentCapability contentCapability = ResourceUtils.getContentCapability(resource);

		if (contentCapability == null)
			return null;

		URI url = contentCapability.url();

		final File source = client.getCacheFileFor(url);
		final File link = new File(location, bsn + "-" + version + ".jar");

		IO.createSymbolicLinkOrCopy(link, source);

		Promise<File> go = client.build().useCache(MAX_STALE).async(url.toURL()).map(new Function<File,File>() {
			@Override
			public File apply(File t) {
				return link;
			}
		});

		if (listeners.length == 0)
			return go.getValue();

		new DownloadListenerPromise(reporter, name + ": get " + bsn + ";" + version + " " + url, go, listeners);
		return link;
	}

	List<String> list(String pattern) throws Exception {
		return getBridge().list(pattern);
	}

	SortedSet<Version> versions(String bsn) throws Exception {
		return getBridge().versions(bsn);
	}

	private Repository readFile() throws Exception {
		try (XMLResourceParser xp = new XMLResourceParser(this.indexFile.toURI())) {
			List<Resource> resources = xp.parse();
			return new ResourcesRepository(resources);
		}
	}

	private Repository readRepository() throws Exception {
		P2Impl p2 = new P2Impl(client, this.url, Processor.getExecutor());
		List<Artifact> artifacts = p2.getArtifacts();
		List<Promise<Resource>> fetched = new ArrayList<>(artifacts.size());
		Set<URI> visitedURIs = new HashSet<>(artifacts.size());
		Set<ArtifactID> visitedArtifacts = new HashSet<>(artifacts.size());
		Map<ArtifactID,Resource> knownResources = new HashMap<>();

		if (getBridge() != null) {
			for (Capability capability : getBridge().getRepository()
					.findProviders(singleton(MD5_REQUIREMENT))
					.get(MD5_REQUIREMENT)) {
				Resource resource = capability.getResource();
				IdentityCapability identity = ResourceUtils.getIdentityCapability(resource);
				ArtifactID artifact = new ArtifactID(identity.osgi_identity(), identity.version(),
						(String) capability.getAttributes().get(MD5_ATTRIBUTE));

				knownResources.put(artifact, resource);
			}
		}

		for (final Artifact a : artifacts) {
			if (!visitedURIs.add(a.uri))
				continue;
			if (a.md5 != null) {
				ArtifactID id = new ArtifactID(a.id, toVersion(a.version), a.md5);
				if (!visitedArtifacts.add(id))
					continue;
				if (knownResources.containsKey(id)) {
					fetched.add(Promises.resolved(knownResources.get(id)));
					continue;
				}
			}

			Promise<Resource> promise = client.build()
					.useCache(MAX_STALE)
					.async(a.uri.toURL())
					.map(new Function<File,Resource>() {
						@Override
						public Resource apply(File file) {
							try {
								ResourceBuilder rb = new ResourceBuilder();
								rb.addFile(file, a.uri);
								if (a.md5 != null)
									rb.addCapability(new CapabilityBuilder(P2_CAPABILITY_NAMESPACE)
											.addAttribute(MD5_ATTRIBUTE, a.md5));
								return rb.build();
							} catch (Exception e) {
								logger.debug("{}: Failed to create resource for %s from {}", name, a, file, e);
								return RECOVERY;
							}
						}
					})
					.recover(new Function<Promise< ? >,Resource>() {
						@Override
						public Resource apply(Promise< ? > failed) {
							try {
								logger.debug("{}: Failed to create resource for {}", name, a,
										failed.getFailure());
							} catch (InterruptedException e) {
								// impossible
							}
							return RECOVERY;
						}
					});
			fetched.add(promise);
		}

		Promise<List<Resource>> all = Promises.all(fetched);
		return all.map(new Function<List<Resource>,ResourcesRepository>() {
			@Override
			public ResourcesRepository apply(List<Resource> resources) {
				ResourcesRepository rr = new ResourcesRepository();
				for (Resource resource : resources) {
					if (resource != RECOVERY) {
						rr.add(resource);
					}
				}
				return rr;
			}
		}).getValue();
	}

	private void save(Repository repository) throws IOException, Exception {
		XMLResourceGenerator xrg = new XMLResourceGenerator();
		xrg.repository(repository).name(name).save(indexFile);
	}

	Map<Requirement,Collection<Capability>> findProviders(Collection< ? extends Requirement> requirements) {
		return getBridge().getRepository().findProviders(requirements);
	}

	public void refresh() throws Exception {
		Repository repository = readRepository();
		save(repository);
		this.bridge = new BridgeRepository(repository);
	}

	@Override
	public void close() throws IOException {}

	BridgeRepository getBridge() {
		return bridge;
	}
}
