/*******************************************************************************
 * Copyright (c) 2000, 2021 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.debug.core.refactoring;

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

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IModuleDescription;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.internal.debug.core.JDIDebugPlugin;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.CompositeChange;

/**
 *
 * provides methods to create refactoring changes
 */
public class JDTDebugRefactoringUtil {

	/**
	 * Take a list of Changes, and return a unique Change, a CompositeChange, or null.
	 */
	public static Change createChangeFromList(List<Change> changes, String changeLabel) {
		int nbChanges= changes.size();
		switch (nbChanges) {
		case 0:
			return null;
		case 1:
			return changes.get(0);
		default:
			return new CompositeChange(changeLabel, changes.toArray(new Change[changes.size()]));
		}
	}

	/**
	 * Returns the new container name for the given project and launch configuration
	 * @param javaProject the java to get the new container name for
	 * @param launchConfiguration the associated launch configuration
	 * @return the new container name
	 * @since 3.2
	 */
	protected static String computeNewContainerName(ILaunchConfiguration launchConfiguration) {
        IFile file = launchConfiguration.getFile();
        if (file != null) {
            return file.getParent().getProjectRelativePath().toString();
        }
        return null;
    }

	/**
	 * Returns a change for the given launch configuration if the launch configuration needs to
	 * be updated for this IType change. It specifically looks to see if the main type of the launch configuration
	 * is an inner type of the given IType.
	 * @param config the launch configuration
	 * @param type the type to check for
	 * @param newfqname the new fully qualified name
	 * @param pname the new project name
	 * @return the <code>Change</code> for this outer type
	 * @throws CoreException
	 * @since 3.2
	 */
	protected static Change createChangesForOuterTypeChange(ILaunchConfiguration config, IType type, String newfqname, String pname) throws CoreException {
		IType[] innerTypes = type.getTypes();
		if(innerTypes.length == 0) {
			return null;
		}
		Change change = null;
		String mtname = config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, (String)null);
		for (int i= 0; i < innerTypes.length; i++) {
			String newTypeName = newfqname + '$' + innerTypes[i].getElementName();
			// if it matches, check the type
			if (innerTypes[i].getFullyQualifiedName().equals(mtname)) {
				return new LaunchConfigurationProjectMainTypeChange(config, newTypeName, pname);
			}
			// if it's not the type, check the inner types
			change = createChangesForOuterTypeChange(config, innerTypes[i], newTypeName, pname);
		}
		return change;
	}

	/**
	 * Provides a public mechanism for creating the <code>Change</code> for moving a package
	 * @param packageFragment the fragment to move
	 * @param destination the destination to move it to
	 * @return the <code>Change</code> for moving the package
	 * @throws CoreException
	 * @since 3.2
	 */
	public static Change createChangesForPackageMove(IPackageFragment pfragment, IPackageFragmentRoot destination) throws CoreException {
		List<Change> changes = new ArrayList<>();
		ILaunchConfiguration[] configs = getJavaTypeLaunchConfigurations(pfragment.getJavaProject().getElementName());
		String mtname = null;
		for (int i= 0; i < configs.length; i++) {
			mtname = configs[i].getAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, (String)null);
			if(mtname != null) {
				if(mtname.lastIndexOf(pfragment.getElementName()) > -1) {
					changes.add(new LaunchConfigurationProjectMainTypeChange(configs[i], null, destination.getJavaProject().getElementName()));
				}
			}
		}
		return JDTDebugRefactoringUtil.createChangeFromList(changes, RefactoringMessages.LaunchConfigurationProjectMainTypeChange_7);
	}

	/**
	 * Provides a public mechanism for creating the <code>Change</code> for renaming a package
	 * @param packageFragment the fragment to rename
	 * @param newName the new name for the fragment
	 * @return the Change for the renaming
	 * @throws CoreException
	 * @since 3.2
	 */
	public static Change createChangesForPackageRename(IPackageFragment pfragment, String newname) throws CoreException {
		List<Change> changes = new ArrayList<>();
		ILaunchConfiguration[] configs = getJavaTypeLaunchConfigurations(pfragment.getJavaProject().getElementName());
		String mtname;
		for (int i= 0; i < configs.length; i++) {
			mtname = configs[i].getAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, (String)null);
			if(mtname != null) {
				String pkname = ""; //$NON-NLS-1$
				int index = mtname.lastIndexOf('.');
				if(index > 0) {
					pkname = mtname.substring(0, index);
				}
				if (pfragment.getElementName().equals(pkname)) {
					String ntname = newname + '.' + mtname.substring(index + 1);
					changes.add(new LaunchConfigurationProjectMainTypeChange(configs[i], ntname, null));
				}
			}
			else {
				changes.add(new LaunchConfigurationProjectMainTypeChange(configs[i], null, null));
			}
		}
		return JDTDebugRefactoringUtil.createChangeFromList(changes, RefactoringMessages.LaunchConfigurationProjectMainTypeChange_7);
	}

	/**
	 * Provides a public mechanism for creating the <code>Change</code> for renaming a project
	 * @param javaProject the project to rename
	 * @param newProjectName the new name for the project
	 * @return the Change for the project rename
	 * @throws CoreException
	 * @since 3.2
	 */
	public static Change createChangesForProjectRename(IJavaProject project, String newname) throws CoreException {
		List<Change> changes = new ArrayList<>();
		ILaunchConfiguration[] configs = getJavaTypeLaunchConfigurations(project.getElementName());
		LaunchConfigurationProjectMainTypeChange change = null;
		for (int i= 0; i < configs.length; i++) {
			change = new LaunchConfigurationProjectMainTypeChange(configs[i], null, newname);
            String newcname = computeNewContainerName(configs[i]);
            if (newcname != null) {
                change.setNewContainerName(newcname);
            }
			changes.add(change);
		}
		return JDTDebugRefactoringUtil.createChangeFromList(changes, RefactoringMessages.LaunchConfigurationProjectMainTypeChange_7);
	}

	/**
	 * Provides a public mechanism for creating the <code>Change</code> for renaming a module
	 *
	 * @param module
	 *            the module to rename
	 * @param newname
	 *            the new name for the module
	 * @return the Change for the module rename
	 * @throws CoreException
	 */
	public static Change createChangesForModuleRename(IModuleDescription module, String newname) throws CoreException {
		List<Change> changes = new ArrayList<>();
		ILaunchConfiguration[] configs = getJavaTypeLaunchConfigurationsForModule(module.getElementName());
		LaunchConfigurationProjectMainTypeChange change = null;
		for (int i = 0; i < configs.length; i++) {
			change = new LaunchConfigurationProjectMainTypeChange(configs[i], null, newname);
			String newcname = computeNewContainerName(configs[i]);
			if (newcname != null) {
				change.setNewContainerName(newcname);
			}
			changes.add(change);
		}
		return JDTDebugRefactoringUtil.createChangeFromList(changes, RefactoringMessages.LaunchConfigurationProjectMainTypeChange_7);
	}

	/**
	 * Creates a <code>Change</code> for a type change
	 * @param type the type that is changing
	 * @param newfqname the new fully qualified name
	 * @param pname the project name
	 * @return the <code>Change</code> for changing the specified type
	 * @throws CoreException
	 * @since 3.2
	 */
	protected static Change createChangesForTypeChange(IType type, String newfqname, String pname) throws CoreException {
		List<Change> changes = new ArrayList<>();
		String typename = type.getFullyQualifiedName();
		ILaunchConfiguration[] configs = getJavaTypeLaunchConfigurations(type.getJavaProject().getElementName());
		String mtname;
		for (int i= 0; i < configs.length; i++) {
			mtname = configs[i].getAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, (String)null);
			if (typename.equals(mtname)) {
				changes.add(new LaunchConfigurationProjectMainTypeChange(configs[i], newfqname, pname));
			}
			else {
				Change change = createChangesForOuterTypeChange(configs[i], type, newfqname, pname);
				if (change != null) {
					changes.add(change);
				}
			}
		}
		return JDTDebugRefactoringUtil.createChangeFromList(changes, RefactoringMessages.LaunchConfigurationProjectMainTypeChange_7);
	}

	/**
	 * Provides a public mechanism for creating the <code>Change</code> for moving a type
	 * @param type the type being moved
	 * @param destination the destination to move the type to
	 * @return the <code>Change</code> for the type move
	 * @throws CoreException
	 * @since 3.2
	 */
	public static Change createChangesForTypeMove(IType type, IJavaElement destination) throws CoreException {
		IJavaProject pdestination = destination.getJavaProject();
		String newpname = null;
		if (!type.getJavaProject().equals(pdestination)) {
			newpname = pdestination.getElementName();
		}
		String newfqname = type.getElementName();
		if (destination instanceof IType) {
			newfqname = ((IType)destination).getFullyQualifiedName() + '$' + type.getElementName();
		}
		else if (destination instanceof IPackageFragment) {
			if (!((IPackageFragment) destination).isDefaultPackage()) {
				newfqname = destination.getElementName() + '.' + type.getElementName();
			}
		}
		return createChangesForTypeChange(type, newfqname, newpname);
	}

	/**
	 * Provides a public mechanism for creating the <code>Change</code> for renaming a type
	 * @param type the type to rename
	 * @param newname the new name for the type
	 * @return the <code>Change</code> for the type rename
	 * @throws CoreException
	 * @since 3.2
	 */
	public static Change createChangesForTypeRename(IType type, String newname) throws CoreException {
		IType dtype = type.getDeclaringType();
		String newfqname = newname;
		if (dtype == null) {
			IPackageFragment packageFragment = type.getPackageFragment();
			if (!packageFragment.isDefaultPackage()) {
				newfqname = packageFragment.getElementName() + '.' + newname;
			}
		}
		else {
			newfqname = dtype.getFullyQualifiedName() + '$' + newname;
		}
		return createChangesForTypeChange(type, newfqname, null);
	}

	/**
	 * Returns a listing of configurations that have a specific project name attribute in them
	 * @param pname the project attribute to compare against
	 * @return the list of java type launch configurations that have the specified project attribute
	 * @since 3.2
	 */
	protected static ILaunchConfiguration[] getJavaTypeLaunchConfigurations(String pname) {
		try {
			ILaunchConfiguration[] configs = DebugPlugin.getDefault().getLaunchManager().getLaunchConfigurations();
			ArrayList<ILaunchConfiguration> list = new ArrayList<>();
			String attrib;
			for(int i = 0; i < configs.length; i++) {
				attrib = configs[i].getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, (String)null);
				if(attrib != null) {
					if(attrib.equals(pname)) {
						list.add(configs[i]);
					}
				}
			}
			return list.toArray(new ILaunchConfiguration[list.size()]);
		}
		catch(CoreException e) {JDIDebugPlugin.log(e);}
		return new ILaunchConfiguration[0];
	}

	/**
	 * Returns a listing of configurations that have a specific module name attribute in them
	 *
	 * @param pname
	 *            the module attribute to compare against
	 * @return the list of java type launch configurations that have the specified module attribute
	 */
	protected static ILaunchConfiguration[] getJavaTypeLaunchConfigurationsForModule(String pname) {
		try {
			ILaunchConfiguration[] configs = DebugPlugin.getDefault().getLaunchManager().getLaunchConfigurations();
			ArrayList<ILaunchConfiguration> list = new ArrayList<>();
			String attrib;
			for (int i = 0; i < configs.length; i++) {
				attrib = configs[i].getAttribute(IJavaLaunchConfigurationConstants.ATTR_MODULE_NAME, (String) null);
				if (attrib != null) {
					if (attrib.equals(pname)) {
						list.add(configs[i]);
					}
				}
			}
			return list.toArray(new ILaunchConfiguration[list.size()]);
		} catch (CoreException e) {
			JDIDebugPlugin.log(e);
		}
		return new ILaunchConfiguration[0];
	}

}
