package aQute.bnd.repository.fileset;

import java.io.File;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.jar.JarFile;

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

import aQute.bnd.header.Attrs;
import aQute.bnd.osgi.Domain;
import aQute.bnd.osgi.repository.BaseRepository;
import aQute.bnd.osgi.repository.BridgeRepository;
import aQute.bnd.osgi.repository.ResourcesRepository;
import aQute.bnd.osgi.resource.CapabilityBuilder;
import aQute.bnd.osgi.resource.ResourceBuilder;
import aQute.bnd.osgi.resource.ResourceUtils;
import aQute.bnd.osgi.resource.ResourceUtils.ContentCapability;
import aQute.bnd.service.Plugin;
import aQute.bnd.service.RepositoryPlugin;
import aQute.bnd.util.repository.DownloadListenerPromise;
import aQute.bnd.version.Version;
import aQute.lib.exceptions.Exceptions;
import aQute.lib.strings.Strings;
import aQute.libg.cryptography.SHA256;
import aQute.service.reporter.Reporter;

public class FileSetRepository extends BaseRepository implements Plugin, RepositoryPlugin {
	private final static Logger					logger	= LoggerFactory.getLogger(FileSetRepository.class);
	private final String						name;
	private final Collection<File>				files;
	private final Deferred<BridgeRepository>	repository;
	private Reporter							reporter;

	public FileSetRepository(String name, Collection<File> files) throws Exception {
		this.name = name;
		this.files = files;
		this.repository = new Deferred<>();
	}

	private Collection<File> getFiles() {
		return files;
	}

	private BridgeRepository getBridge() throws Exception {
		Promise<BridgeRepository> promise = repository.getPromise();
		if (!promise.isDone()) {
			repository.resolveWith(readFiles());
		}
		return promise.getValue();
	}

	private Promise<BridgeRepository> readFiles() {
		List<Promise<Resource>> promises = new ArrayList<>(getFiles().size());
		for (File file : getFiles()) {
			promises.add(parse(Promises.resolved(file)));
		}
		Promise<List<Resource>> all = Promises.all(promises);
		return all.map(new Function<List<Resource>,BridgeRepository>() {
			@Override
			public BridgeRepository apply(List<Resource> resources) {
				try {
					ResourcesRepository rr = new ResourcesRepository();
					for (Resource r : resources) {
						if (r != null) {
							logger.debug("{}: adding resource {}", getName(), r);
							rr.add(r);
						}
					}
					return new BridgeRepository(rr);
				} catch (Exception e) {
					throw Exceptions.duck(e);
				}
			}
		});
	}

	private Promise<Resource> parse(Promise<File> file) {
		return file.map(new Function<File,Resource>() {
			@Override
			public Resource apply(File file) {
				try {
					if (!file.isFile()) {
						return null;
					}
					ResourceBuilder rb = new ResourceBuilder();
					try (JarFile jar = new JarFile(file)) {
						Domain manifest = Domain.domain(jar.getManifest());
						boolean hasIdentity = rb.addManifest(manifest);
						if (!hasIdentity) {
							return null;
						}
					} catch (Exception f) {
						return null;
					}
					logger.debug("{}: parsing {}", getName(), file);
					Attrs attrs = new Attrs();
					attrs.put(ContentNamespace.CAPABILITY_URL_ATTRIBUTE, file.toURI().toString());
					attrs.putTyped(ContentNamespace.CAPABILITY_SIZE_ATTRIBUTE, file.length());
					attrs.put(ContentNamespace.CONTENT_NAMESPACE, SHA256.digest(file).asHex());
					rb.addCapability(CapabilityBuilder.createCapReqBuilder(ContentNamespace.CONTENT_NAMESPACE, attrs));
					return rb.build();
				} catch (Exception e) {
					logger.debug("{}: failed to parse {}", getName(), file, e);
					throw Exceptions.duck(e);
				}
			}
		});
	}

	@Override
	public File get(String bsn, Version version, Map<String,String> properties, DownloadListener... listeners)
			throws Exception {
		Promise<File> promise = get(bsn, version);
		if (promise == null) {
			return null;
		}
		if (listeners.length != 0) {
			new DownloadListenerPromise(reporter, "Get " + bsn + "-" + version + " for " + getName(), promise,
					listeners);
		}
		return promise.getValue();
	}

	private Promise<File> get(final String bsn, final Version version) throws Exception {
		logger.debug("{}: get {} {}", getName(), bsn, version);
		Resource resource = getBridge().get(bsn, version);
		if (resource == null) {
			logger.debug("{}: resource not found {} {}", getName(), bsn, version);
			return null;
		}
		ContentCapability content = ResourceUtils.getContentCapability(resource);
		if (content == null) {
			logger.warn("{}: No content capability for {}", getName(), resource);
			return null;
		}
		URI uri = content.url();
		if (uri == null) {
			logger.warn("{}: No content URI for {}", getName(), resource);
			return null;
		}
		logger.debug("{}: get returning {}", getName(), uri);
		return Promises.resolved(new File(uri));
	}

	@Override
	public boolean canWrite() {
		return false;
	}

	@Override
	public PutResult put(InputStream stream, PutOptions options) throws Exception {
		throw new UnsupportedOperationException("Read only");
	}

	@Override
	public List<String> list(String pattern) throws Exception {
		List<String> bsns = getBridge().list(pattern);
		logger.debug("{}: list({}) {}", getName(), pattern, bsns);
		return bsns;
	}

	@Override
	public SortedSet<Version> versions(String bsn) throws Exception {
		SortedSet<Version> versions = getBridge().versions(bsn);
		logger.debug("{}: versions({}) {}", getName(), bsn, versions);
		return versions;
	}

	@Override
	public String getName() {
		return name;
	}

	@Override
	public String getLocation() {
		try {
			return Strings.join(list(null));
		} catch (Exception e) {
			return Strings.join(getFiles());
		}
	}

	@Override
	public String toString() {
		return String.format("%s [%-40s r/w=%s]", getName(), getLocation(), canWrite());
	}

	@Override
	public Map<Requirement,Collection<Capability>> findProviders(Collection< ? extends Requirement> requirements) {
		try {
			return getBridge().getRepository().findProviders(requirements);
		} catch (Exception e) {
			throw Exceptions.duck(e);
		}
	}

	@Override
	public void setProperties(Map<String,String> map) throws Exception {
		// ignored
	}

	@Override
	public void setReporter(Reporter reporter) {
		this.reporter = reporter;
	}
}
