/*******************************************************************************
 * Copyright (c) 2006, 2023 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
 *     Stephan Herrmann - Contribution for
 *								bug 345305 - [compiler][null] Compiler misidentifies a case of "variable can only be null"
 *******************************************************************************/
package org.eclipse.jdt.internal.compiler.ast;

import java.util.List;

import org.eclipse.jdt.internal.compiler.ASTVisitor;
import org.eclipse.jdt.internal.compiler.codegen.CodeStream;
import org.eclipse.jdt.internal.compiler.flow.FlowContext;
import org.eclipse.jdt.internal.compiler.flow.FlowInfo;
import org.eclipse.jdt.internal.compiler.impl.Constant;
import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeIds;

/**
 * CombinedBinaryExpression is an implementation of BinaryExpression that
 * specifically attempts to mitigate the issues raised by expressions which
 * have a very deep leftmost branch. It does so by maintaining a table of
 * direct references to its subexpressions, and implementing non-recursive
 * variants of the most significant recursive algorithms of its ancestors.
 * The subexpressions table only holds intermediate binary expressions. Its
 * role is to provide the reversed navigation through the left relationship
 * of BinaryExpression to Expression. To cope with potentially very deep
 * left branches, an instance of CombinedBinaryExpression is created once in
 * a while, using variable thresholds held by {@link #arityMax}.
 * As a specific case, the topmost node of all binary expressions that are
 * deeper than one is a CombinedBinaryExpression, but it has no references
 * table.<br>
 * Notes:
 * <ul>
 * <li>CombinedBinaryExpression is not meant to behave in other ways than
 *     BinaryExpression in any observable respect;</li>
 * <li>visitors that implement their own traversal upon binary expressions
 *     should consider taking advantage of combined binary expressions, or
 *     else face a risk of StackOverflowError upon deep instances;</li>
 * <li>callers that need to change the operator should rebuild the expression
 *     from scratch, or else amend the references table as needed to cope with
 *     the resulting, separated expressions.</li>
 * </ul>
 */
public class CombinedBinaryExpression extends BinaryExpression {

	/**
	 * The number of consecutive binary expressions of this' left branch that
	 * bear the same operator as this.<br>
	 * Notes:
	 * <ul><li>the presence of a CombinedBinaryExpression instance resets
	 *         arity, even when its operator is compatible;</li>
	 *	   <li>this property is maintained by the parser.</li>
	 * </ul>
	 */
	public int arity;

	/**
	 * The threshold that will trigger the creation of the next full-fledged
	 * CombinedBinaryExpression. This field is only maintained for the
	 * topmost binary expression (it is 0 otherwise). It enables a variable
	 * policy, which scales better with very large expressions.
	 */
	public int arityMax;

	/**
	 * Upper limit for {@link #arityMax}.
	 */
	public static final int ARITY_MAX_MAX = 160;

	/**
	 * Default lower limit for {@link #arityMax}.
	 */
	public static final int ARITY_MAX_MIN = 20;

	/**
	 * Default value for the first term of the series of {@link #arityMax}
	 * values. Changing this allows for experimentation. Not meant to be
	 * changed during a parse operation.
	 */
	public static int defaultArityMaxStartingValue = ARITY_MAX_MIN;

