/*******************************************************************************
 * Copyright (c) 2000, 2020 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
 *     Eric Rizzo - Externalize Strings wizard always defaults to the "legacy" mechanism - http://bugs.eclipse.org/271375
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.refactoring.nls;

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

import org.eclipse.osgi.util.NLS;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubProgressMonitor;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;

import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.Refactoring;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.RefactoringStatusContext;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.SourceRange;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.manipulation.SharedASTProviderCore;

import org.eclipse.jdt.internal.core.manipulation.util.BasicElementLabels;
import org.eclipse.jdt.internal.corext.refactoring.Checks;
import org.eclipse.jdt.internal.corext.refactoring.base.JavaStringStatusContext;
import org.eclipse.jdt.internal.corext.refactoring.changes.DynamicValidationStateChange;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
import org.eclipse.jdt.internal.corext.util.Messages;


public class NLSRefactoring extends Refactoring {

	public static final String BUNDLE_NAME_FIELD= "BUNDLE_NAME"; //$NON-NLS-1$
	public static final String PROPERTY_FILE_EXT= ".properties"; //$NON-NLS-1$
	public static final String DEFAULT_ACCESSOR_CLASSNAME= "Messages"; //$NON-NLS-1$

	public static final String KEY= "${key}"; //$NON-NLS-1$
	public static final String DEFAULT_SUBST_PATTERN= "getString(" + KEY + ")"; //$NON-NLS-1$ //$NON-NLS-2$

	public static final String DEFAULT_PROPERTY_FILENAME= "messages"; //$NON-NLS-1$

	//private IPath fPropertyFilePath;

	private String fAccessorClassName;
	private IPackageFragment fAccessorClassPackage;
	private String fResourceBundleName;
	private IPackageFragment fResourceBundlePackage;

	private String fSubstitutionPattern;
	private ICompilationUnit fCu;
	private NLSSubstitution[] fSubstitutions;

	private String fPrefix;

	/**
	 * <code>true</code> if the standard resource bundle mechanism
	 * is used and <code>false</code> NLSing is done the Eclipse way.
	 */
	private boolean fIsEclipseNLS;

	private NLSRefactoring(ICompilationUnit cu) {
		Assert.isNotNull(cu);
		fCu= cu;

		CompilationUnit astRoot= SharedASTProviderCore.getAST(fCu, SharedASTProviderCore.WAIT_YES, null);
		NLSHint nlsHint= new NLSHint(fCu, astRoot);

		fSubstitutions= nlsHint.getSubstitutions();
		setAccessorClassName(nlsHint.getAccessorClassName());
		setAccessorClassPackage(nlsHint.getAccessorClassPackage());

		setIsEclipseNLS(detectIsEclipseNLS());
		setResourceBundleName(nlsHint.getResourceBundleName());
		setResourceBundlePackage(nlsHint.getResourceBundlePackage());
		setSubstitutionPattern(DEFAULT_SUBST_PATTERN);

		String cuName= fCu.getElementName();
		if (fIsEclipseNLS)
			setPrefix(cuName.substring(0, cuName.length() - 5) + "_"); // A.java -> A_ //$NON-NLS-1$
		else
			setPrefix(cuName.substring(0, cuName.length() - 4)); // A.java -> A.
	}

	public static NLSRefactoring create(ICompilationUnit cu) {
		if (cu == null || !cu.exists())
			return null;
		return new NLSRefactoring(cu);
	}

	public boolean isEclipseNLSAvailable() {
		if (getCu() == null)
			return false;

		IJavaProject javaProject= getCu().getJavaProject();
		if (javaProject == null || !javaProject.exists())
			return false;

		try {
			return javaProject.findType("org.eclipse.osgi.util.NLS") != null; //$NON-NLS-1$
		} catch (JavaModelException e) {
			return false;
		}
	}


	/**
	 * no validation is done
	 *
	 * @param pattern
	 *            Example: "Messages.getString(${key})". Must not be
	 *            <code>null</code>. should (but does not have to) contain
	 *            NLSRefactoring.KEY (default value is $key$) only the first
	 *            occurrence of this key will be used
	 */
	public void setSubstitutionPattern(String pattern) {
		Assert.isNotNull(pattern);
		fSubstitutionPattern= pattern;
	}

	/**
	 * to show the pattern in the UI
	 * @return the substitution pattern
	 */
	public String getSubstitutionPattern() {
		if (fIsEclipseNLS)
			return KEY;
		else
			return fSubstitutionPattern;
	}

	public ICompilationUnit getCu() {
		return fCu;
	}

	@Override
	public String getName() {
		return Messages.format(NLSMessages.NLSRefactoring_compilation_unit, BasicElementLabels.getFileName(fCu));
	}

	@Override
	public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException {

		if (fSubstitutions.length == 0) {
			String message= Messages.format(NLSMessages.NLSRefactoring_no_strings, BasicElementLabels.getFileName(fCu));
			return RefactoringStatus.createFatalErrorStatus(message);
		}
		return new RefactoringStatus();
	}

	@Override
	public RefactoringStatus checkFinalConditions(IProgressMonitor pm) throws CoreException {
		checkParameters();
		try {

			pm.beginTask(NLSMessages.NLSRefactoring_checking, 5);

			RefactoringStatus result= new RefactoringStatus();

			result.merge(checkIfAnythingToDo());
			if (result.hasFatalError()) {
				return result;
			}
			pm.worked(1);

			result.merge(validateModifiesFiles(pm));
			if (result.hasFatalError()) {
				return result;
			}
			pm.worked(1);
			if (pm.isCanceled())
				throw new OperationCanceledException();

			result.merge(checkSubstitutionPattern());
			pm.worked(1);

			if (pm.isCanceled())
				throw new OperationCanceledException();


			result.merge(checkKeys());
			pm.worked(1);
			if (pm.isCanceled())
				throw new OperationCanceledException();

			if (!propertyFileExists() && willModifyPropertyFile()) {
				String msg= Messages.format(NLSMessages.NLSRefactoring_will_be_created, BasicElementLabels.getPathLabel(getPropertyFilePath(), false));
				result.addInfo(msg);
			}
			pm.worked(1);

			return result;
		} finally {
			pm.done();
		}
	}

	@Override
	public Change createChange(IProgressMonitor pm) throws CoreException {
		try {
			checkParameters();

			pm.beginTask("", 3); //$NON-NLS-1$

			final DynamicValidationStateChange result= new DynamicValidationStateChange(NLSMessages.NLSRefactoring_change_name);

			boolean createAccessorClass= willCreateAccessorClass();
			if (NLSSubstitution.countItems(fSubstitutions, NLSSubstitution.EXTERNALIZED) == 0) {
				createAccessorClass= false;
			}
			if (createAccessorClass) {
				result.add(AccessorClassCreator.create(fCu, fAccessorClassName, getAccessorCUPath(), fAccessorClassPackage, getPropertyFilePath(), fIsEclipseNLS, fSubstitutions, getSubstitutionPattern(), new SubProgressMonitor(pm, 1)));
			}
			pm.worked(1);

			if (willModifySource()) {
				result.add(NLSSourceModifier.create(getCu(), fSubstitutions, getSubstitutionPattern(), fAccessorClassPackage, fAccessorClassName, fIsEclipseNLS));
			}
			pm.worked(1);

			if (willModifyPropertyFile()) {
				result.add(NLSPropertyFileModifier.create(fSubstitutions, getPropertyFilePath()));
				if (isEclipseNLS() && !createAccessorClass) {
					Change change= AccessorClassModifier.create(getAccessorCu(), fSubstitutions);
					if (change != null)
						result.add(change);
				}
			}
			pm.worked(1);

			return result;
		} finally {
			pm.done();
		}
	}

	private void checkParameters() {
		Assert.isNotNull(fSubstitutions);
		Assert.isNotNull(fAccessorClassPackage);

		// these values have defaults ...
		Assert.isNotNull(fAccessorClassName);
		Assert.isNotNull(getSubstitutionPattern());
	}

	private IFile[] getAllFilesToModify() {

		List<IResource> files= new ArrayList<>(2);
		if (willModifySource()) {
			IResource resource= fCu.getResource();
			if (resource.exists()) {
				files.add(resource);
			}
		}

		if (willModifyPropertyFile()) {
			IFile file= getPropertyFileHandle();
			if (file.exists()) {
				files.add(file);
			}
		}

		if (willModifyAccessorClass()) {
			IFile file= getAccessorClassFileHandle();
			if (file.exists()) {
				files.add(file);
			}
		}

		return files.toArray(new IFile[files.size()]);
	}

	public IFile getPropertyFileHandle() {
		return ResourcesPlugin.getWorkspace().getRoot().getFile(getPropertyFilePath());
	}

	public IPath getPropertyFilePath() {
		return fResourceBundlePackage.getPath().append(fResourceBundleName);
	}

	public IFile getAccessorClassFileHandle() {
		return ResourcesPlugin.getWorkspace().getRoot().getFile(getAccessorClassFilePath());
	}

	public IPath getAccessorClassFilePath() {
		return getAccessorCUPath();
	}

	private RefactoringStatus validateModifiesFiles(IProgressMonitor pm) throws CoreException {
		return Checks.validateModifiesFiles(getAllFilesToModify(), getValidationContext(), pm);
	}

	//should stop checking if fatal error
	private RefactoringStatus checkIfAnythingToDo() throws JavaModelException {
		if (NLSSubstitution.countItems(fSubstitutions, NLSSubstitution.EXTERNALIZED) != 0 && willCreateAccessorClass())
			return null;

		if (willModifyPropertyFile())
			return null;

		if (willModifySource())
			return null;

		RefactoringStatus result= new RefactoringStatus();
		result.addFatalError(NLSMessages.NLSRefactoring_nothing_to_do);
		return result;
	}

	private boolean propertyFileExists() {
		return getPropertyFileHandle().exists();
	}

	private RefactoringStatus checkSubstitutionPattern() {
		String pattern= getSubstitutionPattern();

		RefactoringStatus result= new RefactoringStatus();
		if (pattern.trim().length() == 0) {//
			result.addError(NLSMessages.NLSRefactoring_pattern_empty);
		}

		if (!pattern.contains(KEY)) {
			String msg= Messages.format(NLSMessages.NLSRefactoring_pattern_does_not_contain, KEY);
			result.addWarning(msg);
		}

		if (pattern.indexOf(KEY) != pattern.lastIndexOf(KEY)) {
			String msg= Messages.format(NLSMessages.NLSRefactoring_Only_the_first_occurrence_of, KEY);
			result.addWarning(msg);
		}

		return result;
	}

	private RefactoringStatus checkKeys() {
		RefactoringStatus result= new RefactoringStatus();
		for (NLSSubstitution substitution : fSubstitutions) {
			if ((substitution.getState() == NLSSubstitution.EXTERNALIZED) && substitution.hasStateChanged()) {
				result.merge(checkKey(substitution.getKey()));
			}
		}
		return result;
	}

	private static RefactoringStatus checkKey(String key) {
		RefactoringStatus result= new RefactoringStatus();

		if (key == null)
			result.addFatalError(NLSMessages.NLSRefactoring_null);

		if (key.startsWith("!") || key.startsWith("#")) { //$NON-NLS-1$ //$NON-NLS-2$
			RefactoringStatusContext context= new JavaStringStatusContext(key, new SourceRange(0, 0));
			result.addWarning(NLSMessages.NLSRefactoring_warning, context);
		}

		if ("".equals(key.trim())) //$NON-NLS-1$
			result.addFatalError(NLSMessages.NLSRefactoring_empty);

		final String[] UNWANTED_STRINGS= {" ", ":", "\"", "\\", "'", "?", "="}; //$NON-NLS-7$ //$NON-NLS-6$ //$NON-NLS-5$ //$NON-NLS-4$ //$NON-NLS-3$ //$NON-NLS-2$ //$NON-NLS-1$
		//feature in resource bundle - does not work properly if keys have ":"
		for (String s : UNWANTED_STRINGS) {
			if (key.contains(s)) {
				String[] args= {key, s};
				String msg= Messages.format(NLSMessages.NLSRefactoring_should_not_contain, args);
				result.addError(msg);
			}
		}
		return result;
	}

	public boolean willCreateAccessorClass() throws JavaModelException {

		ICompilationUnit compilationUnit= getAccessorCu();
		if (compilationUnit.exists()) {
			return false;
		}

		if (typeNameExistsInPackage(fAccessorClassPackage, fAccessorClassName)) {
			return false;
		}

		return (!Checks.resourceExists(getAccessorCUPath()));
	}

	private ICompilationUnit getAccessorCu() {
		return fAccessorClassPackage.getCompilationUnit(getAccessorCUName());
	}

	private boolean willModifySource() {
		NLSSubstitution[] subs= fSubstitutions;
		for (NLSSubstitution sub : subs) {
			if (sub.hasSourceChange()) {
				return true;
			}
		}
		return false;
	}

	private boolean willModifyPropertyFile() {
		for (NLSSubstitution substitution : fSubstitutions) {
			if (substitution.hasPropertyFileChange()) {
				return true;
			}
		}
		return false;
	}

	private boolean willModifyAccessorClass() {
		if (!isEclipseNLS())
			return false;

		for (NLSSubstitution substitution : fSubstitutions) {
			if (substitution.hasAccessorClassChange()) {
				return true;
			}
		}
		return false;
	}

	private boolean typeNameExistsInPackage(IPackageFragment pack, String name) throws JavaModelException {
		return Checks.findTypeInPackage(pack, name) != null;
	}

	private String getAccessorCUName() {
		return getAccessorClassName() + JavaModelUtil.DEFAULT_CU_SUFFIX;
	}

	private IPath getAccessorCUPath() {
		return fAccessorClassPackage.getPath().append(getAccessorCUName());
	}

	public NLSSubstitution[] getSubstitutions() {
		return fSubstitutions;
	}

	public String getPrefix() {
		return fPrefix;
	}

	public void setPrefix(String prefix) {
		fPrefix= prefix;
		if (fSubstitutions != null) {
			for (NLSSubstitution fSubstitution : fSubstitutions) {
				fSubstitution.setPrefix(prefix);
			}
		}
	}

	public void setAccessorClassName(String name) {
		Assert.isNotNull(name);
		fAccessorClassName= name;
	}

	public void setAccessorClassPackage(IPackageFragment packageFragment) {
		Assert.isNotNull(packageFragment);
		fAccessorClassPackage= packageFragment;
	}

	/**
	 * Sets whether the Eclipse NLSing mechanism or
	 * standard resource bundle mechanism is used.
	 *
	 * @param isEclipseNLS	<code>true</code> if NLSing is done the Eclipse way
	 * 						and <code>false</code> if the standard resource bundle mechanism is used
	 * @since 3.1
	 */
	public void setIsEclipseNLS(boolean isEclipseNLS) {
		fIsEclipseNLS= isEclipseNLS;
	}

	public void setResourceBundlePackage(IPackageFragment resourceBundlePackage) {
		Assert.isNotNull(resourceBundlePackage);
		fResourceBundlePackage= resourceBundlePackage;
	}

	public void setResourceBundleName(String resourceBundleName) {
		Assert.isNotNull(resourceBundleName);
		fResourceBundleName= resourceBundleName;
	}

	public IPackageFragment getAccessorClassPackage() {
		return fAccessorClassPackage;
	}

	/**
	 * Computes whether the Eclipse NLSing mechanism is used.
	 *
	 * @return		<code>true</code> if NLSing is done the Eclipse way
	 * 				and <code>false</code> if the standard resource bundle mechanism is used
	 * @since 3.1
	 */
	public boolean detectIsEclipseNLS() {
		ICompilationUnit accessorCU= getAccessorClassPackage().getCompilationUnit(getAccessorCUName());
		IType type= accessorCU.getType(getAccessorClassName());
		if (type.exists()) {
			try {
				String superclassName= type.getSuperclassName();
				if (!"NLS".equals(superclassName) && !NLS.class.getName().equals(superclassName)) //$NON-NLS-1$
					return false;
				IType superclass= type.newSupertypeHierarchy(null).getSuperclass(type);
				return superclass != null && NLS.class.getName().equals(superclass.getFullyQualifiedName());
			} catch (JavaModelException e) {
				// use default
			}
		}
		// Bug 271375: Make the default be to use Eclipse's NLS mechanism if it's available.
		return isEclipseNLSAvailable();
	}

	/**
	 * Returns whether the Eclipse NLSing mechanism or
	 * the standard resource bundle mechanism is used.
	 *
	 * @return		<code>true</code> if NLSing is done the Eclipse way
	 * 				and <code>false</code> if the standard resource bundle mechanism is used
	 * @since 3.1
	 */
	public boolean isEclipseNLS() {
		return fIsEclipseNLS;
	}

	public IPackageFragment getResourceBundlePackage() {
		return fResourceBundlePackage;
	}

	public String getAccessorClassName() {
		return fAccessorClassName;
	}

	public String getResourceBundleName() {
		return fResourceBundleName;
	}
}
