package at.oefai.aaa.agent.jam;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import at.oefai.aaa.agent.jam.types.Binding;
import at.oefai.aaa.agent.jam.types.ExpList;
import at.oefai.aaa.agent.jam.types.Expression;
import at.oefai.aaa.agent.jam.types.Value;
import at.oefai.aaa.agent.jam.types.DList;

/**
 * Base class for defining primitive functionality
 * @author Marc Huber
 * @author Jaeho Lee
 */
public class UserFunctions implements Functions {
    // a logger for info and debug output, root logger for a start
    private final AgentLogger log;
    private final Interpreter interpreter;
    private Map<String,Map<String,List<String>>> pathMap = null;
    // a timestamp for behaviour plan executions
    private long timeStamp = 0;

    /** Primary constructor */
    UserFunctions(Interpreter pInterpreter) {
        this.interpreter = pInterpreter;
        this.log = pInterpreter.getLog();
    }

    // Member functions

    public Value execute(String name, ExpList args, Binding binding, Goal currentGoal) {
        // cache the arity
        int arity = args.size();

        // Create a TestClass object instance and return it
        // Out:TestClass object
        /*if (name.equals("createNewTestClassObject")) {
            if (arity != 1) {
                log.warning("Invalid number of arguments: " + arity + " to function \"" + name + "\"\n");
                return Value.FALSE;
            }
            ListIterator ele = args.listIterator();
            Expression exp = (Expression) ele.next();
            TestClass obj = new TestClass();
            binding.setValue(exp, Value.newValue(obj));
            return Value.TRUE;
        }
        */

        // Synchronize world model state with extern world object
        if (name.equals("perceiveEnvironment")) {
            //log.info("synch World Model of " + _interpreter.getName());
            if (this.interpreter instanceof IEnvironment) {
                WorldModelTable wmt = this.interpreter.getWorldModel();
                WorldModelTableEvent[] eventQueue = ((IEnvironment) this.interpreter).getPerceptionsOf(this.interpreter.getName());
                if (eventQueue == null || eventQueue.length < 1) {
                    return Value.TRUE;
                }
                PlanTable pt = this.interpreter.getPlanLibrary();
                DList<Plan> plans = null;
                for (int i = 0; i < eventQueue.length; i++) {
                    Relation r = eventQueue[i].getRelation();
                    r = new Relation(r.getName(),r.getArgs());
                    switch (eventQueue[i].getType()) {
                        case ASSERT:
                            wmt.assertPerception(r);
                            plans = pt.getPerceiveAssertPlans(r.getName());
                            if (plans.size() > 0) {
                                this.executeMatchingPlan(r, plans);
                            }
                            break;
                        case RETRACT:
                            wmt.retractPerception(r);
                            plans = pt.getPerceiveRetractPlans(r.getName());
                            if (plans.size() > 0) {
                                this.executeMatchingPlan(r, plans);
                            }
                            break;
                        default: // nothing
                            assert false : "perceived event with unknown type " +
                                    "neither ASSERT nor RETRACT: " + eventQueue[i].getType();
                    }
                }
                return Value.TRUE;
            }
            return Value.FALSE; // error: could not perceive
        }

        // initialize a time stamp now for later comparison checkTime
        if (name.equals("initTime")) {
            if (arity != 0) {
                this.log.warning("Invalid number of arguments: " + arity + " to function \"" + name + "\"\n");
                return Value.FALSE;
            }
            this.timeStamp = this.interpreter.currentSimTimeMillis();
            return Value.TRUE;
        }

        // initialize a time stamp now for later comparison checkTime
        if (name.equals("checkTimeSmallerMillis")) {
            if (arity != 1) {
                this.log.warning("Invalid number of arguments: " + arity + " to function \"" + name + "\"\n");
                return Value.FALSE;
            }
            Value v = args.getFirst().eval(binding);
            long diff = v.getLong();
            if ((this.interpreter.currentSimTimeMillis() - this.timeStamp) > diff) {
                return Value.FALSE;
            }
            return Value.TRUE;
        }

        // get the current simulation time as long (unused)
        if (name.equals("getCurrentSimTime")) {
            if (arity != 1) {
                this.log.warning("Invalid number of arguments: " + arity + " to function \"" + name + "\"\n");
                return Value.FALSE;
            }
            Expression varExp = args.getFirst();
            binding.setValue(varExp, Value.newValue(this.interpreter.currentSimTimeMillis()));
            return Value.TRUE;
        }

        // get the current simulation time as formatted string (for logging)
        if (name.equals("getCurrentSimTimeString")) {
            if (arity != 1) {
                this.log.warning("Invalid number of arguments: " + arity + " to function \"" + name + "\"\n");
                return Value.FALSE;
            }
            Expression varExp = args.getFirst();
            String timeString = this.formatMillis(this.interpreter.currentSimTimeMillis());
            binding.setValue(varExp, Value.newValue(timeString));
            return Value.TRUE;
        }

        // get a the responsible one from an appraisal (the fact that was appraised)
        if (name.equals("getResponsible")) {
            if (arity != 2) {
                this.log.warning("Invalid number of arguments: " + arity + " to function \"" + name + "\"\n");
                return Value.FALSE;
            }
            if (this.interpreter instanceof IEnvironment) {
                Value v = args.getLast().eval(binding);
                Appraisal app = (Appraisal) v.getObject();
                Expression varExp = args.getFirst();
                binding.setValue(varExp, Value.newValue(app.getRelation().getResponsible()));
                return Value.TRUE;
            }
            return Value.FALSE;
        }

        // get the intensity of the appraisal
        if (name.equals("getIntensity")) {
            if (arity != 2) {
                this.log.warning("Invalid number of arguments: " + arity + " to function \"" + name + "\"\n");
                return Value.FALSE;
            }
            if (this.interpreter instanceof IEnvironment) {
                Value v = args.getLast().eval(binding);
                Appraisal app = (Appraisal) v.getObject();
                Expression varExp = args.getFirst();
                binding.setValue(varExp, Value.newValue(app.getIntensity()));
                return Value.TRUE;
            }
            return Value.FALSE;
        }


        // calculate a path for movement (faster and easier than in JAM itself)
        // return as space delimited stringtokens
        if (name.equals("getPathFromTo")) {
            if (arity != 3) {
                this.log.warning("Invalid number of arguments: " + arity + " to function \"" + name + "\"\n");
                return Value.FALSE;
            }
            if (this.interpreter instanceof IEnvironment) {
                Expression varExp = args.getNth(1);
                Value v1 = args.getNth(2).eval(binding);
                Value v2 = args.getNth(3).eval(binding);
                String start = v1.getString();
                String end = v2.getString();
                binding.setValue(varExp, Value.newValue(listToString(getPath(start, end))));
                return Value.TRUE;
            }
            return Value.FALSE;
        }

        // calculate distance of a path as an integer (number of steps)
        if (name.equals("getDistanceFromTo")) {
            if (arity != 3) {
                this.log.warning("Invalid number of arguments: " + arity + " to function \"" + name + "\"\n");
                return Value.FALSE;
            }
            if (this.interpreter instanceof IEnvironment) {
                Expression varExp = args.getNth(1);
                Value v1 = args.getNth(2).eval(binding);
                Value v2 = args.getNth(3).eval(binding);
                String start = v1.getString();
                String end = v2.getString();
                binding.setValue(varExp, Value.newValue(getDistance(start, end)));
                return Value.TRUE;
            }
            return Value.FALSE;
        }

        // get all possible steps from a certain point
        // return as space delimited stringtokens
        if (name.equals("getStepsFrom")) {
            if (arity != 2) {
                this.log.warning("Invalid number of arguments: " + arity + " to function \"" + name + "\"\n");
                return Value.FALSE;
            }
            if (this.interpreter instanceof IEnvironment) {
                Expression varExp = args.getNth(1);
                Value v1 = args.getNth(2).eval(binding);
                String start = v1.getString();
                binding.setValue(varExp, Value.newValue(listToString(getSteps(start))));
                return Value.TRUE;
            }
            return Value.FALSE;
        }


        //
        // perform an action in the "real" world
        //
        if (name.equals("worldAction")) {
            if (arity < 1) {
                this.log.warning("Invalid number of arguments: " + arity + " to function \"" + name + "\"\n");
                return Value.FALSE;
            }
            if (this.interpreter instanceof IEnvironment) {
                ExpList evaldExpList = args.explistEval(binding);
                if (evaldExpList.allDefined()) {
                    List<Value> newArgs = evaldExpList.asValueList(binding);
                    Value v = newArgs.remove(0);
                    String action = v.getString();
                    this.log.info(this.interpreter.getName() + " tries action " +
                             action + " on World Model, arguments: (" +
                             newArgs + ")");
                    if (((IEnvironment) this.interpreter).requestAction(this.interpreter.getName(), action, newArgs)) {
                        return Value.TRUE;
                    }
                } else {
                    this.log.warning(this.interpreter.getName() + ": worldAction called with undefined argument"
                            + ", args: " + args.formattedString(binding));
                }
            }
            return Value.FALSE;
        }

        // return the conformance to the agent's standards that would be calculated
        // by the Appraiser, if the arguments were the args of a AgentWantsTo FACT
        if (name.equals("getConformance")) {
            if (arity < 3) { // this is the actual minimum
                this.log.warning("Invalid number of arguments: " + arity + " to function \"" + name + "\"\n");
                return Value.FALSE;
            }
            if (this.interpreter instanceof IEnvironment) {
                Expression varExp = args.getLast();
                ExpList evaldExpList = args.explistEval(binding);
                evaldExpList.removeLast();
                Relation rel = new Relation(Relation.AGENTWANTS_FACT, evaldExpList);
                float conformance = Appraiser.getConformance(new WorldModelRelation(rel),
                        this.interpreter.getWorldModel().getRelations(Relation.STANDARD_FACT),
                        this.log);
                binding.setValue(varExp, Value.newValue(conformance));
                return Value.TRUE;
            }
            return Value.FALSE;
        }

        // the basic step of meta-level plans (SP)
        if (name.equals("metaProcessing")) {
            if (arity != 1) {
                this.log.warning("Invalid number of arguments: " + arity + " to function \"" + name + "\"\n");
                return Value.FALSE;
            }
            if (this.interpreter instanceof IEnvironment) {
                this.saveFailedIntention();
                Value v = args.getFirst().eval(binding);
                APL apl = (APL) v.getObject();
                APLElement bestElem = apl.getUtilityRandom();
                IntentionPair bestIP; // assigned in while condition
                // exits for the following while: if apl is empty, then bestElem == null, so continue without intent, and
                // if the bestElem intention did not fail recently just use it below. new bestElem at the end of the loop.
                while (bestElem != null
                       && this.interpreter.getMetaData().intentionFailedRecently(bestIP = new IntentionPair(bestElem))) {
                    boolean abandonGoal = true;
                    Goal g = bestElem.getFromGoal();
                    // SP: delete the oldest failed intention ("forget it") -> due to the
                    // limited number of actions, make it here
                    this.interpreter.getMetaData().saveFailedIntention(null);
                    for (APLElement aplel : apl.getIntentions()) {
                        // check if another APLElement exists for the same goal, which
                        // means, there is a possible (equal or less desirable) alternative
                        // StefanRank: avoid using matchGoal as it also compares the utilities,
                        // plans must be different anyway. I think goal identity should be possible,
                        // the difference lies in the binding and the plan and these are in the APLElement.
                        if (aplel != bestElem && aplel.getFromGoal() == g) {
                            abandonGoal = false;
                            this.log.info("Meta: Found one more APLElement instead of avoided recently failed one:\n"
                                          + aplel);
                            break;
                        }
                    }
                    if (abandonGoal) {
                        // no other APL Element found for that goal, this means,
                        // the goal is not achievable at the moment
                        Goal prevGoal = g.getPrevGoal();
                        if (prevGoal != null) { // it is not a toplevel goal
                            Goal prevprevGoal = prevGoal.getPrevGoal();
                            if (prevprevGoal != null) { // at least two goals above
                                this.interpreter.getMetaData().saveFailedIntention(new IntentionPair(prevGoal.getIntention()));
                                this.interpreter.getIntentionStructure().addRetryGoal(prevprevGoal);
                            } else { // only one goal above
                                if (prevGoal.getGoalAction() instanceof MaintainGoalAction) {
                                    this.interpreter.getMetaData().saveFailedIntention(new IntentionPair(prevGoal.getIntention()));
                                    this.interpreter.getIntentionStructure().addRetryGoal(prevGoal);
                                    this.log.info("Meta: start corresponding toplevel maintain goal again: " + prevGoal);
                                } else {
                                    //this.interpreter.getIntentionStructure().addFailedGoal(prevGoal);
                                    prevGoal.setStatus(Goal.Status.ABANDONED);
                                    this.log.info("Meta: dropped corresponding failed toplevel goal: " + prevGoal);
                                }
                            }
                        } else { // it is a top goal
                            if (g.getGoalAction() instanceof MaintainGoalAction) {
                                this.interpreter.getIntentionStructure().addRetryGoal(g);
                                this.log.info("Meta: start toplevel maintain goal again: " + g);
                            } else {
                                //this.interpreter.getIntentionStructure().addFailedGoal(g);
                                g.setStatus(Goal.Status.ABANDONED);
                                this.log.info("Meta: dropped a recently failed toplevel goal: " + g);
                            }
                        }
                    }
                    apl.remove(bestElem);
                    bestElem = apl.getUtilityRandom();
                }
                if (bestElem != null) {
                    this.interpreter.getIntentionStructure().intend(bestElem);
                }
                // unposting of superceded coping goals is now done in AppraisalRegister
                return Value.TRUE;
            }
            return Value.FALSE;
        }

        if (name.equals("getMood")) {
            if (arity != 1) {
                this.log.warning("Invalid number of arguments: " + arity + " to function \"" + name + "\"\n");
                return Value.FALSE;
            }
            if (this.interpreter instanceof IEnvironment) {
                Expression varExp = args.getFirst();
                float mood = this.interpreter.getMetaData().getMood();
                binding.setValue(varExp, Value.newValue(mood));
                return Value.TRUE;
            }
            return Value.FALSE;
        }

        //
        // animate an action in the "real" world
        //
        if (name.equals("animate")) {
            if (arity < 2) {
                this.log.warning("Invalid number of arguments: " + arity + " to function \"" + name + "\"\n");
                return Value.FALSE;
            }
            if (this.interpreter instanceof Animator) {
                Expression varTask = args.getFirst();
                ExpList evaldExpList = args.explistEval(binding);
                evaldExpList.removeFirst(); // the task variable
                List<Value> newArgs = evaldExpList.asValueList(binding);
                Value v = newArgs.remove(0); // the actor name
                String actor = v.getString();
                v = newArgs.remove(0); // the action name
                String actionName = v.getString();
                this.log.info("ANIMATION: " + actor + " does " +
                         actionName + ", arguments: (" +
                         newArgs + ")");
                Animator.ITask task = ((Animator) this.interpreter).animate(actor, actionName, newArgs);
                // now bind the task to a variable (there should be error code for the cast)
                binding.setValue(varTask,Value.newValue(task));
                return Value.TRUE;
            }
            return Value.FALSE;
        }

        // test if action is completed
        if (name.equals("animCompleteTest")) {
            if (arity != 1) {
                this.log.warning("Invalid number of arguments: " + arity + " to function \"" + name + "\"\n");
                return Value.FALSE;
            }

            if (this.interpreter instanceof Animator) {
                Value v = args.getFirst().eval(binding);
                Animator.ITask task = (Animator.ITask) v.getObject();
                // check if finished
                return (task.isFinished() ? Value.TRUE : Value.FALSE);
            }
            return Value.FALSE;
        }

        // Nothing matches
        return Value.UNDEFINED;
    }


