/*******************************************************************************
 * Copyright (c) 2005, 2008 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.core.refactoring.descriptors;

import java.util.Map;

import org.eclipse.ltk.core.refactoring.RefactoringContribution;
import org.eclipse.ltk.core.refactoring.RefactoringCore;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;

import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaConventions;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.refactoring.IJavaRefactorings;

import org.eclipse.jdt.internal.core.refactoring.descriptors.JavaRefactoringDescriptorUtil;

/**
 * Refactoring descriptor for the introduce parameter object refactoring.
 * <p>
 * An instance of this refactoring descriptor may be obtained by calling
 * {@link RefactoringContribution#createDescriptor()} on a refactoring
 * contribution requested by invoking
 * {@link RefactoringCore#getRefactoringContribution(String)} with the
 * appropriate refactoring id.
 * </p>
 *
 * @since 1.2
 *
 * @noinstantiate This class is not intended to be instantiated by clients.
 * @noextend This class is not intended to be subclassed by clients.
 */
public class IntroduceParameterObjectDescriptor extends JavaRefactoringDescriptor {

	/**
	 * Instances of Parameter are used to describe the position of parameter and fields.
	 */
	public static class Parameter {

		private boolean fCreateField= false;

		private String fFieldName= null;

		private final int fIndex;

		/**
		 * Creates a new parameter object. It is not recommended to call this constructor directly.
		 * Use {@link IntroduceParameterObjectDescriptor#createParameters(IMethod)} instead.
		 * @param index the index of the parameter in the method
		 */
		public Parameter(int index) {
			super();
			fIndex= index;
		}

		/**
		 * The name of the field that will be created if {@link #isCreateField()} is <code>true</code>
		 *
		 * @return the field name
		 * @see #isCreateField()
		 * @see #setFieldName(String)
		 */
		public String getFieldName() {
			return fFieldName;
		}

		/**
		 * The index of the parameter in the original method signature. The parameter object has the special index {@link IntroduceParameterObjectDescriptor#PARAMETER_OBJECT_IDX}.
		 * The position in the new method signature depends on the position in the array passed to {@link IntroduceParameterObjectDescriptor#setParameters(IntroduceParameterObjectDescriptor.Parameter[])}
		 *
		 * @return returns the index of the parameter in the original method signature or {@link IntroduceParameterObjectDescriptor#PARAMETER_OBJECT_IDX}
		 * for the parameter object
		 *
		 * @see IntroduceParameterObjectDescriptor#PARAMETER_OBJECT
		 * @see IntroduceParameterObjectDescriptor#PARAMETER_OBJECT_IDX
		 * @see IntroduceParameterObjectDescriptor#setParameters(IntroduceParameterObjectDescriptor.Parameter[])
		 */
		public int getIndex() {
			return fIndex;
		}

		/**
		 * If <code>true</code> the parameter will be removed from the method's signature and will be added
		 * to the parameter object. The default is <code>false</code>
		 *
		 * @return <code>true</code> if the parameter will be created as field, <code>false</code> if
		 * 			it will remain in the method
		 */
		public boolean isCreateField() {
			return fCreateField;
		}

		/**
		 * Sets whether the parameter will be removed from the method's signature or will be added to the parameter object. The
		 * default is <code>false</code>. Changing the creatField property of the parameter object will throw a {@link IllegalArgumentException}
		 *
		 * @param createField <code>true</code> if the parameter should be created as field, <code>false</code>
		 * 						if it will remain in the method
		 */
		public void setCreateField(boolean createField) {
			if (fIndex == PARAMETER_OBJECT_IDX)
				throw new IllegalArgumentException("You can not set create field for the parameter object"); //$NON-NLS-1$
			fCreateField= createField;
		}


		/**
		 * Sets the name of the field that will be created in the parameter object if {@link #isCreateField()}
		 *  is <code>true</code>. Changing the fieldName of the parameter object will throw a {@link IllegalArgumentException}
		 *
		 * @param fieldName the new name of the field. A <code>null</code> indicates that the field name should be automatically derived
		 * @see #isCreateField()
		 */
		public void setFieldName(String fieldName) {
			if (fIndex == PARAMETER_OBJECT_IDX)
				throw new IllegalArgumentException("You can not set the field name of the parameter object"); //$NON-NLS-1$
			fFieldName= fieldName;
		}
	}

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

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

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

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

