/*******************************************************************************
 * Copyright (c) 2000, 2015 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.compiler.parser;

import java.util.HashSet;
import java.util.Set;

import org.eclipse.jdt.core.compiler.*;
import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Argument;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.Block;
import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration;
import org.eclipse.jdt.internal.compiler.ast.ForeachStatement;
import org.eclipse.jdt.internal.compiler.ast.LocalDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Statement;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;

public class RecoveredBlock extends RecoveredStatement implements TerminalTokens {

	public Block blockDeclaration;
	public RecoveredStatement[] statements;
	public int statementCount;
	public boolean preserveContent = false;
	public RecoveredLocalVariable pendingArgument;

	int pendingModifiers;
	int pendingModifersSourceStart = -1;
	RecoveredAnnotation[] pendingAnnotations;
	int pendingAnnotationCount;

public RecoveredBlock(Block block, RecoveredElement parent, int bracketBalance){
	super(block, parent, bracketBalance);
	this.blockDeclaration = block;
	this.foundOpeningBrace = true;

	this.preserveContent = parser().methodRecoveryActivated || parser().statementRecoveryActivated;
}
@Override
public RecoveredElement add(AbstractMethodDeclaration methodDeclaration, int bracketBalanceValue) {
	if (this.parent != null && this.parent instanceof RecoveredMethod) {
		RecoveredMethod enclosingRecoveredMethod = (RecoveredMethod) this.parent;
		if (enclosingRecoveredMethod.methodBody == this && enclosingRecoveredMethod.parent == null) {
			resetPendingModifiers();
			// the element cannot be added because we are in the body of a top level method
			return this; // ignore this element
		}
	}
	return super.add(methodDeclaration, bracketBalanceValue);
}
/*
 * Record a nested block declaration
 */
@Override
public RecoveredElement add(Block nestedBlockDeclaration, int bracketBalanceValue) {
	resetPendingModifiers();

	/* do not consider a nested block starting passed the block end (if set)
		it must be belonging to an enclosing block */
	if (this.blockDeclaration.sourceEnd != 0
		&& nestedBlockDeclaration.sourceStart > this.blockDeclaration.sourceEnd){
		return this.parent.add(nestedBlockDeclaration, bracketBalanceValue);
	}

	RecoveredBlock element = new RecoveredBlock(nestedBlockDeclaration, this, bracketBalanceValue);

	// if we have a pending Argument, promote it into the new block
	if (this.pendingArgument != null){
		element.attach(this.pendingArgument);
		this.pendingArgument = null;
	}
	if(parser().statementRecoveryActivated) {
		addBlockStatement(element);
	}
	attach(element);
	if (nestedBlockDeclaration.sourceEnd == 0) return element;
	return this;
}
/*
 * Record a local declaration
 */
@Override
public RecoveredElement add(LocalDeclaration localDeclaration, int bracketBalanceValue) {
	return this.add(localDeclaration, bracketBalanceValue, false);
}
/*
 * Record a local declaration
 */
public RecoveredElement add(LocalDeclaration localDeclaration, int bracketBalanceValue, boolean delegatedByParent) {

	if (localDeclaration.isRecoveredFromLoneIdentifier()) {
		return this; // skip, the local will be mutated into an assignment and added later, see Parser.consumeLocalVariableDeclarationStatement
	}
	/* local variables inside method can only be final and non void */
/*
	char[][] localTypeName;
	if ((localDeclaration.modifiers & ~AccFinal) != 0 // local var can only be final
		|| (localDeclaration.type == null) // initializer
		|| ((localTypeName = localDeclaration.type.getTypeName()).length == 1 // non void
			&& CharOperation.equals(localTypeName[0], VoidBinding.sourceName()))){

		if (delegatedByParent){
			return this; //ignore
		} else {
			this.updateSourceEndIfNecessary(this.previousAvailableLineEnd(localDeclaration.declarationSourceStart - 1));
			return this.parent.add(localDeclaration, bracketBalance);
		}
	}
*/
		/* do not consider a local variable starting passed the block end (if set)
		it must be belonging to an enclosing block */
	if (this.blockDeclaration.sourceEnd != 0
			&& localDeclaration.declarationSourceStart > this.blockDeclaration.sourceEnd){
		resetPendingModifiers();
		if (delegatedByParent) return this; //ignore
		return this.parent.add(localDeclaration, bracketBalanceValue);
	}

	RecoveredLocalVariable element = new RecoveredLocalVariable(localDeclaration, this, bracketBalanceValue);

	if(this.pendingAnnotationCount > 0) {
		element.attach(
				this.pendingAnnotations,
				this.pendingAnnotationCount,
				this.pendingModifiers,
				this.pendingModifersSourceStart);
	}
	resetPendingModifiers();

	if (localDeclaration instanceof Argument){
		this.pendingArgument = element;
		return this;
	}

	attach(element);
	if (localDeclaration.declarationSourceEnd == 0) return element;
	return this;
}
/*
 * Record a statement declaration
 */
