/*******************************************************************************
 *  Copyright (c) 2005, 2017 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.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.debug.internal.ui.IInternalDebugUIConstants;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IImportContainer;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IOrdinaryClassFile;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.debug.core.IJavaBreakpoint;
import org.eclipse.jdt.debug.core.IJavaObject;
import org.eclipse.jdt.debug.core.IJavaThread;
import org.eclipse.jdt.internal.debug.ui.JDIDebugUIPlugin;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;

/**
 * An abstract change for breakpoint type refactoring changes
 * @since 3.2
 *
 */
public abstract class BreakpointChange extends Change {

	/**
	 * constant for no line number
	 *
	 * @since 3.5
	 */
	protected static final int NO_LINE_NUMBER = -1;

	private final IJavaBreakpoint fBreakpoint;
	private final String fTypeName;
	private final int fHitCount;
	private final IJavaObject[] fInstanceFilters;
	private final int fSuspendPolicy;
	private final IJavaThread[] fThreadFilters;
	private final boolean fEnabled;
	private final String fWorkingSetNames;

	/**
	 * Constructor
	 * @param breakpoint
	 * @throws CoreException
	 */
	public BreakpointChange(IJavaBreakpoint breakpoint) throws CoreException {
		fBreakpoint = breakpoint;
		fTypeName = breakpoint.getTypeName();
		fHitCount = breakpoint.getHitCount();
		fInstanceFilters = breakpoint.getInstanceFilters();
		fSuspendPolicy = breakpoint.getSuspendPolicy();
		fThreadFilters = breakpoint.getThreadFilters();
		fEnabled = breakpoint.isEnabled();
		fWorkingSetNames = (String) breakpoint.getMarker().getAttribute(IInternalDebugUIConstants.WORKING_SET_NAME);
	}

	/**
	 * Applies the original attributes to the new breakpoint
	 *
	 * @param breakpoint the new breakpoint
	 * @throws CoreException
	 */
	protected void apply(IJavaBreakpoint breakpoint) throws CoreException {
		breakpoint.setHitCount(fHitCount);
		for (IJavaObject filter : fInstanceFilters) {
			breakpoint.addInstanceFilter(filter);
		}
		breakpoint.setSuspendPolicy(fSuspendPolicy);
		for (IJavaThread filter : fThreadFilters) {
			breakpoint.setThreadFilter(filter);
		}
		breakpoint.setEnabled(fEnabled);
	}

	/**
	 * Returns the original breakpoints prior to the change
	 * @return the original breakpoint prior to the change
	 */
	protected IJavaBreakpoint getOriginalBreakpoint() {
		return fBreakpoint;
	}

	/**
	 * Returns the original name of the type the associated breakpoint was set on.
	 * This can be different than the type being changed.
	 *
	 * @return
	 */
	protected String getOriginalBreakpointTypeName() {
		return fTypeName;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.ltk.core.refactoring.Change#initializeValidationData(org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public void initializeValidationData(IProgressMonitor pm) {
		// do nothing
	}

	/* (non-Javadoc)
	 * @see org.eclipse.ltk.core.refactoring.Change#isValid(org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public RefactoringStatus isValid(IProgressMonitor pm) throws CoreException, OperationCanceledException {
		return new RefactoringStatus();
	}

	/* (non-Javadoc)
	 * @see org.eclipse.ltk.core.refactoring.Change#getModifiedElement()
	 */
	@Override
	public Object getModifiedElement() {
		return fBreakpoint;
	}

	/**
	 * Returns an array of ints representing the new line number, char start and char end
	 * of the member.
	 *
	 * @param member
	 * @return array of 3 ints or <code>null</code>
	 */
	protected int[] getNewLineNumberAndRange(IMember member) throws CoreException {
		ISourceRange nameRange = member.getNameRange();
		int offset = nameRange.getOffset();
		int lineNumber = getNewLineNumber(member, offset);
		return new int[]{lineNumber, offset, offset + nameRange.getLength()};
	}

	/**
	 * Returns the new line number of the from the source of the specified member's compilation unit
	 * @param member the member to query
	 * @param offset the offset
	 * @return the new line number
	 * @throws JavaModelException
	 */
	private int getNewLineNumber(IMember member, int offset) throws JavaModelException {
		int lineNumber = getLineNumber();
		Document document = new Document(member.getCompilationUnit().getSource());
		try {
			lineNumber = document.getLineOfOffset(offset);
		} catch (BadLocationException e) {
		}
		return lineNumber;
	}

