/*******************************************************************************
 * Copyright (c) 2009, 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
 *
 *******************************************************************************/

package org.eclipse.jdt.internal.ui.callhierarchy;

import java.util.Arrays;
import java.util.List;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
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.Control;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IStatus;

import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.StatusDialog;
import org.eclipse.jface.layout.PixelConverter;
import org.eclipse.jface.operation.IRunnableContext;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.jface.window.Window;

import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.SelectionDialog;
import org.eclipse.ui.preferences.IWorkbenchPreferenceContainer;

import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaConventions;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;

import org.eclipse.jdt.internal.core.manipulation.util.BasicElementLabels;

import org.eclipse.jdt.ui.IJavaElementSearchConstants;
import org.eclipse.jdt.ui.JavaUI;
import org.eclipse.jdt.ui.PreferenceConstants;

import org.eclipse.jdt.internal.ui.IJavaHelpContextIds;
import org.eclipse.jdt.internal.ui.JavaPluginImages;
import org.eclipse.jdt.internal.ui.dialogs.StatusInfo;
import org.eclipse.jdt.internal.ui.dialogs.TextFieldNavigationHandler;
import org.eclipse.jdt.internal.ui.preferences.OptionsConfigurationBlock;
import org.eclipse.jdt.internal.ui.util.BusyIndicatorRunnableContext;
import org.eclipse.jdt.internal.ui.util.ExceptionHandler;
import org.eclipse.jdt.internal.ui.viewsupport.JavaElementImageProvider;
import org.eclipse.jdt.internal.ui.wizards.IStatusChangeListener;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.DialogField;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.IDialogFieldListener;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.IListAdapter;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.IStringButtonAdapter;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.LayoutUtil;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.ListDialogField;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.StringButtonDialogField;

/**
 * Call hierarchy expand with constructors configuration block.
 *
 * @since 3.6
 */
public class ExpandWithConstructorsConfigurationBlock extends OptionsConfigurationBlock {

	/**
	 * Call hierarchy expand with constructors dialog for types and members.
	 */
	private static class CallHierarchyTypesOrMembersDialog extends StatusDialog {

		/**
		 * The change listener class for the dialog field and the string button dialog field.
		 *
		 */
		private class StringButtonAdapter implements IDialogFieldListener, IStringButtonAdapter {
			/*
			 * @see IDialogFieldListener#dialogFieldChanged(DialogField)
			 */
			@Override
			public void dialogFieldChanged(DialogField field) {
				doValidation();
			}

			/*
			 * @see IStringButtonAdapter#changeControlPressed(DialogField)
			 */
			@Override
			public void changeControlPressed(DialogField field) {
				doBrowseTypes();
			}
		}

		/**
		 * The name dialog field to hold the default expand with constructors list.
		 */
		private StringButtonDialogField fNameDialogField;

		/**
		 * The list of previously existing entries.
		 */
		private List<String> fExistingEntries;

		/**
		 * Tells whether it is an member or type.
		 */
		private final boolean fIsEditingMember;

		/**
		 * Creates a call hierarchy preference dialog for members or types.
		 *
		 * @param parent the parent shell
		 * @param existingEntries the existing list of types and members
		 * @param isEditingMember <code>true</code if its a member, <code>false</code> otherwise
		 */
		public CallHierarchyTypesOrMembersDialog(Shell parent, List<String> existingEntries, boolean isEditingMember) {
			super(parent);
			fIsEditingMember= isEditingMember;
			fExistingEntries= existingEntries;

			String label, title;
			if (isEditingMember) {
				title= CallHierarchyMessages.CallHierarchyTypesOrMembersDialog_member_title;
				label= CallHierarchyMessages.CallHierarchyTypesOrMembersDialog_member_labelText;
			} else {
				title= CallHierarchyMessages.CallHierarchyTypesOrMembersDialog_type_title;
				label= CallHierarchyMessages.CallHierarchyTypesOrMembersDialog_type_labelText;
			}
			setTitle(title);

			StringButtonAdapter adapter= new StringButtonAdapter();

			fNameDialogField= new StringButtonDialogField(adapter);
			fNameDialogField.setLabelText(label);
			fNameDialogField.setButtonLabel(CallHierarchyMessages.CallHierarchyTypesOrMembersDialog_browse_button);
			fNameDialogField.setDialogFieldListener(adapter);
			fNameDialogField.setText(""); //$NON-NLS-1$
		}

