package at.oefai.aaa.agent.jam;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import at.oefai.aaa.agent.jam.types.AbstractExpression;
import at.oefai.aaa.agent.jam.types.Binding;
import at.oefai.aaa.agent.jam.types.ExpList;
import at.oefai.aaa.agent.jam.types.Value;
import at.oefai.aaa.agent.jam.types.ValueLong;
import at.oefai.aaa.agent.jam.types.ValueObject;
import at.oefai.aaa.agent.jam.types.ValueReal;
import at.oefai.aaa.agent.jam.types.ValueString;
import at.oefai.aaa.agent.jam.types.Variable;

/**
 * Represents a function call, with or without a classname (i.e. function or class method).
 *
 * @author Marc Huber
 * @author Jaeho Lee
 */

class FunctionCall extends AbstractExpression implements Serializable {
    // a logger for info and debug output
    private final AgentLogger log;
    private final Interpreter interpreter;

    private String name = null;
    private String classname = null;
    private ExpList args = null;
    private Variable object = null;

    /** Constructor with name and argument list. */
    FunctionCall(final String pName, final ExpList pArgs, final Interpreter pInterpreter) {
        this.name = pName;
        this.args = pArgs;
        this.interpreter = pInterpreter;
        this.log = this.interpreter.getLog();
    }

    /**
     * Constructor for class-based method invocation with class and function names and argument
     * list.
     */
    FunctionCall(final String pClassName, final String pFunctionName, final ExpList pArgs, final Interpreter pInterpreter) {
        this.name = pFunctionName;
        this.classname = pClassName;
        this.args = pArgs;
        this.interpreter = pInterpreter;
        this.log = this.interpreter.getLog();
    }

    /**
     * Constructor for class-based method invocation with class and function names and argument
     * list.
     */
    FunctionCall(final String pClassName, final String pFunctionName, final Variable pVariable, final ExpList pArgs,
            final Interpreter pInterpreter) {
        this.name = pFunctionName;
        this.classname = pClassName;
        this.args = pArgs;
        this.interpreter = pInterpreter;
        this.object = pVariable;
        this.log = this.interpreter.getLog();
    }

    // Member functions

    public ExpList getArgs() {
        return this.args;
    }

