/*
 * Copyright (c) 2002-2011 LWJGL Project
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * 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.
 *
 * * Neither the name of 'LWJGL' 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 org.lwjgl.util.mapped;

import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLUtil;
import org.lwjgl.MemoryUtil;
import org.objectweb.asm.*;
import org.objectweb.asm.tree.*;
import org.objectweb.asm.tree.analysis.*;
import org.objectweb.asm.tree.analysis.Frame;
import org.objectweb.asm.util.TraceClassVisitor;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;

import static org.objectweb.asm.ClassWriter.*;
import static org.objectweb.asm.Opcodes.*;

/**
 * This class implements the bytecode transformation that mapped object go through.
 * Mapped object classes need to first be registered with the transformer, see {@link #register(Class)}.
 * <p/>
 * The transformer supports some debugging tools, enabled through JVM system properties:<br/>
 * org.lwjgl.util.mapped.PrintTiming=true, prints timing information for the transformation step.<br/>
 * org.lwjgl.util.mapped.PrintActivity=true, prints activity information.<br/>
 * org.lwjgl.util.mapped.PrintBytecode=true, prints the transformed bytecode.<br/>
 * org.lwjgl.util.Debug must also be set to true for the above to work.
 *
 * @author Riven
 */
public class MappedObjectTransformer {

	static final boolean PRINT_ACTIVITY = LWJGLUtil.DEBUG && LWJGLUtil.getPrivilegedBoolean("org.lwjgl.util.mapped.PrintActivity");
	static final boolean PRINT_TIMING   = PRINT_ACTIVITY && LWJGLUtil.getPrivilegedBoolean("org.lwjgl.util.mapped.PrintTiming");
	static final boolean PRINT_BYTECODE = LWJGLUtil.DEBUG && LWJGLUtil.getPrivilegedBoolean("org.lwjgl.util.mapped.PrintBytecode");

	static final Map<String, MappedSubtypeInfo> className_to_subtype;

	static final String MAPPED_OBJECT_JVM = jvmClassName(MappedObject.class);
	static final String MAPPED_HELPER_JVM = jvmClassName(MappedHelper.class);

	static final String MAPPEDSET_PREFIX = jvmClassName(MappedSet.class);
	static final String MAPPED_SET2_JVM  = jvmClassName(MappedSet2.class);
	static final String MAPPED_SET3_JVM  = jvmClassName(MappedSet3.class);
	static final String MAPPED_SET4_JVM  = jvmClassName(MappedSet4.class);

	static final String CACHE_LINE_PAD_JVM = "L" + jvmClassName(CacheLinePad.class) + ";";

	// Public methods
	static final String VIEWADDRESS_METHOD_NAME = "getViewAddress";
	static final String NEXT_METHOD_NAME        = "next";
	static final String ALIGN_METHOD_NAME       = "getAlign";
	static final String SIZEOF_METHOD_NAME      = "getSizeof";
	static final String CAPACITY_METHOD_NAME    = "capacity"; // Used for .asArray().length

	// Internal methods
	static final String VIEW_CONSTRUCTOR_NAME = "constructView$LWJGL"; // Used by runViewConstructor

	static final Map<Integer, String> OPCODE_TO_NAME   = new HashMap<Integer, String>();
	static final Map<Integer, String> INSNTYPE_TO_NAME = new HashMap<Integer, String>();

	static boolean is_currently_computing_frames;

	static {
		getClassEnums(Opcodes.class, OPCODE_TO_NAME, "V1_", "ACC_", "T_", "F_", "MH_");
		getClassEnums(AbstractInsnNode.class, INSNTYPE_TO_NAME);

		className_to_subtype = new HashMap<String, MappedSubtypeInfo>();

		{
			// HACK: required for mapped.view++
			//
			// because the compiler generates:
			// => GETFIELD MappedObject.view
			// => ICONST_1
			// => IADD
			// => PUTFIELD MyMappedType.view
			//
			// instead of:
			// => GETFIELD MyMappedType.view
			// => ICONST_1
			// => IADD
			// => PUTFIELD MyMappedType.view
			//
			className_to_subtype.put(MAPPED_OBJECT_JVM, new MappedSubtypeInfo(MAPPED_OBJECT_JVM, null, -1, -1, -1, false));
		}

		final String vmName = System.getProperty("java.vm.name");
		if ( vmName != null && !vmName.contains("Server") ) {
			System.err.println("Warning: " + MappedObject.class.getSimpleName() + "s have inferiour performance on Client VMs, please consider switching to a Server VM.");
		}
	}

	/**
	 * Registers a class as a mapped object.
	 * The class must extend {@link org.lwjgl.util.mapped.MappedObject} and be annotated with {@link org.lwjgl.util.mapped.MappedField}.
	 *
	 * @param type the mapped object class.
	 */
	public static void register(Class<? extends MappedObject> type) {
		if ( MappedObjectClassLoader.FORKED )
			return;

		final MappedType mapped = type.getAnnotation(MappedType.class);

		if ( mapped != null && mapped.padding() < 0 )
			throw new ClassFormatError("Invalid mapped type padding: " + mapped.padding());

		if ( type.getEnclosingClass() != null && !Modifier.isStatic(type.getModifiers()) )
			throw new InternalError("only top-level or static inner classes are allowed");

		final String className = jvmClassName(type);
		final Map<String, FieldInfo> fields = new HashMap<String, FieldInfo>();

		long sizeof = 0;
		for ( Field field : type.getDeclaredFields() ) {
			FieldInfo fieldInfo = registerField(mapped == null || mapped.autoGenerateOffsets(), className, sizeof, field);
			if ( fieldInfo == null )
				continue;

			fields.put(field.getName(), fieldInfo);

			sizeof = Math.max(sizeof, fieldInfo.offset + fieldInfo.lengthPadded);
		}

		int align = 4;
		int padding = 0;
		boolean cacheLinePadded = false;

		if ( mapped != null ) {
			align = mapped.align();
			if ( mapped.cacheLinePadding() ) {
				if ( mapped.padding() != 0 )
					throw new ClassFormatError("Mapped type padding cannot be specified together with cacheLinePadding.");

				final int cacheLineMod = (int)(sizeof % CacheUtil.getCacheLineSize());
				if ( cacheLineMod != 0 )
					padding = CacheUtil.getCacheLineSize() - cacheLineMod;

				cacheLinePadded = true;
			} else
				padding = mapped.padding();
		}

		sizeof += padding;

		final MappedSubtypeInfo mappedType = new MappedSubtypeInfo(className, fields, (int)sizeof, align, padding, cacheLinePadded);
		if ( className_to_subtype.put(className, mappedType) != null )
			throw new InternalError("duplicate mapped type: " + mappedType.className);
	}