    private List<String> getPath(final String from, final String to) {
        if (this.pathMap == null) {
            buildPathMap();
        }
        return this.pathMap.get(from).get(to);
    }

    private List<String> getSteps(final String from) {
        if (this.pathMap == null) {
            buildPathMap();
        }
        return getNextSteps(from);
    }

    private int getDistance(final String from, final String to) {
        if (from.equals(to)) return 0;
        if (this.pathMap == null) {
            buildPathMap();
        }
        return this.pathMap.get(from).get(to).size();
    }

    private String listToString(final List list) {
        String result = "";
        if (list != null) {
            Iterator iter = list.iterator(); // needed for lookahead
            while (iter.hasNext()) {
                result += iter.next();
                if (iter.hasNext()) result += " ";
            }
        }
        return result;
    }

    private void buildPathMap() {
        this.pathMap = new HashMap<String,Map<String,List<String>>>();
        //log.info(pathsString());
        WorldModelTable worldModel = this.interpreter.getWorldModel();
        List<WorldModelRelation> ps = worldModel.getRelations("PathTo");
        String from = null;
        String to = null;
        for (Relation r : ps) {
            from = r.getArgs().getFirst().eval(null).getString();
            to = r.getArgs().getLast().eval(null).getString();
            enterPathStep(from, to);
        }
        //log.info(pathsString());
        for (String s : this.pathMap.keySet()) {
            enterFullPaths(s);
        }
        //log.info(pathsString());
    }

