package de.ugoe.cs.quest.plugin.mfc.eventcore;

import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.collections15.CollectionUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

/**
 * <p>
 * This class implements a comparator for target strings of MFC GUIs. It
 * internally maintains a collection of all targets that have been compared to
 * ensure the transitivity of the equals relation. This memory can always be
 * deleted by calling {@link #reset()}.
 * </p>
 * 
 * @author Steffen Herbold
 * @version 1.0
 */
public class MFCTargetComparator {

	/**
	 * <p>
	 * If mutable is true, new target strings can be added to the internal
	 * memory. This leads to a very expensive {@link #compare(String, String)}
	 * operation.
	 * </p>
	 * <p>
	 * if mutable is set to false, currently possible equal targets are
	 * pre-computed. This pre-computation is expensive and might take a while.
	 * In turn, the {@link #compare(String, String)} operation becomes very
	 * cheap.
	 * </p>
	 */
	private static boolean mutable = true;

	/**
	 * <p>
	 * Set of all currently known targets.
	 * </p>
	 */
	private static Set<String> knownTargets = new LinkedHashSet<String>();

	/**
	 * <p>
	 * Map that contains for all known target strings all equal target strings.
	 * Pre-computed when {@link #mutable} is set to false.
	 * </p>
	 */
	private static Map<String, Set<String>> equalTargets;

	/**
	 * <p>
	 * Changes the mutability of the comparator. If the mutability is changed
	 * from true to false, the map {@link #equalTargets} is computed.
	 * </p>
	 * 
	 * @param mutable
	 *            new mutability of the comparator
	 */
	public static void setMutable(boolean mutable) {
		if (MFCTargetComparator.mutable == true && mutable == false) {
			equalTargets = new HashMap<String, Set<String>>();
			for (String target1 : knownTargets) {
				Set<String> curEqualTargets = new HashSet<String>();
				for (String target2 : knownTargets) {
					if (compare(target1, target2)) {
						curEqualTargets.add(target2);
					}
				}
				equalTargets.put(target1, curEqualTargets);
			}
		}
		MFCTargetComparator.mutable = mutable;
	}

