/*******************************************************************************
 * Copyright (c) 2013, 2016 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.osgi.container;

import java.util.*;
import org.eclipse.osgi.internal.framework.FilterImpl;
import org.eclipse.osgi.internal.messages.Msg;
import org.eclipse.osgi.report.resolution.ResolutionReport;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.namespace.*;
import org.osgi.framework.wiring.BundleRevision;
import org.osgi.resource.*;
import org.osgi.service.resolver.ResolutionException;

/**
 * A resolution report implementation used by the container for resolution operations.
 * @since 3.10
 */
class ModuleResolutionReport implements ResolutionReport {

	static class Builder {
		private final Map<Resource, List<Entry>> resourceToEntries = new HashMap<>();

		public void addEntry(Resource resource, Entry.Type type, Object data) {
			List<Entry> entries = resourceToEntries.get(resource);
			if (entries == null) {
				entries = new ArrayList<>();
				resourceToEntries.put(resource, entries);
			}
			entries.add(new EntryImpl(type, data));
		}

		public ModuleResolutionReport build(Map<Resource, List<Wire>> resolutionResult, ResolutionException cause) {
			return new ModuleResolutionReport(resolutionResult, resourceToEntries, cause);
		}
	}

	static class EntryImpl implements Entry {
		private final Object data;
		private final Type type;

		EntryImpl(Type type, Object data) {
			this.type = type;
			this.data = data;
		}

		@Override
		public Object getData() {
			return data;
		}

		@Override
		public Type getType() {
			return type;
		}
	}

	private final Map<Resource, List<Entry>> entries;
	private final ResolutionException resolutionException;
	private final Map<Resource, List<Wire>> resolutionResult;

	ModuleResolutionReport(Map<Resource, List<Wire>> resolutionResult, Map<Resource, List<Entry>> entries, ResolutionException cause) {
		this.entries = entries == null ? Collections.<Resource, List<Entry>> emptyMap() : Collections.unmodifiableMap(new HashMap<>(entries));
		this.resolutionResult = resolutionResult == null ? Collections.<Resource, List<Wire>> emptyMap() : Collections.unmodifiableMap(resolutionResult);
		this.resolutionException = cause;
	}

	@Override
	public Map<Resource, List<Entry>> getEntries() {
		return entries;
	}

	@Override
	public ResolutionException getResolutionException() {
		return resolutionException;
	}

	Map<Resource, List<Wire>> getResolutionResult() {
		return resolutionResult;
	}

	private static String getResolutionReport0(String prepend, ModuleRevision revision, Map<Resource, List<ResolutionReport.Entry>> reportEntries, Set<BundleRevision> visited) {
		if (prepend == null) {
			prepend = ""; //$NON-NLS-1$
		}
		if (visited == null) {
			visited = new HashSet<>();
		}
		if (visited.contains(revision)) {
			return ""; //$NON-NLS-1$
		}
		visited.add(revision);
		StringBuilder result = new StringBuilder();
		String id = revision.getRevisions().getModule().getId().toString();
		result.append(prepend).append(revision.getSymbolicName()).append(" [").append(id).append("]").append('\n'); //$NON-NLS-1$ //$NON-NLS-2$

		List<ResolutionReport.Entry> revisionEntries = reportEntries.get(revision);
		if (revisionEntries == null) {
			result.append(prepend).append("  ").append(Msg.ModuleResolutionReport_NoReport); //$NON-NLS-1$
		} else {
			for (ResolutionReport.Entry entry : revisionEntries) {
				printResolutionEntry(result, prepend + "  ", entry, reportEntries, visited); //$NON-NLS-1$
			}
		}
		return result.toString();
	}

