/*******************************************************************************
 * Copyright (c) 2013, 2014 Mentor Graphics 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:
 * 		Mentor Graphics - Initial API and implementation
 * 		John Dallaway - Add methods to get the endianness and address size (Bug 225609) 
 * 		Philippe Gil (AdaCore) - Switch to c language when getting sizeof(void *) when required (Bug 421541)
 *      Alvaro Sanchez-Leon (Ericsson AB) - [Memory] Support 16 bit addressable size (Bug 426730)
 *******************************************************************************/
package org.eclipse.cdt.dsf.gdb.service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

import org.eclipse.cdt.core.IAddress;
import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.ImmediateDataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.ImmediateExecutor;
import org.eclipse.cdt.dsf.concurrent.ImmediateRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.dsf.concurrent.Sequence;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
import org.eclipse.cdt.dsf.datamodel.IDMContext;
import org.eclipse.cdt.dsf.debug.service.IExpressions;
import org.eclipse.cdt.dsf.debug.service.IExpressions.IExpressionDMContext;
import org.eclipse.cdt.dsf.debug.service.IMemory;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IContainerDMContext;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IExitedDMEvent;
import org.eclipse.cdt.dsf.gdb.internal.GdbPlugin;
import org.eclipse.cdt.dsf.gdb.service.command.IGDBControl;
import org.eclipse.cdt.dsf.mi.service.MIMemory;
import org.eclipse.cdt.dsf.mi.service.command.CommandFactory;
import org.eclipse.cdt.dsf.mi.service.command.output.CLIAddressableSizeInfo;
import org.eclipse.cdt.dsf.mi.service.command.output.CLIShowEndianInfo;
import org.eclipse.cdt.dsf.mi.service.command.output.MIDataEvaluateExpressionInfo;
import org.eclipse.cdt.dsf.mi.service.command.output.MIGDBShowLanguageInfo;
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;
import org.eclipse.debug.core.model.MemoryByte;

/**
 * @since 4.2
 */
public class GDBMemory extends MIMemory implements IGDBMemory2 {

	private IGDBControl fCommandControl;

	/**
	 * Cache of the address sizes for each memory context. 
	 */
	private Map<IMemoryDMContext, Integer> fAddressSizes = new HashMap<IMemoryDMContext, Integer>();

	/**
	 * Cache of the addressable sizes for each memory context. 
	 */
	private Map<IMemoryDMContext, Integer> fAddressableSizes = new HashMap<IMemoryDMContext, Integer>();
	
	/**
	 * We assume the endianness is the same for all processes because GDB supports only one target.
	 */
	private Boolean fIsBigEndian;

	public GDBMemory(DsfSession session) {
		super(session);
	}

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

	private void doInitialize(final RequestMonitor requestMonitor) {
		fCommandControl = getServicesTracker().getService(IGDBControl.class);
		getSession().addServiceEventListener(this, null);
		register(
			new String[] { 
				IMemory.class.getName(),
				MIMemory.class.getName(),
				IGDBMemory.class.getName(),
				IGDBMemory2.class.getName(),
				GDBMemory.class.getName(),
			},
			new Hashtable<String, String>());
		requestMonitor.done();
	}

	@Override
	public void shutdown(RequestMonitor requestMonitor) {
		unregister();
    	getSession().removeServiceEventListener(this);
    	fAddressableSizes.clear();
		fAddressSizes.clear();
		super.shutdown(requestMonitor);
	}

	@Override
	protected void readMemoryBlock(final IDMContext dmc, IAddress address, 
		long offset, int word_size, int word_count, final DataRequestMonitor<MemoryByte[]> drm) {
		super.readMemoryBlock(
			dmc, 
			address, 
			offset, 
			word_size, 
			word_count, 
			new DataRequestMonitor<MemoryByte[]>(ImmediateExecutor.getInstance(), drm) {
				@Override
				protected void handleSuccess() {
					IMemoryDMContext memDmc = DMContexts.getAncestorOfType(dmc, IMemoryDMContext.class);
					if (memDmc != null) {
						boolean bigEndian = isBigEndian(memDmc);
						for (MemoryByte b : getData()) {
							b.setBigEndian(bigEndian);
							b.setEndianessKnown(true);
						}
					}
					drm.setData(getData());
					drm.done();
				}
			});
	}

