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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import de.ugoe.cs.quest.eventcore.Event;
import de.ugoe.cs.quest.eventcore.IEventTarget;
import de.ugoe.cs.quest.plugin.jfc.guimodel.JFCGUIElement;
import de.ugoe.cs.quest.plugin.jfc.guimodel.JFCGUIElementSpec;


/**
 * <p>
 * This class provides the functionality to calculate the unique GUITAR replayIDs
 * for {@link JFCEvent}s. This code is mainly based on guitar source code: edu.umd.cs.guitar.
 * model.JFCDefaultIDGeneratorSimple
 *    
 * </p>
 * @author fabian.glaser
 *
 */

public class JFCReplayIDCalculator {
	
	static final int prime = 31;
	private JFCReplayIDValidator validator;
	
	public JFCReplayIDCalculator() {
		this.validator = null;
	}
	
	public JFCReplayIDCalculator(JFCReplayIDValidator validator){
		this.validator = validator;
	}
	
	/**
	 * Properties that are used to identify widgets
	 */
	private static List<String> ID_PROPERTIES = Arrays.asList(
			"Class","Title","Icon");
	
   /**
    * Those classes are invisible widgets but cause false-positive when
    * calculating ID (compare guitar source code: edu.umd.cs.guitar.
    * model.JFCDefaultIDGeneratorSimple)
    */
   private static List<String> IGNORED_CLASSES = Arrays.asList("javax.swing.JPanel",
         "javax.swing.JTabbedPane", "javax.swing.JScrollPane",
         "javax.swing.JSplitPane", "javax.swing.Box",
         "javax.swing.JViewport", "javax.swing.JScrollBar",
         "javax.swing.JLayeredPane",
         "javax.swing.JList$AccessibleJList$AccessibleJListChild",
         "javax.swing.JList$AccessibleJList", "javax.swing.JList",
         "javax.swing.JScrollPane$ScrollBar",
         "javax.swing.plaf.metal.MetalScrollButton");
   
   /**
    * Calculates the replayID of a JFCEvent needed for compatibility with guitar suite
    * @param List of {@link JFCGUIElementSpec}s that represent the component path of a event target
    * for which the replayID should be calculated. 
    * @return replayID
    */
	
   public String calculateReplayID(List<JFCGUIElementSpec> guiElementPath){
	   String replayID = "";
	   long hashCode = 1;
	 
	   ListIterator<JFCGUIElementSpec> iterator = guiElementPath.listIterator();
	   
	   JFCGUIElementSpec currentSpec = iterator.next();
	   String title = currentSpec.getName();
	   String fuzzyTitle = getFuzzyTitle(title);
	   long windowHashCode = fuzzyTitle.hashCode();
	   windowHashCode = (windowHashCode * 2) & 0xffffffffL;

	   long propagatedHashCode = windowHashCode;
	   
	   // added validator to check if generated component ids are known
	   if (validator != null){
		   if (validator.validateReplayID("w" + windowHashCode)){
			   System.out.println("ID w" + windowHashCode + " is valid.");
		   }
		   else{
			   System.err.println(currentSpec + " describes an unknown component.");
		   	   System.err.println("ID w" + windowHashCode + " is unknown." );
		   	   System.err.println();
		   }
			   
	   }
	   
	   // construct looks complicated but avoids going back and forth through path
	   if (iterator.hasNext())
		   currentSpec = iterator.next();
	   else{
		   currentSpec = null;
		   // there are no subcomponents, so we use windowHashCode as hashCode
		   hashCode = windowHashCode;
	   }

	   // walk through component path and calculate hashcode
	   while(currentSpec != null){
		   long localHashCode = getLocalHashCode(currentSpec);
		   hashCode = propagatedHashCode * prime + localHashCode;
		   hashCode = (hashCode * 2) & 0xffffffffL;
		   
		   // added validator to check if generated component ids are known
		   if (validator != null){
			   if (validator.validateReplayID("w" + hashCode)){
				   System.out.println("ID w" + hashCode + " is valid.");
				   System.out.println("==> " + currentSpec + " describes a known component.");
			   }
			   else{
				   System.err.println("ID w" + hashCode + " is unknown." );
				   System.err.println("==> " + currentSpec + " describes an unknown component.");
				   System.err.println();
			   }
		   }

		   if (iterator.hasNext()){
			   currentSpec = iterator.next();
			   Integer index = currentSpec.getIndex();
			   propagatedHashCode = prime * propagatedHashCode
					   + index.hashCode();
		   }
		   else
			   currentSpec = null;
			   
	   }

	   replayID = "e" + hashCode;

	   return replayID;
   }
   
   
	/**
	 * Calculates the replayID of a JFCEvent needed for compatibility with guitar suite 
	 * @param event for which the ID should be calculated
	 * @return replayID
	 */
	public String calculateReplayID(Event event){
		List<JFCGUIElementSpec> guiElementPath = new ArrayList<JFCGUIElementSpec>();
		
		IEventTarget target = event.getTarget();
		if (!target.getPlatform().equals("JFC")){
			throw new IllegalArgumentException("Event target must be of type JFC.");
		}
		
		JFCGUIElement currentTarget = (JFCGUIElement) target;
		
		// extract element path
		while (currentTarget != null){
			JFCGUIElementSpec currentSpec = (JFCGUIElementSpec) currentTarget.getSpecification();
			
			// new specification must be inserted at the beginning of the list
			guiElementPath.add(0, currentSpec);
			currentTarget = (JFCGUIElement) currentTarget.getParent();
		}
		
		// calculation is delegated to other calculateReplayID method
		return this.calculateReplayID(guiElementPath);
	}
	
