/*******************************************************************************
 * Copyright (c) 2009, 2010 Wind River Systems, Inc. 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:
 *     Ted R Williams (Wind River Systems, Inc.) - initial implementation
 *******************************************************************************/

package org.eclipse.cdt.debug.ui.memory.memorybrowser;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;

import org.eclipse.cdt.debug.core.model.provisional.IRecurringDebugContext;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.fieldassist.ControlDecoration;
import org.eclipse.jface.fieldassist.FieldDecoration;
import org.eclipse.jface.fieldassist.FieldDecorationRegistry;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.ui.PlatformUI;

public class GoToAddressBarWidget {
	
	/**
	 * Character sequence that is unlikely to appear naturally in a recurring
	 * debug context ID or memory space ID
	 */
	private static String SEPARATOR = "<seperator>";
	
	/**
	 * At a minimum, the expression history is kept on a per launch
	 * configuration basis. Where debug contexts (processes, in practice) can
	 * provide a recurring ID, we further divide the history by those IDs. This
	 * constant is used when no recurring context ID is available.
	 */
	private static String UNKNOWN_CONTEXT_ID = "Unknown";
	private Combo fExpression;
	private ControlDecoration fEmptyExpression;
	private ControlDecoration fWrongExpression;
	
	private Button fOKButton;
	private Button fOKNewTabButton;
	private Composite fComposite;
	
	protected static int ID_GO_NEW_TAB = 2000;
	
	private IStatus fExpressionStatus = Status.OK_STATUS;
	
    /**
	 * @param parent
	 * @return
	 */
	public Control createControl(Composite parent)
	{
		fComposite = new Composite(parent, SWT.NONE);
		PlatformUI.getWorkbench().getHelpSystem().setHelp(fComposite, // FIXME 	
					".GoToAddressComposite_context"); //$NON-NLS-1$
				
		GridLayout layout = new GridLayout();
		layout.numColumns = 6;
		layout.makeColumnsEqualWidth = false;
		layout.marginHeight = 0;
		layout.marginLeft = 0;
		fComposite.setLayout(layout);
	
		fExpression = createExpressionField(fComposite);
		
		fOKButton = new Button(fComposite, SWT.NONE);
		fOKButton.setText(Messages.getString("GoToAddressBarWidget.Go")); //$NON-NLS-1$
		fOKButton.setEnabled(false);
		
		fOKNewTabButton = new Button(fComposite, SWT.NONE);
		fOKNewTabButton.setText(Messages.getString("GoToAddressBarWidget.NewTab")); //$NON-NLS-1$
		fOKNewTabButton.setEnabled(false);
		
		return fComposite;
	}
	
	/** The launch configuration attribute prefix used to persist expression history */
	private final static String SAVED_EXPRESSIONS = "saved_expressions";  //$NON-NLS-1$
	
	private final static int MAX_SAVED_EXPRESSIONS = 15 ;
	
	private void addExpressionToHistoryPersistence( Object context, String expr, String memorySpace ) {
		/*
		 * Get the saved expressions if any.
		 * 
		 * They are in the form
		 * 
		 *     expression,expression,.....,expression
		 */
		ILaunch launch = getLaunch(context);
		if(launch == null)
		{
			return;
		}
		
		String contextID = getRecurringContextID(context);
		
		ILaunchConfiguration launchConfiguration = launch.getLaunchConfiguration();
		String currentExpressions = "";
		if (launchConfiguration != null) {
			try {
				ILaunchConfigurationWorkingCopy wc = launchConfiguration.getWorkingCopy();
				if (wc != null) {
					currentExpressions = wc.getAttribute(getSaveExpressionKey(contextID,memorySpace), "");
					
					StringTokenizer st = new StringTokenizer(currentExpressions, ","); //$NON-NLS-1$
					/*
					 * Parse through the list creating an ordered array for display.
					 */
					ArrayList<String> list = new ArrayList<String>();
					while(st.hasMoreElements())
					{
						String expression = (String) st.nextElement();
						list.add(expression);
					}
					if(!list.contains(expr))
					{
						list.add(expr);
					
						while(list.size() > MAX_SAVED_EXPRESSIONS)
						{
							list.remove(0);
						}
						
						currentExpressions = "";
						for ( int idx =0 ; idx < list.size(); idx ++ ) {
							if(idx > 0)
							{
								currentExpressions += ",";
							}
							currentExpressions += list.get(idx);
						}
						wc.setAttribute(getSaveExpressionKey(contextID,memorySpace), currentExpressions);					
						wc.doSave();
					}
				}			
			}
			catch(CoreException e) {
			}
		}
	}
	
