package aQute.bnd.make.component;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;

import aQute.bnd.component.DSAnnotations;
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.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.strings.Strings;
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 {

	@Override
	public boolean analyzeJar(Analyzer analyzer) throws Exception {

		ComponentMaker m = new ComponentMaker(analyzer);

		Set<String> l = m.doServiceComponent()
			.keySet();

		List<String> names = DSAnnotations.removeOverlapInServiceComponentHeader(l);

		analyzer.setProperty(Constants.SERVICE_COMPONENT, Strings.join(names));

		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, Collections.emptyMap());
					} else {
						componentEntry(serviceComponents, name, info);
					}
				} catch (Exception e) {
					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 fqn = Verifier.isFQN(name);
			if (fqn)
				createComponentResource(serviceComponents, name, info);
			else {
				reportInvalidUseOfServiceComponentHeader(name);
			}
		}

		/*
		 * Report the use of old bnd annotations
		 * @param name the name used in the Service-Component headers
		 */

		private void reportInvalidUseOfServiceComponentHeader(String name) throws Exception, IOException {
			SetLocation warning = warning(Constants.SERVICE_COMPONENT
				+ " is normally generated by bnd. If you want to point at specific component XML files (wildcards are allowed, see findEntries) make sure it ends in '.xml'. Found %s",
				name);

			FileLine where = getHeader(Pattern.compile(Constants.SERVICE_COMPONENT, Pattern.LITERAL),
				Pattern.compile(name, Pattern.LITERAL));
			if (where != null) {
				where.set(warning);
			}

			warning.reference("https://osgi.org/specification/osgi.cmpn/7.0.0/service.component.html#d0e36857");

			//
			// We now have a selection on the classes in the analyzer.
			// we should generate an error if any of those classes has
			// and old fashioned bnd annotation.
			//

			Collection<Clazz> classes = analyzer.getClasses(null, "NAMED", name, "ANNOTATED",
				"aQute.bnd.annotation.component.Component");

			for (Clazz clazz : classes) {

				//
				// Generate an error on each class that uses the annotations

				SetLocation loc = error(Constants.SERVICE_COMPONENT
					+ " refers to %s that is annotated with the deprecated bnd Component annotation. Support for this was removed in 4.0.0 because they were taken over by OSGi. Please update to the comparable OSGi annotations",
					clazz.getFQN());
				TypeRef cname = clazz.getClassName();
				String source = analyzer.getSourceFileFor(cname);
				if (source != null) {
					File f = getFile(source);
					if (f.isFile()) {
						Pattern pattern = Pattern.compile("^.*@.*Component.*$", Pattern.CASE_INSENSITIVE);
						where = findHeader(f, pattern);
						if (where != null) {
							where.set(loc);
						} else {
							loc.file(source);
							loc.line(1);
						}
					}
				}
			}
		}

		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.nonClassReferTo(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", Collections.emptyMap());

		}

		/**
		 * 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.nonClassReferTo(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.
		 */
		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);
		}
	}

	@Override
	public String toString() {
		return getClass().getSimpleName();
	}
}
