/*******************************************************************************
 * Copyright (c) 2010 Attila Kovacs <attila_kovacs[AT]post.harvard.edu>.
 * All rights reserved. 
 * 
 * This file is part of crush.
 * 
 *     crush is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * 
 *     crush is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 * 
 *     You should have received a copy of the GNU General Public License
 *     along with crush.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Contributors:
 *     Attila Kovacs <attila_kovacs[AT]post.harvard.edu> - initial API and implementation
 ******************************************************************************/
// Copyright (c) 2009 Attila Kovacs 

package util;

import nom.tam.fits.*;
import nom.tam.util.*;

import java.io.*;
import java.util.*;



public class Configurator implements Cloneable {
	public String value;
	public boolean isEnabled = false;
	public int index;
	
	public Hashtable<String, Configurator> branches = new Hashtable<String, Configurator>();
	public Hashtable<String, Configurator> removed = new Hashtable<String, Configurator>();
	public Hashtable<String, Vector<String>> conditionals = new Hashtable<String, Vector<String>>();
	public Vector<String> blacklist = new Vector<String>();
	public Vector<Class<?>> users = new Vector<Class<?>>(); // Not yet used...
	
	public boolean wasUsed = false;
	
	private static int counter = 0;
	
	public static boolean verbose = false;
	
	public Configurator() {}
	
	@Override
	public Object clone() {
		try { return super.clone(); }
		catch(CloneNotSupportedException e) { return null; }
	}
	
	@SuppressWarnings("unchecked")
	public Configurator copy() {
		Configurator copy = (Configurator) clone();
		copy.branches = new Hashtable<String, Configurator>();
		copy.removed = new Hashtable<String, Configurator>();
		copy.blacklist = (Vector<String>) blacklist.clone();
		copy.conditionals = new Hashtable<String, Vector<String>>();
		for(String key : branches.keySet()) copy.branches.put(key, branches.get(key).copy());
		for(String key : removed.keySet()) copy.removed.put(key, removed.get(key).copy());
		for(String key : conditionals.keySet()) copy.conditionals.put(key, (Vector<String>) conditionals.get(key).clone());
		return copy;
	}
	
	@Override
	public boolean equals(Object o) {
		if(value == null) return o == null;
		if(o instanceof String) return ((String) o).equalsIgnoreCase(value);
		else return super.equals(o);
	}
	
	@Override
	public int hashCode() {
		return value.hashCode();
	}
	
	public void parse(Vector<String> lines) {
		for(String line : lines) parse(line);		
	}
	
	public void parse(String line) {
		Entry entry = getEntry(line);
		process(entry.key, entry.value);
	}
	
	protected Entry getEntry(String line) {
		StringTokenizer tokens = new StringTokenizer(line," \t=:");
		Entry entry = new Entry();
		entry.key = tokens.nextToken().toLowerCase();
		entry.value = tokens.hasMoreTokens() ? line.substring(line.indexOf(tokens.nextToken(), entry.key.length())).trim() : "";
		return entry;
	}
	
	
	protected String unalias(String key) {
		String branchName = getBranchName(key);
		String unaliased = branchName;
		
		// Check if the requested key branch is aliased. If so, process as such...
		if(isConfigured("alias." + branchName)) {
			unaliased = get("alias." + branchName).value;
			if(verbose) System.err.println("<a> '" + branchName + "' -> '" + unaliased + "'");
		}
		
		if(key.length() != branchName.length()) unaliased += "." + getRemainder(key, branchName);
		
		return unaliased;
	}
	
