/*******************************************************************************
 * Copyright (c) 2000, 2004 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials 
 * are made available under the terms of the Common Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.compiler.ast;

import org.eclipse.jdt.internal.compiler.ASTVisitor;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.codegen.CodeStream;
import org.eclipse.jdt.internal.compiler.codegen.Label;
import org.eclipse.jdt.internal.compiler.flow.FlowContext;
import org.eclipse.jdt.internal.compiler.flow.FlowInfo;
import org.eclipse.jdt.internal.compiler.flow.LoopingFlowContext;
import org.eclipse.jdt.internal.compiler.lookup.ArrayBinding;
import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.eclipse.jdt.internal.compiler.lookup.LocalVariableBinding;
import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.ParameterizedTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeConstants;

public class ForeachStatement extends Statement {
    
	public LocalDeclaration elementVariable;
	public int elementVariableImplicitWidening = -1; 
	public Expression collection;
	public Statement action;
	
	// set the kind of foreach
	private int kind;
	// possible kinds of iterating behavior
	private static final int ARRAY = 0;
	private static final int RAW_ITERABLE = 1;
	private static final int GENERIC_ITERABLE = 2;

	private TypeBinding collectionElementType;

	// loop labels
	private Label breakLabel;
	private Label continueLabel;
	
	public BlockScope scope;

	// secret variables for codegen
	public LocalVariableBinding indexVariable;
	public LocalVariableBinding collectionVariable;	// to store the collection expression value
	public LocalVariableBinding maxVariable;
	// secret variable names
	private static final char[] SecretIndexVariableName = " index".toCharArray(); //$NON-NLS-1$
	private static final char[] SecretCollectionVariableName = " collection".toCharArray(); //$NON-NLS-1$
	private static final char[] SecretMaxVariableName = " max".toCharArray(); //$NON-NLS-1$
	
	int postCollectionInitStateIndex = -1;
	int mergedInitStateIndex = -1;
	
	public ForeachStatement(
		LocalDeclaration elementVariable,
		Expression collection,
		int start) {

		this.elementVariable = elementVariable;
		this.collection = collection;
		this.sourceStart = start;
		this.kind = -1;
	}

	public FlowInfo analyseCode(
		BlockScope currentScope,
		FlowContext flowContext,
		FlowInfo flowInfo) {
		// initialize break and continue labels
		breakLabel = new Label();
		continueLabel = new Label();

		// process the element variable and collection
		flowInfo = this.elementVariable.analyseCode(scope, flowContext, flowInfo);
		FlowInfo condInfo = flowInfo.copy().unconditionalInits().discardNullRelatedInitializations();
		condInfo = this.collection.analyseCode(scope, flowContext, condInfo);

		// element variable will be assigned when iterating
		condInfo.markAsDefinitelyAssigned(this.elementVariable.binding);

		this.postCollectionInitStateIndex = currentScope.methodScope().recordInitializationStates(condInfo);
		
		// process the action
		LoopingFlowContext loopingContext = new LoopingFlowContext(flowContext, this, breakLabel, continueLabel, scope);
		FlowInfo actionInfo = condInfo.initsWhenTrue().copy();
		FlowInfo exitBranch;
		if (!(action == null || (action.isEmptyBlock() 
		        	&& currentScope.environment().options.complianceLevel <= ClassFileConstants.JDK1_3))) {

			if (!this.action.complainIfUnreachable(actionInfo, scope, false)) {
				actionInfo = action.analyseCode(scope, loopingContext, actionInfo);
			}

			// code generation can be optimized when no need to continue in the loop
			exitBranch = condInfo.initsWhenFalse();
			exitBranch.addInitializationsFrom(flowInfo); // recover null inits from before condition analysis			
			if (!actionInfo.isReachable() && !loopingContext.initsOnContinue.isReachable()) {
				continueLabel = null;
			} else {
				actionInfo = actionInfo.mergedWith(loopingContext.initsOnContinue.unconditionalInits());
				loopingContext.complainOnDeferredChecks(scope, actionInfo);
				exitBranch.addPotentialInitializationsFrom(actionInfo.unconditionalInits());
			}
		} else {
			exitBranch = condInfo.initsWhenFalse();
		}

		// we need the variable to iterate the collection even if the 
		// element variable is not used
		if (!(this.action == null
				|| this.action.isEmptyBlock()
				|| ((this.action.bits & IsUsefulEmptyStatementMASK) != 0))) {
			switch(this.kind) {
				case ARRAY :
					this.collectionVariable.useFlag = LocalVariableBinding.USED;
					this.indexVariable.useFlag = LocalVariableBinding.USED;
					this.maxVariable.useFlag = LocalVariableBinding.USED;
					break;
				case RAW_ITERABLE :
				case GENERIC_ITERABLE :
					this.indexVariable.useFlag = LocalVariableBinding.USED;
					break;
			}
		}
		//end of loop
		FlowInfo mergedInfo = FlowInfo.mergedOptimizedBranches(
				loopingContext.initsOnBreak, 
				false, 
				exitBranch, 
				false, 
				true /*for(;;){}while(true); unreachable(); */);
		mergedInitStateIndex = currentScope.methodScope().recordInitializationStates(mergedInfo);
		return mergedInfo;
	}

	/**
	 * For statement code generation
	 *
	 * @param currentScope org.eclipse.jdt.internal.compiler.lookup.BlockScope
	 * @param codeStream org.eclipse.jdt.internal.compiler.codegen.CodeStream
	 */
	public void generateCode(BlockScope currentScope, CodeStream codeStream) {
	    
		if ((bits & IsReachableMASK) == 0) {
			return;
		}
		int pc = codeStream.position;
		if (this.action == null
				|| this.action.isEmptyBlock()
				|| ((this.action.bits & IsUsefulEmptyStatementMASK) != 0)) {
			codeStream.exitUserScope(scope);
			if (mergedInitStateIndex != -1) {
				codeStream.removeNotDefinitelyAssignedVariables(currentScope, mergedInitStateIndex);
				codeStream.addDefinitelyAssignedVariables(currentScope, mergedInitStateIndex);				
			}
			codeStream.recordPositionsFrom(pc, this.sourceStart);
			return;
		}
		// generate the initializations
		switch(this.kind) {
			case ARRAY :
				collection.generateCode(scope, codeStream, true);
				codeStream.store(this.collectionVariable, false);
				codeStream.iconst_0();
				codeStream.store(this.indexVariable, false);
				codeStream.load(this.collectionVariable);
				codeStream.arraylength();
				codeStream.store(this.maxVariable, false);
				break;
			case RAW_ITERABLE :
			case GENERIC_ITERABLE :
				collection.generateCode(scope, codeStream, true);
				// declaringClass.iterator();
				final TypeBinding collectionTypeBinding = collection.resolvedType;
				MethodBinding iteratorMethodBinding =
					new MethodBinding(
							AccPublic,
							"iterator".toCharArray(),//$NON-NLS-1$
							scope.getJavaUtilIterator(),
							TypeConstants.NoParameters,
							TypeConstants.NoExceptions,
							(ReferenceBinding) collectionTypeBinding);
				if (collectionTypeBinding.isInterface()) {
					codeStream.invokeinterface(iteratorMethodBinding);
				} else {
					codeStream.invokevirtual(iteratorMethodBinding);
				}
				codeStream.store(this.indexVariable, false);
				break;
		}
		
		// label management
		Label actionLabel = new Label(codeStream);
		Label conditionLabel = new Label(codeStream);
		breakLabel.initialize(codeStream);
		if (this.continueLabel != null) {
			this.continueLabel.initialize(codeStream);
		}
		// jump over the actionBlock
		codeStream.goto_(conditionLabel);

		// generate the loop action
		actionLabel.place();

		// generate the loop action
		if (this.elementVariable.binding.resolvedPosition != -1) {
			switch(this.kind) {
				case ARRAY :
					codeStream.load(this.collectionVariable);
					codeStream.load(this.indexVariable);
					codeStream.arrayAt(this.collectionElementType.id);
					if (this.elementVariableImplicitWidening != -1) {
						codeStream.generateImplicitConversion(this.elementVariableImplicitWidening);
					}
					codeStream.store(this.elementVariable.binding, false);
					break;
				case RAW_ITERABLE :
				case GENERIC_ITERABLE :
					codeStream.load(this.indexVariable);
					codeStream.invokeJavaUtilIteratorNext();
					if (this.elementVariable.binding.type.id != T_JavaLangObject) {
						if (this.elementVariableImplicitWidening != -1) {
							codeStream.checkcast(this.collectionElementType);
							codeStream.generateImplicitConversion(this.elementVariableImplicitWidening);
						} else {
							codeStream.checkcast(this.elementVariable.binding.type);
						}
					}
					codeStream.store(this.elementVariable.binding, false);
					break;
			}
			codeStream.addVisibleLocalVariable(this.elementVariable.binding);
			if (this.postCollectionInitStateIndex != -1) {
				codeStream.addDefinitelyAssignedVariables(
					currentScope,
					this.postCollectionInitStateIndex);
			}
		}
		this.action.generateCode(scope, codeStream);

		// continuation point
		int continuationPC = codeStream.position;
		if (this.continueLabel != null) {
			this.continueLabel.place();
			// generate the increments for next iteration
			switch(this.kind) {
				case ARRAY :
					codeStream.iinc(this.indexVariable.resolvedPosition, 1);
					break;
				case RAW_ITERABLE :
				case GENERIC_ITERABLE :
					break;
			}
		}
		// generate the condition
		conditionLabel.place();
		if (this.postCollectionInitStateIndex != -1) {
			codeStream.removeNotDefinitelyAssignedVariables(currentScope, postCollectionInitStateIndex);
		}
		switch(this.kind) {
			case ARRAY :
				codeStream.load(this.indexVariable);
				codeStream.load(this.maxVariable);
				codeStream.if_icmplt(actionLabel);
				break;
			case RAW_ITERABLE :
			case GENERIC_ITERABLE :
				codeStream.load(this.indexVariable);
				codeStream.invokeJavaUtilIteratorHasNext();
				codeStream.ifne(actionLabel);
				break;
		}
		codeStream.recordPositionsFrom(continuationPC, this.elementVariable.sourceStart);

		breakLabel.place();
		codeStream.exitUserScope(scope);
		if (mergedInitStateIndex != -1) {
			codeStream.removeNotDefinitelyAssignedVariables(currentScope, mergedInitStateIndex);
			codeStream.addDefinitelyAssignedVariables(currentScope, mergedInitStateIndex);			
		}
		codeStream.recordPositionsFrom(pc, this.sourceStart);
	}

	public StringBuffer printStatement(int tab, StringBuffer output) {

		printIndent(tab, output).append("for ("); //$NON-NLS-1$
		this.elementVariable.print(0, output); 
		output.append(" : ");//$NON-NLS-1$
		this.collection.print(0, output).append(") "); //$NON-NLS-1$
		//block
		if (this.action == null) {
			output.append(';');
		} else {
			output.append('\n');
			this.action.printStatement(tab + 1, output); //$NON-NLS-1$
		}
		return output;
	}

	public void resolve(BlockScope upperScope) {
		// use the scope that will hold the init declarations
		scope = new BlockScope(upperScope);
		this.elementVariable.resolve(scope); // collection expression can see itemVariable
		TypeBinding elementType = this.elementVariable.type.resolvedType;
		TypeBinding collectionType = this.collection.resolveType(scope);
		this.collection.computeConversion(scope, collectionType, collectionType);
		boolean hasError = elementType == null || collectionType == null;

		if (!hasError) {
			if (collectionType.isArrayType()) { // for(E e : E[])
				this.kind = ARRAY;
				this.collectionElementType = ((ArrayBinding) collectionType).elementsType();
				if (!collectionElementType.isCompatibleWith(elementType)
						&& !scope.isBoxingCompatibleWith(collectionElementType, elementType)) {
					scope.problemReporter().notCompatibleTypesErrorInForeach(collection, collectionElementType, elementType);
				}
				// in case we need to do a conversion
				int compileTimeTypeID = collectionElementType.id;
				if (elementType.isBaseType()) {
					if (!collectionElementType.isBaseType()) {
						compileTimeTypeID = scope.environment().computeBoxingType(collectionElementType).id;
						this.elementVariableImplicitWidening = UNBOXING;
						if (elementType.isBaseType()) {
							this.elementVariableImplicitWidening |= (elementType.id << 4) + compileTimeTypeID;
						}
					} else {
						this.elementVariableImplicitWidening = (elementType.id << 4) + compileTimeTypeID;
					}
				} else {
					if (collectionElementType.isBaseType()) {
						int boxedID = scope.environment().computeBoxingType(collectionElementType).id;
						this.elementVariableImplicitWidening = BOXING | (compileTimeTypeID << 4) | compileTimeTypeID; // use primitive type in implicit conversion
						compileTimeTypeID = boxedID;
					}
				}
			} else if (collectionType instanceof ReferenceBinding) {
			    ReferenceBinding iterableType = ((ReferenceBinding)collectionType).findSuperTypeErasingTo(T_JavaLangIterable, false /*Iterable is not a class*/);
			    if (iterableType != null) {
				    if (iterableType.isParameterizedType()) { // for(E e : Iterable<E>)
					    ParameterizedTypeBinding parameterizedType = (ParameterizedTypeBinding)iterableType;
						if (parameterizedType.arguments.length == 1) { // per construction can only be one
							this.kind = GENERIC_ITERABLE;
							this.collectionElementType = parameterizedType.arguments[0]; 
							if (!collectionElementType.isCompatibleWith(elementType)
									&& !scope.isBoxingCompatibleWith(collectionElementType, elementType)) {
								scope.problemReporter().notCompatibleTypesErrorInForeach(collection, collectionElementType, elementType);
							}
							int compileTimeTypeID = collectionElementType.id;
							// no conversion needed as only for reference types
							if (elementType.isBaseType()) {
								if (!collectionElementType.isBaseType()) {
									compileTimeTypeID = scope.environment().computeBoxingType(collectionElementType).id;
									this.elementVariableImplicitWidening = UNBOXING;
									if (elementType.isBaseType()) {
										this.elementVariableImplicitWidening |= (elementType.id << 4) + compileTimeTypeID;
									}
								} else {
									this.elementVariableImplicitWidening = (elementType.id << 4) + compileTimeTypeID;
								}
							} else {
								if (collectionElementType.isBaseType()) {
									int boxedID = scope.environment().computeBoxingType(collectionElementType).id;
									this.elementVariableImplicitWidening = BOXING | (compileTimeTypeID << 4) | compileTimeTypeID; // use primitive type in implicit conversion
									compileTimeTypeID = boxedID;
								}
							}
						}
				    } else if (iterableType.isGenericType()) { // for (T t : Iterable<T>) - in case used inside Iterable itself
						if (iterableType.typeVariables().length == 1) {
							this.kind = GENERIC_ITERABLE;
							this.collectionElementType = iterableType.typeVariables()[0]; 
							if (!collectionElementType.isCompatibleWith(elementType)
									&& !scope.isBoxingCompatibleWith(collectionElementType, elementType)) {
								scope.problemReporter().notCompatibleTypesErrorInForeach(collection, collectionElementType, elementType);
							}
							int compileTimeTypeID = collectionElementType.id;
							// no conversion needed as only for reference types
							if (elementType.isBaseType()) {
								if (!collectionElementType.isBaseType()) {
									compileTimeTypeID = scope.environment().computeBoxingType(collectionElementType).id;
									this.elementVariableImplicitWidening = UNBOXING;
									if (elementType.isBaseType()) {
										this.elementVariableImplicitWidening |= (elementType.id << 4) + compileTimeTypeID;
									}
								} else {
									this.elementVariableImplicitWidening = (elementType.id << 4) + compileTimeTypeID;
								}
							} else {
								if (collectionElementType.isBaseType()) {
									int boxedID = scope.environment().computeBoxingType(collectionElementType).id;
									this.elementVariableImplicitWidening = BOXING | (compileTimeTypeID << 4) | compileTimeTypeID; // use primitive type in implicit conversion
									compileTimeTypeID = boxedID;
								}
							}
						}
					} else if (iterableType.isRawType()) { // for(Object o : Iterable)
						this.kind = RAW_ITERABLE;
						this.collectionElementType = scope.getJavaLangObject();
						if (!collectionElementType.isCompatibleWith(elementType)
								&& !scope.isBoxingCompatibleWith(collectionElementType, elementType)) {
							scope.problemReporter().notCompatibleTypesErrorInForeach(collection, collectionElementType, elementType);
						}
						// no conversion needed as only for reference types
					}			    
			    }
			}
			switch(this.kind) {
				case ARRAY :
					// allocate #index secret variable (of type int)
					this.indexVariable = new LocalVariableBinding(SecretIndexVariableName, IntBinding, AccDefault, false);
					scope.addLocalVariable(this.indexVariable);
					this.indexVariable.setConstant(NotAConstant); // not inlinable
					
					// allocate #max secret variable
					this.maxVariable = new LocalVariableBinding(SecretMaxVariableName, IntBinding, AccDefault, false);
					scope.addLocalVariable(this.maxVariable);
					this.maxVariable.setConstant(NotAConstant); // not inlinable
					// add #array secret variable (of collection type)
					this.collectionVariable = new LocalVariableBinding(SecretCollectionVariableName, collectionType, AccDefault, false);
					scope.addLocalVariable(this.collectionVariable);
					this.collectionVariable.setConstant(NotAConstant); // not inlinable
					break;
				case RAW_ITERABLE :
				case GENERIC_ITERABLE :
					// allocate #index secret variable (of type Iterator)
					this.indexVariable = new LocalVariableBinding(SecretIndexVariableName, scope.getJavaUtilIterator(), AccDefault, false);
					scope.addLocalVariable(this.indexVariable);
					this.indexVariable.setConstant(NotAConstant); // not inlinable
					break;
				default :
					scope.problemReporter().invalidTypeForCollection(collection);
			}
		}
		if (action != null) {
			action.resolve(scope);
		}
	}
	
	public void traverse(
		ASTVisitor visitor,
		BlockScope blockScope) {

		if (visitor.visit(this, blockScope)) {
			this.elementVariable.traverse(visitor, scope);
			this.collection.traverse(visitor, scope);
			if (action != null) {
				action.traverse(visitor, scope);
			}
		}
		visitor.endVisit(this, blockScope);
	}
}