/***
 * ASM: a very small and fast Java bytecode manipulation framework
 * Copyright (c) 2000-2005 INRIA, France Telecom
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the copyright holders nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */
package clojure.asm.commons;

import clojure.asm.Label;
import clojure.asm.MethodAdapter;
import clojure.asm.MethodVisitor;
import clojure.asm.Opcodes;
import clojure.asm.Type;

/**
 * A {@link MethodAdapter} that renumbers local variables in their order of
 * appearance. This adapter allows one to easily add new local variables to a
 * method. It may be used by inheriting from this class, but the preferred way
 * of using it is via delegation: the next visitor in the chain can indeed add
 * new locals when needed by calling {@link #newLocal} on this adapter (this
 * requires a reference back to this {@link LocalVariablesSorter}).
 *
 * @author Chris Nokleberg
 * @author Eugene Kuleshov
 * @author Eric Bruneton
 */
public class LocalVariablesSorter extends MethodAdapter{

private final static Type OBJECT_TYPE = Type.getObjectType("java/lang/Object");

/**
 * Mapping from old to new local variable indexes. A local variable at index
 * i of size 1 is remapped to 'mapping[2*i]', while a local variable at
 * index i of size 2 is remapped to 'mapping[2*i+1]'.
 */
private int[] mapping = new int[40];

/**
 * Array used to store stack map local variable types after remapping.
 */
private Object[] newLocals = new Object[20];

/**
 * Index of the first local variable, after formal parameters.
 */
protected final int firstLocal;

/**
 * Index of the next local variable to be created by {@link #newLocal}.
 */
protected int nextLocal;

/**
 * Indicates if at least one local variable has moved due to remapping.
 */
private boolean changed;

/**
 * Creates a new {@link LocalVariablesSorter}.
 *
 * @param access access flags of the adapted method.
 * @param desc   the method's descriptor (see {@link Type Type}).
 * @param mv     the method visitor to which this adapter delegates calls.
 */
public LocalVariablesSorter(
		final int access,
		final String desc,
		final MethodVisitor mv){
	super(mv);
	Type[] args = Type.getArgumentTypes(desc);
	nextLocal = (Opcodes.ACC_STATIC & access) != 0 ? 0 : 1;
	for(int i = 0; i < args.length; i++)
		{
		nextLocal += args[i].getSize();
		}
	firstLocal = nextLocal;
}

public void visitVarInsn(final int opcode, final int var){
	Type type;
	switch(opcode)
		{
		case Opcodes.LLOAD:
		case Opcodes.LSTORE:
			type = Type.LONG_TYPE;
			break;

		case Opcodes.DLOAD:
		case Opcodes.DSTORE:
			type = Type.DOUBLE_TYPE;
			break;

		case Opcodes.FLOAD:
		case Opcodes.FSTORE:
			type = Type.FLOAT_TYPE;
			break;

		case Opcodes.ILOAD:
		case Opcodes.ISTORE:
			type = Type.INT_TYPE;
			break;

		case Opcodes.ALOAD:
		case Opcodes.ASTORE:
			type = OBJECT_TYPE;
			break;

			// case RET:
		default:
			type = Type.VOID_TYPE;
		}
	mv.visitVarInsn(opcode, remap(var, type));
}

public void visitIincInsn(final int var, final int increment){
	mv.visitIincInsn(remap(var, Type.INT_TYPE), increment);
}

public void visitMaxs(final int maxStack, final int maxLocals){
	mv.visitMaxs(maxStack, nextLocal);
}

public void visitLocalVariable(
		final String name,
		final String desc,
		final String signature,
		final Label start,
		final Label end,
		final int index){
	int size = "J".equals(desc) || "D".equals(desc) ? 2 : 1;
	int newIndex = remap(index, size);
	mv.visitLocalVariable(name, desc, signature, start, end, newIndex);
}

public void visitFrame(
		final int type,
		final int nLocal,
		final Object[] local,
		final int nStack,
		final Object[] stack){
	if(type != Opcodes.F_NEW)
		{ // uncompressed frame
		throw new IllegalStateException("ClassReader.accept() should be called with EXPAND_FRAMES flag");
		}

	if(!changed)
		{ // optimization for the case where mapping = identity
		mv.visitFrame(type, nLocal, local, nStack, stack);
		return;
		}

	// creates a copy of newLocals
	Object[] oldLocals = new Object[newLocals.length];
	System.arraycopy(newLocals, 0, oldLocals, 0, oldLocals.length);

	// copies types from 'local' to 'newLocals'
	// 'newLocals' already contains the variables added with 'newLocal'

	int index = 0; // old local variable index
	int number = 0; // old local variable number
	for(; number < nLocal; ++number)
		{
		Object t = local[number];
		int size = t == Opcodes.LONG || t == Opcodes.DOUBLE ? 2 : 1;
		if(t != Opcodes.TOP)
			{
			setFrameLocal(remap(index, size), t);
			}
		index += size;
		}

	// removes TOP after long and double types as well as trailing TOPs

	index = 0;
	number = 0;
	for(int i = 0; index < newLocals.length; ++i)
		{
		Object t = newLocals[index++];
		if(t != null && t != Opcodes.TOP)
			{
			newLocals[i] = t;
			number = i + 1;
			if(t == Opcodes.LONG || t == Opcodes.DOUBLE)
				{
				index += 1;
				}
			}
		else
			{
			newLocals[i] = Opcodes.TOP;
			}
		}

	// visits remapped frame
	mv.visitFrame(type, number, newLocals, nStack, stack);

	// restores original value of 'newLocals'
	newLocals = oldLocals;
}

// -------------

/**
 * Creates a new local variable of the given type.
 *
 * @param type the type of the local variable to be created.
 * @return the identifier of the newly created local variable.
 */
public int newLocal(final Type type){
	Object t;
	switch(type.getSort())
		{
		case Type.BOOLEAN:
		case Type.CHAR:
		case Type.BYTE:
		case Type.SHORT:
		case Type.INT:
			t = Opcodes.INTEGER;
			break;
		case Type.FLOAT:
			t = Opcodes.FLOAT;
			break;
		case Type.LONG:
			t = Opcodes.LONG;
			break;
		case Type.DOUBLE:
			t = Opcodes.DOUBLE;
			break;
		case Type.ARRAY:
			t = type.getDescriptor();
			break;
			// case Type.OBJECT:
		default:
			t = type.getInternalName();
			break;
		}
	int local = nextLocal;
	setLocalType(local, type);
	setFrameLocal(local, t);
	nextLocal += type.getSize();
	return local;
}

/**
 * Sets the current type of the given local variable. The default
 * implementation of this method does nothing.
 *
 * @param local a local variable identifier, as returned by {@link #newLocal
 *              newLocal()}.
 * @param type  the type of the value being stored in the local variable
 */
protected void setLocalType(final int local, final Type type){
}

private void setFrameLocal(final int local, final Object type){
	int l = newLocals.length;
	if(local >= l)
		{
		Object[] a = new Object[Math.max(2 * l, local + 1)];
		System.arraycopy(newLocals, 0, a, 0, l);
		newLocals = a;
		}
	newLocals[local] = type;
}

private int remap(final int var, final Type type){
	if(var < firstLocal)
		{
		return var;
		}
	int key = 2 * var + type.getSize() - 1;
	int size = mapping.length;
	if(key >= size)
		{
		int[] newMapping = new int[Math.max(2 * size, key + 1)];
		System.arraycopy(mapping, 0, newMapping, 0, size);
		mapping = newMapping;
		}
	int value = mapping[key];
	if(value == 0)
		{
		value = nextLocal + 1;
		mapping[key] = value;
		setLocalType(nextLocal, type);
		nextLocal += type.getSize();
		}
	if(value - 1 != var)
		{
		changed = true;
		}
	return value - 1;
}

private int remap(final int var, final int size){
	if(var < firstLocal || !changed)
		{
		return var;
		}
	int key = 2 * var + size - 1;
	int value = key < mapping.length ? mapping[key] : 0;
	if(value == 0)
		{
		throw new IllegalStateException("Unknown local variable " + var);
		}
	return value - 1;
}
}
