package at.oefai.aaa.agent;

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

import javax.swing.ListModel;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

import at.oefai.aaa.agent.jam.DataDrivenGoal;
import at.oefai.aaa.agent.jam.Goal;
import at.oefai.aaa.agent.jam.WorldModelRelation;
import at.oefai.aaa.agent.jam.WorldModelTableEvent;
import at.oefai.aaa.agent.jam.WorldModelTableEventListener;
import at.oefai.aaa.agent.jam.ParseException;

/**
 * Wrapper for the interpreter thet publishes tree and list models
 * (and holds a reference to an environment)
 * @author Stefan Rank
 */
public class ModelInterpreter extends StoppableInterpreter {
    private ListModel worldModelListModel = null;
    IntentionTreeModel intentionTreeModel = null; // non-private to allow inner classes direct access

    /**  */
    public ModelInterpreter(String name, String[] argv) throws ParseException, IOException {
        super(name, argv);
    }

    //override endOfCacle to fire intentionstructure changes:
    public void cycleEnd() {
        super.cycleEnd();
        if (this.intentionTreeModel != null) {
            this.intentionTreeModel.fireTreeChange();
            this.intentionTreeModel.fireSelectionChange();
        }
    }


    public ListModel getWorldModelListModel() {
        if (this.worldModelListModel == null) {
            this.worldModelListModel = new WorldModelListModel();
        }
        return this.worldModelListModel;
    }

    public TreeModel getIntentionTreeModel() {
        if (this.intentionTreeModel == null) {
            this.intentionTreeModel = new IntentionTreeModel();
        }
        return this.intentionTreeModel;
    }

    public TreeSelectionModel getIntentionTreeSelectionModel() {
        if (this.intentionTreeModel == null) {
            this.intentionTreeModel = new IntentionTreeModel();
        }
        return this.intentionTreeModel;
    }

    // Inner classes

    private class IntentionTreeModel extends DefaultTreeSelectionModel implements TreeModel {
        private Goal dummyRoot = new DataDrivenGoal(null, null);
        private Goal lastCurrent = null;
        private int lastLength = 0;
        /** listeners to be notified of changes in the intention structure */
        private final List<TreeModelListener> listenerList =
            Collections.synchronizedList(new LinkedList<TreeModelListener>());

        public IntentionTreeModel() {
            this.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
        }

        public Object getRoot() {
            return this.dummyRoot;
        }

        public Object getChild(Object parent, int index) {
            if (parent == this.dummyRoot) {
                return getIntentionStructure().getToplevelGoals().getNth(index + 1);
            }
            assert (index == 0); // only single children possible
            assert parent instanceof Goal; // part of the TreeModel contract
            return ((Goal) parent).getSubgoal();
        }

        public int getChildCount(Object parent) {
            if (parent == this.dummyRoot) {
                return getIntentionStructure().getToplevelGoals().size();
            }
            assert parent instanceof Goal; // part of the TreeModel contract
            return (((Goal) parent).isLeafGoal()) ? 0 : 1;
        }

        public boolean isLeaf(Object node) {
            if (node == this.dummyRoot) {
                return false;
            }
            assert node instanceof Goal; // part of the TreeModel contract
            return ((Goal) node).isLeafGoal();
        }

        public void valueForPathChanged(TreePath path, Object newValue) {
            //ignore for now
        }

        public int getIndexOfChild(Object parent, Object child) {
            if ((parent == null) || (child == null)) {
                return -1;
            } else if (parent == this.dummyRoot) {
                assert child instanceof Goal; // part of the TreeModel contract
                return getIntentionStructure().getToplevelGoals().indexOf((Goal) child);
            } else {
                return 0;
            }
        }

        public void addTreeModelListener(TreeModelListener l) {
            this.listenerList.add(l);
        }

        public void removeTreeModelListener(TreeModelListener l) {
            this.listenerList.remove(l);
        }

        public void fireTreeChange() {
            // the following creates an array copy of the listenerlist to avoid
            // race conditions, for the same reason I do not create an array of
            // the correct size beforehand (as would be possible for toArray()),
            // listeners could be removed, only the toArray call itself is synchronized
            if (this.listenerList.size() > 0) { // keep overhead for no listeners small
                TreeModelListener[] listeners = new TreeModelListener[0];
                listeners = this.listenerList.toArray(listeners);
                TreeModelEvent tme = new TreeModelEvent(this, new Object[] {this.dummyRoot});
                for (TreeModelListener l : listeners) {
                    l.treeStructureChanged(tme);
                }
            }
        }

