package at.oefai.aaa.agent.jam;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import at.oefai.aaa.agent.jam.types.ExpList;
import at.oefai.aaa.agent.jam.types.Value;

/**
 * A basic non-instantiable implementation of the interpreting stuff.
 * @author Stefan Rank
 */
public abstract class AbstractInterpreter implements Interpreter {
    // a logger for info and debug output, root logger for a start
    private final AgentLogger log;
    private final String name;

    // agent functionality parts
    private final PlanTable planLibrary;
    private final WorldModelTable worldModel;
    private final IntentionStructure intentionStructure;
    private final Appraiser appraiser;
    private final Functions systemFunctions;
    private final Functions userFunctions;
    private final MetaData metaData;
    // Mobility flag
    private boolean agentHasMoved = false;

    /** Constructor, only takes a name. */
    public AbstractInterpreter(final String pName) {
        this.name = pName;
        this.log = new AgentLogger(this.name);
        this.planLibrary = new PlanTable();
        this.worldModel = new WorldModelTable(this.log);
        this.intentionStructure = new IntentionStructure(this);
        this.appraiser = new Appraiser(this);
        this.systemFunctions = new SystemFunctions(this);
        this.userFunctions = new UserFunctions(this);
        this.metaData = new MetaData(this);
    }


    /**
     * Constructor, creates an interpreter called pName and
     * considers the options in argv and interprets the rest as jam-files to parse.
     * @param name mynameis
     * @param argv options and jam-files
     * @throws ParseException
     * @throws IOException
     */
    public AbstractInterpreter(final String pName, final String[] argv) throws ParseException, IOException {
        this(pName);
        List<String> args = new ArrayList<String>(Arrays.asList(argv));
        List<String> pStrings = parseArguments(args);
        // Parse remaining Strings as Jam agent definition files (filenames)
        parseFiles(args);
        // Parse command-line Jam agent definitions
        for (String s : pStrings) {
            parseString(s);
        }
    }

    /**
     * Parses the string arguments for unix style args (starting with -).
     * ignores all others
     * ATTENTION: as a side effect removes all considered options from args
     * @param args List of Strings (options are removed, simple filenames remain)
     * @return List of Strings containing all parsable Strings (-p option)
     */
    private List<String> parseArguments(List<String> args) {
        List<String> pStrings = new ArrayList<String>();
        Iterator<String> argIter = args.iterator(); // removes from arg using the iterator (no for-each)
        while (argIter.hasNext()) {
            String currArg = argIter.next();
            if (currArg.charAt(0) == '-') {
                switch (currArg.charAt(1)) {
                    case 'f':
                        this.log.setShowActionFailure(true);
                        break;
                    case 'g':
                        this.log.setShowGoalList(true);
                        break;
                    case 'i':
                        this.log.setShowIntentionStructure(true);
                        break;
                    case 's':
                    case 'a':
                        this.log.setShowAPL(true);
                        break;
                    case 'w':
                        this.log.setShowWorldModel(true);
                        break;
                    case 'p':
                        // Save the parse string to parse after file list.  Read and
                        // save between quotation marks.
                        pStrings.add(currArg.substring(2));
                        this.log.config("*** JAM: Parsable command-line string found. ***\n");
                        break;
                    default:
                        this.log.config("*** JAM: Ignoring unknown flag '" + currArg.charAt(1) + "' ***\n");
                        break;
                }
                argIter.remove();
            }
        }
        return pStrings;
    }


    // Member functions

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

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

    public PlanTable getPlanLibrary() {
        return this.planLibrary;
    }

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

    public IntentionStructure getIntentionStructure() {
        return this.intentionStructure;
    }

    public Functions getSystemFunctions() {
        return this.systemFunctions;
    }

    public Functions getUserFunctions() {
        return this.userFunctions;
    }

    public MetaData getMetaData() {
        return this.metaData;
    }


    public boolean getAgentHasMoved() {
        return this.agentHasMoved;
    }

    public void setAgentHasMoved(final boolean flag) {
        this.agentHasMoved = flag;
    }

    public void simpleAssert(final String relationName, final String[] args) {
        ExpList ex = new ExpList();
        for (int i = 0; i < args.length; i++) {
            ex.add(Value.newValue(args[i]));
        }
        Relation rel = new Relation(relationName, ex);
        getWorldModel().assertRelation(rel, null);
    }




