package at.oefai.aaa.agent.jam;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

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

/**
 * Represents the agent's intentions.
 * @author Marc Huber
 * @author Jaeho Lee
 */
public class IntentionStructure implements Serializable {
    private static final Comparator<Goal> GOAL_COMPARATOR = new Comparator<Goal>() {
        private static final double DOUBLE_COMPARISON_LIMIT = Double.MIN_VALUE * 3;
        public int compare(Goal o1, Goal o2) {
            double diff = o2.getIntendedUtility() - o1.getIntendedUtility();
            if (diff > DOUBLE_COMPARISON_LIMIT) {
                return 1;
            } else if (diff < -DOUBLE_COMPARISON_LIMIT) {
                return -1;
            } else {
                return 0;
            }
        }
    };

    // The list of (Goal, Intention)s being pursued.
    private final DList<Goal> goalStacks = new DList<Goal>();

    private final DList<Goal> behaviourGoalsCache = new DList<Goal>();

    // Store the currently executing goal for access from metalevel actions.
    private Goal currentGoal = null;

    // The Interpreter overwatching this intention structure
    private final AgentLogger log;
    private final WorldModelTable worldModel;

    // statistics
    private int numGoals = 0;

    // List of failed goals that are removed and set to untried
    //private List<Goal> failedGoals = new ArrayList<Goal>();
    private List<Goal> retryGoals = new ArrayList<Goal>();

    /** Default constructor w/ parent interpreter */
    IntentionStructure(final Interpreter interpreter) {
        this.log = interpreter.getLog();
        this.worldModel = interpreter.getWorldModel();
    }

    public int getNumGoalsStat() {
        return this.numGoals;
    }

    public DList<Goal> getToplevelGoals() {
        return this.goalStacks;
    }

    public DList<Goal> getSortedToplevelGoals() {
        this.goalStacks.sort(GOAL_COMPARATOR);
        return this.goalStacks;
    }

    synchronized public DList<Goal> getBehaviourGoals() {
        return this.behaviourGoalsCache;
    }

    synchronized private void cacheBehaviourGoals() {
        this.behaviourGoalsCache.clear();
        for (Goal goal : this.goalStacks) {
            do { // Need to move down to the leaf goal of the intention stack
                // check if it is a behaviour goal
                if (goal instanceof GoalDrivenGoal) {
                    if (goal.getGoalAction().getRelation().isBehaviour()) {
                        this.behaviourGoalsCache.add(goal);
                    }
                }
            } while ((goal = goal.getSubgoal()) != null);
        }
    }

    public Goal getCurrentGoal() {
        return this.currentGoal;
    }

    public WorldModelTable getWorldModel() {
        return this.worldModel;
    }

    public AgentLogger getLog() {
        return this.log;
    }

    /**
     * get a new list containing all the current leafgoals
     * for which goal-driven plans could be waiting
     * @return a new list of goals
     */
    synchronized public List<Goal> getSubGoalsForAPL() {
        List<Goal> result = new ArrayList<Goal>();
        for (Goal goal : this.goalStacks) {
            // Need to move down to the leaf goal of the intention stack
            while (goal.getSubgoal() != null) {
                goal = goal.getSubgoal();
            }
            if (this.log.getShowAPL()) {
                this.log.fine("IS::getGoalsForAPL: Leaf goal of stack is: " + goal.getName());
            }
            if (!goal.generateAPL()) {
                if (this.log.getShowAPL()) {
                    this.log.fine("IS::getGoalsForAPL: Skipping goal because generateAPL said no.");
                }
            } else {
                // Don't waste time generating an APL if it's a "holder" goal for a CONCLUDE-based intention
                if (goal instanceof DataDrivenGoal) {
                    if (this.log.getShowAPL()) {
                        this.log.fine("APL::genGoalBasedAPL: Skipping goal because it is a CONCLUDE-driven intention.");
                    }
                } else {
                    result.add(goal); // only now
                }
            }
        }
        return result;
    }