	private static FieldInfo registerField(final boolean autoGenerateOffsets, final String className, long advancingOffset, final Field field) {
		if ( Modifier.isStatic(field.getModifiers()) ) // static fields are never mapped
			return null;

		// we only support primitives and ByteBuffers
		if ( !field.getType().isPrimitive() && field.getType() != ByteBuffer.class )
			throw new ClassFormatError("field '" + className + "." + field.getName() + "' not supported: " + field.getType());

		MappedField meta = field.getAnnotation(MappedField.class);
		if ( meta == null && !autoGenerateOffsets )
			throw new ClassFormatError("field '" + className + "." + field.getName() + "' missing annotation " + MappedField.class.getName() + ": " + className);

		Pointer pointer = field.getAnnotation(Pointer.class);
		if ( pointer != null && field.getType() != long.class )
			throw new ClassFormatError("The @Pointer annotation can only be used on long fields. @Pointer field found: " + className + "." + field.getName() + ": " + field.getType());

		if ( Modifier.isVolatile(field.getModifiers()) && (pointer != null || field.getType() == ByteBuffer.class) )
			throw new ClassFormatError("The volatile keyword is not supported for @Pointer or ByteBuffer fields. Volatile field found: " + className + "." + field.getName() + ": " + field.getType());

		// quick hack
		long byteLength;
		if ( field.getType() == long.class || field.getType() == double.class ) {
			if ( pointer == null )
				byteLength = 8;
			else
				byteLength = MappedObjectUnsafe.INSTANCE.addressSize();
		} else if ( field.getType() == double.class )
			byteLength = 8;
		else if ( field.getType() == int.class || field.getType() == float.class )
			byteLength = 4;
		else if ( field.getType() == char.class || field.getType() == short.class )
			byteLength = 2;
		else if ( field.getType() == byte.class )
			byteLength = 1;
		else if ( field.getType() == ByteBuffer.class ) {
			byteLength = meta.byteLength();
			if ( byteLength < 0 )
				throw new IllegalStateException("invalid byte length for mapped ByteBuffer field: " + className + "." + field.getName() + " [length=" + byteLength + "]");
		} else
			throw new ClassFormatError(field.getType().getName());

		if ( field.getType() != ByteBuffer.class && (advancingOffset % byteLength) != 0 )
			throw new IllegalStateException("misaligned mapped type: " + className + "." + field.getName());

		CacheLinePad pad = field.getAnnotation(CacheLinePad.class);

		long byteOffset = advancingOffset;
		if ( meta != null && meta.byteOffset() != -1 ) {
			if ( meta.byteOffset() < 0 )
				throw new ClassFormatError("Invalid field byte offset: " + className + "." + field.getName() + " [byteOffset=" + meta.byteOffset() + "]");
			if ( pad != null )
				throw new ClassFormatError("A field byte offset cannot be specified together with cache-line padding: " + className + "." + field.getName());

			byteOffset = meta.byteOffset();
		}

		long byteLengthPadded = byteLength;
		if ( pad != null ) {
			// Pad before
			if ( pad.before() && byteOffset % CacheUtil.getCacheLineSize() != 0 )
				byteOffset += CacheUtil.getCacheLineSize() - (byteOffset & (CacheUtil.getCacheLineSize() - 1));

			// Pad after
			if ( pad.after() && (byteOffset + byteLength) % CacheUtil.getCacheLineSize() != 0 )
				byteLengthPadded += CacheUtil.getCacheLineSize() - (byteOffset + byteLength) % CacheUtil.getCacheLineSize();

			assert !pad.before() || (byteOffset % CacheUtil.getCacheLineSize() == 0);
			assert !pad.after() || ((byteOffset + byteLengthPadded) % CacheUtil.getCacheLineSize() == 0);
		}

		if ( PRINT_ACTIVITY )
			LWJGLUtil.log(MappedObjectTransformer.class.getSimpleName() + ": " + className + "." + field.getName() + " [type=" + field.getType().getSimpleName() + ", offset=" + byteOffset + "]");

		return new FieldInfo(byteOffset, byteLength, byteLengthPadded, Type.getType(field.getType()), Modifier.isVolatile(field.getModifiers()), pointer != null);
	}

	/** Removes final from methods that will be overriden by subclasses. */
	static byte[] transformMappedObject(byte[] bytecode) {
		final ClassWriter cw = new ClassWriter(0);

		ClassVisitor cv = new ClassAdapter(cw) {

			private final String[] DEFINALIZE_LIST = {
				VIEWADDRESS_METHOD_NAME,
				NEXT_METHOD_NAME,
				ALIGN_METHOD_NAME,
				SIZEOF_METHOD_NAME,
				CAPACITY_METHOD_NAME,
			};

			public MethodVisitor visitMethod(int access, final String name, final String desc, final String signature, final String[] exceptions) {
				for ( String method : DEFINALIZE_LIST ) {
					if ( name.equals(method) ) {
						access &= ~ACC_FINAL;
						break;
					}
				}
				return super.visitMethod(access, name, desc, signature, exceptions);
			}
		};

		new ClassReader(bytecode).accept(cv, 0);
		return cw.toByteArray();
	}

	static byte[] transformMappedAPI(final String className, byte[] bytecode) {
		final ClassWriter cw = new ClassWriter(COMPUTE_FRAMES) {

			@Override
			protected String getCommonSuperClass(String a, String b) {
				// HACK: prevent user-code static-initialization-blocks to be executed
				if ( is_currently_computing_frames && !a.startsWith("java/") || !b.startsWith("java/") )
					return "java/lang/Object";

				return super.getCommonSuperClass(a, b);
			}

		};

		final TransformationAdapter ta = new TransformationAdapter(cw, className);

		ClassVisitor cv = ta;
		if ( className_to_subtype.containsKey(className) ) // Do a first pass to generate address getters
			cv = getMethodGenAdapter(className, cv);

		new ClassReader(bytecode).accept(cv, ClassReader.SKIP_FRAMES);

		if ( !ta.transformed )
			return bytecode;

		bytecode = cw.toByteArray();
		if ( PRINT_BYTECODE )
			printBytecode(bytecode);

		return bytecode;
	}

