package aQute.bnd.differ;

import static aQute.bnd.service.diff.Delta.*;
import static aQute.bnd.service.diff.Type.*;
import static java.lang.reflect.Modifier.*;

import java.lang.reflect.*;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.atomic.*;
import java.util.jar.*;
import java.util.regex.*;

import aQute.bnd.annotation.*;
import aQute.bnd.header.*;
import aQute.bnd.osgi.*;
import aQute.bnd.osgi.Clazz.JAVA;
import aQute.bnd.osgi.Clazz.MethodDef;
import aQute.bnd.osgi.Descriptors.PackageRef;
import aQute.bnd.osgi.Descriptors.TypeRef;
import aQute.bnd.service.diff.*;
import aQute.bnd.service.diff.Type;
import aQute.bnd.version.Version;
import aQute.lib.collections.*;
import aQute.libg.generics.*;

/**
 * An element that compares the access field in a binary compatible way. This
 * element is used for classes, methods, constructors, and fields. For that
 * reason we also included the only method that uses this class as a static
 * method.
 * <p>
 * Packages
 * <ul>
 * <li>MAJOR - Remove a public type
 * <li>MINOR - Add a public class
 * <li>MINOR - Add an interface
 * <li>MINOR - Add a method to a class
 * <li>MINOR - Add a method to a provider interface
 * <li>MAJOR - Add a method to a consumer interface
 * <li>MINOR - Add a field
 * <li>MICRO - Add an annotation to a member
 * <li>MINOR - Change the value of a constant
 * <li>MICRO - -abstract
 * <li>MICRO - -final
 * <li>MICRO - -protected
 * <li>MAJOR - +abstract
 * <li>MAJOR - +final
 * <li>MAJOR - +protected
 * </ul>
 */

class JavaElement {
	static Pattern						PARAMETERS_P	= Pattern.compile(".*(\\(.*\\)).*");

	final static EnumSet<Type>			INHERITED		= EnumSet.of(FIELD, METHOD, EXTENDS, IMPLEMENTS);
	private static final Element		PROTECTED		= new Element(ACCESS, "protected", null, MAJOR, MINOR, null);
	private static final Element		STATIC			= new Element(ACCESS, "static", null, MAJOR, MAJOR, null);
	private static final Element		ABSTRACT		= new Element(ACCESS, "abstract", null, MAJOR, MINOR, null);
	private static final Element		FINAL			= new Element(ACCESS, "final", null, MAJOR, MINOR, null);
	// private static final Element DEPRECATED = new Element(ACCESS,
	// "deprecated", null,
	// CHANGED, CHANGED, null);

	final Analyzer						analyzer;
	final Map<PackageRef,Instructions>	providerMatcher	= Create.map();
	final Set<TypeRef>					notAccessible	= Create.set();
	final Map<Object,Element>			cache			= Create.map();
	MultiMap<PackageRef, //
	Element>							packages;
	final MultiMap<TypeRef, //
	Element>							covariant		= new MultiMap<TypeRef,Element>();
	final Set<JAVA>						javas			= Create.set();
	final Packages						exports;

