
/*
 * Janino - An embedded Java[TM] compiler
 *
 * Copyright (c) 2021 Arno Unkrig. 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 holder 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 HOLDER 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.codehaus.janino.util.signature;

import java.io.EOFException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.codehaus.commons.nullanalysis.Nullable;
import org.codehaus.janino.util.charstream.StringCharStream;
import org.codehaus.janino.util.charstream.UnexpectedCharacterException;

/**
 * Helper class for parsing signatures and descriptors. See
 * <a href="http://java.sun.com/docs/books/jvms/second_edition/ClassFileFormat-Java5.pdf">Java 5 class file format</a>,
 * section 4.4.4, "Signatures".
 * <p>
 *   The various structures that the parser returns (e.g. {@link ClassTypeSignature}) all have {@link
 *   Object#toString()} methods that convert them into nice, human-readable strings. This conversion can be customized
 *   using {@link #SignatureParser(Options)} and passing a custom {@link Options} object.
 * </p>
 */
public
class SignatureParser {

    /**
     * @see SignatureParser
     * @see SignatureParser#SignatureParser(Options)
     */
    public
    interface Options {

        /**
         * Optionally modifies package name prefixes before they are used in the various {@link #toString()} methods.
         *
         * @param  packageSpecifier E.g. {@code ""} (the root package) or {@code "java.lang."}
         * @return                  The <var>packageSpecifier</var>, or a beautified version thereof
         */
        String beautifyPackageNamePrefix(String packageSpecifier);
    }

    /**
     * A trivial implementation of {@link Options}.
     */
    public static final Options DEFAULT_OPTIONS = new Options() {

        @Override public String
        beautifyPackageNamePrefix(String packageSpecifier) {
            return packageSpecifier;
        }
    };

    private Options options = SignatureParser.DEFAULT_OPTIONS; // Initialize early to avoid an NPE!

    public
    SignatureParser() {}

    public
    SignatureParser(Options options) { this.options = options; }

    /**
     * Decodes a 'class signature' as defined in JVMS7 4.3.4 / JVMS8 4.7.9.1.
     */
    public ClassSignature
    decodeClassSignature(String s) throws SignatureException {
        try {
            StringCharStream scs = new StringCharStream(s);

            ClassSignature cls = this.parseClassSignature(scs);
            scs.eoi();
            return cls;
        } catch (SignatureException e) {
            throw new SignatureException("Class signature '" + s + "': " + e.getMessage(), e);
        } catch (EOFException e) {
            throw new SignatureException("Class signature '" + s + "': " + e.getMessage(), e);
        } catch (UnexpectedCharacterException e) {
            throw new SignatureException("Class signature '" + s + "': " + e.getMessage(), e);
        }
    }

    /**
     * Decodes a 'method type signature' as defined in JVMS7 4.3.4 / JVMS8 4.7.9.1.
     */
    public MethodTypeSignature
    decodeMethodTypeSignature(String s) throws SignatureException {
        try {
            StringCharStream          scs = new StringCharStream(s);
            final MethodTypeSignature mts = this.parseMethodTypeSignature(scs);
            scs.eoi();
            return mts;
        } catch (SignatureException e) {
            throw new SignatureException("Method type signature '" + s + "': " + e.getMessage(), e);
        } catch (EOFException e) {
            throw new SignatureException("Method type signature '" + s + "': " + e.getMessage(), e);
        } catch (UnexpectedCharacterException e) {
            throw new SignatureException("Method type signature '" + s + "': " + e.getMessage(), e);
        }
    }

    /**
     * Decodes a 'type signature' as defined in JVMS7 4.3.4 / JVMS8 4.7.9.1.
     */
    public TypeSignature
    decodeTypeSignature(String s) throws SignatureException {
        try {
            StringCharStream    scs = new StringCharStream(s);
            final TypeSignature ts  = this.parseTypeSignature(scs);
            scs.eoi();
            return ts;
        } catch (SignatureException e) {
            throw new SignatureException("Field type signature '" + s + "': " + e.getMessage(), e);
        } catch (EOFException e) {
            throw new SignatureException("Field type signature '" + s + "': " + e.getMessage(), e);
        } catch (UnexpectedCharacterException e) {
            throw new SignatureException("Field type signature '" + s + "': " + e.getMessage(), e);
        }
    }