	private static ClassAdapter getMethodGenAdapter(final String className, final ClassVisitor cv) {
		return new ClassAdapter(cv) {

			@Override
			public void visitEnd() {
				final MappedSubtypeInfo mappedSubtype = className_to_subtype.get(className);

				generateViewAddressGetter();
				generateCapacity();
				generateAlignGetter(mappedSubtype);
				generateSizeofGetter();
				generateNext();

				for ( String fieldName : mappedSubtype.fields.keySet() ) {
					final FieldInfo field = mappedSubtype.fields.get(fieldName);

					if ( field.type.getDescriptor().length() > 1 ) {  // ByteBuffer, getter only
						generateByteBufferGetter(fieldName, field);
					} else {
						generateFieldGetter(fieldName, field);
						generateFieldSetter(fieldName, field);
					}
				}

				super.visitEnd();
			}

			private void generateViewAddressGetter() {
				MethodVisitor mv = super.visitMethod(ACC_PUBLIC, VIEWADDRESS_METHOD_NAME, "(I)J", null, null);
				mv.visitCode();
				mv.visitVarInsn(ALOAD, 0);
				mv.visitFieldInsn(GETFIELD, MAPPED_OBJECT_JVM, "baseAddress", "J");
				mv.visitVarInsn(ILOAD, 1);
				mv.visitFieldInsn(GETSTATIC, className, "SIZEOF", "I");
				mv.visitInsn(IMUL);
				mv.visitInsn(I2L);
				mv.visitInsn(LADD);
				if ( MappedObject.CHECKS ) {
					mv.visitInsn(DUP2);
					mv.visitVarInsn(ALOAD, 0);
					mv.visitMethodInsn(INVOKESTATIC, MAPPED_HELPER_JVM, "checkAddress", "(JL" + MAPPED_OBJECT_JVM + ";)V");
				}
				mv.visitInsn(LRETURN);
				mv.visitMaxs(3, 2);
				mv.visitEnd();
			}

			private void generateCapacity() {
				// return (backingByteBuffer().capacity() + (int)(MemoryUtil.getAddress0(backingByteBuffer()) - baseAddress)) / SIZEOF;
				MethodVisitor mv = super.visitMethod(ACC_PUBLIC, CAPACITY_METHOD_NAME, "()I", null, null);
				mv.visitCode();
				mv.visitVarInsn(ALOAD, 0);
				mv.visitMethodInsn(INVOKEVIRTUAL, MAPPED_OBJECT_JVM, "backingByteBuffer", "()L" + jvmClassName(ByteBuffer.class) + ";");
				mv.visitInsn(DUP);
				mv.visitMethodInsn(INVOKEVIRTUAL, jvmClassName(ByteBuffer.class), "capacity", "()I");
				mv.visitInsn(SWAP);
				mv.visitMethodInsn(INVOKESTATIC, jvmClassName(MemoryUtil.class), "getAddress0", "(L" + jvmClassName(Buffer.class) + ";)J");
				mv.visitVarInsn(ALOAD, 0);
				mv.visitFieldInsn(GETFIELD, MAPPED_OBJECT_JVM, "baseAddress", "J");
				mv.visitInsn(LSUB);
				mv.visitInsn(L2I);
				mv.visitInsn(IADD);
				mv.visitFieldInsn(GETSTATIC, className, "SIZEOF", "I");
				mv.visitInsn(IDIV);
				mv.visitInsn(IRETURN);
				mv.visitMaxs(3, 1);
				mv.visitEnd();
			}

			private void generateAlignGetter(final MappedSubtypeInfo mappedSubtype) {
				MethodVisitor mv = super.visitMethod(ACC_PUBLIC, ALIGN_METHOD_NAME, "()I", null, null);
				mv.visitCode();
				visitIntNode(mv, mappedSubtype.sizeof);
				mv.visitInsn(IRETURN);
				mv.visitMaxs(1, 1);
				mv.visitEnd();
			}

			private void generateSizeofGetter() {
				MethodVisitor mv = super.visitMethod(ACC_PUBLIC, SIZEOF_METHOD_NAME, "()I", null, null);
				mv.visitCode();
				mv.visitFieldInsn(GETSTATIC, className, "SIZEOF", "I");
				mv.visitInsn(IRETURN);
				mv.visitMaxs(1, 1);
				mv.visitEnd();
			}

			private void generateNext() {
				MethodVisitor mv = super.visitMethod(ACC_PUBLIC, NEXT_METHOD_NAME, "()V", null, null);
				mv.visitCode();
				mv.visitVarInsn(ALOAD, 0);
				mv.visitInsn(DUP);
				mv.visitFieldInsn(GETFIELD, MAPPED_OBJECT_JVM, "viewAddress", "J");
				mv.visitFieldInsn(GETSTATIC, className, "SIZEOF", "I");
				mv.visitInsn(I2L);
				mv.visitInsn(LADD);
				mv.visitMethodInsn(INVOKEVIRTUAL, className, "setViewAddress", "(J)V");
				mv.visitInsn(RETURN);
				mv.visitMaxs(3, 1);
				mv.visitEnd();
			}

			private void generateByteBufferGetter(final String fieldName, final FieldInfo field) {
				MethodVisitor mv = super.visitMethod(ACC_PUBLIC | ACC_STATIC, getterName(fieldName), "(L" + className + ";I)" + field.type.getDescriptor(), null, null);
				mv.visitCode();
				mv.visitVarInsn(ALOAD, 0);
				mv.visitVarInsn(ILOAD, 1);
				mv.visitMethodInsn(INVOKEVIRTUAL, className, VIEWADDRESS_METHOD_NAME, "(I)J");
				visitIntNode(mv, (int)field.offset);
				mv.visitInsn(I2L);
				mv.visitInsn(LADD);
				visitIntNode(mv, (int)field.length);
				mv.visitMethodInsn(INVOKESTATIC, MAPPED_HELPER_JVM, "newBuffer", "(JI)L" + jvmClassName(ByteBuffer.class) + ";");
				mv.visitInsn(ARETURN);
				mv.visitMaxs(3, 2);
				mv.visitEnd();
			}

			private void generateFieldGetter(final String fieldName, final FieldInfo field) {
				MethodVisitor mv = super.visitMethod(ACC_PUBLIC | ACC_STATIC, getterName(fieldName), "(L" + className + ";I)" + field.type.getDescriptor(), null, null);
				mv.visitCode();
				mv.visitVarInsn(ALOAD, 0);
				mv.visitVarInsn(ILOAD, 1);
				mv.visitMethodInsn(INVOKEVIRTUAL, className, VIEWADDRESS_METHOD_NAME, "(I)J");
				visitIntNode(mv, (int)field.offset);
				mv.visitInsn(I2L);
				mv.visitInsn(LADD);
				mv.visitMethodInsn(INVOKESTATIC, MAPPED_HELPER_JVM, field.getAccessType() + "get", "(J)" + field.type.getDescriptor());
				mv.visitInsn(field.type.getOpcode(IRETURN));
				mv.visitMaxs(3, 2);
				mv.visitEnd();
			}

			private void generateFieldSetter(final String fieldName, final FieldInfo field) {
				MethodVisitor mv = super.visitMethod(ACC_PUBLIC | ACC_STATIC, setterName(fieldName), "(L" + className + ";I" + field.type.getDescriptor() + ")V", null, null);
				mv.visitCode();
				int load = 0;
				switch ( field.type.getSort() ) {
					case Type.BOOLEAN:
					case Type.CHAR:
					case Type.BYTE:
					case Type.SHORT:
					case Type.INT:
						load = ILOAD;
						break;
					case Type.FLOAT:
						load = FLOAD;
						break;
					case Type.LONG:
						load = LLOAD;
						break;
					case Type.DOUBLE:
						load = DLOAD;
						break;
				}
				mv.visitVarInsn(load, 2);
				mv.visitVarInsn(ALOAD, 0);
				mv.visitVarInsn(ILOAD, 1);
				mv.visitMethodInsn(INVOKEVIRTUAL, className, VIEWADDRESS_METHOD_NAME, "(I)J");
				visitIntNode(mv, (int)field.offset);
				mv.visitInsn(I2L);
				mv.visitInsn(LADD);
				mv.visitMethodInsn(INVOKESTATIC, MAPPED_HELPER_JVM, field.getAccessType() + "put", "(" + field.type.getDescriptor() + "J)V");
				mv.visitInsn(RETURN);
				mv.visitMaxs(4, 4);
				mv.visitEnd();
			}

		};
	}

