/*******************************************************************************
 * Copyright (c) 2005, 2010 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.aspectj.org.eclipse.jdt.internal.compiler.codegen;

import java.text.MessageFormat;

import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.TypeIds;

public class StackMapFrame {
	public static final int USED = 1;
	public static final int SAME_FRAME = 0;
	public static final int CHOP_FRAME = 1;
	public static final int APPEND_FRAME = 2;
	public static final int SAME_FRAME_EXTENDED = 3;
	public static final int FULL_FRAME = 4;
	public static final int SAME_LOCALS_1_STACK_ITEMS = 5;
	public static final int SAME_LOCALS_1_STACK_ITEMS_EXTENDED = 6;

	public int pc;
	public int numberOfStackItems;
	private int numberOfLocals;
	public int localIndex;
	public VerificationTypeInfo[] locals;
	public VerificationTypeInfo[] stackItems;
	private int numberOfDifferentLocals = -1;
	public int tagBits;

public StackMapFrame(int initialLocalSize) {
	this.locals = new VerificationTypeInfo[initialLocalSize];
	this.numberOfLocals = -1;
	this.numberOfDifferentLocals = -1;
}
public int getFrameType(StackMapFrame prevFrame) {
	final int offsetDelta = getOffsetDelta(prevFrame);
	switch(this.numberOfStackItems) {
		case 0 :
			switch(numberOfDifferentLocals(prevFrame)) {
				case 0 :
					return offsetDelta <= 63 ? SAME_FRAME : SAME_FRAME_EXTENDED;
				case 1 :
				case 2 :
				case 3 :
					return APPEND_FRAME;
				case -1 :
				case -2 :
				case -3 :
					return CHOP_FRAME;
			}
			break;
		case 1 :
			switch(numberOfDifferentLocals(prevFrame)) {
				case 0 :
					return offsetDelta <= 63 ? SAME_LOCALS_1_STACK_ITEMS : SAME_LOCALS_1_STACK_ITEMS_EXTENDED;
			}
	}
	return FULL_FRAME;
}
public void addLocal(int resolvedPosition, VerificationTypeInfo info) {
	if (this.locals == null) {
		this.locals = new VerificationTypeInfo[resolvedPosition + 1];
		this.locals[resolvedPosition] = info;
	} else {
		final int length = this.locals.length;
		if (resolvedPosition >= length) {
			System.arraycopy(this.locals, 0, this.locals = new VerificationTypeInfo[resolvedPosition + 1], 0, length);
		}
		this.locals[resolvedPosition] = info;
	}
}
public void addStackItem(VerificationTypeInfo info) {
	if (info == null) {
		throw new IllegalArgumentException("info cannot be null"); //$NON-NLS-1$
	}
	if (this.stackItems == null) {
		this.stackItems = new VerificationTypeInfo[1];
		this.stackItems[0] = info;
		this.numberOfStackItems = 1;
	} else {
		final int length = this.stackItems.length;
		if (this.numberOfStackItems == length) {
			System.arraycopy(this.stackItems, 0, this.stackItems = new VerificationTypeInfo[length + 1], 0, length);
		}
		this.stackItems[this.numberOfStackItems++] = info;
	}
}
public void addStackItem(TypeBinding binding) {
	if (this.stackItems == null) {
		this.stackItems = new VerificationTypeInfo[1];
		this.stackItems[0] = new VerificationTypeInfo(binding);
		this.numberOfStackItems = 1;
	} else {
		final int length = this.stackItems.length;
		if (this.numberOfStackItems == length) {
			System.arraycopy(this.stackItems, 0, this.stackItems = new VerificationTypeInfo[length + 1], 0, length);
		}
		this.stackItems[this.numberOfStackItems++] = new VerificationTypeInfo(binding);
	}
}
public StackMapFrame duplicate() {
	int length = this.locals.length;
	StackMapFrame result = new StackMapFrame(length);
	result.numberOfLocals = -1;
	result.numberOfDifferentLocals = -1;
	result.pc = this.pc;
	result.numberOfStackItems = this.numberOfStackItems;

	if (length != 0) {
		result.locals = new VerificationTypeInfo[length];
		for (int i = 0; i < length; i++) {
			final VerificationTypeInfo verificationTypeInfo = this.locals[i];
			if (verificationTypeInfo != null) {
				result.locals[i] = verificationTypeInfo.duplicate();
			}
		}
	}
	length = this.numberOfStackItems;
	if (length != 0) {
		result.stackItems = new VerificationTypeInfo[length];
		for (int i = 0; i < length; i++) {
			result.stackItems[i] = this.stackItems[i].duplicate();
		}
	}
	return result;
}
public int numberOfDifferentLocals(StackMapFrame prevFrame) {
	if (this.numberOfDifferentLocals != -1) return this.numberOfDifferentLocals;
	if (prevFrame == null) {
		this.numberOfDifferentLocals = 0;
		return 0;
	}
	VerificationTypeInfo[] prevLocals = prevFrame.locals;
	VerificationTypeInfo[] currentLocals = this.locals;
	int prevLocalsLength = prevLocals == null ? 0 : prevLocals.length;
	int currentLocalsLength = currentLocals == null ? 0 : currentLocals.length;
	int prevNumberOfLocals = prevFrame.getNumberOfLocals();
	int currentNumberOfLocals = getNumberOfLocals();

	int result = 0;
	if (prevNumberOfLocals == 0) {
		if (currentNumberOfLocals != 0) {
			// need to check if there is a hole in the locals
			result = currentNumberOfLocals; // append if no hole and currentNumberOfLocals <= 3
			int counter = 0;
			for(int i = 0; i < currentLocalsLength && counter < currentNumberOfLocals; i++) {
				if (currentLocals[i] != null) {
					switch(currentLocals[i].id()) {
						case TypeIds.T_double :
						case TypeIds.T_long :
							i++;
					}
					counter++;
				} else {
					result = Integer.MAX_VALUE;
					this.numberOfDifferentLocals = result;
					return result;
				}
			}
		}
	} else if (currentNumberOfLocals == 0) {
		// need to check if there is a hole in the prev locals
		int counter = 0;
		result = -prevNumberOfLocals; // chop frame if no hole and prevNumberOfLocals <= 3
		for(int i = 0; i < prevLocalsLength && counter < prevNumberOfLocals; i++) {
			if (prevLocals[i] != null) {
				switch(prevLocals[i].id()) {
					case TypeIds.T_double :
					case TypeIds.T_long :
						i++;
				}
				counter++;
			} else {
				result = Integer.MAX_VALUE;
				this.numberOfDifferentLocals = result;
				return result;
			}
		}
	} else {
		// need to see if prevLocals matches with currentLocals
		int indexInPrevLocals = 0;
		int indexInCurrentLocals = 0;
		int currentLocalsCounter = 0;
		int prevLocalsCounter = 0;
		currentLocalsLoop: for (;indexInCurrentLocals < currentLocalsLength && currentLocalsCounter < currentNumberOfLocals; indexInCurrentLocals++) {
			VerificationTypeInfo currentLocal = currentLocals[indexInCurrentLocals];
			if (currentLocal != null) {
				currentLocalsCounter++;
				switch(currentLocal.id()) {
					case TypeIds.T_double :
					case TypeIds.T_long :
						indexInCurrentLocals++; // next entry  is null
				}
			}
			if (indexInPrevLocals < prevLocalsLength && prevLocalsCounter < prevNumberOfLocals) {
				VerificationTypeInfo prevLocal = prevLocals[indexInPrevLocals];
				if (prevLocal != null) {
					prevLocalsCounter++;
					switch(prevLocal.id()) {
						case TypeIds.T_double :
						case TypeIds.T_long :
							indexInPrevLocals++; // next entry  is null
					}
				}
				// now we need to check if prevLocal matches with currentLocal
				// the index must be the same
				if (equals(prevLocal, currentLocal) && indexInPrevLocals == indexInCurrentLocals) {
					if (result != 0) {
						result = Integer.MAX_VALUE;
						this.numberOfDifferentLocals = result;
						return result;
					}
				} else {
					// locals at the same location are not equals - this has to be a full frame
					result = Integer.MAX_VALUE;
					this.numberOfDifferentLocals = result;
					return result;
				}
				indexInPrevLocals++;
				continue currentLocalsLoop;
			}
			// process remaining current locals
			if (currentLocal != null) {
				result++;
			} else {
				result = Integer.MAX_VALUE;
				this.numberOfDifferentLocals = result;
				return result;
			}
			indexInCurrentLocals++;
			break currentLocalsLoop;
		}
		if (currentLocalsCounter < currentNumberOfLocals) {
			for(;indexInCurrentLocals < currentLocalsLength && currentLocalsCounter < currentNumberOfLocals; indexInCurrentLocals++) {
				VerificationTypeInfo currentLocal = currentLocals[indexInCurrentLocals];
				if (currentLocal == null) {
					result = Integer.MAX_VALUE;
					this.numberOfDifferentLocals = result;
					return result;
				}
				result++;
				currentLocalsCounter++;
				switch(currentLocal.id()) {
					case TypeIds.T_double :
					case TypeIds.T_long :
						indexInCurrentLocals++; // next entry  is null
				}
			}
		} else if (prevLocalsCounter < prevNumberOfLocals) {
			result = -result;
			// process possible remaining prev locals
			for(; indexInPrevLocals < prevLocalsLength && prevLocalsCounter < prevNumberOfLocals; indexInPrevLocals++) {
				VerificationTypeInfo prevLocal = prevLocals[indexInPrevLocals];
				if (prevLocal == null) {
					result = Integer.MAX_VALUE;
					this.numberOfDifferentLocals = result;
					return result;
				}
				result--;
				prevLocalsCounter++;
				switch(prevLocal.id()) {
					case TypeIds.T_double :
					case TypeIds.T_long :
						indexInPrevLocals++; // next entry  is null
				}
			}
		}
	}
	this.numberOfDifferentLocals = result;
	return result;
}
public int getNumberOfLocals() {
	if (this.numberOfLocals != -1) {
		return this.numberOfLocals;
	}
	int result = 0;
	final int length = this.locals == null ? 0 : this.locals.length;
	for(int i = 0; i < length; i++) {
		if (this.locals[i] != null) {
			switch(this.locals[i].id()) {
				case TypeIds.T_double :
				case TypeIds.T_long :
					i++;
			}
			result++;
		}
	}
	this.numberOfLocals = result;
	return result;
}
public int getOffsetDelta(StackMapFrame prevFrame) {
	if (prevFrame == null) return this.pc;
	return prevFrame.pc == -1 ? this.pc : this.pc - prevFrame.pc - 1;
}
@Override
public String toString() {
	StringBuffer buffer = new StringBuffer();
	printFrame(buffer, this);
	return String.valueOf(buffer);
}
private void printFrame(StringBuffer buffer, StackMapFrame frame) {
	String pattern = "[pc : {0} locals: {1} stack items: {2}\nlocals: {3}\nstack: {4}\n]"; //$NON-NLS-1$
	int localsLength = frame.locals == null ? 0 : frame.locals.length;
	buffer.append(MessageFormat.format(
		pattern,
		new Object[] {
			Integer.toString(frame.pc),
			Integer.toString(frame.getNumberOfLocals()),
			Integer.toString(frame.numberOfStackItems),
			print(frame.locals, localsLength),
			print(frame.stackItems, frame.numberOfStackItems)
		}
	));
}
private String print(VerificationTypeInfo[] infos, int length) {
	StringBuffer buffer = new StringBuffer();
	buffer.append('[');
	if (infos != null) {
		for (int i = 0; i < length; i++) {
			if (i != 0) buffer.append(',');
			VerificationTypeInfo verificationTypeInfo = infos[i];
			if (verificationTypeInfo == null) {
				buffer.append("top"); //$NON-NLS-1$
				continue;
			}
			buffer.append(verificationTypeInfo);
		}
	}
	buffer.append(']');
	return String.valueOf(buffer);
}
public void putLocal(int resolvedPosition, VerificationTypeInfo info) {
	if (this.locals == null) {
		this.locals = new VerificationTypeInfo[resolvedPosition + 1];
		this.locals[resolvedPosition] = info;
	} else {
		final int length = this.locals.length;
		if (resolvedPosition >= length) {
			System.arraycopy(this.locals, 0, this.locals = new VerificationTypeInfo[resolvedPosition + 1], 0, length);
		}
		this.locals[resolvedPosition] = info;
	}
}
public void replaceWithElementType() {
	VerificationTypeInfo info = this.stackItems[this.numberOfStackItems - 1];
	VerificationTypeInfo info2 = info.duplicate();
	info2.replaceWithElementType();
	this.stackItems[this.numberOfStackItems - 1] = info2;
}
public int getIndexOfDifferentLocals(int differentLocalsCount) {
	for (int i = this.locals.length - 1; i >= 0; i--) {
		VerificationTypeInfo currentLocal = this.locals[i];
		if (currentLocal == null) {
			// check the previous slot
			continue;
		} else {
			differentLocalsCount--;
		}
		if (differentLocalsCount == 0) {
			return i;
		}
	}
	return 0;
}
private boolean equals(VerificationTypeInfo info, VerificationTypeInfo info2) {
	if (info == null) {
		return info2 == null;
	}
	if (info2 == null) return false;
	return info.equals(info2);
}
}
