/*
 * Created on 02.12.2003 14:01:39
 *
 * Multimediale Algorithmen und Datenstrukturen Assessments
 */
package mauda;

import mauda.operation.*;

import java.util.*;
import mauda.undoredo.UndoRedoInfo;

/**
 * Records all executed operations, and provides methods to
 * manipulate them.
 * 
 * @author Markus Krebs
 */
public class OperationRecorder implements ExerciseUpdateListener {
	
	private UndoRedoInfo undoRedoInfo;

	private Vector operations;
	
	private int position = -1;
	private int todoPosition = Integer.MAX_VALUE-1;
	private int minPosition = 0;	// untere Abgrenzung für Undo
	
	/**
	 * Creates a OperationRecorder
	 */
	public OperationRecorder() {
		undoRedoInfo = new UndoRedoInfo();
		operations = new Vector();
	}
	
	/* (non-Javadoc)
	 * @see mauda.ExerciseUpdateListener#exerciseUpdate(mauda.ExerciseUpdateEvent)
	 */
	public void exerciseUpdate(ExerciseUpdateEvent e) {
		//System.out.println("OperationRecorder.exerciseUpdate(..): "+e);
		if(e.getID() == ExerciseUpdateEvent.BACK) back();
		else if(e.getID() == ExerciseUpdateEvent.FORWARD) forward();
		else if(e.getID() == ExerciseUpdateEvent.UNDO) undo();
		else if(e.getID() == ExerciseUpdateEvent.REDO) redo();
		else if(e.getID() == ExerciseUpdateEvent.OPERATION_EXECUTED) {
			add(e.getOperation());
			SimpleOperation so = e.getOperation();
			if(so instanceof SubOperation) {
				if(todoPosition==position-1) todoPosition++;
			}
			// folgende Zeile darf man nicht ausführen!
			//todoPosition = getPossibleTodoPosition(todoPosition);
		} else if(e.getID() == ExerciseUpdateEvent.RESET) {
			clear();
		} else if(e.getID() == ExerciseUpdateEvent.CLEAR_PRECEDING) {
			clearPreceding();
		} else if(e.getID() == ExerciseUpdateEvent.CLEAR_FOLLOWING) {
			clearFollowing();
		} else if(e.getID() == ExerciseUpdateEvent.JUMP) {
			position += e.getValue();
			//System.out.println("orecorder: position = "+position+" / op = "+operations.elementAt(position));
		} else if(e.getID() == ExerciseUpdateEvent.SPECIAL) {
			if(e.getMessage().equals("GO_IN_TODO-MODE"))
				goInTodoMode();
		}
	}
		
