/*
 * ConstantPool.java
 *
 * Copyright (c) 2012 Mike Strobel
 *
 * This source code is subject to terms and conditions of the Apache License, Version 2.0.
 * A copy of the license can be found in the License.html file at the root of this distribution.
 * By using this source code in any fashion, you are agreeing to be bound by the terms of the
 * Apache License, Version 2.0.
 *
 * You must not remove this notice, or any other, from this software.
 */

package com.strobel.reflection.emit;

import com.strobel.core.HashUtilities;
import com.strobel.core.StringUtilities;
import com.strobel.core.VerifyArgument;
import com.strobel.reflection.FieldInfo;
import com.strobel.reflection.MethodBase;
import com.strobel.reflection.MethodInfo;
import com.strobel.reflection.Type;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;

/**
 * @author strobelm
 */
@SuppressWarnings({"PublicField", "ProtectedField"})
final class ConstantPool {
    private final static Writer WRITER = new Writer();

    private final ArrayList<Entry> _pool = new ArrayList<>();
    private final HashMap<Key, Entry> _entryMap = new HashMap<>();
    private final Key _lookupKey = new Key();
    private final Key _newKey = new Key();

    private int _size;

    final HashSet<Type<?>> referencedInnerTypes = new LinkedHashSet<>();

    public void write(final CodeStream stream) {
        stream.putShort(_size + 1);

        for (final Entry entry : _pool) {
            if (entry != null) {
                entry.accept(WRITER, stream);
            }
        }
    }

    public Entry get(final int index) {
        VerifyArgument.inRange(0, _size + 1, index, "index");

        final Entry info = _pool.get(index - 1);

        if (info == null) {
            throw new IndexOutOfBoundsException();
        }

        return info;
    }

    public Entry get(final int index, final Tag expectedType) {
        VerifyArgument.inRange(0, _size + 1, index, "index");

        final Entry entry = get(index);
        final Tag actualType = entry.getTag();

        if (actualType != expectedType) {
            throw new IllegalStateException(
                String.format(
                    "Expected type '%s' but found type '%s'.",
                    expectedType,
                    actualType
                )
            );
        }

        return entry;
    }

    public Utf8StringConstant getUtf8StringConstant(final String value) {
        _lookupKey.set(value);
        Entry entry = _entryMap.get(_lookupKey);
        if (entry == null) {
            entry = new Utf8StringConstant(this, value);
        }
        _lookupKey.clear();
        return (Utf8StringConstant)entry;
    }

    public StringConstant getStringConstant(final String value) {
        final Utf8StringConstant utf8Constant = getUtf8StringConstant(value);
        _lookupKey.set(Tag.StringConstant, utf8Constant.index);
        Entry entry = _entryMap.get(_lookupKey);
        if (entry == null) {
            entry = new StringConstant(this, utf8Constant.index);
        }
        _lookupKey.clear();
        return (StringConstant)entry;
    }

    public IntegerConstant getIntegerConstant(final int value) {
        _lookupKey.set(value);
        Entry entry = _entryMap.get(_lookupKey);
        if (entry == null) {
            entry = new IntegerConstant(this, value);
        }
        _lookupKey.clear();
        return (IntegerConstant)entry;
    }

    public FloatConstant getFloatConstant(final float value) {
        _lookupKey.set(value);
        Entry entry = _entryMap.get(_lookupKey);
        if (entry == null) {
            entry = new FloatConstant(this, value);
        }
        _lookupKey.clear();
        return (FloatConstant)entry;
    }

    public LongConstant getLongConstant(final long value) {
        _lookupKey.set(value);
        Entry entry = _entryMap.get(_lookupKey);
        if (entry == null) {
            entry = new LongConstant(this, value);
        }
        _lookupKey.clear();
        return (LongConstant)entry;
    }

    public DoubleConstant getDoubleConstant(final double value) {
        _lookupKey.set(value);
        Entry entry = _entryMap.get(_lookupKey);
        if (entry == null) {
            entry = new DoubleConstant(this, value);
        }
        _lookupKey.clear();
        return (DoubleConstant)entry;
    }

