/*******************************************************************************
 * Copyright (c) 2013 Ericsson 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:
 * Marc Khouzam (Ericsson) - Initial API and implementation
 *******************************************************************************/

package org.eclipse.cdt.tests.dsf.gdb.tests.tests_7_6;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.eclipse.cdt.debug.core.ICDTLaunchConfigurationConstants;
import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.ImmediateRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.Query;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
import org.eclipse.cdt.dsf.datamodel.IDMContext;
import org.eclipse.cdt.dsf.datamodel.IDMEvent;
import org.eclipse.cdt.dsf.debug.service.IExpressions;
import org.eclipse.cdt.dsf.debug.service.IExpressions.IExpressionDMAddress;
import org.eclipse.cdt.dsf.debug.service.IExpressions.IExpressionDMContext;
import org.eclipse.cdt.dsf.debug.service.IFormattedValues;
import org.eclipse.cdt.dsf.debug.service.IMemory;
import org.eclipse.cdt.dsf.debug.service.IMemory.IMemoryChangedEvent;
import org.eclipse.cdt.dsf.debug.service.IMemory.IMemoryDMContext;
import org.eclipse.cdt.dsf.debug.service.IRunControl;
import org.eclipse.cdt.dsf.debug.service.IStack.IFrameDMContext;
import org.eclipse.cdt.dsf.gdb.service.IReverseRunControl;
import org.eclipse.cdt.dsf.gdb.service.IReverseRunControl.IReverseModeChangedDMEvent;
import org.eclipse.cdt.dsf.gdb.service.command.IGDBControl;
import org.eclipse.cdt.dsf.mi.service.command.events.MIStoppedEvent;
import org.eclipse.cdt.dsf.mi.service.command.output.MIInfo;
import org.eclipse.cdt.dsf.service.DsfServiceEventHandler;
import org.eclipse.cdt.dsf.service.DsfServicesTracker;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.cdt.tests.dsf.gdb.framework.BackgroundRunner;
import org.eclipse.cdt.tests.dsf.gdb.framework.BaseTestCase;
import org.eclipse.cdt.tests.dsf.gdb.framework.SyncUtil;
import org.eclipse.cdt.tests.dsf.gdb.launching.TestsPlugin;
import org.eclipse.cdt.tests.dsf.gdb.tests.ITestConstants;
import org.eclipse.debug.core.model.MemoryByte;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * This test case verifies that different commands issued from the
 * GDB console cause proper updating within the CDT views.
 */
@RunWith(BackgroundRunner.class)
public class GDBConsoleSynchronizingTest_7_6 extends BaseTestCase {
	final static private String EXEC_NAME = "ConsoleSyncTestApp.exe";

	final static private int DEFAULT_TIMEOUT = 1000;
	final static private TimeUnit DEFAULT_TIME_UNIT = TimeUnit.MILLISECONDS;
	final static private String NEW_VAR_VALUE = "0x12345678";
	final static private int NEW_VAR_SIZE = 4; // The number of bytes of NEW_VAR_VALUE
	final static private byte[] NEW_MEM = { 0x12, 0x34, 0x56, 0x78 }; // The individual bytes of NEW_VAR_VALUE

	private DsfSession fSession;
	private DsfServicesTracker fServicesTracker;
	private IGDBControl fCommandControl;
	private IMemory fMemoryService;
	private IExpressions fExprService;
	private IRunControl fRunControl;

	private List<IDMEvent<? extends IDMContext>> fEventsReceived = new ArrayList<IDMEvent<? extends IDMContext>>();

    @Override
	protected void setGdbVersion() {
		setGdbProgramNamesLaunchAttributes(ITestConstants.SUFFIX_GDB_7_6);
	}
    
    @Override
    protected void setLaunchAttributes() {
    	super.setLaunchAttributes();

    	setLaunchAttribute(ICDTLaunchConfigurationConstants.ATTR_PROGRAM_NAME, EXEC_PATH + EXEC_NAME);
    }

    @Override
    public void doBeforeTest() throws Exception {
    	super.doBeforeTest();

    	fSession = getGDBLaunch().getSession();
        Runnable runnable = new Runnable() {
            @Override
			public void run() {
                fServicesTracker = new DsfServicesTracker(TestsPlugin.getBundleContext(), fSession.getId());
                Assert.assertTrue(fServicesTracker != null);
        		    
                fCommandControl = fServicesTracker.getService(IGDBControl.class);
                Assert.assertTrue(fCommandControl != null);

                fMemoryService = fServicesTracker.getService(IMemory.class);
                Assert.assertTrue(fMemoryService != null);

                fExprService = fServicesTracker.getService(IExpressions.class);
                Assert.assertTrue(fExprService != null);

                fRunControl = fServicesTracker.getService(IRunControl.class);
                Assert.assertTrue(fRunControl != null);

                // Register to breakpoint events
                fSession.addServiceEventListener(GDBConsoleSynchronizingTest_7_6.this, null);
            }
        };
        fSession.getExecutor().submit(runnable).get();
    }