	@Override
	public void initializeMemoryData(final IMemoryDMContext memContext, RequestMonitor rm) {

		ImmediateExecutor.getInstance().execute(new Sequence(getExecutor(), rm) {

			private String originalLanguage = MIGDBShowLanguageInfo.AUTO;
			private boolean abortLanguageSteps = false;

			// Need a global here as getSteps() can be called more than once.
			private Step[] steps = null;
			private void determineSteps()
			{
				ArrayList<Step> stepsList = new ArrayList<Step>();
				
				if (fAddressSizes.get(memContext) == null) {
					stepsList.add(
							new Step() {
								// store original language
								@Override
								public void execute(final RequestMonitor requestMonitor) {
									fCommandControl.queueCommand(
										fCommandControl.getCommandFactory().createMIGDBShowLanguage(memContext),
										new ImmediateDataRequestMonitor<MIGDBShowLanguageInfo>(requestMonitor) {
											@Override
											protected void handleCompleted() {
												if (isSuccess()) {
													originalLanguage = getData().getLanguage();
												} else {
													abortLanguageSteps = true;
												}
												requestMonitor.done();
											}
										});
								}
							});
					stepsList.add(
							new Step() {
								// switch to c language
								@Override
								public void execute(final RequestMonitor requestMonitor) {
									if (abortLanguageSteps) {
										requestMonitor.done();
										return;
									}

									fCommandControl.queueCommand(
											fCommandControl.getCommandFactory().createMIGDBSetLanguage(memContext, MIGDBShowLanguageInfo.C),
											new ImmediateDataRequestMonitor<MIInfo>(requestMonitor) {
												@Override
												protected void handleCompleted() {
													if (!isSuccess()) {
														abortLanguageSteps = true;
													}
													// Accept failure
													requestMonitor.done();
												}
											});
								}
							});
					
					stepsList.add(new Step() {
						// Run this step even if the language commands where aborted, but accept failures.
						// Resolve Addressable and Address size
						@Override
						public void execute(final RequestMonitor requestMonitor) {
							//Read Minimum addressable memory size and actual address size
							readAddressableSize(memContext, new ImmediateDataRequestMonitor<Integer>(requestMonitor) {
								@Override
								protected void handleCompleted() {
									if (isSuccess()) {
										final Integer minAddressableInOctets = getData();
										//Preserve the addressable size per context
										fAddressableSizes.put(memContext, minAddressableInOctets);
									}
									
									readAddressSize(memContext, new ImmediateDataRequestMonitor<Integer>(requestMonitor) {
										@Override
										protected void handleCompleted() {
											if (isSuccess()) {
												//Preserve the address size per context
												fAddressSizes.put(memContext, getData());
											}

											// Accept failures
											requestMonitor.done();
										}
									});
								}
							});
						}
					});
					
					stepsList.add(
							new Step() {
								// restore original language
								@Override
								public void execute(final RequestMonitor requestMonitor) {
									if (abortLanguageSteps) {
										requestMonitor.done();
										return;
									}

									fCommandControl.queueCommand(
										fCommandControl.getCommandFactory().createMIGDBSetLanguage(memContext, originalLanguage),
										new ImmediateDataRequestMonitor<MIInfo>(requestMonitor) {
											@Override
											protected void handleCompleted() {
												if (!isSuccess()) {
													// If we are unable to set the original language back things could be bad.
													// Let's try setting it to "auto" as a fall back. Log the situation as info.
													GdbPlugin.log(getStatus());
													
													fCommandControl.queueCommand(
															fCommandControl.getCommandFactory().createMIGDBSetLanguage(memContext, MIGDBShowLanguageInfo.AUTO),
															new ImmediateDataRequestMonitor<MIInfo>(requestMonitor) {
																@Override
																protected void handleCompleted() {
																	if (!isSuccess()) {
																		// This error could be bad because we've changed the language to C
																		// but are unable to switch it back. Log the error.
																		// If the language happens to be C anyway, everything will
																		// continue to work, which is why we don't abort the sequence
																		// (which would cause the entire session to fail).
																		GdbPlugin.log(getStatus());
																	}
																	// Accept failure
																	requestMonitor.done();
																}
															});
												} else {
													requestMonitor.done();
												}
											}
										});
								}
							});
					
				}

				if (fIsBigEndian == null) {
					stepsList.add(
						new Step() {
							// read endianness
							@Override
							public void execute(final RequestMonitor requestMonitor) {
								readEndianness(
										memContext, 
										new ImmediateDataRequestMonitor<Boolean>(requestMonitor) {
											@Override
											protected void handleCompleted() {
												if (isSuccess()) {
													fIsBigEndian = getData();
												}
												// Accept failure
												requestMonitor.done();
											}
										});
								}
							});
				}
				
				steps = stepsList.toArray(new Step[stepsList.size()]);
			}

			@Override
			public Step[] getSteps() {
				if (steps == null) {
					determineSteps();
				}

				return steps;
			}
		});
	}