    /**
     * Decodes a 'field type signature' as defined in JVMS7 4.3.4 / JVMS8 4.7.9.1.
     */
    public FieldTypeSignature
    decodeFieldTypeSignature(String s) throws SignatureException {
        try {
            StringCharStream         scs = new StringCharStream(s);
            final FieldTypeSignature fts = this.parseFieldTypeSignature(scs);
            scs.eoi();
            return fts;
        } catch (SignatureException e) {
            throw new SignatureException("Field type signature '" + s + "': " + e.getMessage(), e);
        } catch (EOFException e) {
            throw new SignatureException("Field type signature '" + s + "': " + e.getMessage(), e);
        } catch (UnexpectedCharacterException e) {
            throw new SignatureException("Field type signature '" + s + "': " + e.getMessage(), e);
        }
    }

    /**
     * Decodes a 'method descriptor' as defined in JVMS[78] 4.3.3.
     *
     * @return E.g. {@code "(Object[]) => java.util.stream.Stream"} or {@code "join()"} (void method)
     */
    public MethodTypeSignature
    decodeMethodDescriptor(String s) throws SignatureException {
        try {
            StringCharStream          scs = new StringCharStream(s);
            final MethodTypeSignature mts = this.parseMethodDescriptor(scs);
            scs.eoi();
            return mts;
        } catch (SignatureException e) {
            throw new SignatureException("Method descriptor '" + s + "': " + e.getMessage(), e);
        } catch (EOFException e) {
            throw new SignatureException("Method descriptor '" + s + "': " + e.getMessage(), e);
        } catch (UnexpectedCharacterException e) {
            throw new SignatureException("Method descriptor '" + s + "': " + e.getMessage(), e);
        }
    }

    private TypeSignature
    decodeClassName(String internalName) {
        String className = internalName.replace('/', '.');

        int idx = className.lastIndexOf('.') + 1;

        final String packageNamePrefix = className.substring(0, idx);
        final String simpleClassName   = className.substring(idx);

        return new TypeSignature() {

            @Override public String
            toString() {
                return SignatureParser.this.options.beautifyPackageNamePrefix(packageNamePrefix) + simpleClassName;
            }
        };
    }

    /**
     * Decodes a 'field descriptor' as defined in JLS7 4.3.2.
     */
    public TypeSignature
    decodeFieldDescriptor(String s) throws SignatureException {
        try {
            StringCharStream    scs = new StringCharStream(s);
            final TypeSignature ts  = this.parseFieldDescriptor(scs);
            scs.eoi();
            return ts;
        } catch (SignatureException e) {
            throw new SignatureException("Field descriptor '" + s + "': " + e.getMessage(), e);
        } catch (EOFException e) {
            throw new SignatureException("Field descriptor '" + s + "': " + e.getMessage(), e);
        } catch (UnexpectedCharacterException e) {
            throw new SignatureException("Field descriptor '" + s + "': " + e.getMessage(), e);
        }
    }

    /**
     * Decodes a "class-name-or-field-descriptor" as defined in JLS8 4.4.1 ("name_index").
     */
    public TypeSignature
    decodeClassNameOrFieldDescriptor(String s) throws SignatureException {

        if (Character.isJavaIdentifierStart(s.charAt(0))) {
            return this.decodeClassName(s);
        }

        return this.decodeFieldDescriptor(s);
    }

    /**
     * Decodes a 'return type' as defined in JVMS7 4.3.4 / JVMS8 4.7.9.1.
     */
    public TypeSignature
    decodeReturnType(String s) throws SignatureException {
        try {
            StringCharStream    scs = new StringCharStream(s);
            final TypeSignature ts  = this.parseReturnType(scs);
            scs.eoi();
            return ts;
        } catch (SignatureException e) {
            throw new SignatureException("Return type '" + s + "': " + e.getMessage(), e);
        } catch (EOFException e) {
            throw new SignatureException("Return type '" + s + "': " + e.getMessage(), e);
        } catch (UnexpectedCharacterException e) {
            throw new SignatureException("Return type '" + s + "': " + e.getMessage(), e);
        }
    }

    /**
     * Representation of the "MethodTypeSignature" clause.
     */
    public static
    class MethodTypeSignature {