	protected String unaliasedKey(String key) {
		return new StringTokenizer(unalias(key.toLowerCase()), " \t=:").nextToken();
	}
	
	
	public void process(String key, String argument) {	
		String substitute = unalias(key);
		if(!key.equals(substitute)) {
			key = new StringTokenizer(substitute.toLowerCase(), " \t=:").nextToken();
			if(substitute.length() > key.length()) argument = substitute.substring(key.length()+1) + argument;
		}
		
		if(key.equals("forget")) for(String name : getList(argument)) forget(unaliasedKey(name));
		else if(key.equals("recall")) for(String name : getList(argument)) recall(unaliasedKey(name));
		else if(key.equals("enable")) for(String name : getList(argument)) forget(unaliasedKey(name));
		else if(key.equals("disable")) for(String name : getList(argument)) recall(unaliasedKey(name));
		else if(key.equals("blacklist")) for(String name : getList(argument)) blacklist(unaliasedKey(name));
		else if(key.equals("whitelist")) for(String name : getList(argument)) whitelist(unaliasedKey(name));
		else if(key.equals("remove")) for(String name : getList(argument)) remove(unaliasedKey(name));
		else if(key.equals("replace")) for(String name : getList(argument)) replace(unaliasedKey(name));
		else if(key.equals("config")) {
			try { readConfig(argument); }
			catch(IOException e) { System.err.println("WARNING! Configuration file '" + argument + "' no found."); }
		}
		else if(key.equals("poll")) {
			if(argument.length() == 0) print(System.out);
			else poll(argument);
		}
		else {
			String branchName = getBranchName(key);
			if(verbose) System.err.println("<.> " + branchName);

			if(branchName.equals("*")) {
				for(String name : new Vector<String>(branches.keySet())) process(name + key.substring(1), argument);				
			}	
			else if(branchName.startsWith("[")) {
				String condition = branchName.substring(1, branchName.indexOf(']'));
				String setting = key.substring(condition.length() + 2).trim() + " " + argument;
				addCondition(condition, setting);
			}
			else if(!blacklist.contains(key)) set(branchName, key, argument);
		}
	}

	private void set(String branchName, String key, String argument) {
		setCondition(key);
		setCondition(key + "?" + argument);
		Configurator branch = branches.containsKey(branchName) ? get(branchName) : new Configurator();
		if(key.equals(branchName)) {
			if(verbose) System.err.println("<=> " + argument);
			branch.value = argument;
			branch.isEnabled = true;
			branch.index = counter++; // Update the serial index for the given key...
		}
		else branch.process(getRemainder(key, branchName), argument);
		branches.put(branchName, branch);		
	}
	
	private void addCondition(String condition, String setting) {	
		if(isSatisfied(condition)) parse(setting);
		else {
			Vector<String> list = conditionals.containsKey(condition) ? conditionals.get(condition) : new Vector<String>();
			list.add(setting);
			
			// Replace assignments and first spaces with ?
			StringBuffer canonized = new StringBuffer(condition.length());
			boolean firstSpace = true;
			boolean illegal = false;
			for(int i=0; i<condition.length(); i++) {
				char c = condition.charAt(i);
				if(firstSpace) if(c == ' ' || c == '\t') {
					if(illegal) canonized.append('?');
					illegal = true;
					firstSpace = false;
				}
				if(c == '=') {
					if(illegal) canonized.append('?');
					illegal = true;
				}
				canonized.append(c);
				illegal = false;
			}
	
			conditionals.put(new String(canonized), list);
		}
	}
	
	public boolean isSatisfied(String condition) {
		// If the conditional key is already defined, then simply parse the argument of the condition
		if(condition.contains("?")) {
			StringTokenizer pair = new StringTokenizer(condition, "?");
			String conditionKey = pair.nextToken().toLowerCase();
			if(isConfigured(conditionKey)) if(get(conditionKey).equals(pair.nextToken())) return true;
		}
		else if(isConfigured(condition.toLowerCase())) return true;
		
		return false;
	}
	
	private Vector<String> getList(String argument) {
		Vector<String> list = new Vector<String>();
		StringTokenizer tokens = new StringTokenizer(argument, " \t,");
		while(tokens.hasMoreTokens()) list.add(tokens.nextToken());
		return list;
	}
	
	public void setCondition(String key) {
		key.toLowerCase();
			
		if(!conditionals.containsKey(key)) return;
		else {
			if(verbose) System.err.println("[c] " + key + " > " + conditionals.get(key));
			parse(conditionals.get(key));		
		}
	}
	
	public void recall(String key) {
		if(!blacklist.contains(key)) if(containsKey(key)) get(key).isEnabled = true;
	}
	
	public void forget(String key) {
		if(containsKey(key)) get(key).isEnabled = false;
	}
	
