/*
 * This source code is made available under the terms of the
 * Common Public License Version 1.0 distibuted along with the
 * source code package.
 */
package de.uka.cmtm.serviceregistry.query.protege;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import de.uka.cmtm.serviceregistry.query.ClassNotFoundEvent;
import de.uka.cmtm.serviceregistry.query.ServiceLocatorEventListener;
import de.uka.cmtm.serviceregistry.query.ServiceProfileInfo;
import de.uka.cmtm.serviceregistry.query.ServiceProfileLocator;
import de.uka.cmtm.serviceregistry.query.ServiceProfileParameter;
import de.uka.cmtm.serviceregistry.query.protege.dig.DigProfileIndividualLocator;
import edu.stanford.smi.protegex.owl.ProtegeOWL;
import edu.stanford.smi.protegex.owl.model.OWLClass;
import edu.stanford.smi.protegex.owl.model.OWLIndividual;
import edu.stanford.smi.protegex.owl.model.OWLModel;
import edu.stanford.smi.protegex.owl.model.OWLOntology;

/**
 * Implementation of ServiceProfileLocator that uses a
 * DigProfileIndividualLocator to perform ontology queries.
 * 
 * @author tilmann
 */
public class ProtegeServiceProfileLocator implements ServiceProfileLocator {

	private List<ServiceLocatorEventListener> listeners = new LinkedList<ServiceLocatorEventListener>();

	private static final Log log = LogFactory
			.getLog(ProtegeServiceProfileLocator.class);

	/**
	 * The OWLModel we are working with.
	 */
	private OWLModel owlModel;

	/**
	 * Namespaces of the ontologies in the owlModel.
	 */
	private Set<String> namespacePrefixes = new TreeSet<String>();

	/**
	 * Use this to calculate Relevance of found services.
	 */
	private RelevanceCalculator relevanceCalculator = new SimpleRelevanceCalculator();

	/**
	 * Use this to find service profile individuals
	 */
	private ProfileIndividualLocator profileIndividualLocator;

	/**
	 * Used to check interface constraints.
	 */
	private int expectedSearchLevel = 0;

	/**
	 * The parameter used for profile search;
	 */
	private ProfileIndividualParameter piParameter;

	/**
	 * Creates a new ProfileIndividualLocator
	 * 
	 * @throws InstantiationException
	 *             if there is an error while reading the ontology ob
	 *             instantiating the DigProfileIndividualLocator
	 */
	public ProtegeServiceProfileLocator(String digReasonerUrl,
			String serviceOntologyUrl, String upperServiceOntologyUrl,
			String topOntologyUrl) throws InstantiationException {
		try {
			// Read ontology
			owlModel = ProtegeOWL.createJenaOWLModelFromURI(serviceOntologyUrl);

			// Extract namespace prefixes
			String logStr = "Found ontology prefixes:";
			for (OWLOntology ontology : (Collection<OWLOntology>) owlModel
					.getOWLOntologies()) {
				log.trace("Found ontlogogy " + ontology.getURI());
				String prefix = ontology.getNamespacePrefix();
				if (null != prefix && !"".equals(prefix))
					namespacePrefixes.add(prefix + ":");
				logStr += " " + prefix;
			}
			log.info(logStr);

			// Instantiate DigProfileIndividualLocator
			profileIndividualLocator = new DigProfileIndividualLocator(
					owlModel, digReasonerUrl, upperServiceOntologyUrl,
					topOntologyUrl);

		} catch (Exception e) {
			InstantiationException ie = new InstantiationException(
					"Could not instantiate ProtegeServiceProfileLocator.");
			ie.initCause(e);
			throw ie;
		}
	}

