package org.inria.bmajwatcher.client.ui.bank_editor;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.inria.bmajwatcher.client.ui.styles.MainCss;
import org.inria.bmajwatcher.client.ui.styles.Resources;

import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.FocusWidget;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.TextBox;
import com.smartgwt.client.util.SC;

/**
 * Validates a set of properties.
 * 
 * @author rsabas
 *
 */
public class BankValidator {

	private static BankValidator validator = new BankValidator();
//	private Map<String, Widget> currentValues;
//	private Map<String, String> refValues;
	
	private Map<String, Map<String, FocusWidget>> currentValues;
	private Map<String, Map<String, String>> refValues;
	private Map<String, Map<String, String>> processes;
	private Map<String, List<String>> removedValues;
	
	private Map<String, DirectHttpParameterBuilder> directHttpParameters;
	private Map<String, BankDependencyBuilder> dependenciesParameters;
	
	private Map<String, String> propertiesType = new HashMap<String, String>();
	
	private String currentBank = "";
	private Collection<String> mandatoryProps;
	private String mode = "";
	
	public final static String MODIFY = "modify";
	public final static String NEW = "new";
	
	private MainCss css = Resources.INSTANCE.mainCss();
	
	private BankValidator() {
		init();
	}
	
	private void init() {
		currentValues = new HashMap<String, Map<String, FocusWidget>>();
		refValues = new HashMap<String, Map<String, String>>();
		processes = new HashMap<String, Map<String,String>>();
		removedValues = new HashMap<String, List<String>>();
		directHttpParameters = new HashMap<String, DirectHttpParameterBuilder>();
		dependenciesParameters = new HashMap<String, BankDependencyBuilder>();
	}
	
	public void addType(String prop, String type) {
		propertiesType.put(prop, type);
	}
	
	public void setMandatoryProps(Collection<String> props) {
		this.mandatoryProps = props;
	}
	
	public Collection<String> getMandatoryProps() {
		return mandatoryProps;
	}
	
	public void clear(String bank, String mode) {
		this.mode = mode;
		this.currentBank = bank;
		init();
	}	
	
	public FocusWidget getWidget(String bank, String key) {
		if (currentValues.get(bank) != null)
			return currentValues.get(bank).get(key);
		return null;
	}
	
	public void setBankProcesses(String bank, Map<String, String> props) {
		processes.put(bank, props);
	}
	
	public void addValue(String bank, String key, FocusWidget value) {
		if (currentValues.get(bank) == null) {
			currentValues.put(bank, new HashMap<String, FocusWidget>());
		}
		currentValues.get(bank).put(key, value);
	}
	
	public void setRefValues(String bank, Map<String, String> map) {
//		Map<String, String> copy = new HashMap<String, String>();
//		copy.putAll(map);
//		refValues.put(bank, copy);
		refValues.put(bank, map);
	}
	