	public void remove(String branch) {
		String branchName = getBranchName(branch);
		
		if(branch.equals("blacklist")) {
			blacklist.clear();
			for(String name : branches.keySet()) get(name).remove(branch);
		}
		else if(branchName.equals("*")) {
			for(String name : new Vector<String>(branches.keySet())) remove(name + getRemainder(branch, "*"));;
		}
		else if(branchName.startsWith("[") && branchName.endsWith("]")) {
			branchName = branchName.substring(1, branchName.length()-1).trim();
			for(String condition : new Vector<String>(conditionals.keySet())) if(condition.startsWith(branchName)) conditionals.remove(condition);
		}
		else if(!branch.equals(branchName)) {
			if(branches.containsKey(branchName)) get(branchName).remove(branch.substring(branchName.length()+1));
		}
		else if(branches.containsKey(branch)) {
			if(verbose) System.err.println("<f> " + branch); 
			removed.put(branch, branches.remove(branch));
		}
	}
	
	public void replace(String branch) {
		String branchName = getBranchName(branch);
		
		if(blacklist.contains(branch)) return;
		
		if(branchName.equals("*")) {
			for(String name : new Vector<String>(branches.keySet())) replace(name + getRemainder(branch, "*"));
		}
		else if(!branch.equals(branchName)) {
			if(branches.containsKey(branchName)) get(branchName).replace(branch.substring(branchName.length()+1));
		}
		else if(removed.containsKey(branch)) {
			if(verbose) System.err.println("<r> " + branch);
			branches.put(branch, removed.remove(branch));
			if(conditionals.containsKey(branch)) {
				if(verbose) System.err.println("[c] " + branch);
				for(String line : conditionals.get(branch)) parse(line);				
			}
		}
	}
	
	
	// TODO how to deal with wildcards...
	// TODO how does it react to branches...?
	public void blacklist(String key) {		
		String branchName = getBranchName(key);

		if(branchName.equals("*")) {
			for(String name : new Vector<String>(branches.keySet())) blacklist(name + getRemainder(key, "*"));
		}	
		else if(!key.equals(branchName)) {
			if(branches.containsKey(branchName)) get(branchName).blacklist(key.substring(branchName.length()+1));
			else {
				if(verbose) System.err.println("<b> " + key);
				blacklist.add(key);
			}
		}
		else {
			forget(key);
			if(verbose) System.err.println("<b> " + key);
			blacklist.add(key);
		}	
	}

	// TODO how to deal with wildcards...
	public void whitelist(String key) {
		String branchName = getBranchName(key);
		
		if(blacklist.contains(key)) {
			if(verbose) System.err.println("<w> " + key);
			blacklist.remove(key);
			//enable(key);
		}
		if(branchName.equals("*")) {
			for(String name : new Vector<String>(branches.keySet())) whitelist(name + getRemainder(key, "*"));
		}
		else if(!key.equals(branchName)) {
			if(branches.containsKey(branchName)) {
				if(verbose) System.err.println("<w> " + branchName);
				get(branchName).whitelist(key.substring(branchName.length()+1));
			}
		}
	}
	
	// Looks for first period outside of square brackets (used for conditions)...
	public String getBranchName(String key) {
		int i=0;
		int open = 0;
		for(; i<key.length(); i++) {
			char c = key.charAt(i);
			if(c == '[') open++;
			else if(c == ']') open--;
			else if(open == 0 && c == '.') break;
		}
		
		if(i < key.length()) return key.substring(0, i);
		else return key;
	}
	
	public String getRemainder(String key, String branchName) {
		if(key.length() <= branchName.length()+1) return "";
		else return key.substring(branchName.length()+1);	
	}
	
	public Configurator get(String key) {
		String branchName = getBranchName(key);
		if(branchName.equals(key)) return branches.get(key);
		else if(branches.containsKey(branchName)) return branches.get(branchName).get(key.substring(branchName.length() + 1));
		else return null;
	}
	
	public boolean containsKey(String key) {
		String branchName = getBranchName(key);
		if(!branches.containsKey(branchName)) return false;
		if(branchName.equals(key)) return (branches.containsKey(key));
		return branches.get(branchName).containsKey(key.substring(branchName.length() + 1));
	}
	
	public boolean isConfigured(String key) {
		if(!containsKey(key)) return false;
		Configurator option = get(key);
		if(!option.isEnabled) return false;
		option.wasUsed = true;
		return option.value != null;
	}
	