	/**
	 * Adds an operation to the recorder. Normally this method is
	 * called, when an operation was executed in the 
	 * <code>JedasPanel</code>
	 * @param obj The executed operation
	 */
	// Aktuelle Operation vermerken
	public void add(Object obj) {
		SimpleOperation op = (SimpleOperation)obj;
		// Testen ob gleiche Operation gemacht wird
		// siehe auch UndoRedo
		if(op!=null&&position<operations.size()-1) {
			Object o = operations.elementAt(position+1);
			if(o!=null && o instanceof SimpleOperation) {
				SimpleOperation undoRedoOp = (SimpleOperation)o;
				if(undoRedoOp.equals(op)) {
					position++;
					return;
				}
			} 
		}		
		
		// Undo-Information:
		Object o = null;
		if(op!=null) o = op.clone();
		
		position++;
		undoRedoInfo.storeChange(position, operations, o);
		/*
		while (operations.size() > position)
			operations.removeElementAt(position);
		operations.add(o);*/
		return;
	}
	// BACK
	private void back() {
		if (position <= minPosition) return;
		position--;
	}
	// FORWARD
	private void forward() {
		if (position >= operations.size() - 1) return;
		position++;
	}
	private void undo() {
		undoRedoInfo.undo(operations);
	}
	private void redo() {
		undoRedoInfo.redo(operations);
	}
	/**
	 * Clears all entries
	 */
	public void clear() {
		minPosition = 0;
		position = 0;
		operations.removeAllElements();
		operations.add(null);
		
		Vector newv = new Vector();
		newv.add(null);
		undoRedoInfo.clear(newv);
	}
	private void clearPreceding() {
		Vector newv = new Vector();
		newv.add(null);
		//undoRedoInfo.clearPreceding(operations, newv);
		undoRedoInfo.clear(newv);
		
		todoPosition -= operations.size()-1;
		position = 0;
		Object o = operations.lastElement();
		operations.removeAllElements();
		operations.add(null);
		minPosition = 0;
	}
	private void clearFollowing() {
		undoRedoInfo.clearFollowing(position+1, operations);
		//while (operations.size() > position+1)
		//	operations.removeElementAt(position+1);		
	}
	/**
	 * Gets the current operation
	 * @return current operation
	 */
	public Object getCurrentOperation() {
		return operations.elementAt(position);
	}
	/**
	 * Gets the current offset (recorder-internal-value)
	 * @return current offset
	 */
	public int getCurrentOffset() {
		return position;
	}
	/**
	 * Gets the operation that follows to the actual operation. If
	 * there exists no next operation <code>null</code> will be
	 * returned.
	 * @return Following operation
	 */
	public Object getNextOperation() {
		if(operations.size()<=position+1) return null;
		return operations.elementAt(position+1);
	}
	/**
	 * Switch the OperationRecorder in the todo-state, what means
	 * that all following operations are todo-operations for the
	 * exercise.
	 */
	public void goInTodoMode() {
		todoPosition = position;
		// folgendes darf an dieser Stelle nicht gemacht werden
		//todoPosition = getPossibleTodoPosition(position);
	}
	/**
	 * Creates a possible todo-position from the given todo-position
	 * @param newTodoPosition the new todo-position
	 * @return a possible todo-position
	 */
	public int getPossibleTodoPosition(int newTodoPosition) {
		if(newTodoPosition>=operations.size()) {
			return operations.size();
		}
		Vector v = new Vector();
		for(int i=0; i<operations.size(); i++) {
			SimpleOperation so = (SimpleOperation)operations.elementAt(i);
			if(so instanceof Operation) {
				Operation op = (Operation)so;
				if(op.isExecuted()) v.add(new Integer(i));
				else {
					i++;
					while(i<operations.size()) {
						so = (SimpleOperation)operations.elementAt(i);
						if(so instanceof Operation) {
							i--;
							break;
						}
						i++; 
					}
					v.add(new Integer(i));
				}
			}
		}
		v.add(new Integer(operations.size()));
		for(int i=0; i<v.size(); i++) {
			int p = ((Integer)v.elementAt(i)).intValue();
			if(newTodoPosition>=todoPosition) {
				if(p>=newTodoPosition) return p;	
			} else if(newTodoPosition<todoPosition){
				if(p>newTodoPosition) {
					if(i==0) return 0;
					int oldp = ((Integer)v.elementAt(i-1)).intValue();
					return oldp;
				} 
			}
		}
		return 0;
	}
	/**
	 * Decreases the todo-position, what means that the border
	 * between init- and todo-operations moves up. The last init-
	 * operation becomes a todo-operation
	 * @see mauda.OperationRecorder#increaseTodoPosition()
	 */
	public void decreaseTodoPosition() {
		// ALT
		//if(todoPosition>0) todoPosition--;
		// NEU
		todoPosition = getPossibleTodoPosition(todoPosition-1);
	}
	/**
	 * Increases the todo-position, what means that the border
	 * between init- and todo-operations moves down. The first todo-
	 * operation becomes a init-operation
	 * @see mauda.OperationRecorder#decreaseTodoPosition()
	 */
	public void increaseTodoPosition() {
		// ALT
		//if(todoPosition<operations.size()) todoPosition++;
		// NEU
		todoPosition = getPossibleTodoPosition(todoPosition+1);
	}
	/**
	 * Sets the border between init- and todo-operations manually.
	 * The parameter hereby is the offset, where to place the
	 * new todo-position.
	 * @param p new todo-position
	 * @see mauda.OperationRecorder#getTodoPosition()
	 * @see mauda.OperationRecorder#increaseTodoPosition()
	 * @see mauda.OperationRecorder#decreaseTodoPosition()
	 */
	public void setTodoPosition(int p) {
		todoPosition = p;
	}
	/**
	 * Gets the current todo-position
	 * @return todo-position
	 * @see mauda.OperationRecorder#setTodoPosition(int)
	 * @see mauda.OperationRecorder#increaseTodoPosition()
	 * @see mauda.OperationRecorder#decreaseTodoPosition()
	 */
	public int getTodoPosition() {
		return todoPosition;
	}
	/**
	 * Tests if the todo-position is allowed. An invalid
	 * todo-position might e.g. a position that lies between
	 * two suboperations.
	 * @return true if valid, false if invalid
	 */
	public boolean isValidTodoPosition() {
		if(position<=todoPosition) return true;
		//SimpleOperation lastInitOp = (SimpleOperation)operations.elementAt(todoPosition);
		SimpleOperation firstTodoOp = (SimpleOperation)operations.elementAt(todoPosition+1);
		if(firstTodoOp instanceof SubOperation) return false;
		//javax.swing.JOptionPane.showMessageDialog(MAUDA.getApplicationMainFrame(),
		//		"LASTINIT: "+lastInitOp+"\n"+
		//		"FIRSTToDO: "+firstTodoOp);
		return true;
	}
	/**
	 * Gets the first failure in an exercise represented by a
	 * failure-object. Important: The search of failure will be 
	 * limited to the actual position, what means that when the
	 * first failure lies after the current position, no failure
	 * (null) will be reported!
	 * @return Failure or <code>null</code> if no failure
	 */
	public Failure getFirstFailure() {
		return getFailure(0, position);
	}
	/**
	 * Gets the first failure that lies beyond the specified offset.
	 * The search is not limited to the current position =>
	 * unlimited. Important: The specified offset must lie on an
	 * Operation (not on SubOperation).
	 * @param offset start of the search
	 * @return Failure of <code>null</code> if no failure
	 */
	public Failure getUnlimitedFailure(int offset) {
		return getFailure(offset, operations.size()-1);
	}
	/**
	 * Gets the first failure that lies beyond the specified offset.
	 * The search is limited to the current position => limited.
	 * Important: The specified offset must lie on an Operation
	 * (not on SubOperation).
	 * @param offset start of the search
	 * @return Failure of <code>null</code> if no failure
	 */
	public Failure getLimitedFailure(int offset) {
		return getFailure(offset, position);
	}
	// Fehlerposition ermitteln
	// Übergabe: offset, d.h. mittels getOffset berechnen
	//           limit: Beschränkung auf ein bestimmtes Offset
	// falls es kein Fehler gibt: Rückgabe -1
	private Failure getFailure(int offset, int limit) {
		//System.out.println("getFailure("+offset+","+limit+")");
		if(offset<0) return null;
		Vector vec = new Vector();
		int size = operations.size();
		
		// Operationen zählen, die vor offset sind
		int opCounter = 0;
		if(offset>0) {
			int counter = 0;
			Enumeration en = operations.elements();
			while(en.hasMoreElements() && counter<offset) {
				if(en.nextElement() instanceof Operation) opCounter++;
				counter++;
			}
		}
		//System.out.println("opCounter = "+opCounter);
		
		for(int i=offset; i<=limit; i++) {
			SimpleOperation so = (SimpleOperation)operations.elementAt(i);
			if(so instanceof Operation) {
				Operation op = (Operation)so;
				if(!op.isExecuted()) {
					//System.out.print("X");
					if(i<todoPosition) return null;
					SubOperationQueue soqCorrect = op.getSubOperationQueue();
					SubOperationQueue soq = new SubOperationQueue();
					i++;
					int marker = i;
					while(i<=limit && operations.elementAt(i) instanceof SubOperation) {
						soq.add((SubOperation)operations.elementAt(i));
						i++;
					}
					//System.out.println("SOQ-Correct: "+soqCorrect);
					//System.out.println("   -Present: "+soq);
					i--;
					int m = Math.min(soqCorrect.length(), soq.length());
					for(int j=0; j<m; j++) {
						SubOperation so1 = soqCorrect.get(j);
						SubOperation so2 = soq.get(j);
						if(!so1.equals(so2)) {
							Failure f = new Failure();
							f.setSuperOp(op);
							f.setOffset(marker+j);
							f.setMaxOffset(marker+soq.length()-1);
							f.setCorrectOp(so1);
							f.setCurrentOp(so2);
							f.setOpNr(opCounter);
							f.setSubOpNr(j);
							f.setDescription(Failure.DIFFERENT);
							return f;
						}
					}
					if(soqCorrect.length()>soq.length()) {
						Failure f = new Failure();
						f.setSuperOp(op);
						f.setOffset(marker+soq.length());
						f.setMaxOffset(marker+soq.length());
						f.setOpNr(opCounter);
						f.setSubOpNr(m);
						f.setDescription(Failure.MISSING_SUBOP);
						for(int j=soq.length(); j<soqCorrect.length(); j++)
							f.addDifferentOperation(soqCorrect.get(j));
						return f;
					} 
					else if(soqCorrect.length()<soq.length()) {
						Failure f = new Failure();
						f.setSuperOp(op);
						f.setOffset(marker+soqCorrect.length());
						f.setMaxOffset(marker+soq.length()-1);
						f.setOpNr(opCounter);
						f.setSubOpNr(m);
						f.setDescription(Failure.ADDITIONAL_FALSE_SUBOP);
						for(int j=soqCorrect.length(); j<soq.length(); j++)
							f.addDifferentOperation(soq.get(j));
						return f;
					}
				}
				opCounter++;
			}
		}
		return null;
	}
	// Teil der Operationenfolge zurückgeben als OperationQueue
	// Rückgabe der OperationQueue, d.h. SubOperation -> Operation
	private OperationQueue getPart(int start, int end) {
		//if(end>position) end = position;
		OperationQueue oq = new OperationQueue();
		for(int i=start; i<=end; i++) {
			int counter = -1;
			Object o = operations.elementAt(i);
			if(o != null) {
				Operation operation = (Operation)o;
				if(!operation.isExecuted()) {
					operation = (Operation)operation.clone();
					operation.getSubOperationQueue().clear();
					// SubOperation(en) hinzufügen
					i++;
					while(i<=end) {
						o = operations.elementAt(i);
						if(!(o instanceof SubOperation)) break;
						operation.add((SubOperation)o);
						i++;
					}
					i--;
				}
				oq.add(operation);	
			}
		}
		return oq;		
	}
	/**
	 * Gets the init-operations as an <code>OperationQueue</code>.
	 * Important:
	 * <ul>
	 * <li>The return <i>is limited</i> to the current position.</li>
	 * <li>The manually performed SubOperations will be delivered 
	 *   as SubOperations of the corresponding Operation</li>
	 * </ul>
	 * @return init-operations
	 * @see mauda.OperationRecorder#getAllInitOperationQueue()
	 */
	public OperationQueue getInitOperationQueue() {
		int end = todoPosition;
		if(end>position) end = position;
		return getPart(0,end);
	}
	/**
	 * Gets the init-operations as an <code>OperationQueue</code>.
	 * Important:
	 * <ul>
	 * <li>The return <i>is not limited</i> to the current position,
	 *   what means all present init-operations will be returned.</li>
	 * <li>The manually performed SubOperations will be delivered 
	 *   as SubOperations of the corresponding Operation</li>
	 * </ul>
	 * @return init-operations
	 * @see mauda.OperationRecorder#getInitOperationQueue()
	 */
	public OperationQueue getAllInitOperationQueue() {
		int end = todoPosition;
		if(end>=operations.size()) end = operations.size()-1;
		return getPart(0,end);
	}
	/**
	 * Gets the todo-operations as an <code>OperationQueue</code>.
	 * Important:
	 * <ul>
	 * <li>The return <i>is limited</i> to the current position.</li>
	 * <li>The manually performed SubOperations will be delivered 
	 *   as SubOperations of the corresponding Operation</li>
	 * </ul>	 	 
	 * @return todo-operations
	 * @see mauda.OperationRecorder#getAllTodoOperationQueue()
	 */
	public OperationQueue getTodoOperationQueue() {
		int end = operations.size();
		if(end>position) end = position;
		return getPart(todoPosition+1, end);
	}
	/**
	 * Gets the todo-operations as an <code>OperationQueue</code>.
	 * Important:
	 * <ul>
	 * <li>The return <i>is not limited</i> to the current position,
	 *   what means that all present todo-operations will  be returned.</li>
	 * <li>The manually performed SubOperations will be delivered 
	 *   as SubOperations of the corresponding Operation</li>
	 * </ul> 
	 * @return todo-operations
	 * @see mauda.OperationRecorder#getTodoOperationQueue()
	 */
	public OperationQueue getAllTodoOperationQueue() {
		return getPart(todoPosition+1, operations.size()-1);
	}
	
