package at.oefai.aaa.agent.jam;

import java.io.Serializable;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

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.IndexedMap;
import at.oefai.aaa.agent.jam.types.SymbolMap;
import at.oefai.aaa.agent.jam.types.Value;
import at.oefai.aaa.agent.jam.types.Variable;

/**
 * A JAM agent's knowledge about the world.
 * @author Marc Huber
 * @author Jaeho Lee
 * @author Stefan Rank
 */
public class WorldModelTable implements Serializable {
    private final AgentLogger log; // a logger for info and debug output
    // table stores a list of worldmodelrelations per string
    private final IndexedMap<String,WorldModelRelation> table =
        new IndexedMap<String,WorldModelRelation>();
    private final List<WorldModelTableEventListener> listenerList =
        Collections.synchronizedList(new LinkedList<WorldModelTableEventListener>());

    WorldModelTable(final AgentLogger logger) {
        this.log = logger;
    }

    /**
     * Listener registration point: addWorldModelTableEventListener.
     * @param l the listener
     */
    public void addWorldModelTableEventListener(final WorldModelTableEventListener l) {
        this.listenerList.add(l);
    }

    /**
     * Unregistration of listeners: removeWorldModelTableEventListener.
     * @param l the listener
     */
    public void removeWorldModelTableEventListener(final WorldModelTableEventListener l) {
        this.listenerList.remove(l);
    }

    /**
     * Method fireWorldModelTaleEvent iterates through all listeneres, uses one event object for all.
     * @param type ASSERT or RETRACT
     * @param r the relation (is not cloned, only a reference is used !!!)
     */
    private void fireWorldModelTableEvent(final WorldModelTableEvent.Type type, final WorldModelRelation r) {
        if (this.listenerList.size() > 0) { // keep overhead for no listeners small
            assert (type == WorldModelTableEvent.Type.ASSERT || type == WorldModelTableEvent.Type.RETRACT);
            WorldModelTableEvent wmtEvent = new WorldModelTableEvent(this, type, r); // one event for all
            WorldModelTableEventListener[] listeners = new WorldModelTableEventListener[0];
            listeners = this.listenerList.toArray(listeners);
            for (int i = 0; i < listeners.length; ++i) {
                listeners[i].worldModelTableChanged(wmtEvent);
            }
        }
    }

    /** Return an array of all currently present entries as ASSERT events, synchronized. */
    public synchronized WorldModelTableEvent[] getAllCurrent() {
        WorldModelTableEvent[] wmtes = new WorldModelTableEvent[this.table.size()];
        for (int i = 0; i < this.table.size(); i++) {
            wmtes[i] = new WorldModelTableEvent(this, WorldModelTableEvent.Type.ASSERT,
                                                this.table.get(i));
        }
        return wmtes;
    }

    /** Return an array of all currently present relations, synchronized. */
    public synchronized WorldModelRelation[] getAllRelations() {
        WorldModelRelation[] wmrs = new WorldModelRelation[this.table.size()];
        for (int i = 0; i < this.table.size(); i++) {
            wmrs[i] = this.table.get(i);
        }
        return wmrs;
    }



    /** Check to see if any World Model entries match the specified relation and binding. */
    public synchronized boolean match(final Relation relation, final Binding binding) {
        for (WorldModelRelation wr : this.table.getBucket(relation.getName())) {
            if (wr.matchRelation(relation, binding)) {
                return true;
            }
        }
        return false;
    }

    /** Add a new World Model entry. */
    public synchronized void assertRelation(final Relation r, final Binding b) {
        if (!match(r, b)) {
            // Note: Creating a new relation with args evaluated
            Relation newR = new Relation(r, b);
            innerAssert(newR, false);
            if (this.log.getShowWorldModel()) { this.log.info("asserting relation: " + newR.verboseString(null)); }
        }

    }

    /** Add a new World Model entry as perception assuming the relation has no variables, only constants. */
    public synchronized void assertPerception(final Relation r) {
        if (!match(r, null)) {
            // Note: Creating a new relation and marking it as perception
            Relation newR = new Relation(r.getName(), r.getArgs());
            innerAssert(newR, true);
            if (this.log.getShowWorldModel()) { this.log.info("asserting perception: " + newR.verboseString(null)); }
        }
    }

    private void innerAssert(final Relation newR, final boolean isPerception) {
        WorldModelRelation wmr = new WorldModelRelation(newR, isPerception);
        wmr.setAsserted(true);
        this.table.put(wmr.getName(), wmr);
        fireWorldModelTableEvent(WorldModelTableEvent.Type.ASSERT, wmr); // tell those who want to know
    }


    /** Remove a World Model entry. */
    public synchronized void retractRelation(final Relation r, final Binding b) {
        for (WorldModelRelation wr : this.table.getBucket(r.getName())) {
            if (wr.matchRelation(r, b)) {
                if (this.log.getShowWorldModel()) { this.log.info("retracting relation:" + r.formattedString(b)); }
                this.table.remove(wr.getName(), wr);
                wr.setAsserted(false);
                fireWorldModelTableEvent(WorldModelTableEvent.Type.RETRACT, wr); // tell those who want to know
                break; // otherwise there might be a concurrent modification exception from next()
            }
        }

    }