	private static final String CLASS_NAME= "class_name"; //$NON-NLS-1$

	private static final String DELEGATE= "delegate"; //$NON-NLS-1$

	private static final String DEPRECATE_DELEGATE= "deprecate_delegate"; //$NON-NLS-1$

	private static final String GETTERS= "getters"; //$NON-NLS-1$

	private static final String PACKAGE_NAME= "package_name"; //$NON-NLS-1$

	private static final String PARAMETER_NAME= "parameter_name"; //$NON-NLS-1$

	private static final String SETTERS= "setters"; //$NON-NLS-1$

	private static final String TOP_LEVEL= "top_level"; //$NON-NLS-1$

	/**
	 * The parameter index of the special parameter object. The value is "-1".
	 */
	public static final int PARAMETER_OBJECT_IDX= -1;

	/**
	 * Singleton instance that represents the parameter object
	 */
	public static final Parameter PARAMETER_OBJECT= new Parameter(PARAMETER_OBJECT_IDX);


	/**
	 * Creates the parameters for this method. The first object is the parameter object.
	 * By default all parameters are marked for field creation
	 *
	 * @param method derive parameter from this method
	 * @return an array of parameter corresponding to the parameter declared in the method. The first object
	 * 			will be the parameter object. All parameter are marked for field creation
	 */
	public static Parameter[] createParameters(IMethod method) {
		int length= method.getNumberOfParameters();
		Parameter[] result= new Parameter[length + 1];
		result[0]= PARAMETER_OBJECT;
		for (int i= 0; i < length; i++) {
			result[i + 1]= new Parameter(i);
			result[i + 1].setCreateField(true);
		}
		return result;
	}

	private String fClassName;

	private boolean fDelegate= false;

	private boolean fDeprecateDelegate= true;

	private boolean fGetters= false;

	private IMethod fMethod;

	private String fPackageName;

	private String fParameterName;

	private Parameter[] fParameters;

	private boolean fSetters= false;

	/**
	 * If <code>true</code> the class will be created as top level class. Default true
	 */
	private boolean fTopLevel= true;

	/**
	 * Creates a new refactoring descriptor.
	 */
	public IntroduceParameterObjectDescriptor() {
		super(IJavaRefactorings.INTRODUCE_PARAMETER_OBJECT);
	}

	/**
	 * Creates a new refactoring descriptor.
	 *
	 * @param project
	 *            the non-empty name of the project associated with this
	 *            refactoring, or <code>null</code> for a workspace
	 *            refactoring
	 * @param description
	 *            a non-empty human-readable description of the particular
	 *            refactoring instance
	 * @param comment
	 *            the human-readable comment of the particular refactoring
	 *            instance, or <code>null</code> for no comment
	 * @param arguments
	 * 			  a map of arguments that will be persisted and describes
	 * 			  all settings for this refactoring
	 * @param flags
	 *            the flags of the refactoring descriptor
	 * @throws IllegalArgumentException if the argument map contains invalid keys/values
	 */
	public IntroduceParameterObjectDescriptor(final String project, final String description, final String comment, final Map<String, String> arguments, final int flags) throws IllegalArgumentException {
		super(IJavaRefactorings.INTRODUCE_PARAMETER_OBJECT, project, description, comment, arguments, flags);
		initializeFromMap(arguments);
	}

	/**
	 * The name of the class that will be generated. If <code>null</code> the refactoring will automatically choose a class name.
	 *
	 * @return the name of the class that will be generated or <code>null</code> if the name will be automatically chosen
	 */
	public String getClassName() {
		return fClassName;
	}

	/**
	 * The method the refactoring will operate on. Can be set using {@link #setMethod(IMethod)}.
	 *
	 * @return the method that the refactoring will operate on.
	 */
	public IMethod getMethod() {
		return fMethod;
	}

	/**
	 * The parameter object class will be created in this package if the top level is <code>true</code>. Can be set using
	 * {@link #setPackageName(String)}. If the package name was <code>null</code> and the method has already been set this method returns
	 * the package where the method is declared in.
	 *
	 * @return the package name that has been set or the package where the method is declared. Can return <code>null</code>
	 * 			if neither the package nor the method has been set
	 */
	public String getPackageName() {
		if (fPackageName == null && fMethod != null) {
			IType type= fMethod.getDeclaringType();
			if (type != null)
				return type.getPackageFragment().getElementName();
		}
		return fPackageName;
	}