    /**
     * Arrange the intention stacks according to their evaluated utilities.
     * [This seems like it doesn't use the leaf-level utilities, which would be incorrect.  Need to verify this.]
     */
    private void sortStacksByUtility() {
        this.goalStacks.shuffle(); // shuffle, so that equal utilities are randomly selected
        confirmContexts(); // Make sure all variable bindings are up to date
        this.goalStacks.sort(GOAL_COMPARATOR);
    }

    /**
     * Add an intention to the agent's list of intentions
     * @param intention The instantiated plan to be intended
     */
    synchronized public APLElement intend(final APLElement intention) { //, final boolean force) {
        // StefanRank: eliminated the force param, as it seems unnecessary, see below
        // TODO: think about force and finally remove the commented stuff below ;-)
        if (intention.getFromGoal() != null && intention.getFromGoal().getIntention() == null) {
            // If the intention is for a top-level goal then do not intend the
            // APL element if it is not of higher utility than the current highest-utility intention stack
            // Stefan Rank: why??? it could be the intention for a toplevelgoal that got chosen
            // over a plan for a subgoal for the highest utility stack, because of the plan utilities.
            // the stack utilities (goal utilities) would not have been considered.
            // it would get dropped here and this would occur every cycle (if everything else stays the same)
            // changed to just intend it... (i would use different names for plan- and goal-utility)
            /*if (!force && intention.getFromGoal().getPrevGoal() == null) {
                sortStacksByUtility();
                APLElement tmpIntention = ((Goal) goalStacks.getFirst()).getIntention();
                if ((tmpIntention == null) || (intention.evalUtility() > tmpIntention.evalUtility())) {
                    if (log.getShowAPL())
                        log.info("IS::intend: new or highest utility intention for toplevelgoal");
                } else {
                    if (log.getShowAPL())
                        log.info("IS::intend: Intention (" + intention.getPlan().getName() + ") not being intended...");
                    return intention;
                }
            }*/
            if (this.log.getShowAPL())
                this.log.info("\n\nJAM::IntentionStructure: Intending plan: " + intention.verboseString()
                        + " to goal: " + intention.getFromGoal() + " "
                        + intention.getFromGoal().formattedString());
            intention.getFromGoal().setIntention(intention);
            intention.getFromGoal().setStatus(Goal.Status.ACTIVE);
            // StefanRank (BLOCKED-debugging): maybe we should set the parent goal to active too, as it might have been BLOCKED?
            if (intention.getFromGoal().getPrevGoal() != null
                    && intention.getFromGoal().getPrevGoal().getStatus() == Goal.Status.BLOCKED //test
                    ) {
                intention.getFromGoal().getPrevGoal().setStatus(Goal.Status.ACTIVE);
            }
            PlanSequenceConstruct body = intention.getPlan().getBody();
            intention.getFromGoal().setRuntimeState(body.newRuntimeState());
        }
        if (this.log.getShowAPL())
            this.log.fine("JAM::IntentionStructure:Intention Structure now:\n" + verboseString());
        return intention;
    }

