/*******************************************************************************
 * Copyright (c) 2000, 2013 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 - Contributions for
 *								bug 319201 - [null] no warning when unboxing SingleNameReference causes NPE
 *								bug 345305 - [compiler][null] Compiler misidentifies a case of "variable can only be null"
 *								bug 403086 - [compiler][null] include the effect of 'assert' in syntactic null analysis for fields
 *								bug 403147 - [compiler][null] FUP of bug 400761: consolidate interaction between unboxing, NPE, and deferred checking
 *******************************************************************************/
package org.eclipse.jdt.internal.compiler.ast;

import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.codegen.*;
import org.eclipse.jdt.internal.compiler.flow.*;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.impl.Constant;
import org.eclipse.jdt.internal.compiler.lookup.*;
import org.eclipse.jdt.internal.compiler.ASTVisitor;

public class AssertStatement extends Statement {

	public Expression assertExpression, exceptionArgument;

	// for local variable attribute
	int preAssertInitStateIndex = -1;
	private FieldBinding assertionSyntheticFieldBinding;

public AssertStatement(	Expression exceptionArgument, Expression assertExpression, int startPosition) {
	this.assertExpression = assertExpression;
	this.exceptionArgument = exceptionArgument;
	this.sourceStart = startPosition;
	this.sourceEnd = exceptionArgument.sourceEnd;
}

public AssertStatement(Expression assertExpression, int startPosition) {
	this.assertExpression = assertExpression;
	this.sourceStart = startPosition;
	this.sourceEnd = assertExpression.sourceEnd;
}

@Override
public FlowInfo analyseCode(BlockScope currentScope, FlowContext flowContext, FlowInfo flowInfo) {
	this.preAssertInitStateIndex = currentScope.methodScope().recordInitializationStates(flowInfo);

	Constant cst = this.assertExpression.optimizedBooleanConstant();
	this.assertExpression.checkNPEbyUnboxing(currentScope, flowContext, flowInfo);
	boolean isOptimizedTrueAssertion = cst != Constant.NotAConstant && cst.booleanValue() == true;
	boolean isOptimizedFalseAssertion = cst != Constant.NotAConstant && cst.booleanValue() == false;
	
	flowContext.tagBits |= FlowContext.HIDE_NULL_COMPARISON_WARNING;
	FlowInfo conditionFlowInfo = this.assertExpression.analyseCode(currentScope, flowContext, flowInfo.copy());
	flowContext.extendTimeToLiveForNullCheckedField(1); // survive this assert as a Statement
	flowContext.tagBits &= ~FlowContext.HIDE_NULL_COMPARISON_WARNING;
	UnconditionalFlowInfo assertWhenTrueInfo = conditionFlowInfo.initsWhenTrue().unconditionalInits();
	FlowInfo assertInfo = conditionFlowInfo.initsWhenFalse();
	if (isOptimizedTrueAssertion) {
		assertInfo.setReachMode(FlowInfo.UNREACHABLE_OR_DEAD);
	}

	if (this.exceptionArgument != null) {
		// only gets evaluated when escaping - results are not taken into account
		FlowInfo exceptionInfo = this.exceptionArgument.analyseCode(currentScope, flowContext, assertInfo.copy());

		if (isOptimizedTrueAssertion){
			currentScope.problemReporter().fakeReachable(this.exceptionArgument);
		} else {
			flowContext.checkExceptionHandlers(
				currentScope.getJavaLangAssertionError(),
				this,
				exceptionInfo,
				currentScope);
		}
	}

	if (!isOptimizedTrueAssertion){
		// add the assert support in the clinit
		manageSyntheticAccessIfNecessary(currentScope, flowInfo);
	}
	// account for potential AssertionError:
	flowContext.recordAbruptExit();
	if (isOptimizedFalseAssertion) {
		return flowInfo; // if assertions are enabled, the following code will be unreachable
		// change this if we need to carry null analysis results of the assert
		// expression downstream
	} else {
		CompilerOptions compilerOptions = currentScope.compilerOptions();
		if (!compilerOptions.includeNullInfoFromAsserts) {
			// keep just the initializations info, don't include assert's null info
			// merge initialization info's and then add back the null info from flowInfo to
			// make sure that the empty null info of assertInfo doesnt change flowInfo's null info.
			return ((flowInfo.nullInfoLessUnconditionalCopy()).mergedWith(assertInfo.nullInfoLessUnconditionalCopy())).addNullInfoFrom(flowInfo);
		}
		return flowInfo.mergedWith(assertInfo.nullInfoLessUnconditionalCopy()).
			addInitializationsFrom(assertWhenTrueInfo.discardInitializationInfo());
		// keep the merge from the initial code for the definite assignment
		// analysis, tweak the null part to influence nulls downstream
	}
}

@Override
public void generateCode(BlockScope currentScope, CodeStream codeStream) {
	if ((this.bits & IsReachable) == 0) {
		return;
	}
	int pc = codeStream.position;

	if (this.assertionSyntheticFieldBinding != null) {
		BranchLabel assertionActivationLabel = new BranchLabel(codeStream);
		codeStream.fieldAccess(Opcodes.OPC_getstatic, this.assertionSyntheticFieldBinding, null /* default declaringClass */);
		codeStream.ifne(assertionActivationLabel);

		BranchLabel falseLabel;
		this.assertExpression.generateOptimizedBoolean(currentScope, codeStream, (falseLabel = new BranchLabel(codeStream)), null , true);
		codeStream.newJavaLangAssertionError();
		codeStream.dup();
		if (this.exceptionArgument != null) {
			this.exceptionArgument.generateCode(currentScope, codeStream, true);
			codeStream.invokeJavaLangAssertionErrorConstructor(this.exceptionArgument.implicitConversion & 0xF);
		} else {
			codeStream.invokeJavaLangAssertionErrorDefaultConstructor();
		}
		codeStream.athrow();

		// May loose some local variable initializations : affecting the local variable attributes
		if (this.preAssertInitStateIndex != -1) {
			codeStream.removeNotDefinitelyAssignedVariables(currentScope, this.preAssertInitStateIndex);
		}
		falseLabel.place();
		assertionActivationLabel.place();
	} else {
		// May loose some local variable initializations : affecting the local variable attributes
		if (this.preAssertInitStateIndex != -1) {
			codeStream.removeNotDefinitelyAssignedVariables(currentScope, this.preAssertInitStateIndex);
		}
	}
	codeStream.recordPositionsFrom(pc, this.sourceStart);
}

@Override
public void resolve(BlockScope scope) {
	this.assertExpression.resolveTypeExpecting(scope, TypeBinding.BOOLEAN);
	if (this.exceptionArgument != null) {
		TypeBinding exceptionArgumentType = this.exceptionArgument.resolveType(scope);
		if (exceptionArgumentType != null){
		    int id = exceptionArgumentType.id;
		    switch(id) {
				case T_void :
					scope.problemReporter().illegalVoidExpression(this.exceptionArgument);
					//$FALL-THROUGH$
				default:
				    id = T_JavaLangObject;
				//$FALL-THROUGH$
				case T_boolean :
				case T_byte :
				case T_char :
				case T_short :
				case T_double :
				case T_float :
				case T_int :
				case T_long :
				case T_JavaLangString :
					this.exceptionArgument.implicitConversion = (id << 4) + id;
			}
		}
	}
}

@Override
public void traverse(ASTVisitor visitor, BlockScope scope) {
	if (visitor.visit(this, scope)) {
		this.assertExpression.traverse(visitor, scope);
		if (this.exceptionArgument != null) {
			this.exceptionArgument.traverse(visitor, scope);
		}
	}
	visitor.endVisit(this, scope);
}

public void manageSyntheticAccessIfNecessary(BlockScope currentScope, FlowInfo flowInfo) {
	if ((flowInfo.tagBits & FlowInfo.UNREACHABLE_OR_DEAD) == 0) {
		// need assertion flag: $assertionsDisabled on outer most source clas
		// (in case of static member of interface, will use the outermost static member - bug 22334)
		SourceTypeBinding outerMostClass = currentScope.enclosingSourceType();
		while (outerMostClass.isLocalType()) {
			ReferenceBinding enclosing = outerMostClass.enclosingType();
			if (enclosing == null || enclosing.isInterface()) break;
			outerMostClass = (SourceTypeBinding) enclosing;
		}
		this.assertionSyntheticFieldBinding = outerMostClass.addSyntheticFieldForAssert(currentScope);

		// find <clinit> and enable assertion support
		TypeDeclaration typeDeclaration = outerMostClass.scope.referenceType();
		AbstractMethodDeclaration[] methods = typeDeclaration.methods;
		for (int i = 0, max = methods.length; i < max; i++) {
			AbstractMethodDeclaration method = methods[i];
			if (method.isClinit()) {
				((Clinit) method).setAssertionSupport(this.assertionSyntheticFieldBinding, currentScope.compilerOptions().sourceLevel < ClassFileConstants.JDK1_5);
				break;
			}
		}
	}
}

@Override
public StringBuffer printStatement(int tab, StringBuffer output) {
	printIndent(tab, output);
	output.append("assert "); //$NON-NLS-1$
	this.assertExpression.printExpression(0, output);
	if (this.exceptionArgument != null) {
		output.append(": "); //$NON-NLS-1$
		this.exceptionArgument.printExpression(0, output);
	}
	return output.append(';');
}
}