@Override
public RecoveredElement add(Statement stmt, int bracketBalanceValue) {
	return this.add(stmt, bracketBalanceValue, false);
}

/*
 * Record a statement declaration
 */
public RecoveredElement add(Statement stmt, int bracketBalanceValue, boolean delegatedByParent) {
	
	resetPendingModifiers();

	/* do not consider a nested block starting passed the block end (if set)
		it must be belonging to an enclosing block */
	if (this.blockDeclaration.sourceEnd != 0
			&& stmt.sourceStart > this.blockDeclaration.sourceEnd){
		if (delegatedByParent) return this; //ignore
		return this.parent.add(stmt, bracketBalanceValue);
	}

	RecoveredStatement element = new RecoveredStatement(stmt, this, bracketBalanceValue);
	attach(element);
	if (!isEndKnown(stmt)) return element;
	return this;
}
boolean isEndKnown(Statement stmt) {
	if (stmt instanceof ForeachStatement) {
		if (((ForeachStatement) stmt).action == null)
			return false;
	}
	return stmt.sourceEnd != 0;
}
/*
 * Addition of a type to an initializer (act like inside method body)
 */
@Override
public RecoveredElement add(TypeDeclaration typeDeclaration, int bracketBalanceValue) {
	return this.add(typeDeclaration, bracketBalanceValue, false);
}
/*
 * Addition of a type to an initializer (act like inside method body)
 */
public RecoveredElement add(TypeDeclaration typeDeclaration, int bracketBalanceValue, boolean delegatedByParent) {

	/* do not consider a type starting passed the block end (if set)
		it must be belonging to an enclosing block */
	if (this.blockDeclaration.sourceEnd != 0
			&& typeDeclaration.declarationSourceStart > this.blockDeclaration.sourceEnd){
		resetPendingModifiers();
		if (delegatedByParent) return this; //ignore
		return this.parent.add(typeDeclaration, bracketBalanceValue);
	}

	RecoveredType element = new RecoveredType(typeDeclaration, this, bracketBalanceValue);
	if(this.pendingAnnotationCount > 0) {
		element.attach(
				this.pendingAnnotations,
				this.pendingAnnotationCount,
				this.pendingModifiers,
				this.pendingModifersSourceStart);
	}
	resetPendingModifiers();
	attach(element);
	if (typeDeclaration.declarationSourceEnd == 0) return element;
	return this;
}
@Override
public RecoveredElement addAnnotationName(int identifierPtr, int identifierLengthPtr, int annotationStart, int bracketBalanceValue) {
	if (this.pendingAnnotations == null) {
		this.pendingAnnotations = new RecoveredAnnotation[5];
		this.pendingAnnotationCount = 0;
	} else {
		if (this.pendingAnnotationCount == this.pendingAnnotations.length) {
			System.arraycopy(
				this.pendingAnnotations,
				0,
				(this.pendingAnnotations = new RecoveredAnnotation[2 * this.pendingAnnotationCount]),
				0,
				this.pendingAnnotationCount);
		}
	}

	RecoveredAnnotation element = new RecoveredAnnotation(identifierPtr, identifierLengthPtr, annotationStart, this, bracketBalanceValue);

	this.pendingAnnotations[this.pendingAnnotationCount++] = element;

	return element;
}
@Override
public void addModifier(int flag, int modifiersSourceStart) {
	this.pendingModifiers |= flag;

	if (this.pendingModifersSourceStart < 0) {
		this.pendingModifersSourceStart = modifiersSourceStart;
	}
}
/*
 * Attach a recovered statement
 */