    @Override
    public void doAfterTest() throws Exception {
        Runnable runnable = new Runnable() {
            @Override
			public void run() {
            	fSession.removeServiceEventListener(GDBConsoleSynchronizingTest_7_6.this);
            }
        };
        fSession.getExecutor().submit(runnable).get();
        fEventsReceived.clear();
        fServicesTracker.dispose();
        super.doAfterTest();
    }

	//////////////////////////////////////////////////////////////////////////////////////
	// Start of tests
	//////////////////////////////////////////////////////////////////////////////////////

    /**
     * This test verifies that setting a variable from the console
     * using the set command will properly trigger a DSF event to 
     * indicate the change.  This test makes sure the value that
     * changes is in the memory cache also.
     */
	@Test
	public void testSettingVariableWithSetWithMemory() throws Throwable {
        MIStoppedEvent stoppedEvent = SyncUtil.runToLocation("testMemoryChanges");
        
        final IFrameDMContext frameDmc = SyncUtil.getStackFrame(stoppedEvent.getDMContext(), 0);
        final IExpressionDMContext exprDmc = SyncUtil.createExpression(frameDmc, "i");

        // Read the memory that will change first, or else there will be no event for it
		Query<IExpressionDMAddress> query = new Query<IExpressionDMAddress>() {
			@Override
			protected void execute(final DataRequestMonitor<IExpressionDMAddress> rm) {
				fExprService.getExpressionAddressData(exprDmc, rm);
			}
		};

		fSession.getExecutor().execute(query);
		IExpressionDMAddress data = query.get();
		
        IMemoryDMContext memoryDmc = DMContexts.getAncestorOfType(frameDmc, IMemoryDMContext.class);
        SyncUtil.readMemory(memoryDmc, data.getAddress(), 0, 1, NEW_VAR_SIZE);
        
        fEventsReceived.clear();
        
        String newValue = NEW_VAR_VALUE;
        queueConsoleCommand("set variable i=" + newValue);
        
        IMemoryChangedEvent memoryEvent = waitForEvent(IMemoryChangedEvent.class);
        assertEquals(1, memoryEvent.getAddresses().length);
        assertEquals(data.getAddress(), memoryEvent.getAddresses()[0]);

        // Now verify the memory service knows the new memory value
        MemoryByte[] memory = SyncUtil.readMemory(memoryDmc, data.getAddress(), 0, 1, NEW_VAR_SIZE);
        assertEquals(NEW_VAR_SIZE, memory.length);
        for (int i=0; i<NEW_VAR_SIZE; i++) {
        	if (memory[0].isBigEndian()) {
        		assertEquals(NEW_MEM[i], memory[i].getValue());
        	} else {
        		assertEquals(NEW_MEM[i], memory[NEW_VAR_SIZE-1-i].getValue());
        	}
        }
        
        // Now verify the expressions service knows the new value
        String exprValue = SyncUtil.getExpressionValue(exprDmc, IFormattedValues.HEX_FORMAT);
        assertEquals(newValue, exprValue);
	}

	private void testSettingVariableWithCommon(String commandPrefix) throws Throwable {
		MIStoppedEvent stoppedEvent = SyncUtil.runToLocation("testMemoryChanges");

		final IFrameDMContext frameDmc = SyncUtil.getStackFrame(stoppedEvent.getDMContext(), 0);
		final IExpressionDMContext exprDmc = SyncUtil.createExpression(frameDmc, "i");

		// Read the memory that will change first, or else there will be no event for it
		Query<IExpressionDMAddress> query = new Query<IExpressionDMAddress>() {
			@Override
			protected void execute(final DataRequestMonitor<IExpressionDMAddress> rm) {
				fExprService.getExpressionAddressData(exprDmc, rm);
			}
		};

		fSession.getExecutor().execute(query);
		IExpressionDMAddress data = query.get();
		
		fEventsReceived.clear();

		final String newValue = NEW_VAR_VALUE;
		queueConsoleCommand(commandPrefix + " = " + newValue);

		IMemoryChangedEvent memoryEvent = waitForEvent(IMemoryChangedEvent.class);
		assertEquals(1, memoryEvent.getAddresses().length);
		assertEquals(data.getAddress(), memoryEvent.getAddresses()[0]);

		// Now verify the memory service knows the new memory value
		IMemoryDMContext memoryDmc = DMContexts.getAncestorOfType(frameDmc, IMemoryDMContext.class);
		MemoryByte[] memory = SyncUtil.readMemory(memoryDmc, data.getAddress(), 0, 1, NEW_VAR_SIZE);
		assertEquals(NEW_VAR_SIZE, memory.length);
		for (int i=0; i<NEW_VAR_SIZE; i++) {
			if (memory[0].isBigEndian()) {
				assertEquals(NEW_MEM[i], memory[i].getValue());
			} else {
				assertEquals(NEW_MEM[i], memory[NEW_VAR_SIZE-1-i].getValue());
			}
		}

		// Now verify the expressions service knows the new value
		String exprValue = SyncUtil.getExpressionValue(exprDmc, IFormattedValues.HEX_FORMAT);
		assertEquals(newValue, exprValue);
	}

