package aQute.bnd.repository.maven.provider;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.osgi.resource.Capability;
import org.osgi.resource.Requirement;
import org.osgi.resource.Resource;
import org.osgi.util.function.Function;
import org.osgi.util.promise.Failure;
import org.osgi.util.promise.Promise;
import org.osgi.util.promise.Promises;

import aQute.bnd.header.Attrs;
import aQute.bnd.osgi.Domain;
import aQute.bnd.osgi.resource.ResourceBuilder;
import aQute.bnd.osgi.resource.ResourceUtils;
import aQute.bnd.service.repository.Phase;
import aQute.bnd.service.repository.SearchableRepository.ResourceDescriptor;
import aQute.bnd.version.Version;
import aQute.lib.collections.MultiMap;
import aQute.lib.io.IO;
import aQute.lib.json.JSONCodec;
import aQute.lib.strings.Strings;
import aQute.libg.cryptography.SHA1;
import aQute.libg.cryptography.SHA256;
import aQute.maven.api.Archive;
import aQute.maven.api.IMavenRepo;
import aQute.maven.api.Program;
import aQute.service.reporter.Reporter;

/**
 * Maintains an index of descriptors of archives. These descriptors are stored
 * in the maven repository
 */
class IndexFile {

	private static final JSONCodec CODEC = new JSONCodec();

	public class BundleDescriptor extends ResourceDescriptor {
		public long		lastModified;
		public Archive	archive;
		public boolean	merged;
		String			error;
		Promise<File>	promise;
		Resource		resource;

		public synchronized Resource getResource() {
			if (resource == null) {

				try {
					File f = promise.getValue();
					ResourceBuilder rb = new ResourceBuilder();
					rb.addFile(f, f.toURI());
					resource = rb.build();
				} catch (Exception e) {
					reporter.exception(e, "Failed to get file for %s", archive);
					resource = ResourceUtils.DUMMY_RESOURCE;
				}
			}
			return resource;
		}
	}

	private final ConcurrentMap<Archive,Promise<File>>		promises	= new ConcurrentHashMap<>();
	final ConcurrentMap<Archive,BundleDescriptor>		descriptors	= new ConcurrentHashMap<>();
	final File											indexFile;
	final File											cacheDir;
	private final IMavenRepo								repo;
	private final Reporter									reporter;
	private long									lastModified;
	private AtomicBoolean							refresh		= new AtomicBoolean();
	private final ReadWriteLock							lock		= new ReentrantReadWriteLock();

	IndexFile(Reporter reporter, File file, IMavenRepo repo) throws Exception {
		this.reporter = reporter;
		this.indexFile = file;
		this.repo = repo;
		this.cacheDir = new File(indexFile.getParentFile(), indexFile.getName() + ".info");
	}

	void open() throws Exception {
		loadIndexFile();
		sync();
	}

	private void sync() throws Exception {
		List<Promise<Void>> sync = new ArrayList<>(promises.size());
		for (Iterator<Entry<Archive,Promise<File>>> i = promises.entrySet().iterator(); i.hasNext();) {
			Entry<Archive,Promise<File>> entry = i.next();
			final Archive archive = entry.getKey();
			Promise<File> promise = entry.getValue();
			i.remove();
			sync.add(promise.<Void> then(null, new Failure() {
				@Override
				public void fail(Promise< ? > resolved) throws Exception {
					reporter.exception(resolved.getFailure(), "Failed to sync %s", archive);
				}
			}));
		}
		Promises.all(sync).getFailure(); // block until all promises resolved
	}

	BundleDescriptor add(Archive archive) throws Exception {
		BundleDescriptor old = descriptors.putIfAbsent(archive, createInitialDescriptor(archive));
		BundleDescriptor descriptor = descriptors.get(archive);
		updateDescriptor(descriptor, repo.get(archive).getValue());
		if (old == null || !Arrays.equals(descriptor.id, old.id)) {
			saveIndexFile();
			return descriptor;
		}
		return null;
	}

	BundleDescriptor remove(Archive archive) throws Exception {
		BundleDescriptor descriptor = descriptors.remove(archive);
		if (descriptor != null) {
			saveIndexFile();
		}
		return descriptor;
	}

	public void remove(String bsn) throws Exception {
		for (Iterator<BundleDescriptor> bd = descriptors.values().iterator(); bd.hasNext();) {
			if (isBsn(bsn, bd.next()))
				bd.remove();
		}
		saveIndexFile();
	}