	private static class TransformationAdapter extends ClassAdapter {

		final String className;

		boolean transformed;

		TransformationAdapter(final ClassVisitor cv, final String className) {
			super(cv);
			this.className = className;
		}

		@Override
		public FieldVisitor visitField(final int access, final String name, final String desc, final String signature, final Object value) {
			// remove redirected fields
			final MappedSubtypeInfo mappedSubtype = className_to_subtype.get(className);
			if ( mappedSubtype != null && mappedSubtype.fields.containsKey(name) ) {
				if ( PRINT_ACTIVITY )
					LWJGLUtil.log(MappedObjectTransformer.class.getSimpleName() + ": discarding field: " + className + "." + name + ":" + desc);
				return null;
			}

			if ( (access & ACC_STATIC) == 0 ) {
				return new FieldNode(access, name, desc, signature, value) {
					public void visitEnd() {
						if ( visibleAnnotations == null ) { // early-out
							accept(cv);
							return;
						}

						boolean before = false;
						boolean after = false;
						int byteLength = 0;
						for ( AnnotationNode pad : visibleAnnotations ) {
							if ( CACHE_LINE_PAD_JVM.equals(pad.desc) ) {
								if ( "J".equals(desc) || "D".equals(desc) )
									byteLength = 8;
								else if ( "I".equals(desc) || "F".equals(desc) )
									byteLength = 4;
								else if ( "S".equals(desc) || "C".equals(desc) )
									byteLength = 2;
								else if ( "B".equals(desc) || "Z".equals(desc) )
									byteLength = 1;
								else
									throw new ClassFormatError("The @CacheLinePad annotation cannot be used on non-primitive fields: " + className + "." + name);

								transformed = true;

								after = true;
								if ( pad.values != null ) {
									for ( int i = 0; i < pad.values.size(); i += 2 ) {
										final boolean value = pad.values.get(i + 1).equals(Boolean.TRUE);
										if ( "before".equals(pad.values.get(i)) )
											before = value;
										else
											after = value;
									}
								}
								break;
							}
						}

						/*
							We make the fields public to force the JVM to keep the fields in the object.
							Instead of using only longs or integers, we use the same type as the original
							field. That's because modern JVMs usually reorder fields by type:
							longs, then doubles, then integers, then booleans, etc. This way it's more
							likely that the padding will work as expected.
						 */

						if ( before ) {
							final int count = CacheUtil.getCacheLineSize() / byteLength - 1;
							for ( int i = count; i >= 1; i-- )
								cv.visitField(access | ACC_PUBLIC | ACC_SYNTHETIC, name + "$PAD_" + i, desc, signature, null);
						}

						accept(cv);

						if ( after ) {
							final int count = CacheUtil.getCacheLineSize() / byteLength - 1;
							for ( int i = 1; i <= count; i++ )
								cv.visitField(access | ACC_PUBLIC | ACC_SYNTHETIC, name + "$PAD" + i, desc, signature, null);
						}
					}
				};
			} else
				return super.visitField(access, name, desc, signature, value);
		}

		@Override
		public MethodVisitor visitMethod(final int access, String name, final String desc, final String signature, final String[] exceptions) {
			// Move MappedSubtype constructors to another method
			if ( "<init>".equals(name) ) {
				final MappedSubtypeInfo mappedSubtype = className_to_subtype.get(className);
				if ( mappedSubtype != null ) {
					if ( !"()V".equals(desc) )
						throw new ClassFormatError(className + " can only have a default constructor, found: " + desc);

					final MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
					mv.visitVarInsn(ALOAD, 0);
					mv.visitMethodInsn(INVOKESPECIAL, MAPPED_OBJECT_JVM, "<init>", "()V");
					mv.visitInsn(RETURN);
					mv.visitMaxs(0, 0);

					// put the method body in another method
					name = VIEW_CONSTRUCTOR_NAME;
				}
			}

			final MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
			return new MethodNode(access, name, desc, signature, exceptions) {

				/** When true, the method has touched a mapped object and needs to be transformed. We track this
				 * so we can skip the expensive frame analysis and tree API usage. */
				boolean needsTransformation;

				@Override
				public void visitMaxs(int a, int b) {
					try {
						is_currently_computing_frames = true;
						super.visitMaxs(a, b);
					} finally {
						is_currently_computing_frames = false;
					}
				}

				@Override
				public void visitFieldInsn(final int opcode, final String owner, final String name, final String desc) {
					if ( className_to_subtype.containsKey(owner) || owner.startsWith(MAPPEDSET_PREFIX) )
						needsTransformation = true;

					super.visitFieldInsn(opcode, owner, name, desc);
				}

				@Override
				public void visitMethodInsn(final int opcode, final String owner, final String name, final String desc) {
					if ( className_to_subtype.containsKey(owner) )
						needsTransformation = true;

					super.visitMethodInsn(opcode, owner, name, desc);
				}

				@Override
				public void visitEnd() {
					if ( needsTransformation ) { // Early-out for methods that do not touch a mapped object.
						//System.err.println("\nTRANSFORMING: " + className + "." + name + desc);
						transformed = true;
						try {
							transformMethod(analyse());
						} catch (Exception e) {
							throw new RuntimeException(e);
						}
					}

					// Pass the instruction stream to the adapter's MethodVisitor
					accept(mv);
				}

				private Frame<BasicValue>[] analyse() throws AnalyzerException {
					final Analyzer<BasicValue> a = new Analyzer<BasicValue>(new SimpleVerifier());
					a.analyze(className, this);
					return a.getFrames();
				}

				private void transformMethod(final Frame<BasicValue>[] frames) {
					final InsnList instructions = this.instructions;

					final Map<Integer, MappedSubtypeInfo> arrayVars = new HashMap<Integer, MappedSubtypeInfo>();

					/*
					We need this map because we insert/remove instructions from the stream and we need a way
					to match each original instruction with the corresponding frame.
					TODO: Can we keep track of everything more efficiently without a map?
					 */
					final Map<AbstractInsnNode, Frame<BasicValue>> frameMap = new HashMap<AbstractInsnNode, Frame<BasicValue>>();
					for ( int i = 0; i < frames.length; i++ )
						frameMap.put(instructions.get(i), frames[i]);

					for ( int i = 0; i < instructions.size(); i++ ) { // f is a separate cursor for frames
						final AbstractInsnNode instruction = instructions.get(i);

						//System.out.println("MAIN LOOP #" + i + " - " + getOpcodeName(instruction));

						switch ( instruction.getType() ) {
							case AbstractInsnNode.VAR_INSN:
								if ( instruction.getOpcode() == ALOAD ) {
									VarInsnNode varInsn = (VarInsnNode)instruction;
									final MappedSubtypeInfo mappedSubtype = arrayVars.get(varInsn.var);
									if ( mappedSubtype != null )
										i = transformArrayAccess(instructions, i, frameMap, varInsn, mappedSubtype, varInsn.var);
								}
								break;
							case AbstractInsnNode.FIELD_INSN:
								FieldInsnNode fieldInsn = (FieldInsnNode)instruction;

								final InsnList list = transformFieldAccess(fieldInsn);
								if ( list != null )
									i = replace(instructions, i, instruction, list);

								break;
							case AbstractInsnNode.METHOD_INSN:
								MethodInsnNode methodInsn = (MethodInsnNode)instruction;
								final MappedSubtypeInfo mappedType = className_to_subtype.get(methodInsn.owner);
								if ( mappedType != null )
									i = transformMethodCall(instructions, i, frameMap, methodInsn, mappedType, arrayVars);
								break;
						}
					}
				}
			};
		}
	}

