package aQute.bnd.testing;

import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.ServiceReference;

import aQute.bnd.make.component.ComponentAnnotationReader;
import aQute.bnd.osgi.Analyzer;
import aQute.bnd.osgi.Clazz;
import aQute.bnd.osgi.Resource;
import aQute.bnd.osgi.URLResource;
import aQute.lib.collections.MultiMap;

/**
 * Intended to wire a number components for testing using the DS (from bnd)
 * annotations.
 * <p>
 * TODO add the OSGi Annotations and support more options. needs cleanup
 */
public class DSTestWiring {
	static Pattern REFERENCE = Pattern.compile("([^/]+)/([^/]+)(?:/([^/]+))?");

	public static class Reference {
		String	name;
		Method	set;
		Method	unset;
		boolean	multiple;
		boolean	optional;
		boolean	dynamic;
		String	target;

		@Override
		public String toString() {
			return "Reference [" + (name != null ? "name=" + name + ", " : "") + "multiple=" + multiple + ", "
					+ (target != null ? "target=" + target : "") + "]";
		}
	}

	BundleContext context = null;

	{
		Bundle b = FrameworkUtil.getBundle(DSTestWiring.class);
		if (b != null)
			context = b.getBundleContext();
	}

	public void setContext(BundleContext context) {
		this.context = context;
	}

	/**
	 * A representation of a component.
	 */
	public class Component<T> {

		Class<T>			type;
		T					instance;
		Map<String,Object>	properties	= new HashMap<String,Object>();
		boolean				wiring;
		Method				activate;
		Method				deactivate;
		List<Reference>		references	= new ArrayList<DSTestWiring.Reference>();

		/*
		 * Wire
		 */
		T wire(List<Component< ? >> ordered) throws Exception {

			//
			// We can defer instantiating the type
			//
			if (instance == null)
				instance = type.getConstructor().newInstance();

			//
			// Are we already done?
			//

			if (ordered.contains(this))
				return instance;

			//
			// Check for cycles
			//
			if (wiring)
				throw new RuntimeException("Cycle " + type);

			wiring = true;

			//
			// Now figure out our references
			//

			ClassLoader loader = type.getClassLoader();
			if (loader != null) {
				URL url = loader.getResource(type.getName().replace('.', '/') + ".class");
				if (url != null) {

					doReferences(url);

					outer: for (Reference ref : references) {
						Method m = ref.set;
						Class< ? > requested = m.getParameterTypes()[0];
						List<Component< ? >> refComp = map.get(requested);
						if (refComp == null || refComp.isEmpty()) {
							if (!ref.optional) {
								// Do we have a context as backup? If
								// use it to get services.
								if (context != null) {
									ServiceReference< ? > refs[] = context.getServiceReferences(requested.getName(),
											ref.target);

									for (int i = 1; i < 30 && refs == null; i++) {
										Thread.sleep(100 * i + 1);
										refs = context.getServiceReferences(requested.getName(), ref.target);
									}
									if (refs != null && refs.length > 0) {
										for (ServiceReference< ? > r : refs) {
											Object o = context.getService(r);
											m.setAccessible(true);
											m.invoke(instance, o);
											if (!ref.multiple)
												continue outer;
										}
										continue outer;
									}
								}
								throw new IllegalStateException(type + " requires at least one component for "
										+ ref.name + " of type " + requested);
							}
						} else
							for (Component< ? > c : refComp) {
								m.setAccessible(true);
								m.invoke(instance, c.wire(ordered));
								if (!ref.multiple)
									break;
							}
					}
					if (activate != null) {
						activate.setAccessible(true);
						Class< ? > types[] = activate.getParameterTypes();
						Object[] parameters = new Object[types.length];
						for (int i = 0; i < types.length; i++) {
							if (Map.class.isAssignableFrom(types[i])) {
								parameters[i] = properties;
							} else if (map.containsKey(types[i]))
								parameters[i] = map.get(types[i]).get(0).instance;
							else
								throw new IllegalArgumentException(
										"Not a pojo " + activate.getDeclaringClass() + ", requires " + types[i]);
						}
						activate.invoke(instance, parameters);
					}
				}
			}
			ordered.add(this);
			return instance;
		}