	Collection<String> list() {
		Set<String> result = new HashSet<>();
		for (BundleDescriptor descriptor : descriptors.values()) {
			result.add(descriptor.bsn);
		}
		return result;
	}

	Collection<Version> list(String bsn) {

		Set<Version> result = new HashSet<>();
		for (BundleDescriptor descriptor : descriptors.values()) {
			if (isBsn(bsn, descriptor)) {
				result.add(descriptor.version);
			}
		}
		return result;
	}

	boolean isBsn(String bsn, BundleDescriptor descriptor) {
		return bsn.equals(descriptor.bsn) || bsn.equals(descriptor.archive.getWithoutVersion());
	}

	public synchronized BundleDescriptor getDescriptor(String bsn, Version version) throws Exception {
		sync();
		for (BundleDescriptor descriptor : descriptors.values()) {
			if (isBsn(bsn, descriptor) && version.equals(descriptor.version)) {
				return descriptor;
			}
		}
		return null;
	}

	int getErrors(String name) {
		int errors = 0;
		for (BundleDescriptor bd : descriptors.values())
			if ((name == null || name.equals(bd.bsn)) && bd.error != null)
				errors++;
		return errors;
	}

	Set<Program> getProgramsForBsn(String name) {
		Set<Program> set = new HashSet<>();
		for (BundleDescriptor bd : descriptors.values())
			if (name == null || name.equals(bd.bsn)) {
				set.add(bd.archive.revision.program);
			}
		return set;
	}

	long last = 0L;

	boolean refresh() throws Exception {

		refresh.getAndSet(false);

		if (indexFile.lastModified() != lastModified && last + 10000 < System.currentTimeMillis()) {
			loadIndexFile();
			last = System.currentTimeMillis();

			return true;
		} else {
			boolean refreshed = false;
			for (BundleDescriptor bd : descriptors.values()) {
				if ((bd.promise != null) && bd.promise.isDone() && (bd.promise.getFailure() == null)) {
					File f = bd.promise.getValue();
					if ((f != null) && f.isFile() && (f.lastModified() != bd.lastModified)) {
						updateDescriptor(bd, f);
						refreshed = true;
					}
				}
			}
			if (refreshed)
				return true;
		}
		return false;
	}

	private void loadIndexFile() throws Exception {
		lastModified = indexFile.lastModified();
		Set<Archive> toBeDeleted = new HashSet<>(descriptors.keySet());
		if (indexFile.isFile()) {
			lock.readLock().lock();
			try (BufferedReader rdr = IO.reader(indexFile)) {
				String line;
				while ((line = rdr.readLine()) != null) {
					line = Strings.trim(line);
					if (line.startsWith("#") || line.isEmpty())
						continue;

					Archive a = Archive.valueOf(line);
					if (a == null) {
						reporter.error("MavenBndRepository: invalid entry %s in file %s", line, indexFile);
					} else {
						toBeDeleted.remove(a);
						loadDescriptorAsync(a);
					}
				}
			} finally {
				lock.readLock().unlock();
			}

			this.descriptors.keySet().removeAll(toBeDeleted);
			this.promises.keySet().removeAll(toBeDeleted);
		}
	}

	void updateDescriptor(final Archive archive, File file) {
		BundleDescriptor descriptor = descriptors.get(archive);
		updateDescriptor(descriptor, file);
	}

	void updateDescriptor(BundleDescriptor descriptor, File file) {
		try {
			if (file == null || !file.isFile()) {
				File descriptorFile = getDescriptorFile(descriptor.archive);
				IO.delete(descriptorFile);
				reporter.error("Could not find file %s", descriptor.archive);
				descriptor.error = "File not found";
			} else {
				if (descriptor.lastModified != file.lastModified()) {

					Domain m = Domain.domain(file);
					if (m == null)
						m = Domain.domain(Collections.<String, String> emptyMap());

					Entry<String,Attrs> bsn = m.getBundleSymbolicName();
					descriptor.bsn = bsn != null ? bsn.getKey() : null;
					descriptor.version = m.getBundleVersion() == null ? null
							: Version.parseVersion(m.getBundleVersion());

					if (descriptor.bsn == null) {
						descriptor.bsn = descriptor.archive.getWithoutVersion();
						descriptor.version = descriptor.archive.revision.version.getOSGiVersion();
					} else if (descriptor.version == null)
						descriptor.version = Version.LOWEST;
					descriptor.description = m.getBundleDescription();
					descriptor.id = SHA1.digest(file).digest();
					descriptor.included = false;
					descriptor.lastModified = file.lastModified();
					descriptor.sha256 = SHA256.digest(file).digest();
					saveDescriptor(descriptor);
					refresh.set(true);
				}
				if (descriptor.promise == null && file != null)
					descriptor.promise = Promises.resolved(file);
			}
		} catch (Exception e) {
			e.printStackTrace();
			descriptor.error = e.toString();
			refresh.set(true);

		}
	}

