/*******************************************************************************
 * Copyright (c) 2008, 2013 Wind River 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:
 *     Wind River Systems - initial API and implementation
 *     Ericsson		      - Modified for additional functionality
 *     Ericsson           - Version 7.0
 *     Nokia              - create and use backend service. 
 *     Ericsson           - Added IReverseControl support
 *     Marc Khouzam (Ericsson) - Added IReverseModeChangedDMEvent (Bug 399163)
 *     Marc Khouzam (Ericsson) - Started inheriting from GDBRunControl (Bug 405123)
 *******************************************************************************/

package org.eclipse.cdt.dsf.gdb.service;


import java.util.Hashtable;

import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.ImmediateRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.dsf.datamodel.AbstractDMEvent;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
import org.eclipse.cdt.dsf.datamodel.IDMContext;
import org.eclipse.cdt.dsf.debug.service.IRunControl;
import org.eclipse.cdt.dsf.debug.service.IRunControl2;
import org.eclipse.cdt.dsf.debug.service.IStack.IFrameDMContext;
import org.eclipse.cdt.dsf.debug.service.command.ICommand;
import org.eclipse.cdt.dsf.debug.service.command.ICommandControlService.ICommandControlDMContext;
import org.eclipse.cdt.dsf.gdb.internal.GdbPlugin;
import org.eclipse.cdt.dsf.gdb.service.IGDBTraceControl.ITraceRecordSelectedChangedDMEvent;
import org.eclipse.cdt.dsf.mi.service.IMICommandControl;
import org.eclipse.cdt.dsf.mi.service.IMIExecutionDMContext;
import org.eclipse.cdt.dsf.mi.service.IMIProcesses;
import org.eclipse.cdt.dsf.mi.service.IMIRunControl;
import org.eclipse.cdt.dsf.mi.service.MIRunControl;
import org.eclipse.cdt.dsf.mi.service.MIStack;
import org.eclipse.cdt.dsf.mi.service.command.CommandFactory;
import org.eclipse.cdt.dsf.mi.service.command.output.MIInfo;
import org.eclipse.cdt.dsf.service.DsfServiceEventHandler;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;

public class GDBRunControl_7_0 extends GDBRunControl implements IReverseRunControl {
	
	/** @since 4.2 */
	protected static class GdbReverseModeChangedDMEvent extends AbstractDMEvent<ICommandControlDMContext>
	implements IReverseModeChangedDMEvent {
		private boolean fIsEnabled;
		
		public GdbReverseModeChangedDMEvent(ICommandControlDMContext context, boolean enabled) {
			super(context);
			fIsEnabled = enabled;
		}
		
		@Override
		public boolean isReverseModeEnabled() {
			return fIsEnabled;
		}
	}

	private IMICommandControl fCommandControl;
    private IGDBBackend fGdb;
	private IMIProcesses fProcService;
	private CommandFactory fCommandFactory;

	private boolean fReverseSupported = true;
	private boolean fReverseStepping = false;
	private boolean fReverseModeEnabled = false;
	
	/**
	 * This variable allows us to know if run control operation
	 * should be enabled or disabled.  Run control operations are
	 * always enabled except when dealing with post-mortem debug
	 * session, or when visualizing tracepoints.
	 */
	private boolean fRunControlOperationsEnabled = true;

    public GDBRunControl_7_0(DsfSession session) {
        super(session);
    }
    
    @Override
    public void initialize(final RequestMonitor requestMonitor) {
        super.initialize(
            new ImmediateRequestMonitor(requestMonitor) { 
                @Override
                public void handleSuccess() {
                    doInitialize(requestMonitor);
                }});
    }

    private void doInitialize(final RequestMonitor requestMonitor) {
    	
        fGdb = getServicesTracker().getService(IGDBBackend.class);
        fProcService = getServicesTracker().getService(IMIProcesses.class);
        fCommandControl = getServicesTracker().getService(IMICommandControl.class);
        fCommandFactory = fCommandControl.getCommandFactory();

		if (fGdb.getSessionType() == SessionType.CORE) {
			// No execution for core files, so no support for reverse
			fRunControlOperationsEnabled = false;
			fReverseSupported = false;
		}

        register(new String[]{IRunControl.class.getName(),
        					  IRunControl2.class.getName(),
        					  IMIRunControl.class.getName(),
        					  MIRunControl.class.getName(),
        					  IReverseRunControl.class.getName(),
        					  GDBRunControl.class.getName(),
        					  GDBRunControl_7_0.class.getName()}, 
        		 new Hashtable<String,String>());
        requestMonitor.done();
    }

    @Override
    public void shutdown(final RequestMonitor requestMonitor) {
        unregister();
        super.shutdown(requestMonitor);
    }
    
    @Override
	protected boolean runControlOperationsEnabled() {
		return fRunControlOperationsEnabled;
	}