	static int transformMethodCall(final InsnList instructions, int i, final Map<AbstractInsnNode, Frame<BasicValue>> frameMap, final MethodInsnNode methodInsn, final MappedSubtypeInfo mappedType, final Map<Integer, MappedSubtypeInfo> arrayVars) {
		switch ( methodInsn.getOpcode() ) {
			case INVOKEVIRTUAL:
				if ( "asArray".equals(methodInsn.name) && methodInsn.desc.equals("()[L" + MAPPED_OBJECT_JVM + ";") ) {
					// Go forward and store the local variable index.
					// We only allow this pattern: INVOKEVIRTUAL -> CHECKCAST -> ASTORE.
					// We remove the first two and store the target MappedSubtype in the ASTORE variable
					AbstractInsnNode nextInstruction;
					checkInsnAfterIsArray(nextInstruction = methodInsn.getNext(), CHECKCAST);
					checkInsnAfterIsArray(nextInstruction = nextInstruction.getNext(), ASTORE);

					final Frame<BasicValue> frame = frameMap.get(nextInstruction);
					final String targetType = frame.getStack(frame.getStackSize() - 1).getType().getElementType().getInternalName();
					if ( !methodInsn.owner.equals(targetType) ) {
						/*
						This may happen with the current API, like so:
							MappedA foo = MappedA.malloc(...);
							MappedB[] cursor = foo.asArray();
						We have to parameterize MappedObject to avoid this.
						 */
						throw new ClassCastException("Source: " + methodInsn.owner + " - Target: " + targetType);
					}

					final VarInsnNode varInstruction = (VarInsnNode)nextInstruction;

					arrayVars.put(varInstruction.var, mappedType);

					instructions.remove(methodInsn.getNext()); // Remove CHECKCAST
					instructions.remove(methodInsn); // Remove INVOKEVIRTUAL
				}

				if ( "dup".equals(methodInsn.name) && methodInsn.desc.equals("()L" + MAPPED_OBJECT_JVM + ";") ) {
					i = replace(instructions, i, methodInsn, generateDupInstructions(methodInsn));
					break;
				}

				if ( "slice".equals(methodInsn.name) && methodInsn.desc.equals("()L" + MAPPED_OBJECT_JVM + ";") ) {
					i = replace(instructions, i, methodInsn, generateSliceInstructions(methodInsn));
					break;
				}

				if ( "runViewConstructor".equals(methodInsn.name) && "()V".equals(methodInsn.desc) ) {
					i = replace(instructions, i, methodInsn, generateRunViewConstructorInstructions(methodInsn));
					break;
				}

				if ( "copyTo".equals(methodInsn.name) && methodInsn.desc.equals("(L" + MAPPED_OBJECT_JVM + ";)V") ) {
					i = replace(instructions, i, methodInsn, generateCopyToInstructions(mappedType));
					break;
				}

				if ( "copyRange".equals(methodInsn.name) && methodInsn.desc.equals("(L" + MAPPED_OBJECT_JVM + ";I)V") ) {
					i = replace(instructions, i, methodInsn, generateCopyRangeInstructions(mappedType));
					break;
				}

				break;
			case INVOKESPECIAL:
				// super() in VIEW_CONSTRUCTOR_NAME, remove
				if ( methodInsn.owner.equals(MAPPED_OBJECT_JVM) && "<init>".equals(methodInsn.name) && "()V".equals(methodInsn.desc) ) {
					instructions.remove(methodInsn.getPrevious()); // ALOAD
					instructions.remove(methodInsn); // INVOKESPECIAL

					i -= 2;
				}
				break;
			case INVOKESTATIC:
				boolean isMapDirectMethod = "map".equals(methodInsn.name) && methodInsn.desc.equals("(JI)L" + MAPPED_OBJECT_JVM + ";");
				boolean isMapBufferMethod = "map".equals(methodInsn.name) && methodInsn.desc.equals("(Ljava/nio/ByteBuffer;)L" + MAPPED_OBJECT_JVM + ";");
				boolean isMallocMethod = "malloc".equals(methodInsn.name) && methodInsn.desc.equals("(I)L" + MAPPED_OBJECT_JVM + ";");

				if ( (isMapDirectMethod || isMapBufferMethod) || isMallocMethod )
					i = replace(instructions, i, methodInsn, generateMapInstructions(mappedType, methodInsn.owner, isMapDirectMethod, isMallocMethod));
				break;
		}

		return i;
	}

	private static InsnList generateCopyRangeInstructions(final MappedSubtypeInfo mappedType) {
		final InsnList list = new InsnList();

		// stack: instances, target, this
		list.add(getIntNode(mappedType.sizeof));
		// stack: sizeof, instances, target, this
		list.add(new InsnNode(IMUL));
		// stack: bytes, target, this
		list.add(new MethodInsnNode(INVOKESTATIC, MAPPED_HELPER_JVM, "copy", "(L" + MAPPED_OBJECT_JVM + ";L" + MAPPED_OBJECT_JVM + ";I)V"));
		// stack: -

		return list;
	}

	private static InsnList generateCopyToInstructions(final MappedSubtypeInfo mappedType) {
		final InsnList list = new InsnList();

		// stack: target, this
		list.add(getIntNode(mappedType.sizeof - mappedType.padding));
		// stack: sizeof, target, this
		list.add(new MethodInsnNode(INVOKESTATIC, MAPPED_HELPER_JVM, "copy", "(L" + MAPPED_OBJECT_JVM + ";L" + MAPPED_OBJECT_JVM + ";I)V"));
		// stack: -

		return list;
	}

