/*******************************************************************************
 * Copyright (c) 2000, 2022 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.core.manipulation.search;

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

import org.eclipse.core.runtime.CoreException;

import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.BreakStatement;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ContinueStatement;
import org.eclipse.jdt.core.dom.DoStatement;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.ForStatement;
import org.eclipse.jdt.core.dom.Initializer;
import org.eclipse.jdt.core.dom.LabeledStatement;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.NodeFinder;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SwitchExpression;
import org.eclipse.jdt.core.dom.SwitchStatement;
import org.eclipse.jdt.core.dom.WhileStatement;

import org.eclipse.jdt.internal.core.manipulation.util.BasicElementLabels;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.dom.TokenScanner;
import org.eclipse.jdt.internal.corext.util.Messages;


/**
 * Class used to find the target for a break or continue statement according
 * to the language specification.
 * <p>
 * The target statement is a while, do, switch, for or a labeled statement.
 * Break is described in section 14.15 of the JLS3 and continue in section 14.16.</p>
 *
 * @since 3.2
 */
public class BreakContinueTargetFinder extends ASTVisitor implements IOccurrencesFinder {

	public static final String ID= "BreakContinueTargetFinder"; //$NON-NLS-1$

	private ASTNode fSelected;
	private boolean fIsBreak;
	private SimpleName fLabel;
	private String fDescription;
	private CompilationUnit fASTRoot;

	private static final Class<?>[] STOPPERS=        {MethodDeclaration.class, Initializer.class};
	private static final Class<?>[] BREAKTARGETS=    {ForStatement.class, EnhancedForStatement.class, WhileStatement.class, DoStatement.class, SwitchStatement.class, SwitchExpression.class};
	private static final Class<?>[] CONTINUETARGETS= {ForStatement.class, EnhancedForStatement.class, WhileStatement.class, DoStatement.class};
	private static final int BRACE_LENGTH= 1;

	/*
	 * Initializes the finder. Returns error message or <code>null</code> if everything is OK.
	 */
	@Override
	public String initialize(CompilationUnit root, int offset, int length) {
		return initialize(root, NodeFinder.perform(root, offset, length));
	}

	/*
	 * Initializes the finder. Returns error message or <code>null</code> if everything is OK.
	 */
	@Override
	public String initialize(CompilationUnit root, ASTNode node) {
		ASTNode controlNode= getBreakOrContinueNode(node);
		if (controlNode != null) {
			fASTRoot= root;

			try {
				if (root.getTypeRoot() == null || root.getTypeRoot().getBuffer() == null)
					return SearchMessages.BreakContinueTargetFinder_cannot_highlight;
			} catch (JavaModelException e) {
				return SearchMessages.BreakContinueTargetFinder_cannot_highlight;
			}
			fSelected= controlNode;
			fIsBreak= fSelected instanceof BreakStatement;
			fLabel= getLabel();
			fDescription= Messages.format(SearchMessages.BreakContinueTargetFinder_occurrence_description, BasicElementLabels.getJavaElementName(ASTNodes.asString(fSelected)));
			return null;
		} else {
			return SearchMessages.BreakContinueTargetFinder_no_break_or_continue_selected;
		}
	}


	//extract the control node: handle labels
	private ASTNode getBreakOrContinueNode(ASTNode selectedNode) {
		if (selectedNode instanceof BreakStatement)
			return selectedNode;
		if (selectedNode instanceof ContinueStatement)
			return selectedNode;
		if (selectedNode instanceof SimpleName && selectedNode.getParent() instanceof BreakStatement)
			return selectedNode.getParent();
		if (selectedNode instanceof SimpleName && selectedNode.getParent() instanceof ContinueStatement)
			return selectedNode.getParent();
		return null;
	}


	private SimpleName getLabel() {
		if (fIsBreak){
			BreakStatement bs= (BreakStatement) fSelected;
			return bs.getLabel();
		} else {
			ContinueStatement cs= (ContinueStatement) fSelected;
			return cs.getLabel();
		}
	}