    /** Execute the highest-utility intention */
    synchronized public Goal.Status think() {
        /** @done remember all do-help-hinder goals currently present, so that also those removed during think
         * are used in appraisal */
        this.cacheBehaviourGoals();
        /** @done after thinking reduce utility of the goals proportionally see Moffat's autoboredom
         * is one in this.reduceUtilities() called when exiting think 'successfully' */
        Goal theCurrentGoal;
        this.sortStacksByUtility(); // start with highest
        // Go through stacks in sorted order and try to run something.
        ListIterator<Goal> dle = this.goalStacks.listIterator(); // needed for .remove()
        while (dle.hasNext()) {
            theCurrentGoal = dle.next();
            if (theCurrentGoal.getStatus() == Goal.Status.ABANDONED) {
                if (this.log.getShowISorGoalList())
                    this.log.fine("Plan abandoned!  Removing intention!\n");
                theCurrentGoal.removeIntention(true);
                dle.remove();
            } else if (theCurrentGoal.getRuntimeState() == null) {
                if (this.log.getShowISorGoalList())
                    this.log.fine("Goal \"" + theCurrentGoal.getName()
                            + "\" has no runtime state, skipping.\n");
                // top goal but no runtimestate... reduce utility more than normal (autoboredom)
                //theCurrentGoal.reduceUtility(); // dont
            } else { // there is a runtime state
                this.currentGoal = theCurrentGoal; // to show where we are (should be nulled if goal removed)
                // If this is an achievement goal (currently ACHIEVE or MAINTAIN) then
                // check to see if the goal's already been achieved.
                if (theCurrentGoal.getGoalAction() != null
                    && (theCurrentGoal.getGoalAction() instanceof AchieveGoalAction
                        || theCurrentGoal.getGoalAction() instanceof MaintainGoalAction)) {
                    Relation rel = theCurrentGoal.getGoalAction().getRelation();
                    // Binding argument is null in the match() function below because this is
                    // a top-level goal and therefore has no variable bindings
                    if (this.worldModel.match(rel, null)) {
                        if (this.log.getShowISorGoalList())
                            this.log.fine("Goal \"" + theCurrentGoal.getName() + " already achieved!.\n");
                        theCurrentGoal.setRuntimeState(null);
                        // MAINTAIN goals should stick around on the Intention Structure
                        // (until, at least, it is explicitly removed by the programmer using an UNPOST)
                        if (theCurrentGoal.getGoalAction() != null
                            && !(theCurrentGoal.getGoalAction() instanceof MaintainGoalAction)) {
                            dle.remove();
                            this.currentGoal = null; // currentgoal is finished and removed
                        }
                        theCurrentGoal.setStatus(Goal.Status.SUCCESS);
                        //this.reduceUtilities();
                        this.renewLeafGoals();
                        return Goal.Status.SUCCESS;
                    }
                }
                // Check to see if the plan's context is still valid
                if (theCurrentGoal.confirmContext()) { // strangely negated return value ???
                    if (this.log.getShowISorGoalList())
                        this.log.fine("Plan context failed!  Removing intention!\n");
                    theCurrentGoal.removeIntention(true);
                    theCurrentGoal.setStatus(Goal.Status.FAILURE);
                    theCurrentGoal.setSubgoal(null);
                    theCurrentGoal.reduceUtility();
                    this.renewLeafGoals();
                    return Goal.Status.FAILURE;
                }
                // As soon as something executes (successfully or not) then return.
                if (this.log.getShowISorGoalList())
                    this.log.info(
                        "Executing top-level goal \""
                            + theCurrentGoal.getName()
                            + "\" (plan named \""
                            + theCurrentGoal.getIntention().getPlan().getName()
                            + "\".");
                //already done above: //this.currentGoal = theCurrentGoal;
                PlanRuntimeState.State returnValue = theCurrentGoal.execute();
                switch (returnValue) {
                    case CONSTRUCT_FAILED :
                        if (this.log.getShowISorGoalList())
                            this.log.fine("Top-level goal \"" + theCurrentGoal.getName() + "\" failed!");
                        if (theCurrentGoal.getIntention().getPlan().getFailure() != null) {
                            theCurrentGoal.executeFailure();
                        }
                        theCurrentGoal.removeIntention(false);
                        theCurrentGoal.setStatus(Goal.Status.FAILURE);
                        theCurrentGoal.setSubgoal(null);
                        theCurrentGoal.reduceUtility();
                        this.renewLeafGoals();
                        return Goal.Status.FAILURE;
                    case CONSTRUCT_COMPLETE :
                        if (this.log.getShowGoalList())
                            this.log.fine("Just completed top-level goal " + theCurrentGoal.getName());
                        theCurrentGoal.setRuntimeState(null);
                        // Execute the plan's "postcondition" (i.e., after-effects)
                        if (theCurrentGoal.getIntention().getPlan().getEffects() != null) {
                            theCurrentGoal.executeEffects();
                        }
                        // If a MAINTAIN goal, then leave on the intention structure to
                        // continue monitoring for the required state but assert that the
                        // goal has been achieved. Otherwise, remove the goal from the
                        // intention structure and remove it's particular intention.
                        if (theCurrentGoal.getGoalAction() == null
                            || (theCurrentGoal.getGoalAction() != null
                                && !(theCurrentGoal.getGoalAction() instanceof MaintainGoalAction))) {
                            dle.remove();
                            this.currentGoal = null; // currentgoal is done
                        }
                        // Stefan Rank: excluded MAINTAIN goals from the following posting of the 'result'
                        // i use them for everlasting goals that try to achieve external stuff
                        // setting the intention to null must be done (should actually be done for all):
                        theCurrentGoal.setIntention(null);
                        if (theCurrentGoal.getGoalAction() instanceof AchieveGoalAction) {
                            // Assert achieved goal state onto World Model
                            Relation rel = (theCurrentGoal.getGoalAction().getRelation());
                            this.worldModel.assertRelation(rel, null);
                        }
                        if (theCurrentGoal.getGoalAction() instanceof MaintainGoalAction) {
                            theCurrentGoal.setStatus(Goal.Status.UNTRIED); // maintain means try again
                        } else {
                            theCurrentGoal.setStatus(Goal.Status.SUCCESS);
                        }
                        this.renewLeafGoals();
                        return Goal.Status.SUCCESS;
                    case CONSTRUCT_INCOMP :
                        this.renewLeafGoals();
                        theCurrentGoal.setStatus(Goal.Status.ACTIVE);
                        return Goal.Status.ACTIVE;
                    default :
                        this.log.warning("JAM: Execution returned invalid value: " + returnValue);
                }
            }
        }
        this.renewLeafGoals();
        return Goal.Status.ACTIVE;
    }

