/*******************************************************************************
 * Copyright (c) 2000, 2011 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.corext.refactoring.rename;

import java.util.Collections;
import java.util.LinkedList;
import java.util.Map;

import org.eclipse.core.runtime.Assert;

import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IInitializer;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.Signature;


/**
 * Helper class to transplant a IJavaElement handle from a certain state of the
 * Java Model into another.
 *
 * The changes to the workspace include one type rename, a number of field
 * renames, and a number of method renames including signature changes.
 *
 * The returned handle exists in the target model state.
 *
 * @since 3.2
 *
 */
public class RefactoringHandleTransplanter {

	private final IType fOldType;
	private final IType fNewType;
	private final Map<IJavaElement, String> fRefactoredSimilarElements;

	/**
	 * @param oldType old type
	 * @param newType renamed type
	 * @param refactoredSimilarElements map from similar element (IJavaElement) to new name (String), or <code>null</code>
	 */
	public RefactoringHandleTransplanter(IType oldType, IType newType, Map<IJavaElement, String> refactoredSimilarElements) {
		fOldType= oldType;
		fNewType= newType;
		if (refactoredSimilarElements == null)
			fRefactoredSimilarElements= Collections.emptyMap();
		else
			fRefactoredSimilarElements= refactoredSimilarElements;
	}

	/**
	 * Converts the handle. Handle need not exist, but must be a source
	 * reference.
	 *
	 * @param handle
	 * @return the new handle
	 */
	public IMember transplantHandle(IMember handle) {

		/*
		 * Create a list of handles from top-level type to the handle
		 */
		final LinkedList<IMember> oldElements= new LinkedList<>();
		addElements(handle, oldElements);

		/*
		 * Step through the elements and re-locate them in the new parents.
		 */
		final IMember[] newElements= convertElements(oldElements.toArray(new IMember[0]));

		return newElements[newElements.length - 1];
	}

	private void addElements(IMember element, LinkedList<IMember> chain) {
		chain.addFirst(element);
		IJavaElement parent= element.getParent();
		if (parent instanceof IMember)
			addElements((IMember) parent, chain);
	}

	private IMember[] convertElements(IMember[] oldElements) {

		final IMember[] newElements= new IMember[oldElements.length];
		final IMember first= oldElements[0];

		Assert.isTrue(first instanceof IType);

		if (first.equals(fOldType))
			// We renamed a top level type.
			newElements[0]= fNewType;
		else
			newElements[0]= first;

		/*
		 * Note that we only need to translate the information necessary to
		 * create new handles. For example, the return type of a method is not
		 * relevant; neither is information about generic specifics in types.
		 */

		for (int i= 1; i < oldElements.length; i++) {
			final IJavaElement newParent= newElements[i - 1];
			final IJavaElement currentElement= oldElements[i];
			switch (newParent.getElementType()) {
				case IJavaElement.TYPE: {
					switch (currentElement.getElementType()) {
						case IJavaElement.TYPE: {
							final String newName= resolveTypeName((IType) currentElement);
							newElements[i]= ((IType) newParent).getType(newName);
							break;
						}
						case IJavaElement.METHOD: {
							final String newName= resolveElementName(currentElement);
							final String[] newParameterTypes= resolveParameterTypes((IMethod) currentElement);
							newElements[i]= ((IType) newParent).getMethod(newName, newParameterTypes);
							break;
						}
						case IJavaElement.INITIALIZER: {
							final IInitializer initializer= (IInitializer) currentElement;
							newElements[i]= ((IType) newParent).getInitializer(initializer.getOccurrenceCount());
							break;
						}
						case IJavaElement.FIELD: {
							final String newName= resolveElementName(currentElement);
							newElements[i]= ((IType) newParent).getField(newName);
							break;
						}
					}
					break;
				}
				case IJavaElement.METHOD: {
					switch (currentElement.getElementType()) {
						case IJavaElement.TYPE: {
							newElements[i]= resolveTypeInMember((IMethod) newParent, (IType) currentElement);
							break;
						}
					}
					break;
				}
				case IJavaElement.INITIALIZER: {
					switch (currentElement.getElementType()) {
						case IJavaElement.TYPE: {
							newElements[i]= resolveTypeInMember((IInitializer) newParent, (IType) currentElement);
							break;
						}
					}
					break;
				}
				case IJavaElement.FIELD: {
					switch (currentElement.getElementType()) {
						case IJavaElement.TYPE: {
							// anonymous type in field declaration
							newElements[i]= resolveTypeInMember((IField) newParent, (IType) currentElement);
							break;
						}
					}
					break;
				}
			}
		}
		return newElements;
	}

	private String[] resolveParameterTypes(IMethod method) {
		final String[] oldParameterTypes= method.getParameterTypes();
		final String[] newparams= new String[oldParameterTypes.length];

		final String[] possibleOldSigs= new String[4];
		possibleOldSigs[0]= Signature.createTypeSignature(fOldType.getElementName(), false);
		possibleOldSigs[1]= Signature.createTypeSignature(fOldType.getElementName(), true);
		possibleOldSigs[2]= Signature.createTypeSignature(fOldType.getFullyQualifiedName(), false);
		possibleOldSigs[3]= Signature.createTypeSignature(fOldType.getFullyQualifiedName(), true);

		final String[] possibleNewSigs= new String[4];
		possibleNewSigs[0]= Signature.createTypeSignature(fNewType.getElementName(), false);
		possibleNewSigs[1]= Signature.createTypeSignature(fNewType.getElementName(), true);
		possibleNewSigs[2]= Signature.createTypeSignature(fNewType.getFullyQualifiedName(), false);
		possibleNewSigs[3]= Signature.createTypeSignature(fNewType.getFullyQualifiedName(), true);

		// Textually replace all occurrences
		// This handles stuff like Map<SomeClass, some.package.SomeClass>
		for (int i= 0; i < oldParameterTypes.length; i++) {
			newparams[i]= oldParameterTypes[i];
			for (int j= 0; j < possibleOldSigs.length; j++) {
				newparams[i]= replaceAll(newparams[i], possibleOldSigs[j], possibleNewSigs[j]);
			}
		}
		return newparams;
	}

	private String resolveElementName(IJavaElement element) {
		final String newName= fRefactoredSimilarElements.get(element);
		if (newName != null)
			return newName;
		else
			return element.getElementName();
	}

	private IMember resolveTypeInMember(final IMember newParent, IType oldChild) {
		// Local type or anonymous type. Only local types can be renamed.
		String newName= ""; //$NON-NLS-1$
		if (oldChild.getElementName().length() != 0)
			newName= resolveTypeName(oldChild);
		return newParent.getType(newName, oldChild.getOccurrenceCount());
	}

	private String resolveTypeName(IType type) {
		return type.equals(fOldType) ? fNewType.getElementName() : type.getElementName();
	}

    private static String replaceAll(final String source, final String replaceFrom, final String replaceTo) {
		final StringBuilder buf= new StringBuilder(source.length());
		int currentIndex= 0;
		int matchIndex;
		while ((matchIndex= source.indexOf(replaceFrom, currentIndex)) != -1) {
			buf.append(source.substring(currentIndex, matchIndex));
			buf.append(replaceTo);
			currentIndex= matchIndex + replaceFrom.length();
		}
		buf.append(source.substring(currentIndex));
		return buf.toString();
	}
}
