package aQute.bnd.osgi.resource;

import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

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.contract.ContractNamespace;
import org.osgi.namespace.extender.ExtenderNamespace;
import org.osgi.namespace.service.ServiceNamespace;
import org.osgi.resource.Capability;
import org.osgi.resource.Namespace;
import org.osgi.resource.Requirement;
import org.osgi.resource.Resource;
import org.osgi.service.repository.ContentNamespace;

import aQute.bnd.build.model.clauses.VersionedClause;
import aQute.bnd.header.Attrs;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.Macro;
import aQute.bnd.osgi.Processor;
import aQute.bnd.version.Version;
import aQute.lib.converter.Converter;
import aQute.lib.converter.Converter.Hook;
import aQute.lib.filter.Filter;
import aQute.lib.strings.Strings;

public class ResourceUtils {

	/**
	 * A comparator that compares the identity versions
	 */
	public static final Comparator<Resource>			IDENTITY_VERSION_COMPARATOR	= new Comparator<Resource>() {

																						@Override
																						public int compare(Resource o1,
																								Resource o2) {
																							if (o1 == o2)
																								return 0;

																							if (o1 == null)
																								return -1;

																							if (o2 == null)
																								return 1;

																							if (o1.equals(o2))
																								return 0;

																							String v1 = getIdentityVersion(
																									o1);
																							String v2 = getIdentityVersion(
																									o2);

																							if (v1 == v2)
																								return 0;

																							if (v1 == null)
																								return -1;

																							if (v2 == null)
																								return 1;

																							return new Version(v1)
																									.compareTo(
																											new Version(
																													v2));
																						}

																					};

	private static final Comparator< ? super Resource>	RESOURCE_COMPARATOR			= new Comparator<Resource>() {

																						@Override
																						public int compare(Resource o1,
																								Resource o2) {
																							if (o1 == o2)
																								return 0;

																							if (o1 == null)
																								return -1;
																							if (o2 == null)
																								return 1;

																							if (o1.equals(o2))
																								return 0;

																							if (o1 instanceof ResourceImpl
																									&& o2 instanceof ResourceImpl) {
																								return ((ResourceImpl) o1)
																										.compareTo(o2);
																							}

																							return o1.toString()
																									.compareTo(o2
																											.toString());
																						}
																					};

	public static final Resource						DUMMY_RESOURCE				= new ResourceBuilder().build();
	public static final String							WORKSPACE_NAMESPACE			= "bnd.workspace.project";

	static Converter									cnv							= new Converter();

	static {
		cnv.hook(Version.class, new Hook() {

			@Override
			public Object convert(java.lang.reflect.Type dest, Object o) throws Exception {
				if (o instanceof org.osgi.framework.Version)
					return new Version(o.toString());

				return null;
			}

		});
	}

	public static interface IdentityCapability extends Capability {
		public enum Type {
			bundle(IdentityNamespace.TYPE_BUNDLE), fragment(IdentityNamespace.TYPE_FRAGMENT), unknown(
					IdentityNamespace.TYPE_UNKNOWN),;
			private String s;

			private Type(String s) {
				this.s = s;
			}

			@Override
			public String toString() {
				return s;
			}

		}

		String osgi_identity();

		boolean singleton();

		Version version();

		Type type();

		URI uri();

		String copyright();

		String description(String string);

		String documentation();

		String license();
	}

	public interface ContentCapability extends Capability {
		String osgi_content();

		URI url();

		long size();

		String mime();
	}

	public interface BundleCap extends Capability {
		String osgi_wiring_bundle();

		boolean singleton();

		Version bundle_version();
	}

	public static ContentCapability getContentCapability(Resource resource) {
		List<ContentCapability> caps = getContentCapabilities(resource);
		if (caps.isEmpty())
			return null;
		return caps.get(0);
	}

	public static List<ContentCapability> getContentCapabilities(Resource resource) {
		List<ContentCapability> result = new ArrayList<>();

		for (Capability c : resource.getCapabilities(ContentNamespace.CONTENT_NAMESPACE)) {
			result.add(as(c, ContentCapability.class));
		}
		return result;
	}

	public static IdentityCapability getIdentityCapability(Resource resource) {
		List<Capability> caps = resource.getCapabilities(IdentityNamespace.IDENTITY_NAMESPACE);
		if (caps.isEmpty())
			return null;

		return as(caps.get(0), IdentityCapability.class);
	}

	public static String getIdentityVersion(Resource resource) {
		IdentityCapability cap = getIdentityCapability(resource);
		if (cap == null)
			return null;

		Object v = cap.getAttributes().get(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE);
		if (v == null)
			return null;

		return v.toString();
	}

	public static BundleCap getBundleCapability(Resource resource) {
		List<Capability> caps = resource.getCapabilities(BundleNamespace.BUNDLE_NAMESPACE);
		if (caps.isEmpty())
			return null;

		return as(caps.get(0), BundleCap.class);
	}

