/*******************************************************************************
 * Copyright (c) 2004 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials 
 * are made available under the terms of the Common Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.debug.internal.ui.views.memory;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.model.IDebugElement;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.core.model.IMemoryBlock;
import org.eclipse.debug.core.model.IMemoryBlockRetrieval;
import org.eclipse.debug.internal.core.memory.IExtendedMemoryBlock;
import org.eclipse.debug.internal.core.memory.IExtendedMemoryBlockRetrieval;
import org.eclipse.debug.internal.core.memory.MemoryByte;
import org.eclipse.debug.internal.ui.DebugUIMessages;
import org.eclipse.debug.internal.ui.DebugUIPlugin;
import org.eclipse.debug.internal.ui.IInternalDebugUIConstants;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.widgets.TabItem;

/**
 * Content provider for MemoryViewTab
 * 
 * @since 3.0
 */
public class MemoryViewContentProvider extends BasicDebugViewContentProvider {
	
	private static final String PREFIX = "MemoryViewContentProvider."; //$NON-NLS-1$
	private static final String UNABLE_TO_RETRIEVE_CONTENT = PREFIX + "Unable_to_retrieve_content"; //$NON-NLS-1$

	private static final String DEFAULT_PADDED_STR = "--"; //$NON-NLS-1$
		
	// cached information
	protected Vector lineCache;
	
	// keeps track of all memory line ever retrieved
	// allow us to compare and compute deltas
	protected Hashtable contentCache;
	
	private BigInteger fBufferTopAddress;
	private BigInteger fBaseAddress;
	
	private IMemoryBlock fMemoryBlock;
	private TabItem fTabItem;
	private MemoryViewTab fViewTab;
	private boolean fLockRefresh = false;
	
	// implementation of MemoryByte for IMemoryBlock Support
	private class MByte extends MemoryByte
	{
		protected MByte(byte value, byte flags)
		{
			this.value = value;
			this.flags = flags;
		}
		
		protected MByte()
		{
			
		}
	}
	
	/**
	 * @param memoryBlock
	 * @param newTab
	 */
	public MemoryViewContentProvider(IMemoryBlock memoryBlock, TabItem newTab)
	{
		fMemoryBlock = memoryBlock;
		fTabItem = newTab;
		lineCache = new Vector();
		contentCache = new Hashtable();
		
		fViewTab = (MemoryViewTab)fTabItem.getData();
			
		DebugPlugin.getDefault().addDebugEventListener(this);
	}
	
	/**
	 * @param viewer
	 */
	public void setViewer(StructuredViewer viewer)
	{
		fViewer = viewer;
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object)
	 */
	public void inputChanged(Viewer v, Object oldInput, Object newInput) {
	}

	public void dispose() {

		// fTabItem disposed by view tab
		
		DebugPlugin.getDefault().removeDebugEventListener(this);		
		
		super.dispose();
	}

	/* (non-Javadoc)
	 * @see org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java.lang.Object)
	 */
	public Object[] getElements(Object parent) {

		// if cache is empty, get memory
		if (lineCache.isEmpty()) { 
		
			try {
				if (fMemoryBlock instanceof IExtendedMemoryBlock)
				{
					// calculate top buffered address
					BigInteger address = ((IExtendedMemoryBlock)fMemoryBlock).getBigBaseAddress();
					
					if (address == null)
					{
						address = new BigInteger("0"); //$NON-NLS-1$
					}
					
					MemoryViewTab viewTab = (MemoryViewTab)fTabItem.getData();
					
					BigInteger bigInt = address;
					if (bigInt.compareTo(BigInteger.valueOf(32)) <= 0) {
						viewTab.TABLE_PREBUFFER = 0;
					} else {
						viewTab.TABLE_PREBUFFER = bigInt.divide(BigInteger.valueOf(32)).min(BigInteger.valueOf(viewTab.TABLE_DEFAULTBUFFER)).intValue();
					}
				
					address = bigInt.subtract(BigInteger.valueOf(fViewTab.getBytesPerLine()*viewTab.TABLE_PREBUFFER));
				
					// get stoarage to fit the memory view tab size
					getMemoryToFitTable(address, fViewTab.getNumberOfVisibleLines()+viewTab.TABLE_PREBUFFER+viewTab.TABLE_POSTBUFFER, true);
				}
				else
				{
					// get as much memory as the memory block can handle
					MemoryViewTab viewTab = (MemoryViewTab)fTabItem.getData();
					viewTab.TABLE_PREBUFFER=0;
					viewTab.TABLE_POSTBUFFER=0;
					viewTab.TABLE_DEFAULTBUFFER=0;
					
					long startAddress = fMemoryBlock.getStartAddress();					
					BigInteger address = BigInteger.valueOf(startAddress);
					
					long length = fMemoryBlock.getLength();
					long numLines = length / fViewTab.getBytesPerLine();
					
					getMemoryToFitTable(address, numLines, true);
				}
			} catch (DebugException e) {
				DebugUIPlugin.log(e.getStatus());
				((MemoryViewTab)fTabItem.getData()).displayError(e);
				return lineCache.toArray();
			}
		}
		return lineCache.toArray();
	}
	
