/*******************************************************************************
 * Copyright (c) 2010, 2024 IBM Corporation and others.
 * All rights reserved. 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
 *******************************************************************************/
package org.eclipse.wst.sse.ui.internal.quickoutline;

import org.eclipse.core.commands.Command;
import org.eclipse.jface.action.IMenuManager;
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.text.IInformationControl;
import org.eclipse.jface.text.IInformationControlExtension;
import org.eclipse.jface.text.IInformationControlExtension2;
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider;
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider.IStyledLabelProvider;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Item;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.keys.IBindingService;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.sse.ui.IContentSelectionProvider;
import org.eclipse.wst.sse.ui.internal.SSEUIPlugin;
import org.eclipse.wst.sse.ui.quickoutline.AbstractQuickOutlineConfiguration;
import org.eclipse.wst.sse.ui.quickoutline.StringMatcher;
import org.eclipse.wst.sse.ui.quickoutline.StringPatternFilter;

/**
 * Popup dialog that contains the filtering input and the outline
 * view of the editor's input.
 *
 * <p>
 * Based on {@link org.eclipse.jdt.internal.ui.text.AbstractInformationControl}
 * </p>
 *
 */
public class QuickOutlinePopupDialog extends PopupDialog implements IInformationControl, IInformationControlExtension, IInformationControlExtension2, DisposeListener {

	/** Section used for storing the dialog's size and position settings */
	private static final String DIALOG_SECTION = "org.eclipse.wst.sse.ui.quick_outline"; //$NON-NLS-1$

	/** Text field for entering filter patterns */
	private Text fFilterText;

	/** Tree for presenting the information outline */
	private TreeViewer fTreeViewer;

	/** The model to be outlined */
	private IStructuredModel fModel;

	private ILabelProvider fLabelProvider;
	private ITreeContentProvider fContentProvider;

	private IContentSelectionProvider fSelectionProvider;

	private StringPatternFilter fFilter;

	/**
	 * Support for cycling using the original command's invocation trigger
	 * (key binding or otherwise), taken from
	 * {@link org.eclipse.jdt.internal.ui.text.JavaOutlineInformationControl}
	 */
	private KeyAdapter fKeyAdapter;
	private Command fInvokingCommand;
	private TriggerSequence[] fInvokingCommandKeySequences;
	private AbstractQuickOutlineConfiguration fFirstConfiguration;
	private AbstractQuickOutlineConfiguration fConfiguration;

	public QuickOutlinePopupDialog(Shell parent, int shellStyle, IStructuredModel model, AbstractQuickOutlineConfiguration configuration) {
		super(parent, shellStyle, true, true, true, true, true, null, null);
		fConfiguration = fFirstConfiguration = configuration;
		fContentProvider = configuration.getContentProvider();
		fLabelProvider = configuration.getLabelProvider();
		fSelectionProvider = configuration.getContentSelectionProvider();
		fFilter = configuration.getFilter();
		fModel = model;

		fInvokingCommand = PlatformUI.getWorkbench().getService(ICommandService.class).getCommand(DIALOG_SECTION);

		updateStatusText();

		create();
	}

	protected Control createDialogArea(Composite parent) {
		createTreeViewer(parent, SWT.H_SCROLL | SWT.V_SCROLL);
		addListeners(fTreeViewer.getTree());

		installFilter();
		
		return fTreeViewer.getControl();
	}

	protected Control createTitleControl(Composite parent) {
		createFilterText(parent);
		return fFilterText;
	}

	protected void createTreeViewer(Composite parent, int style) {
		Tree tree = new Tree(parent, style);
		GridData gd = new GridData(GridData.FILL_BOTH);
		gd.heightHint = tree.getItemHeight() * 12;
		tree.setLayoutData(gd);

		fTreeViewer = new TreeViewer(tree);
		fTreeViewer.setContentProvider(fContentProvider);
		if (fLabelProvider instanceof IStyledLabelProvider) {
			fTreeViewer.setLabelProvider(new DelegatingStyledCellLabelProvider((IStyledLabelProvider) fLabelProvider));
		}
		else {
			fTreeViewer.setLabelProvider(fLabelProvider);
		}
		fTreeViewer.setAutoExpandLevel(2);
		fTreeViewer.setUseHashlookup(true);
		fTreeViewer.setInput(fModel);
	}

