package biz.aQute.resolve.internal;

import java.io.*;
import java.util.*;
import java.util.Map.Entry;

import org.osgi.framework.*;
import org.osgi.framework.namespace.*;
import org.osgi.resource.*;
import org.osgi.resource.Resource;
import org.osgi.service.log.*;
import org.osgi.service.repository.*;

import aQute.bnd.build.*;
import aQute.bnd.build.model.*;
import aQute.bnd.build.model.clauses.*;
import aQute.bnd.header.*;
import aQute.bnd.osgi.*;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.resource.*;
import aQute.bnd.service.*;
import aQute.bnd.service.resolve.hook.*;
import aQute.libg.filters.*;
import aQute.libg.filters.Filter;
import biz.aQute.resolve.*;

public class BndrunResolveContext extends GenericResolveContext {

	public static final String			RUN_EFFECTIVE_INSTRUCTION	= "-resolve.effective";
	public static final String			PROP_RESOLVE_PREFERENCES	= "-resolve.preferences";

	private Registry					registry;

	private EE							ee;

	private List<ExportedPackage>		sysPkgsExtra = new ArrayList<ExportedPackage>();
	private Parameters					sysCapsExtraParams = new Parameters();
	private Parameters					resolvePrefs;

	private Version						frameworkResourceVersion	= null;
	private FrameworkResourceRepository	frameworkResourceRepo;
	private final Processor				properties;

