/*******************************************************************************
 * Copyright (c) 2000, 2009 QNX Software Systems and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     QNX Software Systems - Initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.debug.mi.core.cdi;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Observable;
import java.util.Observer;

import org.eclipse.cdt.debug.core.cdi.CDIException;
import org.eclipse.cdt.debug.core.cdi.ICDIEventManager;
import org.eclipse.cdt.debug.core.cdi.event.ICDIEvent;
import org.eclipse.cdt.debug.core.cdi.event.ICDIEventListener;
import org.eclipse.cdt.debug.core.cdi.model.ICDIStackFrame;
import org.eclipse.cdt.debug.mi.core.MIException;
import org.eclipse.cdt.debug.mi.core.MIPlugin;
import org.eclipse.cdt.debug.mi.core.MISession;
import org.eclipse.cdt.debug.mi.core.cdi.event.ChangedEvent;
import org.eclipse.cdt.debug.mi.core.cdi.event.CreatedEvent;
import org.eclipse.cdt.debug.mi.core.cdi.event.DestroyedEvent;
import org.eclipse.cdt.debug.mi.core.cdi.event.DisconnectedEvent;
import org.eclipse.cdt.debug.mi.core.cdi.event.ExitedEvent;
import org.eclipse.cdt.debug.mi.core.cdi.event.MemoryChangedEvent;
import org.eclipse.cdt.debug.mi.core.cdi.event.ResumedEvent;
import org.eclipse.cdt.debug.mi.core.cdi.event.SuspendedEvent;
import org.eclipse.cdt.debug.mi.core.cdi.model.MemoryBlock;
import org.eclipse.cdt.debug.mi.core.cdi.model.Target;
import org.eclipse.cdt.debug.mi.core.cdi.model.Thread;
import org.eclipse.cdt.debug.mi.core.command.Command;
import org.eclipse.cdt.debug.mi.core.command.CommandFactory;
import org.eclipse.cdt.debug.mi.core.command.MIExecContinue;
import org.eclipse.cdt.debug.mi.core.command.MIExecFinish;
import org.eclipse.cdt.debug.mi.core.command.MIStackInfoDepth;
import org.eclipse.cdt.debug.mi.core.command.MIStackSelectFrame;
import org.eclipse.cdt.debug.mi.core.command.MIThreadSelect;
import org.eclipse.cdt.debug.mi.core.event.MIBreakpointChangedEvent;
import org.eclipse.cdt.debug.mi.core.event.MIBreakpointCreatedEvent;
import org.eclipse.cdt.debug.mi.core.event.MIBreakpointDeletedEvent;
import org.eclipse.cdt.debug.mi.core.event.MIChangedEvent;
import org.eclipse.cdt.debug.mi.core.event.MICreatedEvent;
import org.eclipse.cdt.debug.mi.core.event.MIDestroyedEvent;
import org.eclipse.cdt.debug.mi.core.event.MIDetachedEvent;
import org.eclipse.cdt.debug.mi.core.event.MIEvent;
import org.eclipse.cdt.debug.mi.core.event.MIGDBExitEvent;
import org.eclipse.cdt.debug.mi.core.event.MIInferiorCreatedEvent;
import org.eclipse.cdt.debug.mi.core.event.MIInferiorExitEvent;
import org.eclipse.cdt.debug.mi.core.event.MIInferiorSignalExitEvent;
import org.eclipse.cdt.debug.mi.core.event.MIMemoryChangedEvent;
import org.eclipse.cdt.debug.mi.core.event.MIMemoryCreatedEvent;
import org.eclipse.cdt.debug.mi.core.event.MIRegisterChangedEvent;
import org.eclipse.cdt.debug.mi.core.event.MIRegisterCreatedEvent;
import org.eclipse.cdt.debug.mi.core.event.MIRunningEvent;
import org.eclipse.cdt.debug.mi.core.event.MISharedLibChangedEvent;
import org.eclipse.cdt.debug.mi.core.event.MISharedLibCreatedEvent;
import org.eclipse.cdt.debug.mi.core.event.MISharedLibEvent;
import org.eclipse.cdt.debug.mi.core.event.MISharedLibUnloadedEvent;
import org.eclipse.cdt.debug.mi.core.event.MISignalChangedEvent;
import org.eclipse.cdt.debug.mi.core.event.MIStoppedEvent;
import org.eclipse.cdt.debug.mi.core.event.MIThreadCreatedEvent;
import org.eclipse.cdt.debug.mi.core.event.MIThreadExitEvent;
import org.eclipse.cdt.debug.mi.core.event.MIVarChangedEvent;
import org.eclipse.cdt.debug.mi.core.event.MIVarCreatedEvent;
import org.eclipse.cdt.debug.mi.core.event.MIVarDeletedEvent;
import org.eclipse.cdt.debug.mi.core.output.MIInfo;
import org.eclipse.cdt.debug.mi.core.output.MIStackInfoDepthInfo;

/**
 */