        public void fireSelectionChange() {
            Goal current = getIntentionStructure().getCurrentGoal();
            if (current == null) {
                if (this.lastCurrent != null) { // only if its a change
                    this.lastCurrent = null;
                    this.setSelectionPath(null);
                }
            } else { // i.e. current != null
                Goal tmpCurrent = current;
                int length = 1; // calculate the number of goals in the path
                while (tmpCurrent.getSubgoal() != null) {
                    length++;
                    tmpCurrent = tmpCurrent.getSubgoal();
                }
                // only if it is a different goal or a different depth
                //if ((current != lastCurrent) || (length != lastLength)) {
                    this.lastCurrent = current;
                    this.lastLength = length;
                    Goal[] garray = new Goal[length + 1];
                    garray[0] = this.dummyRoot;
                    for (int i = 1; i <= length; i++) {
                        garray[i] = current;
                        current = current.getSubgoal();
                    }
                    TreePath tp = new TreePath(garray);
                    this.setSelectionPath(tp);
                //}
            }
        }
    }


    private class WorldModelListModel implements ListModel, WorldModelTableEventListener {

        /** listeners to be notified of changes in the world model */
        private final List<ListDataListener> listenerList =
            Collections.synchronizedList(new LinkedList<ListDataListener>());
        /** stringlist as cache, the worldmodeltable isnt properly synchronized/threadsafe*/
        private final List<String> relationList =
            Collections.synchronizedList(new LinkedList<String>());

        WorldModelListModel() {
            WorldModelTableEvent[] events = getWorldModel().getAllCurrent();
            getWorldModel().addWorldModelTableEventListener(this);
            for (int i = 0; i < events.length; ++i) {
                // but only use non-perceptions...
                WorldModelRelation rel = events[i].getRelation();
                if (!rel.isPerception()) {
                    this.relationList.add(rel.verboseString(null));
                }
            }
        }

        /**
         * Returns the length of the worldmodel.
         * @return the length of the list
         */
        public int getSize() {
            //return _getWorldModel().getSize();
            return this.relationList.size();
        }

        /**
         * Returns the verboseString at the specified index.
         * @param index the requested index
         * @return the value at <code>index</code>
         */
        public Object getElementAt(int index) {
            //return _getWorldModel().get(index).verboseString();
            // check the size here as the updates can be very frequent
            // (and thus may happen between a getSize() and a getElementAt() call)
            if (index < this.relationList.size() && index >= 0) {
                return this.relationList.get(index);
            }
            return null;
        }

        /**
         * Adds a listener
         * @param l the <code>ListDataListener</code> to be added
         */
        public void addListDataListener(ListDataListener l) {
            this.listenerList.add(l);
        }

        /**
         * Removes a listener
         * @param l the <code>ListDataListener</code> to be removed
         */
        public void removeListDataListener(ListDataListener l) {
            this.listenerList.remove(l);
        }

        /**
         * callback that tranforms the event to a listdataevent
         * @param e
         */
        public void worldModelTableChanged(WorldModelTableEvent e) {
            if (this.listenerList.size() > 0) { // keep overhead for no listeners small
                assert(e != null);
                WorldModelRelation rel = e.getRelation();
                if (!rel.isPerception()) {
                    //int index = e.getWorldModelRelation().getID();
                    ListDataListener[] listeners = new ListDataListener[0];
                    listeners = this.listenerList.toArray(listeners);
                    String relString = rel.verboseString(null);
                    if (e.getType() == WorldModelTableEvent.Type.ASSERT) {
                        this.relationList.add(relString);
                        int index = this.relationList.indexOf(relString);
                        ListDataEvent lde = new ListDataEvent(this, ListDataEvent.INTERVAL_ADDED, index, index);
                        fireIntervalAdded(listeners, lde);
                    } else if (e.getType() == WorldModelTableEvent.Type.RETRACT) {
                        int index = this.relationList.indexOf(relString);
                        if (index >= 0) {
                            this.relationList.remove(relString);
                            ListDataEvent lde = new ListDataEvent(this, ListDataEvent.INTERVAL_REMOVED, index, index);
                            fireIntervalRemoved(listeners, lde);
                        }
                    }
                }
            }
        }

        private void fireIntervalAdded(ListDataListener[] listeners, ListDataEvent lde) {
            for (int i = 0; i < listeners.length; ++i) {
                listeners[i].intervalAdded(lde);
            }
        }

        private void fireIntervalRemoved(ListDataListener[] listeners, ListDataEvent lde) {
            for (int i = 0; i < listeners.length; ++i) {
                listeners[i].intervalRemoved(lde);
            }
        }
    }

}