	public BndrunResolveContext(BndEditModel runModel, Registry registry, LogService log) {
		super(log);
		try {
			this.registry = registry;
			this.properties = runModel.getProperties();
		}
		catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	protected synchronized void init() {
		if (initialised)
			return;

		try {
			loadEE();
			loadSystemPackagesExtra();
			loadSystemCapabilitiesExtra();
			loadpaths();
			loadRepositories();
			constructBlacklist();
			loadEffectiveSet();
			findFramework();
			constructInputRequirements();
			loadPreferences();
		}
		catch (Exception e) {
			throw new RuntimeException(e);
		}
		super.init();
	}

	/**
	 * Load the capabilities and packages from the -testpath and -runpath since
	 * we make those available. Testpath is only added whent he Test-Cases header
	 * is set in the lowest properties file.
	 */
	private void loadpaths() throws Exception {
		loadpath(Constants.RUNPATH, "-runpath");
		if ( properties.get(Constants.TESTCASES) != null && !properties.is(Constants.NOJUNITOSGI))
			loadpath(Constants.TESTPATH, "-testpath");
	}

	/**
	 * Read the bundles from a path (test/run) and copy their exported packages to the
	 * packages extra list and their capabilities to the capabilities extra list just
	 * like the launcher does. This only works when we have access to a project
	 * or bndrun file parented by a project. 
	 */
	private void loadpath(String path, String source ) throws Exception {
		Processor parent = properties.getParent();
		if ( parent == null || ! (parent instanceof Project))
			return;

		//
		// Get the full contents of the path
		//
		
		Project p = (Project) parent;
		String header = properties.mergeProperties(path);
		
		//
		// Now, from this path, get the bundles
		//
		
		List<Container> bundles = p.getBundles(Strategy.HIGHEST, header, source);
		for ( Container c : bundles ) {
			
			if ( c.getAttributes() != null && Processor.isTrue(c.getAttributes().get("-skip:")))
					continue;
			
			//
			// If the container is ok, we add the exported packages and the capabilities
			// to the frameworks' extra packages/caps.
			//
			
			if ( c.getError() == null) {
				Domain domain = Domain.domain(c.getManifest());
				this.sysCapsExtraParams.putAll(domain.getProvideCapability());
				this.sysPkgsExtra.addAll( toExportedPackages( domain.getExportPackage()));
			} else
				p.warning("The %s contains an error %s for bundle %s", source, c.getError(), c.getBundleSymbolicName());
		}
	}

	private void loadEE() {
		EE tmp = EE.parse(properties.getProperty(Constants.RUNEE));
		ee = (tmp != null) ? tmp : EE.JavaSE_1_6;
	}

	private void loadEffectiveSet() {
		String effective = properties.getProperty(RUN_EFFECTIVE_INSTRUCTION);
		if (effective == null)
			effectiveSet = null;
		else {
			effectiveSet = new HashMap<String, Set<String>>();
			for (Entry<String,Attrs> entry : new Parameters(effective).entrySet()) {
				String skip = entry.getValue().get("skip:");
				Set<String> toSkip = skip == null ? new HashSet<String>() : 
					new HashSet<String>(Arrays.asList(skip.split(",")));
				effectiveSet.put(entry.getKey(), toSkip);
			}
		}
	}

	private void loadSystemPackagesExtra() {
		Parameters p = new Parameters( properties.mergeProperties(Constants.RUNSYSTEMPACKAGES));

		sysPkgsExtra = toExportedPackages(p); // runModel.getSystemPackages();
	}

	private List<ExportedPackage> toExportedPackages(Parameters p) {
		List<ExportedPackage> list = new ArrayList<ExportedPackage>();
		for ( Entry<String,Attrs> e : p.entrySet()) {
			list.add( new ExportedPackage(Processor.removeDuplicateMarker(e.getKey()), e.getValue()));
		}

		return list;
	}

	private void loadSystemCapabilitiesExtra() {
		String header = properties.mergeProperties(Constants.RUNSYSTEMCAPABILITIES);
		sysCapsExtraParams.putAll( new Parameters(header));
	}

	private void loadRepositories() throws IOException {
		// Get all of the repositories from the plugin registry
		List<Repository> allRepos = registry.getPlugins(Repository.class);

		// Workspace ws = registry.getPlugin(Workspace.class);
		// if (ws != null) {
		// for (InfoRepository ir : registry.getPlugins(InfoRepository.class)) {
		// allRepos.add(new InfoRepositoryWrapper(ir, ws.getCache("ir-" +
		// ir.getName())));
		// }
		// }


		// Reorder/filter if specified by the run model

		String rn = properties.mergeProperties(Constants.RUNREPOS);
		if (rn == null) {
			// No filter, use all
			for (Repository repo : allRepos) {
				super.addRepository(repo);
			}
		} else {
			Parameters repoNames = new Parameters(rn);

			// Map the repository names...
			Map<String,Repository> repoNameMap = new HashMap<String,Repository>(allRepos.size());
			for (Repository repo : allRepos)
				repoNameMap.put(repo.toString(), repo);

			// Create the result list
			for (String repoName : repoNames.keySet()) {
				Repository repo = repoNameMap.get(repoName);
				if (repo != null)
					super.addRepository(repo);
			}
		}
	}

	private void findFramework() {
		Requirement frameworkReq = getFrameworkRequirement();
		if (frameworkReq == null)
			return;

		// Iterate over repos looking for matches
		for (Repository repo : repositories) {
			Map<Requirement,Collection<Capability>> providers = findProviders(repo, frameworkReq);
			Collection<Capability> frameworkCaps = providers.get(frameworkReq);
			if (frameworkCaps != null) {
				for (Capability frameworkCap : frameworkCaps) {
					if (findFrameworkContractCapability(frameworkCap.getResource()) != null) {
						Version foundVersion = toVersion(frameworkCap.getAttributes().get(
								IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE));
						if (foundVersion != null) {
							if (frameworkResourceVersion == null
									|| (foundVersion.compareTo(frameworkResourceVersion) > 0)) {
								systemResource = frameworkCap.getResource();
								frameworkResourceVersion = foundVersion;
								frameworkResourceRepo = new FrameworkResourceRepository(systemResource, ee,
										sysPkgsExtra, sysCapsExtraParams);
								systemCapabilityIndex = frameworkResourceRepo.getCapabilityIndex();
							}
						}
					}
				}
			}
		}
	}

	private Requirement getFrameworkRequirement() {
		String header = properties.getProperty(Constants.RUNFW);
		if (header == null)
			return null;

		// Get the identity and version of the requested JAR
		Parameters params = new Parameters(header);
		if (params.size() > 1)
			throw new IllegalArgumentException("Cannot specify more than one OSGi Framework.");
		Entry<String,Attrs> entry = params.entrySet().iterator().next();
		String identity = entry.getKey();

		String versionStr = entry.getValue().getVersion();

		// Construct a filter & requirement to find matches
		Filter filter = new SimpleFilter(IdentityNamespace.IDENTITY_NAMESPACE, identity);
		if (versionStr != null)
			filter = new AndFilter().addChild(filter).addChild(new LiteralFilter(Filters.fromVersionRange(versionStr)));
		Requirement frameworkReq = new CapReqBuilder(IdentityNamespace.IDENTITY_NAMESPACE).addDirective(
				Namespace.REQUIREMENT_FILTER_DIRECTIVE, filter.toString()).buildSyntheticRequirement();
		return frameworkReq;
	}

	@Override
	public Resource getInputResource() {
		if (inputResource != null) {
			return inputResource;
		}
		constructInputRequirements();
		return inputResource;
	}

	@Override
	public Resource getSystemResource() {
		if (systemResource != null) {
			return systemResource;
		}
		findFramework();
		return systemResource;
	}

	@Override
	public boolean isSystemResource(Resource resource) {
		Resource systemResource = getSystemResource();
		return resource == systemResource;
	}

	private void constructInputRequirements() {
		Parameters inputRequirements = new Parameters(properties.mergeProperties(Constants.RUNREQUIRES));
		if (inputRequirements == null || inputRequirements.isEmpty()) {
			inputResource = null;
		} else {
			List<Requirement> requires = CapReqBuilder.getRequirementsFrom(inputRequirements);

			ResourceBuilder resBuilder = new ResourceBuilder();
			CapReqBuilder identity = new CapReqBuilder(IdentityNamespace.IDENTITY_NAMESPACE).addAttribute(
					IdentityNamespace.IDENTITY_NAMESPACE, IDENTITY_INITIAL_RESOURCE);
			resBuilder.addCapability(identity);

			for (Requirement req : requires) {
				resBuilder.addRequirement(req);
			}

			inputResource = resBuilder.build();
		}
	}

	private void constructBlacklist() {
		Parameters blacklist = new Parameters(properties.mergeProperties(Constants.RUNBLACKLIST));
		if (blacklist != null && !blacklist.isEmpty()) {
			List<Requirement> reject = CapReqBuilder.getRequirementsFrom(blacklist);
			setBlackList(reject);
		}
	}


	private void loadPreferences() {
		resolvePrefs = new Parameters(properties.getProperty(PROP_RESOLVE_PREFERENCES));
	}

	protected void postProcessProviders(Requirement requirement, Set<Capability> wired, List<Capability> candidates) {
		if (candidates.size() == 0)
			return;

		// Call resolver hooks
		for (ResolverHook resolverHook : registry.getPlugins(ResolverHook.class)) {
			resolverHook.filterMatches(requirement, candidates);
		}

		// Process the resolve preferences
		boolean prefsUsed = false;
		if (resolvePrefs != null && !resolvePrefs.isEmpty()) {
			List<Capability> insertions = new LinkedList<Capability>();
			for (Iterator<Capability> iterator = candidates.iterator(); iterator.hasNext();) {
				Capability cap = iterator.next();
				if (resolvePrefs.containsKey(getResourceIdentity(cap.getResource()))) {
					iterator.remove();
					insertions.add(cap);
				}
			}

			if (!insertions.isEmpty()) {
				candidates.addAll(0, insertions);
				prefsUsed = true;
			}
		}

		// If preferences were applied, then don't need to call the callbacks
		if (!prefsUsed) {
			for (ResolutionCallback callback : callbacks) {
				callback.processCandidates(requirement, wired, candidates);
			}
		}
	}
}