	private static void printResolutionEntry(StringBuilder result, String prepend, ResolutionReport.Entry entry, Map<Resource, List<ResolutionReport.Entry>> reportEntries, Set<BundleRevision> visited) {
		switch (entry.getType()) {
			case MISSING_CAPABILITY :
				result.append(prepend).append(Msg.ModuleResolutionReport_UnresolvedReq).append(printRequirement(entry.getData())).append('\n');
				break;
			case SINGLETON_SELECTION :
				result.append(prepend).append(Msg.ModuleResolutionReport_AnotherSingleton).append(entry.getData()).append('\n');
				break;
			case UNRESOLVED_PROVIDER :
				@SuppressWarnings("unchecked")
				Map<Requirement, Set<Capability>> unresolvedProviders = (Map<Requirement, Set<Capability>>) entry.getData();
				for (Map.Entry<Requirement, Set<Capability>> unresolvedRequirement : unresolvedProviders.entrySet()) {
					// for now only printing the first possible unresolved candidates
					Set<Capability> unresolvedCapabilities = unresolvedRequirement.getValue();
					if (!unresolvedCapabilities.isEmpty()) {
						Capability unresolvedCapability = unresolvedCapabilities.iterator().next();
						// make sure this is not a case of importing and exporting the same package
						if (!unresolvedRequirement.getKey().getResource().equals(unresolvedCapability.getResource())) {
							result.append(prepend).append(Msg.ModuleResolutionReport_UnresolvedReq).append(printRequirement(unresolvedRequirement.getKey())).append('\n');
							result.append(prepend).append("  -> ").append(printCapability(unresolvedCapability)).append('\n'); //$NON-NLS-1$
							result.append(getResolutionReport0(prepend + "     ", (ModuleRevision) unresolvedCapability.getResource(), reportEntries, visited)); //$NON-NLS-1$
						}
					}
				}
				break;
			case FILTERED_BY_RESOLVER_HOOK :
				result.append(Msg.ModuleResolutionReport_FilteredByHook).append('\n');
				break;
			case USES_CONSTRAINT_VIOLATION :
				result.append(prepend).append(Msg.ModuleResolutionReport_UsesConstraintError).append('\n');
				result.append("  ").append(entry.getData()); //$NON-NLS-1$
				break;
			default :
				result.append(Msg.ModuleResolutionReport_Unknown).append("type=").append(entry.getType()).append(" data=").append(entry.getData()).append('\n'); //$NON-NLS-1$ //$NON-NLS-2$
				break;
		}
	}

	private static Object printCapability(Capability cap) {
		if (PackageNamespace.PACKAGE_NAMESPACE.equals(cap.getNamespace())) {
			return Constants.EXPORT_PACKAGE + ": " + createOSGiCapability(cap); //$NON-NLS-1$
		} else if (BundleNamespace.BUNDLE_NAMESPACE.equals(cap.getNamespace())) {
			return Constants.BUNDLE_SYMBOLICNAME + ": " + createOSGiCapability(cap); //$NON-NLS-1$
		} else if (HostNamespace.HOST_NAMESPACE.equals(cap.getNamespace())) {
			return Constants.BUNDLE_SYMBOLICNAME + ": " + createOSGiCapability(cap); //$NON-NLS-1$
		}
		return Constants.PROVIDE_CAPABILITY + ": " + cap.toString(); //$NON-NLS-1$
	}

	private static String createOSGiCapability(Capability cap) {
		Map<String, Object> attributes = new HashMap<>(cap.getAttributes());
		Map<String, String> directives = cap.getDirectives();
		String name = String.valueOf(attributes.remove(cap.getNamespace()));
		return name + ModuleRevision.toString(attributes, false, true) + ModuleRevision.toString(directives, true, true);
	}

	private static String printRequirement(Object data) {
		if (!(data instanceof Requirement)) {
			return String.valueOf(data);
		}
		Requirement req = (Requirement) data;
		if (PackageNamespace.PACKAGE_NAMESPACE.equals(req.getNamespace())) {
			return Constants.IMPORT_PACKAGE + ": " + createOSGiRequirement(req, PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE, PackageNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE); //$NON-NLS-1$
		} else if (BundleNamespace.BUNDLE_NAMESPACE.equals(req.getNamespace())) {
			return Constants.REQUIRE_BUNDLE + ": " + createOSGiRequirement(req, BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE); //$NON-NLS-1$
		} else if (HostNamespace.HOST_NAMESPACE.equals(req.getNamespace())) {
			return Constants.FRAGMENT_HOST + ": " + createOSGiRequirement(req, HostNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE); //$NON-NLS-1$
		}
		return Constants.REQUIRE_CAPABILITY + ": " + req.toString(); //$NON-NLS-1$
	}

	private static String createOSGiRequirement(Requirement requirement, String... versions) {
		Map<String, String> directives = new HashMap<>(requirement.getDirectives());
		String filter = directives.remove(Namespace.REQUIREMENT_FILTER_DIRECTIVE);
		if (filter == null)
			throw new IllegalArgumentException("No filter directive found:" + requirement); //$NON-NLS-1$
		FilterImpl filterImpl;
		try {
			filterImpl = FilterImpl.newInstance(filter);
		} catch (InvalidSyntaxException e) {
			throw new IllegalArgumentException("Invalid filter directive", e); //$NON-NLS-1$
		}
		Map<String, String> matchingAttributes = filterImpl.getStandardOSGiAttributes(versions);
		String name = matchingAttributes.remove(requirement.getNamespace());
		if (name == null)
			throw new IllegalArgumentException("Invalid requirement: " + requirement); //$NON-NLS-1$
		return name + ModuleRevision.toString(matchingAttributes, false, true) + ModuleRevision.toString(directives, true, true);
	}

	@Override
	public String getResolutionReportMessage(Resource resource) {
		return getResolutionReport0(null, (ModuleRevision) resource, getEntries(), null);
	}
}