        /**
         * The formal types of the method, e.g. '{@code void <T, U> int meth(T t, U u) ...}'.
         */
        public final List<FormalTypeParameter> formalTypeParameters;

        /**
         * The types of the method's parameters.
         */
        public final List<TypeSignature> parameterTypes;

        /**
         * The return type of the method.
         */
        public final TypeSignature returnType;

        /**
         * The exceptions declared for the method.
         */
        public final List<ThrowsSignature> thrownTypes;

        public
        MethodTypeSignature(
            List<FormalTypeParameter> formalTypeParameters,
            List<TypeSignature>       parameterTypes,
            TypeSignature             returnType,
            List<ThrowsSignature>     thrownTypes
        ) {
            this.formalTypeParameters = formalTypeParameters;
            this.parameterTypes       = parameterTypes;
            this.returnType           = returnType;
            this.thrownTypes          = thrownTypes;
        }

        /**
         * Combines the name of the declaring class, the name of the method and this method type signature into a nice,
         * human-readable string like '{@code <T> MyClass.meth(List<T> l, int i) => double}'.
         */
        public String
        toString(String declaringClassName, String methodName) {
            StringBuilder sb = new StringBuilder();

            // Formal type parameters.
            if (!this.formalTypeParameters.isEmpty()) {
                Iterator<FormalTypeParameter> it = this.formalTypeParameters.iterator();
                sb.append('<' + it.next().toString());
                while (it.hasNext()) sb.append(", " + it.next().toString());
                sb.append("> ");
            }

            // Name.
            if ("<init>".equals(methodName) && this.returnType == SignatureParser.VOID) {
                sb.append(declaringClassName);
            } else
            {
                sb.append(declaringClassName).append('.').append(methodName);
            }
            sb.append('(');
            Iterator<TypeSignature> it = this.parameterTypes.iterator();
            if (it.hasNext()) {
                for (;;) {
                    sb.append(it.next().toString());
                    if (!it.hasNext()) break;
                    sb.append(", ");
                }
            }
            sb.append(')');

            if (!this.thrownTypes.isEmpty()) {
                Iterator<ThrowsSignature> it2 = this.thrownTypes.iterator();
                sb.append(" throws ").append(it2.next());
                while (it.hasNext()) sb.append(", ").append(it2.next());
            }

            if (this.returnType != SignatureParser.VOID) sb.append(" => ").append(this.returnType.toString());
            return sb.toString();
        }

        @Override public String
        toString() {
            StringBuilder sb = new StringBuilder();

            // Formal type parameters.
            if (!this.formalTypeParameters.isEmpty()) {
                Iterator<FormalTypeParameter> it = this.formalTypeParameters.iterator();
                sb.append('<' + it.next().toString());
                while (it.hasNext()) sb.append(", " + it.next().toString());
                sb.append("> ");
            }

            sb.append('(');
            Iterator<TypeSignature> it = this.parameterTypes.iterator();
            if (it.hasNext()) {
                sb.append(it.next());
                while (it.hasNext()) sb.append(", ").append(it.next());
            }
            sb.append(')');

            if (this.returnType != SignatureParser.VOID) sb.append(" => ").append(this.returnType.toString());

            return sb.toString();
        }
    }

    /**
     * Representation of the "ClassSignature" clause.
     */
    public static
    class ClassSignature {

        /**
         * The class's formal type parameters, e.g. '{@code class MyMap<K, V> ...}'.
         */
        public final List<FormalTypeParameter> formalTypeParameters;

        /**
         * The class's superclass type.
         */
        public ClassTypeSignature superclassSignature;

        /**
         * The interfaces that the class implements.
         */
        public final List<ClassTypeSignature> superinterfaceSignatures;

        public
        ClassSignature(
            List<FormalTypeParameter> formalTypeParameters,
            ClassTypeSignature        superclassSignature,
            List<ClassTypeSignature>  superinterfaceSignatures
        ) {
            this.formalTypeParameters     = formalTypeParameters;
            this.superclassSignature      = superclassSignature;
            this.superinterfaceSignatures = superinterfaceSignatures;
        }