	/**
	 * Returns the name of the parameter. Can return <code>null</code> in which case the refactoring chooses a name. Default is <code>null</code>
	 *
	 * @return the name of the parameter. Can return <code>null</code> in which case the refactoring chooses a name. Default is <code>null</code>
	 */
	public String getParameterName() {
		return fParameterName;
	}

	/**
	 * Returns the parameters. Can return <code>null</code> if all parameters should be converted to fields. Default is <code>null</code>.
	 *
	 * @return the parameters. Can return <code>null</code> if all parameters should be converted to fields. Default is <code>null</code>
	 */
	public Parameter[] getParameters() {
		return fParameters;
	}

	/**
	 * Returns <code>true</code> if delegates will be kept. Default is <code>false</code>.
	 *
	 * @return <code>true</code> if delegates will be kept. Default is <code>false</code>
	 */
	public boolean isDelegate() {
		return fDelegate;
	}

	/**
	 * Returns <code>true</code> if delegates will be marked as deprecated. Default is <code>false</code>.
	 *
	 * @return <code>true</code> if delegates will be marked as deprecated. Default is <code>false</code>
	 */
	public boolean isDeprecateDelegate() {
		return fDeprecateDelegate;
	}

	/**
	 * Returns <code>true</code> if getters are generated for fields. Default is <code>false</code>.
	 *
	 * @return <code>true</code> if getters are generated for fields. Default is <code>false</code>
	 */
	public boolean isGetters() {
		return fGetters;
	}

	/**
	 * Returns <code>true</code> if setters are generated for fields. Default is <code>false</code>.
	 *
	 * @return <code>true</code> if setters are generated for fields. Default is <code>false</code>
	 */
	public boolean isSetters() {
		return fSetters;
	}

	/**
	 * Returns <code>true</code> if the new type is created as top level type.
	 * <code>false</code> is returned when the type is created as enclosing type
	 * of the type declaring the method declaration to be changed. Default is <code>true</code>.
	 *
	 * @return <code>true</code> if the new type is created as top level type.
	 * 		   <code>false</code> is returned when the type is created as enclosing
	 * 		   type of the type declaring the method declaration to be changed. Default is <code>true</code>
	 */
	public boolean isTopLevel() {
		return fTopLevel;
	}

	@Override
	protected void populateArgumentMap() {
		super.populateArgumentMap();
		JavaRefactoringDescriptorUtil.setJavaElement(fArguments, ATTRIBUTE_INPUT, getProject(), fMethod);
		Parameter[] parameters= fParameters;
		if (parameters == null) {
			parameters= createParameters(fMethod);
		}
		JavaRefactoringDescriptorUtil.setInt(fArguments, PARAMETER_COUNT, parameters.length);
		for (int i= 0; i < parameters.length; i++) {
			Parameter param= parameters[i];
			JavaRefactoringDescriptorUtil.setInt(fArguments, JavaRefactoringDescriptorUtil.getAttributeName(PARAMETER_IDX, i), param.getIndex());
			JavaRefactoringDescriptorUtil.setBoolean(fArguments, JavaRefactoringDescriptorUtil.getAttributeName(PARAMETER_CREATE_FIELD, i), param.isCreateField());
			if (param.isCreateField())
				JavaRefactoringDescriptorUtil.setString(fArguments, JavaRefactoringDescriptorUtil.getAttributeName(PARAMETER_FIELD_NAME, i), param.getFieldName());
		}
		JavaRefactoringDescriptorUtil.setString(fArguments, CLASS_NAME, fClassName);
		JavaRefactoringDescriptorUtil.setString(fArguments, PACKAGE_NAME, fPackageName);
		JavaRefactoringDescriptorUtil.setString(fArguments, PARAMETER_NAME, fParameterName);
		JavaRefactoringDescriptorUtil.setBoolean(fArguments, DELEGATE, fDelegate);
		JavaRefactoringDescriptorUtil.setBoolean(fArguments, DEPRECATE_DELEGATE, fDeprecateDelegate);
		JavaRefactoringDescriptorUtil.setBoolean(fArguments, GETTERS, fGetters);
		JavaRefactoringDescriptorUtil.setBoolean(fArguments, SETTERS, fSetters);
		JavaRefactoringDescriptorUtil.setBoolean(fArguments, TOP_LEVEL, fTopLevel);
	}

