/*******************************************************************************
 * Copyright (c) 2000, 2011 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.ui.refactoring;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

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.Item;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;

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

import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.layout.PixelConverter;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.CellEditor;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.ColumnWeightData;
import org.eclipse.jface.viewers.ComboBoxCellEditor;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.ICellModifier;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TableLayout;
import org.eclipse.jface.window.Window;

import org.eclipse.ui.PlatformUI;

import org.eclipse.ltk.core.refactoring.Refactoring;
import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;

import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.JavaModelException;

import org.eclipse.jdt.internal.corext.refactoring.structure.PushDownRefactoringProcessor;
import org.eclipse.jdt.internal.corext.refactoring.structure.PushDownRefactoringProcessor.MemberActionInfo;
import org.eclipse.jdt.internal.corext.util.Messages;

import org.eclipse.jdt.ui.JavaElementLabelProvider;
import org.eclipse.jdt.ui.JavaElementLabels;

import org.eclipse.jdt.internal.ui.IJavaHelpContextIds;
import org.eclipse.jdt.internal.ui.util.ExceptionHandler;
import org.eclipse.jdt.internal.ui.util.SWTUtil;
import org.eclipse.jdt.internal.ui.util.TableLayoutComposite;

public final class PushDownWizard extends RefactoringWizard {

	private final PushDownRefactoringProcessor fProcessor;

	private static class PushDownInputPage extends UserInputWizardPage {

		private static class MemberActionInfoLabelProvider extends LabelProvider implements ITableLabelProvider {

			private static String getActionLabel(final int action) {
				switch (action) {
					case MemberActionInfo.NO_ACTION:
						return ""; //$NON-NLS-1$
					case MemberActionInfo.PUSH_ABSTRACT_ACTION:
						return RefactoringMessages.PushDownInputPage_leave_abstract;
					case MemberActionInfo.PUSH_DOWN_ACTION:
						return RefactoringMessages.PushDownInputPage_push_down;
					default:
						Assert.isTrue(false);
						return null;
				}
			}

			private static String[] getAvailableActionLabels(final MemberActionInfo info) {
				final int[] actions= info.getAvailableActions();
				final String[] result= new String[actions.length];
				for (int index= 0; index < actions.length; index++) {
					result[index]= getActionLabel(actions[index]);
				}
				return result;
			}

			private final ILabelProvider fLabelProvider= new JavaElementLabelProvider(JavaElementLabelProvider.SHOW_DEFAULT | JavaElementLabelProvider.SHOW_SMALL_ICONS);

			@Override
			public void dispose() {
				fLabelProvider.dispose();
				super.dispose();
			}

			@Override
			public Image getColumnImage(final Object element, final int index) {
				final MemberActionInfo info= (MemberActionInfo) element;
				switch (index) {
					case MEMBER_COLUMN:
						return fLabelProvider.getImage(info.getMember());
					case ACTION_COLUMN:
						return null;
					default:
						Assert.isTrue(false);
						return null;
				}
			}

			@Override
			public String getColumnText(final Object element, final int index) {
				final MemberActionInfo info= (MemberActionInfo) element;
				switch (index) {
					case MEMBER_COLUMN:
						return fLabelProvider.getText(info.getMember());
					case ACTION_COLUMN:
						return getActionLabel(info.getAction());
					default:
						Assert.isTrue(false);
						return null;
				}
			}
		}

		private class PushDownCellModifier implements ICellModifier {

			@Override
			public boolean canModify(final Object element, final String property) {
				if (!ACTION_PROPERTY.equals(property))
					return false;
				return ((MemberActionInfo) element).isEditable();
			}

			@Override
			public Object getValue(final Object element, final String property) {
				if (!ACTION_PROPERTY.equals(property))
					return null;

				final MemberActionInfo info= (MemberActionInfo) element;
				return Integer.valueOf(info.getAction());
			}

			@Override
			public void modify(final Object element, final String property, final Object value) {
				if (!ACTION_PROPERTY.equals(property))
					return;

				final int action= ((Integer) value).intValue();
				MemberActionInfo info;
				if (element instanceof Item) {
					info= (MemberActionInfo) ((Item) element).getData();
				} else
					info= (MemberActionInfo) element;
				if (!canModify(info, property))
					return;
				info.setAction(action);
				PushDownInputPage.this.updateWizardPage(null, true);
			}
		}

		private static final int ACTION_COLUMN= 1;

		private final static String ACTION_PROPERTY= "action"; //$NON-NLS-1$

		private static final int MEMBER_COLUMN= 0;

		private final static String MEMBER_PROPERTY= "member"; //$NON-NLS-1$

		private static final String PAGE_NAME= "PushDownInputPage"; //$NON-NLS-1$

		private static final int ROW_COUNT= 10;

		private static int countEditableInfos(final MemberActionInfo[] infos) {
			int result= 0;
			for (MemberActionInfo info : infos) {
				if (info.isEditable()) {
					result++;
				}
			}
			return result;
		}

		private static void setInfoAction(final MemberActionInfo[] infos, final int action) {
			for (MemberActionInfo info : infos) {
				info.setAction(action);
			}
		}

		private Button fDeselectAllButton;

		private Button fEditButton;

		private Button fSelectAllButton;

		private Label fStatusLine;

		private PullPushCheckboxTableViewer fTableViewer;

		private final PushDownRefactoringProcessor fProcessor;

		public PushDownInputPage(PushDownRefactoringProcessor processor) {
			super(PAGE_NAME);
			fProcessor= processor;
		}

		private boolean areAllElementsMarkedAsNoAction() {
			return countInfosForAction(MemberActionInfo.NO_ACTION) == ((MemberActionInfo[]) fTableViewer.getInput()).length;
		}

		private boolean areAllElementsMarkedAsPushDownAction() {
			return countInfosForAction(MemberActionInfo.PUSH_DOWN_ACTION) == ((MemberActionInfo[]) fTableViewer.getInput()).length;
		}

		private void checkPageCompletionStatus(final boolean displayErrorMessage) {
			if (areAllElementsMarkedAsNoAction()) {
				if (displayErrorMessage)
					setErrorMessage(RefactoringMessages.PushDownInputPage_Select_members_to_push_down);
				setPageComplete(false);
			} else {
				setErrorMessage(null);
				setPageComplete(true);
			}
		}

		private int countInfosForAction(final int action) {
			int count= 0;
			for (MemberActionInfo info : (MemberActionInfo[]) fTableViewer.getInput()) {
				if (info.getAction() == action)
					count++;
			}
			return count;
		}

		private void createButtonComposite(final Composite parent) {
			final Composite composite= new Composite(parent, SWT.NONE);
			composite.setLayoutData(new GridData(GridData.FILL_VERTICAL));
			final GridLayout layout= new GridLayout();
			layout.marginHeight= 0;
			layout.marginWidth= 0;
			composite.setLayout(layout);

			fSelectAllButton= new Button(composite, SWT.PUSH);
			fSelectAllButton.setText(RefactoringMessages.PullUpWizard_select_all_label);
			fSelectAllButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
			fSelectAllButton.setEnabled(true);
			SWTUtil.setButtonDimensionHint(fSelectAllButton);
			fSelectAllButton.addSelectionListener(new SelectionAdapter() {

				@Override
				public void widgetSelected(final SelectionEvent event) {
					final IMember[] members= getMembers();
					setActionForMembers(members, MemberActionInfo.PUSH_DOWN_ACTION);
					updateWizardPage(null, true);
				}
			});

			fDeselectAllButton= new Button(composite, SWT.PUSH);
			fDeselectAllButton.setText(RefactoringMessages.PullUpWizard_deselect_all_label);
			fDeselectAllButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
			fDeselectAllButton.setEnabled(false);
			SWTUtil.setButtonDimensionHint(fDeselectAllButton);
			fDeselectAllButton.addSelectionListener(new SelectionAdapter() {

				@Override
				public void widgetSelected(final SelectionEvent event) {
					final IMember[] members= getMembers();
					setActionForMembers(members, MemberActionInfo.NO_ACTION);
					updateWizardPage(null, true);
				}
			});

			fEditButton= new Button(composite, SWT.PUSH);
			fEditButton.setText(RefactoringMessages.PushDownInputPage_Edit);
			final GridData data= new GridData(GridData.FILL_HORIZONTAL);
			data.verticalIndent= new PixelConverter(parent).convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN);
			fEditButton.setLayoutData(data);
			fEditButton.setEnabled(false);
			SWTUtil.setButtonDimensionHint(fEditButton);
			fEditButton.addSelectionListener(new SelectionAdapter() {

				@Override
				public void widgetSelected(final SelectionEvent event) {
					PushDownInputPage.this.editSelectedMembers();
				}
			});

			final Button addButton= new Button(composite, SWT.PUSH);
			addButton.setText(RefactoringMessages.PushDownInputPage_Add_Required);
			addButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
			SWTUtil.setButtonDimensionHint(addButton);
			addButton.addSelectionListener(new SelectionAdapter() {

				@Override
				public void widgetSelected(final SelectionEvent event) {
					PushDownInputPage.this.markAdditionalRequiredMembersAsMembersToPushDown();
				}
			});
		}

		@Override
		public void createControl(final Composite parent) {
			final Composite composite= new Composite(parent, SWT.NONE);
			composite.setLayout(new GridLayout());

			createMemberTableLabel(composite);
			createMemberTableComposite(composite);
			createStatusLine(composite);

			setControl(composite);
			Dialog.applyDialogFont(composite);
			PlatformUI.getWorkbench().getHelpSystem().setHelp(getControl(), IJavaHelpContextIds.PUSH_DOWN_WIZARD_PAGE);
		}

		private void createMemberTable(final Composite parent) {
			final TableLayoutComposite layouter= new TableLayoutComposite(parent, SWT.NONE);
			layouter.addColumnData(new ColumnWeightData(60, true));
			layouter.addColumnData(new ColumnWeightData(40, true));

			final Table table= new Table(layouter, SWT.H_SCROLL | SWT.V_SCROLL | SWT.MULTI | SWT.BORDER | SWT.FULL_SELECTION | SWT.CHECK);
			table.setHeaderVisible(true);
			table.setLinesVisible(true);

			final GridData data= new GridData(GridData.FILL_BOTH);
			data.heightHint= SWTUtil.getTableHeightHint(table, ROW_COUNT);
			data.widthHint= convertWidthInCharsToPixels(30);
			layouter.setLayoutData(data);

			final TableLayout layout= new TableLayout();
			table.setLayout(layout);

			final TableColumn first= new TableColumn(table, SWT.NONE);
			first.setText(RefactoringMessages.PushDownInputPage_Member);

			final TableColumn second= new TableColumn(table, SWT.NONE);
			second.setText(RefactoringMessages.PushDownInputPage_Action);

			fTableViewer= new PullPushCheckboxTableViewer(table);
			fTableViewer.setUseHashlookup(true);
			fTableViewer.setContentProvider(ArrayContentProvider.getInstance());
			fTableViewer.setLabelProvider(new MemberActionInfoLabelProvider());
			fTableViewer.addSelectionChangedListener(new ISelectionChangedListener() {

				@Override
				public void selectionChanged(final SelectionChangedEvent event) {
					PushDownInputPage.this.updateButtonEnablementState((IStructuredSelection) event.getSelection());
				}
			});
			fTableViewer.addCheckStateListener(new ICheckStateListener() {

				@Override
				public void checkStateChanged(final CheckStateChangedEvent event) {
					final boolean checked= event.getChecked();
					final MemberActionInfo info= (MemberActionInfo) event.getElement();
					if (checked)
						info.setAction(MemberActionInfo.PUSH_DOWN_ACTION);
					else
						info.setAction(MemberActionInfo.NO_ACTION);
					updateWizardPage(null, true);
				}
			});
			fTableViewer.addDoubleClickListener(new IDoubleClickListener() {

				@Override
				public void doubleClick(final DoubleClickEvent event) {
					PushDownInputPage.this.editSelectedMembers();
				}
			});

			fTableViewer.setInput(fProcessor.getMemberActionInfos());
			updateWizardPage(null, false);
			setupCellEditors(table);
		}

		private void createMemberTableComposite(final Composite parent) {
			final Composite composite= new Composite(parent, SWT.NONE);
			composite.setLayoutData(new GridData(GridData.FILL_BOTH));
			final GridLayout layout= new GridLayout();
			layout.numColumns= 2;
			layout.marginWidth= 0;
			layout.marginHeight= 0;
			composite.setLayout(layout);

			createMemberTable(composite);
			createButtonComposite(composite);
		}

		private void createMemberTableLabel(final Composite parent) {
			final Label label= new Label(parent, SWT.NONE);
			label.setText(RefactoringMessages.PushDownInputPage_Specify_actions);
			label.setLayoutData(new GridData());
		}

		private void createStatusLine(final Composite composite) {
			fStatusLine= new Label(composite, SWT.NONE);
			final GridData data= new GridData(SWT.FILL, SWT.CENTER, false, false);
			data.horizontalSpan= 2;
			updateStatusLine();
			fStatusLine.setLayoutData(data);
		}

		// String -> Integer
		private Map<String, Integer> createStringMappingForSelectedElements() {
			final Map<String, Integer> result= new HashMap<>();
			int action= MemberActionInfo.PUSH_DOWN_ACTION;
			result.put(MemberActionInfoLabelProvider.getActionLabel(action), Integer.valueOf(action));
			int action1= MemberActionInfo.PUSH_ABSTRACT_ACTION;
			result.put(MemberActionInfoLabelProvider.getActionLabel(action1), Integer.valueOf(action1));
			return result;
		}

		private void editSelectedMembers() {
			if (!fEditButton.isEnabled())
				return;
			final ISelection preserved= fTableViewer.getSelection();
			try {
				MemberActionInfo[] selectedMembers= getSelectedMemberActionInfos();
				final String labelText= selectedMembers.length == 1 ? Messages.format(RefactoringMessages.PushDownInputPage_Mark_selected_members_singular, JavaElementLabels.getElementLabel(
						selectedMembers[0].getMember(), JavaElementLabels.M_PARAMETER_TYPES)) : Messages.format(RefactoringMessages.PushDownInputPage_Mark_selected_members_plural, String
						.valueOf(selectedMembers.length));
				final Map<String, Integer> stringMapping= createStringMappingForSelectedElements();
				final String[] keys= stringMapping.keySet().toArray(new String[stringMapping.keySet().size()]);
				Arrays.sort(keys);
				final int initialSelectionIndex= getInitialSelectionIndexForEditDialog(stringMapping, keys);

				final ComboSelectionDialog dialog= new ComboSelectionDialog(getShell(), RefactoringMessages.PushDownInputPage_Edit_members, labelText, keys, initialSelectionIndex);
				dialog.setBlockOnOpen(true);
				if (dialog.open() == Window.CANCEL)
					return;
				final int action= stringMapping.get(dialog.getSelectedString()).intValue();
				setInfoAction(selectedMembers, action);
			} finally {
				updateWizardPage(preserved, true);
			}
		}

		private boolean enableEditButton(final IStructuredSelection selection) {
			if (selection.isEmpty() || selection.size() == 0)
				return false;
			return selection.size() == countEditableInfos(getSelectedMemberActionInfos());
		}

		private MemberActionInfo[] getActiveInfos() {
			final MemberActionInfo[] infos= fProcessor.getMemberActionInfos();
			final List<MemberActionInfo> result= new ArrayList<>(infos.length);
			for (MemberActionInfo info : infos) {
				if (info.isActive())
					result.add(info);
			}
			return result.toArray(new MemberActionInfo[result.size()]);
		}

		private int getCommonActionCodeForSelectedInfos() {
			final MemberActionInfo[] infos= getSelectedMemberActionInfos();
			if (infos.length == 0)
				return -1;

			final int code= infos[0].getAction();
			for (MemberActionInfo info : infos) {
				if (code != info.getAction()) {
					return -1;
				}
			}
			return code;
		}

		private int getInitialSelectionIndexForEditDialog(final Map<String, Integer> mapping, final String[] keys) {
			final int commonActionCode= getCommonActionCodeForSelectedInfos();
			if (commonActionCode == -1)
				return 0;
			for (final Iterator<String> iterator= mapping.keySet().iterator(); iterator.hasNext();) {
				final String key= iterator.next();
				final int action= mapping.get(key).intValue();
				if (commonActionCode == action) {
					for (int index= 0; index < keys.length; index++) {
						if (key.equals(keys[index]))
							return index;
					}
					Assert.isTrue(false);// there's no way
				}
			}
			return 0;
		}

		private IMember[] getMembers() {
			final MemberActionInfo[] infos= (MemberActionInfo[]) fTableViewer.getInput();
			final List<IMember> result= new ArrayList<>(infos.length);
			for (MemberActionInfo info : infos) {
				result.add(info.getMember());
			}
			return result.toArray(new IMember[result.size()]);
		}

		private MemberActionInfo[] getSelectedMemberActionInfos() {
			Assert.isTrue(fTableViewer.getSelection() instanceof IStructuredSelection);
			final List<?> result= ((IStructuredSelection) fTableViewer.getSelection()).toList();
			return result.toArray(new MemberActionInfo[result.size()]);
		}

		public void markAdditionalRequiredMembersAsMembersToPushDown() {
			try {
				PlatformUI.getWorkbench().getActiveWorkbenchWindow().run(false, false, new IRunnableWithProgress() {

					@Override
					public void run(final IProgressMonitor pm) throws InvocationTargetException {
						try {
							fProcessor.computeAdditionalRequiredMembersToPushDown(pm);
							updateWizardPage(null, true);
						} catch (final JavaModelException e) {
							throw new InvocationTargetException(e);
						} finally {
							pm.done();
						}
					}
				});
			} catch (final InvocationTargetException e) {
				ExceptionHandler.handle(e, getShell(), RefactoringMessages.PushDownInputPage_Push_Down, RefactoringMessages.PushDownInputPage_Internal_Error);
			} catch (final InterruptedException e) {
				Assert.isTrue(false);// not cancelable
			}
		}

		private void setActionForMembers(final IMember[] members, final int action) {
			final MemberActionInfo[] infos= (MemberActionInfo[]) fTableViewer.getInput();
			for (IMember member : members) {
				for (MemberActionInfo info : infos) {
					if (info.getMember().equals(member)) {
						info.setAction(action);
					}
				}
			}
		}

		private void setupCellEditors(final Table table) {
			final ComboBoxCellEditor comboBoxCellEditor= new ComboBoxCellEditor();
			comboBoxCellEditor.setStyle(SWT.READ_ONLY);
			fTableViewer.setCellEditors(new CellEditor[] { null, comboBoxCellEditor});
			fTableViewer.addSelectionChangedListener(new ISelectionChangedListener() {

				@Override
				public void selectionChanged(final SelectionChangedEvent event) {
					if (comboBoxCellEditor.getControl() == null && !table.isDisposed())
						comboBoxCellEditor.create(table);
					Assert.isTrue(event.getSelection() instanceof IStructuredSelection);
					final IStructuredSelection ss= (IStructuredSelection) event.getSelection();
					if (ss.size() != 1)
						return;
					final MemberActionInfo mac= (MemberActionInfo) ss.getFirstElement();
					comboBoxCellEditor.setItems(MemberActionInfoLabelProvider.getAvailableActionLabels(mac));
					comboBoxCellEditor.setValue(Integer.valueOf(mac.getAction()));
				}
			});

			final ICellModifier cellModifier= new PushDownCellModifier();
			fTableViewer.setCellModifier(cellModifier);
			fTableViewer.setColumnProperties(new String[] { MEMBER_PROPERTY, ACTION_PROPERTY});
		}

		@Override
		public void setVisible(final boolean visible) {
			super.setVisible(visible);
			if (visible) {
				fTableViewer.setSelection(new StructuredSelection(getActiveInfos()), true);
				fTableViewer.getControl().setFocus();
			}
		}

		private void updateButtonEnablementState(final IStructuredSelection tableSelection) {
			if (tableSelection == null || fEditButton == null)
				return;
			fEditButton.setEnabled(enableEditButton(tableSelection));
			if (fSelectAllButton != null)
				fSelectAllButton.setEnabled(!areAllElementsMarkedAsPushDownAction());
			if (fDeselectAllButton != null)
				fDeselectAllButton.setEnabled(!areAllElementsMarkedAsNoAction());
		}

		private void updateStatusLine() {
			if (fStatusLine == null)
				return;
			Object[] selectedMembers= fTableViewer.getCheckedElements();
			final int selected= selectedMembers.length;
			final String msg= selected == 1 ? Messages.format(RefactoringMessages.PushDownInputPage_status_line_singular, JavaElementLabels.getElementLabel(
					((MemberActionInfo)selectedMembers[0]).getMember(), JavaElementLabels.M_PARAMETER_TYPES)) : Messages.format(RefactoringMessages.PushDownInputPage_status_line_plural, String
					.valueOf(selected));
			fStatusLine.setText(msg);
		}

		private void updateWizardPage(final ISelection preserved, final boolean displayErrorMessage) {
			fTableViewer.refresh();
			if (preserved != null) {
				fTableViewer.getControl().setFocus();
				fTableViewer.setSelection(preserved);
			}
			checkPageCompletionStatus(displayErrorMessage);
			updateButtonEnablementState(((IStructuredSelection) fTableViewer.getSelection()));
			updateStatusLine();
		}
	}

	public PushDownWizard(PushDownRefactoringProcessor processor, Refactoring ref) {
		super(ref, DIALOG_BASED_USER_INTERFACE);
		fProcessor= processor;
		setDefaultPageTitle(RefactoringMessages.PushDownWizard_defaultPageTitle);
	}

	@Override
	protected void addUserInputPages() {
		addPage(new PushDownInputPage(fProcessor));
	}
}
