/*******************************************************************************
 * Copyright (c) 2000, 2008 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.wst.jsdt.internal.corext.refactoring.rename;

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

import org.eclipse.core.runtime.Assert;
import org.eclipse.wst.jsdt.core.IJavaScriptProject;
import org.eclipse.wst.jsdt.core.JavaScriptCore;
import org.eclipse.wst.jsdt.internal.ui.text.JavaWordIterator;

import com.ibm.icu.text.BreakIterator;

/**
 * This class contains methods for suggesting new names for variables or methods
 * whose name consists at least partly of the name of their declaring type (or
 * in case of methods, the return type or a parameter type).
 * 
 * The methods return the newly suggested method or variable name in case of a
 * match, or null in case nothing matched.
 * 
 * In any case, prefixes and suffixes are removed from variable names. As method
 * names have no configurable suffixes or prefixes, they are left unchanged. The
 * remaining name is called "stripped element name".
 * 
 * After the match according to the strategy, prefixes and suffixes are
 * reapplied to the names.
 * 
 * EXACT STRATEGY (always performed).
 * ----------------------------------------------------------------
 * 
 * The stripped element name is directly compared with the type name:
 * 
 * a) the first character must match case-insensitive
 * 
 * b) all other characters must match case-sensitive
 * 
 * In case of a match, the new type name is returned (first character adapted,
 * respectively). Suffixes/Prefixes are reapplied.
 * 
 * Note that this also matches fields with names like "SomeField", "fsomeField",
 * and method names like "JavaElement()".
 * 
 * EMBEDDED STRATEGY (performed second if chosen by user).
 * ----------------------------------------------------------------
 * 
 * A search is performed in the stripped element name for the old type name:
 * 
 * a) the first character must match case-insensitive
 * 
 * b) all other characters must match case-sensitive
 * 
 * c) the stripped element name must end after the type name, or the next
 * character must be a non-letter, or the next character must be upper cased.
 * 
 * In case of a match, the new type is inserted into the stripped element name,
 * replacing the old type name, first character adapted to the correct case.
 * Suffixes/Prefixes are reapplied.
 * 
 * Note that this also matches methods with names like "createjavaElement()" or
 * fields like "fjavaElementCache".
 * 
 * SUFFIX STRATEGY (performed third if chosen by user)
 * ----------------------------------------------------------------
 * 
 * The new and old type names are analyzed for "camel case suffixes", that is,
 * substrings which begin with an uppercased letter. For example,
 * "SimpleJavaElement" is split into the three hunks "Simple",
 * "Java", and "Element". If one type name has more suffixes than the
 * other, both are stripped to the smaller size.
 * 
 * Then, a search is performed in the stripped variable name hunks from back to
 * front. At least the last hunk must be found, others may then extend the match. 
 * Each hunk must match like in the exact strategy, i.e.
 * 
 * a) the first character must match case-insensitive
 * 
 * b) all other characters must match case-sensitive
 * 
 * In case of a match, the matched hunks of the new type replace
 * the hunks of the old type. Suffixes/Prefixes are reapplied.
 * 
 * Note that numbers and other non-letter characters belong to the previous
 * camel case substring. 
 * 
 * 
 * 
 * 
 */
public class RenamingNameSuggestor {
	
	/*
	 * ADDITIONAL OPTIONS
	 * ----------------------------------------------------------------
	 * 
	 * There are two additional flags which may be set in this class to allow
	 * better matching of special cases:
	 * 
	 * a) Special treatment of leading "I"s in type names, i.e. interface names
	 * 	  like "IJavaScriptElement". If the corresponding flag is set, leading "I"s are
	 * 	  stripped from type names if the second char is also uppercase to allow
	 * 	  exact matching of variable names like "javaElement" for type
	 * 	  "IJavaScriptElement". Note that embedded matching already matches cases like
	 * 	  this.
	 * 
	 * b) Special treatment of all-uppercase type names or all-uppercase type
	 * 	  name camel-case hunks, i.e. names like "AST" or "PersonalURL". If the
	 * 	  corresponding flag is set, the type name hunks will be transformed such
	 * 	  that variables like "fAst", "ast", "personalUrl", or "url" are found as
	 * 	  well. The target name will be transformed too if it is an
	 * 	  all-uppercase type name camel-case hunk as well.
	 * 
	 * 	  NOTE that in exact or embedded mode, the whole type name must be
	 * 	  all-uppercase to allow matching custom-lowercased variable names, i.e.
	 *    there are no attempts to "guess" which hunk of the new name should be lowercased
	 *    to match a partly lowercased variable name. In suffix mode, hunks of the 
	 *    new type which are at the same position as in the old type will be 
	 *    lowercased if necessary.
	 *    
	 * c) Support for (english) plural forms. If the corresponding flag is set, the
	 *    suggestor will try to match variables which have plural forms of the
	 *    type name, for example "handies" for "Handy" or "phones" for "MobilePhone".
	 *    The target name will be transformed as well, i.e. conversion like
	 *    "fHandies" -> "fPhones" are supported.   
	 * 
	 */