    public TypeInfo getTypeInfo(final Type<?> type) {
        final Utf8StringConstant name = getUtf8StringConstant(type.getInternalName());

        if (type.isNested()) {
            referencedInnerTypes.add(type);

            final Type declaringType = type.getDeclaringType();
            final MethodBase declaringMethod = type.getDeclaringMethod();
            final String shortName = type.getShortName();

            if (declaringType != null) {
                getTypeInfo(declaringType);
            }

            if (declaringMethod != null) {
                getMethodReference(declaringMethod);
            }

            if (!StringUtilities.isNullOrWhitespace(shortName)) {
                getUtf8StringConstant(shortName);
            }
        }

        _lookupKey.set(Tag.TypeInfo, name.index);

        Entry entry = _entryMap.get(_lookupKey);

        if (entry == null) {
            entry = new TypeInfo(this, name.index);
        }

        _lookupKey.clear();

        return (TypeInfo)entry;
    }

    public FieldReference getFieldReference(final FieldInfo field) {
        final TypeInfo typeInfo = getTypeInfo(field.getDeclaringType());
        final NameAndTypeDescriptor nameAndDescriptor = getNameAndTypeDescriptor(
            field.getName(),
            field.getErasedSignature()
        );
        _lookupKey.set(Tag.FieldReference, typeInfo.index, nameAndDescriptor.index);
        Entry entry = _entryMap.get(_lookupKey);
        if (entry == null) {
            entry = new FieldReference(this, typeInfo.index, nameAndDescriptor.index);
        }
        _lookupKey.clear();
        return (FieldReference)entry;
    }

    public MethodReference getMethodReference(final MethodBase method) {
        final TypeInfo typeInfo = getTypeInfo(method.getDeclaringType());
        final NameAndTypeDescriptor nameAndDescriptor = getNameAndTypeDescriptor(
            method.getName(),
            method.getErasedSignature()
        );
        _lookupKey.set(Tag.MethodReference, typeInfo.index, nameAndDescriptor.index);
        Entry entry = _entryMap.get(_lookupKey);
        if (entry == null) {
            entry = new MethodReference(this, typeInfo.index, nameAndDescriptor.index);
        }
        _lookupKey.clear();
        return (MethodReference)entry;
    }

    public InterfaceMethodReference getInterfaceMethodReference(final MethodInfo method) {
        final TypeInfo typeInfo = getTypeInfo(method.getDeclaringType());
        final NameAndTypeDescriptor nameAndDescriptor = getNameAndTypeDescriptor(
            method.getName(),
            method.getErasedSignature()
        );
        _lookupKey.set(Tag.InterfaceMethodReference, typeInfo.index, nameAndDescriptor.index);
        Entry entry = _entryMap.get(_lookupKey);
        if (entry == null) {
            entry = new InterfaceMethodReference(this, typeInfo.index, nameAndDescriptor.index);
        }
        _lookupKey.clear();
        return (InterfaceMethodReference)entry;
    }

    NameAndTypeDescriptor getNameAndTypeDescriptor(final String name, final String typeDescriptor) {
        final Utf8StringConstant utf8Name = getUtf8StringConstant(name);
        final Utf8StringConstant utf8Descriptor = getUtf8StringConstant(typeDescriptor);
        _lookupKey.set(Tag.NameAndTypeDescriptor, utf8Name.index, utf8Descriptor.index);
        Entry entry = _entryMap.get(_lookupKey);
        if (entry == null) {
            entry = new NameAndTypeDescriptor(this, utf8Name.index, utf8Descriptor.index);
        }
        _lookupKey.clear();
        return (NameAndTypeDescriptor)entry;
    }

    MethodHandle getMethodHandle(final ReferenceKind referenceKind, final int referenceIndex) {
        _lookupKey.set(Tag.MethodHandle, referenceIndex, referenceKind);
        Entry entry = _entryMap.get(_lookupKey);
        if (entry == null) {
            entry = new MethodHandle(this, referenceKind, referenceIndex);
        }
        _lookupKey.clear();
        return (MethodHandle)entry;
    }

    MethodType getMethodType(final int descriptorIndex) {
        _lookupKey.set(Tag.MethodType, descriptorIndex);
        Entry entry = _entryMap.get(_lookupKey);
        if (entry == null) {
            entry = new MethodType(this, descriptorIndex);
        }
        _lookupKey.clear();
        return (MethodType)entry;
    }

