package aQute.bnd.make.component;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;

import aQute.bnd.component.HeaderReader;
import aQute.bnd.component.TagResource;
import aQute.bnd.header.Attrs;
import aQute.bnd.header.Parameters;
import aQute.bnd.make.metatype.MetaTypeReader;
import aQute.bnd.osgi.Analyzer;
import aQute.bnd.osgi.Clazz;
import aQute.bnd.osgi.Clazz.QUERY;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.Descriptors.TypeRef;
import aQute.bnd.osgi.Processor;
import aQute.bnd.osgi.Resource;
import aQute.bnd.osgi.Verifier;
import aQute.bnd.service.AnalyzerPlugin;
import aQute.lib.tag.Tag;

/**
 * This class is an analyzer plugin. It looks at the properties and tries to
 * find out if the Service-Component header contains the bnd shortut syntax. If
 * not, the header is copied to the output, if it does, an XML file is created
 * and added to the JAR and the header is modified appropriately.
 */
public class ServiceComponent implements AnalyzerPlugin {

	public boolean analyzeJar(Analyzer analyzer) throws Exception {

		ComponentMaker m = new ComponentMaker(analyzer);

		Map<String,Map<String,String>> l = m.doServiceComponent();

		analyzer.setProperty(Constants.SERVICE_COMPONENT, Processor.printClauses(l));

		analyzer.getInfo(m, Constants.SERVICE_COMPONENT + ": ");
		m.close();

		return false;
	}

	private static class ComponentMaker extends Processor {
		Analyzer analyzer;

		ComponentMaker(Analyzer analyzer) {
			super(analyzer);
			this.analyzer = analyzer;
		}

		/**
		 * Iterate over the Service Component entries. There are two cases:
		 * <ol>
		 * <li>An XML file reference</li>
		 * <li>A FQN/wildcard with a set of attributes</li>
		 * </ol>
		 * An XML reference is immediately expanded, an FQN/wildcard is more
		 * complicated and is delegated to
		 * {@link #componentEntry(Map, String, Map)}.
		 * 
		 * @throws Exception
		 */
		Map<String,Map<String,String>> doServiceComponent() throws Exception {
			Map<String,Map<String,String>> serviceComponents = newMap();
			String header = getProperty(SERVICE_COMPONENT);
			Parameters sc = parseHeader(header);

			for (Entry<String,Attrs> entry : sc.entrySet()) {
				String name = entry.getKey();
				Map<String,String> info = entry.getValue();

				try {
					if (name.indexOf('/') >= 0 || name.endsWith(".xml")) {
						// Normal service component, we do not process it
						serviceComponents.put(name, EMPTY);
					} else {
						componentEntry(serviceComponents, name, info);
					}
				} catch (Exception e) {
					e.printStackTrace();
					error("Invalid " + Constants.SERVICE_COMPONENT + " header: %s %s, throws %s", name, info, e);
					throw e;
				}
			}
			return serviceComponents;
		}

		/**
		 * Parse an entry in the Service-Component header. This header supports
		 * the following types:
		 * <ol>
		 * <li>An FQN + attributes describing a component</li>
		 * <li>A wildcard expression for finding annotated components.</li>
		 * </ol>
		 * The problem is the distinction between an FQN and a wildcard because
		 * an FQN can also be used as a wildcard. If the info specifies
		 * {@link Constants#NOANNOTATIONS} then wildcards are an error and the
		 * component must be fully described by the info. Otherwise the
		 * FQN/wildcard is expanded into a list of classes with annotations. If
		 * this list is empty, the FQN case is interpreted as a complete
		 * component definition. For the wildcard case, it is checked if any
		 * matching classes for the wildcard have been compiled for a class file
		 * format that does not support annotations, this can be a problem with
		 * JSR14 who silently ignores annotations. An error is reported in such
		 * a case.
		 * 
		 * @param serviceComponents
		 * @param name
		 * @param info
		 * @throws Exception
		 * @throws IOException
		 */
		private void componentEntry(Map<String,Map<String,String>> serviceComponents, String name,
				Map<String,String> info) throws Exception, IOException {

			boolean annotations = !Processor.isTrue(info.get(NOANNOTATIONS));
			boolean fqn = Verifier.isFQN(name);

			if (annotations) {

				// Annotations possible!

				Collection<Clazz> annotatedComponents = analyzer.getClasses("", QUERY.ANNOTATED.toString(),
						"aQute.bnd.annotation.component.Component", //
						QUERY.NAMED.toString(), name //
				);

				if (fqn) {
					if (annotatedComponents.isEmpty()) {

						// No annotations, fully specified in header

						createComponentResource(serviceComponents, name, info);
					} else {

						// We had a FQN so expect only one

						for (Clazz c : annotatedComponents) {
							annotated(serviceComponents, c, info);
						}
					}
				} else {

					// We did not have an FQN, so expect the use of wildcards

					if (annotatedComponents.isEmpty())
						checkAnnotationsFeasible(name);
					else
						for (Clazz c : annotatedComponents) {
							annotated(serviceComponents, c, info);
						}
				}
			} else {
				// No annotations
				if (fqn)
					createComponentResource(serviceComponents, name, info);
				else
					error("Set to %s but entry %s is not an FQN ", NOANNOTATIONS, name);

			}
		}