    /**
     * This test verifies that setting a variable from the console
     * using the set command will properly trigger a DSF event to
     * indicate the change, when the address is not in the memory cache.
     */
	@Test
	public void testSettingVariableWithSet() throws Throwable {
		testSettingVariableWithCommon("set variable i");
	}

    /**
     * This test verifies that setting a variable from the console
     * using the print command will properly trigger a DSF event 
     * to indicate the change.
     */
	@Test
	public void testSettingVariableWithPrint() throws Throwable {
		testSettingVariableWithCommon("print i");
	}

	/**
     * This test verifies that setting a memory location from the 
     * console will properly trigger a DSF event to indicate the change.
     */
	@Test
	public void testSettingMemory() throws Throwable {
        MIStoppedEvent stoppedEvent = SyncUtil.runToLocation("testMemoryChanges");
        
        final IFrameDMContext frameDmc = SyncUtil.getStackFrame(stoppedEvent.getDMContext(), 0);
        final IExpressionDMContext exprDmc = SyncUtil.createExpression(frameDmc, "i");

        // Read the memory that will change first, or else there will be no event for it
        Query<IExpressionDMAddress> query = new Query<IExpressionDMAddress>() {
			@Override
			protected void execute(final DataRequestMonitor<IExpressionDMAddress> rm) {
				fExprService.getExpressionAddressData(exprDmc, rm);
			}
		};

		fSession.getExecutor().execute(query);
		IExpressionDMAddress data = query.get();
		
        fEventsReceived.clear();

        final String newValue = NEW_VAR_VALUE;
        queueConsoleCommand("set {int}&i=" + newValue);

        IMemoryChangedEvent memoryEvent = waitForEvent(IMemoryChangedEvent.class);
        assertEquals(1, memoryEvent.getAddresses().length);
        assertEquals(data.getAddress(), memoryEvent.getAddresses()[0]);
        
        // Now verify the memory service knows the new memory value
        IMemoryDMContext memoryDmc = DMContexts.getAncestorOfType(frameDmc, IMemoryDMContext.class);
        MemoryByte[] memory = SyncUtil.readMemory(memoryDmc, data.getAddress(), 0, 1, NEW_VAR_SIZE);
        assertEquals(NEW_VAR_SIZE, memory.length);
        for (int i=0; i<NEW_VAR_SIZE; i++) {
        	if (memory[0].isBigEndian()) {
        		assertEquals(NEW_MEM[i], memory[i].getValue());
        	} else {
        		assertEquals(NEW_MEM[i], memory[NEW_VAR_SIZE-1-i].getValue());
        	}
        }

        // Now verify the expressions service knows the new value
        String exprValue = SyncUtil.getExpressionValue(exprDmc, IFormattedValues.HEX_FORMAT);
        assertEquals(newValue, exprValue);
	}
	
	/**
     * This test verifies that enabling reverse debugging from the 
     * console will properly trigger a DSF event to indicate the change and
     * will be processed by the service.
     */
	@Test
	public void testEnableRecord() throws Throwable {
		assertTrue("Reverse debugging is not supported", fRunControl instanceof IReverseRunControl);
		final IReverseRunControl reverseService = (IReverseRunControl)fRunControl;
		
		SyncUtil.runToLocation("testMemoryChanges");

		// check starting state
		Query<Boolean> query = new Query<Boolean>() {
			@Override
			protected void execute(final DataRequestMonitor<Boolean> rm) {
				reverseService.isReverseModeEnabled(fCommandControl.getContext(), rm); 
			}
		};

		fSession.getExecutor().execute(query);
		Boolean enabled = query.get();
		assertTrue("Reverse debugging should not be enabled", !enabled);
		
        fEventsReceived.clear();

        queueConsoleCommand("record");

        // Wait for the event
        IReverseModeChangedDMEvent event = waitForEvent(IReverseModeChangedDMEvent.class);
        assertEquals(true, event.isReverseModeEnabled());
        
        // Check the service
        query = new Query<Boolean>() {
			@Override
			protected void execute(final DataRequestMonitor<Boolean> rm) {
				reverseService.isReverseModeEnabled(fCommandControl.getContext(), rm); 
			}
		};
		fSession.getExecutor().execute(query);
		enabled = query.get();
		assertTrue("Reverse debugging should be enabled", enabled);
	}