	// Rückgabe des ganzen Vector, d.h. mit SubOperationen
	private Vector getSegment(int start, int end) {
		if(end>position) end = position;
		Vector oq = new Vector();
		for(int i=start; i<=end; i++) {
			int counter = -1;
			Object o = operations.elementAt(i);
			if(o != null) oq.add(o);
		}
		return oq;
		
	}
	
	/**
	 * Gets the init-operations as an <code>Vector</code>.
	 * Important: 
	 * <ul>
	 * <li>The return <i>is not limited</i> to the current 
	 *   position, what means that all present init-operations will 
	 *   be returned.</li>
	 * <li>The manually performed SubOperations will be delivered
	 *   as extra-entries in the returned vector, and not as
	 *   SubOperations of the corresponding operation.</li>
	 * </ul>
	 * @return init-operations
	 * @see mauda.OperationRecorder#getInitOperationQueue()
	 * @see mauda.OperationRecorder#getAllInitOperationQueue()
	 */
	public Vector getInitOperations() {
		return getSegment(0, todoPosition);
	}
	/**
	 * Gets the todo-operations as an <code>Vector</code>.
	 * Important:
	 * <ul> 
	 * <li>The return <i>is not limited</i> to the current 
	 *   position, what means that all present todo-operations will 
	 *   be returned.</li>
	 * <li>The manually performed SubOperations will be delivered
	 *   as extra-entries in the returned vector, and not as
	 *   SubOperations of the corresponding operation.</li>
	 * </ul>
	 * @return todo-operations
	 * @see mauda.OperationRecorder#getTodoOperationQueue()
	 * @see mauda.OperationRecorder#getAllTodoOperationQueue()
	 */
	public Vector getTodoOperations() {
		return getSegment(todoPosition+1, operations.size());
	}
	
