/*******************************************************************************
 * Copyright (c) 2000, 2015 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
 *******************************************************************************/
package org.eclipse.jdt.internal.debug.ui.propertypages;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.debug.core.IJavaExceptionBreakpoint;
import org.eclipse.jdt.internal.debug.ui.ExceptionHandler;
import org.eclipse.jdt.internal.debug.ui.Filter;
import org.eclipse.jdt.internal.debug.ui.FilterLabelProvider;
import org.eclipse.jdt.internal.debug.ui.FilterViewerComparator;
import org.eclipse.jdt.internal.debug.ui.JDIDebugUIPlugin;
import org.eclipse.jdt.ui.IJavaElementSearchConstants;
import org.eclipse.jdt.ui.JavaUI;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.CheckboxTableViewer;
import org.eclipse.jface.viewers.ColumnLayoutData;
import org.eclipse.jface.viewers.ColumnWeightData;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.IContentProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TableLayout;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.TableEditor;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.ElementListSelectionDialog;
import org.eclipse.ui.dialogs.SelectionDialog;


public class ExceptionFilterEditor {

	protected static final String DEFAULT_PACKAGE = "(default package)"; //$NON-NLS-1$
	private IJavaExceptionBreakpoint fBreakpoint;
	private Button fAddFilterButton;
	private Button fAddPackageButton;
	private Button fAddTypeButton;
	private Button fRemoveFilterButton;
	private Text fEditorText;
	private String fInvalidEditorText = null;
	private TableEditor fTableEditor;
	private TableItem fNewTableItem;
	private Filter fNewFilter;
	private CheckboxTableViewer fFilterViewer;
	private Table fFilterTable;
	private FilterContentProvider fFilterContentProvider;

	private SelectionListener fSelectionListener= new SelectionAdapter() {
		@Override
		public void widgetSelected(SelectionEvent e) {
			Object source = e.getSource();
			if (source == fAddTypeButton) {
				addType();
			} else if (source == fAddPackageButton) {
				addPackage();
			} else if (source == fAddFilterButton) {
				editFilter();
			} else if (source == fRemoveFilterButton) {
				removeFilters();
			}
		}
	};

	public ExceptionFilterEditor(Composite parent, JavaExceptionBreakpointAdvancedPage page) {
		fBreakpoint = (IJavaExceptionBreakpoint) page.getBreakpoint();
		// top level container
		Composite outer = new Composite(parent, SWT.NONE);
		GridLayout layout = new GridLayout();
		layout.numColumns = 2;
		layout.marginHeight = 0;
		layout.marginWidth = 0;

		outer.setLayout(layout);
		GridData gd = new GridData(GridData.FILL_BOTH);
		outer.setLayoutData(gd);
		outer.setFont(parent.getFont());
		// filter table
		Label label= new Label(outer, SWT.NONE);
		label.setText(PropertyPageMessages.ExceptionFilterEditor_5);
		label.setFont(parent.getFont());
		gd= new GridData();
		gd.horizontalSpan= 2;
		label.setLayoutData(gd);

		fFilterTable = new Table(outer, SWT.CHECK | SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION);

		TableLayout tableLayout = new TableLayout();
		ColumnLayoutData[] columnLayoutData = new ColumnLayoutData[1];
		columnLayoutData[0] = new ColumnWeightData(100);
		tableLayout.addColumnData(columnLayoutData[0]);
		fFilterTable.setLayout(tableLayout);
		fFilterTable.setFont(parent.getFont());
		new TableColumn(fFilterTable, SWT.NONE);

		fFilterViewer = new CheckboxTableViewer(fFilterTable);
		fTableEditor = new TableEditor(fFilterTable);
		fFilterViewer.setLabelProvider(new FilterLabelProvider());
		fFilterViewer.setComparator(new FilterViewerComparator());
		fFilterContentProvider = new FilterContentProvider(fFilterViewer);
		fFilterViewer.setContentProvider(fFilterContentProvider);
		// input just needs to be non-null
		fFilterViewer.setInput(this);
		gd = new GridData(GridData.FILL_BOTH | GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL);
		gd.widthHint = 100;
		gd.heightHint = 100;
		fFilterViewer.getTable().setLayoutData(gd);
		fFilterViewer.addCheckStateListener(new ICheckStateListener() {
			@Override
			public void checkStateChanged(CheckStateChangedEvent event) {
				Filter filter = (Filter) event.getElement();
				fFilterContentProvider.toggleFilter(filter);
			}
		});
		fFilterViewer.addSelectionChangedListener(new ISelectionChangedListener() {
			@Override
			public void selectionChanged(SelectionChangedEvent event) {
				ISelection selection = event.getSelection();
				if (selection.isEmpty()) {
					fRemoveFilterButton.setEnabled(false);
				} else {
					fRemoveFilterButton.setEnabled(true);
				}
			}
		});
		fFilterViewer.getTable().addKeyListener(new KeyAdapter() {
			@Override
			public void keyPressed(KeyEvent event) {
				if (event.character == SWT.DEL && event.stateMask == 0) {
					removeFilters();
				}
			}
		});

		createFilterButtons(outer);
	}