	/**
	 * @return
	 */
	public IMemoryBlock getMemoryBlock() {
		return fMemoryBlock;
	}
	
	/**
	 * Get memory to fit table
	 * @param startingAddress
	 * @param numberOfLines
	 * @param updateDelta
	 * @throws DebugException
	 */
	public void getMemoryToFitTable(BigInteger startingAddress, long numberOfLines, boolean updateDelta) throws DebugException
	{
		boolean error = false;
		DebugException dbgEvt = null;
		
		// calculate address size
		String adjustedAddress = startingAddress.toString(16);
		
		int addressSize = getAddressSize(startingAddress);
		
		int addressLength = addressSize * IInternalDebugUIConstants.CHAR_PER_BYTE;

		// align starting address with double word boundary
		if (fMemoryBlock instanceof IExtendedMemoryBlock)
		{
			if (!adjustedAddress.endsWith("0")) //$NON-NLS-1$
			{
				adjustedAddress = adjustedAddress.substring(0, adjustedAddress.length() - 1);
				adjustedAddress += "0"; //$NON-NLS-1$
				startingAddress = new BigInteger(adjustedAddress, 16);
			}
		}

		IExtendedMemoryBlock extMemoryBlock = null;
		MemoryByte[] memoryBuffer = null;
		
		// required number of bytes
		long reqNumBytes = fViewTab.getBytesPerLine() * numberOfLines;
		String paddedString = DEFAULT_PADDED_STR;
		
		try
		{
			if (fMemoryBlock instanceof IExtendedMemoryBlock)
			{
				// get memory from memory block
				extMemoryBlock = (IExtendedMemoryBlock) fMemoryBlock;
				
				memoryBuffer =	extMemoryBlock.getBytesFromAddress(startingAddress,	reqNumBytes);
				
				if(memoryBuffer == null)
				{
					DebugException e = new DebugException(DebugUIPlugin.newErrorStatus(DebugUIMessages.getString(UNABLE_TO_RETRIEVE_CONTENT), null));
					throw e;
				}
				
				// get padded string
				IMemoryBlockRetrieval retrieval = extMemoryBlock.getMemoryBlockRetrieval();
				if (retrieval != null && retrieval instanceof IExtendedMemoryBlockRetrieval)
				{
					paddedString = ((IExtendedMemoryBlockRetrieval)retrieval).getPaddedString();
					
					if (paddedString == null)
					{
						paddedString = DEFAULT_PADDED_STR;
					}
				}
			}
			else
			{
				// get memory from memory block
				byte[] memory = fMemoryBlock.getBytes();
				
				if (memory == null)
				{
					DebugException e = new DebugException(DebugUIPlugin.newErrorStatus(DebugUIMessages.getString(UNABLE_TO_RETRIEVE_CONTENT), null));	
					throw e;					
				}
				
				// create memory byte for IMemoryBlock
				memoryBuffer = new MemoryByte[memory.length];
				for (int i=0; i<memory.length; i++)
				{
					MByte tmp = new MByte();
					tmp.value = memory[i];
					tmp.flags |= MemoryByte.VALID;
					memoryBuffer[i] = tmp;
				}
				
				paddedString = DEFAULT_PADDED_STR;
			}
		}
		catch (DebugException e)
		{
			memoryBuffer = makeDummyContent(numberOfLines);
			
			// finish creating the content provider before throwing an event
			error = true; 
			dbgEvt = e;
		}
		
		// if debug adapter did not return enough memory, create dummy memory
		if (memoryBuffer.length < reqNumBytes)
		{
			ArrayList newBuffer = new ArrayList();
			
			for (int i=0; i<memoryBuffer.length; i++)
			{
				newBuffer.add(memoryBuffer[i]);
			}
			
			for (int i=memoryBuffer.length; i<reqNumBytes; i++)
			{
				byte value = 0;
				byte flags = 0;
				flags |= MemoryByte.READONLY;
				newBuffer.add(new MByte(value, flags));
			}
			
		}
		
		// clear line cacheit'
		if (!lineCache.isEmpty())
		{
			lineCache.clear();
		}
		String address = startingAddress.toString(16);
		// save address of the top of buffer
		fBufferTopAddress = startingAddress;
		if (fMemoryBlock instanceof IExtendedMemoryBlock)
			fBaseAddress = ((IExtendedMemoryBlock) fMemoryBlock).getBigBaseAddress();
		else
			fBaseAddress = BigInteger.valueOf(fMemoryBlock.getStartAddress());
		
		if (fBaseAddress == null)
			fBaseAddress = new BigInteger("0"); //$NON-NLS-1$
		
			
		// update tab name in case base address has changed
		fViewTab.setTabName(fMemoryBlock, true);
		
		boolean manageDelta = true;
		
		// If change information is not managed by the memory block
		// The view tab will manage it and calculate delta information
		// for its content cache.
		if (fMemoryBlock instanceof IExtendedMemoryBlock)
		{
			manageDelta = !((IExtendedMemoryBlock)fMemoryBlock).isMemoryChangesManaged();
		}
			
		// put memory information into MemoryViewLine
		for (int i = 0; i < numberOfLines; i++)
		{ //chop the raw memory up 
			String tmpAddress = address.toUpperCase();
			if (tmpAddress.length() < addressLength)
			{
				for (int j = 0; tmpAddress.length() < addressLength; j++)
				{
					tmpAddress = "0" + tmpAddress; //$NON-NLS-1$
				}
			}
			MemoryByte[] memory = new MemoryByte[fViewTab.getBytesPerLine()];
			boolean isMonitored = true;
			
			// counter for memory, starts from 0 to number of bytes per line
			int k = 0;
			// j is the counter for memArray, memory returned by debug adapter
			for (int j = i * fViewTab.getBytesPerLine();
				j < i * fViewTab.getBytesPerLine() + fViewTab.getBytesPerLine();
				j++)
			{
				
				byte changeFlag = memoryBuffer[j].flags;
				if (manageDelta)
				{
					// turn off both change and unchanged bits to make sure that
					// the change bits returned by debug adapters do not take
					// any effect
					
					changeFlag |= MemoryByte.CHANGED;
					changeFlag ^= MemoryByte.CHANGED;
					
					changeFlag |= MemoryByte.UNCHANGED;
					changeFlag ^= MemoryByte.UNCHANGED;
				}
				
				MByte newByteObj = new MByte(memoryBuffer[j].value, changeFlag);
				memory[k] =  newByteObj;
				k++;
				
				
				if (!manageDelta)
				{
					// if byte is not changed nor unchanged, we do not currently have 
					// history for it.
					if ((memoryBuffer[j].flags & MemoryByte.CHANGED) == 0 &&
						(memoryBuffer[j].flags & MemoryByte.UNCHANGED) == 0)
					{
						isMonitored = false;
					}
				}
			}
			
			MemoryViewLine newLine = new MemoryViewLine(tmpAddress, memory, lineCache.size(), paddedString);
			
			MemoryViewLine oldLine = (MemoryViewLine)contentCache.get(newLine.getAddress());
			
			if (manageDelta)
			{
				if (oldLine != null)
					newLine.isMonitored = true;
			}
			else
			{
				// check the byte for information
				newLine.isMonitored = isMonitored;
			}
			
			// calculate delta info for the memory view line
			if (manageDelta && !fViewTab.isDisplayingError())
			{
				if (updateDelta)
				{
					if (oldLine != null)
					{
						newLine.markDeltas(oldLine);
					}
				}
				else
				{
					if (oldLine != null)
					{
						// deltas can only be reused if the line has not been changed
						// otherwise, force a refresh
						if (newLine.isLineChanged(oldLine))
						{
							newLine.markDeltas(oldLine);
						}
						else
						{
							newLine.copyDeltas(oldLine);
						}
					}
				}
			}
			else if (manageDelta && fViewTab.isDisplayingError())
			{
				// show as unmonitored if the view tab is previoulsy displaying error
				newLine.isMonitored = false;
			}
			lineCache.add(newLine);
			
//			// add to contentCache
//			contentCache.put(newLine.getAddress(), newLine);
			
			// increment row address
			BigInteger bigInt = new BigInteger(address, 16);
			address = bigInt.add(BigInteger.valueOf(fViewTab.getBytesPerLine())).toString(16);
		}
		
		if (error){
			throw dbgEvt;
		}
	}
	