	/**
	 * Adds all operations of the Vector to the recorder.
	 * @param v vector of operations
	 */
	public void addOperations(Vector v) {
		Enumeration en = v.elements();
		while(en.hasMoreElements()) {
			Operation op = (Operation)en.nextElement();
			operations.add(op);
			if(!op.isExecuted()) {
				SubOperationQueue soq = op.getSubOperationQueue();
				operations.addAll(soq.toVector());
			}
		}
		position = operations.size()-1;
	}
	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	public String toString() {
		String s="";
		s +=	"#Entries: "+operations.size()+"\n"+
				"Position: "+position+"\n\n";
		int counter = -1;
		Enumeration en = operations.elements();
		while(en.hasMoreElements()) {
			Object o = en.nextElement();
			if(o == null) {
				s += "EMPTY\n";
			} else if(o instanceof Operation) {
				s += (Operation)o+"\n";
			}
			counter++;
			if(counter==position)     s += "----------- ACTUAL VIEW -----------\n";
			if(counter==todoPosition) s += "------------- EXERCISE ------------\n"; 
		}
		return s;
	}
	/**
	 * Check if an undo is possible
	 * @return Returns true, if possible, false, otherwise
	 */	
	public boolean canUndo() {
		return undoRedoInfo.canUndo();
	}
	