		private void doReferences(URL url) throws Exception {
			//
			// Get the component definition
			//

			try (Analyzer a = new Analyzer(); Resource resource = new URLResource(url)) {
				Clazz clazz = new Clazz(a, "", resource);
				Map<String,String> d = ComponentAnnotationReader.getDefinition(clazz);

				for (String key : d.keySet()) {
					if ("activate:".equals(key))
						activate = findMethod(d.get(key));
					else if ("deactivate:".equals(key))
						deactivate = findMethod(d.get(key));
					else {
						//
						// Pick out the references
						//

						Matcher matcher = REFERENCE.matcher(key);
						if (matcher.matches()) {
							Reference r = new Reference();
							r.name = matcher.group(1);
							r.set = findMethod(matcher.group(2));
							r.unset = findMethod(matcher.group(3));
							// TODO handle target
							String type = d.get(key);
							if (type.endsWith("*")) {
								r.multiple = true;
								r.optional = true;
								r.dynamic = true;
							} else if (type.endsWith("?")) {
								r.multiple = false;
								r.optional = true;
								r.dynamic = true;
							} else if (type.endsWith("+")) {
								r.multiple = true;
								r.optional = false;
								r.dynamic = true;
							} else {
								r.multiple = false;
								r.optional = false;
								r.dynamic = false;
							}

							references.add(r);

						}
					}
				}
			}
		}

		private Method findMethod(String group) {
			for (Method m : type.getDeclaredMethods())
				if (m.getName().equals(group))
					return m;
			return null;
		}

		public Component<T> $(String key, Object value) {
			properties.put(key, value);
			return this;
		}

		public Component<T> instance(T x) {
			this.instance = x;
			return this;
		}

		void index(Class< ? > c) {
			while (c != null && c != Object.class) {
				map.add(c, this);
				for (Class< ? > interf : c.getInterfaces()) {
					index(interf);
				}
				c = c.getSuperclass();
			}
		}

		@Override
		public String toString() {
			return "Component [" + (type != null ? "type=" + type + ", " : "")
					+ (activate != null ? "activate=" + activate + ", " : "")
					+ (deactivate != null ? "deactivate=" + deactivate + ", " : "")
					+ (references != null ? "references=" + references : "") + "]";
		}
	}

	final MultiMap<Class< ? >,Component< ? >>	map			= new MultiMap<Class< ? >,Component< ? >>();
	final Set<Component< ? >>					components	= new HashSet<Component< ? >>();;
	final List<Component< ? >>					ordered		= new ArrayList<Component< ? >>();;

	public void wire() throws Exception {
		for (Component< ? > c : components) {
			c.wire(ordered);
		}
	}

	public <T> Component<T> add(Class<T> type) throws Exception {
		Component<T> c = new Component<T>();
		c.type = type;
		c.index(type);
		components.add(c);
		return c;
	}

	/**
	 * Add the class by name. If the class cannot be found in the local class
	 * loader, and a Bundle Context is specified, try each bundle for that
	 * class.
	 * 
	 * @param cname the name of the class
	 * @return the class
	 * @throws ClassNotFoundException if not found
	 * @throws Exception if something goes wrong
	 */
	public Component< ? > add(String cname) throws ClassNotFoundException, Exception {
		try {
			return add(getClass().getClassLoader().loadClass(cname));
		} catch (ClassNotFoundException cnfe) {
			if (context != null) {
				for (Bundle b : context.getBundles()) {
					try {
						Class< ? > c = b.loadClass(cname);
						return add(c);
					} catch (ClassNotFoundException e) {
						// ignore
					}
				}
			}
			throw cnfe;
		}
	}

	@SuppressWarnings("unchecked")
	public <T> Component<T> add(T instance) throws Exception {
		return add((Class<T>) instance.getClass()).instance(instance);
	}

	public <T> T get(Class<T> c) {
		List<Component< ? >> components = map.get(c);
		if (components == null || components.size() == 0)
			return null;

		return c.cast(components.get(0).instance);
	}

}