void attach(RecoveredStatement recoveredStatement) {

	if (this.statements == null) {
		this.statements = new RecoveredStatement[5];
		this.statementCount = 0;
	} else {
		if (this.statementCount == this.statements.length) {
			System.arraycopy(
				this.statements,
				0,
				(this.statements = new RecoveredStatement[2 * this.statementCount]),
				0,
				this.statementCount);
		}
	}
	this.statements[this.statementCount++] = recoveredStatement;
}
void attachPendingModifiers(RecoveredAnnotation[] pendingAnnots, int pendingAnnotCount, int pendingMods, int pendingModsSourceStart) {
	this.pendingAnnotations = pendingAnnots;
	this.pendingAnnotationCount = pendingAnnotCount;
	this.pendingModifiers = pendingMods;
	this.pendingModifersSourceStart = pendingModsSourceStart;
}
/*
 * Answer the associated parsed structure
 */
@Override
public ASTNode parseTree(){
	return this.blockDeclaration;
}
@Override
public void resetPendingModifiers() {
	this.pendingAnnotations = null;
	this.pendingAnnotationCount = 0;
	this.pendingModifiers = 0;
	this.pendingModifersSourceStart = -1;
}
@Override
public String toString(int tab) {
	StringBuffer result = new StringBuffer(tabString(tab));
	result.append("Recovered block:\n"); //$NON-NLS-1$
	this.blockDeclaration.print(tab + 1, result);
	if (this.statements != null) {
		for (int i = 0; i < this.statementCount; i++) {
			result.append("\n"); //$NON-NLS-1$
			result.append(this.statements[i].toString(tab + 1));
		}
	}
	return result.toString();
}
/*
 * Rebuild a block from the nested structure which is in scope
 */
public Block updatedBlock(int depth, Set<TypeDeclaration> knownTypes){

	// if block was not marked to be preserved or empty, then ignore it
	if (!this.preserveContent || this.statementCount == 0) return null;
	
	Statement[] updatedStatements = new Statement[this.statementCount];
	int updatedCount = 0;


	// may need to update the end of the last statement
	RecoveredStatement lastStatement = this.statements[this.statementCount - 1];
	RecoveredMethod enclosingMethod = enclosingMethod();
	RecoveredInitializer enclosingIntializer = enclosingInitializer();
	int bodyEndValue = 0;
	if(enclosingMethod != null) {
		bodyEndValue = enclosingMethod.methodDeclaration.bodyEnd;
		if(enclosingIntializer != null && enclosingMethod.methodDeclaration.sourceStart < enclosingIntializer.fieldDeclaration.sourceStart) {
			bodyEndValue = enclosingIntializer.fieldDeclaration.declarationSourceEnd;
		}
	} else if(enclosingIntializer != null) {
		bodyEndValue = enclosingIntializer.fieldDeclaration.declarationSourceEnd;
	} else {
		bodyEndValue = this.blockDeclaration.sourceEnd - 1;
	}

	if(lastStatement instanceof RecoveredLocalVariable) {
		RecoveredLocalVariable lastLocalVariable = (RecoveredLocalVariable) lastStatement;
		if(lastLocalVariable.localDeclaration.declarationSourceEnd == 0) {
			lastLocalVariable.localDeclaration.declarationSourceEnd = bodyEndValue;
			lastLocalVariable.localDeclaration.declarationEnd = bodyEndValue;
		}
	} else if(lastStatement instanceof RecoveredBlock) {
		RecoveredBlock lastBlock = (RecoveredBlock) lastStatement;
		if(lastBlock.blockDeclaration.sourceEnd == 0) {
			lastBlock.blockDeclaration.sourceEnd = bodyEndValue;
		}
	} else if(!(lastStatement instanceof RecoveredType)){
		if(lastStatement.statement.sourceEnd == 0) {
			lastStatement.statement.sourceEnd = bodyEndValue;
		}
	}

	int lastEnd = this.blockDeclaration.sourceStart;

	// only collect the non-null updated statements
	next:
	for (int i = 0; i < this.statementCount; i++){
		Statement updatedStatement = this.statements[i].updatedStatement(depth, knownTypes);
		if (updatedStatement != null) {
			for (int j = 0; j < i; j++) {
				if (updatedStatements[j] instanceof LocalDeclaration) {
					LocalDeclaration local = (LocalDeclaration) updatedStatements[j];
					if (local.initialization != null) {
						if (updatedStatement.sourceStart >= local.initialization.sourceStart && updatedStatement.sourceEnd <= local.initialization.sourceEnd)
							continue next;
					}
				}
			}
			updatedStatements[updatedCount++] = updatedStatement;
			
			if (updatedStatement instanceof LocalDeclaration) {
				LocalDeclaration localDeclaration = (LocalDeclaration) updatedStatement;
				if(localDeclaration.declarationSourceEnd > lastEnd) {
					lastEnd = localDeclaration.declarationSourceEnd;
				}
			} else if (updatedStatement instanceof TypeDeclaration) {
				TypeDeclaration typeDeclaration = (TypeDeclaration) updatedStatement;
				if(typeDeclaration.declarationSourceEnd > lastEnd) {
					lastEnd = typeDeclaration.declarationSourceEnd;
				}
			} else {
				if (updatedStatement.sourceEnd > lastEnd) {
					lastEnd = updatedStatement.sourceEnd;
				}
			}
		}
	}
	if (updatedCount == 0) return null; // not interesting block

	// resize statement collection if necessary
	if (updatedCount != this.statementCount){
		this.blockDeclaration.statements = new Statement[updatedCount];
		System.arraycopy(updatedStatements, 0, this.blockDeclaration.statements, 0, updatedCount);
	} else {
		this.blockDeclaration.statements = updatedStatements;
	}

	if (this.blockDeclaration.sourceEnd == 0) {
		if(lastEnd < bodyEndValue) {
			this.blockDeclaration.sourceEnd = bodyEndValue;
		} else {
			this.blockDeclaration.sourceEnd = lastEnd;
		}
	}

	return this.blockDeclaration;
}
/*
 * Rebuild a statement from the nested structure which is in scope
 */
