package org.biomoby.registry.rdfagent.verifier;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.biomoby.shared.MobyData;
import org.biomoby.shared.MobyNamespace;
import org.biomoby.shared.MobyPrimaryData;
import org.biomoby.shared.MobyPrimaryDataSet;
import org.biomoby.shared.MobyPrimaryDataSimple;
import org.biomoby.shared.MobySecondaryData;
import org.biomoby.shared.MobyService;

/**
 * This class compares 2 services and determines what it is that is different
 * between them. If you are interested in the validity of the inputs/outputs for
 * a particular service, then take a look at
 * areInputsOutputsForServiceValid(MobyService).
 * <p>
 * To use this class, do the following:
 * 
 * <pre>
 * <code>
 *     		// create 2 services and do stuff with them
 *     		MobyService service1 = new MobyService();
 *     		MobyService service2 = new MobyService();
 *    
 *     		...
 *    
 *     		// now use the MobyServiceComparator class 
 *     		MobyServiceComparator c = new MobyServiceComparator();
 *     		if (c.areServicesDifferent(service1, service2)) {
 *     			// print out the differences
 *     			System.out.println(c.getDifferences());
 *     		} else {
 *     			System.out.println(&quot;The 2 services are identical&quot;);
 *     		}
 *     
 *     		// lets check if the inputs and outputs for service 1 are valid
 *     		if (c.areInputsOutputsForServiceValid(service1)) {
 *     			System.out.println(&quot;The inputs/outputs are valid!&quot;);
 *     		}
 * </code>
 * </pre>
 * 
 * @author Eddie created Feb 2, 2006
 */
@SuppressWarnings("unchecked")
public class MobyServiceComparator {

	private boolean isDifferent;

	private StringBuffer buffer = null;

	/**
	 * Constructor
	 * 
	 */
	public MobyServiceComparator() {
		init();
	}

	private void init() {
		isDifferent = false;
		buffer = new StringBuffer();
	}

	/**
	 * 
	 * @param service1
	 *            a service to compare to service2
	 * @param service2
	 *            a service to compare with service1
	 * @return true if the service signatures are not equal, false otherwise.
	 *         For services that are null, true is always returned.
	 */
	public boolean areServicesDifferent(MobyService service1, MobyService service2) {
		init();
		if (service1 == null || service2 == null)
			return true;

		/*
		 * compare all aspects of the services any differences are recorded in
		 * the string buffer
		 */

		// compare the name - usually different if using this class stand
		// alone
		if (!service1.getName().equals(service2.getName())) {
			addDifference("Service1 has name '" + service1.getName()
					+ "' which is different from '" + service2.getName() + "'.");
			isDifferent = true;
		}

		// compare authorities
		if (!service1.getAuthority().equals(service2.getAuthority())) {
			addDifference("Service1 has authority '" + service1.getAuthority()
					+ "' which is different from '" + service2.getAuthority() + "'.");
			isDifferent = true;
		}

		// compare authoritative
		if (service1.isAuthoritative() != service2.isAuthoritative()) {
			addDifference("Service1 is '" + (service1.isAuthoritative() ? "" : "not ")
					+ "authoritative' while service2 is"
					+ (service2.isAuthoritative() ? "." : " not."));
			isDifferent = true;
		}

		// compare category
		if (!service1.getCategory().equals(service2.getCategory())) {
			addDifference("Service1 has category '" + service1.getCategory()
					+ "' which is different from '" + service2.getCategory() + "'.");
			isDifferent = true;
		}
		// compare descriptions
		if (!service1.getDescription().equals(service2.getDescription())) {
			addDifference("Service1 has description '" + service1.getDescription()
					+ "' which is different from '" + service2.getDescription() + "'.");
			isDifferent = true;
		}

		// compare email
		if (!service1.getEmailContact().equals(service2.getEmailContact())) {
			addDifference("Service1 has contact address '" + service1.getEmailContact()
					+ "' which is different from '" + service2.getEmailContact() + "'.");
			isDifferent = true;
		}

		// compare signature url
		if (!service1.getSignatureURL().equals(service2.getSignatureURL())) {
			addDifference("Service1 has signature url '" + service1.getSignatureURL()
					+ "' which is different from '" + service2.getSignatureURL() + "'.");
			isDifferent = true;
		}

		// compare type
		if (!service1.getType().equals(service2.getType())) {
			addDifference("Service1 has type '" + service1.getType()
					+ "' which is different from '" + service2.getType() + "'.");
			isDifferent = true;
		}

		// compare the service urls
		if (!service1.getURL().equals(service2.getURL())) {
			addDifference("Service1 has service url '" + service1.getURL()
					+ "' which is different from '" + service2.getURL() + "'.");
			isDifferent = true;
		}

		// compare primary inputs
		Map primaryInputs1 = getMapFromMobyData(service1.getPrimaryInputs());
		Map primaryInputs2 = getMapFromMobyData(service2.getPrimaryInputs());
		compareMobyDataMaps(primaryInputs1, primaryInputs2, true);
		// free up memory
		primaryInputs1 = null;
		primaryInputs2 = null;

		// compare secondary inputs
		Map secondaryInputs1 = getMapFromMobyData(service1.getSecondaryInputs());
		Map secondaryInputs2 = getMapFromMobyData(service2.getSecondaryInputs());
		compareMobyDataMaps(secondaryInputs1, secondaryInputs2, true);
		// free up memory
		secondaryInputs1 = null;
		secondaryInputs2 = null;

		// compare primary outputs
		Map outputs1 = getMapFromMobyData(service1.getPrimaryOutputs());
		Map outputs2 = getMapFromMobyData(service2.getPrimaryOutputs());
		compareMobyDataMaps(outputs1, outputs2, false);
		// free up memory
		outputs1 = null;
		outputs2 = null;

		return isDifferent;
	}