	private void initializeFromMap(Map<String, String> map) throws IllegalArgumentException {
		IMethod method= (IMethod) JavaRefactoringDescriptorUtil.getJavaElement(map, ATTRIBUTE_INPUT, getProject());
		setMethod(method);
		initializeParameter(map);
		setClassName(JavaRefactoringDescriptorUtil.getString(map, CLASS_NAME, true));
		setPackageName(JavaRefactoringDescriptorUtil.getString(map, PACKAGE_NAME, true));
		setParameterName(JavaRefactoringDescriptorUtil.getString(map, PARAMETER_NAME, true));
		setDelegate(JavaRefactoringDescriptorUtil.getBoolean(map, DELEGATE, fDelegate));
		setDeprecateDelegate(JavaRefactoringDescriptorUtil.getBoolean(map, DEPRECATE_DELEGATE, fDeprecateDelegate));
		setGetters(JavaRefactoringDescriptorUtil.getBoolean(map, GETTERS, fGetters));
		setSetters(JavaRefactoringDescriptorUtil.getBoolean(map, SETTERS, fSetters));
		setTopLevel(JavaRefactoringDescriptorUtil.getBoolean(map, TOP_LEVEL, fTopLevel));
	}

	private void initializeParameter(Map<String, String> map) throws IllegalArgumentException {
		int[] idx= JavaRefactoringDescriptorUtil.getIntArray(map, PARAMETER_COUNT, PARAMETER_IDX);
		boolean[] createField= JavaRefactoringDescriptorUtil.getBooleanArray(map, PARAMETER_COUNT, PARAMETER_CREATE_FIELD, 0);
		Parameter[] result=new Parameter[idx.length];
		for (int i= 0; i < idx.length; i++) {
			int index= idx[i];
			if (index == PARAMETER_OBJECT_IDX) {
				result[i]= new Parameter(PARAMETER_OBJECT_IDX);
			} else {
				Parameter parameter= new Parameter(index);
				result[i]= parameter;
				parameter.setCreateField(createField[i]);
				if (createField[i])
					parameter.setFieldName(JavaRefactoringDescriptorUtil.getString(map, JavaRefactoringDescriptorUtil.getAttributeName(PARAMETER_FIELD_NAME, i)));
			}
		}
		setParameters(result);
	}

	/**
	 * Sets the name of the class for the generated parameter object. The name can be <code>null</code> to
	 * indicate that the refactoring should chose one.
	 *
	 * @param className the name of the generated class or <code>null</code>. Default is <code>null</code>
	 */
	public void setClassName(String className) {
		fClassName= className;
	}

	/**
	 * Sets delegate keeping. If <code>true</code> delegates will be kept.
	 *
	 * @param delegate <code>true</code> to keep delegates. Default is <code>false</code>
	 */
	public void setDelegate(boolean delegate) {
		fDelegate= delegate;
	}

	/**
	 * Sets deprecate delegate. If <code>true</code> generated delegates will be marked as deprecated.
	 *
	 * @param deprecateDelegate <code>true</code> to deprecate kept delegates. Default is <code>false</code>
	 */
	public void setDeprecateDelegate(boolean deprecateDelegate) {
		fDeprecateDelegate= deprecateDelegate;
	}

	/**
	 * Sets whether getters will be created for all fields.
	 *
	 * @param getters <code>true</code> to create getters. Default is <code>false</code>.
	 */
	public void setGetters(boolean getters) {
		fGetters= getters;
	}

	/**
	 * Sets the method. The method may not be <code>null</code>, has to exist, and has to be
	 * in a Java project.
	 *
	 * @param method the method. May not be <code>null</code>
	 */
	public void setMethod(IMethod method) {
		if (method == null)
			throw new IllegalArgumentException("The method must not be null"); //$NON-NLS-1$
		if (!method.exists())
			throw new IllegalArgumentException("The method must exist"); //$NON-NLS-1$
		if (method.getJavaProject() == null)
			throw new IllegalArgumentException("The method has to be in a Java project"); //$NON-NLS-1$
		fMethod= method;
	}