        /**
         * Combines the name of the class and this class signature into a nice, human-readable string like '{@code
         * MyMap<K, V> extends SomeClass implements Interface1, Interface2}'.
         */
        public String
        toString(String className) {
            StringBuilder sb = new StringBuilder(className);
            if (!this.formalTypeParameters.isEmpty()) {
                Iterator<FormalTypeParameter> it = this.formalTypeParameters.iterator();
                sb.append('<').append(it.next().toString());
                while (it.hasNext()) sb.append(", ").append(it.next().toString());
                sb.append('>');
            }
            sb.append(" extends ").append(this.superclassSignature.toString());
            if (!this.superinterfaceSignatures.isEmpty()) {
                Iterator<ClassTypeSignature> it = this.superinterfaceSignatures.iterator();
                sb.append(" implements ").append(it.next().toString());
                while (it.hasNext()) sb.append(", ").append(it.next().toString());
            }
            return sb.toString();
        }
    }

    /**
     * Representation of the "ClassTypeSignature" clause, e.g. '{@code pkg.Outer<T>.Inner<U>}'.
     */
    public static
    class ClassTypeSignature implements ThrowsSignature, FieldTypeSignature {

        /**
         * <pre>{ identifier '/' }</pre>
         */
        public final String packageSpecifier;

        /**
         * <pre>identifier</pre>
         */
        public final String simpleClassName;

        /**
         * The {@link TypeArgument}s of this class.
         */
        public final List<TypeArgument> typeArguments;

        /**
         * The nested types.
         */
        public final List<SimpleClassTypeSignature> suffixes;

        private final Options options;

        /**
         * @param packageSpecifier <code>{ identifier '/' }</code>
         */
        public
        ClassTypeSignature(
            String                         packageSpecifier,
            String                         simpleClassName,
            List<TypeArgument>             typeArguments,
            List<SimpleClassTypeSignature> suffixes,
            Options                        options
        ) {
            this.packageSpecifier = packageSpecifier;
            this.simpleClassName  = simpleClassName;
            this.typeArguments    = typeArguments;
            this.suffixes         = suffixes;
            this.options          = options;
        }

        @Override public <T, EX extends Throwable> T
        accept(FieldTypeSignatureVisitor<T, EX> visitor) throws EX { return visitor.visitClassTypeSignature(this); }

        /**
         * Converts this class type signature into a nice, human-readable string, e.g. {@code "pkg.Outer<T>.Inner<U>"}.
         */
        @Override public String
        toString() {

            String packageNamePrefix = this.packageSpecifier.replace('/', '.');

            StringBuilder sb = (
                new StringBuilder()
                .append(this.options.beautifyPackageNamePrefix(packageNamePrefix))
                .append(this.simpleClassName)
            );

            if (!this.typeArguments.isEmpty()) {
                Iterator<TypeArgument> it = this.typeArguments.iterator();
                sb.append('<').append(it.next().toString());
                while (it.hasNext()) {
                    sb.append(", ").append(it.next().toString());
                }
                sb.append('>');
            }
            for (SimpleClassTypeSignature suffix : this.suffixes) {
                sb.append('.').append(suffix.toString());
            }
            return sb.toString();
        }
    }

    /**
     * The class type signature of the {@link Object} class.
     */
    public final ClassTypeSignature
    object = new ClassTypeSignature(
        "java/lang/",
        "Object",
        Collections.<TypeArgument>emptyList(),
        Collections.<SimpleClassTypeSignature>emptyList(),
        this.options
    );

    /**
     * Representation of the "SimpleClassTypeSignature" clause, e.g. '{@code MyMap<K, V>}'.
     */
    public static
    class SimpleClassTypeSignature {

        /**
         * The simple name of the class.
         */
        public final String simpleClassName;

        /**
         * The type arguments of the class, e.g. '{@code <A extends x, B super x, *, x>}'.
         */
        public final List<TypeArgument> typeArguments;

        public
        SimpleClassTypeSignature(String simpleClassName, List<TypeArgument> typeArguments) {
            this.simpleClassName = simpleClassName;
            this.typeArguments   = typeArguments;
        }

        /**
         * Converts this simple class type signature into a nice, human-readable string like '{@code MyClass<U>}'.
         */
        @Override public String
        toString() {
            StringBuilder sb = new StringBuilder(this.simpleClassName);
            if (!this.typeArguments.isEmpty()) {
                Iterator<TypeArgument> it = this.typeArguments.iterator();
                sb.append('<').append(it.next().toString());
                while (it.hasNext()) sb.append(", ").append(it.next().toString());
                sb.append('>');
            }
            return sb.toString();
        }
    }