	/**
	 * A table of references to the binary expressions of this' left branch.
	 * Instances of CombinedBinaryExpression are not repeated here. Instead,
	 * the left subexpression of referencesTable[0] may be a combined binary
	 * expression, if appropriate. Null when this only cares about tracking
	 * the expression's arity.
	 */
	public BinaryExpression referencesTable[];

/**
 * Make a new CombinedBinaryExpression. If arity is strictly greater than one,
 * a references table is built and initialized with the reverse relationship of
 * the one defined by {@link BinaryExpression#left}. arity and left must be
 * compatible with each other (that is, there must be at least arity - 1
 * consecutive compatible binary expressions into the leftmost branch of left,
 * the topmost of which being left's immediate left expression).
 * @param left the left branch expression
 * @param right the right branch expression
 * @param operator the operator for this binary expression - only PLUS for now
 * @param arity the number of binary expressions of a compatible operator that
 *        already exist into the leftmost branch of left (including left); must
 *        be strictly greater than 0
 */
public CombinedBinaryExpression(Expression left, Expression right, int operator, int arity) {
	super(left, right, operator);
	initArity(left, arity);
}
public CombinedBinaryExpression(CombinedBinaryExpression expression) {
	super(expression);
	initArity(expression.left, expression.arity);
}

@Override
public FlowInfo analyseCode(BlockScope currentScope, FlowContext flowContext,
		FlowInfo flowInfo) {
	// keep implementation in sync with BinaryExpression#analyseCode
	if (this.referencesTable == null) {
		return super.analyseCode(currentScope, flowContext, flowInfo);
	}
	try {
		BinaryExpression cursor;
		cursor = this.referencesTable[0];
		flowInfo = cursor.left.analyseCode(currentScope, flowContext, flowInfo).
				unconditionalInits();
		if (cursor.resolvedType.id != TypeIds.T_JavaLangString) {
			cursor.left.checkNPE(currentScope, flowContext, flowInfo);
		}
		for (int i = 0, end = this.arity; i < end; i ++) {
			cursor = this.referencesTable[i];
			flowInfo = cursor.right.
					analyseCode(currentScope, flowContext, flowInfo).
						unconditionalInits();
			if (cursor.resolvedType.id != TypeIds.T_JavaLangString) {
				cursor.right.checkNPE(currentScope, flowContext, flowInfo);
			}
		}
		flowInfo = this.right.analyseCode(currentScope, flowContext, flowInfo).unconditionalInits();
		if (this.resolvedType.id != TypeIds.T_JavaLangString) {
			this.right.checkNPE(currentScope, flowContext, flowInfo);
		}
		return flowInfo;
	} finally {
		// account for exception possibly thrown by arithmetics
		flowContext.recordAbruptExit();
	}
}

@Override
public void generateOptimizedStringConcatenation(BlockScope blockScope,
		CodeStream codeStream, int typeID) {
	// keep implementation in sync with BinaryExpression and Expression
	// #generateOptimizedStringConcatenation
	if (this.referencesTable == null) {
		super.generateOptimizedStringConcatenation(blockScope, codeStream,
			typeID);
	} else {
		if ((((this.bits & ASTNode.OperatorMASK) >> ASTNode.OperatorSHIFT) ==
				OperatorIds.PLUS)
			&& ((this.bits & ASTNode.ReturnTypeIDMASK) == TypeIds.T_JavaLangString)) {
			if (this.constant != Constant.NotAConstant) {
				codeStream.generateConstant(this.constant, this.implicitConversion);
				codeStream.invokeStringConcatenationAppendForType(
						this.implicitConversion & TypeIds.COMPILE_TYPE_MASK);
			} else {
				BinaryExpression cursor = this.referencesTable[0];

				int restart = 0;
	//			int cursorTypeID;
				int pc = codeStream.position;
				for (restart = this.arity - 1; restart >= 0; restart--) {
					if ((cursor = this.referencesTable[restart]).constant !=
							Constant.NotAConstant) {
						codeStream.generateConstant(cursor.constant,
							cursor.implicitConversion);
						codeStream.invokeStringConcatenationAppendForType(
							cursor.implicitConversion & TypeIds.COMPILE_TYPE_MASK);
						break;
					}
					// never happens for now - may reconsider if we decide to
					// cover more than string concatenation
	//				if (!((((cursor = this.referencesTable[restart]).bits &
	//						ASTNode.OperatorMASK) >> ASTNode.OperatorSHIFT) ==
	//							OperatorIds.PLUS) &
	//						((cursorTypeID = cursor.bits & ASTNode.ReturnTypeIDMASK) ==
	//							TypeIds.T_JavaLangString)) {
	//					if (cursorTypeID == T_JavaLangString &&
	//							cursor.constant != Constant.NotAConstant &&
	//							cursor.constant.stringValue().length() == 0) {
	//						break; // optimize str + ""
	//					}
	//					cursor.generateCode(blockScope, codeStream, true);
	//					codeStream.invokeStringConcatenationAppendForType(
	//							cursorTypeID);
	//					break;
	//				}
				}
				restart++;
				if (restart == 0) { // reached the leftmost expression
					cursor.left.generateOptimizedStringConcatenation(
						blockScope,
						codeStream,
						cursor.left.implicitConversion & TypeIds.COMPILE_TYPE_MASK);
				}
				int pcAux;
				for (int i = restart; i < this.arity; i++) {
					codeStream.recordPositionsFrom(pc,
						(cursor = this.referencesTable[i]).left.sourceStart);
					pcAux = codeStream.position;
					cursor.right.generateOptimizedStringConcatenation(blockScope,
						codeStream,	cursor.right.implicitConversion &
							TypeIds.COMPILE_TYPE_MASK);
					codeStream.recordPositionsFrom(pcAux, cursor.right.sourceStart);
				}
				codeStream.recordPositionsFrom(pc, this.left.sourceStart);
				pc = codeStream.position;
				this.right.generateOptimizedStringConcatenation(
					blockScope,
					codeStream,
					this.right.implicitConversion & TypeIds.COMPILE_TYPE_MASK);
				codeStream.recordPositionsFrom(pc, this.right.sourceStart);
			}
		} else {
			super.generateOptimizedStringConcatenation(blockScope, codeStream,
				typeID);
		}
	}
}

@Override
public void buildStringForConcatation(BlockScope blockScope, CodeStream codeStream, int typeID, StringBuilder recipe, List<TypeBinding> argTypes) {
	if (this.referencesTable == null) {
		super.buildStringForConcatation(blockScope, codeStream,	typeID, recipe, argTypes);
	} else {
		// copied from below method generateOptimizedStringConcatenationCreation(BlockScope, CodeStream, int)
		if ((((this.bits & ASTNode.OperatorMASK) >> ASTNode.OperatorSHIFT) ==
				OperatorIds.PLUS) &&
					((this.bits & ASTNode.ReturnTypeIDMASK) ==
						TypeIds.T_JavaLangString) &&
					this.constant == Constant.NotAConstant) {
			BinaryExpression cursor = this.referencesTable[this.arity - 1];
			int restart = 0;
			for (restart = this.arity - 1; restart >= 0; restart--) {
				if (((((cursor = this.referencesTable[restart]).bits &
						ASTNode.OperatorMASK) >> ASTNode.OperatorSHIFT) ==
							OperatorIds.PLUS) &&
						((cursor.bits & ASTNode.ReturnTypeIDMASK) ==
							TypeIds.T_JavaLangString)) {
					if (cursor.constant != Constant.NotAConstant) {
						cursor.buildStringForConcatation(blockScope, codeStream, typeID, recipe, argTypes);
						break;
					}
				} else {
					cursor.buildStringForConcatation(blockScope, codeStream, cursor.implicitConversion &
							TypeIds.COMPILE_TYPE_MASK, recipe, argTypes);
					break;
				}
			}
			restart++;
			if (restart == 0) { // reached the leftmost expression
				cursor.left.buildStringForConcatation(blockScope, codeStream, cursor.left.implicitConversion & TypeIds.COMPILE_TYPE_MASK, recipe, argTypes);
			}
			for (int i = restart; i < this.arity; i++) {
				cursor = this.referencesTable[i];
				cursor.right.buildStringForConcatation(blockScope, codeStream, cursor.right.implicitConversion &
						TypeIds.COMPILE_TYPE_MASK, recipe, argTypes);
			}
			this.right.buildStringForConcatation(blockScope, codeStream, this.right.implicitConversion & TypeIds.COMPILE_TYPE_MASK, recipe, argTypes);
		} else {
			super.buildStringForConcatation(blockScope, codeStream,	typeID, recipe, argTypes);
		}
	}
}
@Override
public void generateOptimizedStringConcatenationCreation(BlockScope blockScope,
		CodeStream codeStream, int typeID) {
	// keep implementation in sync with BinaryExpression
	// #generateOptimizedStringConcatenationCreation
	if (this.referencesTable == null) {
		super.generateOptimizedStringConcatenationCreation(blockScope,
			codeStream,	typeID);
	} else {
		if ((((this.bits & ASTNode.OperatorMASK) >> ASTNode.OperatorSHIFT) ==
				OperatorIds.PLUS) &&
					((this.bits & ASTNode.ReturnTypeIDMASK) ==
						TypeIds.T_JavaLangString) &&
					this.constant == Constant.NotAConstant) {
			int pc = codeStream.position;
			BinaryExpression cursor = this.referencesTable[this.arity - 1];
				// silence warnings
			int restart = 0;
			for (restart = this.arity - 1; restart >= 0; restart--) {
				if (((((cursor = this.referencesTable[restart]).bits &
						ASTNode.OperatorMASK) >> ASTNode.OperatorSHIFT) ==
							OperatorIds.PLUS) &&
						((cursor.bits & ASTNode.ReturnTypeIDMASK) ==
							TypeIds.T_JavaLangString)) {
					if (cursor.constant != Constant.NotAConstant) {
						codeStream.newStringContatenation(); // new: java.lang.StringBuffer
						codeStream.dup();
						codeStream.ldc(cursor.constant.stringValue());
						codeStream.invokeStringConcatenationStringConstructor();
						// invokespecial: java.lang.StringBuffer.<init>(Ljava.lang.String;)V
						break;
					}
				} else {
					cursor.generateOptimizedStringConcatenationCreation(blockScope,
						codeStream, cursor.implicitConversion &
							TypeIds.COMPILE_TYPE_MASK);
					break;
				}
			}
			restart++;
			if (restart == 0) { // reached the leftmost expression
				cursor.left.generateOptimizedStringConcatenationCreation(
					blockScope,
					codeStream,
					cursor.left.implicitConversion & TypeIds.COMPILE_TYPE_MASK);
			}
			int pcAux;
			for (int i = restart; i < this.arity; i++) {
				codeStream.recordPositionsFrom(pc,
					(cursor = this.referencesTable[i]).left.sourceStart);
				pcAux = codeStream.position;
				cursor.right.generateOptimizedStringConcatenation(blockScope,
					codeStream,	cursor.right.implicitConversion &
						TypeIds.COMPILE_TYPE_MASK);
				codeStream.recordPositionsFrom(pcAux, cursor.right.sourceStart);
			}
			codeStream.recordPositionsFrom(pc, this.left.sourceStart);
			pc = codeStream.position;
			this.right.generateOptimizedStringConcatenation(
				blockScope,
				codeStream,
				this.right.implicitConversion & TypeIds.COMPILE_TYPE_MASK);
			codeStream.recordPositionsFrom(pc, this.right.sourceStart);
		} else {
			super.generateOptimizedStringConcatenationCreation(blockScope,
				codeStream, typeID);
		}
	}
}
private void initArity(Expression expression, int value) {
	this.arity = value;
	if (value > 1) {
		this.referencesTable = new BinaryExpression[value];
		this.referencesTable[value - 1] = (BinaryExpression) expression;
		for (int i = value - 1; i > 0; i--) {
			this.referencesTable[i - 1] =
				(BinaryExpression) this.referencesTable[i].left;
		}
	} else {
		this.arityMax = defaultArityMaxStartingValue;
	}
}

@Override
public StringBuffer printExpressionNoParenthesis(int indent,
		StringBuffer output) {
	// keep implementation in sync with
	// BinaryExpression#printExpressionNoParenthesis and
	// OperatorExpression#printExpression
	if (this.referencesTable == null) {
		return super.printExpressionNoParenthesis(indent, output);
	}
	String operatorString = operatorToString();
	for (int i = this.arity - 1; i >= 0; i--) {
		output.append('(');
	}
	output = this.referencesTable[0].left.
		printExpression(indent, output);
	for (int i = 0, end = this.arity;
				i < end; i++) {
		output.append(' ').append(operatorString).append(' ');
		output = this.referencesTable[i].right.
			printExpression(0, output);
		output.append(')');
	}
	output.append(' ').append(operatorString).append(' ');
	return this.right.printExpression(0, output);
}

@Override
public TypeBinding resolveType(BlockScope scope) {
	// keep implementation in sync with BinaryExpression#resolveType
	if (this.referencesTable == null) {
		return super.resolveType(scope);
	}
	BinaryExpression cursor;
	if ((cursor = this.referencesTable[0]).left instanceof CastExpression) {
		cursor.left.bits |= ASTNode.DisableUnnecessaryCastCheck;
			// will check later on
	}
	cursor.left.resolveType(scope);
	for (int i = 0, end = this.arity; i < end; i ++) {
		this.referencesTable[i].nonRecursiveResolveTypeUpwards(scope);
	}
	nonRecursiveResolveTypeUpwards(scope);
	return this.resolvedType;
}

@Override
public void traverse(ASTVisitor visitor, BlockScope scope) {
	if (this.referencesTable == null) {
		super.traverse(visitor, scope);
	} else {
		if (visitor.visit(this, scope)) {
			int restart;
			for (restart = this.arity - 1;
					restart >= 0;
					restart--) {
				if (!visitor.visit(
						this.referencesTable[restart], scope)) {
					visitor.endVisit(
						this.referencesTable[restart], scope);
					break;
				}
			}
			restart++;
			// restart now points to the deepest BE for which
			// visit returned true, if any
			if (restart == 0) {
				this.referencesTable[0].left.traverse(visitor, scope);
			}
			for (int i = restart, end = this.arity;
						i < end; i++) {
				this.referencesTable[i].right.traverse(visitor, scope);
				visitor.endVisit(this.referencesTable[i], scope);
			}
			this.right.traverse(visitor, scope);
		}
		visitor.endVisit(this, scope);
	}
}

/**
 * Change {@link #arityMax} if and as needed. The current policy is to double
 * arityMax each time this method is called, until it reaches
 * {@link #ARITY_MAX_MAX}. Other policies may consider incrementing it less
 * agressively. Call only after an appropriate value has been assigned to
 * {@link #left}.
 */
// more sophisticate increment policies would leverage the leftmost expression
// to hold an indication of the number of uses of a given arityMax in a row
public void tuneArityMax() {
	if (this.arityMax < ARITY_MAX_MAX) {
		this.arityMax *= 2;
	}
}
}