	public static final int STRATEGY_EXACT= 1;
	public static final int STRATEGY_EMBEDDED= 2;
	public static final int STRATEGY_SUFFIX= 3;
	
	private static final String PLURAL_S= "s"; //$NON-NLS-1$
	private static final String PLURAL_IES= "ies"; //$NON-NLS-1$
	private static final String SINGULAR_Y= "y"; //$NON-NLS-1$

	private int fStrategy;
	private String[] fFieldPrefixes;
	private String[] fFieldSuffixes;
	private String[] fStaticFieldPrefixes;
	private String[] fStaticFieldSuffixes;
	private String[] fLocalPrefixes;
	private String[] fLocalSuffixes;
	private String[] fArgumentPrefixes;
	private String[] fArgumentSuffixes;
	
	private boolean fExtendedInterfaceNameMatching;
	private boolean fExtendedAllUpperCaseHunkMatching;
	private boolean fExtendedPluralMatching;

	public RenamingNameSuggestor() {
		this(STRATEGY_SUFFIX);
	}

	public RenamingNameSuggestor(int strategy) {

		Assert.isTrue(strategy >= 1 && strategy <= 3);

		fStrategy= strategy;
		fExtendedInterfaceNameMatching= true;
		fExtendedAllUpperCaseHunkMatching= true;
		fExtendedPluralMatching= true;

		resetPrefixes();
	}

	public String suggestNewFieldName(IJavaScriptProject project, String oldFieldName, boolean isStatic, String oldTypeName, String newTypeName) {

		initializePrefixesAndSuffixes(project);

		if (isStatic)
			return suggestNewVariableName(fStaticFieldPrefixes, fStaticFieldSuffixes, oldFieldName, oldTypeName, newTypeName);
		else
			return suggestNewVariableName(fFieldPrefixes, fFieldSuffixes, oldFieldName, oldTypeName, newTypeName);
	}

	public String suggestNewLocalName(IJavaScriptProject project, String oldLocalName, boolean isArgument, String oldTypeName, String newTypeName) {

		initializePrefixesAndSuffixes(project);

		if (isArgument)
			return suggestNewVariableName(fArgumentPrefixes, fArgumentSuffixes, oldLocalName, oldTypeName, newTypeName);
		else
			return suggestNewVariableName(fLocalPrefixes, fLocalSuffixes, oldLocalName, oldTypeName, newTypeName);
	}

	public String suggestNewMethodName(String oldMethodName, String oldTypeName, String newTypeName) {

		Assert.isNotNull(oldMethodName);
		Assert.isNotNull(oldTypeName);
		Assert.isNotNull(newTypeName);
		Assert.isTrue(oldMethodName.length() > 0);
		Assert.isTrue(oldTypeName.length() > 0);
		Assert.isTrue(newTypeName.length() > 0);

		resetPrefixes();

		return match(oldTypeName, newTypeName, oldMethodName);
	}

	public String suggestNewVariableName(String[] prefixes, String[] suffixes, String oldVariableName, String oldTypeName, String newTypeName) {

		Assert.isNotNull(prefixes);
		Assert.isNotNull(suffixes);
		Assert.isNotNull(oldVariableName);
		Assert.isNotNull(oldTypeName);
		Assert.isNotNull(newTypeName);
		Assert.isTrue(oldVariableName.length() > 0);
		Assert.isTrue(oldTypeName.length() > 0);
		Assert.isTrue(newTypeName.length() > 0);

		final String usedPrefix= findLongestPrefix(oldVariableName, prefixes);
		final String usedSuffix= findLongestSuffix(oldVariableName, suffixes);
		final String strippedVariableName= oldVariableName.substring(usedPrefix.length(), oldVariableName.length() - usedSuffix.length());

		String newVariableName= match(oldTypeName, newTypeName, strippedVariableName);
		return (newVariableName != null) ? usedPrefix + newVariableName + usedSuffix : null;
	}