    InvokeDynamicInfo getInvokeDynamicInfo(
        final int bootstrapMethodAttributeIndex,
        final int nameAndTypeDescriptorIndex) {
        _lookupKey.set(Tag.InvokeDynamicInfo, bootstrapMethodAttributeIndex, nameAndTypeDescriptorIndex);
        Entry entry = _entryMap.get(_lookupKey);
        if (entry == null) {
            entry = new InvokeDynamicInfo(this, bootstrapMethodAttributeIndex, nameAndTypeDescriptorIndex);
        }
        _lookupKey.clear();
        return (InvokeDynamicInfo)entry;
    }

    public static abstract class Entry {
        protected final int index;
        protected final ConstantPool owner;

        Entry(final ConstantPool owner) {
            this.owner = owner;
            this.index = owner._size + 1;
            owner._pool.add(this);
            owner._size += size();
            for (int i = 1; i < size(); i++) {
                owner._pool.add(null);
            }
        }

        public abstract Tag getTag();

        /**
         * The number of slots in the constant pool used by this entry.
         * 2 for DoubleConstant and LongConstant; 1 for everything else.
         */
        public int size() {
            return 1;
        }

        public abstract int byteLength();

        public abstract <R, D> R accept(Visitor<R, D> visitor, D data);
    }

    public static enum ReferenceKind {
        GetField(1, "getfield"),
        GetStatic(2, "getstatic"),
        PutField(3, "putfield"),
        PutStatic(4, "putstatic"),
        InvokeVirtual(5, "invokevirtual"),
        InvokeStatic(6, "invokestatic"),
        InvokeSpecial(7, "invokespecial"),
        NewInvokeSpecial(8, "newinvokespecial"),
        InvokeInterface(9, "invokeinterface");

        public final int tag;
        public final String name;

        ReferenceKind(final int tag, final String name) {
            this.tag = tag;
            this.name = name;
        }

        static ReferenceKind fromTag(final int tag) {
            switch (tag) {
                case 1:
                    return GetField;
                case 2:
                    return GetStatic;
                case 3:
                    return PutField;
                case 4:
                    return PutStatic;
                case 5:
                    return InvokeVirtual;
                case 6:
                    return InvokeStatic;
                case 7:
                    return InvokeSpecial;
                case 8:
                    return NewInvokeSpecial;
                case 9:
                    return InvokeInterface;
                default:
                    return null;
            }
        }
    }

    public static enum Tag {
        Utf8StringConstant(1),
        IntegerConstant(3),
        FloatConstant(4),
        LongConstant(5),
        DoubleConstant(6),
        TypeInfo(7),
        StringConstant(8),
        FieldReference(9),
        MethodReference(10),
        InterfaceMethodReference(11),
        NameAndTypeDescriptor(12),
        MethodHandle(15),
        MethodType(16),
        InvokeDynamicInfo(18);

        public final int value;

        Tag(final int value) {
            this.value = value;
        }
    }

    public interface Visitor<R, P> {
        R visitTypeInfo(TypeInfo info, P p);
        R visitDoubleConstant(DoubleConstant info, P p);
        R visitFieldReference(FieldReference info, P p);
        R visitFloatConstant(FloatConstant info, P p);
        R visitIntegerConstant(IntegerConstant info, P p);
        R visitInterfaceMethodReference(InterfaceMethodReference info, P p);
        R visitInvokeDynamicInfo(InvokeDynamicInfo info, P p);
        R visitLongConstant(LongConstant info, P p);
        R visitNameAndTypeDescriptor(NameAndTypeDescriptor info, P p);
        R visitMethodReference(MethodReference info, P p);
        R visitMethodHandle(MethodHandle info, P p);
        R visitMethodType(MethodType info, P p);
        R visitStringConstant(StringConstant info, P p);
        R visitUtf8StringConstant(Utf8StringConstant info, P p);
    }

    private final static class Writer implements Visitor<Void, CodeStream> {

        @Override
        public Void visitTypeInfo(final TypeInfo info, final CodeStream codeStream) {
            codeStream.putByte(info.getTag().value);
            codeStream.putShort(info.nameIndex);
            return null;
        }

        @Override
        public Void visitDoubleConstant(final DoubleConstant info, final CodeStream codeStream) {
            codeStream.putByte(info.getTag().value);
            codeStream.putDouble(info.value);
            return null;
        }