    /** Perform an agent's plan */
    public Goal.Status executePlan(Plan plan, Binding b) {
        if (plan == null) {
            return Goal.Status.SUCCESS;
        }
        if (b == null) {
            b = new Binding(plan.getSymbolMap());
        }
        PlanRuntimeState toplevelState = plan.getBody().newRuntimeState();
        while (toplevelState != null) {
            PlanRuntimeState.State returnVal = toplevelState.execute(b, null);
            switch (returnVal) {
                case CONSTRUCT_FAILED :
                    this.log.severe("IntentionStructure:executePlan - Plan failed\n"
                               + plan.verboseString(b));
                    return Goal.Status.FAILURE;
                case CONSTRUCT_COMPLETE :
                    return Goal.Status.SUCCESS;
                case CONSTRUCT_INCOMP :
                    break;
                default :
                    this.log.warning("JAM::IntentionStructure:executePlan - "
                                + "Plan returned unknown state: " + returnVal + "!\n"
                                + plan.verboseString(b));
                    break;
            }
        }
        return Goal.Status.SUCCESS;
    }

    /** Go through all of the stacks and confirm the context */
    private void confirmContexts() {
        APLElement intent;
        for (Goal subGoal: this.goalStacks) {
            while (subGoal != null) {
                intent = subGoal.getIntention();
                if (intent != null) {
                    intent.getPlan().confirmContext(subGoal.getIntentionBinding());
                }
                subGoal = subGoal.getSubgoal();
            }
        }
    }

    /** Go through all of the stacks and mark all inactive goals as being "new" in order to trigger APL generation.
     */
    public void renewLeafGoals() {
        if (this.log.getShowGoalList())
            this.log.fine("renewing leaf goals.");
        synchronized (this.goalStacks) {
            for (Goal subGoal : this.goalStacks) {
                while (subGoal != null) {
                    if (subGoal.getSubgoal() == null || subGoal.getIntention() == null) {
                        subGoal.setNew(true);
                    }
                    subGoal = subGoal.getSubgoal();
                }
            }
        }
    }

    /** Go through all of the stacks and reduce their utility (~autoboredom, Moffat) */
    private void reduceUtilities() {
        if (this.log.getShowGoalList())
            this.log.fine("reducing utilities of toplevelgoals.");
        synchronized (this.goalStacks) {
            for (Goal goal : this.goalStacks) {
                goal.reduceUtility();
            }
        }
    }