	/**
	 * Calculates the hashcode part of a component.
	 * @param spec The {@link JFCGUIElementSpec} for which the hashcode should be calculated.
	 * @return the local hashcode
	 */
	
	private long getLocalHashCode(JFCGUIElementSpec spec){
		long hashcode = 1;
		String wClass = spec.getType();
		if (IGNORED_CLASSES.contains(wClass)) {
		    hashcode = (prime * hashcode + (wClass.equals("null") ? 0 : (wClass
		               .hashCode())));
		    return hashcode;
		}
		else{
			Map<String, String> idProperties = extractIDProperties(spec);
			for (String property: idProperties.keySet()){
				String value = idProperties.get(property);
				if (!value.equals("null")){
					hashcode = prime * hashcode + property.hashCode();
					hashcode = prime * hashcode + value.hashCode();
				}
			}
		}
		
		hashcode = (hashcode * 2) & 0xffffffffL;
		
		return hashcode;
	}
	
	/**
	 * Extracts the IDProperties from a given {@link JFCGUIElementSpec}.
	 * @param spec
	 * @return LinkedHashMap that contains the IDProperties and its values.
	 */
	
	private Map<String, String> extractIDProperties(JFCGUIElementSpec spec){
		LinkedHashMap<String, String> idProperties = new LinkedHashMap<String, String>();
		if (ID_PROPERTIES.contains("Class")){
			idProperties.put("Class", spec.getType());
		}
		if (ID_PROPERTIES.contains("Title")){
			String name = spec.getName();
			// spec returns extra "" that need to be removed
			idProperties.put("Title", name.substring(1, name.length() - 1));
		}
		if (ID_PROPERTIES.contains("Icon")){
			idProperties.put("Icon", spec.getIcon());
		}
		if (ID_PROPERTIES.contains("Index")){
			idProperties.put("Index", Integer.toString(spec.getIndex()));
		}	
		return idProperties;
	}
	
	/**
	 * Guitar has a special way to deal with window titles when
	 * calculating unique widget IDs. This method mimics Guitar's
	 * behavior (compare guitar source code: edu.umd.cs.guitar.
	 * model.JFCDefaultIDGeneratorSimple).
	 * @param title
	 * @return fuzzyTitle
	 */

	private String getFuzzyTitle(String title){
		final List<String> PATTERNS = 
				Arrays.asList("Rachota .*",
						"OmegaT-.*",
						"Buddi.*",
						"Open:.*",
						"JabRef.*",
						"GanttProject.*",
						".*Pauker.*",
						".*FreeMind.*",
						".* - ArgoUML.*",
						"Save Project .*");


		for (String sPattern : PATTERNS) {
			if (matchRegex(title, sPattern)) {
				return sPattern;
			}
		}

		return title;
	}

	/**
	 * Determine if the input string matches the input regex pattern.
	 * This method mimics Guitars behavior.
	 * Attempt to match the pattern 'sPattern' with the string 'sInputString'.

	 * @param sInputString    Input string to match with pattern
	 * @param sPattern        Regex pattern to match with string
	 * @return                True if match, false otherwise
	 */
	
	private static boolean
	matchRegex(String sInputString,
			String sPattern)
	{
		Pattern pattern;
		Matcher matcher;

		pattern = Pattern.compile(sPattern);
		matcher = pattern.matcher(sInputString);
		if (matcher.matches()) {
			return true;
		}

		return false;
	}
	
};
