/*******************************************************************************
 * Copyright (c) 2006, 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
 *******************************************************************************/
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.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
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.Label;
import org.eclipse.swt.widgets.Tree;

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

import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.jface.wizard.IWizardPage;

import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.source.SourceViewer;

import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.ContainerCheckedTreeViewer;

import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;

import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.ISourceReference;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.JavaModelException;

import org.eclipse.jdt.internal.corext.refactoring.RefactoringCoreMessages;
import org.eclipse.jdt.internal.corext.refactoring.structure.PullUpRefactoringProcessor;
import org.eclipse.jdt.internal.corext.refactoring.util.JavaElementUtil;
import org.eclipse.jdt.internal.corext.util.Messages;
import org.eclipse.jdt.internal.core.manipulation.util.Strings;

import org.eclipse.jdt.ui.JavaElementComparator;
import org.eclipse.jdt.ui.JavaElementLabelProvider;
import org.eclipse.jdt.ui.JavaElementLabels;
import org.eclipse.jdt.ui.PreferenceConstants;
import org.eclipse.jdt.ui.text.JavaSourceViewerConfiguration;

import org.eclipse.jdt.internal.ui.IJavaHelpContextIds;
import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.javaeditor.JavaSourceViewer;
import org.eclipse.jdt.internal.ui.util.ExceptionHandler;
import org.eclipse.jdt.internal.ui.util.SWTUtil;

/**
 * Wizard page for pull up refactoring wizards which allows to specify the
 * methods to be deleted in subtypes after pull up.
 *
 * @since 3.2
 */
public class PullUpMethodPage extends UserInputWizardPage {

	private static class PullUpFilter extends ViewerFilter {

		private static boolean anySubtypeCanBeShown(final IType type, final Map<IType, IMember[]> typeToMemberArray, final ITypeHierarchy hierarchy) {
			for (IType subType : hierarchy.getSubtypes(type)) {
				if (canBeShown(subType, typeToMemberArray, hierarchy)) {
					return true;
				}
			}
			return false;
		}

		private static boolean canBeShown(final IType type, final Map<IType, IMember[]> typeToMemberArray, final ITypeHierarchy hierarchy) {
			if (typeToMemberArray.containsKey(type))
				return true;
			return anySubtypeCanBeShown(type, typeToMemberArray, hierarchy);
		}

		private static Set<IType> computeShowableSubtypesOfMainType(final ITypeHierarchy hierarchy, final Map<IType, IMember[]> typeToMemberArray) {
			final Set<IType> result= new HashSet<>();
			for (IType subtype : hierarchy.getAllSubtypes(hierarchy.getType())) {
				if (canBeShown(subtype, typeToMemberArray, hierarchy))
					result.add(subtype);
			}
			return result;
		}

		private static Set<IType> computeTypesToShow(final ITypeHierarchy hierarchy, final Map<IType, IMember[]> typeToMemberArray) {
			final Set<IType> typesToShow= new HashSet<>();
			typesToShow.add(hierarchy.getType());
			typesToShow.addAll(computeShowableSubtypesOfMainType(hierarchy, typeToMemberArray));
			return typesToShow;
		}

		private final Set<IType> fTypesToShow;

		public PullUpFilter(final ITypeHierarchy hierarchy, final IMember[] members) {
			// IType -> IMember[]
			final Map<IType, IMember[]> map= PullUpMethodPage.createTypeToMemberArrayMapping(members);
			fTypesToShow= computeTypesToShow(hierarchy, map);
		}

		@Override
		public boolean select(final Viewer viewer, final Object parentElement, final Object element) {
			if (element instanceof IMethod)
				return true;
			return fTypesToShow.contains(element);
		}
	}

	private static class PullUpHierarchyContentProvider implements ITreeContentProvider {

		private IType fDeclaringType;

		private ITypeHierarchy fHierarchy;

		private Map<IType, IMember[]> fTypeToMemberArray; // IType -> IMember[]

