package aQute.bnd.main;

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.Map.Entry;
import java.util.jar.*;

import javax.xml.transform.*;
import javax.xml.transform.stream.*;

import aQute.bnd.build.*;
import aQute.bnd.differ.*;
import aQute.bnd.differ.Baseline.BundleInfo;
import aQute.bnd.differ.Baseline.Info;
import aQute.bnd.header.*;
import aQute.bnd.osgi.*;
import aQute.bnd.osgi.Descriptors.PackageRef;
import aQute.bnd.service.diff.*;
import aQute.bnd.version.*;
import aQute.lib.collections.*;
import aQute.lib.getopt.*;
import aQute.lib.tag.*;

/**
 * Implements commands to maintain the Package versions db.
 */
public class BaselineCommands {
	static TransformerFactory	transformerFactory	= TransformerFactory.newInstance();
	final bnd					bnd;
	final Baseline				baseline;
	final DiffPluginImpl		differ				= new DiffPluginImpl();
	final Collection<String>	SKIP_HEADERS		= Arrays.asList(Constants.CREATED_BY, Constants.BND_LASTMODIFIED,
															Constants.BUNDLE_MANIFESTVERSION, "Manifest-Version",
															Constants.TOOL);

	BaselineCommands(bnd bnd) throws IOException {
		this.bnd = bnd;
		this.baseline = new Baseline(bnd, differ);
	}

	@Description("Compare a newer bundle to a baselined bundle and provide versioning advice")
	@Arguments(arg = {
			"[newer jar]", "[older jar]"
	})
	interface baseLineOptions extends Options {
		@Description("Output file with fixup info")
		String fixup();

		@Description("Show any differences")
		boolean diff();

		@Description("Be quiet, only report errors")
		boolean quiet();

		@Description("Show all, also unchanged")
		boolean all();
	}

	/**
	 * Compare
	 */

	@Description("Compare a newer bundle to a baselined bundle and provide versioning advice. If no parameters are given, and there "
			+ "is a local project, then we use the projects current build and the baseline jar in the release repo.")
	public void _baseline(baseLineOptions opts) throws Exception {

		List<String> args = opts._();
		if (args.size() == 0) {
			Project project = bnd.getProject();
			if (project != null) {
				for (Builder b : project.getSubBuilders()) {
					ProjectBuilder pb = (ProjectBuilder) b;
					Jar older = pb.getBaselineJar();
					if ( older == null) {
						bnd.error("No baseline JAR available. Did you set " + Constants.BASELINE);
						return;
					}
					try {
						pb.setProperty(Constants.BASELINE, ""); // do not do baselining in build
						// make sure disabling is after getting the baseline jar
						
						Jar newer = pb.build();
						try {
							differ.setIgnore(pb.getProperty(Constants.DIFFIGNORE));
							baseline(opts, newer, older);
							bnd.getInfo(b);
						}
						finally {
							newer.close();
						}
					}
					finally {
						older.close();
					}
				}
				bnd.getInfo(project);
				return;
			}
		}

		if (args.size() != 2) {
			throw new IllegalArgumentException("Accepts only two argument (<jar>)");
		}
		File newer = bnd.getFile(args.remove(0));
		if (!newer.isFile())
			throw new IllegalArgumentException("Not a valid newer input file: " + newer);

		File older = bnd.getFile(args.remove(0));
		if (!older.isFile())
			throw new IllegalArgumentException("Not a valid older input file: " + older);

		Jar nj = new Jar(newer);
		Jar oj = new Jar(older);
		baseline(opts, nj, oj);
	}