	/**
	 * Remove value key and associated value from data structure
	 * 
	 * @param bank
	 * @param key
	 * @param allBanks
	 * 
	 * @return whether the removed property was found in a parent file
	 */
	public boolean removeValue(String bank, String key, Map<String, Map<String, String>> allBanks) {
		boolean foundInParent = false;
		
		if (refValues.containsKey(bank)) {
			
			if (refValues.get(bank).containsKey(key)) {
				refValues.get(bank).remove(key);
			}
			
			if (currentValues.get(bank).containsKey(key)) {			
				/*
				 * Add to remove list
				 */
				if (removedValues.get(bank) == null)
					removedValues.put(bank, new ArrayList<String>());
				removedValues.get(bank).add(key);
				
				/*
				 * Remove from GUI
				 */
				currentValues.get(bank).get(key).getParent().removeFromParent();
				
				/*
				 * Remove from map
				 */
				currentValues.get(bank).remove(key);
				
				/*
				 * If the removed property is defined in another file,
				 * add to the current bank as inherited property. 
				 */
				if (!key.startsWith("inherited:")) {
					for (String s : refValues.keySet()) {
						if (!s.equals(currentBank) && refValues.get(s).containsKey(key)) {
							foundInParent = true;
							if (!refValues.get(currentBank).containsKey(key)) {
								FocusWidget w;
								if ((w = currentValues.get(currentBank).remove("inherited:" + key)) != null)
									w.removeFromParent();
								
								refValues.get(currentBank).put("inherited:" + key, refValues.get(s).get(key));
								break;
							}
						}
					}
					if (!foundInParent) {
						for (String s : allBanks.keySet()) {
							if (!s.equals(currentBank) && allBanks.get(s).containsKey(key)) {
								foundInParent = true;
								FocusWidget w;
								if ((w = currentValues.get(currentBank).remove("inherited:" + key)) != null)
									w.removeFromParent();
								refValues.get(currentBank).put("inherited:" + key, allBanks.get(s).get(key));
								break;
							}
						}
					}
					/*
					 * Remove the inherited property from the bank if it
					 * has no parent. 
					 */
					if (!foundInParent) {
						removeValue(currentBank, "inherited:" + key, allBanks);
//						refValues.get(currentBank).remove("inherited:" + key);
					}
				} else {
					/*
					 * It is an inherited property, so exists in a parent.
					 */
					foundInParent = true;
				}
			}
		}
		return foundInParent;
	}
	
	
	public void removeBank(String bank, Map<String, Map<String, String>> allBanks) {
		Collection<String> keyCopy = new ArrayList<String>();
		for (String prop : allBanks.get(bank).keySet())
			keyCopy.add(prop);
		
		for (String key : keyCopy)
			removeValue(bank, key, allBanks);
		
		currentValues.remove(bank);
		refValues.remove(bank);
		removedValues.remove(bank);
	}
	
	
	/**
	 * When a value is changed in a parent file, update the value in its descendants.
	 * 
	 * @param property property to update
	 * @param bankName
	 */
	public void updateInheritedProperty(String property, String bankName) {
		FocusWidget value = null;
		String prop = property.split(":")[1];
		for (String bank : currentValues.keySet()) {
			if (!bank.equals(bankName)) {
				if (currentValues.get(bank).containsKey(prop)) {
					if (bank.equals("global.properties") && value == null)
						value = currentValues.get(bank).get(prop);
					else if (!bank.equals("global.properties"))
						value = currentValues.get(bank).get(prop);
				}
			}
		}
		if (value != null) {
			FocusWidget widget = currentValues.get(bankName).get(property);
			if (widget instanceof TextBox) {
				((TextBox) widget).setText(((TextBox) value).getText());
			} else if (widget instanceof CheckBox) {
				((CheckBox) widget).setValue(((CheckBox) value).getValue());
			} else if (widget instanceof ListBox) {
				((ListBox) widget).setSelectedIndex(((ListBox) value).getSelectedIndex());
			}
		}
	}
	
	public static boolean propertyExistsInOtherFile(String property, String bankName, Map<String, Map<String, String>> ref) {
		for (String bank : ref.keySet()) {
			if (!bank.equals(bankName)) {
				if (ref.get(bank).containsKey(property))
					return true;
			}
		}
		
		return false;
	}
	
	private boolean bankContainsMandatoryProperty(Collection<String> bankProps, String mandatoryProp) {
		for (String s : bankProps) {
			if (s.split("__")[1].equals(mandatoryProp))
				return true;
		}
		return false;
	}
	