        @Override
        public Void visitFieldReference(final FieldReference info, final CodeStream codeStream) {
            codeStream.putByte(info.getTag().value);
            codeStream.putShort(info.typeInfoIndex);
            codeStream.putShort(info.nameAndTypeDescriptorIndex);
            return null;
        }

        @Override
        public Void visitFloatConstant(final FloatConstant info, final CodeStream codeStream) {
            codeStream.putByte(info.getTag().value);
            codeStream.putFloat(info.value);
            return null;
        }

        @Override
        public Void visitIntegerConstant(final IntegerConstant info, final CodeStream codeStream) {
            codeStream.putByte(info.getTag().value);
            codeStream.putInt(info.value);
            return null;
        }

        @Override
        public Void visitInterfaceMethodReference(final InterfaceMethodReference info, final CodeStream codeStream) {
            codeStream.putByte(info.getTag().value);
            codeStream.putShort(info.typeInfoIndex);
            codeStream.putShort(info.nameAndTypeDescriptorIndex);
            return null;
        }

        @Override
        public Void visitInvokeDynamicInfo(final InvokeDynamicInfo info, final CodeStream codeStream) {
            codeStream.putByte(info.getTag().value);
            codeStream.putShort(info.bootstrapMethodAttributeIndex);
            codeStream.putShort(info.nameAndTypeDescriptorIndex);
            return null;
        }

        @Override
        public Void visitLongConstant(final LongConstant info, final CodeStream codeStream) {
            codeStream.putByte(info.getTag().value);
            codeStream.putLong(info.value);
            return null;
        }

        @Override
        public Void visitNameAndTypeDescriptor(final NameAndTypeDescriptor info, final CodeStream codeStream) {
            codeStream.putByte(info.getTag().value);
            codeStream.putShort(info.nameIndex);
            codeStream.putShort(info.typeDescriptorIndex);
            return null;
        }

        @Override
        public Void visitMethodReference(final MethodReference info, final CodeStream codeStream) {
            codeStream.putByte(info.getTag().value);
            codeStream.putShort(info.typeInfoIndex);
            codeStream.putShort(info.nameAndTypeDescriptorIndex);
            return null;
        }

        @Override
        public Void visitMethodHandle(final MethodHandle info, final CodeStream codeStream) {
            codeStream.putByte(info.getTag().value);
            codeStream.putShort(info.referenceKind.ordinal());
            codeStream.putShort(info.referenceIndex);
            return null;
        }

        @Override
        public Void visitMethodType(final MethodType info, final CodeStream codeStream) {
            codeStream.putByte(info.getTag().value);
            codeStream.putShort(info.descriptorIndex);
            return null;
        }

        @Override
        public Void visitStringConstant(final StringConstant info, final CodeStream codeStream) {
            codeStream.putByte(info.getTag().value);
            codeStream.putShort(info.stringIndex);
            return null;
        }

        @Override
        public Void visitUtf8StringConstant(final Utf8StringConstant info, final CodeStream codeStream) {
            codeStream.putByte(info.getTag().value);
            codeStream.putUtf8(info.value);
            return null;
        }
    }

    public static final class TypeInfo extends Entry {
        public final int nameIndex;

        public TypeInfo(final ConstantPool owner, final int nameIndex) {
            super(owner);
            this.nameIndex = nameIndex;
            owner._newKey.set(getTag(), nameIndex);
            owner._entryMap.put(owner._newKey.clone(), this);
            owner._newKey.clear();
        }

        public String getName() {
            return ((Utf8StringConstant)owner.get(nameIndex, Tag.Utf8StringConstant)).value;
        }

        @Override
        public Tag getTag() {
            return Tag.TypeInfo;
        }

        @Override
        public int byteLength() {
            return 3;
        }

        @Override
        public <R, D> R accept(final Visitor<R, D> visitor, final D data) {
            return visitor.visitTypeInfo(this, data);
        }

        @Override
        public String toString() {
            return "TypeIndex[index: " + index + ", nameIndex: " + nameIndex + "]";
        }
    }

    public static final class MethodType extends Entry {
        public final int descriptorIndex;