	private void baseline(baseLineOptions opts, Jar newer, Jar older) throws FileNotFoundException,
			UnsupportedEncodingException, IOException, Exception {
		PrintStream out = null;

		if (opts.fixup() != null) {
			out = new PrintStream(bnd.getFile(opts.fixup()), "UTF-8");
		}

		Set<Info> infos = baseline.baseline(newer, older, null);
		BundleInfo bundleInfo = baseline.getBundleInfo();

		Info[] sorted = infos.toArray(new Info[infos.size()]);
		Arrays.sort(sorted, new Comparator<Info>() {
			public int compare(Info o1, Info o2) {
				return o1.packageName.compareTo(o2.packageName);
			}
		});

		if (!opts.quiet()) {
			bnd.out.printf("===============================================================%n%s %s %s-%s", bundleInfo.mismatch ? '*' : ' ', bundleInfo.bsn, newer.getVersion(), older.getVersion());
			if (bundleInfo.mismatch && bundleInfo.suggestedVersion != null)
				bnd.out.printf(" suggests %s", bundleInfo.suggestedVersion);

			bnd.out.printf("%n===============================================================%n");

			boolean hadHeader = false;
			for (Info info : sorted) {
				if (info.packageDiff.getDelta() != Delta.UNCHANGED || opts.all()) {
					if (!hadHeader) {
						bnd.out.printf("  %-50s %-10s %-10s %-10s %-10s %-10s%n", "Package", "Delta", "New", "Old",
								"Suggest", "If Prov.");
						hadHeader = true;
					}
					bnd.out.printf("%s %-50s %-10s %-10s %-10s %-10s %-10s%n", info.mismatch ? '*' : ' ',
							info.packageName, //
							info.packageDiff.getDelta(), //
							info.newerVersion, //
							info.olderVersion.equals(Version.LOWEST) ? "-": info.olderVersion,//
							info.suggestedVersion.compareTo(info.newerVersion) <= 0 ? "ok" : info.suggestedVersion, //
							info.suggestedIfProviders == null ? "-" : info.suggestedIfProviders);
				}
			}
		}

		if (out != null) {
			// Create a fixup file

			Manifest manifest = newer.getManifest();
			if (manifest == null)
				manifest = new Manifest();

			for (Map.Entry<Object,Object> e : manifest.getMainAttributes().entrySet()) {
				String key = e.getKey().toString();

				if (!SKIP_HEADERS.contains(key)) {
					if (!Constants.EXPORT_PACKAGE.equals(key)) {
						out.printf("%-40s = ", key);
						String value = (String) e.getValue();
						out.append(value);
					}
					out.println();
				}
			}
			doExportPackage(sorted, out);
			out.close();
		}
	}

	/**
	 * Print out the packages from spec jars and check in which ees they appear.
	 * Example
	 * 
	 * <pre>
	 * package overview -ee j2se-1.6.0 -ee j2se-1.5.0 -ee j2ee-1.4.0 javax.activation-1.1.jar
	 * </pre>
	 */
	@Description("Print out the packages from spec jars and check in which ees they appear. Very specific. For example, schema ee.j2se-1.6.0 ee.j2se-1.5.0 ee.j2ee-1.4.0")
	interface schemaOptions extends Options {
		@Description("Output file")
		String output(String deflt);

		@Description("Specify an XSL file for pretty printing")
		String xsl();
	}

	class PSpec implements Comparable<PSpec> {
		String			packageName;
		Version			version;
		int				id;
		public Attrs	attrs;
		public Tree		tree;
		public Attrs	uses	= new Attrs();

		public int compareTo(PSpec o) {
			return version.compareTo(o.version);
		}

	}