	/**
	 * Clear all expression history persisted in the launch configuration that
	 * created the given debug context
	 * 
	 * @param context
	 *            the debug context. In practice, this will always be a process
	 *            context
	 */
	public void clearExpressionHistoryPersistence(Object context) {
		if(context == null) {
			return;
		}
		
		ILaunch launch = getLaunch(context);
		if(launch == null) {
			return;
		}
		
		// We maintain history for every process this launch configuration has
		// launched. And where memory spaces are involved, each space has its
		// own history. Here we just wipe out the persistence of all processes
		// and memory spaces stored in the launch configuration that created the
		// given processes.
		ILaunchConfiguration launchConfiguration = launch.getLaunchConfiguration();
		if (launchConfiguration != null) {
			try {
				ILaunchConfigurationWorkingCopy wc = launchConfiguration.getWorkingCopy();
				if (wc != null) {
					Map<?,?> attributes = wc.getAttributes();
					Iterator<?> iterator = attributes.keySet().iterator();
					while (iterator.hasNext()) {
						String key = (String)iterator.next();
						if (key.startsWith(SAVED_EXPRESSIONS)) {
							wc.removeAttribute(key);
						}
					}
					wc.doSave();
				}
			}
			catch(CoreException e) {
				// Some unexpected snag working with the launch configuration
				MemoryBrowserPlugin.log(e);
			}
		}
	}

	/**
	 * Get the expression history persisted in the launch configuration for the
	 * given debug context and memory space (where applicable)
	 * 
	 * @param context
	 *            the debug context. In practice, this will always be a process
	 *            context
	 * @param memorySpace
	 *            memory space ID or null if not applicable
	 * @return a list of expressions, or empty collection if no history
	 *         available (never null)
	 * @throws CoreException
	 *             if there's a problem working with the launch configuration
	 */
	private String[] getSavedExpressions(Object context, String memorySpace) throws CoreException {
		/*
		 * Get the saved expressions if any.
		 * 
		 * They are in the form
		 * 
		 *     expression,expression,.....,expression
		 */
		
		ILaunch launch = getLaunch(context);
		if(launch == null) {
			return new String[0];
		}
		
		ILaunchConfiguration launchConfiguration = launch.getLaunchConfiguration();
		String expressions = "";
		if (launchConfiguration != null) {
			expressions = launchConfiguration.getAttribute(getSaveExpressionKey(getRecurringContextID(context),memorySpace), "");
		}

		StringTokenizer st = new StringTokenizer(expressions, ","); //$NON-NLS-1$
		/*
		 * Parse through the list creating an ordered array for display.
		 */
		ArrayList<String> list = new ArrayList<String>();
		while(st.hasMoreElements()) {
			list.add(st.nextToken());
		}
		return list.toArray(new String[list.size()]);
	}
	