    @Override
    public void suspend(IExecutionDMContext context, final RequestMonitor rm){
        canSuspend(
            context, 
            new DataRequestMonitor<Boolean>(getExecutor(), rm) {
                @Override
                protected void handleSuccess() {
                    if (getData()) {
                        fGdb.interruptAndWait(getInterruptTimeout(), rm);
                    } else {
                        rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, INVALID_STATE, "Context cannot be suspended.", null)); //$NON-NLS-1$
                        rm.done();
                    }
                }
            });
    }

	@Override
    public void getExecutionContexts(IContainerDMContext containerDmc, final DataRequestMonitor<IExecutionDMContext[]> rm) {
		fProcService.getProcessesBeingDebugged(
				containerDmc,
				new DataRequestMonitor<IDMContext[]>(getExecutor(), rm) {
					@Override
					protected void handleSuccess() {
						if (getData() instanceof IExecutionDMContext[]) {
							IExecutionDMContext[] execDmcs = (IExecutionDMContext[])getData();
							rm.setData(execDmcs);
						} else {
							rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, INTERNAL_ERROR, "Invalid contexts", null)); //$NON-NLS-1$
						}
						rm.done();
					}
				});
    }
    
    /** @since 2.0 */
	@Override
	public void canReverseResume(IExecutionDMContext context, DataRequestMonitor<Boolean> rm) {
		if (!runControlOperationsEnabled()) {
			rm.setData(false);
			rm.done();
			return;
		}

		rm.setData(fReverseModeEnabled && doCanResume(context));
		rm.done();
	}

    /** @since 2.0 */
	@Override
	public void canReverseStep(final IExecutionDMContext context, StepType stepType, final DataRequestMonitor<Boolean> rm) {
		if (!runControlOperationsEnabled()) {
			rm.setData(false);
			rm.done();
			return;
		}

		if (context instanceof IContainerDMContext) {
    		rm.setData(false);
    		rm.done();
    		return;
    	}

    	if (stepType == StepType.STEP_RETURN) {
    		
    		// Check the stuff we know first, before going to the backend for 
    		// stack info
    		if (!fReverseModeEnabled || !doCanResume(context)) {
    	   		rm.setData(false);
        		rm.done();
        		return;    			
    		}
    		
    		// A step return will always be done in the top stack frame.
    		// If the top stack frame is the only stack frame, it does not make sense
    		// to do a step return since GDB will reject it.
            MIStack stackService = getServicesTracker().getService(MIStack.class);
            if (stackService != null) {
            	// Check that the stack is at least two deep.
            	stackService.getStackDepth(context, 2, new DataRequestMonitor<Integer>(getExecutor(), rm) {
            		@Override
            		public void handleCompleted() {
            			if (isSuccess() && getData() == 1) {
            				rm.setData(false);
            				rm.done();
            			} else {
            				canReverseResume(context, rm);
            			}
            		}
            	});
            	return;
            }
    	}

	   	canReverseResume(context, rm);		
	}

    /** @since 2.0 */
	@Override
	public boolean isReverseStepping(IExecutionDMContext context) {
		return !isTerminated() && fReverseStepping;
	}

    /** @since 2.0 */
	@Override
	public void reverseResume(final IExecutionDMContext context, final RequestMonitor rm) {
		if (fReverseModeEnabled && doCanResume(context)) {
			ICommand<MIInfo> cmd = null;
            if (context instanceof IContainerDMContext) {
            	cmd = fCommandFactory.createMIExecReverseContinue(context);
            } else {
        		IMIExecutionDMContext dmc = DMContexts.getAncestorOfType(context, IMIExecutionDMContext.class);
    			if (dmc == null){
    	            rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, INVALID_STATE, "Given context: " + context + " is not an execution context.", null)); //$NON-NLS-1$ //$NON-NLS-2$
    	            rm.done();
    	            return;
    			}
            	cmd = fCommandFactory.createMIExecReverseContinue(dmc);
            }

            setResumePending(true);
            // Cygwin GDB will accept commands and execute them after the step
            // which is not what we want, so mark the target as unavailable
            // as soon as we send a resume command.
            getCache().setContextAvailable(context, false);

            getConnection().queueCommand(cmd, new DataRequestMonitor<MIInfo>(getExecutor(), rm)  {
            	@Override
                public void handleFailure() {
           			setResumePending(false);
           			getCache().setContextAvailable(context, true);
            	}
            });
        } else {
            rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, INVALID_STATE, "Given context: " + context + ", is already running or reverse not enabled.", null)); //$NON-NLS-1$ //$NON-NLS-2$
            rm.done();
        }
		
	}

    /** @since 2.0 */
	@Override
	public void reverseStep(final IExecutionDMContext context, StepType stepType, final RequestMonitor rm) {
    	assert context != null;

    	IMIExecutionDMContext dmc = DMContexts.getAncestorOfType(context, IMIExecutionDMContext.class);
		if (dmc == null){
            rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, NOT_SUPPORTED, "Given context: " + context + " is not an execution context.", null)); //$NON-NLS-1$ //$NON-NLS-2$
            rm.done();
            return;
		}
    	
    	if (!fReverseModeEnabled || !doCanResume(context)) {
            rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, INVALID_STATE, "Cannot resume context", null)); //$NON-NLS-1$
            rm.done();
            return;
        }

        ICommand<MIInfo> cmd = null;
        switch(stepType) {
            case STEP_INTO:
            	cmd = fCommandFactory.createMIExecReverseStep(dmc, 1);
                break;
            case STEP_OVER:
                cmd = fCommandFactory.createMIExecReverseNext(dmc, 1);
                break;
            case STEP_RETURN:
                // The -exec-finish command operates on the selected stack frame, but here we always
                // want it to operate on the top stack frame.  So we manually create a top-frame
                // context to use with the MI command.
                // We get a local instance of the stack service because the stack service can be shut
                // down before the run control service is shut down.  So it is possible for the
                // getService() request below to return null.
                MIStack stackService = getServicesTracker().getService(MIStack.class);
                if (stackService != null) {
                    IFrameDMContext topFrameDmc = stackService.createFrameDMContext(dmc, 0);
                    cmd = fCommandFactory.createMIExecUncall(topFrameDmc);
                } else {
                    rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, NOT_SUPPORTED, "Cannot create context for command, stack service not available.", null)); //$NON-NLS-1$
                    rm.done();
                    return;
                }
                break;
            case INSTRUCTION_STEP_INTO:
                cmd = fCommandFactory.createMIExecReverseStepInstruction(dmc, 1);
                break;
            case INSTRUCTION_STEP_OVER:
                cmd = fCommandFactory.createMIExecReverseNextInstruction(dmc, 1);
                break;
            default:
                rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, INTERNAL_ERROR, "Given step type not supported", null)); //$NON-NLS-1$
                rm.done();
                return;
        }
        
    	setResumePending(true);
        fReverseStepping = true;
        getCache().setContextAvailable(context, false);

        getConnection().queueCommand(cmd, new DataRequestMonitor<MIInfo>(getExecutor(), rm)  {
        	@Override
        	public void handleFailure() {
        		setResumePending(false);
        		fReverseStepping = false;
        		getCache().setContextAvailable(context, true);
        	}
        });
	}

    /** @since 2.0 */
	@Override
	public void canEnableReverseMode(ICommandControlDMContext context, DataRequestMonitor<Boolean> rm) {
		rm.setData(fReverseSupported);
		rm.done();
	}

    /** @since 2.0 */
	@Override
    public void isReverseModeEnabled(ICommandControlDMContext context, DataRequestMonitor<Boolean> rm) {
		rm.setData(fReverseModeEnabled);
		rm.done();
	}

    /** @since 2.0 */
	@Override
    public void enableReverseMode(ICommandControlDMContext context, final boolean enable, final RequestMonitor rm) {
    	if (!fReverseSupported) {
            rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, NOT_SUPPORTED, "Reverse mode is not supported.", null)); //$NON-NLS-1$
    		rm.done();
    		return;
    	}
    	
    	if (fReverseModeEnabled == enable) {
    		rm.done();
    		return;
    	}
    	
    	getConnection().queueCommand(
    			fCommandFactory.createCLIRecord(context, enable),
    			new DataRequestMonitor<MIInfo>(getExecutor(), rm) {
    				@Override
    				public void handleSuccess() {
    					setReverseModeEnabled(enable);
    					rm.done();
    				}
    			});
	}

	// Overridden to use the new MI command -exec-jump
	/** @since 3.0 */
	@Override
	public void resumeAtLocation(IExecutionDMContext context, String location, RequestMonitor rm) {
		assert context != null;

		final IMIExecutionDMContext dmc = DMContexts.getAncestorOfType(context, IMIExecutionDMContext.class);
		if (dmc == null){
			rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, INTERNAL_ERROR, "Given context: " + context + " is not an thread execution context.", null)); //$NON-NLS-1$  //$NON-NLS-2$
			rm.done();
			return;
		}

		if (doCanResume(dmc)) {
			setResumePending(true);
			getCache().setContextAvailable(dmc, false);
			getConnection().queueCommand(
					// The MI command -exec-jump was added in GDB 7.0
					fCommandFactory.createMIExecJump(dmc, location),
					new DataRequestMonitor<MIInfo>(getExecutor(), rm) {
						@Override
						protected void handleFailure() {
							setResumePending(false);
							getCache().setContextAvailable(dmc, true);

							super.handleFailure();
						}
					});
		} else {
			rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, NOT_SUPPORTED,
					"Cannot resume given DMC.", null)); //$NON-NLS-1$
					rm.done();
		}		
	}

    /**
	 * @since 3.0
	 */
    @DsfServiceEventHandler 
    public void eventDispatched(ITraceRecordSelectedChangedDMEvent e) {
    	if (e.isVisualizationModeEnabled()) {
    		// We have started looking at trace records.  We can no longer
    		// do run control operations.
    		fRunControlOperationsEnabled = false;
    	} else {
    		// We stopped looking at trace data and gone back to debugger mode
    		fRunControlOperationsEnabled = true;
    	}
    }

    /** @since 2.0 */
    public void setReverseModeEnabled(boolean enabled) {
    	if (fReverseModeEnabled != enabled) {
    		fReverseModeEnabled = enabled;
    		getSession().dispatchEvent(new GdbReverseModeChangedDMEvent(fCommandControl.getContext(), fReverseModeEnabled), 
    				getProperties());
    	}
    }
}
