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

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
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.graphics.Point;
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.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;

import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.LabelProvider;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.ITextSelection;

import org.eclipse.ui.IWorkbenchSite;
import org.eclipse.ui.IWorkingSet;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.ListDialog;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.compiler.InvalidInputException;

import org.eclipse.jdt.internal.core.manipulation.util.BasicElementLabels;
import org.eclipse.jdt.internal.corext.refactoring.RefactoringAvailabilityTester;
import org.eclipse.jdt.internal.corext.refactoring.nls.NLSElement;
import org.eclipse.jdt.internal.corext.refactoring.nls.NLSLine;
import org.eclipse.jdt.internal.corext.refactoring.nls.NLSScanner;
import org.eclipse.jdt.internal.corext.refactoring.reorg.ReorgUtils;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
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.JavaPlugin;
import org.eclipse.jdt.internal.ui.actions.ActionMessages;
import org.eclipse.jdt.internal.ui.actions.ActionUtil;
import org.eclipse.jdt.internal.ui.actions.SelectionConverter;
import org.eclipse.jdt.internal.ui.javaeditor.CompilationUnitEditor;
import org.eclipse.jdt.internal.ui.refactoring.nls.ExternalizeWizard;
import org.eclipse.jdt.internal.ui.util.ExceptionHandler;


/**
 * Externalizes the strings of a compilation unit or find all strings
 * in a package or project that are not externalized yet. Opens a wizard that
 * gathers additional information to externalize the strings.
 * <p>
 * The action is applicable to structured selections containing elements
 * of type <code>ICompilationUnit</code>, <code>IType</code>, <code>IJavaProject</code>,
 * <code>IPackageFragment</code>, and <code>IPackageFragmentRoot</code>
 *
 * <p>
 * This class may be instantiated; it is not intended to be subclassed.
 * </p>
 *
 * @since 2.0
 *
 * @noextend This class is not intended to be subclassed by clients.
 */
public class ExternalizeStringsAction extends SelectionDispatchAction {

	private CompilationUnitEditor fEditor;

	private NonNLSElement[] fElements;

	/**
	 * Creates a new <code>ExternalizeStringsAction</code>. The action requires
	 * that the selection provided by the site's selection provider is of type <code>
	 * org.eclipse.jface.viewers.IStructuredSelection</code>.
	 *
	 * @param site the site providing context information for this action
	 */
	public ExternalizeStringsAction(IWorkbenchSite site) {
		super(site);
		setText(ActionMessages.ExternalizeStringsAction_label);
		PlatformUI.getWorkbench().getHelpSystem().setHelp(this, IJavaHelpContextIds.EXTERNALIZE_STRINGS_ACTION);
	}

	/**
	 * Note: This constructor is for internal use only. Clients should not call this constructor.
	 * @param editor the compilation unit editor
	 *
	 * @noreference This constructor is not intended to be referenced by clients.
	 */
	public ExternalizeStringsAction(CompilationUnitEditor editor) {
		this(editor.getEditorSite());
		fEditor= editor;
		setEnabled(fEditor != null && SelectionConverter.canOperateOn(fEditor));
	}

	@Override
	public void selectionChanged(ITextSelection selection) {
	}

	@Override
	public void selectionChanged(IStructuredSelection selection) {
		try {
			setEnabled(RefactoringAvailabilityTester.isExternalizeStringsAvailable(selection));
		} catch (JavaModelException e) {
			if (JavaModelUtil.isExceptionToBeLogged(e))
				JavaPlugin.log(e);
			setEnabled(false);//no UI - happens on selection changes
		}
	}

	@Override
	public void run(ITextSelection selection) {
		IJavaElement element= SelectionConverter.getInput(fEditor);
		if (!(element instanceof ICompilationUnit))
			return;
		run((ICompilationUnit)element);
	}

