package at.oefai.aaa.agent.jam;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

import at.oefai.aaa.agent.jam.types.Binding;
import at.oefai.aaa.agent.jam.types.DList;

/**
 * A JAM agent's Applicable Plans List (APL).
 * @author Marc Huber
 * @author Jaeho Lee
 */
public class APL implements Serializable {
    private final List<APLElement> intentions = new ArrayList<APLElement>();
    private final AgentLogger log;
    private static final Random RAND = new Random(new Date().getTime());

    /**
     * Generate an Applicable Plan List (APL) from the plans, current
     * state of the world model, and the goals on the intention structure.
     */
    APL(final PlanTable pt, final WorldModelTable wm, final IntentionStructure intentionStructure, final int metaLevel) {
        this.log = intentionStructure.getLog();
        if (this.log.getShowAPL()) this.log.info("JAM::APL: Intention Structure is:\n" + intentionStructure.verboseString() + "\n");
        // StefanRank: the following two commentary lines are obsoleted by advance*Ages() calls in AbstractInterpreter
        // Make sure to do the WM function first as the Goal function clears
        // "new" field in the world model entries.
        // SP: CONCLUDE plans in allen leveln, da sonst der Metalevel-Plan nicht ausgefuehrt wird
        this.intendWMBasedAPL(pt, wm, intentionStructure, metaLevel);
        if (metaLevel < 1) { // no (unnecessary?) goal plans on metalevels
            // Stefan Rank: im totally prohibiting apl generation for higher levels
            // because currently age of world model entries is advanced after the ramp up#
            // but it is also the only way to prevent the same plans getting selected
            // at higher meta level stages
            // (age was advanced at the end of genGoalBasedAPL with clearnew)
            this.genGoalBasedAPL(pt, wm, intentionStructure, metaLevel);
        }
    }


    /** Find the APL elements based on goal matches. */
    private void genGoalBasedAPL(final PlanTable pt, final WorldModelTable wm,
                                 final IntentionStructure intentionStructure, final int metaLevel) {
        // Go through each APL ready goal
        for (Goal goal : intentionStructure.getSubGoalsForAPL()) {
            // Don't waste time generating an APL if it's a satisfied MAINTAIN goal
            // or if it's a MAINTAIN goal and agent is performing metalevel reasoning
            if (goal.getGoalAction() instanceof MaintainGoalAction) {
                if (wm.match(goal.getGoalAction().getRelation(), null)) {
                    //if (_log.getShowAPL()) _log.info("APL::genGoalBasedAPL: Skipping satisfied MAINTAIN goal\n");
                    continue;
                }
                if (metaLevel >= 1) {
                    //if (_log.getShowAPL()) _log.info("APL::genGoalBasedAPL: Skipping MAINTAIN goal while doing meta-reasoning\n");
                    continue;
                }
            }
            //this.log.info("GoalForAPL: " + goal.toString());
            // now get the matching plans:
            // Go through each possible plan
            for (Plan plan : pt.getGoalPlans(goal.getName())) {
                Binding planBinding = new Binding(plan.getSymbolMap());
                // Filter according to goal type and by ":BY" and ":NOT-BY" lists
                if (goal.getGoalAction() == null || (goal.getGoalAction() != null
                        && !goal.getGoalAction().isEligible(plan, planBinding))) {
                    if (this.log.getShowAPL()) this.log.info("APL::genGoalBasedAPL: plan " + plan + "(" + plan.getName() + ")"
                            + " with planBinding " + planBinding + ", is not eligible for goal " + goal);
                } else {
                    // Now go through each possible plan variable binding and
                    // unify goal and plan relations
                    Relation planRelation = plan.getGoalSpecification().getRelation();
                    if (goal.matchRelation(planRelation, planBinding)) {
                        if (this.log.getShowAPL()) this.log.info("APL::genGoalBasedAPL: instantiating plan " + plan
                                + " with planBinding " + planBinding.formattedString() + ", for goal "
                                + goal.toString());
                        instantiate(plan, planBinding, goal);
                    }
                }
            }
            // Maintain goals should always be considered for APL generation.
            // Once a Maintain goal gets intended, then further APL generation
            // will be passed over by the generateAPL() member function above
            // because of the existance of the intention.
            if (goal.getGoalAction() != null && (!(goal.getGoalAction() instanceof MaintainGoalAction))) {
                goal.setNew(false);
            }
        }
    }

