package biz.aQute.bnd.reporter.plugins.entries.bundle;

import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.jar.Manifest;

import javax.xml.parsers.DocumentBuilder;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import aQute.bnd.annotation.plugin.BndPlugin;
import aQute.bnd.header.OSGiHeader;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.Resource;
import aQute.bnd.service.Plugin;
import aQute.bnd.service.reporter.ReportEntryPlugin;
import aQute.lib.xml.XML;
import aQute.service.reporter.Reporter;
import biz.aQute.bnd.reporter.component.dto.ComponentDescriptionDTO;
import biz.aQute.bnd.reporter.component.dto.ReferenceDTO;
import biz.aQute.bnd.reporter.generator.EntryNamesReference;
import biz.aQute.bnd.reporter.manifest.dto.TypedAttributeValueDTO;

/**
 * This plugin allows to add all the components defined in a bundle to the
 * report.
 */
@BndPlugin(name = "entry." + EntryNamesReference.COMPONENTS)
public class ComponentsPlugin implements ReportEntryPlugin<Jar>, Plugin {

	final private static String			IMPLEMENTATION_TAG			= "implementation";
	final private static String			SERVICE_TAG					= "service";
	final private static String			PROVIDE_TAG					= "provide";
	final private static String			PROPERTY_TAG				= "property";
	final private static String			PROPERTIES_TAG				= "properties";
	final private static String			REFERENCE_TAG				= "reference";

	final private static String			CLASS_ATTR					= "class";
	final private static String			ENABLED_ATTR				= "enabled";
	final private static String			FACTORY_ATTR				= "factory";
	final private static String			IMMEDIATE_ATTR				= "immediate";
	final private static String			CONFIGURATION_POLICY_ATTR	= "configuration-policy";
	final private static String			CONFIGURATION_PID_ATTR		= "configuration-pid";
	final private static String			ACTIVATE_ATTR				= "activate";
	final private static String			DEACTIVATE_ATTR				= "deactivate";
	final private static String			MODIFIED_ATTR				= "modified";
	final private static String			NAME_ATTR					= "name";
	final private static String			TYPE_ATTR					= "type";
	final private static String			VALUE_ATTR					= "value";
	final private static String			ENTRY_ATTR					= "entry";
	final private static String			BIND_ATTR					= "bind";
	final private static String			FIELD_ATTR					= "field";
	final private static String			FIELD_OPTION_ATTR			= "field-option";
	final private static String			CARDINALITY_ATTR			= "cardinality";
	final private static String			INTERFACE_ATTR				= "interface";
	final private static String			POLICY_ATTR					= "policy";
	final private static String			POLICY_OPTION_ATTR			= "policy-option";
	final private static String			SCOPE_ATTR					= "scope";
	final private static String			TARGET_ATTR					= "target";
	final private static String			UNBIND_ATTR					= "unbind";
	final private static String			UPDATED_ATTR				= "updated";

	private Reporter					_reporter;
	private final Map<String, String>	_properties					= new HashMap<>();

	public ComponentsPlugin() {
		_properties.put(ReportEntryPlugin.ENTRY_NAME_PROPERTY, EntryNamesReference.COMPONENTS);
		_properties.put(ReportEntryPlugin.SOURCE_CLASS_PROPERTY, Jar.class.getCanonicalName());
	}