    /**
     * Representation of the "ArrayTypeSignature" clause. The array's component have a {@link TypeSignature}.
     */
    public static
    class ArrayTypeSignature implements FieldTypeSignature {

        /**
         * The type of the array components.
         */
        public final TypeSignature componentTypeSignature;

        public
        ArrayTypeSignature(TypeSignature componentTypeSignature) {
            this.componentTypeSignature = componentTypeSignature;
        }

        @Override public <T, EX extends Throwable> T
        accept(FieldTypeSignatureVisitor<T, EX> visitor) throws EX { return visitor.visitArrayTypeSignature(this); }

        @Override public String toString() { return this.componentTypeSignature.toString() + "[]"; }
    }

    /**
     * Representation of the "TypeVariableSignature" clause, e.g. '{@code T}'.
     */
    public static
    class TypeVariableSignature implements ThrowsSignature, FieldTypeSignature {

        /**
         * The name of the type variable, e.g. '{@code T}'.
         */
        public String identifier;

        public
        TypeVariableSignature(String identifier) { this.identifier = identifier; }

        @Override public <T, EX extends Throwable> T
        accept(FieldTypeSignatureVisitor<T, EX> visitor) throws EX { return visitor.visitTypeVariableSignature(this); }

        @Override public String toString() { return this.identifier; }
    }

    /**
     * Representation of the "TypeSignature" clause. A 'type signature' is one of
     * <dl>
     *   <dt>{@link PrimitiveTypeSignature}:</dt>
     *   <dd>'{@code byte}', '{@code int}', etc., but <em>not</em> '{@code void}'</dd>
     *   <dt>{@link FieldTypeSignature}:</dt>
     *   <dd>
     *     One of:
     *     <dl>
     *       <dd>{@link ClassTypeSignature} (e.g. '{@code pkg.Outer<T>.Inner<U>}')</dd>
     *       <dd>{@link ArrayTypeSignature}</dd>
     *       <dd>{@link TypeVariableSignature} (e.g. '{@code T}')</dd>
     *     </dl>
     *   </dd>
     *   <dt>{@link SignatureParser#VOID}</dt>
     * </dl>
     */
    public
    interface TypeSignature {
        @Override String toString();
    }

    /**
     * Representation of the "ThrowsSignature" clause.
     */
    public
    interface ThrowsSignature {
    }

    /**
     * Representation of the "FormalTypeParameter" clause, e.g. '{@code T extends MyClass & MyInterface}'.
     */
    public static
    class FormalTypeParameter {

        /**
         * The name of the formal type parameter, e.g. '{@code T}'.
         */
        public final String identifier;

        /**
         * The class that this formal type parameter (optionally) extends.
         */
        @Nullable public final FieldTypeSignature classBound;

        /**
         * The interfaces that this formal type parameter (optionally) extends.
         */
        public final List<FieldTypeSignature> interfaceBounds;

        public
        FormalTypeParameter(
            String                       identifier,
            @Nullable FieldTypeSignature classBound,
            List<FieldTypeSignature>     interfaceBounds
        ) {
            this.identifier      = identifier;
            this.classBound      = classBound;
            this.interfaceBounds = interfaceBounds;
        }

        @Override public String
        toString() {
            FieldTypeSignature cb = this.classBound;
            if (cb == null) {
                Iterator<FieldTypeSignature> it = this.interfaceBounds.iterator();
                if (!it.hasNext()) return this.identifier;

                StringBuilder sb = new StringBuilder(this.identifier).append(" extends ").append(it.next().toString());
                while (it.hasNext()) sb.append(" & ").append(it.next().toString());
                return sb.toString();
            } else {
                StringBuilder sb = new StringBuilder(this.identifier).append(" extends ").append(cb.toString());
                for (FieldTypeSignature ib : this.interfaceBounds) sb.append(" & ").append(ib.toString());
                return sb.toString();
            }
        }
    }

    /**
     * Representation of the "PrimitiveTypeSignature" clause, i.e. '{@code byte}', '{@code int}', etc., but <em>not</em>
     * '{@code void}'.
     */
    public static
    class PrimitiveTypeSignature implements TypeSignature {

