package aQute.bnd.deployer.repository.providers;

import static aQute.bnd.deployer.repository.api.Decision.accept;
import static aQute.bnd.deployer.repository.api.Decision.reject;
import static aQute.bnd.deployer.repository.api.Decision.undecided;
import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
import static javax.xml.stream.XMLStreamConstants.PROCESSING_INSTRUCTION;
import static javax.xml.stream.XMLStreamConstants.START_ELEMENT;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.stream.StreamSource;

import org.osgi.framework.Version;
import org.osgi.framework.namespace.BundleNamespace;
import org.osgi.framework.namespace.ExecutionEnvironmentNamespace;
import org.osgi.framework.namespace.HostNamespace;
import org.osgi.framework.namespace.IdentityNamespace;
import org.osgi.framework.namespace.PackageNamespace;
import org.osgi.namespace.service.ServiceNamespace;
import org.osgi.resource.Namespace;
import org.osgi.resource.Resource;
import org.osgi.service.bindex.BundleIndexer;
import org.osgi.service.log.LogService;
import org.osgi.service.repository.ContentNamespace;

import aQute.bnd.deployer.repository.api.CheckResult;
import aQute.bnd.deployer.repository.api.IRepositoryContentProvider;
import aQute.bnd.deployer.repository.api.IRepositoryIndexProcessor;
import aQute.bnd.deployer.repository.api.Referral;
import aQute.bnd.osgi.resource.CapReqBuilder;
import aQute.bnd.osgi.resource.ResourceBuilder;
import aQute.bnd.service.Registry;
import aQute.lib.io.IO;

public class ObrContentProvider implements IRepositoryContentProvider {

	public static final String	NAME						= "OBR";

	private static final String	INDEX_NAME					= "repository.xml";
	private static final String	EMPTY_REPO_TEMPLATE			= "<?xml version='1.0' encoding='UTF-8'?>\n<repository name='%s' lastmodified='0' xmlns='http://www.osgi.org/xmlns/obr/v1.0.0'/>";

	private static final String	NS_URI						= "http://www.osgi.org/xmlns/obr/v1.0.0";
	private static final String	PI_DATA_STYLESHEET			= "type='text/xsl' href='http://www2.osgi.org/www/obr2html.xsl'";
	private static final String	PI_TARGET_STYLESHEET		= "xml-stylesheet";

	private static final String	TAG_REPOSITORY				= "repository";

	private static final String	TAG_RESOURCE				= "resource";
	private static final String	ATTR_RESOURCE_SYMBOLIC_NAME	= "symbolicname";
	private static final String	ATTR_RESOURCE_URI			= "uri";
	private static final String	ATTR_RESOURCE_VERSION		= "version";

	private static final String	TAG_REFERRAL				= "referral";
	private static final String	ATTR_REFERRAL_URL			= "url";
	private static final String	ATTR_REFERRAL_DEPTH			= "depth";

	private static final String	TAG_CAPABILITY				= "capability";
	private static final String	TAG_REQUIRE					= "require";
	private static final String	ATTR_NAME					= "name";
	private static final String	ATTR_EXTEND					= "extend";
	private static final String	ATTR_OPTIONAL				= "optional";
	private static final String	ATTR_FILTER					= "filter";

	private static final String	TAG_PROPERTY				= "p";
	private static final String	ATTR_PROPERTY_NAME			= "n";
	private static final String	ATTR_PROPERTY_TYPE			= "t";
	private static final String	ATTR_PROPERTY_VALUE			= "v";

	private static final String	PROPERTY_USES				= "uses";
	private static final String	TYPE_VERSION				= "version";

	private BundleIndexer		indexer;

	private static enum ParserState {
		beforeRoot, inRoot, inResource, inCapability
	}

	public ObrContentProvider(BundleIndexer indexer) {
		this.indexer = indexer;
	}

	public String getName() {
		return NAME;
	}

	public String getDefaultIndexName(boolean pretty) {
		return INDEX_NAME;
	}