	/**
	 * Return the line number for the breakpoint
	 * @return the line number for the breakpoint
	 */
	protected int getLineNumber() {
		return -1;
	}

	/**
	 * Returns the hit count for the breakpoint
	 * @return the hit count for the breakpoint
	 */
	protected int getHitCount() {
		return fHitCount;
	}

	/**
	 * Returns the <code>IType</code> within the specified parent type given by simpleName
	 * @param parent
	 * @param simpleName
	 * @return the <code>IType</code> within the specified parent type given by simpleName
	 */
	public static IType getType(IJavaElement parent, String simpleName) {
		switch (parent.getElementType()) {
			case IJavaElement.COMPILATION_UNIT:
				return ((ICompilationUnit)parent).getType(simpleName);
			case IJavaElement.TYPE:
				return ((IType)parent).getType(simpleName);
			case IJavaElement.FIELD:
			case IJavaElement.INITIALIZER:
			case IJavaElement.METHOD:
				return ((IMember)parent).getType(simpleName, -1);
		}
		return null;
	}

	/**
	 * Returns the <code>IJavaElement</code> contained within the specified parent one, or the parent one
	 * by default
	 * @param parent
	 * @param element
	 * @return the <code>IJavaElement</code> contained within the specified parent one, or the parent one
	 * by default
	 */
	public static IJavaElement findElement(IJavaElement parent, IJavaElement element) {
		List<IJavaElement> children = getPath(element);
		List<IJavaElement> path = getPath(parent);
		IJavaElement currentElement = parent;
		for (int i = children.size() - path.size() - 1; i >= 0; i--) {
			IJavaElement child = children.get(i);
			switch (child.getElementType()) {
				case IJavaElement.PACKAGE_DECLARATION:
					currentElement = ((ICompilationUnit)currentElement).getPackageDeclaration(child.getElementName());
					break;
				case IJavaElement.IMPORT_CONTAINER:
					currentElement = ((ICompilationUnit)currentElement).getImportContainer();
					break;
				case IJavaElement.IMPORT_DECLARATION:
					currentElement = ((IImportContainer)currentElement).getImport(child.getElementName());
					break;
				case IJavaElement.TYPE:
					switch (currentElement.getElementType()) {
						case IJavaElement.COMPILATION_UNIT:
							currentElement = ((ICompilationUnit)currentElement).getType(child.getElementName());
							break;
						case IJavaElement.CLASS_FILE:
							if (currentElement instanceof IOrdinaryClassFile) {
								currentElement = ((IOrdinaryClassFile) currentElement).getType();
							}
							break;
						case IJavaElement.TYPE:
							currentElement = ((IType)currentElement).getType(child.getElementName());
							break;
						case IJavaElement.FIELD:
						case IJavaElement.INITIALIZER:
						case IJavaElement.METHOD:
							currentElement =  ((IMember)currentElement).getType(child.getElementName(), ((IMember)child).getOccurrenceCount());
							break;
					}
					break;
				case IJavaElement.INITIALIZER:
					currentElement = ((IType)currentElement).getInitializer(((IMember)child).getOccurrenceCount());
					break;
				case IJavaElement.FIELD:
					currentElement = ((IType)currentElement).getField(child.getElementName());
					break;
				case IJavaElement.METHOD:
					currentElement = ((IType)currentElement).getMethod(child.getElementName(), ((IMethod)child).getParameterTypes());
					break;
			}

		}
		return currentElement;
	}

	/**
	 * Returns the path of the given element up to but not including its compilation unit,
	 * in bottom up order.
	 *
	 * @param element
	 * @return element's path
	 */
	private static List<IJavaElement> getPath(IJavaElement element) {
		ArrayList<IJavaElement> children = new ArrayList<>();
		while (element != null && element.getElementType() != IJavaElement.COMPILATION_UNIT) {
			children.add(element);
			element = element.getParent();
		}
		return children;
	}

	/**
	 * Returns a label for the given breakpoint generated from the JDI model presentation.
	 *
	 * @param breakpoint a breakpoint
	 * @return standard label for the breakpoint
	 */
	protected String getBreakpointLabel(IBreakpoint breakpoint) {
		return JDIDebugUIPlugin.getDefault().getModelPresentation().getText(breakpoint);
	}

	/**
	 * Returns the original breakpoint working set names
	 * @return the original breakpoint working set names
	 */
	protected String getOriginalWorkingSets() {
		return fWorkingSetNames;
	}
}