	public void mapValueTo(String branchName) {
		if(value != null) if(value.length() > 0) {
			if(containsKey(branchName)) get(branchName).value = value;
			else process(branchName, value);
		}
		value = "";
	}
	
	public boolean hasUser(Class<?> c) {
		return users.contains(c);		
	}
	
	public boolean hasUserInstanceOf(Class<?> c) {
		for(Class<?> user : users) if(c.isAssignableFrom(user)) return true;
		return false;
	}
	
	public void addUser(Class<?> c) {
		if(users.contains(c)) return;
		users.add(c);
	}
	
	public void addUser(Object o) {
		addUser(o.getClass());
	}
	
	public void intersect(Configurator options) {
		for(String key : getKeys()) {
			if(!options.containsKey(key)) remove(key);
			else {
				Configurator option = get(key);
				Configurator other = options.get(key);
				if(option.isEnabled && !other.isEnabled) option.isEnabled = false;
				else if(!option.value.equals(other.value)) option.isEnabled = false;
			}
		}
	}
	
	// TODO Difference conditionals and blacklists too...
	public Configurator difference(Configurator options) {
		Configurator difference = new Configurator();

		for(String key : getKeys()) {
			if(!options.containsKey(key)) difference.parse(key + " " + get(key).value);
			else {
				Configurator option = get(key);
				Configurator other = options.get(key);
			
				if(option.isEnabled && !other.isEnabled) difference.parse(key + " " + get(key).value);
				else if(!option.value.equals(other.value)) difference.parse(key + " " + get(key).value);
			}
		}
		return difference;
	}
	
	public void setIteration(int i, int rounds) {	
		if(!containsKey("iteration")) return;
		Hashtable<String, Vector<String>> settings = get("iteration").conditionals;

		// Parse explicit iteration settings
		if(settings.containsKey(i + "")) parse(settings.get(i + ""));		

		// Parse relative iteration settings
		for(String spec : settings.keySet()) if(spec.endsWith("%")) {
			int k = (int) Math.round(rounds * 0.01 * Double.parseDouble(spec.substring(0, spec.length()-1)));
			if(i == k) parse(settings.get(spec));
		}

		// Parse end-based settings
		String spec = "last" + (i==rounds ? "" : "-" + (rounds-i));
		if(settings.containsKey(spec)) parse(settings.get(spec));
	}
	
	
	public String getValue() {
		return value; 
		
	}
	
	public double getDouble() {
		return Double.parseDouble(getValue());
	}
	
	public int getInt() {
		return Integer.decode(getValue());
	}
	
	public boolean getBoolean() {
		return Util.parseBoolean(getValue());
	}
	
	public Range getRange() {
		return Range.parse(getValue());		
	}
	
	public Range getRange(boolean nonNegative) {
		return Range.parse(getValue(), nonNegative);		
	}
	
	public Vector2D getVector2D() {
		return new Vector2D(getValue());		
	}
	
	public Vector<String> getList() {
		Vector<String> list = new Vector<String>();
		StringTokenizer tokens = new StringTokenizer(getValue(), " \t,");
		while(tokens.hasMoreTokens()) list.add(tokens.nextToken());
		return list;
	}
	
	public Vector<String> getLowerCaseList() {
		Vector<String> list = new Vector<String>();
		StringTokenizer tokens = new StringTokenizer(getValue(), " \t,");
		while(tokens.hasMoreTokens()) list.add(tokens.nextToken().toLowerCase());
		return list;		
	}
	
	public Vector<Double> getDoubles() {
		Vector<String> list = getList();
		Vector<Double> doubles = new Vector<Double>(list.size());	
		for(String entry : list) {
			try { doubles.add(Double.parseDouble(entry)); }
			catch(NumberFormatException e) { doubles.add(Double.NaN); }
		}
		return doubles;
	}
	
	// Also takes ranges...
	public Vector<Integer> getIntegers() {
		Vector<String> list = getList();
		Vector<Integer> ints = new Vector<Integer>(list.size());	
		for(String entry : list) {
			try { ints.add(Integer.decode(entry)); }
			catch(NumberFormatException e) {
				Range range = Range.parse(entry, true);
				if(Double.isInfinite(range.min) || Double.isInfinite(range.max)) throw e;
				int from = (int)Math.ceil(range.min);
				int to = (int)Math.floor(range.max);
				for(int i=from; i<=to; i++) ints.add(i);	
			}
		}
		return ints;
	}
	