    /** Find the APL elements based on world model matches */
    private void intendWMBasedAPL(final PlanTable pt, final WorldModelTable wm, final IntentionStructure intentionStructure,
                                  final int metaLevel) {
        // Do a quick check to see if anything changed in the World Model.
        // If not then there's nothing new to match so simply return.
        if (! wm.anyNew()) {
            if (this.log.getShowAPL()) this.log.info("APL::genWMBasedAPL: No new World Model entries.");
            return;
        }
        // for each CONCLUDE plan
        Iterator<Plan> planIter = pt.getConcludePlans().iterator();
        while (planIter.hasNext()) {
            Plan plan = planIter.next();
            //this.log.info("Conclude-Plan: " + plan.getName() + " in metalevel " + metaLevel); // SP
            Relation concludeRelation = plan.getConcludeSpecification();
            if (this.log.getShowAPL()) this.log.info("APL::genWMBasedAPL: Checking plan: " + plan.getName() + "\n" +
                         "          w/ CONCLUDE relation of: " + concludeRelation.getName());

            // check to see if the concludeRelation matches a newly changed WM entry.
            Binding planBinding = new Binding(plan.getSymbolMap());
            DList<Binding> bl = wm.getWMBindingList(planBinding, concludeRelation, true);
            if (this.log.getShowAPL()) this.log.info("APL::genWMBasedAPL: BindingList from WorldModel is " + bl.size()
                         + " elements long.\n");
            // If no matches then continue with next plan;
            if (bl.size() == 0) {
                //this.log.info("No Binding for Conclude-Plan: " + plan.getName()); // SP
                continue;
            }
            // If at least one match then check context and add to APL if context passes
            if (plan.checkContext(bl) && plan.checkPrecondition(bl)) {
                // binding list now changed by context and precondition
                for (Binding b : bl) {
                    //_log.infoAPL("APL::genWMBasedAPL: Adding APL element w/ plan: " + plan.getName()
                    //    + " and binding: " + b.formattedString() + "\n");
                    // Create a temporary goal to "hold" the intention on the intention structure
                    //_log.infoAPLorIntentionStructure(
                    //    "APL::genWMBasedAPL: Adding \"CONCLUDE\" goal to IntentionStructure. Relation is: " +
                    //    concludeRelation.verboseString(b));
                    // Maybe I should wait to add a goal to the Intention Structure until the
                    // plan is actually intended??
                    Goal g = intentionStructure.addUnique(concludeRelation, b);
                    // Stefan Rank: changed...
                    //add(plan, g, b);
                    // ...to directly intending the plan as otherwise it could be missed later
                    // the goal would remain without intention. as Conclude plans dont have any
                    // utility specified (always 0) and only new/changed wm entries are considered here
                    // this cant be influenced otherwise..
                    if(metaLevel == 0) { // only for the normal CONCLUDE plans: intend directly
                        intentionStructure.intend(new APLElement(plan, g, b));
                        if (this.log.getShowAPL())
                            this.log.info("intend CONCLUDE-Plan: " + g + " in metalevel " + metaLevel); // SP
                    } else { // normal APL adding, and selection
                        add(plan,g,b);
                        if (this.log.getShowAPL())
                            this.log.info("add CONCLUDE-Plan: " + g + " in metalevel " + metaLevel); // SP
                    }
                }
            } else {
                if (this.log.getShowAPL()) this.log.info("APL::genWMBasedAPL: Plan: \"" + plan.getName()
                                               + "\" did not pass context/precondition check.");
            }
        }
    }

    /** Append an applicable plan onto the list of possibilities. */
    private APLElement add(Plan p, Goal g, Binding b) {
        APLElement se = new APLElement(p, g, b);
        this.intentions.add(se);
        return se;
    }

    /** Remove the given element from the APL. */
    public void remove(final APLElement a){
        int oldSize = this.getSize();
        if (intentions.remove(a)) {
            if (this.log.getShowAPL()) this.log.info("removed Element from APL (before " + oldSize
                    + " now " + this.getSize() + " elements)");
        }
    }

    /** Determine the number of applicable plans. */
    int getSize() {
        return this.intentions.size();
    }


    /** Retrieve the nth element in the list (1-based). */
    APLElement getNth(int num) {
        return (this.intentions.size() >= num && num > 0) ? this.intentions.get(num - 1) : null;
    }


    /** Retrieve a random applicable plan from a list of those with the highest utility. */
    APLElement getUtilityRandom() {
        if (getSize() == 0) {
            return null;
        }
        double p = 0;
        double maxUtility = Double.NEGATIVE_INFINITY;
        List<APLElement> maxAPs = new ArrayList<APLElement>(getSize()); // to hold those with max
        //_log.infoAPL("APL: Searching through " + getSize() + " plans for maximal utility");
        // use getNth (1-based) which gives null when out of bounds (no exception)
        for (APLElement intention : this.intentions) {
            p = intention.evalUtility();
            if (p > maxUtility) {
                maxUtility = p;
                maxAPs.clear();
                maxAPs.add(intention);
            } else if (Math.abs(p - maxUtility) < Double.MIN_VALUE * 2) {
                // equal in the rounding error range
                // statt: } else if (p == maxUtility) {
                maxAPs.add(intention);
            }
        }
        //_log.infoAPL("APL: " + maxAPs.size() + " plans found with maxUtility of: " + maxUtility);
        // choose random one of the maxAPs
        return maxAPs.get(Math.abs(RAND.nextInt()) % maxAPs.size());
    }

    /** Go through and find all combinations of variable bindings for the plan/goal combination. */
    private void instantiate(Plan plan, Binding planBinding, Goal goal) {
        DList<Binding> bindingList = new DList<Binding>(planBinding);
        //if (_log.getShowAPL()) _log.info("Checking bindingList against context and precondition expressions.");
        if (plan.checkContext(bindingList)) {
            if (plan.checkPrecondition(bindingList)) {
                for (Binding b : bindingList) {
                    if (goal.isNew() || b.isNewWMBinding()) {
                        if (this.log.getShowAPL()) this.log.info("APL::instantiate: instantiating APL for plan: " + plan + ":\""
                            + plan.getName() + "\"\n" + "goal: " + goal.toString() + " and binding: "
                            + b.verboseString());
                        add(plan, goal, b);
                    }
                }
            }
        }
    }

    /** Display information about the applicable plans. */
    String verboseString() {
        StringBuffer sb = new StringBuffer("Applicable Plan List:\nSize: " + this.intentions.size() + "\n");
        for (APLElement intention : this.intentions) {
            sb.append(intention.verboseString());
        }
        return sb.toString();
    }

    // returns all Intentions
    List<APLElement> getIntentions(){
        return this.intentions;
    }
}