	private static InsnList generateRunViewConstructorInstructions(final MethodInsnNode methodInsn) {
		final InsnList list = new InsnList();

		// stack: this
		list.add(new InsnNode(DUP));
		// stack: this, this
		list.add(new MethodInsnNode(INVOKEVIRTUAL, methodInsn.owner, VIEW_CONSTRUCTOR_NAME, "()V"));
		// stack: this

		return list;
	}

	private static InsnList generateSliceInstructions(final MethodInsnNode methodInsn) {
		final InsnList list = new InsnList();

		// stack: this
		list.add(new TypeInsnNode(NEW, methodInsn.owner));
		// stack: new, this
		list.add(new InsnNode(DUP));
		// stack: new, new, this
		list.add(new MethodInsnNode(INVOKESPECIAL, methodInsn.owner, "<init>", "()V"));
		// stack: new, this
		list.add(new MethodInsnNode(INVOKESTATIC, MAPPED_HELPER_JVM, "slice", "(L" + MAPPED_OBJECT_JVM + ";L" + MAPPED_OBJECT_JVM + ";)L" + MAPPED_OBJECT_JVM + ";"));
		// stack: new

		return list;
	}

	private static InsnList generateDupInstructions(final MethodInsnNode methodInsn) {
		final InsnList list = new InsnList();

		// stack: this
		list.add(new TypeInsnNode(NEW, methodInsn.owner));
		// stack: new, this
		list.add(new InsnNode(DUP));
		// stack: new, new, this
		list.add(new MethodInsnNode(INVOKESPECIAL, methodInsn.owner, "<init>", "()V"));
		// stack: new, this
		list.add(new MethodInsnNode(INVOKESTATIC, MAPPED_HELPER_JVM, "dup", "(L" + MAPPED_OBJECT_JVM + ";L" + MAPPED_OBJECT_JVM + ";)L" + MAPPED_OBJECT_JVM + ";"));
		// stack: new

		return list;
	}

	private static InsnList generateMapInstructions(final MappedSubtypeInfo mappedType, final String className, final boolean mapDirectMethod, final boolean mallocMethod) {
		final InsnList trg = new InsnList();

		if ( mallocMethod ) {
			// stack: count
			trg.add(getIntNode(mappedType.sizeof));
			// stack: sizeof, count
			trg.add(new InsnNode(IMUL));
			// stack: bytes
			trg.add(new MethodInsnNode(INVOKESTATIC, mappedType.cacheLinePadded ? jvmClassName(CacheUtil.class) : jvmClassName(BufferUtils.class), "createByteBuffer", "(I)L" + jvmClassName(ByteBuffer.class) + ";"));
			// stack: buffer
		} else if ( mapDirectMethod ) {
			// stack: capacity, address
			trg.add(new MethodInsnNode(INVOKESTATIC, MAPPED_HELPER_JVM, "newBuffer", "(JI)L" + jvmClassName(ByteBuffer.class) + ";"));
			// stack: buffer
		}

		// stack: buffer
		trg.add(new TypeInsnNode(NEW, className));
		// stack: new, buffer
		trg.add(new InsnNode(DUP));
		// stack: new, new, buffer
		trg.add(new MethodInsnNode(INVOKESPECIAL, className, "<init>", "()V"));
		// stack: new, buffer
		trg.add(new InsnNode(DUP_X1));
		// stack: new, buffer, new
		trg.add(new InsnNode(SWAP));
		// stack: buffer, new, new
		trg.add(getIntNode(mappedType.align));
		// stack: int, buffer, new, new
		trg.add(getIntNode(mappedType.sizeof));
		// stack: int, int, buffer, new, new
		trg.add(new MethodInsnNode(INVOKESTATIC, MAPPED_HELPER_JVM, "setup", "(L" + MAPPED_OBJECT_JVM + ";Ljava/nio/ByteBuffer;II)V"));
		// stack: new

		return trg;
	}

	static InsnList transformFieldAccess(final FieldInsnNode fieldInsn) {
		final MappedSubtypeInfo mappedSubtype;
		mappedSubtype = className_to_subtype.get(fieldInsn.owner);
		if ( mappedSubtype == null ) { // early out
			// MappedSet.view
			outer:
			if ( "view".equals(fieldInsn.name) && fieldInsn.owner.startsWith(MAPPEDSET_PREFIX) )
				return generateSetViewInstructions(fieldInsn);

			return null; // early out
		}

		if ( "SIZEOF".equals(fieldInsn.name) )
			return generateSIZEOFInstructions(fieldInsn, mappedSubtype);

		if ( "view".equals(fieldInsn.name) )
			return generateViewInstructions(fieldInsn, mappedSubtype);

		if ( "baseAddress".equals(fieldInsn.name) || "viewAddress".equals(fieldInsn.name) ) {
			return generateAddressInstructions(fieldInsn);
		}

		final FieldInfo field = mappedSubtype.fields.get(fieldInsn.name);
		if ( field == null ) // early out
			return null;

		// now we're going to transform ByteBuffer-typed field access
		if ( fieldInsn.desc.equals("L" + jvmClassName(ByteBuffer.class) + ";") )
			return generateByteBufferInstructions(fieldInsn, mappedSubtype, field.offset);

		// we're now going to transform the field access
		return generateFieldInstructions(fieldInsn, field);
	}

	private static InsnList generateSetViewInstructions(final FieldInsnNode fieldInsn) {
		if ( fieldInsn.getOpcode() == GETFIELD )
			throwAccessErrorOnReadOnlyField(fieldInsn.owner, fieldInsn.name);
		if ( fieldInsn.getOpcode() != PUTFIELD )
			throw new InternalError();

		final InsnList list = new InsnList();

		// stack: index, this
		if ( MAPPED_SET2_JVM.equals(fieldInsn.owner) )
			list.add(new MethodInsnNode(INVOKESTATIC, MAPPED_HELPER_JVM, "put_views", "(L" + MAPPED_SET2_JVM + ";I)V"));
		else if ( MAPPED_SET3_JVM.equals(fieldInsn.owner) )
			list.add(new MethodInsnNode(INVOKESTATIC, MAPPED_HELPER_JVM, "put_views", "(L" + MAPPED_SET3_JVM + ";I)V"));
		else if ( MAPPED_SET4_JVM.equals(fieldInsn.owner) )
			list.add(new MethodInsnNode(INVOKESTATIC, MAPPED_HELPER_JVM, "put_views", "(L" + MAPPED_SET4_JVM + ";I)V"));
		else
			throw new InternalError();
		// stack: -

		return list;
	}

	private static InsnList generateSIZEOFInstructions(final FieldInsnNode fieldInsn, final MappedSubtypeInfo mappedSubtype) {
		if ( !"I".equals(fieldInsn.desc) )
			throw new InternalError();

		final InsnList list = new InsnList();

		if ( fieldInsn.getOpcode() == GETSTATIC ) {
			list.add(getIntNode(mappedSubtype.sizeof));
			return list;
		}

		if ( fieldInsn.getOpcode() == PUTSTATIC )
			throwAccessErrorOnReadOnlyField(fieldInsn.owner, fieldInsn.name);

		throw new InternalError();
	}