    /** Remove a World Model entry (perception variant without binding). */
    public synchronized void retractPerception(final Relation r) {
        // retract the relation, if it wasnt a perception its bad luck for the relation
        // should have been named otherwise...
        retractRelation(r, null);
    }


    /** Change a World Model entry. */
    public synchronized void update(final Relation oldRel, final Relation newRel, final Binding b) {
        if (this.log.getShowWorldModel()) { this.log.info("JAM::WorldModel:update (via retract then assert)"); }
        this.retractRelation(oldRel, b);
        this.assertRelation(newRel, b);
    }

    /**
     * Returns a list of all possible bindings for matching the relation with current worldmodelrelations.
     * @param b th binding to start from
     * @param r the relation to match
     * @param newOnesOnly if true only bindings involving new entries are returned
     * @return a list of bindings
     */
    public synchronized DList<Binding> getWMBindingList(final Binding b, final Relation r, final boolean newOnesOnly) {
        DList<Binding> bl = new DList<Binding>();
        // Loop through all World Model entries matching the relation
        for (WorldModelRelation wr : this.table.getBucket(r.getName())) {
            Binding newBinding = new Binding(b);
            if (wr.matchRelation(r, newBinding)) {
                // check this binding
                if ((!newOnesOnly) || newBinding.isNewWMBinding()) {
                    bl.add(newBinding);
                }
            }
        }
        return bl;
    }


    /** See if there are ANY new World Model entries. */
    public synchronized boolean anyNew() {
        for (int i = 0; i < this.table.size(); i++) {
            if (this.table.get(i).isNew()) {
                return true;
            }
        }
        return false;
    }

    /** Set all World Model entries to be "one cycle older". */
    public synchronized void advanceAllAges() {
        for (int i = 0; i < this.table.size(); i++) {
            this.table.get(i).advanceAge();
        }
    }

    /** Set only the brandnew World Model entries to be "one cycle older".
     * (used in metalevel reasoning to prevent reconsideration of a previous APL) */
    public synchronized void advanceNewWMRAges() {
        for (int i = 0; i < this.table.size(); i++) {
            WorldModelRelation wmr = this.table.get(i);
            if (wmr.getAge() == 0) { // it is brandnew
                wmr.advanceAge();
            }
        }
    }


    /** Output information related to the World Model. */
    public synchronized String verboseString() {
        WorldModelRelation w;
        StringBuffer sb = new StringBuffer("World Model (" + this.table.size() + " entries) is now:" + "\n");
        for (int i = 0; i < this.table.size(); i++) {
            w = this.table.get(i);
            sb.append(i + ":[" + (w.isNew() ? "N" : " ") + (w.isPerception() ? "P" : " ") + "] : ");
            sb.append(w.formattedString(null));
            sb.append("\n");
        }
        return sb.toString();
    }

    /** Return the entire list of relations in the table that match the specified label. */
    public List<WorldModelRelation> getRelations(final String name) {
        return this.table.getBucket(name).shallowListClone();
    }

    public synchronized float getPreference(final String pName) {
        float pref = 0f;
        // search worldmodel for existing preference
        SymbolMap tmp = new SymbolMap();
        Binding tmpBinding = new Binding(tmp);
        ExpList el = new ExpList();
        Expression e = Value.newValue(pName);
        el.addLast(e);
        el.addLast(tmp.getVariable("tmpPreferenceVariable"));
        Relation rel = Relation.newPreferenceRelation(el);
        if (this.match(rel, tmpBinding)) {
            pref = (float) ((Variable) rel.getArgs().getLast()).eval(tmpBinding).getReal();
        }
        return pref;
    }

    public synchronized float changePreference(final String pName, final float pChange) {
        float pref = pChange;
        Relation oldRel = null;
        for (WorldModelRelation wr : this.table.getBucket(Relation.PREFER_FACT)) {
            ExpList args = wr.getArgs();
            if (args.getFirst().eval(null).getString().equals(pName)) {
                pref += (float) args.getLast().eval(null).getReal();
                oldRel = wr;
                break;
            }
        }
        // doesnt exist so create new
        ExpList el = new ExpList();
        el.addLast(Value.newValue(pName));
        el.addLast(Value.newValue(pref));
        Relation rel = Relation.newPreferenceRelation(el);
        if (oldRel != null) {
            this.update(oldRel, rel, null);
        } else {
            this.assertRelation(rel, null);
        }
        return pref;
    }

    public synchronized float changeStandard(final WorldModelRelation standard, final float change) {
        ExpList elOld = standard.getArgs();
        float standardValue = (float) elOld.getLast().eval(null).getReal();
        standardValue += change;
        ExpList elNew = new ExpList();
        for (int i = 1; i < elOld.size(); i++) {
            elNew.addLast(elOld.getNth(i));
        }
        elNew.addLast(Value.newValue(standardValue));
        Relation rel = Relation.newStandardRelation(elNew);
        this.update(standard, rel, null);
        return standardValue;
    }


}
