import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.NoSuchElementException;
import java.util.Scanner;
import java.util.LinkedList;
import java.util.StringTokenizer;
import java.util.Calendar;

/**
 * 
 * @author Dominik S. Kaaser
 * @version 1.3.0
 * @since 1.0.0
 */

public class CLI extends Thread {

	@Retention(RetentionPolicy.RUNTIME)
	public @interface Invokable {
		String helptext();
	}

	private static String prodVersion = "1.3.0";

	private Scanner s;

	private boolean run = true;

	private LinkedHashMap<Class<?>, Integer> refcount = new LinkedHashMap<Class<?>, Integer>();
	private LinkedHashMap<String, Object> instances = new LinkedHashMap<String, Object>();

	public static void main(String[] args) throws Exception {

		Calendar cal1 = Calendar.getInstance();
		cal1.set(2011, 0, 1, 0, 0, 0);
		Calendar cal2 = Calendar.getInstance();
		if (cal2.after(cal1))
			System.out.println("Achtung: Eine neuere Version ist verfügbar.\nBitte laden Sie sich diese von folgender Adresse herunter:\nhttp://download.kaaser.at/CLI\n");

		System.out.println("Testprogramm für Java-Klassen, Version " + prodVersion + "\n(C) Copyright 2010 Dominik S. Kaaser, dominik@kaaser.at\n");

		System.setErr(System.out);

		CLI cli = new CLI();

		cli.invoke(cli, "help");

		if (args.length > 0) {
			Thread.sleep(100);
			for (int i = 0; i < args.length; i++) {
				System.out.println("load " + args[i]);
				cli.invoke(cli, "load " + args[i]);
				System.out.print("> ");
			}
		}

		cli.start();

	}

	public CLI() {
		super();
	}

	public void run() {
		s = new Scanner(System.in);
		while (run) {
			try {
				System.out.print("> ");
				String line = "";
				try {
					line = s.nextLine();
				} catch (NoSuchElementException e) {
					break;
				}
				if (line.length() == 0)
					continue;

				String[] split = line.split("\\.", 2);

				if (instances.containsKey(split[0]) && split.length == 2)
					invoke(instances.get(split[0]), split[1]);
				else
					invoke(this, line);

			} catch (NoSuchMethodException e) {
				System.out.println("# Fehler:");
				System.out.println("  - " + e.getMessage());

			} catch (Exception e) {
				System.out.println("# Exception: " + e.getClass().getName());
				filterStackTrace(e);
				e.printStackTrace();
			}
		}
	}

	private void filterStackTrace(Exception e) {
		LinkedList<StackTraceElement> result = new LinkedList<StackTraceElement>();
		StackTraceElement[] stackTrace = e.getStackTrace();
		for (StackTraceElement element : stackTrace) {
			if (element.isNativeMethod())
				break;
			result.add(element);
		}
		StackTraceElement[] elements = new StackTraceElement[result.size() + 1];
		int i = 0;
		Iterator<StackTraceElement> it = result.iterator();
		while (it.hasNext())
			elements[i++] = it.next();
		elements[i] = stackTrace[stackTrace.length - 1];
		e.setStackTrace(elements);
	}

	private void invoke(Object object, String command) throws NoSuchMethodException, Exception {
		boolean success = false;
		String[] split = commandlineSplit(command);
		String name = split[0];

		for (Method method : object.getClass().getDeclaredMethods()) {
			if (method.getModifiers() != Modifier.PUBLIC)
				continue;
			if (!method.getName().equals(name))
				continue;
			Class<?>[] parameters = method.getParameterTypes();

			boolean array = false;
			for (Class<?> parameter : parameters)
				if (parameter.isArray()) {
					if (array)
						throw new Exception(new NoSuchMethodException("Die Methode " + name + " kann nicht ausgeführt werden:\n    Die Parameter können nicht vernünftig auf mehrere Arrays aufgeteilt werden."));
					array = true;
				}

			if (parameters.length != split.length - 1 && !array)
				continue;

			int arraylength = split.length - parameters.length;
			
			if(arraylength <= 0 && array)
				continue;

			Object[] cast = new Object[parameters.length];
			try {
				int j = 0;
				for (int i = 0; i < cast.length; i++) {

					if (parameters[i].isArray()) {
						cast[i] = Array.newInstance(parameters[i].getComponentType(), arraylength);
						for (int k = 0; k < arraylength; k++)
							((Object[]) cast[i])[k] = cast(split[++j], parameters[i].getComponentType());

					} else
						cast[i] = cast(split[++j], parameters[i]);
				}

				Object result = method.invoke(object, cast);
				success = true;
				if (result != null)
					System.out.println("+ " + result.getClass().getSimpleName() + ": " + result);

			} catch (Exception e) {
				if (e.getCause() != null)
					throw (Exception) e.getCause();
			}
		}
		if (!success)
			throw new NoSuchMethodException("Unbekannter Befehl: " + name);
	}

	private synchronized int incrementRefcount(Class<?> type) {
		int currentRefcount = 0;
		if (refcount.containsKey(type))
			currentRefcount = refcount.get(type).intValue();
		refcount.put(type, new Integer(++currentRefcount));
		return currentRefcount;
	}