		/*
		 * @see org.eclipse.jface.dialogs.Dialog#isResizable()		 *
		 */
		@Override
		protected boolean isResizable() {
			return true;
		}

		/**
		 * Sets the initial selection in the name dialog field.
		 *
		 * @param editedEntry the edited entry
		 */
		public void setInitialSelection(String editedEntry) {
			Assert.isNotNull(editedEntry);
			if (editedEntry.length() == 0)
				fNameDialogField.setText(""); //$NON-NLS-1$
			else
				fNameDialogField.setText(editedEntry);
		}

		/**
		 * Returns the resulting text from the name dialog field.
		 *
		 * @return the resulting text from the name dialog field
		 */
		public String getResult() {
			String val= fNameDialogField.getText();
			if (!fIsEditingMember)
				val= val + WILDCARD;
			return val;
		}

		/*
		 * @see org.eclipse.jface.dialogs.Dialog#createDialogArea(org.eclipse.swt.widgets.Composite)
		 */
		@Override
		protected Control createDialogArea(Composite parent) {
			Composite composite= (Composite)super.createDialogArea(parent);
			initializeDialogUnits(composite);

			GridLayout layout= (GridLayout)composite.getLayout();
			layout.numColumns= 2;

			fNameDialogField.doFillIntoGrid(composite, 3);

			fNameDialogField.getChangeControl(null).setVisible(!fIsEditingMember);

			LayoutUtil.setHorizontalSpan(fNameDialogField.getLabelControl(null), 2);

			int fieldWidthHint= convertWidthInCharsToPixels(60);
			Text text= fNameDialogField.getTextControl(null);
			LayoutUtil.setWidthHint(text, fieldWidthHint);
			LayoutUtil.setHorizontalGrabbing(text);
			LayoutUtil.setHorizontalSpan(text, fIsEditingMember ? 2 : 1);
			TextFieldNavigationHandler.install(text);

			DialogField.createEmptySpace(composite, 1);

			fNameDialogField.postSetFocusOnDialogField(parent.getDisplay());

			applyDialogFont(composite);
			return composite;
		}

		/**
		 * Creates the type hierarchy for type selection.
		 */
		private void doBrowseTypes() {
			IRunnableContext context= new BusyIndicatorRunnableContext();
			IJavaSearchScope scope= SearchEngine.createWorkspaceScope();
			int style= IJavaElementSearchConstants.CONSIDER_ALL_TYPES;
			try {
				SelectionDialog dialog= JavaUI.createTypeDialog(getShell(), context, scope, style, false, fNameDialogField.getText());
				dialog.setTitle(CallHierarchyMessages.CallHierarchyTypesOrMembersDialog_ChooseTypeDialog_title);
				dialog.setMessage(CallHierarchyMessages.CallHierarchyTypesOrMembersDialog_ChooseTypeDialog_description);
				if (dialog.open() == Window.OK) {
					IType res= (IType)dialog.getResult()[0];
					fNameDialogField.setText(res.getFullyQualifiedName('.'));
				}
			} catch (JavaModelException e) {
				ExceptionHandler.handle(e, getShell(), CallHierarchyMessages.CallHierarchyTypesOrMembersDialog_ChooseTypeDialog_title,
						CallHierarchyMessages.CallHierarchyTypesOrMembersDialog_ChooseTypeDialog_error_message);
			}
		}

		/**
		 * Validates the entered type or member and updates the status.
		 */
		private void doValidation() {
			StatusInfo status= new StatusInfo();
			String newText= fNameDialogField.getText();
			if (newText.length() == 0) {
				status.setError(""); //$NON-NLS-1$
			} else {
				IStatus val= JavaConventions.validateJavaTypeName(newText, JavaCore.VERSION_1_3, JavaCore.VERSION_1_3, null);
				if (val.matches(IStatus.ERROR)) {
					if (fIsEditingMember)
						status.setError(CallHierarchyMessages.CallHierarchyTypesOrMembersDialog_error_invalidMemberName);
					else
						status.setError(CallHierarchyMessages.CallHierarchyTypesOrMembersDialog_error_invalidTypeName);
				} else {
					if (doesExist(newText)) {
						status.setError(CallHierarchyMessages.CallHierarchyTypesOrMembersDialog_error_entryExists);
					}
				}
			}
			updateStatus(status);
		}

		/**
		 * Checks if the entry already exists.
		 *
		 * @param name the type or member name
		 * @return <code>true</code> if it already exists in the list of types and members,
		 *         <code>false</code> otherwise
		 */
		private boolean doesExist(String name) {
			return fExistingEntries.contains(name);
		}