	/**
	 * Create a schema of a set of jars outling the packages and their versions.
	 * This will create a list of packages with multiple versions, link to their
	 * specifications, and the deltas between versions.
	 * 
	 * <pre>
	 *    bnd package schema <file.jar>*
	 * </pre>
	 * 
	 * @param opts
	 * @throws Exception
	 */
	public void _schema(schemaOptions opts) throws Exception {
		MultiMap<String,PSpec> map = new MultiMap<String,PSpec>();

		Tag top = new Tag("jschema");
		int n = 1000;
		for (String spec : opts._()) {
			File f = bnd.getFile(spec);
			if (!f.isFile()) {
				bnd.messages.NoSuchFile_(f);
			} else {

				// For each specification jar we found

				bnd.trace("spec %s", f);
				Jar jar = new Jar(f); // spec
				Manifest m = jar.getManifest();
				Attributes main = m.getMainAttributes();
				Tag specTag = new Tag(top, "specification");
				specTag.addAttribute("jar", spec);
				specTag.addAttribute("name", main.getValue("Specification-Name"));
				specTag.addAttribute("title", main.getValue("Specification-Title"));
				specTag.addAttribute("jsr", main.getValue("Specification-JSR"));
				specTag.addAttribute("url", main.getValue("Specification-URL"));
				specTag.addAttribute("version", main.getValue("Specification-Version"));
				specTag.addAttribute("vendor", main.getValue("Specification-Vendor"));
				specTag.addAttribute("id", n);
				specTag.addContent(main.getValue(Constants.BUNDLE_DESCRIPTION));

				Parameters exports = OSGiHeader.parseHeader(m.getMainAttributes().getValue(Constants.EXPORT_PACKAGE));

				// Create a map with versions. Ensure import ranges overwrite
				// the
				// exported versions
				Parameters versions = new Parameters();
				versions.putAll(exports);
				versions.putAll(OSGiHeader.parseHeader(m.getMainAttributes().getValue(Constants.IMPORT_PACKAGE)));

				Analyzer analyzer = new Analyzer();
				analyzer.setJar(jar);
				analyzer.analyze();

				Tree tree = differ.tree(analyzer);

				for (Entry<String,Attrs> entry : exports.entrySet()) {

					// For each exported package in the specification JAR

					Attrs attrs = entry.getValue();
					String packageName = entry.getKey();
					PackageRef packageRef = analyzer.getPackageRef(packageName);

					String version = attrs.get(Constants.VERSION_ATTRIBUTE);

					PSpec pspec = new PSpec();
					pspec.packageName = packageName;
					pspec.version = new Version(version);
					pspec.id = n;
					pspec.attrs = attrs;
					pspec.tree = tree;

					Collection<PackageRef> uses = analyzer.getUses().get(packageRef);
					if (uses != null) {
						for (PackageRef x : uses) {
							if (x.isJava())
								continue;

							String imp = x.getFQN();

							if (imp.equals(packageName))
								continue;
							String v = null;
							if (versions.containsKey(imp))
								v = versions.get(imp).get(Constants.VERSION_ATTRIBUTE);
							pspec.uses.put(imp, v);
						}
					}
					map.add(packageName, pspec);
				}
				jar.close();
				n++;
			}
		}

		// We now gather all the information about all packages in the map.
		// Next phase is generating the XML. Sorting the packages is
		// important because XSLT is brain dead.

		SortedList<String> names = new SortedList<String>(map.keySet());

		Tag packagesTag = new Tag(top, "packages");
		Tag baselineTag = new Tag(top, "baseline");

		for (String pname : names) {

			// For each distinct package name

			SortedList<PSpec> specs = new SortedList<PSpec>(map.get(pname));

			PSpec older = null;
			Parameters olderExport = null;

			for (PSpec newer : specs) {

				// For each package in the total set

				Tag pack = new Tag(packagesTag, "package");
				pack.addAttribute("name", newer.packageName);
				pack.addAttribute("version", newer.version);
				pack.addAttribute("spec", newer.id);

				Parameters newerExport = new Parameters();
				newerExport.put(pname, newer.attrs);

				if (older != null) {
					String compareId = newer.packageName + "-" + newer.id + "-" + older.id;
					pack.addAttribute("delta", compareId);

					bnd.trace(" newer=%s older=%s", newerExport, olderExport);

					Set<Info> infos = baseline.baseline(newer.tree, newerExport, older.tree, olderExport,
							new Instructions(pname));

					for (Info info : infos) {
						Tag tag = getTag(info);
						tag.addAttribute("id", compareId);
						tag.addAttribute("newerSpec", newer.id);
						tag.addAttribute("olderSpec", older.id);
						baselineTag.addContent(tag);
					}
					older.tree = null;
					older.attrs = null;
					older = newer;
				}

				//
				// XRef, show the used packages for this package
				//

				for (Entry<String,String> uses : newer.uses.entrySet()) {
					Tag reference = new Tag(pack, "import");
					reference.addAttribute("name", uses.getKey());
					reference.addAttribute("version", uses.getValue());
				}

				older = newer;
				olderExport = newerExport;
			}
		}

		String o = opts.output("schema.xml");
		File of = bnd.getFile(o);
		File pof = of.getParentFile();
		if (!pof.exists() && !pof.mkdirs()) {
			throw new IOException("Could not create directory " + pof);
		}
		OutputStreamWriter fw = new OutputStreamWriter(new FileOutputStream(of), "UTF-8");
		try {
			PrintWriter pw = new PrintWriter(fw);
			try {
				pw.println("<?xml version='1.0'?>");
				top.print(0, pw);
			}
			finally {
				pw.close();
			}
		}
		finally {
			fw.close();
		}

		if (opts.xsl() != null) {
			URL home = bnd.getBase().toURI().toURL();
			URL xslt = new URL(home, opts.xsl());
			String path = of.getAbsolutePath();
			if (path.endsWith(".xml"))
				path = path.substring(0, path.length() - 4);

			path = path + ".html";
			File html = new File(path);
			bnd.trace("xslt %s %s %s %s", xslt, of, html, html.exists());
			FileOutputStream out = new FileOutputStream(html);
			try {
				Transformer transformer = transformerFactory.newTransformer(new StreamSource(xslt.openStream()));
				transformer.transform(new StreamSource(of), new StreamResult(out));
			}
			finally {
				out.close();
			}
		}
	}