	/**
	 * This method can be used to search for services profiles suitable to the
	 * previously set searchParameters. The resulting list may also contain
	 * services that do not perfectly match the parameters. This is indicated
	 * with a relevance below 1. How exact sercice profiles are matched can be
	 * specified wiht the searchLevel parameter. Subsequent calls for a single
	 * search parameter must start with search level 0 and may increase
	 * searchlevel only by one each time up to the maximum that can be obtained
	 * by getAvailableSearchLevels.
	 * 
	 * @param searchLevel
	 *            the search level to be applied
	 * @return a list of search results
	 */
	public List<ServiceProfileInfo> findServiceProfiles(int searchLevel)
			throws IOException {

		// debug output
		log.debug("findServiceProfiles invoced with serach level "
				+ searchLevel);

		if (searchLevel >= getAvailableSearchLevels())
			throw new IllegalArgumentException("SearchLevel " + searchLevel
					+ " not supported.");

		if (piParameter == null)
			throw new IllegalStateException(
					"The search parameter must be set prior to a call to findServiceProfiles");

		// TODO: Maybe we can drop this if we have DIG 2.0 queries
		if (searchLevel != expectedSearchLevel)
			throw new IllegalStateException("Wrong search level: Given "
					+ searchLevel + " Expected " + expectedSearchLevel);
		expectedSearchLevel = (expectedSearchLevel + 1)
				% getAvailableSearchLevels();

		// Prepare result list
		List<ServiceProfileInfo> foundProfiles = new LinkedList<ServiceProfileInfo>();

		// Look for matching profile individuals
		List<ProfileIndividualInfo> profileIndividuals = profileIndividualLocator
				.findProfileIndividuals(searchLevel);

		for (ProfileIndividualInfo individual : profileIndividuals) {

			// compute relevance
			double relevance = relevanceCalculator.calculateRelevance(
					individual, piParameter, profileIndividualLocator
							.getConformClasses());

			// create result list entry
			foundProfiles.add(createServiceProfileInfo(individual, relevance));
		}

		// debug output
		if (log.isDebugEnabled()) {
			StringBuilder sb = new StringBuilder();
			sb.append("Found ServiceProfileInfos:\n");
			for (ServiceProfileInfo profile : foundProfiles)
				sb.append("---------------\n").append(profile);
			log.debug(sb.toString());
		}

		return foundProfiles;
	}

	/**
	 * This method creates a ServiceProfileInfo for a given
	 * ProfileIndividualInfo by filling out lists with the names of the
	 * OWLClasses minus prefix.
	 * 
	 * @param profile
	 *            the profile to be converted
	 * @param relevance
	 *            the relevance to set for the output
	 * @return the newly created ServiceProfileInfo
	 */
	private ServiceProfileInfo createServiceProfileInfo(
			ProfileIndividualInfo profile, double relevance) {

		ServiceProfileInfo spInfo = new ServiceProfileInfo();
		spInfo.setRelevance(relevance);
		spInfo.setProfileUri(profile.getProfile().getURI());
		spInfo.setCategory(removePrefix(profile.getProfile().getRDFType()
				.getName()));
		if (profile.getDescription() != null) {
			spInfo.setDescription(profile.getDescription().getString());
		}

		List<String> list;

		list = new ArrayList<String>(profile.getPreconditions().size());
		for (OWLIndividual ind : profile.getPreconditions()) {
			list.add(removePrefix(ind.getRDFType().getName()));
		}
		spInfo.setPreconditions(list);

		list = new ArrayList<String>(profile.getEffects().size());
		for (OWLIndividual ind : profile.getEffects()) {
			list.add(removePrefix(ind.getRDFType().getName()));
		}
		spInfo.setEffects(list);

		list = new ArrayList<String>(profile.getLogicalInputs().size());
		for (OWLIndividual ind : profile.getLogicalInputs()) {
			list.add(removePrefix(ind.getRDFType().getName()));
		}
		spInfo.setLogicalInputs(list);

		list = new ArrayList<String>(profile.getLogicalOutputs().size());
		for (OWLIndividual ind : profile.getLogicalOutputs()) {
			list.add(removePrefix(ind.getRDFType().getName()));
		}
		spInfo.setLogicalOutputs(list);

		list = new ArrayList<String>(profile.getUserRoles().size());
		for (OWLIndividual ind : profile.getUserRoles()) {
			list.add(removePrefix(ind.getRDFType().getName()));
		}
		spInfo.setUserRoles(list);

		return spInfo;
	}

	/**
	 * Remove the ontlology prefix from a class name
	 * 
	 * @param name
	 *            the protege name of the class
	 * @return the name without prefix
	 */
	private String removePrefix(String name) {
		return name.substring(name.indexOf(":") + 1);
	}