        public MethodType(final ConstantPool owner, final int descriptorIndex) {
            super(owner);
            this.descriptorIndex = descriptorIndex;
            owner._newKey.set(getTag(), descriptorIndex);
            owner._entryMap.put(owner._newKey.clone(), this);
            owner._newKey.clear();
        }

        public String getType() {
            return ((Utf8StringConstant)owner.get(descriptorIndex, Tag.Utf8StringConstant)).value;
        }

        @Override
        public Tag getTag() {
            return Tag.MethodType;
        }

        @Override
        public int byteLength() {
            return 3;
        }

        @Override
        public <R, D> R accept(final Visitor<R, D> visitor, final D data) {
            return visitor.visitMethodType(this, data);
        }

        @Override
        public String toString() {
            return "MethodType[index: " + index + ", descriptorIndex: " + descriptorIndex + "]";
        }
    }

    public static abstract class ReferenceEntry extends Entry {
        public final Tag tag;
        public final int typeInfoIndex;
        public final int nameAndTypeDescriptorIndex;

        protected ReferenceEntry(final ConstantPool cp, final Tag tag, final int typeInfoIndex, final int nameAndTypeDescriptorIndex) {
            super(cp);
            this.tag = tag;
            this.typeInfoIndex = typeInfoIndex;
            this.nameAndTypeDescriptorIndex = nameAndTypeDescriptorIndex;
            owner._newKey.set(tag, typeInfoIndex, nameAndTypeDescriptorIndex);
            owner._entryMap.put(owner._newKey.clone(), this);
            owner._newKey.clear();
        }

        public Tag getTag() {
            return tag;
        }

        public int byteLength() {
            return 5;
        }

        public TypeInfo getClassInfo() {
            return (TypeInfo)owner.get(typeInfoIndex, Tag.TypeInfo);
        }

        public String getClassName() {
            return getClassInfo().getName();
        }

        public NameAndTypeDescriptor getNameAndTypeInfo() {
            return (NameAndTypeDescriptor)owner.get(nameAndTypeDescriptorIndex, Tag.NameAndTypeDescriptor);
        }

        @Override
        public String toString() {
            return getClass().getSimpleName() + "[index: " +
                   index +
                   ", typeInfoIndex: " +
                   this.typeInfoIndex +
                   ", nameAndTypeDescriptorIndex: " +
                   this.nameAndTypeDescriptorIndex +
                   "]";
        }
    }

    public static final class FieldReference extends ReferenceEntry {
        public FieldReference(final ConstantPool owner, final int typeIndex, final int nameAndTypeDescriptorIndex) {
            super(owner, Tag.FieldReference, typeIndex, nameAndTypeDescriptorIndex);
        }

        @Override
        public <R, D> R accept(final Visitor<R, D> visitor, final D data) {
            return visitor.visitFieldReference(this, data);
        }
    }

    public static final class MethodReference extends ReferenceEntry {
        public MethodReference(final ConstantPool owner, final int typeIndex, final int nameAndTypeDescriptorIndex) {
            super(owner, Tag.MethodReference, typeIndex, nameAndTypeDescriptorIndex);
        }

        @Override
        public <R, D> R accept(final Visitor<R, D> visitor, final D data) {
            return visitor.visitMethodReference(this, data);
        }
    }

    public static final class InterfaceMethodReference extends ReferenceEntry {
        public InterfaceMethodReference(final ConstantPool owner, final int typeIndex, final int nameAndTypeDescriptorIndex) {
            super(owner, Tag.InterfaceMethodReference, typeIndex, nameAndTypeDescriptorIndex);
        }

        @Override
        public <R, D> R accept(final Visitor<R, D> visitor, final D data) {
            return visitor.visitInterfaceMethodReference(this, data);
        }
    }

    public static class MethodHandle extends Entry {
        public final ReferenceKind referenceKind;
        public final int referenceIndex;

        public MethodHandle(final ConstantPool owner, final ReferenceKind referenceKind, final int referenceIndex) {
            super(owner);
            this.referenceKind = referenceKind;
            this.referenceIndex = referenceIndex;
            owner._newKey.set(getTag(), referenceIndex, referenceKind);
            owner._entryMap.put(owner._newKey.clone(), this);
            owner._newKey.clear();
        }