	public Vector<String> getKeys() {
		Vector<String> keys = new Vector<String>();
		for(String branchName : branches.keySet()) {
			Vector<String> branchDict = branches.get(branchName).getKeys();		
			if(isConfigured(branchName)) keys.add(branchName);
			for(String key : branchDict) keys.add(branchName + "." + key);			
		}	
		return keys;
	}
	
	public Hashtable<String, Vector<String>> getConditions() {
		Hashtable<String, Vector<String>> conditions = new Hashtable<String, Vector<String>>();
		for(String key : conditionals.keySet()) conditions.put(key, conditionals.get(key));
		
		for(String branchName : branches.keySet()) {
			Hashtable<String, Vector<String>> branchConditions = branches.get(branchName).getConditions();	
			if(!branchConditions.isEmpty())
			for(String key : branchConditions.keySet()) conditions.put(branchName + "." + key, branchConditions.get(key));			
		}	
		return conditions;
	}
	
	public Vector<String> getTimeOrderedKeys() {		
		Vector<String> keys = getKeys();	
		Collections.sort(keys,
			new Comparator<String>() {
				public int compare(String key1, String key2) {
					int i1 = get(key1).index;
					int i2 = get(key2).index;
					if(i1 == i2) return 0;
					return i1 > i2 ? 1 : -1;
				}
		});
		return keys;
	}
	
	public Vector<String> getAlphabeticalKeys() {
		Vector<String> keys = getKeys();
		Collections.sort(keys);
		return keys;
	}
	
	public void print(PrintStream out) {
		out.println("# Current configuration is: ");
		out.println("# --------------------------------------------------------------------");
		for(String key : getAlphabeticalKeys()) if(isConfigured(key))	{
			out.print("  " + key);
			String value = get(key).getValue();
			if(value.length() > 0) out.print(" = " + value);
			out.println();
		}
		out.println("# --------------------------------------------------------------------");
	}
	
	public void poll(String pattern) {
		pattern = pattern.toLowerCase();
		while(pattern.endsWith("*")) pattern = pattern.substring(0, pattern.length()-1);
		
		System.out.println("# Currently set keys starting with '" + pattern + "': ");
		System.out.println("# --------------------------------------------------------------------");
		for(String key : getAlphabeticalKeys()) if(isConfigured(key)) if(key.startsWith(pattern)) {
			System.out.print("  " + key);
			String value = get(key).getValue();
			if(value.length() > 0) System.out.print(" = " + value);
			System.out.println();
		}
		System.out.println("# --------------------------------------------------------------------");
	}
	
	public void readConfig(String fileName) throws IOException {
		File configFile = new File(fileName);
		if(configFile.exists()) {
			System.err.println("Reading configuration from " + fileName);
			
			BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(configFile)));
			String line = null;
			while((line = in.readLine()) != null) if(line.length() > 0) if(line.charAt(0) != '#') parse(line);
		}
		else throw new FileNotFoundException(fileName);
	}

	public void editHeader(Cursor cursor) throws FitsException, HeaderCardException {
		// Add all active configuration keys...
		for(String key : getAlphabeticalKeys()) {
			Configurator option = get(key);
			if(option.isEnabled) Util.addLongHierarchKey(cursor, key, option.value);
		}
		
		// Add all the conditionals...
		Hashtable<String, Vector<String>> conditions = getConditions();
		Vector<String> conditionKeys = new Vector<String>(conditions.keySet());	
		Collections.sort(conditionKeys);
		
		for(String key : conditionKeys) {
			StringBuilder values = new StringBuilder();
			for(String value : conditions.get(key)) {
				if(values.length() > 0) values.append(';');
				values.append(value);
			}
			Util.addLongHierarchKey(cursor, "[" + key + "]", new String(values));
		}
		
		// Write the blacklist....
		StringBuilder keys = new StringBuilder();
		for(String key : blacklist) {
			if(keys.length() > 0) keys.append(',');
			keys.append(key);
		}
		
		if(!blacklist.isEmpty()) Util.addLongHierarchKey(cursor, "blacklist", new String(keys));
		
	}
	
 }


class Entry {
	String key;
	String value;
}