    /** Parse the list of files for plans, goals, world model entries, and Observer specifications. */
    public void parseFiles(final List<String> argv) throws IOException, ParseException {
        for (String argString : argv) {
            //fileBuf.setLength(0);
            if (argString != null) {
                Reader fr = new BufferedReader(new FileReader(argString));
                this.log.config("Interpreter::parse: Building interpreter from: " + argString);
                JAMParser.parse(fr, this, argString);
                fr.close();
            }
        }
    }

    /** Parse the string for plans, goals, world model entries, and Observer specifications. */
    public void parseString(final String pString) throws ParseException {
        JAMParser.parse(pString, this, null);
    }


    // Execution members and methods:

    private boolean assertsAPLs = true;
    private boolean appraises = true;

    // statistics:
    private Date runStart;
    private Date runEnd;
    private long runTotal = 0;
    private Date observerStart;
    private Date observerEnd;
    private long observerTotal = 0;
    private Date aplStart;
    private Date aplEnd;
    private long aplTotal = 0;
    private Date planExecutionStart;
    private Date planExecutionEnd;
    private long planExecutionTotal = 0;
    private Date intendStart;
    private Date intendEnd;
    private long intendTotal = 0;

    private int numAPLs = 0;
    private int numNullAPLs = 0;
    private int numCycles = 0;


    public void setAppraises(final boolean flag) {
        this.appraises = flag;
    }

    public void setAssertsAPLs(final boolean flag) {
        // should actually be more like 'doesMetaLevelReasoning'
        this.assertsAPLs = flag;
    }

    public void cycleInit() {
        if (this.runStart == null) {
            this.runStart = new Date();
        }
        ++this.numCycles;
        // Execute the "execute" body here when it's implemented
        // returnValue = _intentionStructure.executePlan(_execute);
        // Execute the Observer procedure
        execObserver();
        // insert the appraisal step here?
        if (this.appraises) {
            this.appraiser.appraise();
        }
    }

    public void execObserver() {
        this.observerStart = new Date();
        this.intentionStructure.executePlan(this.planLibrary.getObserver(), null); // ignores returnvalue
        this.observerEnd = new Date();
        this.observerTotal += this.observerEnd.getTime() - this.observerStart.getTime();
    }

    public void cycleDo() {
        // Loop until agent completes metalevel "ramp up"
        int metaLevel = 0;
        APL lastAPL = null;
        // ugly ugly synchronized block to avoid having added a request while generating apls
        // which then ages with advanceAllAges and would not spawn a conclude goal...
        synchronized (this.worldModel) {
            // first APL is special, it then calls advancesAllAges of WorldModelRelations
            APL apl = generateAPL(metaLevel);
            // advanceAllAges() nur einmal pro cycle (=mehrere levels)
            // spaetere levels verwenden advanceNewWMRAges() siehe unten --StefanRank
            this.worldModel.advanceAllAges();
            while (true) { // metalevel loop
                int aplSize = apl.getSize();
                //this.log.info("numCycles=" + this.numCycles + " APL-size=" + aplSize + " metaLevel=" + metaLevel);
                // If the new APL is not empty then add entry to the
                // World Model to trigger the next level of reasoning.
                if (aplSize > 0) {
                    if (this.assertsAPLs) { // may be needed for metalevel reasoning
                        assertGeneratedAPL(metaLevel, apl, aplSize);
                    }
                    if (this.log.getShowAPL()) {
                        this.log.info("JAM: APL generated: " + apl.verboseString()); }
                    lastAPL = apl;
                    metaLevel++;
                    if (this.log.getShowAPL()) {
                        this.log.info("JAM: Metalevel now at: " + metaLevel); }
                    // Generate a new Applicable Plan List (APL), next metalevel
                    apl = generateAPL(metaLevel);
                    // now advance only brandnew WMR-ages, so that also asserted APL facts get older.
                    this.worldModel.advanceNewWMRAges();
                    // onwards to higher ground (the next metalevel)
                } else { // If this APL is empty then no new level of reasoning
                    ++this.numNullAPLs;
                    if ((this.intentionStructure.allGoalsDone())) {
                        // Make this run-time configurable to an alternative, where
                        // the system keeps running, even after there are no goals.
                        cycleAllGoalsDone();
                        return;
                    }
                    // this finishes this cycle, the relation ages were already advanced above
                    // (after appraising and APL generation but before think, which could add new achieve relations)
                    // If the previous APL was not empty, select something from the previous
                    // APL, and intend it.
                    if (lastAPL != null && lastAPL.getSize() != 0) {
                        //log.info("APL: last_APL was NOT empty, picking element.\n");
                        pickIntention(lastAPL);
                    }
                    // then execute something in the intention structure
                    this.planExecutionStart = new Date();
                    this.intentionStructure.think();
                    this.planExecutionEnd = new Date();
                    this.planExecutionTotal += this.planExecutionEnd.getTime() - this.planExecutionStart.getTime();
                    //this.intentionStructure.removeFailedGoals();
                    this.intentionStructure.resetRetryGoals();
                    lastAPL = null;
                    cycleEnd();
                    return;
                }
            } // Inner, metalevel loop
        }
    }