	private static InsnList generateViewInstructions(final FieldInsnNode fieldInsn, final MappedSubtypeInfo mappedSubtype) {
		if ( !"I".equals(fieldInsn.desc) )
			throw new InternalError();

		final InsnList list = new InsnList();

		if ( fieldInsn.getOpcode() == GETFIELD ) {
			if ( mappedSubtype.sizeof_shift != 0 ) {
				// stack: instance
				list.add(getIntNode(mappedSubtype.sizeof_shift));
				// stack: sizeof, instance
				list.add(new MethodInsnNode(INVOKESTATIC, MAPPED_HELPER_JVM, "get_view_shift", "(L" + MAPPED_OBJECT_JVM + ";I)I"));
				// stack: view
			} else {
				// stack: instance
				list.add(getIntNode(mappedSubtype.sizeof));
				// stack: sizeof, instance
				list.add(new MethodInsnNode(INVOKESTATIC, MAPPED_HELPER_JVM, "get_view", "(L" + MAPPED_OBJECT_JVM + ";I)I"));
				// stack: view
			}
			return list;
		}

		if ( fieldInsn.getOpcode() == PUTFIELD ) {
			if ( mappedSubtype.sizeof_shift != 0 ) {
				// stack: view, instance
				list.add(getIntNode(mappedSubtype.sizeof_shift));
				// stack: sizeof, view, instance
				list.add(new MethodInsnNode(INVOKESTATIC, MAPPED_HELPER_JVM, "put_view_shift", "(L" + MAPPED_OBJECT_JVM + ";II)V"));
				// stack: -
			} else {
				// stack: view, instance
				list.add(getIntNode(mappedSubtype.sizeof));
				// stack: sizeof, view, instance
				list.add(new MethodInsnNode(INVOKESTATIC, MAPPED_HELPER_JVM, "put_view", "(L" + MAPPED_OBJECT_JVM + ";II)V"));
				// stack: -
			}
			return list;
		}

		throw new InternalError();
	}

	private static InsnList generateAddressInstructions(final FieldInsnNode fieldInsn) {
		if ( !"J".equals(fieldInsn.desc) )
			throw new IllegalStateException();

		if ( fieldInsn.getOpcode() == GETFIELD ) // do not change a thing
			return null;

		if ( fieldInsn.getOpcode() == PUTFIELD )
			throwAccessErrorOnReadOnlyField(fieldInsn.owner, fieldInsn.name);

		throw new InternalError();
	}

	private static InsnList generateByteBufferInstructions(final FieldInsnNode fieldInsn, final MappedSubtypeInfo mappedSubtype, final long fieldOffset) {
		if ( fieldInsn.getOpcode() == PUTFIELD )
			throwAccessErrorOnReadOnlyField(fieldInsn.owner, fieldInsn.name);

		if ( fieldInsn.getOpcode() == GETFIELD ) {
			final InsnList list = new InsnList();

			// stack: ref
			list.add(new FieldInsnNode(GETFIELD, mappedSubtype.className, "viewAddress", "J"));
			// stack: long
			list.add(new LdcInsnNode(fieldOffset));
			// stack: long, long
			list.add(new InsnNode(LADD));
			// stack: long
			list.add(new LdcInsnNode(mappedSubtype.fields.get(fieldInsn.name).length));
			// stack: long, long
			list.add(new InsnNode(L2I));
			// stack: int, long
			list.add(new MethodInsnNode(INVOKESTATIC, MAPPED_HELPER_JVM, "newBuffer", "(JI)L" + jvmClassName(ByteBuffer.class) + ";"));
			// stack: buffer

			return list;
		}

		throw new InternalError();
	}

	private static InsnList generateFieldInstructions(final FieldInsnNode fieldInsn, final FieldInfo field) {
		final InsnList list = new InsnList();

		if ( fieldInsn.getOpcode() == PUTFIELD ) {
			// stack: value, ref
			list.add(getIntNode((int)field.offset));
			// stack: fieldOffset, value, ref
			list.add(new MethodInsnNode(INVOKESTATIC, MAPPED_HELPER_JVM, field.getAccessType() + "put", "(L" + MAPPED_OBJECT_JVM + ";" + fieldInsn.desc + "I)V"));
			// stack -
			return list;
		}

		if ( fieldInsn.getOpcode() == GETFIELD ) {
			// stack: ref
			list.add(getIntNode((int)field.offset));
			// stack: fieldOffset, ref
			list.add(new MethodInsnNode(INVOKESTATIC, MAPPED_HELPER_JVM, field.getAccessType() + "get", "(L" + MAPPED_OBJECT_JVM + ";I)" + fieldInsn.desc));
			// stack: -
			return list;
		}

		throw new InternalError();
	}

