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

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 org.apache.commons.collections15.CollectionUtils;

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

	/**
	 * <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 (JFCTargetComparator.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);
			}
		}
		JFCTargetComparator.mutable = mutable;
	}

	/**
	 * <p>
	 * Compares to target strings. The strings are equal, if
	 * <ul>
	 * <li>the class, index, and text of all widgets are equal</li>
	 * <li>either the title or the hashCode of all widgets are equal</li>
	 * <li>either the title 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) {
			instance.addTarget(target1);
			instance.addTarget(target2);
			knownTargets.add(target1);
			knownTargets.add(target2);
			JFCWidget widget1 = instance.find(target1);
			JFCWidget widget2 = instance.find(target2);
			result = (widget1 == widget2);
		}

		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 JFCTargetComparator();
	}

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

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

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

	/**
	 * <p>
	 * Adds a target to the memory.
	 * </p>
	 * 
	 * @param target
	 *            target to be added
	 */
	private void addTarget(String target) {
		if (target != null) {
			String[] targetParts = target.split("\\]\\.\\[");
			if (targetParts.length == 1) {
				addWidget(target.substring(1, target.length() - 1), null);
			} else {
				JFCWidget parent = null;
				for (int i = 0; i < targetParts.length; i++) {
					if (i == 0) {
						parent = addWidget(targetParts[i].substring(1), parent);
					} else if (i == targetParts.length - 1) {
						parent = addWidget(
								targetParts[i].substring(0,
										targetParts[i].length() - 1), parent);
					} else {
						parent = addWidget(targetParts[i], 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 JFCWidget addWidget(String widgetString, JFCWidget parent) {
		String[] widgetInfo = widgetString.split("','");
		JFCWidget widget = generateWidget(widgetString);

		if (parent == null) {
			int index = rootWidgets.indexOf(widget);
			if (index >= 0) {
				widget = rootWidgets.get(index);
				widget.titles.add(widgetInfo[0]);
				widget.hashCodes.add(widgetInfo[4]);
			} else {
				rootWidgets.add(widget);
			}
		} else {
			int index = parent.children.indexOf(widget);
			if (index >= 0) {
				widget = parent.children.get(index);
				widget.titles.add(widgetInfo[0]);
				widget.hashCodes.add(widgetInfo[4]);
			} else {
				parent.children.add(widget);
			}
		}

		return widget;
	}

	/**
	 * <p>
	 * Creates a new {@link JFCWidget} from a widget string.
	 * </p>
	 * 
	 * @param widgetString
	 *            string describing the widget
	 * @return created {@link JFCWidget}
	 */
	private JFCWidget generateWidget(String widgetString) {
		String[] widgetInfo = widgetString.split("','");
		JFCWidget widget = new JFCWidget();
		if (widgetInfo[0].startsWith("'Pos(")) {
			widget.titles.add("Pos");
		} else {
			widget.titles.add(widgetInfo[0]);
		}
		widget.widgetClass = widgetInfo[1];
		widget.text = widgetInfo[2];
		widget.index = widgetInfo[3];
		widget.hashCodes.add(widgetInfo[4]);
		return widget;
	}

	/**
	 * <p>
	 * Tries to find the {@link JFCWidget} that the target string identifies in
	 * the known GUI hierarchy, by traversing the known widgets starting with
	 * the {@link #rootWidgets}.
	 * </p>
	 * 
	 * @param target
	 *            target string whose widget is searched for
	 * @return respective {@link JFCWidget} instance if it is found; null
	 *         otherwise
	 */
	private JFCWidget find(String target) {
		JFCWidget widget = null;
		if (target != null) {
			String[] targetParts = target.split("\\]\\.\\[");
			if (targetParts.length == 1) {
				JFCWidget generatedWidget = generateWidget(target.substring(1,
						target.length() - 1));
				int index = rootWidgets.indexOf(generatedWidget);
				if (index >= 0) {
					widget = rootWidgets.get(index);
				} else {
					return null;
				}
			} else {
				JFCWidget parent = null;
				for (int i = 0; i < targetParts.length; i++) {
					if (i == 0) {
						JFCWidget generatedWidget = generateWidget(targetParts[i]
								.substring(1));
						int index = rootWidgets.indexOf(generatedWidget);
						if (index >= 0) {
							parent = rootWidgets.get(index);
						} else {
							return null;
						}
					} else if (i == targetParts.length - 1) {
						JFCWidget generatedWidget = generateWidget(targetParts[i]
								.substring(0, targetParts[i].length() - 1));
						int index = parent.children.indexOf(generatedWidget);
						if (index >= 0) {
							widget = parent.children.get(index);
						} else {
							return null;
						}
					} else {
						JFCWidget generatedWidget = generateWidget(targetParts[i]);
						int index = parent.children.indexOf(generatedWidget);
						if (index >= 0) {
							parent = parent.children.get(index);
						} else {
							return null;
						}
					}
				}
			}
		}
		return widget;
	}

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

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

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

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

		/**
		 * <p>
		 * Index of the widget.
		 * </p>
		 */
		String index;

		/**
		 * <p>
		 * Text of the widget.
		 * </p>
		 */
		String text;

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

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

		/**
		 * <p>
		 * Two widgets are equal, if {@link #widgetClass}, {@link #index}, and
		 * {@link #text} are equal and the intersection of either the
		 * {@link #hashCodes}, the {@link #titles}, 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 JFCWidget) {
				JFCWidget other = (JFCWidget) obj;
				boolean titleEqual = CollectionUtils.containsAny(titles,
						other.titles);
				boolean hashEqual = CollectionUtils.containsAny(hashCodes,
						other.hashCodes);

				boolean retVal;

				if (widgetClass.equals("Class")) {
					retVal = (widgetClass.equals(other.widgetClass)
							&& text.equals(other.text) && (titleEqual || hashEqual));
				} else {
					retVal = (widgetClass.equals(other.widgetClass)
							&& index.equals(other.index)
							&& text.equals(other.text) && (titleEqual || hashEqual));
				}
				return retVal;
			}
			return false;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Object#hashCode()
		 */
		@Override
		public int hashCode() {
			if (hashCode == 0) {
				int multiplier = 7;
				hashCode = multiplier * hashCode + widgetClass.hashCode();
				if (!widgetClass.equals("Class")) {
					hashCode = multiplier * hashCode + index.hashCode();
				}
				hashCode = multiplier * hashCode + text.hashCode();
			}
			return hashCode;
		}
	}
}