	/*
	 * method that compares Maps of MobyData types.
	 */
	private void compareMobyDataMaps(Map dataMap1, Map dataMap2, boolean isInput) {
		for (Iterator it = dataMap1.keySet().iterator(); it.hasNext();) {
			// is the key in map2 ?
			String key = (String) it.next();
			if (dataMap2.containsKey(key)) {
				MobyData data1 = (MobyData) dataMap1.get(key);
				MobyData data2 = (MobyData) dataMap2.remove(key);
				if (data1 instanceof MobyPrimaryDataSet) {
					MobyPrimaryDataSet set1 = (MobyPrimaryDataSet) data1;
					MobyPrimaryDataSet set2 = (MobyPrimaryDataSet) data2;

					Map collectionInputs1 = getMapFromCollection(set1.getElements());
					Map collectionInputs2 = getMapFromCollection(set2.getElements());
					for (Iterator collectionInputIt = collectionInputs1.keySet().iterator(); collectionInputIt
							.hasNext();) {
						String inputKey = (String) collectionInputIt.next();
						if (collectionInputs2.containsKey(inputKey)) {
							MobyPrimaryDataSimple colData1 = (MobyPrimaryDataSimple) collectionInputs1
									.get(inputKey);
							MobyPrimaryDataSimple colData2 = (MobyPrimaryDataSimple) collectionInputs2
									.remove(inputKey);

							if (colData1.getDataType().getName().equals(
									colData2.getDataType().getName())) {

								// check the namespaces if of the same type
								Map namespaces1 = getMapFromMobyNamespace(colData1.getNamespaces());
								Map namespaces2 = getMapFromMobyNamespace(colData2.getNamespaces());
								for (Iterator nsIt = namespaces1.keySet().iterator(); nsIt
										.hasNext();) {
									String nsKey = (String) nsIt.next();
									// remove it from namespaces2
									if (namespaces2.remove(nsKey) == null) {
										// namespae2 was different -> key not in
										// it
										addDifference("A collection "
												+ (isInput ? "input" : "output")
												+ " for service2 did not have the namespace '"
												+ nsKey + "'.");
										isDifferent = true;
									}
								}
								// now go through what is left in namespaces2
								for (Iterator nsIt = namespaces2.keySet().iterator(); nsIt
										.hasNext();) {
									String nsKey = (String) nsIt.next();
									// namespae2 was different -> key not in it
									addDifference("A collection " + (isInput ? "input" : "output")
											+ " for service2 had the additional namespace: '"
											+ nsKey + "'");
									isDifferent = true;
								}
							} else {
								// different object types
								addDifference("Service1 had an " + (isInput ? "input" : "output")
										+ " in a collection of type '"
										+ colData1.getDataType().getName()
										+ "' while service2 had an "
										+ (isInput ? "input" : "output") + " of type '"
										+ colData2.getDataType().getName()
										+ "'. Both had an articlename of '" + inputKey + "'.");
								isDifferent = true;
							}
						} else {
							addDifference("Service1 had a collection "
									+ (isInput ? "input" : "output") + " named '" + inputKey
									+ "' while service2 did not.");
							isDifferent = true;
						}
					}
					for (Iterator collectionInputIt = collectionInputs2.keySet().iterator(); collectionInputIt
							.hasNext();) {
						addDifference("Service2 had a collection " + (isInput ? "input" : "output")
								+ " named '" + (String) collectionInputIt.next()
								+ "' while service1 did not.");
						isDifferent = true;
					}
				} else if (data1 instanceof MobyPrimaryDataSimple) {
					// check for same object type
					if (((MobyPrimaryDataSimple) data1).getDataType().getName().equals(
							((MobyPrimaryDataSimple) data2).getDataType().getName())) {

						// check the namespaces if of the same type
						Map namespaces1 = getMapFromMobyNamespace(((MobyPrimaryDataSimple) data1)
								.getNamespaces());
						Map namespaces2 = getMapFromMobyNamespace(((MobyPrimaryDataSimple) data2)
								.getNamespaces());
						for (Iterator nsIt = namespaces1.keySet().iterator(); nsIt.hasNext();) {
							String nsKey = (String) nsIt.next();
							// remove it from namespaces2
							if (namespaces2.remove(nsKey) == null) {
								// namespae2 was different -> key not in it
								addDifference("An " + (isInput ? "input" : "output")
										+ " for service2 did not have the namespace '" + nsKey
										+ "'.");
								isDifferent = true;
							}
						}
						// now go through what is left in namespaces2
						for (Iterator nsIt = namespaces2.keySet().iterator(); nsIt.hasNext();) {
							String nsKey = (String) nsIt.next();
							// namespae2 was different -> key not in it
							addDifference("An " + (isInput ? "input" : "output")
									+ " for service2 had the additional namespace: '" + nsKey
									+ "'.");
							isDifferent = true;
						}
					} else {
						// different object types
						addDifference("Service1 had an " + (isInput ? "input" : "output")
								+ " of type '"
								+ ((MobyPrimaryDataSimple) data1).getDataType().getName()
								+ "' while service2 had an " + (isInput ? "input" : "output")
								+ " of type '"
								+ ((MobyPrimaryDataSimple) data2).getDataType().getName()
								+ "'. Both had an articlename of '" + key + "'.");
						isDifferent = true;
					}
				} else if (data1 instanceof MobySecondaryData) {
					// check for same object type, enums, max, min, default, etc
					MobySecondaryData secondary1 = (MobySecondaryData) data1;
					MobySecondaryData secondary2 = (MobySecondaryData) data2;

					if (secondary1.getDataType().equals(secondary2.getDataType())) {

						// compare maximums
						if (!secondary1.getMaxValue().equals(secondary2.getMaxValue())) {
							addDifference("Service1 had a secondary input maxinmum value '"
									+ secondary1.getMaxValue()
									+ "' while service2 had a secondary input maximum value of '"
									+ secondary2.getMaxValue() + "'. Both had an articlename of '"
									+ key + "'.");
							isDifferent = true;
						}
						// compare minimums
						if (!secondary1.getMinValue().equals(secondary2.getMinValue())) {
							addDifference("Service1 had a secondary input mininmum value of '"
									+ secondary1.getMinValue()
									+ "' while service2 had a secondary input minimum value of '"
									+ secondary2.getMinValue() + "'. Both had an articlename of '"
									+ key + "'.");
							isDifferent = true;
						}
						// compare defaults
						if (!secondary1.getDefaultValue().equals(secondary2.getDefaultValue())) {
							addDifference("Service1 had a secondary input default value of '"
									+ secondary1.getDefaultValue()
									+ "' while service2 had a secondary input default value of '"
									+ secondary2.getDefaultValue()
									+ "'. Both had an articlename of '" + key + "'.");
							isDifferent = true;
						}
						// compare descriptions
						if (!secondary1.getDescription().equals(secondary2.getDescription())) {
							addDifference("Service1 had a secondary input description of '"
									+ secondary1.getDescription()
									+ "' while service2 had a secondary input description of '"
									+ secondary2.getDescription()
									+ "'. Both had an articlename of '" + key + "'.");
							isDifferent = true;
						}
						
						// compare allowable values
						Map allowable1 = getMapStrings(secondary1.getAllowedValues());
						Map allowable2 = getMapStrings(secondary2.getAllowedValues());
						for (Iterator nsIt = allowable1.keySet().iterator(); nsIt.hasNext();) {
							String valueKey = (String) nsIt.next();
							// remove it from namespaces2
							if (allowable2.remove(valueKey) == null) {
								// secondary2 didnt have the value
								addDifference("A secondary input for service2 did not have the allowable value of '"
										+ valueKey + "'.");
								isDifferent = true;
							}
						}
						// now iterate through secondary2
						// now go through what is left in namespaces2
						for (Iterator nsIt = allowable2.keySet().iterator(); nsIt.hasNext();) {
							String valueKey = (String) nsIt.next();
							// namespae2 was different -> key not in it
							addDifference("A secondary input for service2 had the additional allowable value: '"
									+ valueKey + "'");
							isDifferent = true;
						}

					} else {
						// different types
						addDifference("Service1 had a secondary input of type '"
								+ secondary1.getDataType()
								+ "' while service2 had a secondary input of type "
								+ secondary2.getDataType() + ". Both had an articlename of '" + key
								+ "'.");
						isDifferent = true;
					}

				} else {
					// unknown type! shouldnt get here

				}
			} else {
				// no -> record difference
				addDifference("Service1 had an " + (isInput ? "input" : "output") + " named '"
						+ key + "' while service2 did not.");
				isDifferent = true;

			}
		}
		// now look through the second map for any new types
		// record them
		for (Iterator it = dataMap2.keySet().iterator(); it.hasNext();) {
			addDifference("Service2 had an " + (isInput ? "input" : "output") + " named '"
					+ (String) it.next() + "' while service1 did not.");
			isDifferent = true;
		}
	}

