/*******************************************************************************
 * Copyright (c) 2005, 2016 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *     Tom Hochstein (Freescale) - Bug 393703 - NotHandledException selecting inactive command under 'Previous Choices' in Quick access
 *     René Brandstetter - Bug 433778
 *     Patrik Suzzi <psuzzi@gmail.com> - Bug 491410
 *******************************************************************************/
package org.eclipse.ui.internal.quickaccess;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import org.eclipse.core.commands.Command;
import org.eclipse.core.runtime.Adapters;
import org.eclipse.core.runtime.Assert;
import org.eclipse.e4.core.commands.ExpressionContext;
import org.eclipse.e4.ui.model.application.MApplication;
import org.eclipse.e4.ui.model.application.ui.basic.MWindow;
import org.eclipse.jface.bindings.TriggerSequence;
import org.eclipse.jface.bindings.keys.KeySequence;
import org.eclipse.jface.bindings.keys.SWTKeySupport;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.dialogs.PopupDialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.util.Util;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.internal.WorkbenchPlugin;
import org.eclipse.ui.internal.WorkbenchWindow;
import org.eclipse.ui.internal.progress.ProgressManagerUtil;
import org.eclipse.ui.keys.IBindingService;

/**
 * This is the quick access popup dialog used in 3.x. The new quick access is
 * done through a shell in {@link SearchField}.
 *
 * @since 3.3
 *
 */
public class QuickAccessDialog extends PopupDialog {
	private TriggerSequence[] invokingCommandKeySequences;
	private Command invokingCommand;
	private QuickAccessContents contents;
	private KeyAdapter keyAdapter;
	private Text filterText;
	private IWorkbenchWindow window;
	private LinkedList previousPicksList = new LinkedList();
	private static final String TEXT_ARRAY = "textArray"; //$NON-NLS-1$
	private static final String TEXT_ENTRIES = "textEntries"; //$NON-NLS-1$
	private static final String ORDERED_PROVIDERS = "orderedProviders"; //$NON-NLS-1$
	private static final String ORDERED_ELEMENTS = "orderedElements"; //$NON-NLS-1$
	static final int MAXIMUM_NUMBER_OF_ELEMENTS = 60;
	static final int MAXIMUM_NUMBER_OF_TEXT_ENTRIES_PER_ELEMENT = 3;
	protected Map textMap = new HashMap();
	protected Map elementMap = new HashMap();
	protected Map providerMap;