    /** Old GoalList functionality */
    public boolean allGoalsDone() {
        return this.goalStacks.size() == 0;
    }

    /** Find and remove a goal from the Intention Structure */
    synchronized public void drop(GoalAction goalAction, Binding b) {
        // Find the goal and remove it
        if (this.log.getShowISorGoalList())
            this.log.info("IntentionStructure::drop:Dropping goal: " + goalAction.verboseString(b, "", ""));
        // Go through all the stacks
        Iterator<Goal> iter = this.goalStacks.listIterator(); // needed for .remove()
        while (iter.hasNext()) {
            Goal aGoal = iter.next();
            // Go through all the subgoals
            while (aGoal != null && !aGoal.matchGoal(goalAction, b)) {
                //_log.info("Matching against goal: ");
                //aGoal.getGoalAction()
                aGoal = aGoal.getSubgoal();
            }
            // See if we found a match
            if (aGoal != null) {
                if (this.log.getShowISorGoalList())
                    this.log.fine("Goals matched, ");
                if (aGoal.getIntention() != null) {
                    if (this.log.getShowISorGoalList())
                        this.log.fine("marking as abandoned.");
                    aGoal.setStatus(Goal.Status.ABANDONED);
                    return;
                }
                // Goal not being executed, so take it off the intention structure
                if (this.log.getShowISorGoalList()) {
                    this.log.fine("removing goal from stack");
                }
                iter.remove();
            }
        }
    }

    public Goal addUnique(GoalAction goalAction) {
        return this.addUnique(goalAction, null, null);
    }

    /** Add the specified goal to the intention structure only if it doesn't already exist. */
    synchronized public Goal addUnique(GoalAction goalAction, Goal prevGoal, Binding b) {
        assert(goalAction != null) : "GoalDrivenGoal needs a GoalAction";
        // ************* Need to clean this up into other classes *******
        // not a conclude goal so
        // go through all the stacks and check all goals for matches
        for (Goal aGoal : this.goalStacks) {
            // go through all subgoals too
            while (aGoal != null) {
                // SP: extended this condition, so that the two goals need the same
                // prevGoals too, since don't posting it because the goals are matching
                // can lead to a blocking goal (not really blocked but without further
                // execution)
                if (aGoal.matchGoal(goalAction, b) && aGoal.getPrevGoal() == prevGoal) {
                    return aGoal;
                }
                aGoal = aGoal.getSubgoal();
            }
        }
        // its a new goal wo add it
        GoalAction newAction = goalAction;
        if (prevGoal == null) {
            // Create a new goal action where the goal arguments are grounded
            // (i.e., everything is converted to simple values)
            Relation newRelation = goalAction.getRelation();
            ExpList newExpList = new ExpList();
            for (Expression e : goalAction.getRelation().getArgs()) {
                newExpList.addLast(Value.newValue(e.eval(b)));
            }
            newRelation = new Relation(goalAction.getRelation().getName(), newExpList);

            if (goalAction instanceof AchieveGoalAction) {
                newAction =
                    new AchieveGoalAction(
                        newRelation,
                        goalAction.getUtility(),
                        goalAction.getBy(),
                        goalAction.getNotBy());
            } else if (goalAction instanceof PerformGoalAction) {
                newAction =
                    new PerformGoalAction(
                        newRelation,
                        goalAction.getUtility(),
                        goalAction.getBy(),
                        goalAction.getNotBy());
            } else if (goalAction instanceof MaintainGoalAction) {
                newAction = new MaintainGoalAction(newRelation, goalAction.getUtility());
            } else if (goalAction instanceof QueryGoalAction) {
                newAction = new QueryGoalAction(newRelation, goalAction.getUtility());
            }
            newAction.getRelation().evalArgs(b);
        }
        ++this.numGoals;
        Goal newGoal = new GoalDrivenGoal(newAction, prevGoal, this);
        // Not a duplicate goal, so add at top-level if no previous goal, otherwise
        // its already been added to current intention stack (through the Goal constructor).
        if (prevGoal == null) {
            if (this.log.getShowGoalList())
                this.log.info("JAM::IntentionStructure:addUnique(): Adding new top-level goal " + newGoal.getName());
            this.goalStacks.addLast(newGoal);
        } else {
            if (this.log.getShowGoalList())
                this.log.info(
                    "JAM::IntentionStructure:addUnique(): Adding subgoal "
                        + newGoal.getName()
                        + " to goal "
                        + prevGoal.getName());
        }
        return newGoal;
    }