	private Map getMapFromCollection(MobyPrimaryDataSimple[] elements) {
		HashMap map = new HashMap();
		for (int i = 0; i < elements.length; i++) {
			map.put(elements[i].getDataType().getName(), elements[i]);
		}
		return map;
	}

	private Map getMapFromMobyData(MobyData[] datas) {
		HashMap map = new HashMap();
		for (int i = 0; i < datas.length; i++) {
			MobyData data = datas[i];
			if (data instanceof MobyPrimaryDataSimple) {
				map.put("Simple:" + data.getName(), data);
			} else if (data instanceof MobyPrimaryDataSet) {
				map.put("Collection:" + data.getName(), data);
			} else if (data instanceof MobySecondaryData) {
				map.put("Secondary:" + data.getName(), data);
			}
		}
		return map;
	}

	/**
	 * 
	 * @param service
	 *            a MobyService object
	 * @return true if the service has inputs and outputs that are structured
	 *         correctly, i.e. are all named, false otherwise.
	 */
	public boolean areInputsOutputsForServiceValid(MobyService service) {
		boolean b = areInputsOutputsForServiceValid(service.getPrimaryInputs());
		b = b && areInputsOutputsForServiceValid(service.getSecondaryInputs());
		b = b && areInputsOutputsForServiceValid(service.getPrimaryOutputs());
		return b;
	}