	/**
	 * <p>
	 * Compares to target strings. The strings are equal, if TODO
	 * <ul>
	 * <li>the class, resourceId, and modality of all widgets are equal</li>
	 * <li>either the name or the hashCode of all widgets are equal</li>
	 * <li>either the name or the hashCode has been observed in one equal
	 * instance of a widget, for all widgets.</li>
	 * </ul>
	 * </p>
	 * <p>
	 * All target strings are remembered internally, to be able to test for the
	 * third property.
	 * </p>
	 * 
	 * @param target1
	 *            first target string
	 * @param target2
	 *            second target string
	 * @return true, if both targets are equal; false otherwise
	 */
	public static boolean compare(String target1, String target2) {
		boolean result = false;
		if (mutable) {
			try {
				MFCWidget widget1 = null;
				MFCWidget widget2 = null;
				if (!"dummy".equals(target1)) {
					instance.addTarget(target1);
					knownTargets.add(target1);
					widget1 = instance.find(target1);
				}
				if (!"dummy".equals(target2)) {
					instance.addTarget(target2);
					knownTargets.add(target2);
					widget2 = instance.find(target2);
				}
				if (widget1 == null) {
					return false;
				}
				result = (widget1 == widget2);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		if (!mutable) {
			Set<String> curEquals = equalTargets.get(target1);
			if (curEquals != null) {
				result = curEquals.contains(target2);
			}
		}

		return result;
	}

	/**
	 * <p>
	 * Resets the internal memory of targets.
	 * </p>
	 */
	public static void reset() {
		instance = new MFCTargetComparator();
	}

	/**
	 * <p>
	 * Internal handle to the instance of this class (implemented as
	 * Singleton!).
	 * </p>
	 */
	private static MFCTargetComparator instance = new MFCTargetComparator();

	/**
	 * <p>
	 * Private Constructor. Creates a new instance of the class and prevents
	 * instantiation from outside of this class.
	 * </p>
	 */
	private MFCTargetComparator() {
	}

	/**
	 * <p>
	 * List of the root widgets found in the target string.
	 * </p>
	 */
	private List<MFCWidget> rootWidgets = new ArrayList<MFCTargetComparator.MFCWidget>();

	/**
	 * <p>
	 * Adds a target to the memory.
	 * </p>
	 * 
	 * @param target
	 *            target to be added
	 */
	private void addTarget(String target) throws Exception {
		if (target != null) {
			DocumentBuilder documentBuilder = DocumentBuilderFactory
					.newInstance().newDocumentBuilder();
			Document doc = documentBuilder.parse(new ByteArrayInputStream(
					("<dummy>" + target + "</dummy>").getBytes("UTF-8")));
			doc.getDocumentElement().normalize();
			NodeList widgets = doc.getElementsByTagName("window");

			MFCWidget parent = null;
			for (int i = 0; i < widgets.getLength(); i++) {
				Element currentWidget = (Element) widgets.item(i);
				parent = addWidget(currentWidget, parent);
			}
		}
	}

	/**
	 * <p>
	 * Adds a widget extracted from a target to the memory. The widget is placed
	 * as a child/parent of other widget according to the GUI hierarchy of the
	 * application.
	 * </p>
	 * <p>
	 * In case the widget already exists, the existing widget is returned and
	 * the known targets and hashCodes of the existing widget are updated.
	 * </p>
	 * 
	 * @param widgetString
	 *            string identifying the widget
	 * @param parent
	 *            parent widget; if null, it is a root widget and added to
	 *            {@link #rootWidgets}
	 * @return the created widget.
	 */
	private MFCWidget addWidget(Element widgetElement, MFCWidget parent) {
		MFCWidget widget = generateWidget(widgetElement);

		if (parent == null) {
			int index = rootWidgets.indexOf(widget);
			if (index >= 0) {
				widget = rootWidgets.get(index);
				widget.names.add(widgetElement.getAttribute("name"));
				widget.hwnds.add(widgetElement.getAttribute("hwnd"));
			} else {
				rootWidgets.add(widget);
			}
		} else {
			int index = parent.children.indexOf(widget);
			if (index >= 0) {
				widget = parent.children.get(index);
				widget.names.add(widgetElement.getAttribute("name"));
				widget.hwnds.add(widgetElement.getAttribute("hwnd"));
			} else {
				parent.children.add(widget);
			}
		}
		return widget;
	}

	/**
	 * <p>
	 * Creates a new {@link MFCWidget} from a widget XML element.
	 * </p>
	 * 
	 * @param widgetElement
	 *            XML element containing information about the widget
	 * @return created {@link MFCWidget}
	 */
	private MFCWidget generateWidget(Element widgetElement) {
		MFCWidget widget = new MFCWidget();
		widget.names.add(widgetElement.getAttribute("name"));
		widget.hwnds.add(widgetElement.getAttribute("hwnd"));
		widget.widgetClass = widgetElement.getAttribute("class");
		widget.resourceId = widgetElement.getAttribute("resourceId");
		widget.modality = widgetElement.getAttribute("isModal");
		return widget;
	}

	/**
	 * <p>
	 * Tries to find the {@link MFCWidget} that the target string identifies in
	 * the known GUI hierarchy, by traversing the known widgets starting with
	 * the {@link #rootWidgets}.
	 * 
	 * @param target
	 *            target string whose widget is searched for
	 * @return respective {@link MFCWidget} instance if it is found; null
	 *         otherwise
	 */
	private MFCWidget find(String target) throws Exception {
		MFCWidget widget = null;
		if (target != null) {
			DocumentBuilder documentBuilder = DocumentBuilderFactory
					.newInstance().newDocumentBuilder();
			Document doc = documentBuilder.parse(new ByteArrayInputStream(
					("<dummy>" + target + "</dummy>").getBytes("UTF-8")));
			doc.getDocumentElement().normalize();
			NodeList widgets = doc.getElementsByTagName("window");

			MFCWidget parent = null;
			for (int i = 0; i < widgets.getLength(); i++) {
				Element currentWidget = (Element) widgets.item(i);
				MFCWidget generatedWidget = generateWidget(currentWidget);
				if (parent == null) {
					int index = rootWidgets.indexOf(generatedWidget);
					if (index >= 0) {
						parent = rootWidgets.get(index);
					} else {
						return null;
					}
				} else {
					int index = parent.children.indexOf(generatedWidget);
					if (index >= 0) {
						parent = parent.children.get(index);
					} else {
						return null;
					}
				}
			}
			widget = parent;
		}
		return widget;
	}

	/**
	 * <p>
	 * Internal class used to store MFCWidget. The implementation is more like a
	 * C-style structure, than an actual class.
	 * </p>
	 * 
	 * @author Steffen Herbold
	 * @version 1.0
	 */
	private static class MFCWidget {

		/**
		 * <p>
		 * Set of all known name strings of the widget.
		 * </p>
		 */
		Set<String> names = new LinkedHashSet<String>();

		/**
		 * <p>
		 * Set of all known hwnds of the widget.
		 * </p>
		 */
		Set<String> hwnds = new LinkedHashSet<String>();

		/**
		 * <p>
		 * Class of the widget.
		 * </p>
		 */
		String widgetClass;

		/**
		 * <p>
		 * Resource id of the widget.
		 * </p>
		 */
		String resourceId;

		/**
		 * <p>
		 * Modality of the widget.
		 * </p>
		 */
		String modality;

		/**
		 * <p>
		 * Pre-computed hash code of the widget.
		 * </p>
		 */
		int hashCode = 0;

		/**
		 * <p>
		 * List of children of the widget.
		 * </p>
		 */
		List<MFCWidget> children = new ArrayList<MFCTargetComparator.MFCWidget>();

		/**
		 * <p>
		 * Two widgets are equal, if {@link #widgetClass}, {@link #resourceId},
		 * and {@link #modality} are equal and the intersection of either the
		 * {@link #hwnds}, the {@link #names}, or both of them is not empty.
		 * </p>
		 * 
		 * @see java.lang.Object#equals(java.lang.Object)
		 */
		@Override
		public boolean equals(Object obj) {
			if (obj instanceof MFCWidget) {
				MFCWidget other = (MFCWidget) obj;
				boolean titleEqual = CollectionUtils.containsAny(names,
						other.names);
				boolean hashEqual = CollectionUtils.containsAny(hwnds,
						other.hwnds);

				return widgetClass.equals(other.widgetClass)
						&& resourceId.equals(other.resourceId)
						&& modality.equals(other.modality)
						&& (titleEqual || hashEqual);
			}
			return false;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Object#hashCode()
		 */
		@Override
		public int hashCode() {
			if (hashCode == 0) {
				int multiplier = 17;
				hashCode = multiplier * hashCode + widgetClass.hashCode();
				hashCode = multiplier * hashCode + resourceId.hashCode();
				hashCode = multiplier * hashCode + modality.hashCode();
			}
			return hashCode;
		}
	}
}
