package aQute.bnd.metatype;

import java.lang.reflect.Array;
import java.util.Collection;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Icon;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.osgi.service.metatype.annotations.Option;

import aQute.bnd.annotation.xml.XMLAttribute;
import aQute.bnd.metatype.MetatypeAnnotations.Options;
import aQute.bnd.osgi.Analyzer;
import aQute.bnd.osgi.Annotation;
import aQute.bnd.osgi.ClassDataCollector;
import aQute.bnd.osgi.Clazz;
import aQute.bnd.osgi.Clazz.FieldDef;
import aQute.bnd.osgi.Clazz.MethodDef;
import aQute.bnd.osgi.Descriptors.TypeRef;
import aQute.bnd.xmlattribute.XMLAttributeFinder;

class OCDReader {
	final Analyzer				analyzer;
	private final Clazz			clazz;
	final EnumSet<Options>		options;
	private final Set<TypeRef>	analyzed	= new HashSet<TypeRef>();;
	private final OCDDef		ocd;
	final XMLAttributeFinder	finder;

	private OCDReader(Analyzer analyzer, Clazz clazz, EnumSet<Options> options, XMLAttributeFinder finder,
			MetatypeVersion minVersion) {
		this.analyzer = analyzer;
		this.clazz = clazz;
		this.options = options;
		this.finder = finder;
		this.ocd = new OCDDef(finder, minVersion);
	}

	static OCDDef getOCDDef(Clazz c, Analyzer analyzer, EnumSet<Options> options, XMLAttributeFinder finder, MetatypeVersion minVersion)
			throws Exception {
		OCDReader r = new OCDReader(analyzer, c, options, finder, minVersion);
		return r.getDef();
	}

	private OCDDef getDef() throws Exception {
		clazz.parseClassFileWithCollector(new OCDDataCollector(ocd));
		if (ocd.id == null) {
			return null;
		}
		parseExtends(clazz);
		return ocd;
	}

	private void parseExtends(Clazz clazz) {
		TypeRef[] inherits = clazz.getInterfaces();
		if (inherits != null) {
			for (TypeRef typeRef : inherits) {
				if (!typeRef.isJava() && analyzed.add(typeRef)) {
					try {
						Clazz inherit = analyzer.findClass(typeRef);
						if (inherit != null) {
							inherit.parseClassFileWithCollector(new OCDDataCollector(ocd));
							parseExtends(inherit);
						} else {
							analyzer.error("Could not obtain super class %s of class %s", typeRef.getFQN(),
									clazz.getClassName().getFQN());
						}
					} catch (Exception e) {
						analyzer.exception(e, "Could not obtain super class %s of class %s; exception %s",
								typeRef.getFQN(), clazz.getClassName().getFQN(), e);
					}
				}
			}
		}
	}

	// TODO what about Queue|Stack|Deque?
	static final Pattern GENERIC = Pattern.compile("((" + Collection.class.getName() + "|" + Set.class.getName() + "|"
			+ List.class.getName() + "|" + Iterable.class.getName() + ")|(.*))<(L.+;)>");

	// Determine if we can identify that this class is a concrete subtype of
	// collection with a no-arg constructor
	// So far this implementation doesn't try very hard. It only looks to see if
	// the class directly implements a known collection interface.
	static final Pattern COLLECTION = Pattern
			.compile("(" + Collection.class.getName() + "|" + Set.class.getName() + "|" + List.class.getName() + "|"
					+ Queue.class.getName() + "|" + Stack.class.getName() + "|" + Deque.class.getName() + ")");

	static final Pattern IDENTIFIERTOPROPERTY = Pattern.compile("(__)|(_)|(\\$_\\$)|(\\$\\$)|(\\$)");

	private final class OCDDataCollector extends ClassDataCollector {
		private final OCDDef				ocd;
		private final Map<MethodDef,ADDef>	methods			= new LinkedHashMap<MethodDef,ADDef>();
		private Clazz						clazz;
		private TypeRef						name;
		private int							hasNoDefault	= 0;
		private boolean						hasValue		= false;
		private FieldDef					prefixField		= null;
		private ADDef						current;

		OCDDataCollector(OCDDef ocd) {
			this.ocd = ocd;
		}

		@Override
		public boolean classStart(Clazz clazz) {
			this.clazz = clazz;
			this.name = clazz.getClassName();
			return true;
		}

		@Override
		public void field(FieldDef defined) {
			if (defined.isStatic() && defined.getName().equals("PREFIX_")) {
				prefixField = defined;
			}
		}

