package at.oefai.aaa.agent.jam;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.logging.Logger;

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;

/**
 * A simple (non-decomposable) member function action within a plan.
 * @author Marc Huber
 */
class ObjectAction extends AbstractAction implements Serializable {
    // a logger for info and debug output, root logger for a start
    private Logger log = Logger.getLogger("");

    private final String className;
    private final String functionName;
    private final Variable object;
    private final ExpList args;


    /** Static member function constructor (i.e., no object reference). */
    ObjectAction(final String pClassName, final String pFunctionName, final ExpList pExpList) {
        this.className = pClassName;
        this.object = null;
        this.functionName = pFunctionName;
        this.args = pExpList;
    }

    /** Non-static member function constructor (i.e., requires an object reference). */
    ObjectAction(final String pClassName, final String pFunctionName, final Variable pVariable, final ExpList pExpList) {
        this.className = pClassName;
        this.functionName = pFunctionName;
        this.object = pVariable;
        this.args = pExpList;
    }


    // Member functions

    public boolean isExecutableAction() {
        return true;
    }

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

    /** Find and invoke the class' method. */
    public Result execute(final Binding b, final Goal currentGoal) {
        Class c = null;
        Object obj = null;
        Object returnedObj = null;
        boolean foundMethod = false;

        if (this.className != null && this.className.length() != 0) {
            try {
                c = Class.forName(this.className);
            } catch (ClassNotFoundException e) {
                this.log.warning("\nCould not find class \"" + this.className + "\"! (Result.FAILED)\n");
                return Result.FAILED;
            }
        }
        if (this.object != null) {
            obj = this.object.eval(b).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.getMessage());
                return Result.FAILED;
            } catch (IllegalAccessException e) {
                this.log.warning("IllegalAccessException" + e.getMessage());
                return Result.FAILED;
            }
        }
        // Check for the case that the class is set up according to
        // our JAM-specified interface.
        if (obj instanceof at.oefai.aaa.agent.jam.PrimitiveAction) {
            //log.info("ObjectAction::execute: currentGoal=" + currentGoal);
            Value v = ((at.oefai.aaa.agent.jam.PrimitiveAction) obj).execute(
                    this.className + "." + this.functionName, this.args, b, currentGoal);
            if (v.eval(b).isTrue()) {
                return Result.SUCCEEDED;
            }
            return Result.FAILED;
        }
        // Go through all the object's methods and find the method with a name matching the plan's action
        // use getMethods instead getDeclaredMethods to get inherited ones (StefanRank) see FunctionCall
        for (Method m : c.getMethods()) {
            // Perform name comparison
            if (this.functionName.equals(m.getName())) {
                Class returntype = m.getReturnType();
                Class[] parameters = m.getParameterTypes();
                Class[] exceptions = m.getExceptionTypes();
                Object[] margs;
                // 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;
                    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 = null;
                        argument = this.args.getNth(j + 1).eval(b);
                        //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 parameters match then invoke method
                    if (matched) {
                        try {
                            Class returnedClass;
                            foundMethod = true;
                            returnedObj = m.invoke(obj, margs);
                            //log.info("returned object is: " + returnedObj);
                            Value v = Value.newValue(returnedObj);
                            //returnedClass = returnedObj.getClass();
                            //log.info("returnedClass is: " + returnedClass + ", w/ name: " + returnedClass.getName());

                            // Go through each function argument and check to see if any of them changed value.
                            //log.info("Values of arguments after invoke:");
                            //for (int j = 0; j < parameters.length; j++) {
                            //    log.info("margs[" + j + "] = " + margs[j]);
                            //}

                            // Why do I have this commented out? This seems to be
                            // the appropriate way to interpret return values.
                            //
                            /*
                             if (v.eval(b).isTrue()) {
                             log.info("Function returned SUCCESS!\n");
                             return Result.SUCCEEDED;
                             }
                             else {
                             log.info("Function returned FAILURE!\n");
                             return Result.FAILED;
                             }
                             */
                            return Result.SUCCEEDED;
                        } catch (IllegalAccessException e) {
                            this.log.warning(e.toString());
                            return Result.FAILED;
                        } catch (IllegalArgumentException e) {
                            this.log.warning(e.toString());
                            return Result.FAILED;
                        } catch (InvocationTargetException e) {
                            this.log.warning(e.toString());
                            return Result.FAILED;
                        }
                    }
                } else {
                    this.log.warning("Argument count mis-match (plan action had " + this.args.size() +
                            " and member function had "
                            + parameters.length + ")");
                }
            }
        }
        if (foundMethod) {
            return Result.SUCCEEDED;
        }
        this.log.warning("ERROR: Class.memberFunction of " + this.className + "." + this.functionName
                         + " not found with matching argument list!");
        return Result.FAILED;
    }

    /** Output action information considering that it may be in-line with other information. */
    public String formattedString(final Binding b) {
        return "PRIMITIVE: " + this.className + " " + this.args.formattedString(b) + "\n";
    }

    public String getName() {
        return this.className;
    }
}