	@Invokable(helptext = "Zeigt diesen Hilfstext an")
	public void help() {
		System.out.println("+ Hilfe\n" + "Das Testprogramm für Java-Klassen ermöglicht  es Ihnen, Objekte zu erzeugen und\n" + "Methoden  direkt  in der  Konsole  auszuführen. Verwenden  Sie die  allgemeinen\n" + "Befehle, um Klassen zu laden und Objekte zu erzeugen, z.B. mit\n" + "> load Konto\n" + "oder im Falle eines nicht-trivialen Konstruktors mit\n" + "> load Interval 1.0 3.14159265\n" + "sowie die Punkt-Notation, um Methoden eines Objekts auszuführen, z.B.\n" + "> konto1.gutschreiben 3\n" + "Die Parameter müssen hierbei wie  auf der Kommandozeile, also durch Leerzeichen\n" + "getrennt, angegeben werden.\n\n" + "+ Allgemeine Befehle:");

		for (Method method : getClass().getDeclaredMethods()) {
			Invokable annotation = method.getAnnotation(Invokable.class);
			if (method.getModifiers() == Modifier.PUBLIC && annotation != null) {

				StringBuilder sb = new StringBuilder();
				sb.append("  - ");
				sb.append(method.getName());
				for (Class<?> param : method.getParameterTypes()) {
					sb.append(' ');
					if (param.isArray())
						sb.append("(...)");
					else
						sb.append(param.getSimpleName());
				}
				int insert = sb.length();
				sb.append(" (" + annotation.helptext() + ')');
				while (sb.length() < 80)
					sb.insert(insert, ' ');

				System.out.println(sb.toString());
			}
		}
	}

	@Invokable(helptext = "Listet alle Methoden des Objekts auf")
	public void help(Object instance) {
		System.out.println("+ Befehle Klasse " + instance.getClass().getSimpleName() + ":");

		Method[] methods = instance.getClass().getDeclaredMethods();
		Constructor<?>[] constructors = instance.getClass().getDeclaredConstructors();

		for (Method method : methods)
			if (method.getModifiers() == Modifier.PUBLIC) {
				System.out.print("  - " + method.getName());
				Class<?>[] params = method.getParameterTypes();
				for (Class<?> param : params)
					System.out.print(" " + param.getSimpleName());
				System.out.println();
			}
		for (Constructor<?> constructor : constructors)
			if (constructor.getModifiers() == Modifier.PUBLIC) {
				Class<?>[] params = constructor.getParameterTypes();
				if (params.length > 0) {
					System.out.print("  - " + instance.getClass().getName());
					for (Class<?> param : params)
						System.out.print(" " + param.getName());
					System.out.println();
				}
			}
	}

	@Invokable(helptext = "Lädt eine Klasse und erzeugt ein neues Objekt")
	public void load(String classname) {

		try {
			Class<?> classobj = Class.forName(classname);
			System.out.println("+ Klasse " + classobj.getName() + " geladen.");

			String varname = classobj.getName().toLowerCase() + incrementRefcount(classobj);
			instances.put(varname, classobj.newInstance());
			System.out.println("  - Neues " + classname + "-Objekt (" + varname + ") erzeugt.");

		} catch (ClassNotFoundException e) {
			System.err.println("# Fehler!\n  - Die von Ihnen angegebene Klasse (" + classname + ") konnte nicht gefunden werden!");
			System.err.println("  - Eventuell müssen Sie die Klasse erst kompilieren, z.B. mit");
			System.err.println("  - javac " + classname + ".java");
		} catch (InstantiationException e) {
			System.err.println("# Fehler!\n  - Es konnte kein " + classname + "-Objekt erzeugt werden:\n" + e.getMessage());
		} catch (IllegalAccessException e) {
			System.err.println("# Fehler!\n  - Es konnte kein " + classname + "-Objekt erzeugt werden:\nSie können nicht auf den entsprechenden Konstruktor zugreifen.\n" + e.getMessage());
		} catch (NoClassDefFoundError e) {
			System.err.println("# Fehler!\n  - Die von Ihnen angegebene Klasse (" + classname + ") konnte nicht gefunden werden!");
			System.err.println("  - Stimmt die Groß/Kleinschreibung?");
		}
	}