		@Override
		public void method(MethodDef defined) {
			if (defined.isStatic()) {
				current = null;
				return;
			}
			current = new ADDef(finder);
			methods.put(defined, current);
			if (clazz.isAnnotation()) {
				if (defined.getName().equals("value")) {
					hasValue = true;
				} else {
					hasNoDefault++;
				}
			}
		}

		@Override
		public void annotationDefault(MethodDef defined, Object value) {
			if (!defined.getName().equals("value")) {
				hasNoDefault--;
			}
		}

		@Override
		public void annotation(Annotation annotation) throws Exception {
			try {
				java.lang.annotation.Annotation a = annotation.getAnnotation();
				if (a instanceof ObjectClassDefinition)
					doOCD((ObjectClassDefinition) a, annotation);
				else if (a instanceof AttributeDefinition) {
					current.ad = (AttributeDefinition) a;
					current.a = annotation;
				} else {
					XMLAttribute xmlAttr = finder.getXMLAttribute(annotation);
					if (xmlAttr != null) {
						doXmlAttribute(annotation, xmlAttr);
					}
				}
			} catch (Exception e) {
				analyzer.exception(e, "During generation of a component on class %s, exception %s", clazz, e);
			}
		}

		@Override
		public void memberEnd() {
			current = null;
		}

		@Override
		public void classEnd() throws Exception {
			current = null;
			if (ocd.id == null) {
				return; // no ObjectClassDefinition annotation found
			}
			String prefix = null;
			if (prefixField != null) {
				Object c = prefixField.getConstant();
				if (prefixField.isFinal() && (prefixField.getType() == analyzer.getTypeRef("java/lang/String"))
						&& (c instanceof String)) {
					prefix = (String) c;
					ocd.updateVersion(MetatypeVersion.VERSION_1_4);
				} else {
					analyzer.warning(
							"Field PREFIX_ in %s is not a static final String field with a compile-time constant value: %s",
							name.getFQN(), c);
				}
			}
			String singleElementAnnotation = null;
			if (hasValue && (hasNoDefault == 0)) {
				StringBuilder sb = new StringBuilder(name.getShorterName());
				boolean lastLowerCase = false;
				for (int i = 0; i < sb.length(); i++) {
					char c = sb.charAt(i);
					if (Character.isUpperCase(c)) {
						sb.setCharAt(i, Character.toLowerCase(c));
						if (lastLowerCase) {
							sb.insert(i++, '.');
						}
						lastLowerCase = false;
					} else {
						lastLowerCase = Character.isLowerCase(c);
					}
				}
				singleElementAnnotation = sb.toString();
				ocd.updateVersion(MetatypeVersion.VERSION_1_4);
			}

			for (Map.Entry<MethodDef,ADDef> entry : methods.entrySet()) {
				MethodDef defined = entry.getKey();
				if (defined.isConstructor()) {
					analyzer.error("Constructor %s for %s.%s found; only interfaces and annotations allowed for OCDs",
							defined.getName(), clazz.getClassName().getFQN(), defined.getName());
		
				}
				if (defined.getPrototype().length > 0) {
					analyzer.error(
							"Element %s for %s.%s has parameters; only no-parameter elements in an OCD interface allowed",
							defined.getName(), clazz.getClassName().getFQN(), defined.getName());
					continue;
				}
				ADDef ad = entry.getValue();
				ocd.attributes.add(ad);
				String key = defined.getName();
				if ((singleElementAnnotation != null) && key.equals("value")) {
					key = singleElementAnnotation;
				} else {
					key = identifierToPropertyName(key);
				}
				if (prefix != null) {
					key = prefix + key;
				}
				ad.id = key;
				ad.name = space(defined.getName());
				String rtype = defined.getGenericReturnType();
				if (rtype.endsWith("[]")) {
					ad.cardinality = Integer.MAX_VALUE;
					rtype = rtype.substring(0, rtype.length() - 2);
				}
				Matcher m = GENERIC.matcher(rtype);
				if (m.matches()) {
					boolean knownCollection = m.group(2) != null;
					boolean collection = knownCollection || identifiableCollection(m.group(3), false, true);
					if (collection) {
						if (ad.cardinality != 0)
							analyzer.error(
									"AD for %s.%s uses an array of collections in return type (%s), Metatype allows either Vector or array",
									clazz.getClassName().getFQN(), defined.getName(), defined.getType().getFQN());
						rtype = Clazz.objectDescriptorToFQN(m.group(4));
						ad.cardinality = Integer.MIN_VALUE;
					}
				}
				if (rtype.indexOf('<') > 0) {
					rtype = rtype.substring(0, rtype.indexOf('<'));
				}
				ad.type = getType(rtype);
		
				ad.required = true;
				TypeRef typeRef = analyzer.getTypeRefFromFQN(rtype);
				try {
					Clazz c = analyzer.findClass(typeRef);
					if (c != null && c.isEnum()) {
						parseOptionValues(c, ad.options);
					}
				} catch (Exception e) {
					analyzer.exception(e, "AD for %s.%s Can not parse option values from type (%s), %s",
							clazz.getClassName().getFQN(), defined.getName(), defined.getType().getFQN(), e);
				}
				if (ad.ad != null) {
					doAD(ad);
				}
				if (ad.defaults == null && clazz.isAnnotation() && defined.getConstant() != null) {
					// defaults from annotation default
					Object value = defined.getConstant();
					boolean isClass = false;
					TypeRef type = defined.getType().getClassRef();
					if (!type.isPrimitive()) {
						if (Class.class.getName().equals(type.getFQN())) {
							isClass = true;
						} else {
							try {
								Clazz r = analyzer.findClass(type);
								if (r.isAnnotation()) {
									analyzer.warning("Nested annotation type found in field %s, %s", defined.getName(),
											type.getFQN());
									return;
								}
							} catch (Exception e) {
								analyzer.exception(e,
										"Exception looking at annotation type default for element with descriptor %s,  type %s",
										defined, type);
							}
						}
					}
					if (value != null) {
						if (value.getClass().isArray()) {
							// add element individually
							ad.defaults = new String[Array.getLength(value)];
							for (int i = 0; i < Array.getLength(value); i++) {
								Object element = Array.get(value, i);
								ad.defaults[i] = valueToProperty(element, isClass);
							}
						} else {
							ad.defaults = new String[] {
									valueToProperty(value, isClass)
							};
						}
					}
				}
			}
		}