	protected void doStore() {
		Object[] filters = fFilterContentProvider.getElements(null);
		List<String> inclusionFilters = new ArrayList<>(filters.length);
		List<String> exclusionFilters = new ArrayList<>(filters.length);
		for (int i = 0; i < filters.length; i++) {
			Filter filter = (Filter) filters[i];
			String name = filter.getName();
			if (name.equals(DEFAULT_PACKAGE)) {
				name = ""; //$NON-NLS-1$
			}
			if (filter.isChecked()) {
				inclusionFilters.add(name);
			} else {
				exclusionFilters.add(name);
			}
		}
		try {
			fBreakpoint.setInclusionFilters(inclusionFilters.toArray(new String[inclusionFilters.size()]));
			fBreakpoint.setExclusionFilters(exclusionFilters.toArray(new String[exclusionFilters.size()]));
		} catch (CoreException ce) {
			JDIDebugUIPlugin.log(ce);
		}

	}

	private void createFilterButtons(Composite container) {
		// button container
		Font font= container.getFont();
		Composite buttonContainer = new Composite(container, SWT.NONE);
		buttonContainer.setFont(font);
		GridData gd = new GridData(GridData.FILL_VERTICAL);
		buttonContainer.setLayoutData(gd);
		GridLayout buttonLayout = new GridLayout();
		buttonLayout.numColumns = 1;
		buttonLayout.marginHeight = 0;
		buttonLayout.marginWidth = 0;
		buttonContainer.setLayout(buttonLayout);

		fAddFilterButton = createPushButton(buttonContainer,
				PropertyPageMessages.ExceptionFilterEditor_6,
				PropertyPageMessages.ExceptionFilterEditor_7);
		fAddTypeButton = createPushButton(buttonContainer,
				PropertyPageMessages.ExceptionFilterEditor_8,
				PropertyPageMessages.ExceptionFilterEditor_9);
		fAddPackageButton = createPushButton(buttonContainer,
				PropertyPageMessages.ExceptionFilterEditor_10,
				PropertyPageMessages.ExceptionFilterEditor_11);
		fRemoveFilterButton = createPushButton(buttonContainer,
				PropertyPageMessages.ExceptionFilterEditor_12,
				PropertyPageMessages.ExceptionFilterEditor_13);
		fRemoveFilterButton.setEnabled(false);
	}

	/**
	 * Creates a fully configured push button with the given label and tooltip.
	 */
	private Button createPushButton(Composite parent, String text, String tooltipText) {
		Button button = new Button(parent, SWT.PUSH);
		button.setFont(parent.getFont());
		button.setText(text);
		button.setToolTipText(tooltipText);
		GridData gd = getButtonGridData(button);
		button.setLayoutData(gd);
		button.addSelectionListener(fSelectionListener);
		return button;
	}