		/**
		 * Check if annotations are actually feasible looking at the class
		 * format. If the class format does not provide annotations then it is
		 * no use specifying annotated components.
		 * 
		 * @param name
		 * @throws Exception
		 */
		private Collection<Clazz> checkAnnotationsFeasible(String name) throws Exception {
			Collection<Clazz> not = analyzer.getClasses("", QUERY.NAMED.toString(), name //
			);

			if (not.isEmpty()) {
				if ("*".equals(name))
					return not;
				error("Specified %s but could not find any class matching this pattern", name);
			}

			for (Clazz c : not) {
				if (c.getFormat().hasAnnotations())
					return not;
			}

			warning("Wildcards are used (%s) requiring annotations to decide what is a component. Wildcard maps to classes that are compiled with java.target < 1.5. Annotations were introduced in Java 1.5",
					name);

			return not;
		}

		void annotated(Map<String,Map<String,String>> components, Clazz c, Map<String,String> info) throws Exception {
			analyzer.warning(
					"%s annotation used in class %s. Bnd DS annotations are deprecated as of Bnd 3.2 and support will be removed in Bnd 4.0. Please change to use OSGi DS annotations.",
					"aQute.bnd.annotation.component.Component", c);
			// Get the component definition
			// from the annotations
			Map<String,String> map = ComponentAnnotationReader.getDefinition(c, this);

			// Pick the name, the annotation can override
			// the name.
			String localname = map.get(COMPONENT_NAME);
			if (localname == null)
				localname = c.getFQN();

			// Override the component info without manifest
			// entries. We merge the properties though.

			String merged = Processor.merge(info.remove(COMPONENT_PROPERTIES), map.remove(COMPONENT_PROPERTIES));
			if (merged != null && merged.length() > 0)
				map.put(COMPONENT_PROPERTIES, merged);
			map.putAll(info);
			createComponentResource(components, localname, map);
		}

		private void createComponentResource(Map<String,Map<String,String>> components, String name,
				Map<String,String> info) throws Exception {

			// We can override the name in the parameters
			if (info.containsKey(COMPONENT_NAME))
				name = info.get(COMPONENT_NAME);

			// Assume the impl==name, but allow override
			String impl = name;
			if (info.containsKey(COMPONENT_IMPLEMENTATION))
				impl = info.get(COMPONENT_IMPLEMENTATION);

			TypeRef implRef = analyzer.getTypeRefFromFQN(impl);
			// Check if such a class exists
			analyzer.referTo(implRef);

			boolean designate = designate(name, info.get(COMPONENT_DESIGNATE), false)
					|| designate(name, info.get(COMPONENT_DESIGNATEFACTORY), true);

			// If we had a designate, we want a default configuration policy of
			// require.
			if (designate && info.get(COMPONENT_CONFIGURATION_POLICY) == null)
				info.put(COMPONENT_CONFIGURATION_POLICY, "require");

			// We have a definition, so make an XML resources
			Resource resource = createComponentResource(name, impl, info);

			String pathSegment = analyzer.validResourcePath(name, "Invalid component name");
			analyzer.getJar().putResource("OSGI-INF/" + pathSegment + ".xml", resource);

			components.put("OSGI-INF/" + pathSegment + ".xml", EMPTY);

		}

		/**
		 * Create a Metatype and Designate record out of the given
		 * configurations.
		 * 
		 * @param name
		 * @param config
		 * @throws Exception
		 */
		private boolean designate(String name, String config, boolean factory) throws Exception {
			if (config == null)
				return false;

			for (String c : Processor.split(config)) {
				TypeRef ref = analyzer.getTypeRefFromFQN(c);
				Clazz clazz = analyzer.findClass(ref);
				if (clazz != null) {
					analyzer.referTo(ref);
					MetaTypeReader r = new MetaTypeReader(clazz, analyzer);
					r.setDesignate(name, factory);
					String rname = "OSGI-INF/metatype/" + name + ".xml";

					analyzer.getJar().putResource(rname, r);
				} else {
					analyzer.error("Cannot find designated configuration class %s for component %s", c, name);
				}
			}
			return true;
		}

		/**
		 * Create the resource for a DS component.
		 * 
		 * @param list
		 * @param name
		 * @param info
		 * @throws UnsupportedEncodingException
		 */
		Resource createComponentResource(String name, String impl, Map<String,String> info) throws Exception {
			HeaderReader hr = new HeaderReader(analyzer);
			Tag tag = hr.createComponentTag(name, impl, info);
			hr.close();
			return new TagResource(tag);
		}
	}

}