@Override
public Statement updatedStatement(int depth, Set<TypeDeclaration> knownTypes){

	return updatedBlock(depth, knownTypes);
}
/*
 * A closing brace got consumed, might have closed the current element,
 * in which case both the currentElement is exited
 */
@Override
public RecoveredElement updateOnClosingBrace(int braceStart, int braceEnd){
	if ((--this.bracketBalance <= 0) && (this.parent != null)){
		this.updateSourceEndIfNecessary(braceStart, braceEnd);

		/* if the block is the method body, then it closes the method too */
		RecoveredMethod method = enclosingMethod();
		if (method != null && method.methodBody == this){
			return this.parent.updateOnClosingBrace(braceStart, braceEnd);
		}
		RecoveredInitializer initializer = enclosingInitializer();
		if (initializer != null && initializer.initializerBody == this){
			return this.parent.updateOnClosingBrace(braceStart, braceEnd);
		}
		return this.parent;
	}
	return this;
}
/*
 * An opening brace got consumed, might be the expected opening one of the current element,
 * in which case the bodyStart is updated.
 */
@Override
public RecoveredElement updateOnOpeningBrace(int braceStart, int braceEnd){

	// create a nested block
	Block block = new Block(0);
	block.sourceStart = parser().scanner.startPosition;
	return this.add(block, 1);
}
/*
 * Final update the corresponding parse node
 */
@Override
public void updateParseTree(){

	updatedBlock(0, new HashSet<TypeDeclaration>());
}
/*
 * Record a field declaration
 */
@Override
public RecoveredElement add(FieldDeclaration fieldDeclaration, int bracketBalanceValue) {
	resetPendingModifiers();

	/* local variables inside method can only be final and non void */
	char[][] fieldTypeName;
	if ((fieldDeclaration.modifiers & ~ClassFileConstants.AccFinal) != 0 // local var can only be final
		|| (fieldDeclaration.type == null) // initializer
		|| ((fieldTypeName = fieldDeclaration.type.getTypeName()).length == 1 // non void
			&& CharOperation.equals(fieldTypeName[0], TypeBinding.VOID.sourceName()))){
		this.updateSourceEndIfNecessary(previousAvailableLineEnd(fieldDeclaration.declarationSourceStart - 1));
		return this.parent.add(fieldDeclaration, bracketBalanceValue);
	}

	/* do not consider a local variable starting passed the block end (if set)
		it must be belonging to an enclosing block */
	if (this.blockDeclaration.sourceEnd != 0
		&& fieldDeclaration.declarationSourceStart > this.blockDeclaration.sourceEnd){
		return this.parent.add(fieldDeclaration, bracketBalanceValue);
	}

	// ignore the added field, since indicates a local variable behind recovery point
	// which thus got parsed as a field reference. This can happen if restarting after
	// having reduced an assistNode to get the following context (see 1GEK7SG)
	return this;
}
}