	/**
	 * Populate the expression history combobox based on the history persisted
	 * in the launch configuration for the given context and memory space (where
	 * applicable)
	 * 
	 * @param context
	 *            the debug context. In practice, this will always be a process
	 *            context
	 * @param memorySpace
	 *            memory space ID; null if not applicable
	 */
	public void loadSavedExpressions(Object context, String memorySpace)
	{
		try {
			// Rebuild the combobox entries, but make sure we don't clear the
			// (expression) field. It's an input field; don't trample anything
			// the user may have previously entered or is in the process of
			// entering (see bugzilla 356346). removeAll() clears all the
			// entries and the field. remove(beg, end) leaves the field in-tact 
			// as long as it's not asked to remove the entry the user selected
			// to set the current field value. So, if the current expression
			// corresponds to an entry, we purge all the entries but that one.
			String[] expressions = getSavedExpressions(context, memorySpace);
			String currentExpression = fExpression.getText(); 
			if (currentExpression.length() > 0)
			{
				int index = fExpression.indexOf(currentExpression);
				if(index > 0) {
					fExpression.remove(0, index-1);
				}
				index = fExpression.indexOf(currentExpression);
				if (fExpression.getItemCount() - index - 1 > 1) {
					fExpression.remove(index+1, fExpression.getItemCount()-1);
				}				
			}
			else {
				// No expression to trample. Use removeAll()
				fExpression.removeAll();
			}
			for (String expression : expressions) {
				if (fExpression.indexOf(expression) < 0) {
					fExpression.add(expression);
				}
			}
		} catch (CoreException e) {
			// Unexpected snag dealing with launch configuration
			MemoryBrowserPlugin.log(e);
		}
	}
	
	public void addExpressionToHistory(Object context, String expr, String memorySpace) {
		/*
		 * Make sure it does not already exist, we do not want to show duplicates.
		 */
		if ( fExpression.indexOf(expr) == -1 ) {
			/*
			 * Cap the size of the list.
			 */
			while ( fExpression.getItemCount() >= MAX_SAVED_EXPRESSIONS ) {
				fExpression.remove(0);
			}
			
			/*
			 * Add the new expression to the combobox
			 */
			fExpression.add(expr);
			
		}
		/*
		 * Add it to the persistense database.
		 */
		addExpressionToHistoryPersistence(context, expr, memorySpace);
	}
	
	/**
	 * Clears the history of expressions for the given debug context, both in
	 * the GUI and the persistence data
	 * 
	 * @param context
	 *            the debug context. In practice, this will always be a process
	 *            context.
	 */
	public void clearExpressionHistory(Object context) {
		/*
		 * Clear the combobox
		 */
		fExpression.removeAll();
		fExpression.computeSize(SWT.DEFAULT, SWT.DEFAULT, true);
		
		/*
		 * Clear the history persisted in the launch configuration
		 */
		clearExpressionHistoryPersistence(context);
		
		/*
		 * Make sure the status image indicator shows OK.
		 */
		handleExpressionStatus(Status.OK_STATUS);
	}
	
	private Combo createExpressionField(Composite parent){
		/*
		 * Create the dropdown box for the editable expressions.
		 */
		Combo combo = new Combo(parent, SWT.DROP_DOWN | SWT.BORDER);
		combo.addModifyListener(new ModifyListener() {
			public void modifyText(ModifyEvent e) {
				updateButtons();
			}
		});
		
		fEmptyExpression = new ControlDecoration(combo, SWT.LEFT | SWT.CENTER);
		fEmptyExpression.setDescriptionText(Messages.getString("GoToAddressBarWidget.EnterExpressionMessage")); //$NON-NLS-1$
		FieldDecoration fieldDec = FieldDecorationRegistry.getDefault().getFieldDecoration(FieldDecorationRegistry.DEC_REQUIRED);
		fEmptyExpression.setImage(fieldDec.getImage());

		fWrongExpression = new ControlDecoration(combo, SWT.LEFT | SWT.TOP);
		fieldDec = FieldDecorationRegistry.getDefault().getFieldDecoration(FieldDecorationRegistry.DEC_ERROR);
		fWrongExpression.setImage(fieldDec.getImage());
		fWrongExpression.hide();
		
		// leave enough room for decorators
		GridData data = new GridData(GridData.FILL_HORIZONTAL);
		data.horizontalIndent = Math.max(fEmptyExpression.getImage().getBounds().width, fWrongExpression.getImage().getBounds().width);
		combo.setLayoutData(data);
		return combo;
	}
		