public class EventManager extends SessionObject implements ICDIEventManager, Observer {

	List list = Collections.synchronizedList(new ArrayList(1));
	MIRunningEvent lastRunningEvent;
	Command lastUserCommand = null;
	boolean fAllowProcessingEvents = true;
	/**
	 * Process the event from MI, do any state work on the CDI,
	 * and fire the corresponding CDI event.
	 */
	@Override
	public void update(Observable o, Object arg) {
		
		MIEvent miEvent = (MIEvent)arg;
		Session session = (Session)getSession();
		Target currentTarget = session.getTarget(miEvent.getMISession());
		if (currentTarget == null) {
			return; // bailout; this no concern to us.  But we should Assert.
		}
		List cdiList = new ArrayList(1);

		if (miEvent instanceof MIStoppedEvent) {
			if (processSuspendedEvent((MIStoppedEvent)miEvent)) {
				cdiList.add(new SuspendedEvent(session, miEvent));
			}
		} else if (miEvent instanceof MIRunningEvent) {
			if (processRunningEvent((MIRunningEvent)miEvent))
				cdiList.add(new ResumedEvent(session, (MIRunningEvent)miEvent));
		} else if (miEvent instanceof MIChangedEvent) {
			if (miEvent instanceof MIVarChangedEvent) {
				cdiList.add(new ChangedEvent(session, (MIVarChangedEvent)miEvent));
			} else if (miEvent instanceof MIRegisterChangedEvent) {
				cdiList.add(new ChangedEvent(session, (MIRegisterChangedEvent)miEvent));
			} else if (miEvent instanceof MIMemoryChangedEvent) {
				// We need to fire an event for all the register blocks
				// that may contain the modified addresses.
				MemoryManager mgr = session.getMemoryManager();
				MemoryBlock[] blocks = mgr.getMemoryBlocks(miEvent.getMISession());
				MIMemoryChangedEvent miMem = (MIMemoryChangedEvent)miEvent;
				BigInteger[] addresses = miMem.getAddresses();
				for (int i = 0; i < blocks.length; i++) {
					if (blocks[i].contains(addresses) &&
						(! blocks[i].isFrozen() || blocks[i].isDirty())) {
						cdiList.add(new MemoryChangedEvent(session, blocks[i], miMem));
						blocks[i].setDirty(false);
					}
				}
			} else if (miEvent instanceof MIBreakpointChangedEvent) {
				MIBreakpointChangedEvent bpoint = (MIBreakpointChangedEvent)miEvent;
				if (bpoint.getNumber() > 0) {
					cdiList.add(new ChangedEvent(session, bpoint));
				} else {
					try {
						// Pass the event to access to the event's hint
						// https://bugs.eclipse.org/bugs/show_bug.cgi?id=135250
						session.getBreakpointManager().update(currentTarget, miEvent);
					} catch (CDIException e) {
					}
				}
			} else if (miEvent instanceof MISharedLibChangedEvent) {
				cdiList.add(new ChangedEvent(session, (MISharedLibChangedEvent)miEvent));
			} else if (miEvent instanceof MISignalChangedEvent) {
				MISignalChangedEvent sig = (MISignalChangedEvent)miEvent;
				String name = sig.getName();
				if (name == null || name.length() == 0) {
					// Something change we do not know what
					// Let the signal manager handle it with an update().
					try {
						SignalManager sMgr = session.getSignalManager();
						sMgr.update(currentTarget);
					} catch (CDIException e) {
					}
				} else {
					cdiList.add(new ChangedEvent(session, sig));
				}
			}
		} else if (miEvent instanceof MIDestroyedEvent) {
			if (miEvent instanceof MIThreadExitEvent) {
				cdiList.add(new DestroyedEvent(session,(MIThreadExitEvent)miEvent)); 
			} else if (miEvent instanceof MIInferiorSignalExitEvent) {
				cdiList.add(new ExitedEvent(session, (MIInferiorSignalExitEvent)miEvent));
			} else if (miEvent instanceof MIInferiorExitEvent) {
				cdiList.add(new ExitedEvent(session, (MIInferiorExitEvent)miEvent));
			} else if (miEvent instanceof MIGDBExitEvent) {
				// Remove the target from the list.
				Target target = session.getTarget(miEvent.getMISession());
				if (target != null) {
					session.removeTargets(new Target[] { target });
				}
				cdiList.add(new ExitedEvent(session, (MIGDBExitEvent)miEvent));
			} else if (miEvent instanceof MIDetachedEvent) {
				cdiList.add(new DisconnectedEvent(session, (MIDetachedEvent)miEvent));
			} else if (miEvent instanceof MIBreakpointDeletedEvent) {
				MIBreakpointDeletedEvent bpoint = (MIBreakpointDeletedEvent)miEvent;
				if (bpoint.getNumber() > 0) {
					cdiList.add(new DestroyedEvent(session, bpoint));
				} else {
					// Something was deleted we do not know what
					// Let the breakpoint manager handle it with an update().
					try {
						session.getBreakpointManager().update(currentTarget);
					} catch (CDIException e) {
					}
				}
			} else if (miEvent instanceof MISharedLibUnloadedEvent) {
				processSharedLibUnloadedEvent((MISharedLibUnloadedEvent)miEvent);
				cdiList.add(new DestroyedEvent(session, (MISharedLibUnloadedEvent)miEvent));
			} else if (miEvent instanceof MIVarDeletedEvent) {
				cdiList.add(new DestroyedEvent(session, (MIVarDeletedEvent)miEvent));
			}
		} else if (miEvent instanceof MICreatedEvent) {
			if (miEvent instanceof MIBreakpointCreatedEvent) {
				MIBreakpointCreatedEvent bpoint = (MIBreakpointCreatedEvent)miEvent;
				if (bpoint.getNumber() > 0) {
					cdiList.add(new CreatedEvent(session, bpoint));
				} else {
					// Something created we do not know what
					// Let the breakpoint manager handle it with an update().
					try {
						session.getBreakpointManager().update(currentTarget);
					} catch (CDIException e) {
					}
				}
			} else if (miEvent instanceof MIVarCreatedEvent) {
				cdiList.add(new CreatedEvent(session, (MIVarCreatedEvent)miEvent));
			} else if (miEvent instanceof MIRegisterCreatedEvent) {
				cdiList.add(new CreatedEvent(session, (MIRegisterCreatedEvent)miEvent));
			} else if (miEvent instanceof MIThreadCreatedEvent) {
				cdiList.add(new CreatedEvent(session, (MIThreadCreatedEvent)miEvent));
			} else if (miEvent instanceof MIMemoryCreatedEvent) {
				cdiList.add(new CreatedEvent(session, (MIMemoryCreatedEvent)miEvent));
			} else if (miEvent instanceof MISharedLibCreatedEvent) {
				cdiList.add(new CreatedEvent(session, (MISharedLibCreatedEvent)miEvent));
			} else if (miEvent instanceof MIInferiorCreatedEvent) {
				cdiList.add(new CreatedEvent(session, (MIInferiorCreatedEvent)miEvent));
			}
		}

		// Fire the event;
		ICDIEvent[] cdiEvents = (ICDIEvent[])cdiList.toArray(new ICDIEvent[0]);
		fireEvents(cdiEvents);
	}