	@DsfServiceEventHandler
	public void eventDispatched(IExitedDMEvent event) {
		if (event.getDMContext() instanceof IContainerDMContext) {
			IMemoryDMContext context = DMContexts.getAncestorOfType(event.getDMContext(), IMemoryDMContext.class);
			if (context != null) {
				fAddressSizes.remove(context);
				fAddressableSizes.remove(context);
			}
		}
	}

	@Override
	public int getAddressSize(IMemoryDMContext context) {
		Integer addressSize = fAddressSizes.get(context);
		return (addressSize != null) ? addressSize.intValue() : 8;
	}

	/**
	 * @since 4.4
	 */
	@Override
	public int getAddressableSize(IMemoryDMContext context) {
		Integer addressableSize = fAddressableSizes.get(context);
		return (addressableSize != null) ? addressableSize.intValue() : 1;
	}

	@Override
	public boolean isBigEndian(IMemoryDMContext context) {
		assert fIsBigEndian != null;
		if (fIsBigEndian == null) {
    		GdbPlugin.log(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, "Endianness was never initialized!")); //$NON-NLS-1$
			return false;
		}
		return fIsBigEndian;
	}

	/**
	 * Address size is determined by space, in octets, used to store an address value (e.g. a pointer) on a target system. 
	 *
	 * <p>NOTE: Implementation requires addressable memory size to be known</p>
	 * @param memContext
	 * @param drm
	 *
	 * @see IGDBMemory#getAddressSize(org.eclipse.cdt.dsf.debug.service.IMemory.IMemoryDMContext)
	 * @see GDBMemory#readAddressableSize(org.eclipse.cdt.dsf.debug.service.IMemory.IMemoryDMContext, DataRequestMonitor)
	 */
	protected void readAddressSize(final IMemoryDMContext memContext, final DataRequestMonitor<Integer> drm) {
		IExpressions exprService = getServicesTracker().getService(IExpressions.class);
		IExpressionDMContext exprContext = exprService.createExpression(memContext, "sizeof (void*)"); //$NON-NLS-1$
		CommandFactory commandFactory = fCommandControl.getCommandFactory();
		fCommandControl.queueCommand(
			commandFactory.createMIDataEvaluateExpression(exprContext),
			new DataRequestMonitor<MIDataEvaluateExpressionInfo>(ImmediateExecutor.getInstance(), drm) {
				@Override
				protected void handleSuccess() {
					try {
						// 'sizeof' returns number of bytes (aka 'chars'). 
						// Multiply with byte size in octets to get storage required to hold a pointer.
						Integer ptrBytes = Integer.decode(getData().getValue());
						drm.setData(ptrBytes * getAddressableSize(memContext));
					}
					catch(NumberFormatException e) {
						drm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, String.format("Invalid address size: %s", getData().getValue()))); //$NON-NLS-1$
					}
					drm.done();
				}
			});
	}

	/**
	 * The minimum addressable size is determined by the space used to store a "char" on a target system
	 * This is then resolved by retrieving a hex representation of -1 casted to the size of a "char"
	 * e.g. from GDB command line 
	 *    > p/x (char)-1
     *    > $7 = 0xffff 
     *    
     * Since two hex characters are representing one octet, for the above example this method should return 2
	 * @since 4.4
	 * 
	 */
	protected void readAddressableSize(IMemoryDMContext memContext, final DataRequestMonitor<Integer> drm) {
		//We use a CLI command here instead of the expression services, since the target may not be available 
		//e.g. when using a remote launch.  
		// Using MI directly is a possibility although there is no way to specify the required output format to hex. 
		CommandFactory commandFactory = fCommandControl.getCommandFactory();
		fCommandControl.queueCommand(
			commandFactory.createCLIAddressableSize(memContext),
			new DataRequestMonitor<CLIAddressableSizeInfo>(ImmediateExecutor.getInstance(), drm) {
				@Override
				protected void handleSuccess() {
					drm.setData(Integer.valueOf(getData().getAddressableSize()));
					drm.done();
				}
			});
	}
	
	protected void readEndianness(IMemoryDMContext memContext, final DataRequestMonitor<Boolean> drm) {
		CommandFactory commandFactory = fCommandControl.getCommandFactory();
		fCommandControl.queueCommand(
			commandFactory.createCLIShowEndian(memContext),
			new DataRequestMonitor<CLIShowEndianInfo>(ImmediateExecutor.getInstance(), drm) {
				@Override
				protected void handleSuccess() {
					drm.setData(Boolean.valueOf(getData().isBigEndian()));
					drm.done();
				}
			});
	}
}
