package eu.icolumbo.breeze;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.StringTokenizer;

import static java.lang.String.format;


/**
 * Method binding for beans.
 * @author Pascal S. de Kloe
 * @author Jethro Bakker
 */
public class FunctionSignature implements Serializable {

	private static final long serialVersionUID = 2L;

	private final String FUNCTION;
	private final String[] arguments;


	public FunctionSignature(String function, String... arguments) {
		this.FUNCTION = function;
		this.arguments = arguments;
	}

	/**
	 * Parses a signature.
	 */
	public static FunctionSignature valueOf(String serial) {
		int paramStart = serial.indexOf("(");
		int paramEnd = serial.indexOf(")");
		if (paramStart < 0 || paramEnd != serial.length() - 1)
			throw new IllegalArgumentException("Malformed method signature: " + serial);

		String function = serial.substring(0, paramStart).trim();

		String arguments = serial.substring(paramStart + 1, paramEnd);
		StringTokenizer tokenizer = new StringTokenizer(arguments, ", ");
		String[] names = new String[tokenizer.countTokens()];
		for (int i = 0; i < names.length; ++i)
			 names[i] = tokenizer.nextToken();

		return new FunctionSignature(function, names);
	}

	/**
	 * Gets the name;
	 */
	public String getFunction() {
		return FUNCTION;
	}

	/**
	 * Gets the names.
	 */
	public String[] getArguments() {
		return arguments;
	}

	/**
	 * Gets a method match.
	 */
	public Method findMethod(Class<?> type)	throws ReflectiveOperationException {
		Method match = null;
		for (Method option : type.getDeclaredMethods()) {
			if (! getFunction().equals(option.getName())) continue;
			Class<?>[] parameters = option.getParameterTypes();
			if (parameters.length != getArguments().length) continue;
			if (match != null) {
				if (refines(option, match)) continue;
				if (! refines(match, option))
					throw ambiguity(match, option);
			}
			match = option;
		}

		Class<?>[] parents = { type.getSuperclass() };
		if (type.isInterface())
			parents = type.getInterfaces();
		for (Class<?> parent : parents) {
			if (parent == null) continue;
			try {
				Method superMatch = findMethod(parent);
				if (match == null) {
					match = superMatch;
					continue;
				}
				if (! refines(superMatch, match))
					throw ambiguity(match, superMatch);
			} catch (NoSuchMethodException ignored) {
			}
		}

		if (match == null) {
			String fmt = "No method %s#%s with %d parameters";
			String msg = format(fmt, type, getFunction(), getArguments().length);
			throw new NoSuchMethodException(msg);
		}

		match.setAccessible(true);
		return match;
	}

	private static boolean refines(Method a, Method b) {
		Class<?>[] aParams = a.getParameterTypes();
		Class<?>[] bParams = b.getParameterTypes();
		int i = aParams.length;
		while (--i >= 0)
			if (! bParams[i].isAssignableFrom(aParams[i]))
				return false;
		return true;
	}

	private static ReflectiveOperationException ambiguity(Method a, Method b)
	throws ReflectiveOperationException {
		String[] readable = { a.toGenericString(), b.toGenericString() };
		Arrays.sort(readable);
		String msg = format("Ambiguity between %s and %s",
				readable[0], readable[1]);
		return new ReflectiveOperationException(msg);
	}

}