	private Tag getTag(Info info) {
		Tag tag = new Tag("info");
		tag.addAttribute("name", info.packageName);
		tag.addAttribute("newerVersion", info.newerVersion);
		tag.addAttribute("olderVersion", info.olderVersion);
		tag.addAttribute("suggestedVersion", info.suggestedVersion);
		tag.addAttribute("suggestedIfProviders", info.suggestedIfProviders);
		tag.addAttribute("mismatch", info.mismatch);
		tag.addAttribute("warning", info.warning);
		StringBuilder sb = new StringBuilder();
		if (info.packageDiff.getDelta() == Delta.UNCHANGED)
			tag.addAttribute("equals", "true");
		else {
			traverseTag(sb, info.packageDiff, "");
			String s = sb.toString().trim();
			if (s.length() != 0) {
				Tag d = new Tag(tag, "diff", s);
				d.setCDATA();
			}
		}
		if (info.providers != null)
			for (String provider : info.providers) {
				Tag p = new Tag(tag, "provider");
				p.addAttribute("provider", provider);
			}
		return tag;
	}

	private void traverseTag(StringBuilder sb, Diff diff, String indent) {
		sb.append(indent);
		sb.append(diff.toString().trim().replace('\n', ' '));
		sb.append("\n");
		if (diff.getDelta() == Delta.ADDED || diff.getDelta() == Delta.REMOVED)
			return;

		for (Diff child : diff.getChildren()) {
			if (child.getDelta() != Delta.UNCHANGED && child.getDelta() != Delta.IGNORED)
				traverseTag(sb, child, indent + " ");
		}
	}

	/**
	 * @param exports
	 * @param out
	 * @throws IOException
	 */
	public void doExportPackage(Info[] infos, PrintStream out) throws IOException {

		out.printf("# Suggested versions\n%-40s = ", Constants.EXPORT_PACKAGE);
		String del = "";
		for (Info info : infos) {
			out.append(del);
			out.printf("\\\n  ");
			out.append(info.packageName);
			info.attributes.put(Constants.VERSION_ATTRIBUTE, info.suggestedVersion.toString());
			for (Map.Entry<String,String> clause : info.attributes.entrySet()) {
				if (clause.getKey().equals(Constants.USES_DIRECTIVE))
					continue;

				out.append(";\\\n    ");
				out.append(clause.getKey());
				out.append("=");
				Processor.quote(out, clause.getValue());
			}

			if (info.providers != null && !info.providers.isEmpty()) {
				out.append(";\\\n    " + Constants.PROVIDER_TYPE_DIRECTIVE + "=\"");
				String del2 = "";
				for (String part : info.providers) {
					out.append(del2);
					out.append("\\\n        ");
					out.append(part);
					del2 = ",";
				}
				out.append("\"");
			}
			del = ",";
		}
	}

}