	/**
	 * Create an element for the API. We take the exported packages and traverse
	 * those for their classes. If there is no manifest or it does not describe
	 * a bundle we assume the whole contents is exported.
	 * 
	 * @param infos
	 */
	JavaElement(Analyzer analyzer) throws Exception {
		this.analyzer = analyzer;

		Manifest manifest = analyzer.getJar().getManifest();
		if (manifest != null && manifest.getMainAttributes().getValue(Constants.BUNDLE_MANIFESTVERSION) != null) {
			exports = new Packages();
			for (Map.Entry<String,Attrs> entry : OSGiHeader.parseHeader(
					manifest.getMainAttributes().getValue(Constants.EXPORT_PACKAGE)).entrySet())
				exports.put(analyzer.getPackageRef(entry.getKey()), entry.getValue());
		} else
			exports = analyzer.getContained();
		//
		// We have to gather the -providers and parse them into instructions
		// so we can efficiently match them during class parsing to find
		// out who the providers and consumers are
		//

		for (Entry<PackageRef,Attrs> entry : exports.entrySet()) {
			String value = entry.getValue().get(Constants.PROVIDER_TYPE_DIRECTIVE);
			if (value != null) {
				providerMatcher.put(entry.getKey(), new Instructions(value));
			}
		}

		// we now need to gather all the packages but without
		// creating the packages yet because we do not yet know
		// which classes are accessible

		packages = new MultiMap<PackageRef,Element>();

		for (Clazz c : analyzer.getClassspace().values()) {
			if (c.isPublic() || c.isProtected()) {
				PackageRef packageName = c.getClassName().getPackageRef();

				if (exports.containsKey(packageName)) {
					Element cdef = classElement(c);
					packages.add(packageName, cdef);
				}
			}
		}

	}

	static Element getAPI(Analyzer analyzer) throws Exception {
		analyzer.analyze();
		JavaElement te = new JavaElement(analyzer);
		return te.getLocalAPI();
	}

	private Element getLocalAPI() throws Exception {
		Set<Element> result = new HashSet<Element>();

		for (Map.Entry<PackageRef,List<Element>> entry : packages.entrySet()) {
			List<Element> set = entry.getValue();
			for (Iterator<Element> i = set.iterator(); i.hasNext();) {

				if (notAccessible.contains(analyzer.getTypeRefFromFQN(i.next().getName())))
					i.remove();

			}
			String version = exports.get(entry.getKey()).get(Constants.VERSION_ATTRIBUTE);
			if (version != null) {
				Version v = new Version(version);
				set.add(new Element(Type.VERSION, v.getWithoutQualifier().toString(), null, IGNORED, IGNORED, null));
			}
			Element pd = new Element(Type.PACKAGE, entry.getKey().getFQN(), set, MINOR, MAJOR, null);
			result.add(pd);
		}

		for (JAVA java : javas) {
			result.add(new Element(CLASS_VERSION, java.toString(), null, Delta.CHANGED, Delta.CHANGED, null));
		}

		return new Element(Type.API, "<api>", result, CHANGED, CHANGED, null);
	}