	@Invokable(helptext = "Lädt eine Klasse mit speziellem Konstruktor")
	public void load(String classname, String[] args) throws Exception {

		try {
			Class<?> classobj = Class.forName(classname);
			System.out.println("+ Klasse " + classobj.getName() + " geladen.");

			String varname = classobj.getName().toLowerCase() + incrementRefcount(classobj);

			Object instance = null;

			for (Constructor<?> constructor : classobj.getDeclaredConstructors()) {

				if (constructor.getModifiers() != Modifier.PUBLIC)
					continue;
				Class<?>[] parameters = constructor.getParameterTypes();
				if (parameters.length != args.length)
					continue;

				Object[] cast = new Object[parameters.length];
				try {
					for (int i = 0; i < cast.length; i++)
						cast[i] = cast(args[i], parameters[i]);

					instance = constructor.newInstance(cast);

				} catch (Exception e) {
					if (e.getCause() != null)
						throw (Exception) e.getCause();
				}
			}

			if (instance == null)
				throw new NoSuchMethodException("Unbekannter Konstruktor für die Klasse " + classname);

			instances.put(varname, instance);
			System.out.println("  - Neues " + classname + "-Objekt (" + varname + ") mit angegebenem Konstruktor erzeugt.");

		} catch (ClassNotFoundException e) {
			System.err.println("# Fehler!\n  - Die von Ihnen angegebene Klasse (" + classname + ") konnte nicht gefunden werden!");
			System.err.println("  - Eventuell müssen Sie die Klasse erst kompilieren, z.B. mit");
			System.err.println("  - javac " + classname + ".java");
		} catch (InstantiationException e) {
			System.err.println("# Fehler!\n  - Es konnte kein " + classname + "-Objekt erzeugt werden:\n" + e.getMessage());
		} catch (IllegalAccessException e) {
			System.err.println("# Fehler!\n  - Es konnte kein " + classname + "-Objekt erzeugt werden:\nSie können nicht auf den entsprechenden Konstruktor zugreifen.\n" + e.getMessage());
		} catch (NoClassDefFoundError e) {
			System.err.println("# Fehler!\n  - Die von Ihnen angegebene Klasse (" + classname + ") konnte nicht gefunden werden!");
			System.err.println("  - Stimmt die Groß/Kleinschreibung?");
		}
	}

	@Invokable(helptext = "Listet alle Objekte auf")
	public void list() {
		if (instances.size() == 0)
			System.out.println("+ Keine Objekte");
		else if (instances.size() == 1)
			System.out.println("+ Ein Objekt:");
		else
			System.out.println("+ " + instances.size() + " Objekte:");

		for (String key : instances.keySet())
			System.out.println("  - " + key + " " + instances.get(key).getClass().getSimpleName());

	}

	@Invokable(helptext = "Beendet die Ausführung")
	public void exit() {
		run = false;
	}

	@Invokable(helptext = "Ruft den Java-Compiler auf")
	public void javac(String filename) throws IOException {
		Process p = Runtime.getRuntime().exec("javac " + filename);

		int c;
		byte[] buf = new byte[1024];

		InputStream input = p.getErrorStream();
		while ((c = input.read(buf, 0, buf.length)) >= 0)
			System.out.write(buf, 0, c);

		input = p.getInputStream();
		while ((c = input.read(buf, 0, buf.length)) >= 0)
			System.out.write(buf, 0, c);

	}

	private static String[] commandlineSplit(String input) {
		StringTokenizer tokenizer = new StringTokenizer(input.trim(), "\" ", true);
		LinkedList<String> result = new LinkedList<String>();
		boolean inQuotes = false;
		String buffer = "";
		while (tokenizer.hasMoreTokens()) {
			String token = tokenizer.nextToken();
			if (token.equals(" ")) {
				if (inQuotes) {
					buffer += token;
				} else {
					if (buffer.length() > 0)
						result.add(buffer);
					buffer = "";
				}
			} else if (token.equals("\"")) {
				if (buffer.length() > 0 && buffer.charAt(buffer.length() - 1) == '\\') {
					buffer += token;
				} else {
					if (inQuotes) {
						if (buffer.length() > 0)
							result.add(buffer);
						buffer = "";
					}
					inQuotes = !inQuotes;
				}
			} else {
				buffer += token;
			}

		}
		if (buffer.length() > 0)
			result.add(buffer);
		return (String[]) result.toArray(new String[0]);
	}

	private Object cast(String input, Class<?> type) throws IllegalArgumentException, SecurityException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {

		if (instances.containsKey(input) && type.isAssignableFrom(instances.get(input).getClass()))
			return type.cast(instances.get(input));

		// bad code as java refuses to use autoboxing in their
		// isAssignable-Checks and thus throwing no-such-method-exceptions on
		// invocation with non-primitive types. I cannot put primitive types
		// into Object[]
		if (type.equals(byte.class) || type.equals(Byte.class))
			return new Byte(Byte.parseByte(input));
		else if (type.equals(short.class) || type.equals(Short.class))
			return new Short(Short.parseShort(input));
		else if (type.equals(int.class) || type.equals(Integer.class))
			return new Integer(Integer.parseInt(input));
		else if (type.equals(long.class) || type.equals(Long.class))
			return new Long(Long.parseLong(input));
		else if (type.equals(float.class) || type.equals(Float.class))
			return new Float(Float.parseFloat(input));
		else if (type.equals(double.class) || type.equals(Double.class))
			return new Double(Double.parseDouble(input));
		else if (type.equals(double.class) || type.equals(Boolean.class))
			return new Boolean(Boolean.parseBoolean(input));
		else if (type.equals(char.class) || type.equals(Character.class))
			return new Character(input.charAt(0));
		else
			return type.cast(input); // desperate mode, will probably fail
	}

}