		/*
		 * @see org.eclipse.jface.window.Window#configureShell(Shell)
		 */
		@Override
		protected void configureShell(Shell newShell) {
			super.configureShell(newShell);
			PlatformUI.getWorkbench().getHelpSystem().setHelp(newShell, IJavaHelpContextIds.CALL_HIERARCHY_EXPAND_WITH_CONSTRUCTORS_DIALOG);
		}

	}

	/*
	 * @see org.eclipse.jdt.internal.ui.preferences.OptionsConfigurationBlock#performOk()
	 */
	@Override
	public boolean performOk() {
		setValue(ANONYMOUS_EXPAND_WITH_CONSTRUCTORS, fIsAnonymous);
		return super.performOk();
	}

	/**
	 * The list label provider class.
	 */
	private static class ListLabelProvider extends LabelProvider {

		public final Image MEMBER_ICON;

		private final Image CLASS_ICON;

		/**
		 * Create the member and class icons.
		 */
		public ListLabelProvider() {
			MEMBER_ICON= JavaElementImageProvider.getDecoratedImage(JavaPluginImages.DESC_MISC_PUBLIC, 0, JavaElementImageProvider.SMALL_SIZE);
			CLASS_ICON= JavaElementImageProvider.getDecoratedImage(JavaPluginImages.DESC_OBJS_CLASS, 0, JavaElementImageProvider.SMALL_SIZE);
		}

		/*
		 * @see org.eclipse.jface.viewers.LabelProvider#getImage(java.lang.Object)
		 */
		@Override
		public Image getImage(Object element) {
			return ((String)element).endsWith(WILDCARD) ? CLASS_ICON : MEMBER_ICON;
		}

		/*
		 * @see org.eclipse.jface.viewers.LabelProvider#getText(java.lang.Object)
		 */
		@Override
		public String getText(Object element) {
			return BasicElementLabels.getJavaElementName((String)element);
		}
	}


	/**
	 * The change listener for <code>ListDialogField</code>.
	 */
	private class ListAdapter implements IListAdapter<String>, IDialogFieldListener {

		/**
		 * Checks if field can be edited.
		 *
		 * @param field the list dialog field
		 * @return <code>true</code> if it can be edited, <code>false</code> otherwise
		 */
		private boolean canEdit(ListDialogField<String> field) {
			List<String> selected= field.getSelectedElements();
			return selected.size() == 1;
		}

		/*
		 * @see org.eclipse.jdt.internal.ui.wizards.dialogfields.IListAdapter#customButtonPressed(org.eclipse.jdt.internal.ui.wizards.dialogfields.ListDialogField, int)
		 */
		@Override
		public void customButtonPressed(ListDialogField<String> field, int index) {
			doButtonPressed(index);
		}

		/*
		 * @see org.eclipse.jdt.internal.ui.wizards.dialogfields.IListAdapter#selectionChanged(org.eclipse.jdt.internal.ui.wizards.dialogfields.ListDialogField)
		 */
		@Override
		public void selectionChanged(ListDialogField<String> field) {
			fList.enableButton(IDX_EDIT, canEdit(field));
			fList.enableButton(IDX_REMOVE, canRemove(field));
		}

		/**
		 * Checks if the field can be removed.
		 *
		 * @param field the list dialog field
		 * @return <code>true</code> if it can be removed, <code>false</code> otherwise
		 */
		private boolean canRemove(ListDialogField<String> field) {
			List<String> selected= field.getSelectedElements();
			return !selected.isEmpty();
		}

		/* )
		 * @see org.eclipse.jdt.internal.ui.wizards.dialogfields.IDialogFieldListener#dialogFieldChanged(org.eclipse.jdt.internal.ui.wizards.dialogfields.DialogField)
		 */
		@Override
		public void dialogFieldChanged(DialogField field) {
			doDialogFieldChanged(field);
		}

		/*
		 * @see org.eclipse.jdt.internal.ui.wizards.dialogfields.IListAdapter#doubleClicked(org.eclipse.jdt.internal.ui.wizards.dialogfields.ListDialogField)
		 */
		@Override
		public void doubleClicked(ListDialogField<String> field) {
			if (canEdit(field)) {
				doButtonPressed(IDX_EDIT);
			}
		}
	}

	private static final String WILDCARD= ".*"; //$NON-NLS-1$

	private static final int IDX_NEW_TYPE= 0;
	private static final int IDX_NEW_MEMBER= 1;
	private static final int IDX_EDIT= 2;
	private static final int IDX_REMOVE= 3;
	private static final int IDX_RESTORE_DEFAULTS= 4;