		public PullUpHierarchyContentProvider(final IType declaringType, final IMember[] members) {
			fDeclaringType= declaringType;
			fTypeToMemberArray= PullUpMethodPage.createTypeToMemberArrayMapping(members);
		}

		@Override
		public void dispose() {
			fHierarchy= null;
			fTypeToMemberArray.clear();
			fTypeToMemberArray= null;
			fDeclaringType= null;
		}

		@Override
		public Object[] getChildren(final Object parentElement) {
			if (parentElement instanceof IType)
				return getSubclassesAndMembers((IType) parentElement);
			else
				return new Object[0];
		}

		@Override
		public Object[] getElements(final Object inputElement) {
			Assert.isTrue(inputElement == null || inputElement instanceof ITypeHierarchy);
			return new IType[] { fHierarchy.getType()};
		}

		private IMember[] getMembers(final IType type) {
			if (fTypeToMemberArray.containsKey(type))
				return (fTypeToMemberArray.get(type));
			else
				return new IMember[0];
		}

		@Override
		public Object getParent(final Object element) {
			if (element instanceof IType)
				return fHierarchy.getSuperclass((IType) element);
			if (element instanceof IMember)
				return ((IMember) element).getDeclaringType();
			Assert.isTrue(false, "Should not get here"); //$NON-NLS-1$
			return null;
		}

		private IType[] getSubclasses(final IType type) {
			if (type.equals(fDeclaringType))
				return new IType[0];
			return fHierarchy.getSubclasses(type);
		}

		private Object[] getSubclassesAndMembers(final IType type) {
			final Set<IMember> set= new HashSet<>();
			set.addAll(Arrays.asList(getSubclasses(type)));
			set.addAll(Arrays.asList(getMembers(type)));
			return set.toArray();
		}

		@Override
		public boolean hasChildren(final Object element) {
			if (!(element instanceof IType))
				return false;
			final IType type= (IType) element;
			return (fHierarchy.getAllSubtypes(type).length > 0) || fTypeToMemberArray.containsKey(type);
		}

		@Override
		public void inputChanged(final Viewer viewer, final Object oldInput, final Object newInput) {
			Assert.isTrue(newInput == null || newInput instanceof ITypeHierarchy);
			fHierarchy= (ITypeHierarchy) newInput;
		}
	}

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

	// IType -> IMember[]
	private static Map<IType, IMember[]> createTypeToMemberArrayMapping(final IMember[] members) {
		final Map<IType, HashSet<IMember>> typeToMemberSet= createTypeToMemberSetMapping(members);

		final Map<IType, IMember[]> typeToMemberArray= new HashMap<>();
		for (Map.Entry<IType, HashSet<IMember>> entry : typeToMemberSet.entrySet()) {
			IType type = entry.getKey();
			final Set<IMember> memberSet= entry.getValue();
			final IMember[] memberArray= memberSet.toArray(new IMember[memberSet.size()]);
			typeToMemberArray.put(type, memberArray);
		}
		return typeToMemberArray;
	}

	// IType -> Set of IMember
	private static Map<IType, HashSet<IMember>> createTypeToMemberSetMapping(final IMember[] members) {
		final Map<IType, HashSet<IMember>> typeToMemberSet= new HashMap<>();
		for (IMember member : members) {
			final IType type= member.getDeclaringType();
			if (!typeToMemberSet.containsKey(type))
				typeToMemberSet.put(type, new HashSet<IMember>());
			typeToMemberSet.get(type).add(member);
		}
		return typeToMemberSet;
	}

	private boolean fChangedSettings= true;

	private Label fSelectionLabel;

	private SourceViewer fSourceViewer;

	private ContainerCheckedTreeViewer fTreeViewer;

	private Label fTypeHierarchyLabel;

	private final PullUpRefactoringProcessor fProcessor;

	public PullUpMethodPage(PullUpRefactoringProcessor processor) {
		super(PAGE_NAME);
		fProcessor= processor;
		setMessage(RefactoringMessages.PullUpInputPage_select_methods);
	}