	protected void createFilterText(Composite parent) {
		fFilterText = new Text(parent, SWT.NONE);
		Dialog.applyDialogFont(fFilterText);

		GridData data= new GridData(GridData.FILL_HORIZONTAL);
		data.horizontalAlignment= GridData.FILL;
		data.verticalAlignment= GridData.CENTER;
		fFilterText.setLayoutData(data);
		fFilterText.addKeyListener(new KeyListener() {
			public void keyPressed(KeyEvent e) {
				if (e.keyCode == 0x0D) // return
					gotoSelectedElement();
				if (e.keyCode == SWT.ARROW_DOWN)
					fTreeViewer.getTree().setFocus();
				if (e.keyCode == SWT.ARROW_UP)
					fTreeViewer.getTree().setFocus();
				if (e.character == 0x1B) // ESC
					dispose();
			}
			public void keyReleased(KeyEvent e) {
				// do nothing
			}
		});
		fFilterText.addKeyListener(getKeyAdapter());
		fFilterText.addModifyListener(new ModifyListener() {
			public void modifyText(ModifyEvent e) {
				String text = ((Text) e.widget).getText();
				int length = text.length();
				if (length > 0 && text.charAt(length - 1) != '*') {
					text = text + '*';
				}
				setMatcherString(text, true);
			}
		});
	}

	protected void installFilter() {
		ViewerFilter[] filters = fTreeViewer.getFilters();
		for (int i = 0; i < filters.length; i++) {
			if (filters[i] instanceof StringPatternFilter) {
				fTreeViewer.removeFilter(filters[i]);
			}
		}
		fTreeViewer.addFilter(fFilter);
	}

	protected void setMatcherString(String pattern, boolean update) {
		fFilter.updatePattern(pattern);
		if (update)
			stringMatcherUpdated();
	}

	/**
	 * The string matcher has been modified. The default implementation
	 * refreshes the view and selects the first matched element
	 */
	protected void stringMatcherUpdated() {
		// refresh viewer to re-filter
		/*
		 * there's an unsolved problem that happens when you have a filter
		 * pattern that matches a different configuration--switching
		 * configurations does not reveal the matching tree item
		 */
		fTreeViewer.getControl().setRedraw(false);
		fTreeViewer.refresh();
		fTreeViewer.expandAll();
		selectFirstMatch();
		fTreeViewer.getControl().setRedraw(true);
	}

	private void selectFirstMatch() {
		TreeItem item = findItem(fTreeViewer.getTree().getItems(), fFilter.getStringMatcher());
		if (item != null) {
			fTreeViewer.getTree().setSelection(item);
		}
		else {
			fTreeViewer.setSelection(StructuredSelection.EMPTY);
		}
	}

	private TreeItem findItem(TreeItem[] items, StringMatcher matcher) {
		if (matcher == null)
			return items.length > 0 ? items[0] : null;
		int length = items.length;
		TreeItem result = null;
		for (int i = 0; i < length; i++) {
			if (matcher.match(items[i].getText()))
				return items[i];
			if (items[i].getItemCount() > 0) {
				result = findItem(items[i].getItems(), matcher);
				if (result != null)
					return result;
			}

		}
		return result;
	}

	protected IDialogSettings getDialogSettings() {
		IDialogSettings settings= SSEUIPlugin.getDefault().getDialogSettings().getSection(DIALOG_SECTION);
		if (settings == null)
			settings = SSEUIPlugin.getDefault().getDialogSettings().addNewSection(DIALOG_SECTION);

		return settings;
	}

	private void gotoSelectedElement() {
		Object element = getSelectedElement();
		dispose();
		if (element != null) {
			ITextEditor editor = getActiveTextEditor();
			if (editor != null) {
				editor.selectAndReveal(((IndexedRegion) element).getStartOffset(),
						((IndexedRegion) element).getEndOffset() - ((IndexedRegion) element).getStartOffset());
			}
		}
	}

	private ITextEditor getActiveTextEditor() {
		IWorkbench wb = PlatformUI.getWorkbench();
		ITextEditor editor = null;
		if (wb != null) {
			IWorkbenchWindow ww = wb.getActiveWorkbenchWindow();
			IWorkbenchPage page = ww.getActivePage();
			if (page != null) {
				IEditorPart part = page.getActiveEditor();
				if (part instanceof ITextEditor)
					editor = (ITextEditor) part;
				else
					editor = part != null ? (ITextEditor) part.getAdapter(ITextEditor.class) : null;
			}
		}
		return editor;
	}