	@Override
	public List<ComponentDescriptionDTO> extract(final Jar jar, final Locale locale) throws Exception {
		Objects.requireNonNull(jar, "jar");
		Objects.requireNonNull(locale, "locale");

		final Set<String> componentPaths = new HashSet<>();
		final DocumentBuilder db = XML.newDocumentBuilderFactory()
			.newDocumentBuilder();

		try {
			final Manifest manifest = jar.getManifest();
			if (manifest != null) {
				componentPaths.addAll(OSGiHeader.parseHeader(manifest.getMainAttributes()
					.getValue(Constants.SERVICE_COMPONENT))
					.keySet());
			}
		} catch (@SuppressWarnings("unused")
		final Exception expected) {
			// Nothing to do
		}

		final List<ComponentDescriptionDTO> comps = new LinkedList<>();
		for (final String path : componentPaths) {
			final Resource r = jar.getResource(path);
			if (r != null) {
				try (InputStream in = r.openInputStream()) {
					final ComponentDescriptionDTO comp = new ComponentDescriptionDTO();
					final Document doc = db.parse(in);
					final Element root = doc.getDocumentElement();

					final NodeList implementations = root.getElementsByTagName(IMPLEMENTATION_TAG);
					if (implementations.getLength() > 0
						&& ((Element) implementations.item(0)).hasAttribute(CLASS_ATTR)) {
						comp.implementationClass = ((Element) implementations.item(0)).getAttribute(CLASS_ATTR);
					} else {
						comp.implementationClass = "!! MISSING !!";
						_reporter.warning("the component does not declare an implementation class %s", path);
					}

					if (root.hasAttribute(NAME_ATTR)) {
						comp.name = root.getAttribute(NAME_ATTR);
					} else {
						comp.name = comp.implementationClass;
					}

					if (root.hasAttribute(ENABLED_ATTR)) {
						comp.defaultEnabled = Boolean.parseBoolean(root.getAttribute(ENABLED_ATTR));
					}

					boolean isFactory = false;
					if (root.hasAttribute(FACTORY_ATTR)) {
						comp.factory = root.getAttribute(FACTORY_ATTR);
						isFactory = true;
					}

					final NodeList services = root.getElementsByTagName(SERVICE_TAG);

					int serviceCount = 0;
					if (services.getLength() > 0) {

						final Element service = (Element) services.item(0);

						final NodeList provides = root.getElementsByTagName(PROVIDE_TAG);
						for (int i = 0; i < provides.getLength(); i++) {
							final Element provide = (Element) provides.item(i);
							if (provide.hasAttribute(INTERFACE_ATTR)) {
								comp.serviceInterfaces.add(provide.getAttribute(INTERFACE_ATTR));
								serviceCount++;
							}
						}

						if (serviceCount > 0) {
							if (service.hasAttribute(SCOPE_ATTR)) {
								comp.scope = service.getAttribute(SCOPE_ATTR);
							} else {
								comp.scope = "singleton";
							}
						}
					}

					if (root.hasAttribute(IMMEDIATE_ATTR)) {
						comp.immediate = Boolean.parseBoolean(root.getAttribute(IMMEDIATE_ATTR));
					} else {
						if (serviceCount == 0 && !isFactory) {
							comp.immediate = true;
						} else {
							comp.immediate = false;
						}
					}

					if (root.hasAttribute(CONFIGURATION_POLICY_ATTR)) {
						comp.configurationPolicy = root.getAttribute(CONFIGURATION_POLICY_ATTR);
					}

					if (root.hasAttribute(CONFIGURATION_PID_ATTR)) {
						for (final String pid : root.getAttribute(CONFIGURATION_PID_ATTR)
							.split(" ")) {
							comp.configurationPid.add(pid);
						}
					} else {
						comp.configurationPid.add(comp.name);
					}

					if (root.hasAttribute(ACTIVATE_ATTR)) {
						comp.activate = root.getAttribute(ACTIVATE_ATTR);
					}

					if (root.hasAttribute(DEACTIVATE_ATTR)) {
						comp.deactivate = root.getAttribute(DEACTIVATE_ATTR);
					}

					if (root.hasAttribute(MODIFIED_ATTR)) {
						comp.modified = root.getAttribute(MODIFIED_ATTR);
					}

					final NodeList propertiesFile = root.getElementsByTagName(PROPERTIES_TAG);

					final Set<String> propertiesName = new HashSet<>();
					for (int i = 0; i < propertiesFile.getLength(); i++) {

						final Element property = (Element) propertiesFile.item(i);
						final String pathProp = property.getAttribute(ENTRY_ATTR);

						if (pathProp.isEmpty()) {
							_reporter.warning("missing property path attribute for component %s", path);
						} else {
							final Resource rP = jar.getResource(pathProp);
							if (rP != null) {
								try (InputStream inProp = rP.openInputStream()) {
									final Properties prop = new Properties();

									prop.load(inProp);

									for (final String key : prop.stringPropertyNames()) {
										final TypedAttributeValueDTO p = new TypedAttributeValueDTO();
										p.type = "String";
										p.values.add(prop.getProperty(key));

										comp.properties.put(key, p);

										propertiesName.add(key);
									}
								}
							} else {
								_reporter.warning("cannot find property file at path %s for component %s", pathProp,
									path);
							}
						}
					}

					final NodeList properties = root.getElementsByTagName(PROPERTY_TAG);

					for (int i = 0; i < properties.getLength(); i++) {
						final TypedAttributeValueDTO p = new TypedAttributeValueDTO();

						final Element property = (Element) properties.item(i);

						String pName = null;
						if (property.hasAttribute(NAME_ATTR)) {
							pName = property.getAttribute(NAME_ATTR);
						} else {
							pName = "!! MISSING !!";
							_reporter.warning("missing property name attribute for component %s", path);
						}

						if (!propertiesName.contains(pName)) {
							if (property.hasAttribute(TYPE_ATTR)) {
								p.type = property.getAttribute(TYPE_ATTR);
							} else {
								p.type = "String";
							}

							if (property.hasAttribute(VALUE_ATTR)) {
								p.values.add(property.getAttribute(VALUE_ATTR));
							} else {
								p.multiValue = true;
								for (final String v : property.getTextContent()
									.split("\n")) {
									p.values.add(v);
								}
							}
							comp.properties.put(pName, p);
						}
					}

					final NodeList references = root.getElementsByTagName(REFERENCE_TAG);

					for (int i = 0; i < references.getLength(); i++) {

						final Element reference = (Element) references.item(i);

						final ReferenceDTO ref = new ReferenceDTO();

						if (reference.hasAttribute(BIND_ATTR)) {
							ref.bind = reference.getAttribute(BIND_ATTR);
						}

						if (reference.hasAttribute(FIELD_ATTR)) {
							ref.field = reference.getAttribute(FIELD_ATTR);
							if (reference.hasAttribute(FIELD_OPTION_ATTR)) {
								ref.fieldOption = reference.getAttribute(FIELD_OPTION_ATTR);
							} else {
								ref.fieldOption = "replace";
							}
						}

						if (reference.hasAttribute(CARDINALITY_ATTR)) {
							ref.cardinality = reference.getAttribute(CARDINALITY_ATTR);
						} else {
							ref.cardinality = "1..1";
						}

						if (reference.hasAttribute(INTERFACE_ATTR)) {
							ref.interfaceName = reference.getAttribute(INTERFACE_ATTR);
						} else {
							ref.interfaceName = "!! MISSING !!";
							_reporter.warning("missing reference interface attribute for component %s", path);
						}

						if (reference.hasAttribute(NAME_ATTR)) {
							ref.name = reference.getAttribute(NAME_ATTR);
						} else {
							ref.name = "!! MISSING !!";
							_reporter.warning("missing reference name attribute for component %s", path);
						}

						if (reference.hasAttribute(POLICY_ATTR)) {
							ref.policy = reference.getAttribute(POLICY_ATTR);
						}

						if (reference.hasAttribute(POLICY_OPTION_ATTR)) {
							ref.policyOption = reference.getAttribute(POLICY_OPTION_ATTR);
						}

						if (reference.hasAttribute(SCOPE_ATTR)) {
							ref.scope = reference.getAttribute(SCOPE_ATTR);
						}

						if (reference.hasAttribute(TARGET_ATTR)) {
							ref.target = reference.getAttribute(TARGET_ATTR);
						}

						if (reference.hasAttribute(UNBIND_ATTR)) {
							ref.unbind = reference.getAttribute(UNBIND_ATTR);
						}

						if (reference.hasAttribute(UPDATED_ATTR)) {
							ref.updated = reference.getAttribute(UPDATED_ATTR);
						}

						comp.references.add(ref);
					}
					comps.add(comp);
				}
			} else {
				if (!path.contains("*")) {
					_reporter.warning("xml component file not found at path %s", path);
				}
			}
		}
		return !comps.isEmpty() ? comps : null;
	}

	@Override
	public Map<String, String> getProperties() {
		return Collections.unmodifiableMap(_properties);
	}

	@Override
	public void setProperties(final Map<String, String> map) throws Exception {
		_properties.putAll(map);
	}

	@Override
	public void setReporter(final Reporter processor) {
		_reporter = processor;
	}
}