	/**
	 * Gets the number of the current operation, counted from 0.
	 * @return Operation-Number
	 */
	public int getCurrentOperationNr() {
		int cnr = -1;
		Enumeration en = operations.elements();
		int counter = -1;	// WAR vorher 0
		while(en.hasMoreElements()&&counter<position) {
			if(en.nextElement() instanceof Operation) cnr++;
			counter++;
		}
		return cnr;
	}
	/**
	 * Gets the number of the current suboperation, counted from 0,
	 * inside the current Operation.
	 * @return SubOperation-Number
	 */
	public int getCurrentSubOperationNr() {
		// Wenn es noch keine SubOperationen gibt, dann ....
		if(operations.elementAt(position) instanceof Operation) return -2;
		
		int cnr = -1;
		int size = operations.size();
		if(size>position) size = position+1;
		while((size-(cnr+2))>0 && operations.elementAt(size-(cnr+2)) instanceof SubOperation) cnr++;
		return cnr;
	}
	/**
	 * Gets the entry at the specified offset
	 * @param offset
	 * @return entry at the specified offset
	 */
	public Object getEntry(int offset) {
		if(offset<0 || offset>=operations.size()) return null; 
		return operations.elementAt(offset);
	}
	
	/**
	 * Adds an entry to the end
	 * @param o The new entry
	 */
	public void addEntry(Object o) { operations.add(o); }
	