	final protected Command getInvokingCommand() {
		return fInvokingCommand;
	}

	final protected TriggerSequence[] getInvokingCommandKeySequences() {
		if (fInvokingCommandKeySequences == null) {
			if (getInvokingCommand() != null) {
				IBindingService bindingService = PlatformUI.getWorkbench().getService(IBindingService.class);
				fInvokingCommandKeySequences = bindingService.getActiveBindingsFor(getInvokingCommand().getId());
				return fInvokingCommandKeySequences;
			}
		}
		return fInvokingCommandKeySequences;
	}

	private KeyAdapter getKeyAdapter() {
		if (fKeyAdapter == null) {
			fKeyAdapter= 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 (int i= 0; i < sequences.length; i++) {
						if (sequences[i].equals(keySequence)) {
							e.doit= false;
							showNextConfiguration();
							return;
						}
					}
				}
			};
		}
		return fKeyAdapter;
	}

	private Object getSelectedElement() {
		if (fTreeViewer == null)
			return null;

		return ((IStructuredSelection) fTreeViewer.getSelection()).getFirstElement();
	}

	private void addListeners(final Tree tree) {
		tree.addKeyListener(new KeyListener() {
			public void keyPressed(KeyEvent e)  {
				if (e.character == 0x1B) // ESC
					dispose();
			}
			public void keyReleased(KeyEvent e) {
				// do nothing
			}
		});

		tree.addSelectionListener(new SelectionListener() {
			public void widgetSelected(SelectionEvent e) {
				// do nothing
			}
			public void widgetDefaultSelected(SelectionEvent e) {
				gotoSelectedElement();
			}
		});

		/* Mouse hover */
		tree.addMouseMoveListener(new MouseMoveListener()	 {
			TreeItem fLastItem= null;
			public void mouseMove(MouseEvent e) {
				if (tree.equals(e.getSource())) {
					Object o= tree.getItem(new Point(e.x, e.y));
					if (o instanceof TreeItem) {
						Rectangle clientArea = tree.getClientArea();
						if (!o.equals(fLastItem)) {
							fLastItem= (TreeItem)o;
							tree.setSelection(new TreeItem[] { fLastItem });
						} else if (e.y - clientArea.y < tree.getItemHeight() / 4) {
							// Scroll up
							Point p= tree.toDisplay(e.x, e.y);
							Item item= fTreeViewer.scrollUp(p.x, p.y);
							if (item instanceof TreeItem) {
								fLastItem= (TreeItem)item;
								tree.setSelection(new TreeItem[] { fLastItem });
							}
						} else if (clientArea.y + clientArea.height - e.y < tree.getItemHeight() / 4) {
							// Scroll down
							Point p= tree.toDisplay(e.x, e.y);
							Item item= fTreeViewer.scrollDown(p.x, p.y);
							if (item instanceof TreeItem) {
								fLastItem= (TreeItem)item;
								tree.setSelection(new TreeItem[] { fLastItem });
							}
						}
					}
				}
			}
		});

		tree.addMouseListener(new MouseAdapter() {
			public void mouseUp(MouseEvent e) {

				if (tree.getSelectionCount() < 1)
					return;

				if (e.button != 1)
					return;

				if (tree.equals(e.getSource())) {
					Object o= tree.getItem(new Point(e.x, e.y));
					TreeItem selection= tree.getSelection()[0];
					if (selection.equals(o))
						gotoSelectedElement();
				}
			}
		});
	}
	public void addDisposeListener(DisposeListener listener) {
		getShell().addDisposeListener(listener);
	}

	public void addFocusListener(FocusListener listener) {
		getShell().addFocusListener(listener);
	}

	public Point computeSizeHint() {
		return getShell().getSize();
	}

	public void dispose() {
		close();
	}

	public boolean isFocusControl() {
		return getShell().getDisplay().getActiveShell() == getShell();
	}

	public void removeDisposeListener(DisposeListener listener) {
		getShell().removeDisposeListener(listener);
	}

	public void removeFocusListener(FocusListener listener) {
		getShell().removeFocusListener(listener);
	}

	public void setBackgroundColor(Color background) {
		applyBackgroundColor(background, getContents());
	}

	public void setFocus() {
		getShell().forceFocus();
		fFilterText.setFocus();
	}

	public void setForegroundColor(Color foreground) {
		applyForegroundColor(foreground, getContents());
	}

	public void setInformation(String information) {
		// nothing to do
	}

	public void setLocation(Point location) {
		/*
		 * If the location is persisted, it gets managed by PopupDialog - fine. Otherwise, the location is
		 * computed in Window#getInitialLocation, which will center it in the parent shell / main
		 * monitor, which is wrong for two reasons:
		 * - we want to center over the editor / subject control, not the parent shell
		 * - the center is computed via the initalSize, which may be also wrong since the size may
		 *   have been updated since via min/max sizing of AbstractInformationControlManager.
		 * In that case, override the location with the one computed by the manager. Note that
		 * the call to constrainShellSize in PopupDialog.open will still ensure that the shell is
		 * entirely visible.
		 */
		if (!getPersistLocation() || getDialogSettings() == null)
			getShell().setLocation(location);
	}

	public void setSize(int width, int height) {
		getShell().setSize(width, height);
	}

	public void setSizeConstraints(int maxWidth, int maxHeight) {
	}

	public void setVisible(boolean visible) {
		if (visible) {
			open();
		} else {
			saveDialogBounds(getShell());
			getShell().setVisible(false);
		}
	}

	public boolean hasContents() {
		return fTreeViewer != null && fTreeViewer.getInput() != null;
	}

	public void setInput(Object input) {
		if (!(input instanceof ISelection)) {
			fTreeViewer.setSelection(new StructuredSelection(input));
		}
		else {
			if (fSelectionProvider != null) {
				ISelection selection = fSelectionProvider.getSelection(fTreeViewer, (ISelection) input);
				fTreeViewer.setSelection(selection);
			}
			else {
				fTreeViewer.setSelection((ISelection) input);
			}
		}
	}

	protected void showNextConfiguration() {
		AbstractQuickOutlineConfiguration nextConfiguration = fConfiguration.getNextConfiguration();
		if (nextConfiguration == null) {
			nextConfiguration = fFirstConfiguration;
		}
		if (fConfiguration != nextConfiguration) {
			fTreeViewer.setInput(null);
			fContentProvider.dispose();
			fLabelProvider.dispose();

			fTreeViewer.setContentProvider(fContentProvider = nextConfiguration.getContentProvider());
			fLabelProvider = nextConfiguration.getLabelProvider();
			if (fLabelProvider instanceof IStyledLabelProvider) {
				fTreeViewer.setLabelProvider(new DelegatingStyledCellLabelProvider((IStyledLabelProvider) fLabelProvider));
			}
			else {
				fTreeViewer.setLabelProvider(fLabelProvider);
			}
			fSelectionProvider = nextConfiguration.getContentSelectionProvider();
			fFilter = nextConfiguration.getFilter();
			installFilter();
			fConfiguration = nextConfiguration;

			fTreeViewer.setInput(fModel);
			// reapply and reappend (if needed) the wildcard for this new filter
			String text = fFilterText.getText();
			int length = text.length();
			if (length > 0 && text.charAt(length - 1) != '*') {
				text = text + '*';
			}
			setMatcherString(text, true);

			updateStatusText();
		}
	}

	public void widgetDisposed(DisposeEvent e) {
		fTreeViewer = null;
		fFilterText = null;
		fModel = null;
	}

	protected void fillDialogMenu(IMenuManager dialogMenu) {
		// Add custom actions
		super.fillDialogMenu(dialogMenu);
	}

	protected void updateStatusText() {
		AbstractQuickOutlineConfiguration nextConfiguration = fConfiguration.getNextConfiguration();
		if (nextConfiguration == null) {
			nextConfiguration = fFirstConfiguration;
		}
		if (fConfiguration != nextConfiguration && nextConfiguration.getShowMessage() != null) {
			TriggerSequence[] sequences = getInvokingCommandKeySequences();
			if (sequences != null && sequences.length > 0) {
				String keySequence = sequences[0].format();
				setInfoText(NLS.bind(nextConfiguration.getShowMessage(), keySequence));
			}
		}
	}
}