	private ListDialogField<String> fList;

	private Button fAnonymousButton;

	protected boolean fIsAnonymous;

	/**
	 * A key that holds the list of methods or types whose methods are by default expanded with constructors in the Call Hierarchy.
	 */
	private static Key DEFAULT_EXPAND_WITH_CONSTRUCTORS_MEMBERS= getJDTUIKey(PreferenceConstants.PREF_DEFAULT_EXPAND_WITH_CONSTRUCTORS_MEMBERS);

	/**
	 * A key that controls whether the methods from anonymous types are by default expanded with constructors in the Call Hierarchy.
	 */
	private static Key ANONYMOUS_EXPAND_WITH_CONSTRUCTORS= getJDTUIKey(PreferenceConstants.PREF_ANONYMOUS_EXPAND_WITH_CONSTRUCTORS);

	/**
	 * Returns all the key values.
	 *
	 * @return array of keys
	 */
	public static Key[] getAllKeys() {
		return new Key[] { DEFAULT_EXPAND_WITH_CONSTRUCTORS_MEMBERS, ANONYMOUS_EXPAND_WITH_CONSTRUCTORS };
	}


	/**
	 * Creates the call hierarchy preferences configuration block.
	 *
	 * @param context the status
	 * @param container the preference container
	 */
	public ExpandWithConstructorsConfigurationBlock(IStatusChangeListener context, IWorkbenchPreferenceContainer container) {
		super(context, null, getAllKeys(), container);
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.preferences.OptionsConfigurationBlock#createContents(org.eclipse.swt.widgets.Composite)
	 */
	@Override
	protected Control createContents(Composite parent) {
		Composite control= new Composite(parent, SWT.NONE);
		GridLayout layout= new GridLayout();
		layout.numColumns= 2;
		layout.marginWidth= 10;
		layout.marginHeight= 10;
		control.setLayout(layout);

		createPreferenceList(control);

		fAnonymousButton= new Button(control, SWT.CHECK);
		fAnonymousButton.setText(CallHierarchyMessages.CallHierarchyTypesOrMembersDialog_anonymousTypes_label);
		fIsAnonymous= getBooleanValue(ANONYMOUS_EXPAND_WITH_CONSTRUCTORS);
		fAnonymousButton.setSelection(fIsAnonymous);
		fAnonymousButton.setLayoutData(new GridData(SWT.LEAD, SWT.TOP, false, false));
		fAnonymousButton.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				fIsAnonymous= fAnonymousButton.getSelection();
			}

		});

		initialize();

		Dialog.applyDialogFont(control);

		return control;
	}

	/**
	 * Create a list dialog field.
	 *
	 * @param parent the composite
	 */
	private void createPreferenceList(Composite parent) {
		String[] buttonLabels= new String[] {
				CallHierarchyMessages.ExpandWithConstructorsConfigurationBlock_newType_button,
				CallHierarchyMessages.ExpandWithConstructorsConfigurationBlock_newMember_button,
				CallHierarchyMessages.ExpandWithConstructorsConfigurationBlock_edit_button,
				CallHierarchyMessages.ExpandWithConstructorsConfigurationBlock_remove_button,
				CallHierarchyMessages.ExpandWithConstructorsConfigurationBlock_restoreDefaults_button
		};

		ListAdapter adapter= new ListAdapter();

		fList= new ListDialogField<>(adapter, buttonLabels, new ListLabelProvider());
		fList.setDialogFieldListener(adapter);
		fList.setLabelText(CallHierarchyMessages.ExpandWithConstructorsConfigurationBlock_description);
		fList.setRemoveButtonIndex(IDX_REMOVE);
		fList.enableButton(IDX_EDIT, false);
		fList.setViewerComparator(new ViewerComparator());

		PixelConverter pixelConverter= new PixelConverter(parent);

		fList.doFillIntoGrid(parent, 3);
		LayoutUtil.setHorizontalSpan(fList.getLabelControl(null), 2);
		LayoutUtil.setWidthHint(fList.getLabelControl(null), pixelConverter.convertWidthInCharsToPixels(60));
		LayoutUtil.setHorizontalGrabbing(fList.getListControl(null));

		Control listControl= fList.getListControl(null);
		GridData gd= (GridData)listControl.getLayoutData();
		gd.verticalAlignment= GridData.FILL;
		gd.grabExcessVerticalSpace= true;
		gd.heightHint= pixelConverter.convertHeightInCharsToPixels(10);
	}

	/**
	 * Initialize the elements of the list dialog field.
	 */
	public void initialize() {
		fList.setElements(Arrays.asList(getDefaultExpandWithConstructorsMembers()));
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.preferences.OptionsConfigurationBlock#performDefaults()
	 */
	@Override
	public void performDefaults() {
		String str= PreferenceConstants.getPreferenceStore().getDefaultString(PreferenceConstants.PREF_DEFAULT_EXPAND_WITH_CONSTRUCTORS_MEMBERS);
		fList.setElements(Arrays.asList(deserializeMembers(str)));
		fIsAnonymous= PreferenceConstants.getPreferenceStore().getDefaultBoolean(PreferenceConstants.PREF_ANONYMOUS_EXPAND_WITH_CONSTRUCTORS);
		fAnonymousButton.setSelection(fIsAnonymous);
		super.performDefaults();
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.preferences.OptionsConfigurationBlock#getFullBuildDialogStrings(boolean)
	 */
	@Override
	protected String[] getFullBuildDialogStrings(boolean workspaceSettings) {
		return null;
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.preferences.OptionsConfigurationBlock#validateSettings(org.eclipse.jdt.internal.ui.preferences.OptionsConfigurationBlock.Key, java.lang.String, java.lang.String)
	 */
	@Override
	protected void validateSettings(Key changedKey, String oldValue, String newValue) {

	}

	/**
	 * Perform the 'New' and 'Edit' button operations by opening the respective call hierarchy
	 * preferences dialog, and 'Restore Defaults' button by restoring to default values for the list dialog.
	 *
	 * @param index the index of the button
	 */
	private void doButtonPressed(int index) {
		switch (index) {
		case IDX_NEW_TYPE:
		case IDX_NEW_MEMBER:
			{
				// add new
				List<String> existing= fList.getElements();
				CallHierarchyTypesOrMembersDialog dialog= new CallHierarchyTypesOrMembersDialog(getShell(), existing, index == IDX_NEW_MEMBER);
				if (dialog.open() == Window.OK) {
					fList.addElement(dialog.getResult());
				}	break;
			}
		case IDX_EDIT:
			{
				// edit
				List<String> selected= fList.getSelectedElements();
				if (selected.isEmpty())
					return;
				String editedEntry= selected.get(0);
				List<String> existing= fList.getElements();
				existing.remove(editedEntry);
				boolean isType= editedEntry.endsWith(WILDCARD);
				CallHierarchyTypesOrMembersDialog dialog= new CallHierarchyTypesOrMembersDialog(getShell(), existing, !isType);
				if (isType)
					dialog.setInitialSelection(editedEntry.substring(0, editedEntry.length() - 2));
				else
					dialog.setInitialSelection(editedEntry);
				if (dialog.open() == Window.OK) {
					fList.replaceElement(editedEntry, dialog.getResult());
				}	break;
			}
		case IDX_RESTORE_DEFAULTS:
			performDefaults();
			break;
		default:
			break;
		}
	}

	/**
	 * Update the key on dialog field change.
	 *
	 * @param field the dialog field
	 */
	protected final void doDialogFieldChanged(DialogField field) {
		// set values in working copy
		if (field == fList) {
			setValue(DEFAULT_EXPAND_WITH_CONSTRUCTORS_MEMBERS, serializeMembers(fList.getElements()));
		}
	}

	/**
	 * Returns the array of strings containing the types or methods for default expand with constructors.
	 *
	 * @return the array of strings containing the types or methods for default expand with constructors
	 */
	private String[] getDefaultExpandWithConstructorsMembers() {
		String str= getValue(DEFAULT_EXPAND_WITH_CONSTRUCTORS_MEMBERS);
		if (str != null && str.length() > 0) {
			return deserializeMembers(str);
		}
		return new String[0];
	}

	/**
	 * Return the array of types and/or methods after splitting the stored preference string.
	 *
	 * @param str the input string
	 * @return the array of types and/or methods
	 */
	private static String[] deserializeMembers(String str) {
		return str.split(";"); //$NON-NLS-1$
	}

	/**
	 * Creates a single output string from the list of strings using a delimiter.
	 *
	 * @param list the input list of types and/or methods
	 * @return the single output string from the list of strings using a delimiter
	 */
	public static String serializeMembers(List<String> list) {
		return String.join(";", list.toArray(new String[list.size()])); //$NON-NLS-1$
	}

}