	/**
	 * Returns the locations of all occurrences or <code>null</code> if no matches are found
	 *
	 * @return the locations of all occurrences or <code>null</code> if no matches are found
	 */
	@Override
	public OccurrenceLocation[] getOccurrences() {
		ASTNode targetNode= findTargetNode(fSelected);
		if (!isEnclosingStatement(targetNode))
			return null;

		List<OccurrenceLocation> list= new ArrayList<>();
		OccurrenceLocation location= getLocationForFirstToken(targetNode);
		if (location != null) {
			list.add(location);
		}
		if (fIsBreak) {
			location= getLocationForClosingBrace(targetNode);
			if (location != null) {
				list.add(location);
			}
		}
		if (!list.isEmpty()) {
			return list.toArray(new OccurrenceLocation[list.size()]);
		}
		return null;
	}

	private boolean isEnclosingStatement(ASTNode targetNode) {
		return (targetNode != null) && !(targetNode instanceof MethodDeclaration) && !(targetNode instanceof Initializer);
	}

	private ASTNode findTargetNode(ASTNode node) {
		do {
			node= node.getParent();
		} while (keepWalkingUp(node));
		return node;
	}

	private OccurrenceLocation getLocationForFirstToken(ASTNode node) {
		try {
			int nextEndOffset= new TokenScanner(fASTRoot.getTypeRoot()).getNextEndOffset(node.getStartPosition(), true);
			return new OccurrenceLocation(node.getStartPosition(), nextEndOffset - node.getStartPosition(), 0, fDescription);
		} catch (CoreException e) {
			// ignore
		}
		return new OccurrenceLocation(node.getStartPosition(), node.getLength(), 0, fDescription);
	}

	private OccurrenceLocation getLocationForClosingBrace(ASTNode targetNode) {
		/* Ideally, we'd scan backwards to find the '}' token, but it may be an overkill
		 * so I'll just assume the closing brace token has a fixed length. */
		int offset= ASTNodes.getExclusiveEnd(targetNode) - BRACE_LENGTH;
		return new OccurrenceLocation(offset, BRACE_LENGTH, 0, fDescription);
	}

	private boolean keepWalkingUp(ASTNode node) {
		if (node == null)
			return false;
		if (isAnyInstanceOf(STOPPERS, node))
			return false;
		if (fLabel != null && LabeledStatement.class.isInstance(node)){
			LabeledStatement ls= (LabeledStatement)node;
			return ! areEqualLabels(ls.getLabel(), fLabel);
		}
		if (fLabel == null) {
			if (isAnyInstanceOf(fIsBreak ? BREAKTARGETS : CONTINUETARGETS, node))
				return node.getParent() instanceof LabeledStatement; // for behavior consistency of break targets: see bug 339176
			if (node instanceof LabeledStatement)
				return false;
		}
		return true;
	}

	private static boolean areEqualLabels(SimpleName labelToMatch, SimpleName labelSelected) {
		return labelSelected.getIdentifier().equals(labelToMatch.getIdentifier());
	}

	private static boolean isAnyInstanceOf(Class<?>[] continueTargets, ASTNode node) {
		for (Class<?> continueTarget : continueTargets) {
			if (continueTarget.isInstance(node)) {
				return true;
			}
		}
		return false;
	}

	@Override
	public CompilationUnit getASTRoot() {
		return fASTRoot;
	}

	@Override
	public String getElementName() {
		return ASTNodes.asString(fSelected);
	}

	@Override
	public String getID() {
		return ID;
	}

	@Override
	public String getJobLabel() {
		return SearchMessages.BreakContinueTargetFinder_job_label;
	}

	@Override
	public int getSearchKind() {
		return IOccurrencesFinder.K_BREAK_TARGET_OCCURRENCE;
	}

	@Override
	public String getUnformattedPluralLabel() {
		if (fIsBreak) {
			return SearchMessages.BreakContinueTargetFinder_break_label_plural;
		} else {
			return SearchMessages.BreakContinueTargetFinder_continue_label_plural;
		}
	}

	@Override
	public String getUnformattedSingularLabel() {
		if (fIsBreak) {
			return SearchMessages.BreakContinueTargetFinder_break_label_singular;
		} else {
			return SearchMessages.BreakContinueTargetFinder_continue_label_singular;
		}
	}
}