		private void doOCD(ObjectClassDefinition o, Annotation annotation) {
			if (ocd.id == null) {
				if (clazz.isInterface()) {
					ocd.id = o.id() == null ? name.getFQN() : o.id();
					ocd.name = o.name() == null ? space(ocd.id) : o.name();
					ocd.description = o.description() == null ? "" : o.description();
					ocd.localization = o.localization() == null ? "OSGI-INF/l10n/" + name.getFQN() : o.localization();
					if (annotation.get("pid") != null) {
						String[] pids = o.pid();
						designates(name.getFQN(), pids, false);
					}
					if (annotation.get("factoryPid") != null) {
						String[] pids = o.factoryPid();
						designates(name.getFQN(), pids, true);
					}
					if (annotation.get("icon") != null) {
						Icon[] icons = o.icon();
						for (Icon icon : icons) {
							ocd.icons.add(new IconDef(icon.resource(), icon.size()));
						}
					}
				} else {
					analyzer.error("ObjectClassDefinition applied to non-interface, non-annotation class %s", clazz);
				}
			}
		}

		private void doAD(ADDef adDef) throws Exception {
			AttributeDefinition ad = adDef.ad;
			Annotation a = adDef.a;

			if (ad.name() != null) {
				adDef.name = ad.name();
			}
			adDef.description = a.get("description");
			if (a.get("type") != null) {
				adDef.type = ad.type();
			}
			if (a.get("cardinality") != null) {
				adDef.cardinality = ad.cardinality();
			}
			adDef.max = ad.max();
			adDef.min = ad.min();
			if (a.get("defaultValue") != null) {
				adDef.defaults = ad.defaultValue();
			}
			if (a.get("required") != null) {
				adDef.required = ad.required();
			}
			if (a.get("options") != null) {
				adDef.options.clear();
				for (Object o : (Object[]) a.get("options")) {
					Option opt = ((Annotation) o).getAnnotation();
					adDef.options.add(new OptionDef(opt.label(), opt.value()));
				}
			}
		}

		private void doXmlAttribute(Annotation annotation, XMLAttribute xmlAttr) {
			if (current == null) {
				if (clazz.isInterface()) {
					ocd.addExtensionAttribute(xmlAttr, annotation);
				}
			} else {
				current.addExtensionAttribute(xmlAttr, annotation);
			}
		}