	protected PullUpRefactoringProcessor getPullUpRefactoringProcessor() {
		return fProcessor;
	}

	private void checkAllParents(final IType parent) {
		final ITypeHierarchy th= getTreeInput();
		final IType root= getTreeInput().getType();
		IType type= parent;
		while (!root.equals(type)) {
			fTreeViewer.setChecked(type, true);
			type= th.getSuperclass(type);
		}
		fTreeViewer.setChecked(root, true);
	}

	public void checkPulledUp() {
		uncheckAll();
		fTreeViewer.setCheckedElements(fProcessor.getMembersToMove());
		final IType parent= fProcessor.getDeclaringType();
		fTreeViewer.setChecked(parent, true);
		checkAllParents(parent);
	}

	private void createButtonComposite(final Composite superComposite) {
		final Composite buttonComposite= new Composite(superComposite, SWT.NONE);
		buttonComposite.setLayoutData(new GridData(GridData.FILL, GridData.BEGINNING, true, false));
		final GridLayout layout= new GridLayout(2, false);
		layout.marginWidth= 0;
		buttonComposite.setLayout(layout);

		fSelectionLabel= new Label(buttonComposite, SWT.LEFT | SWT.WRAP | SWT.HORIZONTAL);
		GridData data= new GridData(GridData.BEGINNING, GridData.BEGINNING, true, false);
		data.widthHint= convertWidthInCharsToPixels(32);
		fSelectionLabel.setLayoutData(data);

		final Button button= new Button(buttonComposite, SWT.PUSH);
		button.setText(RefactoringMessages.PullUpInputPage2_Select);
		button.setLayoutData(new GridData());
		SWTUtil.setButtonDimensionHint(button);
		button.addSelectionListener(new SelectionAdapter() {

			@Override
			public void widgetSelected(final SelectionEvent e) {
				checkPulledUp();
				updateSelectionLabel();
			}
		});
	}

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

		createTreeAndSourceViewer(composite);
		createButtonComposite(composite);
		setControl(composite);

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

	private void createHierarchyTreeComposite(final Composite parent) {
		final Composite composite= new Composite(parent, SWT.NONE);
		composite.setLayoutData(new GridData(GridData.FILL_BOTH));
		final GridLayout layout= new GridLayout();
		layout.marginWidth= 0;
		layout.marginHeight= 0;
		layout.horizontalSpacing= 1;
		layout.verticalSpacing= 1;
		composite.setLayout(layout);

		createTypeHierarchyLabel(composite);
		createTreeViewer(composite);
	}

	private void createSourceViewer(final Composite c) {
		final IPreferenceStore store= JavaPlugin.getDefault().getCombinedPreferenceStore();
		fSourceViewer= new JavaSourceViewer(c, null, null, false, SWT.V_SCROLL | SWT.H_SCROLL | SWT.MULTI | SWT.BORDER | SWT.FULL_SELECTION, store);
		fSourceViewer.configure(new JavaSourceViewerConfiguration(JavaPlugin.getDefault().getJavaTextTools().getColorManager(), store, null, null));
		fSourceViewer.setEditable(false);
		fSourceViewer.getControl().setLayoutData(new GridData(GridData.FILL_BOTH));
		fSourceViewer.getControl().setFont(JFaceResources.getFont(PreferenceConstants.EDITOR_TEXT_FONT));
	}

	private void createSourceViewerComposite(final Composite parent) {
		final Composite c= new Composite(parent, SWT.NONE);
		c.setLayoutData(new GridData(GridData.FILL_BOTH));
		final GridLayout layout= new GridLayout();
		layout.marginWidth= 0;
		layout.marginHeight= 0;
		layout.horizontalSpacing= 1;
		layout.verticalSpacing= 1;
		c.setLayout(layout);

		createSourceViewerLabel(c);
		createSourceViewer(c);
	}

	private void createSourceViewerLabel(final Composite c) {
		final Label label= new Label(c, SWT.WRAP);
		final GridData gd= new GridData(GridData.FILL_HORIZONTAL);
		label.setText(RefactoringMessages.PullUpInputPage2_Source);
		label.setLayoutData(gd);
	}