	/**
	 * Returns the number of operations executed
	 * @return operation-count
	 */
	public int getOperationCount() { return operations.size(); }
	/**
	 * Gets the overall-offset (internal) of the subopnr'th
	 * suboperation inside the opnr'th operation. opnr must be a
	 * value started at 0, but subopnr can be value started at -1.
	 * A value of -1 for subopnr indicates that the offset of the
	 * template-position of the opnr'th operation will be returned.
	 * @param opnr Operation-Number
	 * @param subopnr SubOperation-Number inside Operation-Number
	 * @return the offset for the specified position or -1 if that position not exists
	 */
	// Liefert die Position(Offset) innerhalb des operations-Vectors
	// Liefert -1 als Ergebnis wenn ungültig
	// opnr = 0. - x. Operation
	// subopnr = 0. - x. SubOperation (-1 = Operation-Object)
	public int getOffset(int opnr, int subopnr) {
		//System.out.println("getOffset("+opnr+","+subopnr+")");
		if(subopnr<-1) return -1;
		int offset = -1;
		Enumeration en = operations.elements();
		opnr++;	// Erstes-Operation Objekt finden ==> opnr++
		Object o = null;
		while(opnr>0 && en.hasMoreElements()) {
			o = en.nextElement(); offset++;
			if(o instanceof Operation) opnr--;
		}
		if(opnr>0) return -1;
		if(subopnr==-1) return offset;
		subopnr++;
		while(subopnr>0 && en.hasMoreElements()) {
			o = en.nextElement(); offset++;
			if(!(o instanceof SubOperation)) return -1;
			subopnr--;
		}
		if(subopnr>0) return -1;
		//System.out.println("   = "+offset);
		return offset;
	}
	/**
	 * Gets the present subopnr'th suboperation of the opnr'th
	 * operation.
	 * @param opnr Operation-Number
	 * @param subopnr SubOperation-Number
	 * @return Present SubOperation at the specified position or null if that position not exists
	 * @see mauda.OperationRecorder#getCorrectSubOperation(int,int)
	 */
	// Ausgeführte SubOperation zurückgeben
	public SubOperation getSubOperation(int opnr, int subopnr) {
		int offset = getOffset(opnr, subopnr);
		if(offset<0) return null;
		return (SubOperation)getEntry(offset);
	}
	/**
	 * Gets the correct subopnr'th suboperation of the opnr'th
	 * operation. The returned SubOperation may be equal to a
	 * call to getSubOperation(opnr,subopnr).
	 * @param opnr Operation-Number
	 * @param subopnr SubOperation-Number
	 * @return Correct SubOperation at the specified position or null if that position not exists
	 * @see mauda.OperationRecorder#getSubOperation(int,int)
	 */
	// korrekte SubOperation zurückgeben
	public SubOperation getCorrectSubOperation(int opnr, int subopnr) {
		if(subopnr<0) return null;
		int offset = getOffset(opnr, -1);
		if(offset<0) return null;
		Operation op = (Operation)getEntry(offset);
		SubOperationQueue soq = op.getSubOperationQueue();
		if(subopnr>=soq.length()) return null;
		return soq.get(subopnr);
	}
	/**
	 * Gets the opnr'th operation.
	 * @param opnr Operation-Number
	 * @return Operation at the specified position or null if that position not exists
	 */
	public Operation getOperation(int opnr) {
		if(opnr<0) return null;
		int offset = getOffset(opnr, -1);
		if(offset<0) return null;
		return (Operation)getEntry(offset);
	}
}