	/**
	 * Sets the package where the parameter object will be created in if it is created as top level class.
	 * The package can be <code>null</code> to indicate that the package of the method should be used.
	 *
	 * @param packageName the package for the top level class or <code>null</code>. Default is <code>null</code>.
	 */
	public void setPackageName(String packageName) {
		fPackageName= packageName;
	}

	/**
	 * Sets the name of the parameter object as it will appear in the method signature.
	 * The name can be <code>null</code> to indicate that the refactoring will choose a name.
	 *
	 * @param parameterName the name of the parameter or <code>null</code>. Default is <code>null</code>.
	 */
	public void setParameterName(String parameterName) {
		fParameterName= parameterName;
	}

	/**
	 * Sets the parameters. The parameters can be <code>null</code> to indicate that all parameter
	 * should be used as fields. If not <code>null</code>, the number of parameters passed has to be
	 * the number of parameter of the method + 1. One element has to be the {@link #PARAMETER_OBJECT}.
	 * Each parameter may only appear once.
	 *
	 * @param parameters the parameters or <code>null</code>. Default is <code>null</code>
	 */
	public void setParameters(Parameter[] parameters) {
		fParameters= parameters;
	}

	/**
	 * Sets whether setters will be created for all fields.
	 *
	 * @param setters <code>true</code> to create setters. Default is <code>false</code>
	 */
	public void setSetters(boolean setters) {
		fSetters= setters;
	}

	/**
	 * Sets whether the parameter object class will be created as top level class.
	 * if <code>true</code> the class will be created as top level class in the package
	 * returned by {@link #getPackageName()}. If <code>false</code> the class will be
	 *  created as as nested class in the class containing the method
	 *
	 * @param topLevel <code>true</code> to create the parameter object as top level. Default is <code>true</code>
	 */
	public void setTopLevel(boolean topLevel) {
		fTopLevel= topLevel;
	}

	@Override
	public RefactoringStatus validateDescriptor() {
		RefactoringStatus result= super.validateDescriptor();
		if (!result.isOK())
			return result;
		if (fMethod == null) {
			result.addFatalError("The method must not be null"); //$NON-NLS-1$
			return result;
		}
		IJavaProject javaProject= fMethod.getJavaProject();
		if (javaProject == null) {
			result.addFatalError("Can not derive Java project from method"); //$NON-NLS-1$
			return result;
		}
		String sourceLevel= javaProject.getOption(JavaCore.COMPILER_SOURCE, true);
		String complianceLevel= javaProject.getOption(JavaCore.COMPILER_COMPLIANCE, true);
		if (fParameters != null) {
			if (fParameters.length - 1 != fMethod.getNumberOfParameters()) {
				result.addFatalError("The number of parameters does not match the number of parameters of the method"); //$NON-NLS-1$
			}
			boolean hasParameterObject= false;
			for (int i= 0; i < fParameters.length; i++) {
				Parameter parameter= fParameters[i];
				if (parameter.isCreateField()) {
					String fieldName= parameter.getFieldName();
					if (fieldName == null)
						result.addError("The parameter " + parameter.getIndex() + " is marked for field creation but does not have a field name"); //$NON-NLS-1$ //$NON-NLS-2$
					else {
						result.merge(RefactoringStatus.create(JavaConventions.validateFieldName(fieldName, sourceLevel, complianceLevel)));
					}
				}
				if (parameter == PARAMETER_OBJECT) {
					if (hasParameterObject)
						result.addError("Can not have more than one parameter object"); //$NON-NLS-1$
					else
						hasParameterObject= true;
				}
			}
		}
		if (fClassName != null) {
			result.merge(RefactoringStatus.create(JavaConventions.validateIdentifier(fClassName, sourceLevel, complianceLevel)));
		}
		if (fParameterName != null) {
			result.merge(RefactoringStatus.create(JavaConventions.validateIdentifier(fParameterName, sourceLevel, complianceLevel)));
		}
		if (fPackageName != null && !"".equals(fPackageName)) { //$NON-NLS-1$
			result.merge(RefactoringStatus.create(JavaConventions.validatePackageName(fPackageName, sourceLevel, complianceLevel)));
		}
		return result;
	}
}