	@Override
	public void run(IStructuredSelection selection) {
		ICompilationUnit unit= getCompilationUnit(selection);
		if (unit != null) {//run on cu
			run(unit);
		} else {
			//run on multiple
			try {
				PlatformUI.getWorkbench().getProgressService().run(true, true, createRunnable(selection));
			} catch(InvocationTargetException e) {
				ExceptionHandler.handle(e, getShell(),
						ActionMessages.ExternalizeStringsAction_dialog_title,
						ActionMessages.FindStringsToExternalizeAction_error_message);
				return;
			} catch(InterruptedException e) {
				// OK
				return;
			}
			showResults();
		}
	}

	/**
	 * Note: this method is for internal use only. Clients should not call this method.
	 * @param unit the compilation unit
	 *
	 * @noreference This method is not intended to be referenced by clients.
	 */
	public void run(ICompilationUnit unit) {
		if (!ActionUtil.isEditable(fEditor, getShell(), unit))
			return;

		ExternalizeWizard.open(unit, getShell());
	}


	private static ICompilationUnit getCompilationUnit(IStructuredSelection selection) {
		if (selection.isEmpty() || selection.size() != 1)
			return null;
		Object first= selection.getFirstElement();
		if (first instanceof ICompilationUnit)
			return (ICompilationUnit) first;
		if (first instanceof IType)
			return ((IType) first).getCompilationUnit();
		return null;
	}

	private IRunnableWithProgress createRunnable(final IStructuredSelection selection) {
		return pm -> {
			try {
				fElements= doRun(selection, pm);
			} catch (CoreException e) {
				throw new InvocationTargetException(e);
			}
		};
	}

	private NonNLSElement[] doRun(IStructuredSelection selection, IProgressMonitor pm) throws CoreException {
		List<?> elements= getSelectedElementList(selection);
		if (elements == null || elements.isEmpty())
			return new NonNLSElement[0];

		pm.beginTask(ActionMessages.FindStringsToExternalizeAction_find_strings, elements.size());

		try{
			List<NonNLSElement> result= new ArrayList<>();
			for (Object obj : elements) {
				result.addAll(analyze(obj, pm));
			}
			return result.toArray(new NonNLSElement[result.size()]);
		} finally{
			pm.done();
		}
	}

	private List<NonNLSElement> analyze(Object obj, IProgressMonitor pm) throws CoreException, JavaModelException {
		if (obj instanceof IJavaElement) {
			IJavaElement element= (IJavaElement) obj;
			int elementType= element.getElementType();

			if (elementType == IJavaElement.PACKAGE_FRAGMENT) {
				return analyze((IPackageFragment) element, new SubProgressMonitor(pm, 1));
			} else if (elementType == IJavaElement.PACKAGE_FRAGMENT_ROOT) {
				IPackageFragmentRoot root= (IPackageFragmentRoot)element;
				if (!root.isExternal() && !ReorgUtils.isClassFolder(root)) {
					return analyze((IPackageFragmentRoot) element, new SubProgressMonitor(pm, 1));
				} else {
					pm.worked(1);
				}
			} else if (elementType == IJavaElement.JAVA_PROJECT) {
				return analyze((IJavaProject) element, new SubProgressMonitor(pm, 1));
			} else if (elementType == IJavaElement.COMPILATION_UNIT) {
				ICompilationUnit cu= (ICompilationUnit)element;
				if (cu.exists()) {
					NonNLSElement nlsElement= analyze(cu);
					if (nlsElement != null) {
						pm.worked(1);
						ArrayList<NonNLSElement> result= new ArrayList<>();
						result.add(nlsElement);
						return result;
					}
				}
				pm.worked(1);
			} else if (elementType == IJavaElement.TYPE) {
				IType type= (IType)element;
				ICompilationUnit cu= type.getCompilationUnit();
				if (cu != null && cu.exists()) {
					NonNLSElement nlsElement= analyze(cu);
					if (nlsElement != null) {
						pm.worked(1);
						ArrayList<NonNLSElement> result= new ArrayList<>();
						result.add(nlsElement);
						return result;
					}
				}
				pm.worked(1);
			} else {
				pm.worked(1);
			}
		} else if (obj instanceof IWorkingSet) {
			List<NonNLSElement> result= new ArrayList<>();

			IWorkingSet workingSet= (IWorkingSet) obj;
			for (IAdaptable element : workingSet.getElements()) {
				result.addAll(analyze(element, new NullProgressMonitor()));
			}
			pm.worked(1);
			return result;
		} else {
			pm.worked(1);
		}

		return Collections.emptyList();
	}