	/**
     * This test verifies that disabling reverse debugging from the 
     * console will properly trigger a DSF event to indicate the change and
     * will be processed by the service.
     */
	@Test
	public void testDisableRecord() throws Throwable {
		assertTrue("Reverse debugging is not supported", fRunControl instanceof IReverseRunControl);
		final IReverseRunControl reverseService = (IReverseRunControl)fRunControl;
		
		SyncUtil.runToLocation("testMemoryChanges");

		fEventsReceived.clear();

		// check starting state
		Query<Boolean> query = new Query<Boolean>() {
			@Override
			protected void execute(final DataRequestMonitor<Boolean> rm) {
				reverseService.enableReverseMode(fCommandControl.getContext(), true,
						                         new ImmediateRequestMonitor(rm) {
					@Override
					protected void handleSuccess() {
						reverseService.isReverseModeEnabled(fCommandControl.getContext(), rm); 						
					}
				});
			}
		};

		fSession.getExecutor().execute(query);
		Boolean enabled = query.get();
		assertTrue("Reverse debugging should be enabled", enabled);
		
        // Wait for the event to avoid confusing it with the next one
        IReverseModeChangedDMEvent event = waitForEvent(IReverseModeChangedDMEvent.class);
        assertEquals(true, event.isReverseModeEnabled());
		fEventsReceived.clear();

        queueConsoleCommand("record stop");

        // Wait for the event
        event = waitForEvent(IReverseModeChangedDMEvent.class);
        assertEquals(false, event.isReverseModeEnabled());
        
        // Check the service
        query = new Query<Boolean>() {
			@Override
			protected void execute(final DataRequestMonitor<Boolean> rm) {
				reverseService.isReverseModeEnabled(fCommandControl.getContext(), rm); 
			}
		};
		fSession.getExecutor().execute(query);
		enabled = query.get();
		assertTrue("Reverse debugging should not be enabled", !enabled);
	}

	//////////////////////////////////////////////////////////////////////////////////////
	// End of tests
	//////////////////////////////////////////////////////////////////////////////////////
  	
  	@DsfServiceEventHandler
	public void eventDispatched(IDMEvent<? extends IDMContext> e) {
  		synchronized(this) {
  			fEventsReceived.add(e);
  			notifyAll();
  		}
  	}

  	private void queueConsoleCommand(String command) throws Throwable {
  		queueConsoleCommand(command, DEFAULT_TIMEOUT, DEFAULT_TIME_UNIT);
  	}

  	private void queueConsoleCommand(final String command, int timeout, TimeUnit unit) throws Throwable {
		Query<MIInfo> query = new Query<MIInfo>() {
			@Override
			protected void execute(DataRequestMonitor<MIInfo> rm) {
				fCommandControl.queueCommand(
					fCommandControl.getCommandFactory().createMIInterpreterExecConsole(
						fCommandControl.getContext(), 
						command),
					rm);
			}
		};
		fSession.getExecutor().execute(query);
		query.get(timeout, unit);
  	}
  	
  	private <V extends IDMEvent<? extends IDMContext>> V waitForEvent(Class<V> eventType) throws Exception {
  		return waitForEvent(eventType, DEFAULT_TIMEOUT);
  	}

  	@SuppressWarnings("unchecked")
	private <V extends IDMEvent<? extends IDMContext>> V waitForEvent(Class<V> eventType, int timeout) throws Exception {
  		IDMEvent<?> event = getEvent(eventType);
  		if (event == null) {
			synchronized(this) {
				try {
					wait(timeout);
				} 
				catch (InterruptedException ex) {
				}
			}
			event = getEvent(eventType);
			if (event == null) {
				throw new Exception(String.format("Timed out waiting for '%s' to occur.", eventType.getName()));
			}
		}
  		return (V)event;
	}
  	
  	@SuppressWarnings("unchecked")
	private synchronized <V extends IDMEvent<? extends IDMContext>> V getEvent(Class<V> eventType) {
		for (IDMEvent<?> e : fEventsReceived) {
			if (eventType.isAssignableFrom(e.getClass())) {
				fEventsReceived.remove(e);
				return (V)e;
			}
		}
		return null;
  	}

}