	/**
	 * Find a call in the owl model by searchin the default and afterwards the
	 * imported owl ontologie.
	 * 
	 * @param name
	 *            the name of the class
	 * @return the corresponding OWLClass if found, null otherwise
	 */
	private OWLClass findClass(String name) {

		OWLClass owlClass = null;

		// Look in default ontology
		if (name != null && !"".equals(name)) {
			owlClass = owlModel.getOWLNamedClass(name);

			// Consult imported ontologies if necessary
			if (owlClass == null) {
				Iterator<String> iter = namespacePrefixes.iterator();
				while (owlClass == null && iter.hasNext()) {
					owlClass = owlModel.getOWLNamedClass(iter.next() + name);
				}
			}
			if (owlClass == null)
				sendClassNotFoundEvent(name);
		}
		return owlClass;
	}

	/**
	 * This method creates a ProfileIndividualParameter for a given
	 * ServiceProfileParameter by looking up corresponding OWLClasses in the owl
	 * model and creating the class sets. Classes that cannot be found are
	 * ignored.
	 * 
	 * @param spParameter
	 *            the ServiceProfileParameter to be converted
	 * @return the newly created ProfileIndividualParameter
	 */
	private ProfileIndividualParameter findParameterClasses(
			ServiceProfileParameter spParameter) {

		ProfileIndividualParameter piParameter = new ProfileIndividualParameter();

		piParameter.setCategory(findClass(spParameter.getCategory()));

		Set<OWLClass> classes;

		classes = new HashSet<OWLClass>(spParameter.getUserRoles().size());
		for (String className : spParameter.getUserRoles()) {
			OWLClass owlClass = findClass(className);
			if (owlClass != null)
				classes.add(owlClass);
		}
		piParameter.setUserRoles(classes);

		classes = new HashSet<OWLClass>(spParameter.getPreconditions().size());
		for (String className : spParameter.getPreconditions()) {
			OWLClass owlClass = findClass(className);
			if (owlClass != null)
				classes.add(owlClass);
		}
		piParameter.setPreconditions(classes);

		classes = new HashSet<OWLClass>(spParameter.getEffects().size());
		for (String className : spParameter.getEffects()) {
			OWLClass owlClass = findClass(className);
			if (owlClass != null)
				classes.add(owlClass);
		}
		piParameter.setEffects(classes);

		classes = new HashSet<OWLClass>(spParameter.getLogicalInputs().size());
		for (String className : spParameter.getLogicalInputs()) {
			OWLClass owlClass = findClass(className);
			if (owlClass != null)
				classes.add(owlClass);
		}
		piParameter.setLogicalInputs(classes);

		classes = new HashSet<OWLClass>(spParameter.getLogicalOutputs().size());
		for (String className : spParameter.getLogicalOutputs()) {
			OWLClass owlClass = findClass(className);
			if (owlClass != null)
				classes.add(owlClass);
		}
		piParameter.setLogicalOutputs(classes);

		return piParameter;
	}

	/**
	 * Get the maximum search level that can be used plus one.
	 * 
	 * @return the number of search levels available
	 */
	public int getAvailableSearchLevels() {
		return profileIndividualLocator.getAvailableSearchLevels();
	}

	/**
	 * Sets a ServiceLocatorEventListener to be notified of events during search
	 * 
	 * @param listener
	 *            the ServiceLocatorEventListener to be set
	 */
	public void addServiceLocatorEventListener(
			ServiceLocatorEventListener listener) {
		listeners.add(listener);
	}

	/**
	 * Removes a previously set ServiceLocatorEventListener
	 * 
	 * @param listener
	 *            the ServiceLocatorEventListener to be removed
	 */
	public void removeServiceLocatorEventListener(
			ServiceLocatorEventListener listener) {
		listeners.remove(listener);
	}

	private void sendClassNotFoundEvent(String className) {
		ClassNotFoundEvent e = new ClassNotFoundEvent(this, className);
		for (ServiceLocatorEventListener listener : listeners)
			listener.classNotFound(e);
	}

	/**
	 * Sets the search parameter for subsequent calls to findServiceProfiles
	 * 
	 * @param searchParameter
	 *            the parameters the services shoud have
	 */
	public void setServiceProfileParameter(
			ServiceProfileParameter searchParameter) {
		piParameter = findParameterClasses(searchParameter);
		profileIndividualLocator.setProfileIndividualParameter(piParameter);
		expectedSearchLevel = 0;
	}

}