	private void showResults() {
		if (noStrings())
			MessageDialog.openInformation(getShell(), ActionMessages.ExternalizeStringsAction_dialog_title, ActionMessages.FindStringsToExternalizeAction_noStrings);
		else
			new NonNLSListDialog(getShell(), fElements, countStrings()).open();
	}

	private boolean noStrings() {
		if (fElements != null) {
			for (NonNLSElement fElement : fElements) {
				if (fElement.count != 0) {
					return false;
				}
			}
		}
		return true;
	}

	/*
	 * returns List of Strings
	 */
	private List<NonNLSElement> analyze(IPackageFragment pack, IProgressMonitor pm) throws CoreException {
		try{
			if (pack == null)
				return new ArrayList<>(0);

			ICompilationUnit[] cus= pack.getCompilationUnits();

			pm.beginTask("", cus.length); //$NON-NLS-1$
			pm.setTaskName(pack.getElementName());

			List<NonNLSElement> l= new ArrayList<>(cus.length);
			for (ICompilationUnit cu : cus) {
				pm.subTask(BasicElementLabels.getFileName(cu));
				NonNLSElement element= analyze(cu);
				if (element != null)
					l.add(element);
				pm.worked(1);
				if (pm.isCanceled())
					throw new OperationCanceledException();
			}
			return l;
		} finally {
			pm.done();
		}
	}

	/*
	 * returns List of Strings
	 */
	private List<NonNLSElement> analyze(IPackageFragmentRoot sourceFolder, IProgressMonitor pm) throws CoreException {
		try{
			IJavaElement[] children= sourceFolder.getChildren();
			pm.beginTask("", children.length); //$NON-NLS-1$
			pm.setTaskName(JavaElementLabels.getElementLabel(sourceFolder, JavaElementLabels.ALL_DEFAULT));
			List<NonNLSElement> result= new ArrayList<>();
			for (IJavaElement iJavaElement : children) {
				if (iJavaElement.getElementType() == IJavaElement.PACKAGE_FRAGMENT){
					IPackageFragment pack= (IPackageFragment)iJavaElement;
					if (! pack.isReadOnly())
						result.addAll(analyze(pack, new SubProgressMonitor(pm, 1)));
					else
						pm.worked(1);
				} else
					pm.worked(1);
			}
			return result;
		} finally{
			pm.done();
		}
	}

	/*
	 * returns List of Strings
	 */
	private List<NonNLSElement> analyze(IJavaProject project, IProgressMonitor pm) throws CoreException {
		try{
			IPackageFragment[] packs= project.getPackageFragments();
			pm.beginTask("", packs.length); //$NON-NLS-1$
			List<NonNLSElement> result= new ArrayList<>();
			for (IPackageFragment pack : packs) {
				if (!pack.isReadOnly()) {
					result.addAll(analyze(pack, new SubProgressMonitor(pm, 1)));
				} else {
					pm.worked(1);
				}
			}
			return result;
		} finally{
			pm.done();
		}
	}

	private int countStrings() {
		int found= 0;
		if (fElements != null) {
			for (NonNLSElement fElement : fElements) {
				found+= fElement.count;
			}
		}
		return found;
	}

	private NonNLSElement analyze(ICompilationUnit cu) throws CoreException {
		int count= countNonExternalizedStrings(cu);
		if (count == 0)
			return null;
		else
			return new NonNLSElement(cu, count);
	}

	private int countNonExternalizedStrings(ICompilationUnit cu) throws CoreException {
		try{
			int result= 0;
			for (NLSLine line : NLSScanner.scan(cu)) {
				result+= countNonExternalizedStrings(line);
			}
			return result;
		} catch (InvalidInputException | BadLocationException e) {
			throw new CoreException(new Status(IStatus.ERROR, JavaPlugin.getPluginId(), IStatus.ERROR, Messages.format(ActionMessages.FindStringsToExternalizeAction_error_cannotBeParsed,
					BasicElementLabels.getFileName(cu)), e));
		}
	}