	/**
	 * @param numberOfLines
	 * @return
	 */
	private MemoryByte[] makeDummyContent(long numberOfLines) {
		MemoryByte[] memoryBuffer;
		// make up dummy memory, needed for recovery in case the debug adapter
		// is capable of retrieving memory again

		int numBytes = (int)(fViewTab.getBytesPerLine() * numberOfLines);
		memoryBuffer = new MemoryByte[numBytes];
		
		for (int i=0; i<memoryBuffer.length; i++){
			memoryBuffer[i] = new MByte();
			memoryBuffer[i].value = 0;
			memoryBuffer[i].flags |= MemoryByte.READONLY;
		}
		return memoryBuffer;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.debug.ui.internal.views.BasicDebugViewContentProvider#doHandleDebugEvent(org.eclipse.debug.core.DebugEvent)
	 */
	protected void doHandleDebugEvent(DebugEvent event) {
		
		// do nothing if the debug event did not come from a debug element comes from non-debug element
		if (!(event.getSource() instanceof IDebugElement))
			return;
		
		IDebugElement src = (IDebugElement)event.getSource();
		
		// if a debug event happens from the memory block
		// invoke contentChanged to get content of the memory block updated
		if (event.getKind() == DebugEvent.CHANGE && event.getSource() == fMemoryBlock)
		{
			if (event.getDetail() == DebugEvent.STATE){
				fViewTab.updateLabels();
			}
			else
			{	
				updateContent();
			}
		}
		
		// if the suspend evnet happens from the debug target that the blocked
		// memory block belongs to
		if (event.getKind() == DebugEvent.SUSPEND && src.getDebugTarget() == fMemoryBlock.getDebugTarget())
		{	
			updateContent();
		}

	}
	
	/**
	 * Checks to see if the content needs to be refreshed
	 * TODO: this methd is never called
	 * @return
	 */
	protected boolean isRefreshNeeded()
	{	
		boolean refreshNeeded = false;
		try
		{	
			if (isBaseAddressChanged())
				return true;

			MemoryViewLine[] cached = (MemoryViewLine[])lineCache.toArray(new MemoryViewLine[lineCache.size()]);
							
			// convert IMemory to a flat array of MemoryBlockByte
			ArrayList newMemory = new ArrayList();
				
			if (!(fMemoryBlock instanceof IExtendedMemoryBlock))
			{
				byte[] memory = fMemoryBlock.getBytes();
				
				if (memory == null)
				{
					DebugException e = new DebugException(DebugUIPlugin.newErrorStatus(DebugUIMessages.getString(UNABLE_TO_RETRIEVE_CONTENT), null));
					throw e;					
				}
				
				// create memory byte for IMemoryBlock
				for (int i=0; i<memory.length; i++)
				{
					MByte tmp = new MByte();
					tmp.value = memory[i];
					tmp.flags |= MemoryByte.VALID;
					newMemory.add(tmp);
				}
			}
			else
			{		
			
				IExtendedMemoryBlock extMB = (IExtendedMemoryBlock)fMemoryBlock;
				
				MemoryByte[] memory = extMB.getBytesFromAddress(fBufferTopAddress, lineCache.size()*fViewTab.getBytesPerLine());
				
				if (memory == null)
				{
					DebugException e = new DebugException(DebugUIPlugin.newErrorStatus(DebugUIMessages.getString(UNABLE_TO_RETRIEVE_CONTENT), null));
					throw e;
				}
				
				for (int i=0; i<memory.length; i++)
				{
					newMemory.add(memory[i]);
				}				
			}		
							
			// compare each byte, if one of the bytes is not the same, refresh view tab
			for (int i=0; i<newMemory.size(); i++)
			{
				MemoryByte newByte = (MemoryByte)newMemory.get(i);

				if (i/fViewTab.getBytesPerLine() >= cached.length)
				{
					// if cache cannot be located, need refresh
					refreshNeeded = true;
					break;
				}
				
				MemoryViewLine lineToCheck = cached[i/fViewTab.getBytesPerLine()];
				MemoryByte oldByte = lineToCheck.getByte(i%fViewTab.getBytesPerLine());
				
				// if a byte becomes available or unavailable
				if ((newByte.flags & MemoryByte.VALID)!= (oldByte.flags & MemoryByte.VALID))
				{
					refreshNeeded = true;
					break;
				}
				if (((newByte.flags & MemoryByte.VALID) == MemoryByte.VALID) && 
						((oldByte.flags & MemoryByte.VALID)==MemoryByte.VALID))
				{
					// compare value if both bytes are available
					if (newByte.value != oldByte.value)
					{
						refreshNeeded = true;
						break;
					}
				}
			}
			
		}
		catch (DebugException e)
		{
			fViewTab.displayError(e);
			return false;
		}
		return refreshNeeded;
	}
	
	/**
	 * @return if the base address of the memory block has changed
	 */
	private boolean isBaseAddressChanged()
	{
		if (!(fMemoryBlock instanceof IExtendedMemoryBlock))
			return false;
		
		IExtendedMemoryBlock extMB = (IExtendedMemoryBlock)fMemoryBlock;
			
		// if base address has changed, refresh is needed
		BigInteger newBaseAddress = extMB.getBigBaseAddress();
		
		if (newBaseAddress == null)
		{	
			newBaseAddress = new BigInteger("0"); //$NON-NLS-1$
		}
		
		if (newBaseAddress.compareTo(fBaseAddress) != 0) {
			return true;
		}
		return false;			
	}
	
	public void forceRefresh()
	{
		if (!fLockRefresh) {
			fLockRefresh = true;
			refresh();
			fLockRefresh = false;
		}
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.debug.ui.internal.views.BasicDebugViewContentProvider#refresh()
	 */
	protected void refresh() {
		super.refresh();
	}

	
	/**
	 * Update content of the view tab if the content of the memory block has changed
	 * or if its base address has changed
	 * Update will not be performed if the memory block has not been changed or
	 * if the view tab is disabled.
	 */
	public void updateContent()
	{
		IDebugTarget dt = fMemoryBlock.getDebugTarget();
		
		// no need to update if debug target is disconnected or terminated
		if (dt.isDisconnected() || dt.isTerminated())
		{
			return;
		}
		
		// cache content before getting new ones
		MemoryViewLine[] lines =(MemoryViewLine[]) lineCache.toArray(new MemoryViewLine[lineCache.size()]);
		if (contentCache != null)
		{
			contentCache.clear();
		}
		
		//do not handle event if the tab has been disabled
		if (!fViewTab.isEnabled())
			 return;
		
		for (int i=0; i<lines.length; i++)
		{
			contentCache.put(lines[i].getAddress(), lines[i]);
			lines[i].isMonitored = true;
		}
		
		// if view tab is enabled but block is disabled, enable the block
		if (fViewTab.getMemoryBlock() instanceof IExtendedMemoryBlock)
		{
			if (!((IExtendedMemoryBlock)fViewTab.getMemoryBlock()).isEnabled())
			{
				((IExtendedMemoryBlock)fViewTab.getMemoryBlock()).enable();
			}
		}
		boolean updateTopAddress = false;
		// if base address has changed, set cursor back to the base address
		if (isBaseAddressChanged())
		{
			updateTopAddress = true;
			if (fMemoryBlock instanceof IExtendedMemoryBlock)
			{
				BigInteger address = ((IExtendedMemoryBlock)fMemoryBlock).getBigBaseAddress();
				
				if (address == null)
				{
					// do not change selected address if base address cannot be updated
					if (fViewTab.getSelectedAddress() != null)
						address = fViewTab.getSelectedAddress();
					else
						address = new BigInteger("0"); //$NON-NLS-1$
				}
				
				fViewTab.setSelectedAddress(address, true);
			}
			else
			{
				BigInteger address = BigInteger.valueOf(fMemoryBlock.getStartAddress());
				fViewTab.setSelectedAddress(address, true);
			}	
		}			
		// reset all the deltas currently stored in contentCache
		// This will ensure that changes will be recomputed when user scrolls
		// up or down the memory view.		
		resetDeltas();
		fViewTab.refresh();
		
		if (updateTopAddress)
		{
			// top visible address may have been changed if base address has changed
			fViewTab.updateSyncTopAddress(true);
		}
	}

	/**
	 * @return
	 */
	public BigInteger getBufferTopAddress()
	{
		return fBufferTopAddress;
	}
	
	/**
	 * Calculate address size of the given address
	 * @param address
	 * @return
	 */
	public int getAddressSize(BigInteger address)
	{
		// calculate address size
		 String adjustedAddress = address.toString(16);
		
		 int addressSize = 0;
		 if (fMemoryBlock instanceof IExtendedMemoryBlock)
		 {
			 addressSize = ((IExtendedMemoryBlock)fMemoryBlock).getAddressSize();
		 }
		
		 // handle IMemoryBlock and invalid address size returned by IExtendedMemoryBlock
		 if (addressSize <= 0)
		 {
			 if (adjustedAddress.length() > 8)
			 {
				 addressSize = 8;
			 }
			 else
			 {
				 addressSize = 4;
			 }			
		 }		
		 
		 return addressSize;
	}
	
	/**
	 * @return base address of memory block
	 */
	public BigInteger getContentBaseAddress()
	{
		return fBaseAddress; 
	}
	
	/**
	 * Clear all delta information in the lines
	 */
	public void resetDeltas()
	{
		Enumeration enum = contentCache.elements();
		
		while (enum.hasMoreElements())
		{
			MemoryViewLine line = (MemoryViewLine)enum.nextElement();
			line.unmarkDeltas();
		}
	}
	
	/**
	 * Check if address is out of buffered range
	 * @param address
	 * @return
	 */
	protected boolean isAddressOutOfRange(BigInteger address)
	{
		if (lineCache != null)
		{
			MemoryViewLine first = (MemoryViewLine)lineCache.firstElement();
			MemoryViewLine last = (MemoryViewLine) lineCache.lastElement();
			
			if (first == null ||last == null)
				return true;
			
			BigInteger startAddress = new BigInteger(first.getAddress(), 16);
			BigInteger lastAddress = new BigInteger(last.getAddress(), 16);
			lastAddress = lastAddress.add(BigInteger.valueOf(fViewTab.getBytesPerLine()));
			
			if (startAddress.compareTo(address) <= 0 &&
				lastAddress.compareTo(address) >= 0)
			{
				return false;
			}
			return true;
		}
		return true;
	}
}