        public ReferenceEntry getReference() {
            final Tag actual = owner.get(referenceIndex).getTag();

            Tag expected = Tag.MethodReference;

            // allow these tag types also:
            switch (actual) {
                case FieldReference:
                case InterfaceMethodReference:
                    expected = actual;
            }

            return (ReferenceEntry)owner.get(referenceIndex, expected);
        }

        @Override
        public Tag getTag() {
            return Tag.MethodHandle;
        }

        @Override
        public int byteLength() {
            return 4;
        }

        @Override
        public <R, D> R accept(final Visitor<R, D> visitor, final D data) {
            return visitor.visitMethodHandle(this, data);
        }
    }

    public static class NameAndTypeDescriptor extends Entry {
        public final int nameIndex;
        public final int typeDescriptorIndex;

        public NameAndTypeDescriptor(final ConstantPool owner, final int nameIndex, final int typeDescriptorIndex) {
            super(owner);
            this.nameIndex = nameIndex;
            this.typeDescriptorIndex = typeDescriptorIndex;
            owner._newKey.set(getTag(), nameIndex, typeDescriptorIndex);
            owner._entryMap.put(owner._newKey.clone(), this);
            owner._newKey.clear();
        }

        public Tag getTag() {
            return Tag.NameAndTypeDescriptor;
        }

        public int byteLength() {
            return 5;
        }

        public String getName() {
            return ((Utf8StringConstant)owner.get(nameIndex, Tag.Utf8StringConstant)).value;
        }

        public String getType() {
            return ((Utf8StringConstant)owner.get(typeDescriptorIndex, Tag.Utf8StringConstant)).value;
        }

        public <R, D> R accept(final Visitor<R, D> visitor, final D data) {
            return visitor.visitNameAndTypeDescriptor(this, data);
        }

        @Override
        public String toString() {
            return "NameAndTypeDescriptor[index: " + index + ", descriptorIndex: " + nameIndex + ", typeDescriptorIndex: " + typeDescriptorIndex + "]";
        }
    }

    public static class InvokeDynamicInfo extends Entry {
        public final int bootstrapMethodAttributeIndex;
        public final int nameAndTypeDescriptorIndex;

        public InvokeDynamicInfo(
            final ConstantPool owner,
            final int bootstrapMethodAttributeIndex,
            final int nameAndTypeDescriptorIndex) {

            super(owner);
            this.bootstrapMethodAttributeIndex = bootstrapMethodAttributeIndex;
            this.nameAndTypeDescriptorIndex = nameAndTypeDescriptorIndex;
            owner._newKey.set(getTag(), bootstrapMethodAttributeIndex, nameAndTypeDescriptorIndex);
            owner._entryMap.put(owner._newKey.clone(), this);
            owner._newKey.clear();
        }

        public Tag getTag() {
            return Tag.InvokeDynamicInfo;
        }

        public int byteLength() {
            return 5;
        }

        public NameAndTypeDescriptor getNameAndTypeDescriptor() {
            return (NameAndTypeDescriptor)owner.get(nameAndTypeDescriptorIndex, Tag.NameAndTypeDescriptor);
        }

        public <R, D> R accept(final Visitor<R, D> visitor, final D data) {
            return visitor.visitInvokeDynamicInfo(this, data);
        }

        @Override
        public String toString() {
            return "InvokeDynamicInfo[bootstrapMethodAttributeIndex: " +
                   bootstrapMethodAttributeIndex +
                   ", nameAndTypeDescriptorIndex: " +
                   nameAndTypeDescriptorIndex +
                   "]";
        }
    }

    public static final class DoubleConstant extends Entry {
        public final double value;

        public DoubleConstant(final ConstantPool owner, final double value) {
            super(owner);
            this.value = value;
            owner._newKey.set(value);
            owner._entryMap.put(owner._newKey.clone(), this);
            owner._newKey.clear();
        }

        @Override
        public Tag getTag() {
            return Tag.DoubleConstant;
        }

        @Override
        public int size() {
            return 2;
        }

        @Override
        public int byteLength() {
            return 9;
        }

        @Override
        public <R, D> R accept(final Visitor<R, D> visitor, final D data) {
            return visitor.visitDoubleConstant(this, data);
        }

        @Override
        public String toString() {
            return "DoubleConstant[index: " + index + ", value: " + value + "]";
        }
    }

    public static final class FloatConstant extends Entry {
        public final float value;