		private boolean identifiableCollection(String type, boolean intface, boolean topLevel) {
			try {
				Clazz clazz = analyzer.findClass(analyzer.getTypeRefFromFQN(type));
				if (clazz != null && (!topLevel || !clazz.isAbstract())
						&& ((intface && clazz.isInterface()) ^ clazz.hasPublicNoArgsConstructor())) {
					TypeRef[] intfs = clazz.getInterfaces();
					if (intfs != null) {
						for (TypeRef intf : intfs) {
							if (COLLECTION.matcher(intf.getFQN()).matches()
									|| identifiableCollection(intf.getFQN(), true, false)) {
								return true;
							}
						}
					}
					TypeRef ext = clazz.getSuper();
					return ext != null && identifiableCollection(ext.getFQN(), false, false);
				}
			} catch (Exception e) {
				return false;
			}
			return false;
		}

		private String valueToProperty(Object value, boolean isClass) {
			if (isClass)
				return ((TypeRef) value).getFQN();
			return value.toString();
		}

		private void parseOptionValues(Clazz c, final List<OptionDef> options) throws Exception {
		
			c.parseClassFileWithCollector(new ClassDataCollector() {
				@Override
				public void field(Clazz.FieldDef def) {
					if (def.isEnum()) {
						OptionDef o = new OptionDef(def.getName(), def.getName());
						options.add(o);
					}
				}
			});
		}

		private AttributeType getType(String rtype) {
			if (rtype.endsWith("[]")) {
				analyzer.error("Can only handle array of depth one field , nested type %s", rtype);
				return null;
			}
		
			if ("boolean".equals(rtype) || Boolean.class.getName().equals(rtype))
				return AttributeType.BOOLEAN;
			else if ("byte".equals(rtype) || Byte.class.getName().equals(rtype))
				return AttributeType.BYTE;
			else if ("char".equals(rtype) || Character.class.getName().equals(rtype))
				return AttributeType.CHARACTER;
			else if ("short".equals(rtype) || Short.class.getName().equals(rtype))
				return AttributeType.SHORT;
			else if ("int".equals(rtype) || Integer.class.getName().equals(rtype))
				return AttributeType.INTEGER;
			else if ("long".equals(rtype) || Long.class.getName().equals(rtype))
				return AttributeType.LONG;
			else if ("float".equals(rtype) || Float.class.getName().equals(rtype))
				return AttributeType.FLOAT;
			else if ("double".equals(rtype) || Double.class.getName().equals(rtype))
				return AttributeType.DOUBLE;
			else if (String.class.getName().equals(rtype) || Class.class.getName().equals(rtype) || acceptableType(rtype))
				return AttributeType.STRING;
			else {
				return null;
		
			}
		}

		private boolean acceptableType(String rtype) {
			TypeRef ref = analyzer.getTypeRefFromFQN(rtype);
			try {
				Clazz returnType = analyzer.findClass(ref);
				if (returnType.isEnum()) {
					return true;
				}
				// TODO check this is true for interfaces and annotations
				if (!returnType.isAbstract() || (returnType.isInterface() && options.contains(Options.nested))) {
					return true;
				}
				if (!returnType.isInterface()) {
					analyzer.error("Abstract classes not allowed as interface method return values: %s", rtype);
				} else {
					analyzer.error("Nested metatype only allowed with option: nested type %s", rtype);
				}
				return false;
			} catch (Exception e) {
				analyzer.exception(e, "could not examine class for return type %s, exception message: %s", rtype, e);
				return false;
			}
		}

		private String identifierToPropertyName(String name) {
			Matcher m = IDENTIFIERTOPROPERTY.matcher(name);
			StringBuffer b = new StringBuffer();
			while (m.find()) {
				String replacement;// null;
				if (m.group(1) != null) // __ to _
					replacement = "_";
				else if (m.group(2) != null) // _ to .
					replacement = ".";
				else if (m.group(3) != null) { // $_$ to -
					replacement = "-";
					ocd.updateVersion(MetatypeVersion.VERSION_1_4);
				}
				else if (m.group(4) != null) // $$ to $
					replacement = "\\$";
				else // $ removed.
					replacement = "";

				m.appendReplacement(b, replacement);
			}
			m.appendTail(b);
			return b.toString();
		}

		private String space(String name) {
			return Clazz.unCamel(name);
		}

		private void designates(String name, String[] pids, boolean factory) {
			for (String pid : pids) {
				if ("$".equals(pid)) {
					pid = name;
				}
				ocd.designates.add(new DesignateDef(ocd.id, pid, factory, finder));
			}
		}
	}
}