	/**
	 * Validates the fields.
	 * 
	 * @param ref map that contains properties for all the banks.
	 * 		This is necessary as the currentValues object can contain not
	 * 		all the files as a file is added only when the user select it in
	 * 		the drop down list on the gui.
	 * @param looseValidation if true do not check the mandatory properties 
	 * @return
	 */
	public boolean validateValues(Map<String, Map<String, String>> ref, boolean looseValidation) {
		boolean res = true;
		StringBuilder error = new StringBuilder("Missing following mandatory properties : ");
		int initialSize = error.length();
		
		/*
		 * Look for missing mandatory properties
		 */
		if (!looseValidation) {
			for (String prop : mandatoryProps) {
				boolean contains = false;
				for (String bank : ref.keySet()) {
					
					// Computed bank don't need some mandatory parameters
					if (dependenciesParameters.get(bank) != null && !dependenciesParameters.get(bank).isEmpty() && (
							prop.equals("server") ||
							prop.equals("remote.dir") ||
							prop.equals("remote.files") ||
							prop.equals("local.files"))) {
						contains = true;
						break;
					}
					// If protocol is directhttp dont check remote.files
					if (currentValues.containsKey(bank) && prop.equals("remote.files")) {
						for (String pr : currentValues.get(bank).keySet()) {
							if (pr.contains("protocol")) {
								ListBox lb = (ListBox) currentValues.get(bank).get(pr);
								if (lb.getItemText(lb.getSelectedIndex()).equals("directhttp")) {
									contains = true;
									break;
								}
							}
						}
						if (contains) {
							break;
						}
					}
					
					
					if (currentValues.containsKey(bank)) {
						if ((contains = bankContainsMandatoryProperty(currentValues.get(bank).keySet(), prop)) == true)
							break;
					} else {
						if ((contains = bankContainsMandatoryProperty(ref.get(bank).keySet(), prop)) == true)
							break;
					}
				}
				if (!contains)
					error.append(prop + ",");
			}
		}
		
		if (error.length() > initialSize) {
			res = false;
			SC.warn(error.deleteCharAt(error.lastIndexOf(",")).toString());
		} else {
			/*
			 * Look for empty mandatory properties or wrong typed properties
			 */
			StringBuilder sb = new StringBuilder("Empty mandatory fields in files : ");
			initialSize = sb.length();
			List<String> okProps = new ArrayList<String>();
			for (String bank : currentValues.keySet()) {
				boolean isEmpty = false;
				for (String s : currentValues.get(bank).keySet()) {
					
					String propName = s.split("__")[1];
					
					// These properties don't have to be set for computed bank
					if (dependenciesParameters.get(bank) != null && !dependenciesParameters.get(bank).isEmpty() && (
							propName.equals("server") ||
							propName.equals("remote.dir") ||
							propName.equals("remote.files") ||
							propName.equals("local.files"))) {
						
						continue;
					}
					// If protocol is directhttp dont check remote.files
					if (propName.equals("remote.files")) {
						
						boolean contains = false;
						for (String pr : currentValues.get(bank).keySet()) {
							if (pr.contains("protocol")) {
								ListBox lb = (ListBox) currentValues.get(bank).get(pr);
								if (lb.getItemText(lb.getSelectedIndex()).equals("directhttp")) {
									contains = true;
									break;
								}
							}
						}
						if (contains) {
							continue;
						}
						
					}
					
					FocusWidget w;
					if ((w = currentValues.get(bank).get(s)) instanceof TextBox) {
						TextBox tb = (TextBox) w;
						if (mandatoryProps.contains(propName) && !propName.equals("database.password")) {
							if (tb.getText().isEmpty()) {
								res = false;
								isEmpty = true;
								tb.addStyleName(css.bankEditorErrorWidget());
							} else {
								res = res && true;
								tb.removeStyleName(css.bankEditorErrorWidget());
							}
							okProps.add(propName);
						}
						
						String type = propertiesType.get(propName);
						if (type.equals("integer")) {
							if (!tb.getText().isEmpty()) {
								try {
									Integer.valueOf(tb.getText());
									tb.removeStyleName(css.bankEditorErrorWidget());
									res = res && true;
								} catch (NumberFormatException ex) {
									tb.addStyleName(css.bankEditorErrorWidget());
									res = false;
								}
							} else {
								tb.removeStyleName(css.bankEditorErrorWidget());
							} 
						}
						
					}
				}
				if (isEmpty)
					sb.append(bank + ",");
			}
			
			/*
			 * Look for properties that have not been loaded
			 */
			for (String bank : ref.keySet()) {
				boolean isEmpty = false;
				// Not loaded
				if (!currentValues.keySet().contains(bank)) {
					for (String s : ref.get(bank).keySet()) {
						String propName = s.split("__")[1];
						if (mandatoryProps.contains(propName) && !propName.equals("database.password") &&
								!okProps.contains(propName)) {
							if (ref.get(bank).get(s).trim().isEmpty()) {
								res = false;
								isEmpty = true;
								break;
							}
						}
					}
				}
				if (isEmpty)
					sb.append(bank + ",");
			}
			
			if (sb.length() > initialSize) {
				SC.warn(sb.deleteCharAt(sb.lastIndexOf(",")).toString());
			}
		}
		
		return res;
	}
	