        public FloatConstant(final ConstantPool owner, final float value) {
            super(owner);
            this.value = value;
            owner._newKey.set(value);
            owner._entryMap.put(owner._newKey.clone(), this);
            owner._newKey.clear();
        }

        @Override
        public Tag getTag() {
            return Tag.FloatConstant;
        }

        @Override
        public int byteLength() {
            return 5;
        }

        @Override
        public <R, D> R accept(final Visitor<R, D> visitor, final D data) {
            return visitor.visitFloatConstant(this, data);
        }

        @Override
        public String toString() {
            return "FloatConstant[index: " + index + ", value: " + value + "]";
        }
    }

    public static final class IntegerConstant extends Entry {
        public final int value;

        public IntegerConstant(final ConstantPool owner, final int value) {
            super(owner);
            this.value = value;
            owner._newKey.set(value);
            owner._entryMap.put(owner._newKey.clone(), this);
            owner._newKey.clear();
        }

        @Override
        public Tag getTag() {
            return Tag.IntegerConstant;
        }

        @Override
        public int byteLength() {
            return 5;
        }

        @Override
        public <R, D> R accept(final Visitor<R, D> visitor, final D data) {
            return visitor.visitIntegerConstant(this, data);
        }

        @Override
        public String toString() {
            return "IntegerConstant[index: " + index + ", value: " + value + "]";
        }
    }

    public static final class LongConstant extends Entry {
        public final long value;

        public LongConstant(final ConstantPool owner, final long value) {
            super(owner);
            this.value = value;
            owner._newKey.set(value);
            owner._entryMap.put(owner._newKey.clone(), this);
            owner._newKey.clear();
        }

        @Override
        public Tag getTag() {
            return Tag.LongConstant;
        }

        @Override
        public int byteLength() {
            return 9;
        }

        @Override
        public int size() {
            return 2;
        }

        @Override
        public <R, D> R accept(final Visitor<R, D> visitor, final D data) {
            return visitor.visitLongConstant(this, data);
        }

        @Override
        public String toString() {
            return "LongConstant[index: " + index + ", value: " + value + "]";
        }
    }

    public static final class StringConstant extends Entry {
        public final int stringIndex;

        public StringConstant(final ConstantPool owner, final int stringIndex) {
            super(owner);
            this.stringIndex = stringIndex;
            owner._newKey.set(getTag(), stringIndex);
            owner._entryMap.put(owner._newKey.clone(), this);
            owner._newKey.clear();
        }

        public String getValue() {
            return ((Utf8StringConstant)owner.get(stringIndex)).value;
        }

        @Override
        public Tag getTag() {
            return Tag.StringConstant;
        }

        @Override
        public int byteLength() {
            return 3;
        }

        @Override
        public <R, D> R accept(final Visitor<R, D> visitor, final D data) {
            return visitor.visitStringConstant(this, data);
        }

        @Override
        public String toString() {
            return "StringConstant[index: " + index + ", stringIndex: " + stringIndex + "]";
        }
    }

    public static final class Utf8StringConstant extends Entry {
        public final String value;

        public Utf8StringConstant(final ConstantPool owner, final String value) {
            super(owner);
            this.value = value;
            owner._newKey.set(getTag(), value);
            owner._entryMap.put(owner._newKey.clone(), this);
            owner._newKey.clear();
        }

        @Override
        public Tag getTag() {
            return Tag.Utf8StringConstant;
        }

        @Override
        public int byteLength() {
            class SizeOutputStream extends OutputStream {
                @Override
                public void write(final int b) {
                    size++;
                }

                private int size;
            }

            final SizeOutputStream sizeOut = new SizeOutputStream();
            final DataOutputStream out = new DataOutputStream(sizeOut);

            try {
                out.writeUTF(value);
            }
            catch (IOException ignore) {
            }

            return 1 + sizeOut.size;
        }

        @Override
        public <R, D> R accept(final Visitor<R, D> visitor, final D data) {
            return visitor.visitUtf8StringConstant(this, data);
        }

        @Override
        public String toString() {
            return "Utf8StringConstant[index: " + index + ", value: " + value + "]";
        }
    }