        /**
         * The name of the primitive type, e.g. '{@code int}'.
         */
        public final String typeName;

        PrimitiveTypeSignature(String typeName) {
            this.typeName = typeName;
        }

        @Override public String toString() { return this.typeName; }
    }

    /**
     * Representation of the "TypeArgument" clause.
     * <pre>
     *   type-argument :=
     *     'extends' {@link FieldTypeSignature field-type-signature}
     *     | 'super' {@link FieldTypeSignature field-type-signature}
     *     | '*'
     *     | {@link FieldTypeSignature field-type-signature}
     * </pre>
     */
    public static
    class TypeArgument {

        /**
         * @see TypeArgument
         */
        enum Mode { EXTENDS, SUPER, ANY, NONE }

        /**
         * @see TypeArgument
         */
        public final Mode mode;

        /**
         * Must be {@code} for {@link SignatureParser.TypeArgument.Mode#ANY}, non-{@code null} otherwise.
         *
         * @see TypeArgument
         */
        @Nullable public final FieldTypeSignature fieldTypeSignature;

        /**
         * @param fieldTypeSignature {@code null} iff {@code mode == ANY}
         */
        public
        TypeArgument(Mode mode, @Nullable FieldTypeSignature fieldTypeSignature) {
            assert mode == Mode.ANY ^ fieldTypeSignature != null;
            this.mode               = mode;
            this.fieldTypeSignature = fieldTypeSignature;
        }

        @Override public String
        toString() {

            FieldTypeSignature fts = this.fieldTypeSignature;

            switch (this.mode) {

            case EXTENDS:
                assert fts != null;
                return "extends " + fts.toString();

            case SUPER:
                assert fts != null;
                return "super " + fts.toString();

            case ANY:
                return "*";

            case NONE:
                assert fts != null;
                return fts.toString();

            default:
                throw new IllegalStateException();
            }
        }
    }

    /**
     * Representation of the "FieldTypeSignature" clause. A 'field type signature' is one of
     * <dl>
     *   <dt>{@link ClassTypeSignature}:
     *   <dd>E.g. '{@code pkg.Outer<T>.Inner<U>}'</dd>
     *   <dt>{@link ArrayTypeSignature}
     *   <dt>{@link TypeVariableSignature}:
     *   <dd>E.g. '{@code T}'</dd>
     * </dl>
     */
    public
    interface FieldTypeSignature extends TypeSignature {
        <T, EX extends Throwable> T accept(FieldTypeSignatureVisitor<T, EX> visitor) throws EX;
        @Override String            toString();
    }

    public
    interface FieldTypeSignatureVisitor<T, EX extends Throwable> {
        T visitArrayTypeSignature(ArrayTypeSignature ats) throws EX;
        T visitClassTypeSignature(ClassTypeSignature cts) throws EX;
        T visitTypeVariableSignature(TypeVariableSignature tvs) throws EX;
    }

    /**
     * The primitive '{@code byte}' type.
     */
    public static final PrimitiveTypeSignature BYTE = new PrimitiveTypeSignature("byte");

    /**
     * The primitive '{@code char}' type.
     */
    public static final PrimitiveTypeSignature CHAR = new PrimitiveTypeSignature("char");

    /**
     * The primitive '{@code double}' type.
     */
    public static final PrimitiveTypeSignature DOUBLE = new PrimitiveTypeSignature("double");

    /**
     * The primitive '{@code float}' type.
     */
    public static final PrimitiveTypeSignature FLOAT = new PrimitiveTypeSignature("float");

    /**
     * The primitive '{@code int}' type.
     */
    public static final PrimitiveTypeSignature INT = new PrimitiveTypeSignature("int");

    /**
     * The primitive '{@code long}' type.
     */
    public static final PrimitiveTypeSignature LONG = new PrimitiveTypeSignature("long");

    /**
     * The primitive '{@code short}' type.
     */
    public static final PrimitiveTypeSignature SHORT = new PrimitiveTypeSignature("short");

    /**
     * The primitive '{@code boolean}' type.
     */
    public static final PrimitiveTypeSignature BOOLEAN = new PrimitiveTypeSignature("boolean");