	private boolean areInputsOutputsForServiceValid(MobyData[] datas) {
		for (int i = 0; i < datas.length; i++) {
			MobyData data = datas[i];
			if (data.getName().equals(""))
				return false;
		}
		return true;
	}

	private Map getMapFromMobyNamespace(MobyNamespace[] datas) {
		HashMap map = new HashMap();
		for (int i = 0; i < datas.length; i++) {
			// namespace should only be here once
			map.put(datas[i].getName(), "");
		}
		return map;
	}

	private Map getMapStrings(String[] datas) {
		HashMap map = new HashMap();
		for (int i = 0; i < datas.length; i++) {
			map.put(datas[i], "");
		}
		return map;
	}

	/*
	 * private method for convinience basically takes a string and appends a
	 * newline
	 */
	private void addDifference(String difference) {
		buffer.append("\t" + difference + System.getProperty("line.separator"));
	}

	/**
	 * 
	 * @return a string of differences for the last comparison of services
	 */
	public String getDifferences() {
		return buffer.toString();
	}

	/**
	 * 
	 * @param s
	 *            the service to attempt to extract the errors from
	 * @return a string of possible reasons for the prevention of this service
	 *         to be registered with a registry
	 */
	public String getServiceErrors(MobyService s) {
		StringBuffer sb = new StringBuffer();
		String newline = System.getProperty("line.separator");

		if (s.getName().trim().equals("")) {
			sb.append("\tService name was not found." + newline);
		}
		if (s.getAuthority().trim().equals("")) {
			sb.append("\tService authority was not found." + newline);
		}
		if (s.getCategory().trim().equals("")) {
			sb.append("\tService category was not found." + newline);
		}
		if (s.getDescription().trim().equals("")) {
			sb.append("\tService description was not found." + newline);
		}
		if (s.getEmailContact().trim().equals("")) {
			sb.append("\tService contact email was not found." + newline);
		}
		if (s.getServiceType().getName().trim().equals("")) {
			sb.append("\tService type name was not found." + newline);
		}

		if (s.getPrimaryInputs().length == 0 && s.getPrimaryOutputs().length == 0)
			sb.append("\tService didn't have an input and or output defined." + newline);

		MobyData[] datas = s.getPrimaryInputs();
		for (int i = 0; i < datas.length; i++) {

			if (datas[i] instanceof MobyPrimaryData)
				if (datas[i].getName().trim().equals(""))
					sb.append("\tService input, of type "
							+ ((MobyPrimaryData) datas[i]).getDataType().getName()
							+ ", had an empty articlename." + newline);
		}
		datas = null;
		
		MobySecondaryData[] secondaries = s.getSecondaryInputs();
		for (int i = 0; i < secondaries.length; i++) {
			if (secondaries[i].getName().trim().equals(""))
				sb.append("\tService secondary input, of type "
						+ secondaries[i].getDataType()
						+ ", had an empty articlename." + newline);
		}
		secondaries = null;
		
		datas = s.getPrimaryOutputs();
		for (int i = 0; i < datas.length; i++) {
			if (datas[i] instanceof MobyPrimaryData)
				if (datas[i].getName().trim().equals(""))
					sb.append("\tService output, of type "
							+ ((MobyPrimaryData) datas[i]).getDataType().getName()
							+ ", had an empty articlename." + newline);
		}
		datas = null;

		return sb.toString();
	}
}