	public Map<String, Map<String, String>> getNewBankProperties() {
		Map<String, Map<String, String>> modifiedValues = new HashMap<String, Map<String, String>>();
		for (String bank : currentValues.keySet()) {
			Map<String, String> tmp = new HashMap<String, String>();
			for (String key : currentValues.get(bank).keySet()) {
				String currentValue;
				FocusWidget widget = currentValues.get(bank).get(key);
				if (widget instanceof TextBox) {
					currentValue = ((TextBox) widget).getText();
				} else if (widget instanceof ListBox) {
					ListBox lb = (ListBox) widget;
					currentValue = lb.getItemText(lb.getSelectedIndex());
				} else if (widget instanceof CheckBox) {
					currentValue = ((CheckBox) widget).getValue() ? "true" : "false";
				} else
					return null;
				
				
				if ((mode.equals(NEW) && bank.equals(currentBank)) || !refValues.get(bank).containsKey(key) || !refValues.get(bank).get(key).equals(currentValue)) {
					tmp.put(key, currentValue);
				}
			}
			
			if (removedValues.get(bank) != null) {
				for (String prop : removedValues.get(bank)) {
					tmp.put(prop, "__deleted");
				}
			}
			
			modifiedValues.put(bank, tmp);
			
			// Add processes
			if (processes.get(bank) != null) {
				modifiedValues.get(bank).putAll(processes.get(bank));
			}
			
			// Add directhttp parameters
			if (directHttpParameters.get(bank) != null) {
				modifiedValues.get(bank).putAll(directHttpParameters.get(bank).getParameters());
			}
			
			// Add bank dependencies
			if (dependenciesParameters.get(bank) != null) {
				modifiedValues.get(bank).putAll(dependenciesParameters.get(bank).getParameters());
			}
		}
		
		return modifiedValues;
	}
	
	/**
	 * Removes misc__ properties from a given bank (processes, directhttp parameters...).
	 * 
	 * @param map bank properties
	 */
	public static void clearMiscProperties(Map<String, String> map) {
		List<String> toRemove = new ArrayList<String>();
		for (String key : map.keySet()) {
			if (key.startsWith("misc__") && !key.equals("misc__include.properties"))
				toRemove.add(key);
		}
		
		for (String key : toRemove)
			map.remove(key);
	}
	
	public String getMode() {
		return mode;
	}
	
	public static BankValidator getInstance() {
		return validator;
	}
	
	public void setDependenciesParameters(String bank, BankDependencyBuilder builder) {
		dependenciesParameters.put(bank, builder);
	}

	public void setDirectHttpParameters(String bank, DirectHttpParameterBuilder directHttpParameterBuilder) {
		directHttpParameters.put(bank, directHttpParameterBuilder);
	}
	
	public DirectHttpParameterBuilder getDirectHttpParameters(String bank) {
		return directHttpParameters.get(bank);
	}
}