	private static GridData getButtonGridData(Button button) {
		GridData gd = new GridData(GridData.FILL_HORIZONTAL | GridData.VERTICAL_ALIGN_BEGINNING);
		GC gc = new GC(button);
		gc.setFont(button.getFont());
		FontMetrics fontMetrics = gc.getFontMetrics();
		gc.dispose();
		int widthHint = Dialog.convertHorizontalDLUsToPixels(fontMetrics, IDialogConstants.BUTTON_WIDTH);
		gd.widthHint = Math.max(widthHint, button.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).x);
		return gd;
	}

	/**
	 * Create a new filter in the table (with the default 'new filter' value),
	 * then open up an in-place editor on it.
	 */
	private void editFilter() {
		// if a previous edit is still in progress, finish it
		if (fEditorText != null) {
			validateChangeAndCleanup();
		}

		fNewFilter = fFilterContentProvider.addFilter(""); //$NON-NLS-1$
		fNewTableItem = fFilterTable.getItem(0);

		// create & configure Text widget for editor
		// Fix for bug 1766.  Border behavior on for text fields varies per platform.
		// On Motif, you always get a border, on other platforms,
		// you don't.  Specifying a border on Motif results in the characters
		// getting pushed down so that only there very tops are visible.  Thus,
		// we have to specify different style constants for the different platforms.
		int textStyles = SWT.SINGLE | SWT.LEFT;
		if (!SWT.getPlatform().equals("motif")) { //$NON-NLS-1$
			textStyles |= SWT.BORDER;
		}
		fEditorText = new Text(fFilterTable, textStyles);
		GridData gd = new GridData(GridData.FILL_BOTH);
		fEditorText.setLayoutData(gd);

		// set the editor
		fTableEditor.horizontalAlignment = SWT.LEFT;
		fTableEditor.grabHorizontal = true;
		fTableEditor.setEditor(fEditorText, fNewTableItem, 0);

		// get the editor ready to use
		fEditorText.setText(fNewFilter.getName());
		fEditorText.selectAll();
		setEditorListeners(fEditorText);
		fEditorText.setFocus();
	}

	private void setEditorListeners(Text text) {
		// CR means commit the changes, ESC means abort and don't commit
		text.addKeyListener(new KeyAdapter() {
			@Override
			public void keyReleased(KeyEvent event) {
				if (event.character == SWT.CR) {
					if (fInvalidEditorText != null) {
						fEditorText.setText(fInvalidEditorText);
						fInvalidEditorText = null;
					} else {
						validateChangeAndCleanup();
					}
				} else if (event.character == SWT.ESC) {
					removeNewFilter();
					cleanupEditor();
				}
			}
		});
		// Consider loss of focus on the editor to mean the same as CR
		text.addFocusListener(new FocusAdapter() {
			@Override
			public void focusLost(FocusEvent event) {
				if (fInvalidEditorText != null) {
					fEditorText.setText(fInvalidEditorText);
					fInvalidEditorText = null;
				} else {
					validateChangeAndCleanup();
				}
			}
		});
		// Consume traversal events from the text widget so that CR doesn't
		// traverse away to dialog's default button.  Without this, hitting
		// CR in the text field closes the entire dialog.
		text.addListener(SWT.Traverse, new Listener() {
			@Override
			public void handleEvent(Event event) {
				event.doit = false;
			}
		});
	}

	private void validateChangeAndCleanup() {
		String trimmedValue = fEditorText.getText().trim();
		// if the new value is blank, remove the filter
		if (trimmedValue.length() < 1) {
			removeNewFilter();
		}
		// if it's invalid, beep and leave sitting in the editor
		else if (!validateEditorInput(trimmedValue)) {
			fInvalidEditorText = trimmedValue;
			fEditorText.setText(PropertyPageMessages.ExceptionFilterEditor_14);
			return;
			// otherwise, commit the new value if not a duplicate
		} else {
			Object[] filters = fFilterContentProvider.getElements(null);
			for (int i = 0; i < filters.length; i++) {
				Filter filter = (Filter) filters[i];
				if (filter.getName().equals(trimmedValue)) {
					removeNewFilter();
					cleanupEditor();
					return;
				}
			}
			fNewTableItem.setText(trimmedValue);
			fNewFilter.setName(trimmedValue);
			fFilterViewer.refresh();
		}
		cleanupEditor();
	}

	/**
	 * A valid filter is simply one that is a valid Java identifier.
	 * and, as defined in the JDI spec, the regular expressions used for
	 * scoping must be limited to exact matches or patterns that
	 * begin with '*' or end with '*'. Beyond this, a string cannot be validated
	 * as corresponding to an existing type or package (and this is probably not
	 * even desirable).
	 */
	private boolean validateEditorInput(String trimmedValue) {
		char firstChar = trimmedValue.charAt(0);
		if (!Character.isJavaIdentifierStart(firstChar)) {
			if (!(firstChar == '*')) {
				return false;
			}
		}
		int length = trimmedValue.length();
		for (int i = 1; i < length; i++) {
			char c = trimmedValue.charAt(i);
			if (!Character.isJavaIdentifierPart(c)) {
				if (c == '.' && i != (length - 1)) {
					continue;
				}
				if (c == '*' && i == (length - 1)) {
					continue;
				}
				return false;
			}
		}
		return true;
	}

	/**
	 * Cleanup all widgetry & resources used by the in-place editing
	 */
	private void cleanupEditor() {
		if (fEditorText != null) {
			fNewFilter = null;
			fNewTableItem = null;
			fTableEditor.setEditor(null, null, 0);
			fEditorText.getDisplay().asyncExec(new Runnable() {
				@Override
				public void run() {
					fEditorText.dispose();
					fEditorText = null;
				}
			});
		}
	}

	private void removeFilters() {
		IStructuredSelection selection = (IStructuredSelection) fFilterViewer.getSelection();
		fFilterContentProvider.removeFilters(selection.toArray());
	}

	private void removeNewFilter() {
		fFilterContentProvider.removeFilters(new Object[] { fNewFilter });
	}

	private void addPackage() {
		Shell shell = fAddPackageButton.getDisplay().getActiveShell();
		ElementListSelectionDialog dialog = null;
		try {
			dialog = JDIDebugUIPlugin.createAllPackagesDialog(shell, null, false);
		} catch (JavaModelException jme) {
			String title = PropertyPageMessages.ExceptionFilterEditor_15;
			String message = PropertyPageMessages.ExceptionFilterEditor_16;
			ExceptionHandler.handle(jme, title, message);
			return;
		}
		if (dialog == null) {
			return;
		}
		dialog.setTitle(PropertyPageMessages.ExceptionFilterEditor_15);
		dialog.setMessage(PropertyPageMessages.ExceptionFilterEditor_18);
		dialog.setMultipleSelection(true);
		if (dialog.open() == IDialogConstants.CANCEL_ID) {
			return;
		}
		Object[] packages = dialog.getResult();
		if (packages != null) {
			for (int i = 0; i < packages.length; i++) {
				IJavaElement pkg = (IJavaElement) packages[i];
				String filter = pkg.getElementName();
				if (filter.length() < 1) {
					filter = DEFAULT_PACKAGE;
				} else {
					filter += ".*"; //$NON-NLS-1$
				}
				Filter f = fFilterContentProvider.addFilter(filter);
				fFilterContentProvider.checkFilter(f, true);
			}
		}
	}

	private void addType() {
		Shell shell = fAddTypeButton.getDisplay().getActiveShell();
		SelectionDialog dialog = null;
		try {
			dialog = JavaUI.createTypeDialog(shell, PlatformUI.getWorkbench().getProgressService(), SearchEngine.createWorkspaceScope(), IJavaElementSearchConstants.CONSIDER_CLASSES, false);
		} catch (JavaModelException jme) {
			String title = PropertyPageMessages.ExceptionFilterEditor_19;
			String message = PropertyPageMessages.ExceptionFilterEditor_20;
			ExceptionHandler.handle(jme, title, message);
			return;
		}

		dialog.setTitle(PropertyPageMessages.ExceptionFilterEditor_19);
		dialog.setMessage(PropertyPageMessages.ExceptionFilterEditor_22);
		if (dialog.open() == IDialogConstants.CANCEL_ID) {
			return;
		}

		Object[] types = dialog.getResult();
		IType type;
		if (types != null) {
			for (int i = 0; i < types.length; i++) {
				type = (IType) types[i];
				Filter f = fFilterContentProvider.addFilter(type.getFullyQualifiedName());
				fFilterContentProvider.checkFilter(f, true);
			}
		}
	}

	/**
	 * Content provider for the table.  Content consists of instances of Filter.
	 */
	protected class FilterContentProvider implements IStructuredContentProvider {

		private CheckboxTableViewer fViewer;
		private List<Filter> fFilters;

		public FilterContentProvider(CheckboxTableViewer viewer) {
			fViewer = viewer;
			populateFilters();
		}

		protected void populateFilters() {
			String[] iFilters = null;
			String[] eFilters = null;
			try {
				iFilters = fBreakpoint.getInclusionFilters();
				eFilters = fBreakpoint.getExclusionFilters();
			} catch (CoreException ce) {
				JDIDebugUIPlugin.log(ce);
				iFilters = new String[] {
				};
				eFilters = new String[] {
				};
			}
			fFilters = new ArrayList<>();
			populateFilters(iFilters, true);
			populateFilters(eFilters, false);

		}

		protected void populateFilters(String[] filters, boolean checked) {
			for (int i = 0; i < filters.length; i++) {
				String name = filters[i];
				if (name.length() == 0) {
					name = DEFAULT_PACKAGE;
				}
				Filter filter = addFilter(name);
				checkFilter(filter, checked);
			}
		}

		public Filter addFilter(String name) {
			Filter filter = new Filter(name, false);
			if (!fFilters.contains(filter)) {
				fFilters.add(filter);
				fViewer.add(filter);
			}
			return filter;
		}

		public void removeFilters(Object[] filters) {
			for (int i = 0; i < filters.length; i++) {
				Filter filter = (Filter) filters[i];
				fFilters.remove(filter);
			}
			fViewer.remove(filters);
		}

		public void toggleFilter(Filter filter) {
			boolean newState = !filter.isChecked();
			filter.setChecked(newState);
			fViewer.setChecked(filter, newState);
		}

		public void checkFilter(Filter filter, boolean checked) {
			filter.setChecked(checked);
			fViewer.setChecked(filter, checked);
		}

		/**
		 * @see IStructuredContentProvider#getElements(Object)
		 */
		@Override
		public Object[] getElements(Object inputElement) {
			return fFilters.toArray();
		}

		/**
		 * @see IContentProvider#inputChanged(Viewer, Object, Object)
		 */
		@Override
		public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
		}

		/**
		 * @see IContentProvider#dispose()
		 */
		@Override
		public void dispose() {
		}
	}

}