	public QuickAccessDialog(final IWorkbenchWindow window, final Command invokingCommand) {
		super(ProgressManagerUtil.getDefaultParent(), SWT.RESIZE, true, true, // persist
																				// size
				false, // but not location
				true, true, null, QuickAccessMessages.QuickAccess_StartTypingToFindMatches);
		this.window = window;

		WorkbenchWindow workbenchWindow = (WorkbenchWindow) window;
		final MWindow model = workbenchWindow.getModel();

		BusyIndicator.showWhile(window.getShell() == null ? null : window.getShell().getDisplay(),
				() -> {
					final CommandProvider commandProvider = new CommandProvider();
					commandProvider.setSnapshot(new ExpressionContext(model.getContext()
							.getActiveLeaf()));
					QuickAccessProvider[] providers = new QuickAccessProvider[] {
							new PreviousPicksProvider(previousPicksList),
							new EditorProvider(),
							new ViewProvider(model.getContext().get(MApplication.class), model),
							new PerspectiveProvider(), commandProvider, new ActionProvider(),
							new WizardProvider(), new PreferenceProvider(),
							new PropertiesProvider() };
					providerMap = new HashMap();
					for (QuickAccessProvider provider : providers) {
						providerMap.put(provider.getId(), provider);
					}
					QuickAccessDialog.this.contents = new QuickAccessContents(providers) {
						@Override
						protected void updateFeedback(boolean filterTextEmpty,
								boolean showAllMatches) {
							if (filterTextEmpty) {
								setInfoText(QuickAccessMessages.QuickAccess_StartTypingToFindMatches);
							} else {
								TriggerSequence[] sequences = getInvokingCommandKeySequences();
								if (showAllMatches || sequences == null
										|| sequences.length == 0) {
									setInfoText(""); //$NON-NLS-1$
								} else {
									setInfoText(NLS
											.bind(QuickAccessMessages.QuickAccess_PressKeyToShowAllMatches,
													sequences[0].format()));
								}
							}
						}

						@Override
						protected void doClose() {
							QuickAccessDialog.this.close();
						}

						/**
						 * @param element
						 */
						void addPreviousPick(String text, Object element) {
							// previousPicksList:
							// Remove element from previousPicksList so
							// there are no duplicates
							// If list is max size, remove last(oldest)
							// element
							// Remove entries for removed element from
							// elementMap and textMap
							// Add element to front of previousPicksList
							previousPicksList.remove(element);
							if (previousPicksList.size() == MAXIMUM_NUMBER_OF_ELEMENTS) {
								Object removedElement = previousPicksList.removeLast();
								ArrayList removedList = (ArrayList) textMap
										.remove(removedElement);
								for (int i = 0; i < removedList.size(); i++) {
									elementMap.remove(removedList.get(i));
								}
							}
							previousPicksList.addFirst(element);

							// textMap:
							// Get list of strings for element from textMap
							// Create new list for element if there isn't
							// one and put
							// element->textList in textMap
							// Remove rememberedText from list
							// If list is max size, remove first(oldest)
							// string
							// Remove text from elementMap
							// Add rememberedText to list of strings for
							// element in textMap
							ArrayList textList = (ArrayList) textMap.get(element);
							if (textList == null) {
								textList = new ArrayList();
								textMap.put(element, textList);
							}

							textList.remove(text);
							if (textList.size() == MAXIMUM_NUMBER_OF_TEXT_ENTRIES_PER_ELEMENT) {
								Object removedText = textList.remove(0);
								elementMap.remove(removedText);
							}

							if (text.length() > 0) {
								textList.add(text);

								// elementMap:
								// Put rememberedText->element in elementMap
								// If it replaced a different element update
								// textMap and
								// PreviousPicksList
								Object replacedElement = elementMap.put(text, element);
								if (replacedElement != null && !replacedElement.equals(element)) {
									textList = (ArrayList) textMap.get(replacedElement);
									if (textList != null) {
										textList.remove(text);
										if (textList.isEmpty()) {
											textMap.remove(replacedElement);
											previousPicksList.remove(replacedElement);
										}
									}
								}
							}
						}

						@Override
						protected QuickAccessElement getPerfectMatch(String filter) {
							QuickAccessElement perfectMatch = (QuickAccessElement) elementMap
									.get(filter);
							return perfectMatch;
						}

						@Override
						protected void handleElementSelected(String text, Object selectedElement) {
							if (selectedElement instanceof QuickAccessElement) {
								addPreviousPick(text, selectedElement);
								storeDialog(getDialogSettings());

								/*
								 * Execute after the dialog has been fully
								 * closed/disposed and the correct
								 * EclipseContext is in place.
								 */
								final QuickAccessElement element = (QuickAccessElement) selectedElement;
								window.getShell().getDisplay().asyncExec(() -> element.execute());
							}
						}
					};
					restoreDialog();
					QuickAccessDialog.this.invokingCommand = invokingCommand;
					if (QuickAccessDialog.this.invokingCommand != null
							&& !QuickAccessDialog.this.invokingCommand.isDefined()) {
						QuickAccessDialog.this.invokingCommand = null;
					} else {
						// Pre-fetch key sequence - do not change because
						// scope will
						// change later.
						getInvokingCommandKeySequences();
					}
					// create early
					create();
				});
		QuickAccessDialog.this.contents.refresh(""); //$NON-NLS-1$
	}

	@Override
	protected Control createTitleControl(Composite parent) {
		filterText = new Text(parent, SWT.NONE);

		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).grab(true, false)
				.applyTo(filterText);

		contents.hookFilterText(filterText);
		filterText.addKeyListener(getKeyAdapter());