    /** Perform the function. */
    public Value eval(final Binding binding) {
        Value returnValue;
        Class c = null;
        Object obj = null;
        Object returnedObj = null;
        Goal currentGoal = this.interpreter.getIntentionStructure().getCurrentGoal();
        // If there's a class defined then try reflection
        if (this.classname != null && this.classname.length() != 0) {
            //log.info("Executing class function " + _classname + "." + _name);
            try {
                c = Class.forName(this.classname);
            } catch (ClassNotFoundException e) {
                this.log.warning("\nCould not find class \"" + this.classname + "\"!\n");
                return Value.FALSE;
            }
            if (this.object != null) {
                obj = this.object.eval(binding).getObject();
                c = obj.getClass();
            }
            // Create a local instance of the specified class if there
            // isn't already one specified (i.e., if the method is a static
            // function or otherwise doesn't require an object reference)
            if (obj == null) {
                try {
                    obj = c.newInstance();
                } catch (InstantiationException e) {
                    this.log.warning("InstantiationException", e);
                    return Value.FALSE;
                } catch (IllegalAccessException e) {
                    this.log.warning("IllegalAccessException", e);
                    return Value.FALSE;
                }
            }
            // Check for the case that the class is set up according to our JAM-specified interface.
            if (obj instanceof PrimitiveAction) {
                return ((PrimitiveAction) obj).execute(this.classname + "." + this.name, this.args, binding, currentGoal);
            }
            // Go through all the object's methods and find the method with a name matching the plan's action
            // StefanRank: getMethods instead of getDeclaredMethods, to find inherited methods too
            for (Method m : c.getMethods()) {
                // Perform name comparison
                if (this.name.equals(m.getName())) {
                    Class returntype = m.getReturnType();
                    Class[] parameters = m.getParameterTypes();
                    Class[] exceptions = m.getExceptionTypes();
                    // Ignore return type and exceptions for now
                    // Check to see if parameter list length and types match.
                    if (parameters.length == this.args.size()) {
                        boolean matched = true;
                        Object[] margs = new Object[this.args.size()];
                        // Go through each parameter and check for matching types
                        for (int j = 0; j < parameters.length; j++) {
                            // Check each parameter for a matching type and build an Object array from the arguments
                            // JavaCC parser will be creating Value objects of the appropriate
                            // parameter type though, so will need to restrict member functions
                            // to parameters of type Long, String, Double, and Object.
                            Value argument = this.args.getNth(j + 1).eval(binding);
                            //log.info("parameters[j].getName() = " + parameters[j].getName());
                            //log.info("argument = " + argument);
                            if ((argument instanceof ValueLong)
                                    && ((parameters[j].getName().equals("java.lang.Integer")) || (parameters[j].getName()
                                            .equals("int")))) {
                                margs[j] = new Integer((int) argument.getLong());
                            } else if ((argument instanceof ValueLong)
                                    && ((parameters[j].getName().equals("java.lang.Long"))
                                            || (parameters[j].getName().equals("long")))) {
                                margs[j] = new Long(argument.getLong());
                            } else if ((argument instanceof ValueReal)
                                    && ((parameters[j].getName().equals("java.lang.Float"))
                                            || (parameters[j].getName().equals("java.lang.Double"))
                                            || (parameters[j].getName().equals("float"))
                                            || (parameters[j].getName().equals("double")))) {
                                margs[j] = new Double(argument.getReal());
                            } else if ((argument instanceof ValueString) && (parameters[j].getName().equals("java.lang.String"))) {
                                margs[j] = argument.getString();
                            } else if (argument instanceof ValueObject) {
                                // Need to see if the parameter is an object of some form (and not a String)
                                if ((parameters[j].getName().indexOf("java.lang.String") == -1)
                                        || (!parameters[j].getName().equals("int")) || (!parameters[j].getName().equals("long"))
                                        || (!parameters[j].getName().equals("float")) || (!parameters[j].getName().equals("double"))) {
                                    margs[j] = argument.getObject();
                                }
                            } else {
                                matched = false;
                                break;
                            }
                        }
                        if (matched) { // if parameters match then invoke method
                            try {
                                Class returnedClass = returnedObj.getClass();
                                returnedObj = m.invoke(obj, margs);
                                //log.info("HERE: Returned object is: " + returnedObj);
                                //log.info("returnedClass is: " + returnedClass);
                                if (returnedObj instanceof Double) {
                                    return Value.newValue(((Double) returnedObj).doubleValue());
                                } else if (returnedObj instanceof Float) {
                                    return Value.newValue(((Float) returnedObj).doubleValue());
                                } else if (returnedObj instanceof Integer) {
                                    return Value.newValue(((Integer) returnedObj).longValue());
                                } else if (returnedObj instanceof Long) {
                                    return Value.newValue(((Long) returnedObj).longValue());
                                } else if (returnedObj instanceof String) {
                                    return Value.newValue(returnedObj.toString());
                                }
                                return Value.newValue(returnedObj);
                            } catch (IllegalAccessException e) {
                                this.log.warning(e.getMessage());
                                return Value.FALSE;
                            } catch (IllegalArgumentException e) {
                                this.log.warning(e.getMessage());
                                return Value.FALSE;
                            } catch (InvocationTargetException e) {
                                this.log.warning(e.getMessage());
                                return Value.FALSE;
                            }
                        }
                    } else {
                        this.log.warning("Argument count mis-match (plan action had " + this.args.size() + " and member function had "
                                + parameters.length + ")");
                    }
                }
            }
            return Value.newValue(returnedObj);
        }
        // Otherwise, see if it's a system-defined primitive
        //log.info("Attempting to execute a normal function.");
        returnValue = this.interpreter.getSystemFunctions().execute(this.name, this.args, binding, currentGoal);
        if (returnValue.isDefined()) {
            return returnValue;
        }
        //else log.info("FunctionCall: Action \"" + _classname + "\" not found in system functions, checking user functions.");
        // Last but not least, see if it's a user-defined primitive
        returnValue = this.interpreter.getUserFunctions().execute(this.name, this.args, binding, currentGoal);
        if (!returnValue.isDefined()) {
            this.log.warning("FunctionCall: Action \"" + this.name + "\" not found in user-defined functions!\n");
        }
        return returnValue;
    }

    /** Display information without considering it being in-line with other information. */
    public String verboseString(final Binding b) {
        String s = "FunctionCall: ";
        s += this.classname + " " + this.name + " " + this.object.verboseString(b) + ", " + this.args.verboseString(b);
        return s;
    }

    /** Display information considering that it will be in-line with other information. */
    public String formattedString(final Binding b) {
        return this.verboseString(b);
    }

}