    /**
     * Representation of the 'void' type.
     */
    public static final TypeSignature
    VOID = new TypeSignature() { @Override public String toString() { return "void"; } };

    private TypeSignature
    parseFieldDescriptor(StringCharStream scs) throws EOFException, UnexpectedCharacterException, SignatureException {

        // A type signatures is a superset of a type descriptor, so use "parseTypeSignature()" for simplicity.
        return this.parseTypeSignature(scs);
    }

    private MethodTypeSignature
    parseMethodDescriptor(StringCharStream scs) throws EOFException, UnexpectedCharacterException, SignatureException {
        return this.parseMethodTypeSignature(scs);
    }

    private ClassSignature
    parseClassSignature(StringCharStream scs) throws EOFException, SignatureException, UnexpectedCharacterException {

        List<FormalTypeParameter> ftps = new ArrayList<>();
        if (scs.peekRead('<')) {
            while (!scs.peekRead('>')) ftps.add(this.parseFormalTypeParameter(scs));
        }

        final ClassTypeSignature cts = this.parseClassTypeSignature(scs);

        List<ClassTypeSignature> siss = new ArrayList<>();
        while (!scs.atEoi()) siss.add(this.parseClassTypeSignature(scs));

        return new ClassSignature(ftps, cts, siss);
    }

    private MethodTypeSignature
    parseMethodTypeSignature(StringCharStream scs)
    throws EOFException, UnexpectedCharacterException, SignatureException {

        List<FormalTypeParameter> ftps = new ArrayList<>();
        if (scs.peekRead('<')) {
            while (!scs.peekRead('>')) ftps.add(this.parseFormalTypeParameter(scs));
        }

        scs.read('(');

        List<TypeSignature> pts = new ArrayList<>();
        while (!scs.peekRead(')')) pts.add(this.parseTypeSignature(scs));

        final TypeSignature rt = this.parseReturnType(scs);

        List<ThrowsSignature> tts = new ArrayList<>();
        while (!scs.atEoi()) tts.add(this.parseThrowsSignature(scs));

        return new MethodTypeSignature(ftps, pts, rt, tts);
    }

    private TypeSignature
    parseReturnType(StringCharStream scs) throws EOFException, UnexpectedCharacterException, SignatureException {
        if (scs.peekRead('V')) return SignatureParser.VOID;
        return this.parseTypeSignature(scs);
    }

    private ThrowsSignature
    parseThrowsSignature(StringCharStream scs) throws EOFException, UnexpectedCharacterException, SignatureException {
        scs.read('^');
        return (
            scs.peek('T')
            ? SignatureParser.parseTypeVariableSignature(scs)
            : this.parseClassTypeSignature(scs)
        );
    }

    private ClassTypeSignature
    parseClassTypeSignature(StringCharStream scs)
    throws EOFException, UnexpectedCharacterException, SignatureException {
        scs.read('L');

        String             ps = "";
        String             scn;
        List<TypeArgument> tas = new ArrayList<>();

        PART1:
        for (;;) {
            String s = SignatureParser.parseIdentifier(scs);
            switch (scs.peek("<./;")) {
            case 0: // '<'
                scs.read('<');
                scn = s;
                while (!scs.peekRead('>')) tas.add(this.parseTypeArgument(scs));
                break PART1;
            case 1: // '.'
                scn = s;
                break PART1;
            case 2: // '/'
                ps += s + '/';
                scs.read();
                break;
            case 3: // ';'
                scn = s;
                break PART1;
            default:
                scs.read("<./;");
            }
        }

        List<SimpleClassTypeSignature> ss = new ArrayList<>();
        while (scs.peekRead('.')) {

            final String ir = SignatureParser.parseIdentifierRest(scs);

            List<TypeArgument> ta = new ArrayList<>();
            if (scs.peekRead('<')) while (!scs.peekRead('>')) ta.add(this.parseTypeArgument(scs));

            ss.add(new SimpleClassTypeSignature(ir, ta));
        }

        scs.read(';');

        return new ClassTypeSignature(ps, scn, tas, ss, this.options);
    }

    private static String
    parseIdentifier(StringCharStream scs) throws EOFException, SignatureException {
        char c = scs.read();
        if (!Character.isJavaIdentifierStart(c)) {
            throw new SignatureException("Identifier expected instead of '" + c + "'");
        }
        StringBuilder sb = new StringBuilder().append(c);
        for (;;) {
            if (Character.isJavaIdentifierPart(scs.peek())) {
                sb.append(scs.read());
            } else {
                return sb.toString();
            }
        }
    }

