package org.osgi.service.indexer.impl;

import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.StringTokenizer;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import org.osgi.service.component.ComponentConstants;
import org.osgi.service.indexer.Builder;
import org.osgi.service.indexer.Capability;
import org.osgi.service.indexer.Namespaces;
import org.osgi.service.indexer.Requirement;
import org.osgi.service.indexer.Resource;
import org.osgi.service.indexer.ResourceAnalyzer;
import org.osgi.service.indexer.impl.types.ScalarType;
import org.osgi.service.indexer.impl.types.TypedValue;
import org.osgi.service.indexer.impl.types.VersionKey;
import org.osgi.service.indexer.impl.types.VersionRange;
import org.osgi.service.log.LogService;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class SCRAnalyzer implements ResourceAnalyzer {
	static final Pattern		URI_VERSION_P		= Pattern.compile("/scr/v(\\d+\\.\\d+\\.\\d+)$");
	public static final String	NS_1_0				= Namespaces.NS_OSGI + "/scr/v1.0.0";
	public static final String	NS_1_1				= Namespaces.NS_OSGI + "/scr/v1.1.0";
	public static final String	NS_1_2				= Namespaces.NS_OSGI + "/scr/v1.2.0";
	public static final String	NS_1_2_1			= Namespaces.NS_OSGI + "/scr/v1.2.1";
	public static final String	NS_1_3				= Namespaces.NS_OSGI + "/scr/v1.3.0";

	public static final String	ELEMENT_COMPONENT	= "component";
	public static final String	ELEMENT_SERVICE		= "service";
	public static final String	ELEMENT_PROVIDE		= "provide";
	public static final String	ELEMENT_REFERENCE	= "reference";
	public static final String	ELEMENT_PROPERTY	= "property";

	public static final String	ATTRIB_INTERFACE	= "interface";
	public static final String	ATTRIB_CARDINALITY	= "cardinality";
	public static final String	ATTRIB_NAME			= "name";
	public static final String	ATTRIB_TYPE			= "type";
	public static final String	ATTRIB_VALUE		= "value";
	public static final String	ATTRIB_TARGET		= "target";

	private LogService			log;

	public SCRAnalyzer(LogService log) {
		this.log = log;
	}

	public void analyzeResource(Resource resource, List<Capability> caps, List<Requirement> reqs) throws Exception {
		String header = null;
		Manifest manifest = resource.getManifest();
		if (manifest != null)
			header = manifest.getMainAttributes().getValue(ComponentConstants.SERVICE_COMPONENT);
		if (header == null)
			return;

		StringTokenizer tokenizer = new StringTokenizer(header, ",");
		Version highest = null;
		while (tokenizer.hasMoreTokens()) {
			String pattern = tokenizer.nextToken().trim();
			List<String> paths = Util.findMatchingPaths(resource, pattern);
			if (paths != null)
				for (String path : paths) {
					Version version = processScrXml(resource, path, caps, reqs);
					if (version == null)
						continue;
					if (highest == null || (version.compareTo(highest) > 0))
						highest = version;
				}
		}

		if (highest != null) {
			Version lower = new Version(highest.getMajor(), highest.getMinor(), 0);
			Version upper = new Version(highest.getMajor() + 1, 0, 0);
			Requirement requirement = createRequirement(new VersionRange(true, lower, upper, false));
			reqs.add(requirement);
		}
	}

	private Version processScrXml(Resource resource, String path, List<Capability> caps, List<Requirement> reqs)
			throws IOException {
		Resource childResource = resource.getChild(path);
		if (childResource == null) {
			if (log != null)
				log.log(LogService.LOG_WARNING,
						MessageFormat.format(
								"Cannot analyse SCR requirement version: resource {0} does not contain path {1} referred from Service-Component header.",
								resource.getLocation(), path));
			return null;
		}

		SAXParserFactory spf = SAXParserFactory.newInstance();
		spf.setNamespaceAware(true);

		try {
			SAXParser parser = spf.newSAXParser();
			SCRContentHandler handler = new SCRContentHandler(caps, reqs);
			parser.parse(childResource.getStream(), handler);

			return handler.highest;
		} catch (Exception e) {
			if (log != null)
				log.log(LogService.LOG_ERROR,
						MessageFormat.format("Processing error: failed to parse child resource {0} in resource {1}.",
								path, resource.getLocation()),
						e);
			return null;
		}
	}

	private static Requirement createRequirement(VersionRange range) {
		Builder builder = new Builder().setNamespace(Namespaces.NS_EXTENDER);

		StringBuilder filter = new StringBuilder();
		filter.append('(').append(Namespaces.NS_EXTENDER).append('=').append(Namespaces.EXTENDER_SCR).append(')');

		filter.insert(0, "(&");
		Util.addVersionFilter(filter, range, VersionKey.PackageVersion);
		filter.append(')');

		builder.addDirective(Namespaces.DIRECTIVE_FILTER, filter.toString())
				.addDirective(Namespaces.DIRECTIVE_EFFECTIVE, Namespaces.EFFECTIVE_ACTIVE);
		Requirement requirement = builder.buildRequirement();
		return requirement;
	}

	private static class SCRContentHandler extends DefaultHandler {
		private List<Capability>	caps;
		private List<Requirement>	reqs;

		Version						highest					= null;

		private List<String>		provides				= null;
		private List<Requirement>	references				= null;

		private Map<String,Object>	properties				= null;
		private String				currentPropertyName		= null;
		private ScalarType			currentPropertyType		= null;
		private String				currentPropertyValue	= null;
		private StringBuilder		currentPropertyText		= null;

		public SCRContentHandler(List<Capability> caps, List<Requirement> reqs) {
			super();
			this.caps = caps;
			this.reqs = reqs;
		}

		@Override
		public void startElement(String uri, String localName, String qName, Attributes attribs) throws SAXException {
			super.startElement(uri, localName, qName, attribs);

			String localNameLowerCase = localName.toLowerCase();
			if (ELEMENT_COMPONENT.equals(localNameLowerCase)) {
				provides = new LinkedList<String>();
				properties = new LinkedHashMap<String,Object>();
				references = new LinkedList<Requirement>();

				if (uri == null || "".equals(uri)) {
					setVersion(new Version(1, 0, 0));
				} else {
					//
					// Actually, we do not care that match
					// since we just create a dependency on that
					// version. So lets parse the version out of the
					// URI assuming the URI will look similar in the future,
					// which is a realistic expectation. If the syntax
					// does not match, too bad.
					//

					Matcher m = URI_VERSION_P.matcher(uri);

					if (m.find()) {
						String v = m.group(1);
						setVersion(new Version(v));
					} else
						throw new SAXException("Unknown namespace " + uri);
				}
			}

			else if (ELEMENT_PROVIDE.equals(localNameLowerCase)) {
				String objectClass = attribs.getValue(ATTRIB_INTERFACE);
				provides.add(objectClass);
			}

			else if (ELEMENT_PROPERTY.equals(localNameLowerCase)) {
				currentPropertyName = attribs.getValue(ATTRIB_NAME);
				if (currentPropertyName == null)
					throw new SAXException("Missing required attribute '" + ATTRIB_NAME + "'.");
				currentPropertyType = typeOf(attribs.getValue(ATTRIB_TYPE));
				String value = attribs.getValue(ATTRIB_VALUE);
				if (value != null) {
					currentPropertyValue = value;
				} else {
					currentPropertyText = new StringBuilder();
				}
			}

			else if (ELEMENT_REFERENCE.equals(localNameLowerCase)) {
				references.add(createServiceRequirement(attribs));
			}
		}

		@Override
		public void characters(char[] chars, int start, int length) throws SAXException {
			if (currentPropertyName != null && currentPropertyText != null)
				currentPropertyText.append(chars, start, length);
		}

		@Override
		public void endElement(String uri, String localName, String qName) throws SAXException {
			String localNameLowerCase = localName.toLowerCase();

			if (ELEMENT_PROPERTY.equals(localNameLowerCase)) {
				if (currentPropertyValue != null) {
					Object value = readTyped(currentPropertyType, currentPropertyValue);
					properties.put(currentPropertyName, new TypedValue(currentPropertyType, value));
				} else if (currentPropertyText != null) {
					String[] lines = currentPropertyText.toString().split("\n");
					List<Object> values = new ArrayList<Object>(lines.length);
					for (int i = 0; i < lines.length; i++) {
						String line = lines[i].trim();
						if (line.length() > 0) {
							Object value = readTyped(currentPropertyType, line);
							values.add(value);
						}
					}
					properties.put(currentPropertyName, new TypedValue(currentPropertyType, values));
				}
				currentPropertyName = null;
				currentPropertyType = null;
				currentPropertyValue = null;
				currentPropertyText = null;
			}

			if (ELEMENT_COMPONENT.equals(localNameLowerCase)) {
				if (provides != null && !provides.isEmpty()) {
					Builder builder = new Builder().setNamespace(Namespaces.NS_SERVICE);
					builder.addAttribute(Constants.OBJECTCLASS, provides);
					for (Entry<String,Object> entry : properties.entrySet()) {
						builder.addAttribute(entry.getKey(), entry.getValue());
					}

					StringBuilder uses = new StringBuilder();
					boolean first = true;
					for (String objectClass : provides) {
						if (!first)
							uses.append(',');
						first = false;

						int dotindex = objectClass.lastIndexOf('.');
						if (dotindex < 0)
							throw new SAXException("Service interface in default package.");
						String pkgName = objectClass.substring(0, dotindex);
						uses.append(pkgName);
					}
					builder.addDirective(Constants.USES_DIRECTIVE, uses.toString());

					caps.add(builder.buildCapability());
				}

				if (references != null && !references.isEmpty()) {
					reqs.addAll(references);
				}
			}

			super.endElement(uri, localName, qName);
		}

		private void setVersion(Version version) {
			if (highest == null || (version.compareTo(highest) > 0))
				highest = version;
		}

		private static Requirement createServiceRequirement(Attributes attribs) throws SAXException {
			String interfaceClass = attribs.getValue(ATTRIB_INTERFACE);
			if (interfaceClass == null || interfaceClass.length() == 0) {
				throw new SAXException("Missing required " + ATTRIB_INTERFACE + " attribute");
			}

			Builder builder = new Builder().setNamespace(Namespaces.NS_SERVICE);

			String filter;

			// #770 https://github.com/bndtools/bnd/issues/770
			//
			// We should not include any target properties since this is more a
			// runtime
			// filter that is often overridden by config admin. Since this is
			// handled in
			// runtime anyway, we should be able to ignore it easily.
			// @formatter:off
			//			String target = attribs.getValue(ATTRIB_TARGET);
			//			if (target != null)
			//				filter = String.format("(&(%s=%s)%s)", Constants.OBJECTCLASS, interfaceClass, target);
			//			else
			// @formatter:on

			filter = String.format("(%s=%s)", Constants.OBJECTCLASS, interfaceClass);
			builder.addDirective(Namespaces.DIRECTIVE_FILTER, filter);

			String cardinality = attribs.getValue(ATTRIB_CARDINALITY);
			if (cardinality != null) {
				cardinality = cardinality.trim().toLowerCase();
				if (cardinality.length() > 0) {
					if ('0' == cardinality.charAt(0))
						builder.addDirective(Namespaces.DIRECTIVE_RESOLUTION, Namespaces.RESOLUTION_OPTIONAL);
					if ('n' == cardinality.charAt(cardinality.length() - 1))
						builder.addDirective(Namespaces.DIRECTIVE_CARDINALITY, Namespaces.CARDINALITY_MULTIPLE);
				}
			}

			builder.addDirective(Namespaces.DIRECTIVE_EFFECTIVE, Namespaces.EFFECTIVE_ACTIVE);
			return builder.buildRequirement();
		}

		private static ScalarType typeOf(String name) {
			if (name == null)
				return ScalarType.String;
			if (Long.class.getSimpleName().equals(name) || Integer.class.getSimpleName().equals(name)
					|| Short.class.getSimpleName().equals(name) || Byte.class.getSimpleName().equals(name))
				return ScalarType.Long;
			if (Float.class.getSimpleName().equals(name) || Double.class.getSimpleName().equals(name))
				return ScalarType.Double;
			return ScalarType.String;
		}

		private static Object readTyped(ScalarType type, String string) {
			switch (type) {
				case Long :
					return Long.parseLong(string);
				case Double :
					return Double.parseDouble(string);
				default :
					return string;
			}
		}
	}
}