	protected void updateButtons() {
		boolean empty = getExpressionText().length() == 0;
		
		fOKNewTabButton.setEnabled(!empty);
		fOKButton.setEnabled(!empty);
		
		if (empty) 
			fEmptyExpression.show();
		else 
			fEmptyExpression.hide();
		
		if (fExpressionStatus.isOK())
		    fWrongExpression.hide();
		else
		    fWrongExpression.show();
	}

	public int getHeight()
	{
		int height = fComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT).y;
		return height;
	}
	
	public Button getButton(int id)
	{
		if (id == IDialogConstants.OK_ID)
			return fOKButton;
		if (id == ID_GO_NEW_TAB)
			return fOKNewTabButton;
		return null;
	}
	
	/**
	 * Get expression text
	 * @return
	 */
	public String getExpressionText()
	{
		return fExpression.getText().trim();
	}

	/**
	 * Update expression text from the widget
	 * @param text 
	 */
	public void setExpressionText(String text)
	{
		fExpression.setText(text);
	}

	public Combo getExpressionWidget()
	{
		return fExpression;
	}
	
	/**
	 * decorate expression field according to the status
	 * @param message
	 */
	public void handleExpressionStatus(final IStatus message) {
		if (message.isOK()) {
			fWrongExpression.hide();
		} else {
			fWrongExpression.setDescriptionText(message.getMessage());
			fWrongExpression.show();
		}
		
		fExpressionStatus = message;
	}
	
	/**
	 * Return the expression status
	 * @return expression status
	 */
	public IStatus getExpressionStatus()
    {
        return fExpressionStatus;
    }
	
    private ILaunch getLaunch(Object context)
    {
        IAdaptable adaptable = null;
        ILaunch launch  = null;
		if(context instanceof IAdaptable)
		{
			adaptable = (IAdaptable) context;
			launch  = ((ILaunch) adaptable.getAdapter(ILaunch.class));
		}
		
		return launch;
    	
    }
    
    /**
	 * Get the identifier for the given context if it is a recurring one. See
	 * {@link IRecurringDebugContext}
	 * 
	 * @param context
	 *            the debug context
	 * @return the ID or UNKNOWN_CONTEXT_ID if the context is non-recurring or
	 *         can't provide us its ID
	 */
    private String getRecurringContextID(Object context)
    {
    	String id = UNKNOWN_CONTEXT_ID;
		if (context instanceof IAdaptable) {
			IAdaptable adaptable = (IAdaptable) context;
			IRecurringDebugContext recurringDebugContext = (IRecurringDebugContext)adaptable.getAdapter(IRecurringDebugContext.class);
			if (recurringDebugContext != null) {
				try {
					id = recurringDebugContext.getContextID();
				}
				catch(DebugException e) {
					// If the context can't give us the ID, just treat it as a
					// non-recurring context
				}
			}
		}
		return id;
    	
    }
    
    /**
	 * Get a key that we can use to persist the expression history for the given
	 * debug context and memory space (where applicable). The key is used within
	 * the scope of a launch configuration.
	 * 
	 * @param contextID
	 *            a recurring debug context ID; see
	 *            {@link IRecurringDebugContext}
	 * @param memorySpace
	 *            a memory space identifier, or null if not applicable
	 * @return they key which will be used to persist the expression history
	 */
    private String getSaveExpressionKey(String contextID, String memorySpace) {
    	assert contextID.length() > 0;
    	String key = SAVED_EXPRESSIONS + SEPARATOR + contextID;
    	if (memorySpace != null && memorySpace.length() > 0) { 
    		key += SEPARATOR + memorySpace;
    	}
    	return key;
    }

}