	private void saveDescriptor(BundleDescriptor descriptor) throws IOException, Exception {
		File df = getDescriptorFile(descriptor.archive);
		IO.mkdirs(df.getParentFile());
		CODEC.enc().to(df).put(descriptor);
	}

	private void loadDescriptorAsync(final Archive archive) throws Exception {
		if (updateLocal(archive))
			return;

		updateAsync(archive);
	}

	private boolean updateLocal(final Archive archive) {
		File archiveFile = repo.toLocalFile(archive);
		if (archiveFile.isFile()) {
			File descriptorFile = getDescriptorFile(archive);
			if (descriptorFile.isFile()) {
				try {
					BundleDescriptor descriptor = CODEC.dec().from(descriptorFile).get(BundleDescriptor.class);
					descriptor.promise = Promises.resolved(archiveFile);
					descriptor.resource = null;
					descriptors.put(archive, descriptor);
					return true;
				} catch (Exception e) {
					// ignore
				}
			}
		}
		return false;
	}

	Promise<File> updateAsync(Archive archive) throws Exception {
		Promise<File> promise = repo.get(archive);
		return updateAsync(archive, promise);
	}

	private Promise<File> updateAsync(Archive archive, Promise<File> promise) throws Exception {
		BundleDescriptor descriptor = createInitialDescriptor(archive);
		promise = updateAsync(descriptor, promise);
		descriptors.put(archive, descriptor);
		return promise;
	}

	Promise<File> updateAsync(final BundleDescriptor descriptor, Promise<File> promise) throws Exception {
		descriptor.promise = promise.map(new Function<File,File>() {
			@Override
			public File apply(File file) {
				updateDescriptor(descriptor, file);
				return file;
			}
		});
		descriptor.resource = null;
		promises.put(descriptor.archive, descriptor.promise);
		return descriptor.promise;
	}

	File getDescriptorFile(Archive archive) {
		File dir = new File(repo.toLocalFile(archive).getParentFile(), ".bnd");
		return new File(dir, archive.getName());
	}

	private BundleDescriptor createInitialDescriptor(Archive archive) throws Exception {
		BundleDescriptor descriptor = new BundleDescriptor();
		descriptor.archive = archive;
		descriptor.phase = archive.isSnapshot() ? Phase.STAGING : Phase.MASTER;
		descriptor.url = repo.toRemoteURI(archive);
		descriptor.bsn = archive.getWithoutVersion();
		descriptor.version = archive.revision.version.getOSGiVersion();
		return descriptor;
	}

	private void saveIndexFile() throws Exception {
		lock.writeLock().lock();
		try {
			File tmp = File.createTempFile("index", null);
			try (PrintWriter pw = new PrintWriter(tmp)) {
				List<Archive> archives = new ArrayList<>(this.descriptors.keySet());
				Collections.sort(archives);
				for (Archive archive : archives) {
					pw.println(archive);
				}
			}
			Files.move(tmp.toPath(), indexFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
		} finally {
			lock.writeLock().unlock();
		}

		lastModified = indexFile.lastModified();
	}

	public void save() throws Exception {
		saveIndexFile();
	}

	public Collection<Archive> getArchives() {
		return descriptors.keySet();
	}

	@SuppressWarnings({
			"unchecked", "rawtypes"
	})
	public Map<Requirement,Collection<Capability>> findProviders(Collection< ? extends Requirement> requirements) {
		MultiMap<Requirement,Capability> providers = new MultiMap<>();
		for (BundleDescriptor bd : descriptors.values()) {
			Resource r = bd.getResource();
			if (r != null) {
				for (Requirement req : requirements) {
					for (Capability cap : r.getCapabilities(req.getNamespace())) {
						if (ResourceUtils.matches(req, cap))
							providers.add(req, cap);
					}
				}
			}
		}
		return (Map) providers;
	}

}