    public void cycleAllGoalsDone() {
        StringBuffer sb = new StringBuffer();
        sb.append("\nJAM: All of the agent's top-level goals have been achieved!  Returning...");
        sb.append("\nRuntime statistics follow:\n");
        sb.append("  Number of APLs generated:\t" + this.numAPLs + "\n");
        sb.append("  Number of Null APLs:\t\t" + this.numNullAPLs + "\n");
        sb.append("  Number of Goals established:\t" + this.intentionStructure.getNumGoalsStat() + "\n");
        sb.append("  Number of interpreter cycles:\t" + this.numCycles + "\n");
        this.runEnd = new Date();
        this.runTotal = this.runEnd.getTime() - this.runStart.getTime();
        sb.append("\n  APL generation time:\t\t" + this.aplTotal / 1000.0 + " seconds." + "\n");
        sb.append("  Intending time:\t\t" + this.intendTotal / 1000.0 + " seconds." + "\n");
        sb.append("  Plan execution time:\t\t" + this.planExecutionTotal / 1000.0 + " seconds." + "\n");
        sb.append("  Observer execution time:\t" + this.observerTotal / 1000.0 + " seconds." + "\n");
        sb.append("  Total run time:\t\t" + this.runTotal / 1000.0 + " seconds." + "\n");
        getLog().info(sb.toString());
    }

    public void cycleEnd() {
        // NOTE: Any metalevel plan should do the necessary WM clearing.
        // However, if there is no metalevel plans then we need to clear
        // the World Model.
        if (this.assertsAPLs) { // only if we did it
            Relation rel = new Relation("APL", null);
            getWorldModel().retractRelation(rel, null);
        }
    }

    private APL generateAPL(final int metaLevel) {
        this.aplStart = new Date();
        APL apl = new APL(this.planLibrary, this.worldModel, this.intentionStructure, metaLevel);
        this.aplEnd = new Date();
        this.aplTotal += this.aplEnd.getTime() - this.aplStart.getTime();
        ++this.numAPLs;
        if (this.log.getShowAPL()) {
            this.log.info("APL generated has " + apl.getSize() + " element(s) at metalevel " + metaLevel);
        }
        return apl;
    }

    private void assertGeneratedAPL(final int metaLevel, final APL apl, final int aplSize) {
        ExpList explist = new ExpList();
        explist.addLast(Value.newValue(metaLevel));
        explist.addLast(Value.newValue(apl));
        explist.addLast(Value.newValue(aplSize));
        Relation rel = new Relation("APL", explist);
        getWorldModel().assertRelation(rel, null);
    }

    private void pickIntention(final APL lastapl) {
        this.intendStart = new Date();
        // selectedElement = apl.getRandom();
        // selectedElement = apl.getUtilityFirst();
        APLElement selectedElement = lastapl.getUtilityRandom();
        if (this.log.getShowAPLorIS()) {
            this.log.info("\nJAM: Selected plan \"" + selectedElement.getPlan().getName() + "\" from APL.\n"); }
        this.intentionStructure.intend(selectedElement);
        this.log.info("numCycles=" + this.numCycles + " pickIntention: " + selectedElement.getFromGoal()); // SP
        this.intendEnd = new Date();
        this.intendTotal += this.intendEnd.getTime() - this.intendStart.getTime();
        if (this.log.getShowIntentionStructure()) {
            this.log.info("\nJAM: Intended element, Intention structure now:"
                     + this.intentionStructure.verboseString()); }
    }

}