    private static String
    parseIdentifierRest(StringCharStream scs) throws EOFException, SignatureException {
        char c = scs.read();
        if (!Character.isJavaIdentifierPart(c)) {
            throw new SignatureException("Identifier rest expected instead of '" + c + "'");
        }
        StringBuilder sb = new StringBuilder().append(c);
        for (;;) {
            if (Character.isJavaIdentifierPart(scs.peek())) {
                sb.append(scs.read());
            } else {
                return sb.toString();
            }
        }
    }

    private static TypeVariableSignature
    parseTypeVariableSignature(StringCharStream scs)
    throws EOFException, UnexpectedCharacterException, SignatureException {
        scs.read('T');
        final String identifier = SignatureParser.parseIdentifier(scs);
        scs.read(';');
        return new TypeVariableSignature(identifier);
    }

    private FormalTypeParameter
    parseFormalTypeParameter(StringCharStream scs)
    throws EOFException, SignatureException, UnexpectedCharacterException {
        final String identifier = SignatureParser.parseIdentifier(scs);
        scs.read(':');

        final FieldTypeSignature cb = !scs.peek(':') ? this.parseFieldTypeSignature(scs) : null;

        List<FieldTypeSignature> ibs = new ArrayList<>();
        while (scs.peekRead(':')) ibs.add(this.parseFieldTypeSignature(scs));

        return new FormalTypeParameter(identifier, cb, ibs);
    }

    private TypeSignature
    parseTypeSignature(StringCharStream scs) throws EOFException, UnexpectedCharacterException, SignatureException {
        int idx = scs.peekRead("BCDFIJSZ");
        if (idx != -1) {
            return SignatureParser.PRIMITIVE_TYPES[idx];
        }
        return this.parseFieldTypeSignature(scs);
    }
    private static final PrimitiveTypeSignature[] PRIMITIVE_TYPES = {
        SignatureParser.BYTE,
        SignatureParser.CHAR,
        SignatureParser.DOUBLE,
        SignatureParser.FLOAT,
        SignatureParser.INT,
        SignatureParser.LONG,
        SignatureParser.SHORT,
        SignatureParser.BOOLEAN,
    };

    private FieldTypeSignature
    parseFieldTypeSignature(StringCharStream scs)
    throws EOFException, UnexpectedCharacterException, SignatureException {
        switch (scs.peek("L[T")) {
        case 0:
            return this.parseClassTypeSignature(scs);
        case 1:
            return this.parseArrayTypeSignature(scs);
        case 2:
            return SignatureParser.parseTypeVariableSignature(scs);
        default:
            throw new SignatureException(
                "Parsing field type signature \""
                + scs
                + "\": Class type signature, array type signature or type variable signature expected"
            );
        }
    }

    private FieldTypeSignature
    parseArrayTypeSignature(StringCharStream scs)
    throws EOFException, UnexpectedCharacterException, SignatureException {
        scs.read('[');
        return new ArrayTypeSignature(this.parseTypeSignature(scs));
    }

    private TypeArgument
    parseTypeArgument(StringCharStream scs) throws EOFException, UnexpectedCharacterException, SignatureException {

        if (scs.peekRead('+')) {
            return new TypeArgument(TypeArgument.Mode.EXTENDS, this.parseFieldTypeSignature(scs));
        }

        if (scs.peekRead('-')) {
            return new TypeArgument(TypeArgument.Mode.SUPER, this.parseFieldTypeSignature(scs));
        }

        if (scs.peekRead('*')) {
            return new TypeArgument(TypeArgument.Mode.ANY, null);
        }

        // E.g. "Comparable<java.lang.Boolean>"
        return new TypeArgument(TypeArgument.Mode.NONE, this.parseFieldTypeSignature(scs));
    }

    /**
     * Signalizes am malformed signature.
     */
    public static
    class SignatureException extends Exception {

        private static final long serialVersionUID = 1L;

        public
        SignatureException(String message) {
            super(message);
        }

        public
        SignatureException(String message, Throwable t) {
            super(message, t);
        }
    }
}