	// -------------------------------------- Match methods

	private String match(final String oldTypeName, final String newTypeName, final String strippedVariableName) {

		String oldType= oldTypeName;
		String newType= newTypeName;

		if (fExtendedInterfaceNameMatching && isInterfaceName(oldType) && isInterfaceName(newType)) {
			oldType= getInterfaceName(oldType);
			newType= getInterfaceName(newType);
		}

		String newVariableName= matchDirect(oldType, newType, strippedVariableName);

		if (fExtendedPluralMatching && newVariableName == null && canPluralize(oldType))
			newVariableName= matchDirect(pluralize(oldType), pluralize(newType), strippedVariableName);

		return newVariableName;
	}

	private String matchDirect(String oldType, String newType, final String strippedVariableName) {
		/*
		 * Use all strategies applied by the user. Always start with exact
		 * matching.
		 * 
		 * Note that suffix matching may not match the whole type name if the
		 * new type name has a smaller camel case chunk count.
		 */

		String newVariableName= exactMatch(oldType, newType, strippedVariableName);
		if (newVariableName == null && fStrategy >= STRATEGY_EMBEDDED)
			newVariableName= embeddedMatch(oldType, newType, strippedVariableName);
		if (newVariableName == null && fStrategy >= STRATEGY_SUFFIX)
			newVariableName= suffixMatch(oldType, newType, strippedVariableName);

		return newVariableName;
	}

	private String exactMatch(final String oldTypeName, final String newTypeName, final String strippedVariableName) {

		String newName= exactDirectMatch(oldTypeName, newTypeName, strippedVariableName);
		if (newName != null)
			return newName;

		if (fExtendedAllUpperCaseHunkMatching && isUpperCaseCamelCaseHunk(oldTypeName)) {
			String oldTN= getFirstUpperRestLowerCased(oldTypeName);
			String newTN= isUpperCaseCamelCaseHunk(newTypeName) ? getFirstUpperRestLowerCased(newTypeName) : newTypeName;
			newName= exactDirectMatch(oldTN, newTN, strippedVariableName);
		}

		return newName;
	}

	private String exactDirectMatch(final String oldTypeName, final String newTypeName, final String strippedVariableName) {

		if (strippedVariableName.equals(oldTypeName))
			return newTypeName;

		if (strippedVariableName.equals(getLowerCased(oldTypeName)))
			return getLowerCased(newTypeName);

		return null;
	}

	private String embeddedMatch(String oldTypeName, String newTypeName, String strippedVariableName) {

		// possibility of a match?
		final String lowerCaseVariable= strippedVariableName.toLowerCase();
		final String lowerCaseOldTypeName= oldTypeName.toLowerCase();
		int presumedIndex= lowerCaseVariable.indexOf(lowerCaseOldTypeName);

		while (presumedIndex != -1) {
			// it may be there
			final String presumedTypeName= strippedVariableName.substring(presumedIndex, presumedIndex + oldTypeName.length());
			final String prefix= strippedVariableName.substring(0, presumedIndex);
			final String suffix= strippedVariableName.substring(presumedIndex + oldTypeName.length());

			// can match at all? (depends on suffix)
			if (startsNewHunk(suffix)) {

				String name= exactMatch(oldTypeName, newTypeName, presumedTypeName);
				if (name != null)
					return prefix + name + suffix;
			}

			// did not match -> find next occurrence
			presumedIndex= lowerCaseVariable.indexOf(lowerCaseOldTypeName, presumedIndex + 1);
		}

		return null;
	}
	