	public EventManager(Session session) {
		super(session);
	}
	
	/**
	 * @see org.eclipse.cdt.debug.core.cdi.ICDIEventManager#addEventListener(ICDIEventListener)
	 */
	@Override
	public void addEventListener(ICDIEventListener listener) {
		list.add(listener);
	}

	/**
	 * @see org.eclipse.cdt.debug.core.cdi.ICDIEventManager#removeEventListener(ICDIEventListener)
	 */
	@Override
	public void removeEventListener(ICDIEventListener listener) {
		list.remove(listener);
	}

	public void removeEventListeners() {
		list.clear();
	}

	/**
	 * Send ICDIEvent to the listeners.
	 */
	public void fireEvents(ICDIEvent[] cdiEvents) {
		if (cdiEvents != null && cdiEvents.length > 0) {
			ICDIEventListener[] listeners = (ICDIEventListener[])list.toArray(new ICDIEventListener[0]);
			for (int i = 0; i < listeners.length; i++) {
				listeners[i].handleDebugEvents(cdiEvents);
			}			
		}
	}

	/**
	 * When suspended arrives, reset managers and target.
	 * Alse the variable and the memory needs to be updated and events
	 * fired for changes.
	 */
	boolean processSuspendedEvent(MIStoppedEvent stopped) {
		Session session = (Session)getSession();
		MISession miSession = stopped.getMISession();
		Target currentTarget = session.getTarget(miSession);
		currentTarget.setSupended(true);

		// Bailout early if we do not want to process any events.
		if (!isAllowingProcessingEvents()) {
			return false;
		}


		if (processSharedLibEvent(stopped)) {
			// Event was consumed by the shared lib processing bailout
			return false;
		}

		if (processBreakpointHitEvent(stopped)) {
			// Event was consumed, i.e. it was not the right exception.
			return false;
		}

		int threadId = stopped.getThreadId();
		currentTarget.updateState(threadId);
		try {
			Thread cthread = (Thread)currentTarget.getCurrentThread();
			if (cthread != null) {
				cthread.getCurrentStackFrame();
			} else {
				return true;
			}
		} catch (CDIException e1) {
			//e1.printStackTrace();
			return true;
		}

		// Update the managers.
		// For the Variable/Expression Managers call only the updateManager.
		VariableManager varMgr = session.getVariableManager();
		ExpressionManager expMgr  = session.getExpressionManager();		
		RegisterManager regMgr = session.getRegisterManager();
		MemoryManager memMgr = session.getMemoryManager();
		BreakpointManager bpMgr = session.getBreakpointManager();
		SignalManager sigMgr = session.getSignalManager();
		SourceManager srcMgr = session.getSourceManager();
		SharedLibraryManager libMgr = session.getSharedLibraryManager();
		try {
			if (varMgr.isAutoUpdate()) {
				varMgr.update(currentTarget);
			}
			if (expMgr.isAutoUpdate()) { 
				expMgr.update(currentTarget);
			}
			if (regMgr.isAutoUpdate()) {
				regMgr.update(currentTarget);
			}
			if (memMgr.isAutoUpdate()) {
				memMgr.update(currentTarget);
			}
			if (bpMgr.isAutoUpdate()) {
				bpMgr.update(currentTarget);
			}
			if (sigMgr.isAutoUpdate()) {
				sigMgr.update(currentTarget);
			}
			if (libMgr.isAutoUpdate()) {
				libMgr.update(currentTarget);
			}
			if (srcMgr.isAutoUpdate()) {
				srcMgr.update(currentTarget);
			}
		} catch (CDIException e) {
			// Something went wrong => preventing updates to the model, this is serious
			MIPlugin.log(e);
		}
		return true;
	}

