/*******************************************************************************
 * Copyright (c) 2009, 2011 Wind River Systems, Inc. and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * 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<>();
					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<>();
		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() {
			@Override
			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 = (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 = 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;
	}

}