    private void enterPathStep(final String from, final String to) {
        Map<String,List<String>> entries = this.pathMap.get(from);
        if (entries == null) {
            entries = new HashMap<String,List<String>>();
            this.pathMap.put(from, entries);
        }
        List<String> path = new ArrayList<String>();
        path.add(to);
        entries.put(to, path);
    }

    private void enterFullPaths(final String from) {
        Map<String,List<String>> toMap = this.pathMap.get(from);
        List<List<String>> paths = new ArrayList<List<String>>();
        for (String step : getNextSteps(from)) {
            List<String> sList = new ArrayList<String>();
            sList.add(step);
            paths.add(sList);
        }
        boolean change = true;
        while (change) {
            change = false;
            List<List<String>> newPaths = new ArrayList<List<String>>();
            for (List<String> currPath : paths) {
                int size = currPath.size();
                String last = currPath.get(size - 1);
                List<String> steps = getNextSteps(last);
                steps.remove(from);
                if (size > 1) {
                    steps.remove(currPath.get(size - 2));
                }
                for (String nextStep : steps) {
                    List<String> newPath = new ArrayList<String>(currPath);
                    newPath.add(nextStep);
                    if (toMap.get(nextStep) == null) {
                        toMap.put(nextStep, newPath);
                        newPaths.add(newPath);
                        change = true;
                    }
                }
            }
            paths = newPaths;
        }
    }