    private static final class Key {
        private Tag _tag;
        private int _intValue;
        private long _longValue;
        private String _stringValue1;
        private String _stringValue2;
        private int _refIndex1 = -1;
        private int _refIndex2 = -1;
        private int _hashCode;

        public void clear() {
            _tag = null;
            _intValue = 0;
            _longValue = 0L;
            _stringValue1 = null;
            _stringValue2 = null;
            _refIndex1 = -1;
            _refIndex2 = -1;
        }

        public void set(final int intValue) {
            _tag = Tag.IntegerConstant;
            _intValue = intValue;
            _hashCode = 0x7FFFFFFF & (_tag.value + _intValue);
        }

        public void set(final long longValue) {
            _tag = Tag.LongConstant;
            _longValue = longValue;
            _hashCode = 0x7FFFFFFF & (_tag.value + (int)longValue);
        }

        public void set(final float floatValue) {
            _tag = Tag.FloatConstant;
            _intValue = Float.floatToRawIntBits(floatValue);
            _hashCode = 0x7FFFFFFF & (_tag.value + _intValue);
        }

        public void set(final double doubleValue) {
            _tag = Tag.DoubleConstant;
            _longValue = Double.doubleToRawLongBits(doubleValue);
            _hashCode = 0x7FFFFFFF & (_tag.value + (int)_longValue);
        }

        public void set(final String utf8Value) {
            _tag = Tag.Utf8StringConstant;
            _stringValue1 = utf8Value;
            _hashCode = HashUtilities.combineHashCodes(_tag, utf8Value);
        }

        public void set(
            final Tag tag,
            final int refIndex1,
            final ReferenceKind refKind) {

            _tag = tag;
            _refIndex1 = refIndex1;
            _refIndex2 = refKind.tag;
            _hashCode = HashUtilities.combineHashCodes(tag, refIndex1);
        }

        public void set(
            final Tag tag,
            final int refIndex1) {

            _tag = tag;
            _refIndex1 = refIndex1;
            _hashCode = HashUtilities.combineHashCodes(tag, refIndex1);
        }

        public void set(
            final Tag tag,
            final int refIndex1,
            final int refIndex2) {

            _tag = tag;
            _refIndex1 = refIndex1;
            _refIndex2 = refIndex2;
            _hashCode = HashUtilities.combineHashCodes(tag, refIndex1, refIndex2);
        }

        public void set(
            final Tag tag,
            final String stringValue1) {

            _tag = tag;
            _stringValue1 = stringValue1;
            _hashCode = HashUtilities.combineHashCodes(tag, stringValue1);
        }

        public void set(
            final Tag tag,
            final String stringValue1,
            final String stringValue2) {

            _tag = tag;
            _stringValue1 = stringValue1;
            _stringValue2 = stringValue2;
            _hashCode = HashUtilities.combineHashCodes(tag, stringValue1, stringValue2);
        }

        @Override
        @SuppressWarnings("CloneDoesntCallSuperClone")
        protected Key clone() {
            final Key key = new Key();
            key._tag = _tag;
            key._hashCode = _hashCode;
            key._intValue = _intValue;
            key._longValue = _longValue;
            key._stringValue1 = _stringValue1;
            key._stringValue2 = _stringValue2;
            key._refIndex1 = _refIndex1;
            key._refIndex2 = _refIndex2;
            return key;
        }

        @Override
        public int hashCode() {
            return _hashCode;
        }

        @Override
        public boolean equals(final Object obj) {
            if (!(obj instanceof Key)) {
                return false;
            }

            final Key key = (Key)obj;
            if (key._tag != _tag) {
                return false;
            }

            switch (_tag) {
                case Utf8StringConstant:
                    return StringUtilities.equals(key._stringValue1, _stringValue1);

                case IntegerConstant:
                case FloatConstant:
                    return key._intValue == _intValue;

                case LongConstant:
                case DoubleConstant:
                    return key._longValue == _longValue;

                case TypeInfo:
                case StringConstant:
                case MethodType:
                    return key._refIndex1 == _refIndex1;

                case NameAndTypeDescriptor:
                case FieldReference:
                case MethodReference:
                case InterfaceMethodReference:
                case MethodHandle:
                case InvokeDynamicInfo:
                    return key._refIndex1 == _refIndex1 &&
                           key._refIndex2 == _refIndex2;
            }

            return false;
        }
    }
}