    /** Add the specified goal to the intention structure only if it doesn't already exist. */
    synchronized public Goal addUnique(Relation concludeRel, Binding b) {
        // its a conclude goal
        assert(concludeRel != null) : "DatDrivenGoal needs a concludeRelation";
        // make a save copy (bindings resolved)
        ExpList newExpList = concludeRel.getArgs().explistEval(b);
        Relation newRelation = new Relation(concludeRel.getName(), newExpList);
        // check if its already there
        for (Goal aGoal : this.goalStacks) {
            // dont go through subgoals, conclude-goals are always toplevel
            if (Relation.unify(aGoal.getConcludeRelation(), null, newRelation, null)) {
                return aGoal;
            }
        }
        //aGoal = new Goal(null, concludeRel, null, this);
        Goal theGoal = new DataDrivenGoal(newRelation, this);
        if (this.log.getShowGoalList()) {
            this.log.info(
                "IntentionStructure:addUnique(): Adding new data-driven top-level goal "
                    + theGoal.getConcludeRelation().getName());
        }
        this.goalStacks.addLast(theGoal);
        return theGoal;
    }

    /** Remove the indicated goal by searching through each intention stack and going through each from top to bottom. */
    synchronized public void removeGoal(Goal goal) {
        ListIterator<Goal> stacks = this.goalStacks.listIterator(); // needed for .remove()
        Goal stackGoal;
        while (stacks.hasNext()) {
            stackGoal = stacks.next();
            // Go through stack from top to bottom
            while (stackGoal != null) {
                // If we found the goal then either castrate the subgoal chain
                // at the parent or, if a top-level goal, remove the entire
                // intention stack.
                if (stackGoal == goal) {
                    stackGoal.setStatus(Goal.Status.ABANDONED);
                    if (stackGoal.getPrevGoal() != null) {
                        stackGoal.getPrevGoal().setSubgoal(null);
                    } else {
                        stacks.remove();
                    }
                    return;
                }
                stackGoal = stackGoal.getSubgoal();
            }
        }
    }

    /** Output information about the Intention Structure in a readable format. */
    synchronized public String verboseString() {
        StringBuffer sb = new StringBuffer();
        sb.append("IntentionStructure:" + "\n");
        sb.append("  Stacks: " + this.goalStacks.size() + "\n");
        for (Goal subGoal : this.goalStacks) {
            sb.append("\n");
            // Go through all the subgoals
            while (subGoal != null) {
                sb.append(subGoal.formattedString());
                if (subGoal.getIntention() != null)
                    sb.append(subGoal.getIntention().verboseString());
                subGoal = subGoal.getSubgoal();
            }
        }
        sb.append("\n");
        return sb.toString();
    }

/*
    public void addFailedGoal(Goal g){
        failedGoals.add(g);
    }
*/
    public void addRetryGoal(Goal g){
        retryGoals.add(g);
    }
/*
    public void removeFailedGoals(){
        if (failedGoals.size() > 0) {
            for (Goal g : failedGoals) {
                this.removeGoal(g);
            }
            failedGoals.clear();
        }
    }
*/
    public void resetRetryGoals(){
        if (retryGoals.size() > 0) {
            for (Goal g : retryGoals) {
                g.removeIntention(false);
                g.setStatus(Goal.Status.UNTRIED);
                g.setNew(true);
            }
            retryGoals.clear();
        }
    }
}