	/**
	 * Calculate the class element. This requires parsing the class file and
	 * finding all the methods that were added etc. The parsing will take super
	 * interfaces and super classes into account. For this reason it maintains a
	 * queue of classes/interfaces to parse.
	 * 
	 * @param analyzer
	 * @param clazz
	 * @param infos
	 * @return
	 * @throws Exception
	 */
	Element classElement(final Clazz clazz) throws Exception {
		Element e = cache.get(clazz);
		if (e != null)
			return e;

		final StringBuilder comment = new StringBuilder();
		final Set<Element> members = new LinkedHashSet<Element>();
		final Set<MethodDef> methods = new LinkedHashSet<MethodDef>();
		final Set<Clazz.FieldDef> fields = Create.set();
		final MultiMap<Clazz.Def,Element> annotations = new MultiMap<Clazz.Def,Element>();

		final TypeRef name = clazz.getClassName();

		final String fqn = name.getFQN();
		final String shortName = name.getShortName();

		// Check if this clazz is actually a provider or not
		// providers must be listed in the exported package in the
		// PROVIDER_TYPE directive.
		Instructions matchers = providerMatcher.get(name.getPackageRef());
		boolean p = matchers != null && matchers.matches(shortName);
		final AtomicBoolean provider = new AtomicBoolean(p);

		//
		// Check if we already had this clazz in the cache
		//

		Element before = cache.get(clazz); // for super classes
		if (before != null)
			return before;

		clazz.parseClassFileWithCollector(new ClassDataCollector() {
			boolean			memberEnd;
			Clazz.FieldDef	last;

			@Override
			public void version(int minor, int major) {
				javas.add(Clazz.JAVA.getJava(major, minor));
			}

			@Override
			public void method(MethodDef defined) {
				if ((defined.isProtected() || defined.isPublic())) {
					last = defined;
					methods.add(defined);
				} else {
					last = null;
				}
			}

			@Override
			public void deprecated() {
				if (memberEnd)
					clazz.setDeprecated(true);
				else if (last != null)
					last.setDeprecated(true);
			}

			@Override
			public void field(Clazz.FieldDef defined) {
				if (defined.isProtected() || defined.isPublic()) {
					last = defined;
					fields.add(defined);
				} else
					last = null;
			}

			@Override
			public void constant(Object o) {
				if (last != null) {
					// Must be accessible now
					last.setConstant(o);
				}
			}

			@Override
			public void extendsClass(TypeRef name) throws Exception {
				String comment = null;
				if (!clazz.isInterface())
					comment = inherit(members, name);

				Clazz c = analyzer.findClass(name);
				if ((c == null || c.isPublic()) && !name.isObject())
					members.add(new Element(Type.EXTENDS, name.getFQN(), null, MICRO, MAJOR, comment));
			}

			@Override
			public void implementsInterfaces(TypeRef names[]) throws Exception {
				// TODO is interface reordering important for binary
				// compatibility??

				for (TypeRef name : names) {

					String comment = null;
					if (clazz.isInterface() || clazz.isAbstract())
						comment = inherit(members, name);
					members.add(new Element(Type.IMPLEMENTS, name.getFQN(), null, MINOR, MAJOR, comment));
				}
			}

			/**
			 * @param members
			 * @param name
			 * @param comment
			 * @return
			 */
			Set<Element>	OBJECT	= Create.set();

			public String inherit(final Set<Element> members, TypeRef name) throws Exception {
				if (name.isObject()) {
					if (OBJECT.isEmpty()) {
						Clazz c = analyzer.findClass(name);
						Element s = classElement(c);
						for (Element child : s.children) {
							if (INHERITED.contains(child.type)) {
								String n = child.getName();
								if (child.type == METHOD) {
									if (n.startsWith("<init>") || "getClass()".equals(child.getName())
											|| n.startsWith("wait(") || n.startsWith("notify(")
											|| n.startsWith("notifyAll("))
										continue;
								}
								OBJECT.add(child);
							}
						}
					}
					members.addAll(OBJECT);
				} else {

					Clazz c = analyzer.findClass(name);
					if (c == null) {
						return "Cannot load " + name;
					}
					Element s = classElement(c);
					for (Element child : s.children) {
						if (INHERITED.contains(child.type) && !child.name.startsWith("<")) {
							members.add(child);
						}
					}
				}
				return null;
			}

			/**
			 * Deprecated annotations and Provider/Consumer Type (both bnd and
			 * OSGi) are treated special. Other annotations are turned into a
			 * tree. Starting with ANNOTATED, and then properties. A property is
			 * a PROPERTY property or an ANNOTATED property if it is an
			 * annotation. If it is an array, the key is suffixed with the
			 * index.
			 * 
			 * <pre>
			 * public @interface Outer {
			 *   Inner[] value();
			 * }
			 * public @interface Inner {
			 *   String[] value();
			 * }
			 * @Outer( { @Inner("1","2"}) }
			 * class Xyz {}
			 * 
			 *     ANNOTATED Outer (CHANGED/CHANGED)
			 *      ANNOTATED Inner (CHANGED/CHANGED)
			 *       PROPERTY value.0=1 (CHANGED/CHANGED)
			 *       PROPERTY value.1=2 (CHANGED/CHANGED)
			 * 
			 * </pre>
			 */
			@Override
			public void annotation(Annotation annotation) {
				if (Deprecated.class.getName().equals(annotation.getName().getFQN())) {
					if (memberEnd)
						clazz.setDeprecated(true);
					else if (last != null)
						last.setDeprecated(true);
					return;
				}

				Element e = annotatedToElement(annotation);
				if (memberEnd) {
					members.add(e);

					//
					// Check for the provider/consumer. We use strings because
					// these are not officially
					// released yet
					//
					String name = annotation.getName().getFQN();
					if (ProviderType.class.getName().equals(name)
							|| "org.osgi.annotation.versioning.ProviderType".equals(name)) {
						provider.set(true);
					} else if (ConsumerType.class.getName().equals(name)
							|| "org.osgi.annotation.versioning.ConsumerType".equals(name)) {
						provider.set(false);
					}
				} else if (last != null)
					annotations.add(last, e);
			}

			/*
			 * Return an ANNOTATED element for this annotation. An ANNOTATED
			 * element contains either PROPERTY children or ANNOTATED children.
			 */
			private Element annotatedToElement(Annotation annotation) {
				Collection<Element> properties = Create.set();
				for (String key : annotation.keySet()) {
					addAnnotationMember(properties, key, annotation.get(key));
				}
				return new Element(Type.ANNOTATED, annotation.getName().getFQN(), properties, CHANGED, CHANGED, null);
			}

			/*
			 * This method detects 3 cases: An Annotation, which means it
			 * creates a new child ANNOTATED element, an array, which means it
			 * will repeat recursively but suffixes the key with the index, or a
			 * simple value which is turned into a string.
			 */
			private void addAnnotationMember(Collection<Element> properties, String key, Object member) {
				if (member instanceof Annotation) {
					properties.add(annotatedToElement((Annotation) member));
				} else if (member.getClass().isArray()) {
					int l = Array.getLength(member);
					for (int i = 0; i < l; i++) {
						addAnnotationMember(properties, key + "." + i, Array.get(member, i));
					}
				} else {
					StringBuilder sb = new StringBuilder();
					sb.append(key);
					sb.append('=');
					if ( member instanceof String) {
						sb.append("'");
						sb.append(member);
						sb.append("'");
					} else
						sb.append(member);
						
					properties.add(new Element(Type.PROPERTY, sb.toString(), null, CHANGED, CHANGED, null));
				}
			}

			@Override
			public void innerClass(TypeRef innerClass, TypeRef outerClass, String innerName, int innerClassAccessFlags)
					throws Exception {
				Clazz clazz = analyzer.findClass(innerClass);
				if (clazz != null)
					clazz.setInnerAccess(innerClassAccessFlags);

				if (Modifier.isProtected(innerClassAccessFlags) || Modifier.isPublic(innerClassAccessFlags))
					return;
				notAccessible.add(innerClass);
			}

			@Override
			public void memberEnd() {
				memberEnd = true;
			}
		});

		// This is the heart of the semantic versioning. If we
		// add or remove a method from an interface then
		Delta add;
		Delta remove;
		Type type;

		// Calculate the type of the clazz. A class
		// can be an interface, class, enum, or annotation

		if (clazz.isInterface())
			if (clazz.isAnnotation())
				type = Type.ANNOTATION;
			else
				type = Type.INTERFACE;
		else if (clazz.isEnum())
			type = Type.ENUM;
		else
			type = Type.CLASS;

		if (type == Type.INTERFACE) {
			if (provider.get()) {
				// Adding a method for a provider is not an issue
				// because it must be aware of the changes
				add = MINOR;

				// Removing a method influences consumers since they
				// tend to call this guy.
				remove = MAJOR;
			} else {
				// Adding a method is a major change
				// because the consumer has to implement it
				// or the provider will call a non existent
				// method on the consumer
				add = MAJOR;

				// Removing a method is not an issue for
				// providers, however, consumers could potentially
				// call through this interface :-(

				remove = MAJOR;
			}
		} else {
			// Adding a method to a class can never do any harm
			// except when the class is extended and the new
			// method clashes with the new method. That is
			// why API classes in general should be final, at
			// least not extended by consumers.
			add = MINOR;

			// Removing it will likely hurt consumers
			remove = MAJOR;
		}

		// Remove all synthetic methods, we need
		// to treat them special for the covariant returns
		// We must do this in the correct order so please
		// keep the linked hashset in place!

		Set<MethodDef> synthetic = new LinkedHashSet<MethodDef>();
		for (Iterator<MethodDef> i = methods.iterator(); i.hasNext();) {
			MethodDef m = i.next();
			if (m.isSynthetic()) {
				synthetic.add(m);
				i.remove();
			}
		}

		//System.out.println("methods/synthetic " + clazz+ " " + methods + " " + synthetic);
		for (MethodDef m : methods) {
			
			Collection<Element> children = annotations.get(m);
			if (children == null)
				children = new HashSet<Element>();

			access(children, m.getAccess(), m.isDeprecated());

			// A final class cannot be extended, ergo,
			// all methods defined in it are by definition
			// final. However, marking them final (either
			// on the method or inheriting it from the class)
			// will create superfluous changes if we
			// override a method from a super class that was not
			// final. So we actually remove the final for methods
			// in a final class.
			if (clazz.isFinal())
				children.remove(FINAL);

			// for covariant types we need to add the return types
			// and all the implemented and extended types. This is already
			// do for us when we get the element of the return type.


			getCovariantReturns(children, m.getType(), m.getGenericReturnType());

			/**
			 * No longer includes synthetic methods in the tree
			 */
//			for (Iterator<MethodDef> i = synthetic.iterator(); i.hasNext();) {
//				MethodDef s = i.next();
//				if (s.getName().equals(m.getName()) && Arrays.equals(s.getPrototype(), m.getPrototype())) {
//					i.remove();
//					getCovariantReturns(children, s.getType());
//				}
//			}

			String signature = m.getSignature();
			Matcher matcher;
			if (signature !=null && (matcher = PARAMETERS_P.matcher(signature)).matches()) {
				signature = matcher.group(1);
			} else
				signature = toString(m.getPrototype());

			//
			// Java default methods are concrete implementations of methods
			// on an interface. 
			//
			
			if ( clazz.isInterface() && !m.isAbstract()) {
				
				//
				// We have a Java 8 default method!
				// Such a method is always a minor update
				//
				
				add = MINOR;
			}
			
			Element member = new Element(Type.METHOD, m.getName() + signature, children, add, remove,
					null);

			if (!members.add(member)) {
				members.remove(member);
				members.add(member);
			}
		}

		/**
		 * Repeat for the remaining synthetic methods
		 * 
		 * After long discussions with BJ we decided to skip the
		 * synthetic methods since they do not seem to contribute
		 * to the binary compatibility. They also seem to cause
		 * problems between compilers. ECJ onlyu adds a synthetic methods
		 * for the super class while Javac traverse the whole super chain.
		 * I've actually not been able to figure out how ECJ gets away with this
		 * but the behavior seems ok.
		 */
//		for (MethodDef m : synthetic) {
//			Collection<Element> children = annotations.get(m);
//			if (children == null)
//				children = new HashSet<Element>();
//			access(children, m.getAccess(), m.isDeprecated());
//
//			// A final class cannot be extended, ergo,
//			// all methods defined in it are by definition
//			// final. However, marking them final (either
//			// on the method or inheriting it from the class)
//			// will create superfluous changes if we
//			// override a method from a super class that was not
//			// final. So we actually remove the final for methods
//			// in a final class.
//			if (clazz.isFinal())
//				children.remove(FINAL);
//
//			// for covariant types we need to add the return types
//			// and all the implemented and extended types. This is already
//			// done for us when we get the element of the return type.
//
//			getCovariantReturns(children, m.getType());
//
//			Element member = new Element(Type.METHOD, m.getName() + toString(m.getPrototype()), children, add, remove,
//					"synthetic");
//
//			if (!members.add(member)) {
//				members.remove(member);
//				members.add(member);
//			}
//		}

		for (Clazz.FieldDef f : fields) {
			Collection<Element> children = annotations.get(f);
			if (children == null)
				children = new HashSet<Element>();

			// Fields can have a constant value, this is a new element
			if (f.getConstant() != null) {
				children.add(new Element(Type.CONSTANT, f.getConstant().toString(), null, CHANGED, CHANGED, null));
			}

			access(children, f.getAccess(), f.isDeprecated());
			Element member = new Element(Type.FIELD, f.getType().getFQN() + " " + f.getName(), children, MINOR, MAJOR,
					null);

			if (!members.add(member)) {
				members.remove(member);
				members.add(member);
			}
		}

		access(members, clazz.getAccess(), clazz.isDeprecated());

		// And make the result
		Element s = new Element(type, fqn, members, MINOR, MAJOR, comment.length() == 0 ? null : comment.toString());
		cache.put(clazz, s);
		return s;
	}