	public static Version toVersion(Object v) {
		if (v instanceof Version)
			return (Version) v;

		if (v instanceof org.osgi.framework.Version) {
			org.osgi.framework.Version o = (org.osgi.framework.Version) v;
			String q = o.getQualifier();
			return q.isEmpty() ? new Version(o.getMajor(), o.getMinor(), o.getMicro())
					: new Version(o.getMajor(), o.getMinor(), o.getMicro(), q);
		}

		if (v instanceof String) {
			if (!Version.isVersion((String) v))
				return null;

			return new Version((String) v);
		}

		return null;
	}

	public static final Version getVersion(Capability cap) {
		Object v = cap.getAttributes().get(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE);
		if (v == null)
			return null;

		if (v instanceof Version)
			return (Version) v;

		if (v instanceof org.osgi.framework.Version)
			return new Version(v.toString());

		if (v instanceof String)
			return Version.parseVersion((String) v);

		return null;
	}

	public static URI getURI(Capability contentCapability) {
		Object uriObj = contentCapability.getAttributes().get(ContentNamespace.CAPABILITY_URL_ATTRIBUTE);
		if (uriObj == null)
			return null;

		if (uriObj instanceof URI)
			return (URI) uriObj;

		try {
			if (uriObj instanceof URL)
				return ((URL) uriObj).toURI();

			if (uriObj instanceof String) {
				try {
					URL url = new URL((String) uriObj);
					return url.toURI();
				} catch (MalformedURLException mfue) {
					// Ignore
				}

				File f = new File((String) uriObj);
				if (f.isFile()) {
					return f.toURI();
				}
				return new URI((String) uriObj);
			}

		} catch (URISyntaxException e) {
			throw new IllegalArgumentException("Resource content capability has illegal URL attribute", e);
		}

		return null;
	}

	public static String getVersionAttributeForNamespace(String ns) {
		String name;

		if (IdentityNamespace.IDENTITY_NAMESPACE.equals(ns))
			name = IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE;
		else if (BundleNamespace.BUNDLE_NAMESPACE.equals(ns))
			name = BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE;
		else if (HostNamespace.HOST_NAMESPACE.equals(ns))
			name = HostNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE;
		else if (PackageNamespace.PACKAGE_NAMESPACE.equals(ns))
			name = PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE;
		else if (ServiceNamespace.SERVICE_NAMESPACE.equals(ns))
			name = null;
		else if (ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE.equals(ns))
			name = ExecutionEnvironmentNamespace.CAPABILITY_VERSION_ATTRIBUTE;
		else if (ExtenderNamespace.EXTENDER_NAMESPACE.equals(ns))
			name = ExtenderNamespace.CAPABILITY_VERSION_ATTRIBUTE;
		else if (ContractNamespace.CONTRACT_NAMESPACE.equals(ns))
			name = ContractNamespace.CAPABILITY_VERSION_ATTRIBUTE;
		else
			name = null;

		return name;
	}

