package aQute.bnd.jpm;

import java.io.File;
import java.io.FileWriter;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Formatter;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;

import aQute.bnd.version.Version;
import aQute.lib.converter.TypeReference;
import aQute.lib.io.IO;
import aQute.lib.json.Decoder;
import aQute.lib.json.JSONCodec;
import aQute.libg.reporter.ReporterAdapter;
import aQute.service.library.Library;
import aQute.service.library.Library.RevisionRef;
import aQute.service.library.Revisions;
import aQute.service.reporter.Reporter;
import aQute.struct.struct;

public class Index {
	Reporter reporter = new ReporterAdapter(System.out);

	static public class Repo extends struct {
		public List<Library.RevisionRef>	revisionRefs	= list();
		public boolean						learning		= true;
		public boolean						recurse			= false;

		byte[]								synced;

		// Manually print this so that we
		// have 1 line per resource, making it easier on git
		// to compare.
		void write(Formatter format) throws Exception {
			codec.enc().indent(" ").to(format.out()).put(this);
			format.flush();
		}
	}

	private static final SortedSet<Version>						EMPTY_VERSIONS	= Collections
			.unmodifiableSortedSet(new TreeSet<Version>());

	private File												indexFile;

	private Map<String,TreeMap<Version,Library.RevisionRef>>	cache;

	private Repo												repo;

	public Index(File file) {
		this.indexFile = file;
	}

	static final JSONCodec	codec	= new JSONCodec();

	private boolean			dirty;

	private void init() throws Exception {
		if (repo == null) {
			cache = new TreeMap<String,TreeMap<Version,Library.RevisionRef>>();

			if (indexFile.isFile() && indexFile.length() > 100) {
				try (Decoder dec = codec.dec()) {
					repo = dec.from(indexFile).get(new TypeReference<Repo>() {});
					for (Library.RevisionRef r : repo.revisionRefs) {
						TreeMap<Version,Library.RevisionRef> map = cache.get(r.bsn);
						if (map == null) {
							map = new TreeMap<Version,Library.RevisionRef>(Collections.reverseOrder());
							cache.put(r.bsn, map);
						}
						Version v = toVersion(r.baseline, r.qualifier);
						map.put(v, r);
					}
				}
			} else {
				repo = new Repo();
			}
		}
	}

	public Set<String> getBsns() throws Exception {
		init();
		return cache.keySet();
	}

	public SortedSet<Version> getVersions(String bsn) throws Exception {
		init();

		TreeMap<Version,Library.RevisionRef> map = cache.get(bsn);
		if (map == null)
			return EMPTY_VERSIONS;

		return map.navigableKeySet();
	}

	public boolean addRevision(Library.RevisionRef ref) throws Exception {
		init();
		Version v = toVersion(ref.baseline, ref.qualifier);
		TreeMap<Version,Library.RevisionRef> map = cache.get(ref.bsn);
		if (map == null) {
			map = new TreeMap<Version,Library.RevisionRef>();
			cache.put(ref.bsn, map);
		}

		map.put(v, ref);
		dirty = true;
		repo.revisionRefs.add(ref);
		return true;
	}

	@SuppressWarnings("deprecation")
	public Library.RevisionRef getRevisionRef(String bsn, Version version) throws Exception {
		init();
		Map<Version,Library.RevisionRef> map = cache.get(bsn);
		if (map == null)
			return null;

		// Fixup the change from ref.url to ref.urls ...
		boolean save = false;

		RevisionRef ref = map.get(version);
		if (ref == null) {
			return null;
		}

		if (ref.urls.isEmpty() && ref.url != null) {
			ref.urls.add(ref.url);
			ref.url = null;
			save = true;
		}
		// Another fixup, have file urls in the refs :-(

		if (Boolean.getBoolean("jpm4j.in.test") == false) {
			for (Iterator<URI> i = ref.urls.iterator(); i.hasNext();) {
				URI uri = i.next();
				if (uri.getScheme().equalsIgnoreCase("file")) {
					i.remove();
					save = true;
				}
			}
		}
		if (save)
			save();

		return ref;
	}

	public boolean delete(String bsn, Version v) throws Exception {
		init();
		Map<Version,Library.RevisionRef> map = cache.get(bsn);
		if (map != null) {
			try {
				Library.RevisionRef removed = map.remove(v);
				if (removed != null) {
					for (Iterator<RevisionRef> i = repo.revisionRefs.iterator(); i.hasNext();) {
						RevisionRef other = i.next();
						if (Arrays.equals(other.revision, removed.revision)) {
							i.remove();
						}
					}
					repo.revisionRefs.remove(removed);
					dirty = true;
					return true;
				}
			} finally {
				if (map.isEmpty())
					cache.remove(bsn);
			}
		}

		return false;
	}

	public void save(Writer out) throws Exception {
		init();
		repo.revisionRefs = getRevisionRefs();
		Formatter f = new Formatter(out);
		repo.write(f);
	}

	public void save(OutputStream out) throws Exception {
		save(new OutputStreamWriter(out));
	}

	public void save(File out) throws Exception {
		File tmp = IO.createTempFile(out.getParentFile(), out.getName(), ".tmp");
		try (FileWriter fout = new FileWriter(tmp)) {
			save(fout);
		}
		IO.rename(tmp, out);
	}

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

	void save(boolean force) throws Exception {
		if (dirty || force)
			save(indexFile);
		dirty = false;
	}

	static Version toVersion(String baseline, String qualifier) {
		if (qualifier == null || qualifier.isEmpty())
			return new Version(baseline);
		else
			return new Version(baseline + "." + qualifier);
	}

	public static Version toVersion(Library.RevisionRef ref) {
		return toVersion(ref.baseline, ref.qualifier);
	}

	public Library.RevisionRef getRevisionRef(byte[] sha) throws Exception {
		init();
		for (SortedMap<Version, ? extends RevisionRef> list : cache.values()) {
			for (RevisionRef r : list.values()) {
				if (Arrays.equals(sha, r.revision))
					return r;
			}
		}
		return null;
	}

	public List<RevisionRef> getRevisionRefs() throws Exception {
		init();
		List<RevisionRef> refs = new ArrayList<Library.RevisionRef>();
		for (SortedMap<Version, ? extends RevisionRef> list : cache.values()) {
			refs.addAll(list.values());
		}
		return refs;
	}

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

	public boolean isLearning() throws Exception {
		init();
		return repo.learning;
	}

	public void setLearning(boolean learning) {
		dirty |= repo.learning != learning;
		repo.learning = learning;

	}

	public boolean isRecurse() {
		return repo.recurse;
	}

	public void setRecurse(boolean recurse) throws Exception {
		init();
		dirty |= repo.recurse != recurse;
		repo.recurse = recurse;
	}

	public boolean isDirty() {
		return dirty;
	}

	public Revisions getRevisions() throws Exception {
		init();
		Revisions revisions = new Revisions();
		for (RevisionRef ref : repo.revisionRefs)
			revisions.content.add(ref.revision);
		revisions._id = Revisions.checksum(revisions);
		return revisions;
	}

	public boolean isSynced() throws Exception {
		init();
		return repo.synced != null && Arrays.equals(repo.synced, getRevisions()._id);
	}

	public byte[] getSynced() {
		return repo.synced;
	}

	public void setSynced(byte[] revisions) throws Exception {
		repo.synced = revisions;
		save();
	}
}