	static int transformArrayAccess(final InsnList instructions, int i, final Map<AbstractInsnNode, Frame<BasicValue>> frameMap, final VarInsnNode loadInsn, final MappedSubtypeInfo mappedSubtype, final int var) {
		// We need to go forward in time to find how we use the array var
		final int loadStackSize = frameMap.get(loadInsn).getStackSize() + 1;

		AbstractInsnNode nextInsn = loadInsn;

		while ( true ) {
			nextInsn = nextInsn.getNext();
			if ( nextInsn == null )
				throw new InternalError();

			Frame<BasicValue> frame = frameMap.get(nextInsn);
			if ( frame == null )
				continue;

			int stackSize = frame.getStackSize();

			if ( stackSize == loadStackSize + 1 && nextInsn.getOpcode() == AALOAD ) {
				final AbstractInsnNode aaLoadInsn = nextInsn;

				while ( true ) {
					nextInsn = nextInsn.getNext();
					if ( nextInsn == null )
						break;

					frame = frameMap.get(nextInsn);
					if ( frame == null )
						continue;
					stackSize = frame.getStackSize();

					if ( stackSize == loadStackSize + 1 && nextInsn.getOpcode() == PUTFIELD ) {
						final FieldInsnNode fieldInsn = (FieldInsnNode)nextInsn;

						// stack: value, view, ref
						instructions.insert(nextInsn, new MethodInsnNode(INVOKESTATIC, mappedSubtype.className, setterName(fieldInsn.name), "(L" + mappedSubtype.className + ";I" + fieldInsn.desc + ")V"));
						// stack: -
						instructions.remove(nextInsn);

						break;
					} else if ( stackSize == loadStackSize && nextInsn.getOpcode() == GETFIELD ) {
						final FieldInsnNode fieldInsn = (FieldInsnNode)nextInsn;

						// stack: view, ref
						instructions.insert(nextInsn, new MethodInsnNode(INVOKESTATIC, mappedSubtype.className, getterName(fieldInsn.name), "(L" + mappedSubtype.className + ";I)" + fieldInsn.desc));
						// stack: value
						instructions.remove(nextInsn);

						break;
					} else if ( stackSize == loadStackSize && nextInsn.getOpcode() == DUP && nextInsn.getNext().getOpcode() == GETFIELD ) {
						// May happen with operator+assignment (e.g. cursor[i].value += 10)
						final FieldInsnNode fieldInsn = (FieldInsnNode)nextInsn.getNext();

						final MethodInsnNode getter = new MethodInsnNode(INVOKESTATIC, mappedSubtype.className, getterName(fieldInsn.name), "(L" + mappedSubtype.className + ";I)" + fieldInsn.desc);

						// stack: view, ref
						instructions.insert(nextInsn, new InsnNode(DUP2));
						// stack: view, ref, view, ref
						instructions.insert(nextInsn.getNext(), getter);
						// stack: value, view, ref

						instructions.remove(nextInsn);
						instructions.remove(fieldInsn);

						nextInsn = getter;
						continue;
					} else if ( stackSize < loadStackSize )
						throw new ClassFormatError("Invalid " + mappedSubtype.className + " view array usage detected: " + getOpcodeName(nextInsn));
				}

				instructions.remove(aaLoadInsn);

				return i;
			} else if ( stackSize == loadStackSize && nextInsn.getOpcode() == ARRAYLENGTH ) {
				if ( LWJGLUtil.DEBUG && loadInsn.getNext() != nextInsn )
					throw new InternalError();

				instructions.remove(nextInsn);
				loadInsn.var = var;
				instructions.insert(loadInsn, new MethodInsnNode(INVOKEVIRTUAL, mappedSubtype.className, CAPACITY_METHOD_NAME, "()I"));

				return i + 1;
			} else if ( stackSize < loadStackSize ) // Consumed by something other than AALOAD or ARRAYLENGTH
				throw new ClassFormatError("Invalid " + mappedSubtype.className + " view array usage detected: " + getOpcodeName(nextInsn));
		}
	}

	private static class FieldInfo {

		final long    offset;
		final long    length;
		final long    lengthPadded;
		final Type    type;
		final boolean isVolatile;
		final boolean isPointer;

		FieldInfo(final long offset, final long length, final long lengthPadded, final Type type, final boolean isVolatile, final boolean isPointer) {
			this.offset = offset;
			this.length = length;
			this.lengthPadded = lengthPadded;
			this.type = type;
			this.isVolatile = isVolatile;
			this.isPointer = isPointer;
		}

		String getAccessType() {
			return isPointer ? "a" : type.getDescriptor().toLowerCase() + (isVolatile ? "v" : "");
		}

	}

	private static class MappedSubtypeInfo {

		final String className;

		final int     sizeof;
		final int     sizeof_shift;
		final int     align;
		final int     padding;
		final boolean cacheLinePadded;

		final Map<String, FieldInfo> fields;

		MappedSubtypeInfo(String className, Map<String, FieldInfo> fields, int sizeof, int align, int padding, final boolean cacheLinePadded) {
			this.className = className;

			this.sizeof = sizeof;
			if ( ((sizeof - 1) & sizeof) == 0 )
				this.sizeof_shift = getPoT(sizeof);
			else
				this.sizeof_shift = 0;
			this.align = align;
			this.padding = padding;
			this.cacheLinePadded = cacheLinePadded;

			this.fields = fields;
		}

		private static int getPoT(int value) {
			int pot = -1;
			while ( value > 0 ) {
				pot++;
				value >>= 1;
			}
			return pot;
		}

	}

	// -------------------------------------------------------
	// -------------------[ MACROS & UTILS ]------------------
	// -------------------------------------------------------

	private static void getClassEnums(final Class clazz, final Map<Integer, String> map, final String... prefixFilters) {
		try {
			OUTER:
			for ( Field field : clazz.getFields() ) {
				if ( !Modifier.isStatic(field.getModifiers()) || field.getType() != int.class )
					continue;

				for ( String filter : prefixFilters ) {
					if ( field.getName().startsWith(filter) )
						continue OUTER;
				}

				if ( map.put((Integer)field.get(null), field.getName()) != null )
					throw new IllegalStateException();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	static String getOpcodeName(final AbstractInsnNode insn) {
		final String op = OPCODE_TO_NAME.get(insn.getOpcode());
		return INSNTYPE_TO_NAME.get(insn.getType()) + ": " + insn.getOpcode() + (op == null ? "" : " [" + OPCODE_TO_NAME.get(insn.getOpcode()) + "]");
	}

	static String jvmClassName(Class<?> type) {
		return type.getName().replace('.', '/');
	}

	static String getterName(final String fieldName) {
		return "get$" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1) + "$LWJGL";
	}

	static String setterName(final String fieldName) {
		return "set$" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1) + "$LWJGL";
	}

	private static void checkInsnAfterIsArray(final AbstractInsnNode instruction, final int opcode) {
		if ( instruction == null )
			throw new ClassFormatError("Unexpected end of instructions after .asArray() method.");

		if ( instruction.getOpcode() != opcode )
			throw new ClassFormatError("The result of .asArray() must be stored to a local variable. Found: " + getOpcodeName(instruction));
	}

	static AbstractInsnNode getIntNode(final int value) {
		if ( value <= 5 && -1 <= value )
			return new InsnNode(ICONST_M1 + value + 1);

		if ( value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE )
			return new IntInsnNode(BIPUSH, value);

		if ( value >= Short.MIN_VALUE && value <= Short.MAX_VALUE )
			return new IntInsnNode(SIPUSH, value);

		return new LdcInsnNode(value);
	}

	static void visitIntNode(final MethodVisitor mv, final int value) {
		if ( value <= 5 && -1 <= value )
			mv.visitInsn(ICONST_M1 + value + 1);
		else if ( value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE )
			mv.visitIntInsn(BIPUSH, value);
		else if ( value >= Short.MIN_VALUE && value <= Short.MAX_VALUE )
			mv.visitIntInsn(SIPUSH, value);
		else
			mv.visitLdcInsn(value);
	}

	/** Replace an instruction with a list of instructions. */
	static int replace(final InsnList instructions, final int i, final AbstractInsnNode location, final InsnList list) {
		final int size = list.size();

		instructions.insert(location, list);
		instructions.remove(location);

		return i + (size - 1);
	}

	private static void throwAccessErrorOnReadOnlyField(String className, String fieldName) {
		throw new IllegalAccessError("The " + className + "." + fieldName + " field is final.");
	}

	private static void printBytecode(byte[] bytecode) {
		StringWriter sw = new StringWriter();
		ClassVisitor tracer = new TraceClassVisitor(new ClassWriter(0), new PrintWriter(sw));
		new ClassReader(bytecode).accept(tracer, 0);
		String dump = sw.toString();

		LWJGLUtil.log(dump);
	}

}