	/**
	 * When a shared library is unloading we could possibly have stale libraries.
	 * GDB does no react well to this: see PR
	 * https://bugs.eclipse.org/bugs/show_bug.cgi?id=74496
	 * @param unLoaded
	 * @return
	 */
	boolean processSharedLibUnloadedEvent(MISharedLibUnloadedEvent unLoaded) {
		Session session = (Session)getSession();
		MISession miSession = unLoaded.getMISession();
		Target target = session.getTarget(miSession);

		// We do not need to do fancy checking we can just delete all
		// the expression variable and let UI recreate them by reevaluating.
		ExpressionManager expMgr = session.getExpressionManager();
		try {
			expMgr.deleteAllVariables(target);
		} catch (CDIException e) {
		}

		return false;
	}

	/**
	 * If the deferredBreakpoint processing is set
	 * catch the shared-lib-event go to the last known
	 * stackframe and try to finish.
	 * Save the last user command and issue it again.
	 * @param stopped
	 * @return
	 */
	boolean processSharedLibEvent(MIStoppedEvent stopped) {
		Session session = (Session)getSession();
		MISession miSession = stopped.getMISession();

		Target currentTarget = session.getTarget(miSession);
		SharedLibraryManager mgr = session.getSharedLibraryManager();

		if (mgr.isDeferredBreakpoint(currentTarget)) {
			if (stopped instanceof MISharedLibEvent) {
				// Check if we have a new library loaded
				try {
					mgr.update(currentTarget);
				} catch (CDIException e3) {
				}
				CommandFactory factory = miSession.getCommandFactory();
				int type = (lastRunningEvent == null) ? MIRunningEvent.CONTINUE : lastRunningEvent.getType();
				if (lastUserCommand == null) {
					switch (type) {
						case MIRunningEvent.NEXT:
							lastUserCommand = factory.createMIExecNext(1);
							break;
						case MIRunningEvent.NEXTI:
							lastUserCommand = factory.createMIExecNextInstruction(1);
							break;
						case MIRunningEvent.STEP:
							lastUserCommand = factory.createMIExecStep(1);
							break;
						case MIRunningEvent.STEPI:
							lastUserCommand = factory.createMIExecStepInstruction(1);
							break;
						case MIRunningEvent.FINISH:
							lastUserCommand = factory.createMIExecFinish();
							break;
						case MIRunningEvent.RETURN:
							lastUserCommand = factory.createMIExecReturn();
							break;
						case MIRunningEvent.CONTINUE: {
							MIExecContinue cont = factory.createMIExecContinue();
							cont.setQuiet(true);
							try {
								miSession.postCommand(cont);
								MIInfo info = cont.getMIInfo();
								if (info == null) {
									// throw new CDIException("Target is not responding");
								}
							} catch (MIException e) {
								// throw new MI2CDIException(e);
							}
							return true; // for the continue bailout early no need to the stuff below
						}
					}
				}

				int miLevel = 0;
				int tid = 0;
				Thread currentThread = null;
				try {
					currentThread = (Thread)currentTarget.getCurrentThread();
				} catch (CDIException e1) {
				}
				tid = currentThread.getId();
				// Select the old thread now.
				if (tid > 0) {
					MIThreadSelect selectThread = factory.createMIThreadSelect(tid);
					try {
						miSession.postCommand(selectThread);
					} catch (MIException e) {
						// ignore
					}
				}
				ICDIStackFrame frame = null;
				try {
					frame = currentThread.getCurrentStackFrame();
				} catch (CDIException e2) {
				}
				int count = 0;
				try {
					MIStackInfoDepth depth = factory.createMIStackInfoDepth();
					miSession.postCommand(depth);
					MIStackInfoDepthInfo info = depth.getMIStackInfoDepthInfo();
					if (info == null) {
						//throw new CDIException("No answer");
					}
					count = info.getDepth();
				} catch (MIException e) {
					//throw new MI2CDIException(e);
					//System.out.println(e);
				}
				if (frame != null) {
					// Fortunately the ICDIStackFrame store the level
					// in ascending level the higher the stack the higher the level
					// GDB does the opposite the highest stack is 0.
					// This allow us to do some calculation, in figure out the
					// level of the old stack.  The -1 is because gdb level is zero-based
					miLevel = count - frame.getLevel() - 1;
				}
				if (miLevel >= 0) {
					MIStackSelectFrame selectFrame = factory.createMIStackSelectFrame(miLevel);
					MIExecFinish finish = factory.createMIExecFinish();
					finish.setQuiet(true);
					try {
						miSession.postCommand(selectFrame);
						miSession.postCommand(finish);
					} catch (MIException e) {
						// ignore
					}
				} else {
					// if we are still at the same level in the backtrace
					// for example the StopEventLib was on a different thread
					// redo the last command.
					Command cmd = lastUserCommand;
					cmd.setQuiet(true);
					lastUserCommand = null;
					try {
						miSession.postCommand(cmd);
					} catch (MIException e) {
						// ignore
					}					
				}
				return true;
			} else if (lastUserCommand != null) {
				Command cmd = lastUserCommand;
				cmd.setQuiet(true);
				lastUserCommand = null;
				try {
					miSession.postCommand(cmd);
				} catch (MIException e) {
				}
				return true;
			}
		}
		return false;
	}

	boolean processBreakpointHitEvent(MIStoppedEvent stopped) {
//		Session session = (Session)getSession();
//		if (stopped instanceof MIBreakpointHitEvent) {
//			MIBreakpointHitEvent bpEvent = (MIBreakpointHitEvent)stopped;
//			BreakpointManager bpMgr = session.getBreakpointManager();
//			int bpNo = bpEvent.getNumber();
//		}
		return false;
	}

	/**
	 * Do any processing of before a running event.
	 */
	boolean processRunningEvent(MIRunningEvent running) {
		lastRunningEvent = running;

		Session session = (Session)getSession();
		MISession miSession = running.getMISession();
		Target currentTarget = session.getTarget(miSession);
		currentTarget.setSupended(false);

		// Bailout early if we do not want to process any events.
		if (!isAllowingProcessingEvents() || !running.propagate()) {
			return false;
		}

		return true;
	}

	public boolean isAllowingProcessingEvents() {
		return fAllowProcessingEvents;
	}

	public void allowProcessingEvents(boolean allowed) {
		fAllowProcessingEvents = allowed;
	}
}
