package de.hsadmin.cliClientConnector;

import java.lang.reflect.Method;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import de.hsadmin.cliClientConnector.CLIClientConnectorServlet.FunctionNotKnownException;
import de.hsadmin.cliClientConnector.CLIClientConnectorServlet.UnknownModuleException;
import de.hsadmin.core.model.AbstractEntity;
import de.hsadmin.core.model.ModuleInterface;

/**
 * Parses Arguments for the CLI Client Connector Servlet
 * 
 * @author Christof Donat
 *
 */
public class ArgumentParser {
	/// I am working for this Servlet instance
	private CLIClientConnectorServlet master;
	private DateFormat df = new SimpleDateFormat( "yyyy-MM-dd");
	
	public ArgumentParser(CLIClientConnectorServlet master) {
		this.master = master;
	}
	
	private String getUsageString() {
		return	"     [ (-W name=value|--globalWhere:name=value) ...]\n"+
				"     [ (-S name=value|--globalSet:name=value) ...]\n"+
				"     [ (-D displayspec|--globalDisplay=displayspec) ...]\n"+
				"     [ (-c module.function|--call=module.function)\n" +
				"         [ (-w name=value|--where:name=value) ...]\n"+
				"         [ (-s name=value|--set:name=value) ...]\n"+
				"         [ (-d displayspec|--display:displayspec) ...]\n"+
				"         [ oids ...] ] ]\n"+
				"\n"+
				"(" + CLIClientConnectorServlet.version + ")\n";
	}
	
	private ArrayList<Method> getMethodList(Object o) {
		Method[] meths = o.getClass().getMethods();
		ArrayList<Method> methodlist = new ArrayList<Method>();
		
		for( int i = 0; i < meths.length; i++ ) {
			Method m = meths[i];
			String n = m.getName();
			if( n.startsWith("get") && !n.equals("getNew") ) {
				String fn = m.getName().substring(3).toLowerCase();
				if( fn.equals("class") ) continue;
				if (m.getParameterTypes().length == 0) {
					methodlist.add(m);
				}
			}
		}
		
		return methodlist;
	}
	