	private void createTreeAndSourceViewer(final Composite superComposite) {
		final SashForm composite= new SashForm(superComposite, SWT.HORIZONTAL);
		initializeDialogUnits(superComposite);
		final GridData gd= new GridData(GridData.FILL_BOTH);
		gd.heightHint= convertHeightInCharsToPixels(20);
		gd.widthHint= convertWidthInCharsToPixels(10);
		composite.setLayoutData(gd);
		final GridLayout layout= new GridLayout();
		layout.numColumns= 2;
		layout.marginWidth= 0;
		layout.marginHeight= 0;
		layout.horizontalSpacing= 1;
		layout.verticalSpacing= 1;
		composite.setLayout(layout);

		createHierarchyTreeComposite(composite);
		createSourceViewerComposite(composite);
		composite.setWeights(new int[] { 50, 50});
	}

	private void createTreeViewer(final Composite composite) {
		final Tree tree= new Tree(composite, SWT.CHECK | SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | SWT.H_SCROLL);
		tree.setLayoutData(new GridData(GridData.FILL_BOTH));
		fTreeViewer= new ContainerCheckedTreeViewer(tree);
		fTreeViewer.setLabelProvider(new JavaElementLabelProvider(JavaElementLabelProvider.SHOW_DEFAULT | JavaElementLabelProvider.SHOW_SMALL_ICONS));
		fTreeViewer.setUseHashlookup(true);
		fTreeViewer.setComparator(new JavaElementComparator());
		fTreeViewer.addSelectionChangedListener(this::treeViewerSelectionChanged);
		fTreeViewer.addCheckStateListener(event -> updateSelectionLabel());
	}

	private void createTypeHierarchyLabel(final Composite composite) {
		fTypeHierarchyLabel= new Label(composite, SWT.WRAP);
		final GridData gd= new GridData(GridData.FILL_HORIZONTAL);
		fTypeHierarchyLabel.setLayoutData(gd);
	}

	public void fireSettingsChanged() {
		fChangedSettings= true;
	}

	private IMethod[] getCheckedMethods() {
		final Object[] checked= fTreeViewer.getCheckedElements();
		final List<IMethod> members= new ArrayList<>(checked.length);
		for (Object c : checked) {
			if (c instanceof IMethod) {
				members.add((IMethod) c);
			}
		}
		return members.toArray(new IMethod[members.size()]);
	}

	private ISourceReference getFirstSelectedSourceReference(final SelectionChangedEvent event) {
		final ISelection s= event.getSelection();
		if (!(s instanceof IStructuredSelection))
			return null;
		final IStructuredSelection ss= (IStructuredSelection) s;
		if (ss.size() != 1)
			return null;
		final Object first= ss.getFirstElement();
		if (!(first instanceof ISourceReference))
			return null;
		return (ISourceReference) first;
	}

	@Override
	public IWizardPage getNextPage() {
		initializeRefactoring();
		return super.getNextPage();
	}

	private String getSupertypeSignature() {
		return JavaElementUtil.createSignature(fProcessor.getDestinationType());
	}

	private ITypeHierarchy getTreeInput() {
		return (ITypeHierarchy) fTreeViewer.getInput();
	}

	private void initializeRefactoring() {
		fProcessor.setDeletedMethods(getCheckedMethods());
	}

	private void initializeTreeViewer() {
		try {
			getContainer().run(false, false, pm -> {
				try {
					initializeTreeViewer(pm);
				} finally {
					pm.done();
				}
			});
		} catch (InvocationTargetException e) {
			ExceptionHandler.handle(e, getShell(), RefactoringMessages.PullUpInputPage_pull_Up, RefactoringMessages.PullUpInputPage_exception);
		} catch (InterruptedException e) {
			Assert.isTrue(false);
		}
	}

