/*******************************************************************************
 * Copyright (c) 2013 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) 
 *******************************************************************************/
package org.eclipse.cdt.dsf.gdb.service;

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

import org.eclipse.cdt.core.IAddress;
import org.eclipse.cdt.dsf.concurrent.ConfinedToDsfExecutor;
import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
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.CLIShowEndianInfo;
import org.eclipse.cdt.dsf.mi.service.command.output.MIDataEvaluateExpressionInfo;
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 IGDBMemory {

	private IGDBControl fCommandControl;

	/**
	 * Cache of the address sizes for each memory context. 
	 */
	private Map<IMemoryDMContext, Integer> fAddressSizes = 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(),
				GDBMemory.class.getName(),
			},
			new Hashtable<String, String>());
		requestMonitor.done();
	}

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

	@Override
	protected void readMemoryBlock(final IDMContext dmc, IAddress address, 
		long offset, int word_size, int count, final DataRequestMonitor<MemoryByte[]> drm) {
		super.readMemoryBlock(
			dmc, 
			address, 
			offset, 
			word_size, 
			count, 
			new DataRequestMonitor<MemoryByte[]>(ImmediateExecutor.getInstance(), drm) {
				@Override
				@ConfinedToDsfExecutor("fExecutor")
				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 Step[] fSteps = new Step[] {
				new Step() {
					@Override
					public void execute(final RequestMonitor requestMonitor) {
						Integer addrSize = fAddressSizes.get(memContext);
						if (addrSize != null) {
							requestMonitor.done();
							return;
						}
						readAddressSize(
							memContext, 
							new DataRequestMonitor<Integer>(ImmediateExecutor.getInstance(), requestMonitor) {
								@Override
								@ConfinedToDsfExecutor("fExecutor")
								protected void handleSuccess() {
									fAddressSizes.put(memContext, getData());
									requestMonitor.done();
								}
							});
					}
				},
				
				new Step() {
					@Override
					public void execute(final RequestMonitor requestMonitor) {
						if ( fIsBigEndian != null) {
							requestMonitor.done();
							return;
						}
						readEndianness(
								memContext, 
								new DataRequestMonitor<Boolean>(ImmediateExecutor.getInstance(), requestMonitor) {
									@Override
									@ConfinedToDsfExecutor("fExecutor")
									protected void handleSuccess() {
										fIsBigEndian = getData();
										requestMonitor.done();
									}
								});
					}
				},
					
			};

			@Override
			public Step[] getSteps() {
				return fSteps;
			}
		});
	}

	@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);
			}
		}
	}

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

	@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;
	}

	protected void readAddressSize(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
				@ConfinedToDsfExecutor("fExecutor")
				protected void handleSuccess() {
					try {
						drm.setData(Integer.decode(getData().getValue()));
					}
					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();
				}
			});
	}

	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
				@ConfinedToDsfExecutor("fExecutor")
				protected void handleSuccess() {
					drm.setData(Boolean.valueOf(getData().isBigEndian()));
					drm.done();
				}
			});
	}
}