	private int countNonExternalizedStrings(NLSLine line){
		int result= 0;
		for (NLSElement element : line.getElements()) {
			if (!element.hasTag()) {
				result++;
			}
		}
		return result;
	}

	/**
	 * returns <code>List</code> of <code>IPackageFragments</code>,  <code>IPackageFragmentRoots</code> or
	 * <code>IJavaProjects</code> (all entries are of the same kind)
	 * @param selection the selection
	 * @return the selected elements
	 */
	private static List<?> getSelectedElementList(IStructuredSelection selection) {
		if (selection == null)
			return null;

		return selection.toList();
	}

	//-------private classes --------------

	private static class NonNLSListDialog extends ListDialog {

		private static final int OPEN_BUTTON_ID= IDialogConstants.CLIENT_ID + 1;

		private Button fOpenButton;

		NonNLSListDialog(Shell parent, NonNLSElement[] input, int count) {
			super(parent);
			setAddCancelButton(false);
			setInput(Arrays.asList(input));
			setTitle(ActionMessages.ExternalizeStringsAction_dialog_title);
			String message= count == 1 ? ActionMessages.FindStringsToExternalizeAction_non_externalized_singular : Messages.format(
					ActionMessages.FindStringsToExternalizeAction_non_externalized_plural, new Object[] { Integer.valueOf(count) });
			setMessage(message);
			setContentProvider(ArrayContentProvider.getInstance());
			setLabelProvider(createLabelProvider());
		}

		@Override
		protected Point getInitialSize() {
			return getShell().computeSize(SWT.DEFAULT, SWT.DEFAULT, true);
		}

		@Override
		protected Control createDialogArea(Composite parent) {
			Composite result= (Composite)super.createDialogArea(parent);
			getTableViewer().addSelectionChangedListener(event -> {
				if (fOpenButton != null){
					fOpenButton.setEnabled(! getTableViewer().getSelection().isEmpty());
				}
			});
			getTableViewer().getTable().addSelectionListener(new SelectionAdapter(){
				@Override
				public void widgetDefaultSelected(SelectionEvent e) {
					NonNLSElement element= (NonNLSElement)e.item.getData();
					ExternalizeWizard.open(element.cu, getShell());
				}
			});
			getTableViewer().getTable().setFocus();
			applyDialogFont(result);
			return result;
		}

		@Override
		protected void createButtonsForButtonBar(Composite parent) {
			fOpenButton= createButton(parent, OPEN_BUTTON_ID, ActionMessages.FindStringsToExternalizeAction_button_label, true);
			fOpenButton.setEnabled(false);

			//looks like a 'close' but it a 'cancel'
			createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CLOSE_LABEL, false);
		}

		@Override
		protected void buttonPressed(int buttonId) {
			if (buttonId != OPEN_BUTTON_ID){
				super.buttonPressed(buttonId);
				return;
			}
			ISelection s= getTableViewer().getSelection();
			if (s instanceof IStructuredSelection){
				IStructuredSelection ss= (IStructuredSelection)s;
				if (ss.getFirstElement() instanceof NonNLSElement)
					ExternalizeWizard.open(((NonNLSElement) ss.getFirstElement()).cu, getShell());
			}
		}

		private static LabelProvider createLabelProvider() {
			return new JavaElementLabelProvider(JavaElementLabelProvider.SHOW_DEFAULT){
				@Override
				public String getText(Object element) {
					NonNLSElement nlsel= (NonNLSElement)element;
					String elementName= BasicElementLabels.getPathLabel(nlsel.cu.getResource().getFullPath(), false);
					return Messages.format(
							ActionMessages.FindStringsToExternalizeAction_foundStrings,
							new Object[] {Integer.valueOf(nlsel.count), elementName} );
				}
				@Override
				public Image getImage(Object element) {
					return super.getImage(((NonNLSElement)element).cu);
				}
			};
		}

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


	}

	private static class NonNLSElement{
		ICompilationUnit cu;
		int count;
		NonNLSElement(ICompilationUnit cu, int count){
			this.cu= cu;
			this.count= count;
		}
	}

}