	private void initializeTreeViewer(final IProgressMonitor pm) {
		try {
			pm.beginTask(RefactoringCoreMessages.PullUpRefactoring_checking, 2);
			final IMember[] matchingMethods= fProcessor.getMatchingElements(new SubProgressMonitor(pm, 1), false);
			final ITypeHierarchy hierarchy= fProcessor.getDestinationTypeHierarchy(new SubProgressMonitor(pm, 1));
			removeAllTreeViewFilters();
			fTreeViewer.addFilter(new PullUpFilter(hierarchy, matchingMethods));
			fTreeViewer.setContentProvider(new PullUpHierarchyContentProvider(fProcessor.getDeclaringType(), matchingMethods));
			fTreeViewer.setInput(hierarchy);
			precheckElements(fTreeViewer);
			fTreeViewer.expandAll();
			updateSelectionLabel();
		} catch (JavaModelException e) {
			ExceptionHandler.handle(e, RefactoringMessages.PullUpInputPage_pull_up1, RefactoringMessages.PullUpInputPage_exception);
			fTreeViewer.setInput(null);
		} finally {
			pm.done();
		}
	}

	@Override
	protected boolean performFinish() {
		initializeRefactoring();
		return super.performFinish();
	}

	private void precheckElements(final ContainerCheckedTreeViewer treeViewer) {
		for (IMember member : fProcessor.getMembersToMove()) {
			treeViewer.setChecked(member, true);
		}
	}

	private void removeAllTreeViewFilters() {
		for (ViewerFilter filter : fTreeViewer.getFilters()) {
			fTreeViewer.removeFilter(filter);
		}
	}

	private void setHierarchyLabelText() {
		final String message= Messages.format(RefactoringMessages.PullUpInputPage_subtypes, getSupertypeSignature());
		fTypeHierarchyLabel.setText(message);
	}

	private void setSourceViewerContents(String contents) {
		if (contents != null) {
			final IJavaProject project= fProcessor.getDestinationType().getJavaProject();
			final String[] lines= Strings.convertIntoLines(contents);
			if (lines.length > 0) {
				final int indent= Strings.computeIndentUnits(lines[lines.length - 1], project);
				contents= Strings.changeIndent(contents, indent, project, "", "\n"); //$NON-NLS-1$ //$NON-NLS-2$
			}
		}
		final IDocument document= (contents == null) ? new Document() : new Document(contents);
		JavaPlugin.getDefault().getJavaTextTools().setupJavaDocumentPartitioner(document);
		fSourceViewer.setDocument(document);
	}

	@Override
	public void setVisible(final boolean visible) {
		if (visible && fChangedSettings) {
			fChangedSettings= false;
			initializeTreeViewer();
			setHierarchyLabelText();
		}
		super.setVisible(visible);
	}

	private void showInSourceViewer(final ISourceReference selected) throws JavaModelException {
		if (selected == null)
			setSourceViewerContents(null);
		else
			setSourceViewerContents(selected.getSource());
	}

	private void treeViewerSelectionChanged(final SelectionChangedEvent event) {
		try {
			showInSourceViewer(getFirstSelectedSourceReference(event));
		} catch (JavaModelException e) {
			ExceptionHandler.handle(e, RefactoringMessages.PullUpInputPage_pull_up1, RefactoringMessages.PullUpInputPage_see_log);
		}
	}

	private void uncheckAll() {
		final IType root= getTreeInput().getType();
		fTreeViewer.setChecked(root, false);
	}

	private void updateSelectionLabel() {
		IMethod[] methods= getCheckedMethods();
		int checkedMethodsCount= methods.length;
		String text= checkedMethodsCount == 1 ? Messages.format(RefactoringMessages.PullUpInputPage_hierarchyLabal_singular, JavaElementLabels.getElementLabel(methods[0],
				JavaElementLabels.M_PARAMETER_TYPES)) : Messages.format(RefactoringMessages.PullUpInputPage_hierarchyLabal_plural, String.valueOf(checkedMethodsCount));
		fSelectionLabel.setText(text);

	}
}