	public void parseIndex(InputStream stream, URI baseUri, IRepositoryIndexProcessor listener, LogService log)
			throws Exception {
		XMLInputFactory inputFactory = XMLInputFactory.newInstance();
		inputFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, true);
		inputFactory.setProperty(XMLInputFactory.IS_VALIDATING, false);
		inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false);

		StreamSource source = new StreamSource(stream, baseUri.toString());
		XMLStreamReader reader = inputFactory.createXMLStreamReader(source);

		ResourceBuilder resourceBuilder = null;
		CapReqBuilder capReqBuilder = null;

		while (reader.hasNext()) {
			int type = reader.next();
			String localName;

			switch (type) {
				case START_ELEMENT :
					localName = reader.getLocalName();
					if (TAG_REFERRAL.equals(localName)) {
						Referral referral = new Referral(reader.getAttributeValue(null, ATTR_REFERRAL_URL),
								parseInt(reader.getAttributeValue(null, ATTR_REFERRAL_DEPTH)));
						listener.processReferral(baseUri, referral, referral.getDepth(), 1);
					} else if (TAG_RESOURCE.equals(localName)) {
						resourceBuilder = new ResourceBuilder();

						String bsn = reader.getAttributeValue(null, ATTR_RESOURCE_SYMBOLIC_NAME);
						String versionStr = reader.getAttributeValue(null, ATTR_RESOURCE_VERSION);
						Version version = Version.parseVersion(versionStr);
						String uri = reader.getAttributeValue(null, ATTR_RESOURCE_URI);
						URI resolvedUri = resolveUri(uri, baseUri);
						addBasicCapabilities(resourceBuilder, bsn, version, resolvedUri);
					} else if (TAG_CAPABILITY.equals(localName)) {
						String obrName = reader.getAttributeValue(null, ATTR_NAME);
						String namespace = mapObrNameToR5Namespace(obrName, false);
						capReqBuilder = new CapReqBuilder(namespace);
					} else if (TAG_REQUIRE.equals(localName)) {
						String obrName = reader.getAttributeValue(null, ATTR_NAME);
						boolean extend = "true".equalsIgnoreCase(reader.getAttributeValue(null, ATTR_EXTEND));
						String namespace = mapObrNameToR5Namespace(obrName, extend);
						boolean optional = "true".equalsIgnoreCase(reader.getAttributeValue(null, ATTR_OPTIONAL));

						capReqBuilder = new CapReqBuilder(namespace);
						if (optional)
							capReqBuilder.addDirective(Namespace.REQUIREMENT_RESOLUTION_DIRECTIVE,
									Namespace.RESOLUTION_OPTIONAL);
						String filter = translateObrFilter(namespace, reader.getAttributeValue(null, ATTR_FILTER), log);
						capReqBuilder.addDirective(Namespace.REQUIREMENT_FILTER_DIRECTIVE, filter);
					} else if (TAG_PROPERTY.equals(localName)) {
						String name = reader.getAttributeValue(null, ATTR_PROPERTY_NAME);
						String typeStr = reader.getAttributeValue(null, ATTR_PROPERTY_TYPE);
						String valueStr = reader.getAttributeValue(null, ATTR_PROPERTY_VALUE);
						if (capReqBuilder != null) {
							name = mapObrPropertyToR5(capReqBuilder.getNamespace(), name);
							if (PROPERTY_USES.equals(name))
								capReqBuilder.addDirective(PROPERTY_USES, valueStr);
							else {
								Object value = convertProperty(valueStr, typeStr);
								capReqBuilder.addAttribute(name, value);
							}
						}
					}
					break;
				case END_ELEMENT :
					localName = reader.getLocalName();
					if (TAG_RESOURCE.equals(localName)) {
						if (resourceBuilder != null) {
							Resource resource = resourceBuilder.build();
							listener.processResource(resource);
						}
					} else if (TAG_CAPABILITY.equals(localName)) {
						if (resourceBuilder != null && capReqBuilder != null)
							resourceBuilder.addCapability(capReqBuilder);
						capReqBuilder = null;
					} else if (TAG_REQUIRE.equals(localName)) {
						if (resourceBuilder != null && capReqBuilder != null)
							resourceBuilder.addRequirement(capReqBuilder);
						capReqBuilder = null;
					}
			}
		}
	}

	private static Object convertProperty(String value, String typeName) {
		final Object result;
		if (TYPE_VERSION.equals(typeName))
			result = Version.parseVersion(value);
		else
			result = value;
		return result;
	}

	private static String mapObrNameToR5Namespace(String obrName, boolean extend) {
		if ("bundle".equals(obrName))
			return extend ? HostNamespace.HOST_NAMESPACE : BundleNamespace.BUNDLE_NAMESPACE;
		if ("package".equals(obrName))
			return PackageNamespace.PACKAGE_NAMESPACE;
		if ("service".equals(obrName))
			return ServiceNamespace.SERVICE_NAMESPACE;
		if ("ee".equals(obrName))
			return ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE;

		return obrName;
	}

	private static String translateObrFilter(String namespace, String filter, LogService log) {
		filter = ObrUtil.processFilter(filter, log);

		if (PackageNamespace.PACKAGE_NAMESPACE.equals(namespace))
			return filter.replaceAll("\\(package", "(" + PackageNamespace.PACKAGE_NAMESPACE);

		if (ServiceNamespace.SERVICE_NAMESPACE.equals(namespace))
			return filter.replaceAll("\\(service", "(" + ServiceNamespace.SERVICE_NAMESPACE);

		if (BundleNamespace.BUNDLE_NAMESPACE.equals(namespace)) {
			filter = filter.replaceAll("\\(symbolicname", "(" + BundleNamespace.BUNDLE_NAMESPACE);
			return filter.replaceAll("\\(version", "(" + BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE);
		}

		if (ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE.equals(namespace))
			return filter.replaceAll("\\(ee", "(" + ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE);

		return filter;
	}

	private static String mapObrPropertyToR5(String namespace, String propName) {
		if (BundleNamespace.BUNDLE_NAMESPACE.equals(namespace)) {
			if ("symbolicname".equals(propName))
				return BundleNamespace.BUNDLE_NAMESPACE;
			if ("version".equals(propName))
				return BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE;
		}

		if (PackageNamespace.PACKAGE_NAMESPACE.equals(namespace)) {
			if ("package".equals(propName))
				return PackageNamespace.PACKAGE_NAMESPACE;
		}

		if (ServiceNamespace.SERVICE_NAMESPACE.equals(namespace)) {
			if ("service".equals(propName))
				return ServiceNamespace.SERVICE_NAMESPACE;
		}

		return propName;
	}

	private static URI resolveUri(String uriStr, URI baseUri) throws URISyntaxException {
		URI resolved;

		URI resourceUri = new URI(uriStr);
		if (resourceUri.isAbsolute())
			resolved = resourceUri;
		else
			resolved = baseUri.resolve(resourceUri);

		return resolved;
	}

	private static void addBasicCapabilities(ResourceBuilder builder, String bsn, Version version, URI resolvedUri)
			throws Exception {
		CapReqBuilder identity = new CapReqBuilder(IdentityNamespace.IDENTITY_NAMESPACE)
				.addAttribute(IdentityNamespace.IDENTITY_NAMESPACE, bsn)
				.addAttribute(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE, IdentityNamespace.TYPE_BUNDLE)
				.addAttribute(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE, version);

		CapReqBuilder content = new CapReqBuilder(ContentNamespace.CONTENT_NAMESPACE)
				// null attributes are skipped anyway
				// Setting this to a "reasonable" value got the testGetHttp to
				// failed utterly because it interpreted the value as the SHA?
				// .addAttribute(ContentNamespace.CONTENT_NAMESPACE, null)
				.addAttribute(ContentNamespace.CAPABILITY_URL_ATTRIBUTE, resolvedUri);

		CapReqBuilder host = new CapReqBuilder(HostNamespace.HOST_NAMESPACE)
				.addAttribute(HostNamespace.HOST_NAMESPACE, bsn)
				.addAttribute(HostNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE, version);

		builder.addCapability(identity).addCapability(content).addCapability(host);
	}

	private static int parseInt(String value) {
		if (value == null || "".equals(value))
			return 0;
		return Integer.parseInt(value);
	}

	public CheckResult checkStream(String name, InputStream stream) throws IOException {
		XMLStreamReader reader = null;
		try {
			XMLInputFactory inputFactory = XMLInputFactory.newInstance();

			inputFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, true);
			inputFactory.setProperty(XMLInputFactory.IS_VALIDATING, false);
			inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false);

			reader = inputFactory.createXMLStreamReader(stream);
			ParserState state = ParserState.beforeRoot;

			while (reader.hasNext()) {
				int type = reader.next();
				String localName;

				switch (type) {
					case PROCESSING_INSTRUCTION :
						if (PI_TARGET_STYLESHEET.equals(reader.getPITarget())
								&& PI_DATA_STYLESHEET.equals(reader.getPIData()))
							return new CheckResult(accept, "Recognised stylesheet", null);
						break;
					case START_ELEMENT :
						localName = reader.getLocalName();
						switch (state) {
							case beforeRoot :
								String nsUri = reader.getNamespaceURI();
								if (nsUri != null)
									return CheckResult.fromBool(NS_URI.equals(nsUri),
											"Correct namespace on root element",
											"Incorrect namespace on root element: " + nsUri, null);
								if (!TAG_REPOSITORY.equals(localName))
									return new CheckResult(reject, "Incorrect root element name", null);
								state = ParserState.inRoot;
								break;
							case inRoot :
								if (TAG_RESOURCE.equals(localName)) {
									state = ParserState.inResource;
								} else if (!TAG_REFERRAL.equals(localName)) {
									return new CheckResult(reject,
											String.format("Incorrect element '%s', expected '%s' or '%s'.", localName,
													TAG_RESOURCE, TAG_REFERRAL),
											null);
								}
								break;
							case inResource :
								if (TAG_CAPABILITY.equals(localName)) {
									state = ParserState.inCapability;
								}
								break;
							case inCapability :
								return CheckResult.fromBool(TAG_PROPERTY.equals(localName),
										"Found 'p' tag inside 'capability'",
										String.format("Incorrect element '%s' inside '%s'; expected '%s'.", localName,
												TAG_CAPABILITY, TAG_PROPERTY),
										null);
						}
						break;
					case END_ELEMENT :
						localName = reader.getLocalName();
						if (state == ParserState.inResource && TAG_RESOURCE.equals(localName))
							state = ParserState.inRoot;
						if (state == ParserState.inCapability && TAG_CAPABILITY.equals(localName))
							state = ParserState.inResource;
						break;
				}
			}
			return new CheckResult(undecided, "Reached end of stream", null);
		} catch (XMLStreamException e) {
			return new CheckResult(reject, "Invalid XML", e);
		} finally {
			if (reader != null)
				try {
					reader.close();
				} catch (XMLStreamException e) {}
		}
	}

	public boolean supportsGeneration() {
		return true;
	}

	public void generateIndex(Set<File> files, OutputStream output, String repoName, URI rootUri, boolean pretty,
			Registry registry, LogService log) throws Exception {
		if (!files.isEmpty()) {
			if (indexer == null)
				throw new IllegalStateException("Cannot index repository: no Bundle Indexer provided.");

			Map<String,String> config = new HashMap<String,String>();

			config.put(BundleIndexer.REPOSITORY_NAME, repoName);
			config.put(BundleIndexer.ROOT_URL, rootUri.toString());

			indexer.index(files, output, config);
		} else {
			String content = String.format(EMPTY_REPO_TEMPLATE, repoName);
			IO.store(content, output);
		}
	}

}