	private String suffixMatch(final String oldType, final String newType, final String strippedVariableName) {

		// get an array of all camel-cased elements from both types + the
		// variable
		String[] suffixesOld= getSuffixes(oldType);
		String[] suffixesNew= getSuffixes(newType);
		String[] suffixesVar= getSuffixes(strippedVariableName);

		// get an equal-sized array of the last n camel-cased elements
		int min= Math.min(suffixesOld.length, suffixesNew.length);
		String[] suffixesOldEqual= new String[min];
		String[] suffixesNewEqual= new String[min];
		System.arraycopy(suffixesOld, suffixesOld.length - min, suffixesOldEqual, 0, min);
		System.arraycopy(suffixesNew, suffixesNew.length - min, suffixesNewEqual, 0, min);

		// find endIndex. endIndex is the index of the last hunk of the old type
		// name in the variable name.
		int endIndex= -1;
		for (int j= suffixesVar.length - 1; j >= 0; j--) {
			String newHunkName= exactMatch(suffixesOldEqual[suffixesOldEqual.length - 1], suffixesNewEqual[suffixesNewEqual.length - 1], suffixesVar[j]);
			if (newHunkName != null) {
				endIndex= j;
				break;
			}
		}

		if (endIndex == -1)
			return null; // last hunk not found -> no match

		int stepBack= 0;
		int lastSuffixMatched= -1;
		int hunkInVarName= -1;
		for (int i= suffixesOldEqual.length - 1; i >= 0; i--) {

			hunkInVarName= endIndex - stepBack;
			stepBack++;

			if (hunkInVarName < 0) {
				// we have reached the beginning of the variable name
				break;
			}

			// try to match this hunk:
			String newHunkName= exactMatch(suffixesOldEqual[i], suffixesNewEqual[i], suffixesVar[hunkInVarName]);

			if (newHunkName == null)
				break; // only match complete suffixes

			suffixesVar[hunkInVarName]= newHunkName;
			lastSuffixMatched= i;
		}
		
		if (lastSuffixMatched == 0) {
			// we have matched ALL type hunks in the variable name,
			// insert any new prefixes of the new type name
			int newPrefixes= suffixesNew.length - suffixesNewEqual.length;
			if (newPrefixes > 0) {
				
				// Propagate lowercased start to the front
				if (Character.isLowerCase(suffixesVar[hunkInVarName].charAt(0)) && Character.isUpperCase(suffixesOldEqual[lastSuffixMatched].charAt(0))) {
					suffixesVar[hunkInVarName]= getUpperCased(suffixesVar[hunkInVarName]);
					suffixesNew[0]= getLowerCased(suffixesNew[0]);
				}
				
				String[] newVariableName= new String[suffixesVar.length + newPrefixes];
				System.arraycopy(suffixesVar, 0, newVariableName, 0, hunkInVarName); // hunks before type name in variable name
				System.arraycopy(suffixesNew, 0, newVariableName, hunkInVarName, newPrefixes); // new hunks in new type name
				System.arraycopy(suffixesVar, hunkInVarName, newVariableName, hunkInVarName + newPrefixes, suffixesVar.length - hunkInVarName); // matched + rest hunks
				suffixesVar= newVariableName;
			}
		}

		String varName= concat(suffixesVar);
		if (varName.equals(strippedVariableName))
			return null; // no "silly suggestions"
		else
			return varName;
	}


	// ---------------- Helper methods

	/**
	 * True if the string is the beginning of a new camel case hunk. False if it
	 * is not.
	 */
	private boolean startsNewHunk(String string) {

		if (string.length() == 0)
			return true;

		return isLegalChar(string.charAt(0));
	}

	/**
	 * True if hunk is longer than 1 character and all letters in the hunk are
	 * uppercase. False if not.
	 */
	private boolean isUpperCaseCamelCaseHunk(String hunk) {
		if (hunk.length() < 2)
			return false;

		for (int i= 0; i < hunk.length(); i++) {
			if (!isLegalChar(hunk.charAt(i)))
				return false;
		}
		return true;
	}
	
	/**
	 * False if the character is a letter and it is lowercase. True in all other
	 * cases.
	 */
	private boolean isLegalChar(char c) {
		if (Character.isLetter(c))
			return Character.isUpperCase(c);
		return true;
	}

	/**
	 * Grab a list of camelCase-separated suffixes from the typeName, for
	 * example:
	 * 
	 * "JavaElementName" => { "Java", "Element", "Name }
	 * 
	 * "ASTNode" => { "AST", "Node" }
	 * 
	 */
	private String[] getSuffixes(String typeName) {
		List suffixes= new ArrayList();
		JavaWordIterator iterator= new JavaWordIterator();
		iterator.setText(typeName);
		int lastmatch= 0;
		int match;
		while ( (match= iterator.next()) != BreakIterator.DONE) {
			suffixes.add(typeName.substring(lastmatch, match));
			lastmatch= match;
		}
		return (String[]) suffixes.toArray(new String[0]);
	}