	@SuppressWarnings("unchecked")
	public static <T extends Capability> T as(final Capability cap, Class<T> type) {
		return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class< ? >[] {
				type
		}, new InvocationHandler() {

			@Override
			public Object invoke(Object target, Method method, Object[] args) throws Throwable {
				if (Capability.class == method.getDeclaringClass())
					return method.invoke(cap, args);

				return get(method, cap.getAttributes(), cap.getDirectives(), args);
			}
		});
	}

	@SuppressWarnings("unchecked")
	public static <T extends Requirement> T as(final Requirement req, Class<T> type) {
		return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class< ? >[] {
				type
		}, new InvocationHandler() {

			@Override
			public Object invoke(Object target, Method method, Object[] args) throws Throwable {
				if (Requirement.class == method.getDeclaringClass())
					return method.invoke(req, args);

				return get(method, req.getAttributes(), req.getDirectives(), args);
			}
		});
	}

	@SuppressWarnings("unchecked")
	static <T> T get(Method method, Map<String,Object> attrs, Map<String,String> directives, Object[] args)
			throws Exception {
		String name = method.getName().replace('_', '.');

		Object value;
		if (name.startsWith("$"))
			value = directives.get(name.substring(1));
		else
			value = attrs.get(name);
		if (value == null && args != null && args.length == 1)
			value = args[0];

		return (T) cnv.convert(method.getGenericReturnType(), value);
	}

	public static Set<Resource> getResources(Collection< ? extends Capability> providers) {
		if (providers == null || providers.isEmpty())
			return Collections.emptySet();

		Set<Resource> resources = new TreeSet<Resource>(RESOURCE_COMPARATOR);

		for (Capability c : providers) {
			resources.add(c.getResource());
		}

		return resources;
	}

	public static Requirement createWildcardRequirement() {
		return CapReqBuilder.createSimpleRequirement(IdentityNamespace.IDENTITY_NAMESPACE, "*", null)
				.buildSyntheticRequirement();
	}

	public static boolean isEffective(Requirement r, Capability c) {
		String capabilityEffective = c.getDirectives().get(Namespace.CAPABILITY_EFFECTIVE_DIRECTIVE);

		//
		// resolve on the capability will always match any
		// requirement effective
		//

		if (capabilityEffective == null) // default resolve
			return true;

		if (capabilityEffective.equals(Namespace.EFFECTIVE_RESOLVE))
			return true;

		String requirementEffective = r.getDirectives().get(Namespace.CAPABILITY_EFFECTIVE_DIRECTIVE);

		//
		// If requirement is resolve but capability isn't
		//

		if (requirementEffective == null)
			return false;

		return capabilityEffective.equals(requirementEffective);
	}

	public static boolean matches(Requirement r, Resource resource) {
		for (Capability c : resource.getCapabilities(r.getNamespace())) {
			if (matches(r, c))
				return true;
		}
		return false;
	}
	public static boolean matches(Requirement r, Capability c) {
		if (!r.getNamespace().equals(c.getNamespace()))
			return false;

		if (!isEffective(r, c))
			return false;

		String filter = r.getDirectives().get(Namespace.REQUIREMENT_FILTER_DIRECTIVE);
		if (filter == null)
			return true;

		try {
			Filter f = new Filter(filter);
			return f.matchMap(c.getAttributes());
		} catch (Exception e) {
			return false;
		}
	}

	public static String getEffective(Map<String,String> directives) {
		String effective = directives.get(Namespace.CAPABILITY_EFFECTIVE_DIRECTIVE);
		if (effective == null)
			return Namespace.EFFECTIVE_RESOLVE;
		else
			return effective;
	}

	public static ResolutionDirective getResolution(Requirement r) {
		String resolution = r.getDirectives().get(Namespace.REQUIREMENT_RESOLUTION_DIRECTIVE);
		if (resolution == null || resolution.equals(Namespace.RESOLUTION_MANDATORY))
			return ResolutionDirective.mandatory;

		if (resolution.equals(Namespace.RESOLUTION_OPTIONAL))
			return ResolutionDirective.optional;

		return null;
	}

	public static String toRequireCapability(Requirement req) throws Exception {
		StringBuilder sb = new StringBuilder();
		sb.append(req.getNamespace());

		CapReqBuilder r = new CapReqBuilder(req.getNamespace());
		r.addAttributes(req.getAttributes());
		r.addDirectives(req.getDirectives());
		Attrs attrs = r.toAttrs();
		sb.append(";").append(attrs);
		return sb.toString();
	}

	public static String toProvideCapability(Capability cap) throws Exception {
		StringBuilder sb = new StringBuilder();
		sb.append(cap.getNamespace());

		CapReqBuilder r = new CapReqBuilder(cap.getNamespace());
		r.addAttributes(cap.getAttributes());
		r.addDirectives(cap.getDirectives());
		Attrs attrs = r.toAttrs();
		sb.append(";").append(attrs);
		return sb.toString();
	}

	public static Map<URI,String> getLocations(Resource resource) {
		Map<URI,String> locations = new HashMap<>();
		for (ContentCapability c : getContentCapabilities(resource)) {
			URI uri = c.url();
			String sha = c.osgi_content();

			if (uri != null)
				locations.put(uri, sha);
		}
		return locations;
	}

	public static List<Capability> findProviders(Requirement requirement,
			Collection< ? extends Capability> capabilities) {
		List<Capability> result = new ArrayList<>();
		for (Capability capability : capabilities)
			if (matches(requirement, capability))
				result.add(capability);
		return result;
	}

	public static boolean isFragment(Resource resource) {
		IdentityCapability identity = getIdentityCapability(resource);
		if (identity == null)
			return false;
		return IdentityNamespace.TYPE_FRAGMENT
				.equals(identity.getAttributes().get(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE));
	}

	public static String stripDirective(String name) {
		if (Strings.charAt(name, -1) == ':')
			return Strings.substring(name, 0, -1);
		return name;
	}

	public static String getIdentity(Capability identityCapability) throws IllegalArgumentException {
		String id = (String) identityCapability.getAttributes().get(IdentityNamespace.IDENTITY_NAMESPACE);
		if (id == null)
			throw new IllegalArgumentException("Resource identity capability has missing identity attribute");
		return id;
	}

	/**
	 * Create a VersionedClause by applying a version range mask to the
	 * resource! Masks are defined by
	 * {@link aQute.bnd.osgi.Macro#_range(String[])}. If the resource should
	 * represent a project in the bnd workspace, then instead the VersionClause
	 * will refer to it as a snapshot version: e.g. <bsn>;version=snapshot
	 */
	public static VersionedClause toVersionClause(Resource resource, String mask) {
		Capability idCap = getIdentityCapability(resource);
		String identity = getIdentity(idCap);
		String versionString;
		if (resource.getCapabilities(WORKSPACE_NAMESPACE).isEmpty()) {
			Macro macro = new Macro(new Processor());
			Version version = getVersion(idCap);
			versionString = macro._range(new String[] {
					"range", mask, version.toString()
			});
		} else {
			versionString = "snapshot";
		}
		Attrs attribs = new Attrs();
		attribs.put(Constants.VERSION_ATTRIBUTE, versionString);
		return new VersionedClause(identity, attribs);
	}

	static <T> T requireNonNull(T obj) {
		if (obj != null) {
			return obj;
		}
		throw new NullPointerException();
	}
}