	private String toString(TypeRef[] prototype) {
		StringBuilder sb = new StringBuilder();
		sb.append("(");
		String del = "";
		for (TypeRef ref : prototype) {
			sb.append(del);
			sb.append(ref.getFQN());
			del = ",";
		}
		sb.append(")");
		return sb.toString();
	}

	static Element	BOOLEAN_R	= new Element(RETURN, "boolean");
	static Element	BYTE_R		= new Element(RETURN, "byte");
	static Element	SHORT_R		= new Element(RETURN, "short");
	static Element	CHAR_R		= new Element(RETURN, "char");
	static Element	INT_R		= new Element(RETURN, "int");
	static Element	LONG_R		= new Element(RETURN, "long");
	static Element	FLOAT_R		= new Element(RETURN, "float");
	static Element	DOUBLE_R	= new Element(RETURN, "double");

	private void getCovariantReturns(Collection<Element> elements, TypeRef type, String generics) throws Exception {
		if (type == null || type.isObject())
			return;

		if (type.isPrimitive()) {
			if (type.getFQN().equals("void"))
				return;

			String name = type.getBinary();
			Element e;
			switch (name.charAt(0)) {
				case 'Z' :
					e = BOOLEAN_R;
					break;
				case 'S' :
					e = SHORT_R;
					break;
				case 'I' :
					e = INT_R;
					break;
				case 'B' :
					e = BYTE_R;
					break;
				case 'C' :
					e = CHAR_R;
					break;
				case 'J' :
					e = LONG_R;
					break;
				case 'F' :
					e = FLOAT_R;
					break;
				case 'D' :
					e = DOUBLE_R;
					break;

				default :
					throw new IllegalArgumentException("Unknown primitive " + type);
			}
			elements.add(e);
			return;
		}
		Element current = new Element(RETURN, generics);
		elements.add(current);

		// List<Element> set = covariant.get(type);
		// if (set != null) {
		// elements.addAll(set);
		// return;
		// }
		//
		// Element current = new Element(RETURN, type.getFQN());
		// Clazz clazz = analyzer.findClass(type);
		// if (clazz == null) {
		// elements.add(current);
		// return;
		// }
		//
		// set = Create.list();
		// set.add(current);
		// getCovariantReturns(set, clazz.getSuper());
		//
		// TypeRef[] interfaces = clazz.getInterfaces();
		// if (interfaces != null)
		// for (TypeRef intf : interfaces) {
		// getCovariantReturns(set, intf);
		// }
		//
		// covariant.put(type, set);
		// elements.addAll(set);
	}

	private static void access(Collection<Element> children, int access, @SuppressWarnings("unused")
	boolean deprecated) {
		if (!isPublic(access))
			children.add(PROTECTED);
		if (isAbstract(access))
			children.add(ABSTRACT);
		if (isFinal(access))
			children.add(FINAL);
		if (isStatic(access))
			children.add(STATIC);

		// Ignore for now
		// if (deprecated)
		// children.add(DEPRECATED);

	}

}