		return filterText;
	}

	@Override
	protected Control createDialogArea(Composite parent) {
		Composite composite = (Composite) super.createDialogArea(parent);
		boolean isWin32 = Util.isWindows();
		GridLayoutFactory.fillDefaults().extendedMargins(isWin32 ? 0 : 3, 3, 2, 2)
				.applyTo(composite);

		Table table = contents.createTable(composite, getDefaultOrientation());
		table.addKeyListener(getKeyAdapter());

		return composite;
	}

	final protected TriggerSequence[] getInvokingCommandKeySequences() {
		if (invokingCommandKeySequences == null) {
			if (invokingCommand != null) {
				IBindingService bindingService =
						Adapters.adapt(window.getWorkbench(), IBindingService.class);
				invokingCommandKeySequences = bindingService.getActiveBindingsFor(invokingCommand.getId());
			}
		}
		return invokingCommandKeySequences;
	}

	private KeyAdapter getKeyAdapter() {
		if (keyAdapter == null) {
			keyAdapter = new KeyAdapter() {
				@Override
				public void keyPressed(KeyEvent e) {
					int accelerator = SWTKeySupport.convertEventToUnmodifiedAccelerator(e);
					KeySequence keySequence = KeySequence.getInstance(SWTKeySupport
							.convertAcceleratorToKeyStroke(accelerator));
					TriggerSequence[] sequences = getInvokingCommandKeySequences();
					if (sequences == null)
						return;
					for (TriggerSequence sequence : sequences) {
						if (sequence.equals(keySequence)) {
							e.doit = false;
							contents.setShowAllMatches(!contents.getShowAllMatches());
							return;
						}
					}
				}
			};
		}
		return keyAdapter;
	}

	@Override
	protected Control getFocusControl() {
		return filterText;
	}

	@Override
	public boolean close() {
		storeDialog(getDialogSettings());
		return super.close();
	}

	@Override
	protected Point getDefaultSize() {
		GC gc = new GC(getContents());
		FontMetrics fontMetrics = gc.getFontMetrics();
		gc.dispose();
		int x = Dialog.convertHorizontalDLUsToPixels(fontMetrics, 300);
		if (x < 350) {
			x = 350;
		}
		int y = Dialog.convertVerticalDLUsToPixels(fontMetrics, 270);
		if (y < 420) {
			y = 420;
		}
		return new Point(x, y);
	}

	@Override
	protected Point getDefaultLocation(Point initialSize) {
		Point size = new Point(400, 400);
		Rectangle parentBounds = getParentShell().getBounds();
		int x = parentBounds.x + parentBounds.width / 2 - size.x / 2;
		int y = parentBounds.y + parentBounds.height / 2 - size.y / 2;
		return new Point(x, y);
	}

	@Override
	protected IDialogSettings getDialogSettings() {
		final IDialogSettings workbenchDialogSettings = WorkbenchPlugin.getDefault()
				.getDialogSettings();
		IDialogSettings result = workbenchDialogSettings.getSection(getId());
		if (result == null) {
			result = workbenchDialogSettings.addNewSection(getId());
		}
		return result;
	}

	protected String getId() {
		return "org.eclipse.ui.internal.QuickAccess"; //$NON-NLS-1$
	}

	private void storeDialog(IDialogSettings dialogSettings) {
		String[] orderedElements = new String[previousPicksList.size()];
		String[] orderedProviders = new String[previousPicksList.size()];
		String[] textEntries = new String[previousPicksList.size()];
		ArrayList arrayList = new ArrayList();
		for (int i = 0; i < orderedElements.length; i++) {
			QuickAccessElement quickAccessElement = (QuickAccessElement) previousPicksList.get(i);
			ArrayList elementText = (ArrayList) textMap.get(quickAccessElement);
			Assert.isNotNull(elementText);
			orderedElements[i] = quickAccessElement.getId();
			orderedProviders[i] = quickAccessElement.getProvider().getId();
			arrayList.addAll(elementText);
			textEntries[i] = elementText.size() + ""; //$NON-NLS-1$
		}
		String[] textArray = (String[]) arrayList.toArray(new String[arrayList.size()]);
		dialogSettings.put(ORDERED_ELEMENTS, orderedElements);
		dialogSettings.put(ORDERED_PROVIDERS, orderedProviders);
		dialogSettings.put(TEXT_ENTRIES, textEntries);
		dialogSettings.put(TEXT_ARRAY, textArray);
	}

	private void restoreDialog() {
		IDialogSettings dialogSettings = getDialogSettings();
		if (dialogSettings != null) {
			String[] orderedElements = dialogSettings.getArray(ORDERED_ELEMENTS);
			String[] orderedProviders = dialogSettings.getArray(ORDERED_PROVIDERS);
			String[] textEntries = dialogSettings.getArray(TEXT_ENTRIES);
			String[] textArray = dialogSettings.getArray(TEXT_ARRAY);
			elementMap = new HashMap();
			textMap = new HashMap();
			previousPicksList = new LinkedList();
			if (orderedElements != null && orderedProviders != null && textEntries != null
					&& textArray != null) {
				int arrayIndex = 0;
				for (int i = 0; i < orderedElements.length; i++) {
					QuickAccessProvider quickAccessProvider = (QuickAccessProvider) providerMap
							.get(orderedProviders[i]);
					int numTexts = Integer.parseInt(textEntries[i]);
					if (quickAccessProvider != null) {
						QuickAccessElement quickAccessElement = quickAccessProvider
								.getElementForId(orderedElements[i]);
						if (quickAccessElement != null) {
							ArrayList arrayList = new ArrayList();
							for (int j = arrayIndex; j < arrayIndex + numTexts; j++) {
								String text = textArray[j];
								// text length can be zero for old workspaces,
								// see bug 190006
								if (text.length() > 0) {
									arrayList.add(text);
									elementMap.put(text, quickAccessElement);
								}
							}
							textMap.put(quickAccessElement, arrayList);
							previousPicksList.add(quickAccessElement);
						}
					}
					arrayIndex += numTexts;
				}
			}
		}
	}

}