	private Hashtable<String,String> getValues(Object o, ArrayList<Method> methodlist, ArrayList<String>fieldNames, boolean deep, ArrayList<Object> foundObjects) {
		Hashtable<String,String> row = new Hashtable<String,String>();
		int i, j;
		
		if( foundObjects == null ) foundObjects = new ArrayList<Object>();
		
		for( i = 0; i < methodlist.size(); i++ ) {	
			Method m = methodlist.get(i);
			try {
				String name = fieldNames.get(i);
				String type = m.getReturnType().getCanonicalName();
				String val = "";

				Object value = null;
				try {
					value = m.invoke(o);
				} catch( Exception e ) {
					e.printStackTrace();
				}
				
				if( value == null )
					val = "";
				else if( type.equals("java.lang.String") ) 
					val = (String)value;
				else if( type.equals("java.lang.Integer") )
					val = String.valueOf((Integer)value);
				else if( type.equals("java.lang.Long") )
					val = String.valueOf((Long)value);
				else if( type.equals("java.lang.Boolean") )
					val = String.valueOf((Boolean)value);
				else if( type.equals("java.util.Date") ) {					
					val = df.format((Date)value);
				} else if( type.equals("java.util.Set") ) {
					val = "<Set>";	
				} else try {
					AbstractEntity v = (AbstractEntity)value;
					val = v.createStringKey();
					if( deep && !foundObjects.contains(v) ) {
						foundObjects.add(v);
						ArrayList<String> fieldNamesDeep = new ArrayList<String>();
						ArrayList<Method> methodlistDeep = getMethodList(v);
						for( j = 0; j < methodlistDeep.size(); j++ ) {
							fieldNamesDeep.add(methodlistDeep.get(j).getName().substring(3).toLowerCase());
						}
						Hashtable<String,String> tmp = getValues(v,methodlistDeep,fieldNamesDeep,deep,foundObjects);
						Enumeration<String> keys = tmp.keys();
						while(keys.hasMoreElements()) {
							try {
								String k = (String)keys.nextElement();
								row.put(name+"."+k, tmp.get(k));
							} catch( Exception e ) {
								e.printStackTrace();
							}
						}
					}
				} catch(ClassCastException e) {
					val = value.toString();
				}
				if (val != null) row.put(name, val);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return row;
	}
	
	private String formatObjectsWithoutDisplay(int j, ArrayList<String> fieldNames, ArrayList<Integer>columnWidths, ArrayList<Hashtable<String,String> > rows) {
		int i;
		StringBuffer rval = new StringBuffer();
		
		for( i = 0; i < fieldNames.size(); i++ ) {
			StringBuffer name =  new StringBuffer(fieldNames.get(i));
			int fieldwidth = columnWidths.get(i);
			while( name.length() < fieldwidth ) 
				name.append(" ");
			rval.append(name.toString()+((i < fieldNames.size()-1)?" | ":""));
		}
		rval.append("\n");
		for( i = 0; i < j; i++ ) 
			rval.append("-");
		rval.append("\n");
		
		for( j = 0; j < rows.size(); j++ ) {
			for( i = 0; i < fieldNames.size(); i++ ) {
				StringBuffer value =  new StringBuffer(rows.get(j).get(fieldNames.get(i)));
				int fieldwidth = columnWidths.get(i);
				while( value.length() < fieldwidth ) 
					value.append(" ");
				rval.append(value.toString()+((i < fieldNames.size()-1)?" | ":""));
			}
			rval.append("\n");
		}
		return rval.toString();
	}
	
	private String formatObjectsWithDisplay(ArrayList<Hashtable<String,String> > rows, String displayDef) {
		StringBuffer rval = new StringBuffer();
		for( int j = 0; j < rows.size(); j++) {
			String rv = displayDef;
			Enumeration<String> fNames = rows.get(j).keys();
			while( fNames.hasMoreElements() ) {
				String f = (String)fNames.nextElement();
				rv = rv.replaceAll("\\$\\{"+f+"\\}", rows.get(j).get(f));
			}
			rv = rv.replaceAll("\\\\n", "\n");
			rv = rv.replaceAll("\\\\t", "\t");
			rv = rv.replaceAll("\\\\(.)?", "$1");
			rval.append(rv);
		}
		return rval.toString();
	}
	
	/**
	 * format Objects for the output as a Human readable table
	 * 
	 * @param objects
	 *  format these objects
	 *  
	 * @return humanReadableString
	 *  a string with the human readable representation of the objects
	 */
	public String formatObjects(List<?> objects, String displayDef) {
		if( objects.size() == 0 ) return "";
		if( objects.get(0) == null ) return "";
		ArrayList<Method> methodlist = getMethodList(objects.get(0));
		ArrayList<Integer>columnWidths = new ArrayList<Integer>();
		ArrayList<String>fieldNames = new ArrayList<String>();
		ArrayList<Hashtable<String,String> > rows = 
			new ArrayList<Hashtable<String,String> >();
		int i, j;
		
		for( i = 0; i < methodlist.size(); i++ ) {
			Method m = methodlist.get(i);
			String n = m.getName();
			String fn = n.substring(3).toLowerCase();
			fieldNames.add(fn);
			columnWidths.add(n.length()+3);
		}
		
		for( j = 0; j < objects.size(); j++ ) {
			Object o = objects.get(j);
			Hashtable<String,String> row = getValues(o,methodlist,fieldNames,(displayDef != null), null);
			for( i = 0; i < fieldNames.size(); i++ ) {
				String val = row.get(fieldNames.get(i));
				if( val != null && i < columnWidths.size() && val.length()+3 > columnWidths.get(i) )
					columnWidths.set(i, val.length()+3);
			}
			rows.add(row);
		}		

		if( displayDef == null ) {
			j = 0;
			for( i = 0; i < columnWidths.size(); i++ ) {
				j += columnWidths.get(i)+2;
			}
			j -= 2;

			return formatObjectsWithoutDisplay(j, fieldNames, columnWidths, rows);
		} else
			return formatObjectsWithDisplay(rows, displayDef);
	}
	
	/**
	 * The main parser function. Parses the Parameters, uses the master to call 
	 * the functions and returns a formatet output.
	 * 
	 * @param arguments
	 * @return humanReadableObjectsString
	 */
	public String parse(List<String> arguments, ModuleInterface module) {
		String rval = "";
		
		String currentModule = null;
		String currentFunction = null;
		Map<String,String> currentWhere = new Hashtable<String,String>();
		Map<String,String> currentSet = new Hashtable<String,String>();
		ArrayList<String> currentOIDs = new ArrayList<String>();

		Map<String,String> globalWhere = new Hashtable<String,String>();
		Map<String,String> globalSet = new Hashtable<String,String>();
		String display = null;
		String globalDisplay = null;
		
		for( int i = 0; i < arguments.size(); i++ ) {
			String arg = arguments.get(i);
			
			if( arg.equals("-c") || arg.startsWith("--call:") ) {
				// call
				if( currentModule != null ) {
					try {
						// execute the last call now
						rval += formatObjects(master.callModule(
								currentModule, 
								currentFunction, 
								currentWhere, 
								currentSet, 
								currentOIDs,
								module),(display==null)?globalDisplay:display);
					} catch (FunctionNotKnownException e) {
						rval += "Function unknown: "+currentModule+'.'+currentFunction+"\n";
					} catch (UnknownModuleException e) {
						rval += "Module unknown: "+currentModule+"\n";
					}
				}
				// reset parameters for next call
				currentWhere = new HashMap<String, String>();
				currentWhere.putAll(globalWhere);
				currentSet = new HashMap<String, String>();
				currentSet.putAll(globalSet);
				currentOIDs  = new ArrayList<String>();
				display      = null;
				
				// set the new call
				boolean isShortParam = arg.equals("-c");
				String calldef = isShortParam?arguments.get(i+1):arg.substring(7);

				if( calldef != null ) {
					String[] split = calldef.split("[.]", 2);
					currentModule   = split[0];
					currentFunction = split[1];
				}
				if( isShortParam ) i++;
			} else if( arg.equals("-w") || arg.startsWith("--where:") ) {
				// where
				boolean isShortParam = arg.equals("-w");
				String wheredef = isShortParam?arguments.get(i+1):arg.substring(8);
				if( wheredef != null ) {
					String[] split = wheredef.split("[=]", 2);
					currentWhere.put(split[0],split[1]);
				}
				if( isShortParam ) i++;
			} else if( arg.equals("-W") || arg.startsWith("--globalWhere:") ) {
				// global where
				boolean isShortParam = arg.equals("-W");
				String gwheredef = isShortParam?arguments.get(i+1):arg.substring(14);
				if( gwheredef != null ) {
					String[] split = gwheredef.split("[=]", 2);
					globalWhere.put(split[0],split[1]);
				}
				if( isShortParam ) i++;
			} else if( arg.equals("-s") || arg.startsWith("--set:") ) {
				// set
				boolean isShortParam = arg.equals("-s");
				String setdef = isShortParam?arguments.get(i+1):arg.substring(6);
				if( setdef != null ) {
					String[] split = setdef.split("[=]", 2);
					currentSet.put(split[0],split[1]);
				}
				if( isShortParam ) i++;
			} else if( arg.equals("-S") || arg.startsWith("--globalSet:") ) {
				// global set
				boolean isShortParam = arg.equals("-S");
				String gsetdef = isShortParam?arguments.get(i+1):arg.substring(12);
				if( gsetdef != null ) {
					String[] split = gsetdef.split("[=]", 2);
					globalSet.put(split[0],split[1]);
				}
				if( isShortParam ) i++;
			} else if( arg.equals("-d") || arg.startsWith("--display:") ) {
				// display
				boolean isShortParam = arg.equals("-d");
				display = isShortParam?arguments.get(i+1):arg.substring(10);
				if( isShortParam ) i++;
			} else if( arg.equals("-D") || arg.startsWith("--globalDisplay:") ) {
				// global display
				boolean isShortParam = arg.equals("-D");
				globalDisplay = isShortParam?arguments.get(i+1):arg.substring(16);
				if( isShortParam ) i++;
			} else if( arg.equals("-h") || arg.equals("--help") ) {
				return getUsageString();
			} else if( arg.startsWith("-") ) {
				return "unknown option '"+arg+"'\n"+getUsageString();
			} else currentOIDs.add(arg);
		}
		if( currentModule != null ) {
			try {
				rval += formatObjects(master.callModule(
						currentModule, 
						currentFunction, 
						currentWhere, 
						currentSet, 
						currentOIDs,
						module),(display==null)?globalDisplay:display);
			} catch (FunctionNotKnownException e) {
				rval += "Function unknown: "+currentModule+'.'+currentFunction+"\n";
			} catch (UnknownModuleException e) {
				rval += "Module unknown: "+currentModule+"\n";
			}
		}
		return rval;
	}
}