    private List<String> getNextSteps(final String from) {
        List<String> result = new ArrayList<String>();
        Map<String,List<String>> paths = this.pathMap.get(from);
        for (List<String> p : paths.values()) {
            if (p.size() == 1) {
                result.add(p.get(0));
            }
        }
        return result;
    }

    // debug ouput
    private String pathsString() {
        StringBuffer sb = new StringBuffer("PathMap:\n");
        if (this.pathMap != null) {
            for (Map.Entry<String,Map<String,List<String>>> e : this.pathMap.entrySet()) {
                sb.append(e.getKey() + ":\n");
                Map<String,List<String>> m = e.getValue();
                for (Map.Entry<String,List<String>> innerE : m.entrySet()) {
                    sb.append("    to " + innerE.getKey() + ": " + innerE.getValue() + "\n");
                }
            }
        }
        return sb.toString();
    }


    private void executeMatchingPlan(Relation rel, DList<Plan> plans) {
        for (Plan plan : plans) {
            Binding planBinding = new Binding(plan.getSymbolMap());
            Relation planRelation = plan.getPerceiveSpecification();
            if (Relation.unify(planRelation, planBinding, rel, null)) {
                DList<Binding> bindingList = new DList<Binding>(planBinding);
                if (plan.checkPrecondition(bindingList)) {
                    for (Binding b : bindingList) {
                        this.interpreter.getIntentionStructure().executePlan(plan, b);
                    }
                }
            }
        }
    }