	private String concat(String[] suffixesNewEqual) {
		StringBuffer returner= new StringBuffer();
		for (int j= 0; j < suffixesNewEqual.length; j++) {
			returner.append(suffixesNewEqual[j]);
		}
		return returner.toString();
	}

	private String getLowerCased(String name) {
		if (name.length() > 1)
			return Character.toLowerCase(name.charAt(0)) + name.substring(1);
		else
			return name.toLowerCase();
	}
	
	private String getUpperCased(String name) {
		if (name.length() > 1)
			return Character.toUpperCase(name.charAt(0)) + name.substring(1);
		else
			return name.toLowerCase();
	}

	private String getFirstUpperRestLowerCased(String name) {
		if (name.length() > 1)
			return Character.toUpperCase(name.charAt(0)) + name.substring(1).toLowerCase();
		else
			return name.toLowerCase();
	}

	private boolean isInterfaceName(String typeName) {
		return ( (typeName.length() >= 2) && typeName.charAt(0) == 'I' && Character.isUpperCase(typeName.charAt(1)));
	}

	private String getInterfaceName(String typeName) {
		return typeName.substring(1);
	}

	private String findLongestPrefix(String name, String[] prefixes) {
		String usedPrefix= ""; //$NON-NLS-1$
		int bestLen= 0;
		for (int i= 0; i < prefixes.length; i++) {
			if (name.startsWith(prefixes[i])) {
				if (prefixes[i].length() > bestLen) {
					bestLen= prefixes[i].length();
					usedPrefix= prefixes[i];
				}
			}
		}
		return usedPrefix;
	}

	private String findLongestSuffix(String name, String[] suffixes) {
		String usedPrefix= ""; //$NON-NLS-1$
		int bestLen= 0;
		for (int i= 0; i < suffixes.length; i++) {
			if (name.endsWith(suffixes[i])) {
				if (suffixes[i].length() > bestLen) {
					bestLen= suffixes[i].length();
					usedPrefix= suffixes[i];
				}
			}
		}
		return usedPrefix;
	}
	
	/**
	 * Returns true if the type name can be pluralized by a string operation.
	 * This is always the case if it does not already end with an "s".
	 */
	private boolean canPluralize(String typeName) {
		return !typeName.endsWith(PLURAL_S);
	}

	private String pluralize(String typeName) {
		if (typeName.endsWith(SINGULAR_Y))
			typeName= typeName.substring(0, typeName.length() - 1).concat(PLURAL_IES);
		else if (!typeName.endsWith(PLURAL_S))
			typeName= typeName.concat(PLURAL_S);
		return typeName;
	}

	private void resetPrefixes() {
		String[] empty= new String[0];
		fFieldPrefixes= empty;
		fFieldSuffixes= empty;
		fStaticFieldPrefixes= empty;
		fStaticFieldSuffixes= empty;
		fLocalPrefixes= empty;
		fLocalSuffixes= empty;
		fArgumentPrefixes= empty;
		fArgumentSuffixes= empty;
	}

	private void initializePrefixesAndSuffixes(IJavaScriptProject project) {
		fFieldPrefixes= readCommaSeparatedPreference(project, JavaScriptCore.CODEASSIST_FIELD_PREFIXES);
		fFieldSuffixes= readCommaSeparatedPreference(project, JavaScriptCore.CODEASSIST_FIELD_SUFFIXES);
		fStaticFieldPrefixes= readCommaSeparatedPreference(project, JavaScriptCore.CODEASSIST_STATIC_FIELD_PREFIXES);
		fStaticFieldSuffixes= readCommaSeparatedPreference(project, JavaScriptCore.CODEASSIST_STATIC_FIELD_SUFFIXES);
		fLocalPrefixes= readCommaSeparatedPreference(project, JavaScriptCore.CODEASSIST_LOCAL_PREFIXES);
		fLocalSuffixes= readCommaSeparatedPreference(project, JavaScriptCore.CODEASSIST_LOCAL_SUFFIXES);
		fArgumentPrefixes= readCommaSeparatedPreference(project, JavaScriptCore.CODEASSIST_ARGUMENT_PREFIXES);
		fArgumentSuffixes= readCommaSeparatedPreference(project, JavaScriptCore.CODEASSIST_ARGUMENT_SUFFIXES);
	}

	private String[] readCommaSeparatedPreference(IJavaScriptProject project, String option) {
		String list= project.getOption(option, true);
		return list == null ? new String[0] : list.split(","); //$NON-NLS-1$
	}

}