    // -------------------------------------------------------------------------
    // additional functions needed for the meta reasoning

    // helper method for the metaProcessing action
    private void saveFailedIntention() {
        List<WorldModelRelation> failedIntentions = this.interpreter.getWorldModel().getRelations(Relation.FAILED_INTENTION_FACT);
        if (failedIntentions.size() < 1) {
            return;
        }
        WorldModelRelation failedIntentionRel = failedIntentions.get(0);
        Relation failedGoalRelation = (Relation) failedIntentionRel.getArgs().getFirst().eval(null).getObject();
        Plan failedPlan = (Plan) failedIntentionRel.getArgs().getLast().eval(null).getObject();
        this.interpreter.getMetaData().saveFailedIntention(new IntentionPair(failedGoalRelation, failedPlan));
        this.interpreter.getWorldModel().retractRelation(failedIntentionRel, (Binding) null);
    }

    // TIME (TODO: expand handling of simulation time for explicit time reasoning)

    /** Format millisecond time as string. */
    private String formatMillis(final long millis) {
        StringBuffer sb = new StringBuffer("0000000");
        String s = Long.toString(millis);
        sb.replace(Math.max(0, sb.length() - s.length()), sb.length(), s);
        sb.insert(sb.length() - 3, '.');
        sb.insert(sb.length() - 6, ':');
        return sb.toString();
    }
}
