/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch licenses this file to you under
 * the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.elasticsearch.painless;

import org.apache.lucene.util.Constants;
import org.apache.lucene.util.SetOnce;
import org.elasticsearch.painless.api.Augmentation;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.PrimitiveIterator;
import java.util.Spliterator;

/**
 * The entire API for Painless.  Also used as a whitelist for checking for legal
 * methods and fields during at both compile-time and runtime.
 */
public final class Definition {

    private static final List<String> DEFINITION_FILES = Collections.unmodifiableList(
        Arrays.asList("org.elasticsearch.txt",
                      "java.lang.txt",
                      "java.math.txt",
                      "java.text.txt",
                      "java.time.txt",
                      "java.time.chrono.txt",
                      "java.time.format.txt",
                      "java.time.temporal.txt",
                      "java.time.zone.txt",
                      "java.util.txt",
                      "java.util.function.txt",
                      "java.util.regex.txt",
                      "java.util.stream.txt",
                      "joda.time.txt"));

    /**
     * Whitelist that is "built in" to Painless and required by all scripts.
     */
    public static final Definition BUILTINS = new Definition();

    /** Some native types as constants: */
    public static final Type VOID_TYPE = BUILTINS.getType("void");
    public static final Type BOOLEAN_TYPE = BUILTINS.getType("boolean");
    public static final Type BOOLEAN_OBJ_TYPE = BUILTINS.getType("Boolean");
    public static final Type BYTE_TYPE = BUILTINS.getType("byte");
    public static final Type BYTE_OBJ_TYPE = BUILTINS.getType("Byte");
    public static final Type SHORT_TYPE = BUILTINS.getType("short");
    public static final Type SHORT_OBJ_TYPE = BUILTINS.getType("Short");
    public static final Type INT_TYPE = BUILTINS.getType("int");
    public static final Type INT_OBJ_TYPE = BUILTINS.getType("Integer");
    public static final Type LONG_TYPE = BUILTINS.getType("long");
    public static final Type LONG_OBJ_TYPE = BUILTINS.getType("Long");
    public static final Type FLOAT_TYPE = BUILTINS.getType("float");
    public static final Type FLOAT_OBJ_TYPE = BUILTINS.getType("Float");
    public static final Type DOUBLE_TYPE = BUILTINS.getType("double");
    public static final Type DOUBLE_OBJ_TYPE = BUILTINS.getType("Double");
    public static final Type CHAR_TYPE = BUILTINS.getType("char");
    public static final Type CHAR_OBJ_TYPE = BUILTINS.getType("Character");
    public static final Type OBJECT_TYPE = BUILTINS.getType("Object");
    public static final Type DEF_TYPE = BUILTINS.getType("def");
    public static final Type NUMBER_TYPE = BUILTINS.getType("Number");
    public static final Type STRING_TYPE = BUILTINS.getType("String");
    public static final Type EXCEPTION_TYPE = BUILTINS.getType("Exception");
    public static final Type PATTERN_TYPE = BUILTINS.getType("Pattern");
    public static final Type MATCHER_TYPE = BUILTINS.getType("Matcher");
    public static final Type ITERATOR_TYPE = BUILTINS.getType("Iterator");
    public static final Type ARRAY_LIST_TYPE = BUILTINS.getType("ArrayList");
    public static final Type HASH_MAP_TYPE = BUILTINS.getType("HashMap");

    public enum Sort {
        VOID(       void.class      , Void.class      , null          , 0 , true  , false , false , false ),
        BOOL(       boolean.class   , Boolean.class   , null          , 1 , true  , true  , false , true  ),
        BYTE(       byte.class      , Byte.class      , null          , 1 , true  , false , true  , true  ),
        SHORT(      short.class     , Short.class     , null          , 1 , true  , false , true  , true  ),
        CHAR(       char.class      , Character.class , null          , 1 , true  , false , true  , true  ),
        INT(        int.class       , Integer.class   , null          , 1 , true  , false , true  , true  ),
        LONG(       long.class      , Long.class      , null          , 2 , true  , false , true  , true  ),
        FLOAT(      float.class     , Float.class     , null          , 1 , true  , false , true  , true  ),
        DOUBLE(     double.class    , Double.class    , null          , 2 , true  , false , true  , true  ),

        VOID_OBJ(   Void.class      , null            , void.class    , 1 , true  , false , false , false ),
        BOOL_OBJ(   Boolean.class   , null            , boolean.class , 1 , false , true  , false , false ),
        BYTE_OBJ(   Byte.class      , null            , byte.class    , 1 , false , false , true  , false ),
        SHORT_OBJ(  Short.class     , null            , short.class   , 1 , false , false , true  , false ),
        CHAR_OBJ(   Character.class , null            , char.class    , 1 , false , false , true  , false ),
        INT_OBJ(    Integer.class   , null            , int.class     , 1 , false , false , true  , false ),
        LONG_OBJ(   Long.class      , null            , long.class    , 1 , false , false , true  , false ),
        FLOAT_OBJ(  Float.class     , null            , float.class   , 1 , false , false , true  , false ),
        DOUBLE_OBJ( Double.class    , null            , double.class  , 1 , false , false , true  , false ),

        NUMBER(     Number.class    , null            , null          , 1 , false , false , false , false ),
        STRING(     String.class    , null            , null          , 1 , false , false , false , true  ),

        OBJECT(     null            , null            , null          , 1 , false , false , false , false ),
        DEF(        null            , null            , null          , 1 , false , false , false , false ),
        ARRAY(      null            , null            , null          , 1 , false , false , false , false );

        public final Class<?> clazz;
        public final Class<?> boxed;
        public final Class<?> unboxed;
        public final int size;
        public final boolean primitive;
        public final boolean bool;
        public final boolean numeric;
        public final boolean constant;

        Sort(final Class<?> clazz, final Class<?> boxed, final Class<?> unboxed, final int size,
             final boolean primitive, final boolean bool, final boolean numeric, final boolean constant) {
            this.clazz = clazz;
            this.boxed = boxed;
            this.unboxed = unboxed;
            this.size = size;
            this.bool = bool;
            this.primitive = primitive;
            this.numeric = numeric;
            this.constant = constant;
        }
    }

    public static final class Type {
        public final String name;
        public final int dimensions;
        public final Struct struct;
        public final Class<?> clazz;
        public final org.objectweb.asm.Type type;
        public final Sort sort;

        private Type(final String name, final int dimensions, final Struct struct,
                     final Class<?> clazz, final org.objectweb.asm.Type type, final Sort sort) {
            this.name = name;
            this.dimensions = dimensions;
            this.struct = struct;
            this.clazz = clazz;
            this.type = type;
            this.sort = sort;
        }

        @Override
        public boolean equals(final Object object) {
            if (this == object) {
                return true;
            }

            if (object == null || getClass() != object.getClass()) {
                return false;
            }

            final Type type = (Type)object;

            return this.type.equals(type.type) && struct.equals(type.struct);
        }

        @Override
        public int hashCode() {
            int result = struct.hashCode();
            result = 31 * result + type.hashCode();

            return result;
        }

        @Override
        public String toString() {
            return name;
        }
    }

    public static class Method {
        public final String name;
        public final Struct owner;
        public final boolean augmentation;
        public final Type rtn;
        public final List<Type> arguments;
        public final org.objectweb.asm.commons.Method method;
        public final int modifiers;
        public final MethodHandle handle;

        public Method(String name, Struct owner, boolean augmentation, Type rtn, List<Type> arguments,
                      org.objectweb.asm.commons.Method method, int modifiers, MethodHandle handle) {
            this.name = name;
            this.augmentation = augmentation;
            this.owner = owner;
            this.rtn = rtn;
            this.arguments = Collections.unmodifiableList(arguments);
            this.method = method;
            this.modifiers = modifiers;
            this.handle = handle;
        }

        /**
         * Returns MethodType for this method.
         * <p>
         * This works even for user-defined Methods (where the MethodHandle is null).
         */
        public MethodType getMethodType() {
            // we have a methodhandle already (e.g. whitelisted class)
            // just return its type
            if (handle != null) {
                return handle.type();
            }
            // otherwise compute it
            final Class<?> params[];
            final Class<?> returnValue;
            if (augmentation) {
                // static method disguised as virtual/interface method
                params = new Class<?>[1 + arguments.size()];
                params[0] = Augmentation.class;
                for (int i = 0; i < arguments.size(); i++) {
                    params[i + 1] = arguments.get(i).clazz;
                }
                returnValue = rtn.clazz;
            } else if (Modifier.isStatic(modifiers)) {
                // static method: straightforward copy
                params = new Class<?>[arguments.size()];
                for (int i = 0; i < arguments.size(); i++) {
                    params[i] = arguments.get(i).clazz;
                }
                returnValue = rtn.clazz;
            } else if ("<init>".equals(name)) {
                // constructor: returns the owner class
                params = new Class<?>[arguments.size()];
                for (int i = 0; i < arguments.size(); i++) {
                    params[i] = arguments.get(i).clazz;
                }
                returnValue = owner.clazz;
            } else {
                // virtual/interface method: add receiver class
                params = new Class<?>[1 + arguments.size()];
                params[0] = owner.clazz;
                for (int i = 0; i < arguments.size(); i++) {
                    params[i + 1] = arguments.get(i).clazz;
                }
                returnValue = rtn.clazz;
            }
            return MethodType.methodType(returnValue, params);
        }

        public void write(MethodWriter writer) {
            final org.objectweb.asm.Type type;
            if (augmentation) {
                assert java.lang.reflect.Modifier.isStatic(modifiers);
                type = WriterConstants.AUGMENTATION_TYPE;
            } else {
                type = owner.type;
            }

            if (java.lang.reflect.Modifier.isStatic(modifiers)) {
                writer.invokeStatic(type, method);
            } else if (java.lang.reflect.Modifier.isInterface(owner.clazz.getModifiers())) {
                writer.invokeInterface(type, method);
            } else {
                writer.invokeVirtual(type, method);
            }
        }
    }

    public static final class Field {
        public final String name;
        public final Struct owner;
        public final Type type;
        public final String javaName;
        public final int modifiers;
        private final MethodHandle getter;
        private final MethodHandle setter;

        private Field(String name, String javaName, Struct owner, Type type, int modifiers, MethodHandle getter, MethodHandle setter) {
            this.name = name;
            this.javaName = javaName;
            this.owner = owner;
            this.type = type;
            this.modifiers = modifiers;
            this.getter = getter;
            this.setter = setter;
        }
    }

    // TODO: instead of hashing on this, we could have a 'next' pointer in Method itself, but it would make code more complex
    // please do *NOT* under any circumstances change this to be the crappy Tuple from elasticsearch!
    /**
     * Key for looking up a method.
     * <p>
     * Methods are keyed on both name and arity, and can be overloaded once per arity.
     * This allows signatures such as {@code String.indexOf(String) vs String.indexOf(String, int)}.
     * <p>
     * It is less flexible than full signature overloading where types can differ too, but
     * better than just the name, and overloading types adds complexity to users, too.
     */
    public static final class MethodKey {
        public final String name;
        public final int arity;

        /**
         * Create a new lookup key
         * @param name name of the method
         * @param arity number of parameters
         */
        public MethodKey(String name, int arity) {
            this.name = Objects.requireNonNull(name);
            this.arity = arity;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + arity;
            result = prime * result + name.hashCode();
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null) return false;
            if (getClass() != obj.getClass()) return false;
            MethodKey other = (MethodKey) obj;
            if (arity != other.arity) return false;
            if (!name.equals(other.name)) return false;
            return true;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append(name);
            sb.append('/');
            sb.append(arity);
            return sb.toString();
        }
    }

    public static final class Struct {
        public final String name;
        public final Class<?> clazz;
        public final org.objectweb.asm.Type type;

        public final Map<MethodKey, Method> constructors;
        public final Map<MethodKey, Method> staticMethods;
        public final Map<MethodKey, Method> methods;

        public final Map<String, Field> staticMembers;
        public final Map<String, Field> members;

        private final SetOnce<Method> functionalMethod;

        private Struct(final String name, final Class<?> clazz, final org.objectweb.asm.Type type) {
            this.name = name;
            this.clazz = clazz;
            this.type = type;

            constructors = new HashMap<>();
            staticMethods = new HashMap<>();
            methods = new HashMap<>();

            staticMembers = new HashMap<>();
            members = new HashMap<>();

            functionalMethod = new SetOnce<>();
        }

        private Struct(final Struct struct) {
            name = struct.name;
            clazz = struct.clazz;
            type = struct.type;

            constructors = Collections.unmodifiableMap(struct.constructors);
            staticMethods = Collections.unmodifiableMap(struct.staticMethods);
            methods = Collections.unmodifiableMap(struct.methods);

            staticMembers = Collections.unmodifiableMap(struct.staticMembers);
            members = Collections.unmodifiableMap(struct.members);

            functionalMethod = struct.functionalMethod;
        }

        private Struct freeze() {
            return new Struct(this);
        }

        @Override
        public boolean equals(Object object) {
            if (this == object) {
                return true;
            }

            if (object == null || getClass() != object.getClass()) {
                return false;
            }

            Struct struct = (Struct)object;

            return name.equals(struct.name);
        }

        @Override
        public int hashCode() {
            return name.hashCode();
        }

        /**
         * If this class is a functional interface according to JLS, returns its method.
         * Otherwise returns null.
         */
        public Method getFunctionalMethod() {
            return functionalMethod.get();
        }
    }

    public static class Cast {
        public final Type from;
        public final Type to;
        public final boolean explicit;
        public final Type unboxFrom;
        public final Type unboxTo;
        public final Type boxFrom;
        public final Type boxTo;

        public Cast(final Type from, final Type to, final boolean explicit) {
            this.from = from;
            this.to = to;
            this.explicit = explicit;
            this.unboxFrom = null;
            this.unboxTo = null;
            this.boxFrom = null;
            this.boxTo = null;
        }

        public Cast(final Type from, final Type to, final boolean explicit,
                    final Type unboxFrom, final Type unboxTo, final Type boxFrom, final Type boxTo) {
            this.from = from;
            this.to = to;
            this.explicit = explicit;
            this.unboxFrom = unboxFrom;
            this.unboxTo = unboxTo;
            this.boxFrom = boxFrom;
            this.boxTo = boxTo;
        }

    }

    public static final class RuntimeClass {
        private final Struct struct;
        public final Map<MethodKey, Method> methods;
        public final Map<String, MethodHandle> getters;
        public final Map<String, MethodHandle> setters;

        private RuntimeClass(final Struct struct, final Map<MethodKey, Method> methods,
                             final Map<String, MethodHandle> getters, final Map<String, MethodHandle> setters) {
            this.struct = struct;
            this.methods = Collections.unmodifiableMap(methods);
            this.getters = Collections.unmodifiableMap(getters);
            this.setters = Collections.unmodifiableMap(setters);
        }

        public Struct getStruct() {
            return struct;
        }
    }

    /** Returns whether or not a non-array type exists. */
    public boolean isSimpleType(final String name) {
        return BUILTINS.structsMap.containsKey(name);
    }

    /** Gets the type given by its name */
    public Type getType(final String name) {
        return BUILTINS.getTypeInternal(name);
    }

    /** Creates an array type from the given Struct. */
    public Type getType(final Struct struct, final int dimensions) {
        return BUILTINS.getTypeInternal(struct, dimensions);
    }

    public RuntimeClass getRuntimeClass(Class<?> clazz) {
        return BUILTINS.runtimeMap.get(clazz);
    }

    /** Collection of all simple types. Used by {@code PainlessDocGenerator} to generate an API reference. */
    static Collection<Type> allSimpleTypes() {
        return BUILTINS.simpleTypesMap.values();
    }

    // INTERNAL IMPLEMENTATION:

    private final Map<Class<?>, RuntimeClass> runtimeMap;
    private final Map<String, Struct> structsMap;
    private final Map<String, Type> simpleTypesMap;

    private Definition() {
        structsMap = new HashMap<>();
        simpleTypesMap = new HashMap<>();
        runtimeMap = new HashMap<>();

        // parse the classes and return hierarchy (map of class name -> superclasses/interfaces)
        Map<String, List<String>> hierarchy = addStructs();
        // add every method for each class
        addElements();
        // apply hierarchy: this means e.g. copying Object's methods into String (thats how subclasses work)
        for (Map.Entry<String,List<String>> clazz : hierarchy.entrySet()) {
            copyStruct(clazz.getKey(), clazz.getValue());
        }
        // if someone declares an interface type, its still an Object
        for (Map.Entry<String,Struct> clazz : structsMap.entrySet()) {
            String name = clazz.getKey();
            Class<?> javaPeer = clazz.getValue().clazz;
            if (javaPeer.isInterface()) {
                copyStruct(name, Collections.singletonList("Object"));
            } else if (name.equals("def") == false && name.equals("Object") == false && javaPeer.isPrimitive() == false) {
                // but otherwise, unless its a primitive type, it really should
                assert hierarchy.get(name) != null : "class '" + name + "' does not extend Object!";
                assert hierarchy.get(name).contains("Object") : "class '" + name + "' does not extend Object!";
            }
        }
        // mark functional interfaces (or set null, to mark class is not)
        for (Struct clazz : structsMap.values()) {
            clazz.functionalMethod.set(computeFunctionalInterfaceMethod(clazz));
        }

        // precompute runtime classes
        for (Struct struct : structsMap.values()) {
          addRuntimeClass(struct);
        }
        // copy all structs to make them unmodifiable for outside users:
        for (final Map.Entry<String,Struct> entry : structsMap.entrySet()) {
            entry.setValue(entry.getValue().freeze());
        }
    }

    /** adds classes from definition. returns hierarchy */
    private Map<String,List<String>> addStructs() {
        final Map<String,List<String>> hierarchy = new HashMap<>();
        for (String file : DEFINITION_FILES) {
            int currentLine = -1;
            try {
                try (InputStream stream = Definition.class.getResourceAsStream(file);
                        LineNumberReader reader = new LineNumberReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
                    String line = null;
                    while ((line = reader.readLine()) != null) {
                        currentLine = reader.getLineNumber();
                        line = line.trim();
                        if (line.length() == 0 || line.charAt(0) == '#') {
                            continue;
                        }
                        if (line.startsWith("class ")) {
                            String elements[] = line.split("\u0020");
                            assert elements[2].equals("->") : "Invalid struct definition [" + String.join(" ", elements) +"]";
                            if (elements.length == 7) {
                                hierarchy.put(elements[1], Arrays.asList(elements[5].split(",")));
                            } else {
                                assert elements.length == 5 : "Invalid struct definition [" + String.join(" ", elements) + "]";
                            }
                            String className = elements[1];
                            String javaPeer = elements[3];
                            final Class<?> javaClazz;
                            switch (javaPeer) {
                                case "void":
                                    javaClazz = void.class;
                                    break;
                                case "boolean":
                                    javaClazz = boolean.class;
                                    break;
                                case "byte":
                                    javaClazz = byte.class;
                                    break;
                                case "short":
                                    javaClazz = short.class;
                                    break;
                                case "char":
                                    javaClazz = char.class;
                                    break;
                                case "int":
                                    javaClazz = int.class;
                                    break;
                                case "long":
                                    javaClazz = long.class;
                                    break;
                                case "float":
                                    javaClazz = float.class;
                                    break;
                                case "double":
                                    javaClazz = double.class;
                                    break;
                                default:
                                    javaClazz = Class.forName(javaPeer);
                                    break;
                            }
                            addStruct(className, javaClazz);
                        }
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException("error in " + file + ", line: " + currentLine, e);
            }
        }
        return hierarchy;
    }

    /** adds class methods/fields/ctors */
    private void addElements() {
        for (String file : DEFINITION_FILES) {
            int currentLine = -1;
            try {
                try (InputStream stream = Definition.class.getResourceAsStream(file);
                        LineNumberReader reader = new LineNumberReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
                    String line = null;
                    String currentClass = null;
                    while ((line = reader.readLine()) != null) {
                        currentLine = reader.getLineNumber();
                        line = line.trim();
                        if (line.length() == 0 || line.charAt(0) == '#') {
                            continue;
                        } else if (line.startsWith("class ")) {
                            assert currentClass == null;
                            currentClass = line.split("\u0020")[1];
                        } else if (line.equals("}")) {
                            assert currentClass != null;
                            currentClass = null;
                        } else {
                            assert currentClass != null;
                            addSignature(currentClass, line);
                        }
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException("syntax error in " + file + ", line: " + currentLine, e);
            }
        }
    }

    private void addStruct(final String name, final Class<?> clazz) {
        if (!name.matches("^[_a-zA-Z][\\.,_a-zA-Z0-9]*$")) {
            throw new IllegalArgumentException("Invalid struct name [" + name + "].");
        }

        if (structsMap.containsKey(name)) {
            throw new IllegalArgumentException("Duplicate struct name [" + name + "].");
        }

        final Struct struct = new Struct(name, clazz, org.objectweb.asm.Type.getType(clazz));

        structsMap.put(name, struct);
        simpleTypesMap.put(name, getTypeInternal(name));
    }

    private void addConstructorInternal(final String struct, final String name, final Type[] args) {
        final Struct owner = structsMap.get(struct);

        if (owner == null) {
            throw new IllegalArgumentException(
                "Owner struct [" + struct + "] not defined for constructor [" + name + "].");
        }

        if (!name.matches("<init>")) {
            throw new IllegalArgumentException(
                "Invalid constructor name [" + name + "] with the struct [" + owner.name + "].");
        }

        MethodKey methodKey = new MethodKey(name, args.length);

        if (owner.constructors.containsKey(methodKey)) {
            throw new IllegalArgumentException(
                "Duplicate constructor [" + methodKey + "] found within the struct [" + owner.name + "].");
        }

        if (owner.staticMethods.containsKey(methodKey)) {
            throw new IllegalArgumentException("Constructors and static methods may not have the same signature" +
                " [" + methodKey + "] within the same struct [" + owner.name + "].");
        }

        if (owner.methods.containsKey(methodKey)) {
            throw new IllegalArgumentException("Constructors and methods may not have the same signature" +
                " [" + methodKey + "] within the same struct [" + owner.name + "].");
        }

        final Class<?>[] classes = new Class<?>[args.length];

        for (int count = 0; count < classes.length; ++count) {
            classes[count] = args[count].clazz;
        }

        final java.lang.reflect.Constructor<?> reflect;

        try {
            reflect = owner.clazz.getConstructor(classes);
        } catch (final NoSuchMethodException exception) {
            throw new IllegalArgumentException("Constructor [" + name + "] not found for class" +
                " [" + owner.clazz.getName() + "] with arguments " + Arrays.toString(classes) + ".");
        }

        final org.objectweb.asm.commons.Method asm = org.objectweb.asm.commons.Method.getMethod(reflect);
        final Type returnType = getTypeInternal("void");
        final MethodHandle handle;

        try {
            handle = MethodHandles.publicLookup().in(owner.clazz).unreflectConstructor(reflect);
        } catch (final IllegalAccessException exception) {
            throw new IllegalArgumentException("Constructor " +
                " not found for class [" + owner.clazz.getName() + "]" +
                " with arguments " + Arrays.toString(classes) + ".");
        }

        final Method constructor = new Method(name, owner, false, returnType, Arrays.asList(args), asm, reflect.getModifiers(), handle);

        owner.constructors.put(methodKey, constructor);
    }

    /**
     * Adds a new signature to the definition.
     * <p>
     * Signatures have the following forms:
     * <ul>
     *   <li>{@code void method(String,int)}
     *   <li>{@code boolean field}
     *   <li>{@code Class <init>(String)}
     * </ul>
     * no spaces allowed.
     */
    private void addSignature(String className, String signature) {
        String elements[] = signature.split("\u0020");
        if (elements.length != 2) {
            throw new IllegalArgumentException("Malformed signature: " + signature);
        }
        // method or field type (e.g. return type)
        Type rtn = getTypeInternal(elements[0]);
        int parenIndex = elements[1].indexOf('(');
        if (parenIndex != -1) {
            // method or ctor
            int parenEnd = elements[1].indexOf(')');
            final Type args[];
            if (parenEnd > parenIndex + 1) {
                String arguments[] = elements[1].substring(parenIndex + 1, parenEnd).split(",");
                args = new Type[arguments.length];
                for (int i = 0; i < arguments.length; i++) {
                    args[i] = getTypeInternal(arguments[i]);
                }
            } else {
                args = new Type[0];
            }
            String methodName = elements[1].substring(0, parenIndex);
            if (methodName.equals("<init>")) {
                if (!elements[0].equals(className)) {
                    throw new IllegalArgumentException("Constructors must return their own type");
                }
                addConstructorInternal(className, "<init>", args);
            } else {
                if (methodName.indexOf("*") >= 0) {
                    addMethodInternal(className, methodName.substring(0, methodName.length() - 1), true, rtn, args);
                } else {
                    addMethodInternal(className, methodName, false, rtn, args);
                }
            }
        } else {
            // field
            addFieldInternal(className, elements[1], rtn);
        }
    }

    private void addMethodInternal(String struct, String name, boolean augmentation,
                                   Type rtn, Type[] args) {
        final Struct owner = structsMap.get(struct);

        if (owner == null) {
            throw new IllegalArgumentException("Owner struct [" + struct + "] not defined" +
                " for method [" + name + "].");
        }

        if (!name.matches("^[_a-zA-Z][_a-zA-Z0-9]*$")) {
            throw new IllegalArgumentException("Invalid method name" +
                " [" + name + "] with the struct [" + owner.name + "].");
        }

        MethodKey methodKey = new MethodKey(name, args.length);

        if (owner.constructors.containsKey(methodKey)) {
            throw new IllegalArgumentException("Constructors and methods" +
                " may not have the same signature [" + methodKey + "] within the same struct" +
                " [" + owner.name + "].");
        }

        if (owner.staticMethods.containsKey(methodKey) || owner.methods.containsKey(methodKey)) {
            throw new IllegalArgumentException(
                "Duplicate method signature [" + methodKey + "] found within the struct [" + owner.name + "].");
        }

        final Class<?> implClass;
        final Class<?>[] params;

        if (augmentation == false) {
            implClass = owner.clazz;
            params = new Class<?>[args.length];
            for (int count = 0; count < args.length; ++count) {
                params[count] = args[count].clazz;
            }
        } else {
            implClass = Augmentation.class;
            params = new Class<?>[args.length + 1];
            params[0] = owner.clazz;
            for (int count = 0; count < args.length; ++count) {
                params[count+1] = args[count].clazz;
            }
        }

        final java.lang.reflect.Method reflect;

        try {
            reflect = implClass.getMethod(name, params);
        } catch (NoSuchMethodException exception) {
            throw new IllegalArgumentException("Method [" + name +
                "] not found for class [" + implClass.getName() + "]" +
                " with arguments " + Arrays.toString(params) + ".");
        }

        if (!reflect.getReturnType().equals(rtn.clazz)) {
            throw new IllegalArgumentException("Specified return type class [" + rtn.clazz + "]" +
                " does not match the found return type class [" + reflect.getReturnType() + "] for the" +
                " method [" + name + "]" +
                " within the struct [" + owner.name + "].");
        }

        final org.objectweb.asm.commons.Method asm = org.objectweb.asm.commons.Method.getMethod(reflect);

        MethodHandle handle;

        try {
            handle = MethodHandles.publicLookup().in(implClass).unreflect(reflect);
        } catch (final IllegalAccessException exception) {
            throw new IllegalArgumentException("Method [" + name + "]" +
                " not found for class [" + implClass.getName() + "]" +
                " with arguments " + Arrays.toString(params) + ".");
        }

        final int modifiers = reflect.getModifiers();
        final Method method = new Method(name, owner, augmentation, rtn, Arrays.asList(args), asm, modifiers, handle);

        if (augmentation == false && java.lang.reflect.Modifier.isStatic(modifiers)) {
            owner.staticMethods.put(methodKey, method);
        } else {
            owner.methods.put(methodKey, method);
        }
    }

    private void addFieldInternal(String struct, String name, Type type) {
        final Struct owner = structsMap.get(struct);

        if (owner == null) {
            throw new IllegalArgumentException("Owner struct [" + struct + "] not defined for " +
                " field [" + name + "].");
        }

        if (!name.matches("^[_a-zA-Z][_a-zA-Z0-9]*$")) {
            throw new IllegalArgumentException("Invalid field " +
                " name [" + name + "] with the struct [" + owner.name + "].");
        }

        if (owner.staticMembers.containsKey(name) || owner.members.containsKey(name)) {
             throw new IllegalArgumentException("Duplicate field name [" + name + "]" +
                     " found within the struct [" + owner.name + "].");
        }

        java.lang.reflect.Field reflect;

        try {
            reflect = owner.clazz.getField(name);
        } catch (final NoSuchFieldException exception) {
            throw new IllegalArgumentException("Field [" + name + "]" +
                " not found for class [" + owner.clazz.getName() + "].");
        }

        final int modifiers = reflect.getModifiers();
        boolean isStatic = java.lang.reflect.Modifier.isStatic(modifiers);

        MethodHandle getter = null;
        MethodHandle setter = null;

        try {
            if (!isStatic) {
                getter = MethodHandles.publicLookup().unreflectGetter(reflect);
                setter = MethodHandles.publicLookup().unreflectSetter(reflect);
            }
        } catch (final IllegalAccessException exception) {
            throw new IllegalArgumentException("Getter/Setter [" + name + "]" +
                " not found for class [" + owner.clazz.getName() + "].");
        }

        final Field field = new Field(name, reflect.getName(), owner, type, modifiers, getter, setter);

        if (isStatic) {
            // require that all static fields are static final
            if (!java.lang.reflect.Modifier.isFinal(modifiers)) {
                throw new IllegalArgumentException("Static [" + name + "]" +
                    " within the struct [" + owner.name + "] is not final.");
            }

            owner.staticMembers.put(name, field);
        } else {
            owner.members.put(name, field);
        }
    }

    private void copyStruct(String struct, List<String> children) {
        final Struct owner = structsMap.get(struct);

        if (owner == null) {
            throw new IllegalArgumentException("Owner struct [" + struct + "] not defined for copy.");
        }

        for (int count = 0; count < children.size(); ++count) {
            final Struct child = structsMap.get(children.get(count));

            if (child == null) {
                throw new IllegalArgumentException("Child struct [" + children.get(count) + "]" +
                    " not defined for copy to owner struct [" + owner.name + "].");
            }

            if (!child.clazz.isAssignableFrom(owner.clazz)) {
                throw new ClassCastException("Child struct [" + child.name + "]" +
                    " is not a super type of owner struct [" + owner.name + "] in copy.");
            }

            for (Map.Entry<MethodKey,Method> kvPair : child.methods.entrySet()) {
                MethodKey methodKey = kvPair.getKey();
                Method method = kvPair.getValue();
                if (owner.methods.get(methodKey) == null) {
                    // sanity check, look for missing covariant/generic override
                    if (owner.clazz.isInterface() && child.clazz == Object.class) {
                        // ok
                    } else if (child.clazz == Spliterator.OfPrimitive.class || child.clazz == PrimitiveIterator.class) {
                        // ok, we rely on generics erasure for these (its guaranteed in the javadocs though!!!!)
                    } else if (Constants.JRE_IS_MINIMUM_JAVA9 && owner.clazz == LocalDate.class) {
                        // ok, java 9 added covariant override for LocalDate.getEra() to return IsoEra:
                        // https://bugs.openjdk.java.net/browse/JDK-8072746
                    } else {
                        try {
                            // TODO: we *have* to remove all these public members and use getter methods to encapsulate!
                            final Class<?> impl;
                            final Class<?> arguments[];
                            if (method.augmentation) {
                                impl = Augmentation.class;
                                arguments = new Class<?>[method.arguments.size() + 1];
                                arguments[0] = method.owner.clazz;
                                for (int i = 0; i < method.arguments.size(); i++) {
                                    arguments[i + 1] = method.arguments.get(i).clazz;
                                }
                            } else {
                                impl = owner.clazz;
                                arguments = new Class<?>[method.arguments.size()];
                                for (int i = 0; i < method.arguments.size(); i++) {
                                    arguments[i] = method.arguments.get(i).clazz;
                                }
                            }
                            java.lang.reflect.Method m = impl.getMethod(method.method.getName(), arguments);
                            if (m.getReturnType() != method.rtn.clazz) {
                                throw new IllegalStateException("missing covariant override for: " + m + " in " + owner.name);
                            }
                            if (m.isBridge() && !Modifier.isVolatile(method.modifiers)) {
                                // its a bridge in the destination, but not in the source, but it might still be ok, check generics:
                                java.lang.reflect.Method source = child.clazz.getMethod(method.method.getName(), arguments);
                                if (!Arrays.equals(source.getGenericParameterTypes(), source.getParameterTypes())) {
                                    throw new IllegalStateException("missing generic override for: " + m + " in " + owner.name);
                                }
                            }
                        } catch (ReflectiveOperationException e) {
                            throw new AssertionError(e);
                        }
                    }
                    owner.methods.put(methodKey, method);
                }
            }

            for (Field field : child.members.values()) {
                if (owner.members.get(field.name) == null) {
                    owner.members.put(field.name,
                        new Field(field.name, field.javaName, owner, field.type, field.modifiers, field.getter, field.setter));
                }
            }
        }
    }

    /**
     * Precomputes a more efficient structure for dynamic method/field access.
     */
    private void addRuntimeClass(final Struct struct) {
        final Map<MethodKey, Method> methods = struct.methods;
        final Map<String, MethodHandle> getters = new HashMap<>();
        final Map<String, MethodHandle> setters = new HashMap<>();

        // add all members
        for (final Map.Entry<String, Field> member : struct.members.entrySet()) {
            getters.put(member.getKey(), member.getValue().getter);
            setters.put(member.getKey(), member.getValue().setter);
        }

        // add all getters/setters
        for (final Map.Entry<MethodKey, Method> method : methods.entrySet()) {
            final String name = method.getKey().name;
            final Method m = method.getValue();

            if (m.arguments.size() == 0 &&
                name.startsWith("get") &&
                name.length() > 3 &&
                Character.isUpperCase(name.charAt(3))) {
                final StringBuilder newName = new StringBuilder();
                newName.append(Character.toLowerCase(name.charAt(3)));
                newName.append(name.substring(4));
                getters.putIfAbsent(newName.toString(), m.handle);
            } else if (m.arguments.size() == 0 &&
                name.startsWith("is") &&
                name.length() > 2 &&
                Character.isUpperCase(name.charAt(2))) {
                final StringBuilder newName = new StringBuilder();
                newName.append(Character.toLowerCase(name.charAt(2)));
                newName.append(name.substring(3));
                getters.putIfAbsent(newName.toString(), m.handle);
            }

            if (m.arguments.size() == 1 &&
                name.startsWith("set") &&
                name.length() > 3 &&
                Character.isUpperCase(name.charAt(3))) {
                final StringBuilder newName = new StringBuilder();
                newName.append(Character.toLowerCase(name.charAt(3)));
                newName.append(name.substring(4));
                setters.putIfAbsent(newName.toString(), m.handle);
            }
        }

        runtimeMap.put(struct.clazz, new RuntimeClass(struct, methods, getters, setters));
    }

    /** computes the functional interface method for a class, or returns null */
    private Method computeFunctionalInterfaceMethod(Struct clazz) {
        if (!clazz.clazz.isInterface()) {
            return null;
        }
        // if its marked with this annotation, we fail if the conditions don't hold (means whitelist bug)
        // otherwise, this annotation is pretty useless.
        boolean hasAnnotation = clazz.clazz.isAnnotationPresent(FunctionalInterface.class);
        List<java.lang.reflect.Method> methods = new ArrayList<>();
        for (java.lang.reflect.Method m : clazz.clazz.getMethods()) {
            // default interface methods don't count
            if (m.isDefault()) {
                continue;
            }
            // static methods don't count
            if (Modifier.isStatic(m.getModifiers())) {
                continue;
            }
            // if its from Object, it doesn't count
            try {
                Object.class.getMethod(m.getName(), m.getParameterTypes());
                continue;
            } catch (ReflectiveOperationException e) {
                // it counts
            }
            methods.add(m);
        }
        if (methods.size() != 1) {
            if (hasAnnotation) {
                throw new IllegalArgumentException("Class: " + clazz.name +
                                                   " is marked with FunctionalInterface but doesn't fit the bill: " + methods);
            }
            return null;
        }
        // inspect the one method found from the reflection API, it should match the whitelist!
        java.lang.reflect.Method oneMethod = methods.get(0);
        Method painless = clazz.methods.get(new Definition.MethodKey(oneMethod.getName(), oneMethod.getParameterCount()));
        if (painless == null || painless.method.equals(org.objectweb.asm.commons.Method.getMethod(oneMethod)) == false) {
            throw new IllegalArgumentException("Class: " + clazz.name + " is functional but the functional " +
                                               "method is not whitelisted!");
        }
        return painless;
    }

    private Type getTypeInternal(String name) {
        // simple types (e.g. 0 array dimensions) are a simple hash lookup for speed
        Type simple = simpleTypesMap.get(name);

        if (simple != null) {
            return simple;
        }

        int dimensions = getDimensions(name);
        String structstr = dimensions == 0 ? name : name.substring(0, name.indexOf('['));
        Struct struct = structsMap.get(structstr);

        if (struct == null) {
            throw new IllegalArgumentException("The struct with name [" + name + "] has not been defined.");
        }

        return getTypeInternal(struct, dimensions);
    }

    private Type getTypeInternal(Struct struct, int dimensions) {
        String name = struct.name;
        org.objectweb.asm.Type type = struct.type;
        Class<?> clazz = struct.clazz;
        Sort sort;

        if (dimensions > 0) {
            StringBuilder builder = new StringBuilder(name);
            char[] brackets = new char[dimensions];

            for (int count = 0; count < dimensions; ++count) {
                builder.append("[]");
                brackets[count] = '[';
            }

            String descriptor = new String(brackets) + struct.type.getDescriptor();

            name = builder.toString();
            type = org.objectweb.asm.Type.getType(descriptor);

            try {
                clazz = Class.forName(type.getInternalName().replace('/', '.'));
            } catch (ClassNotFoundException exception) {
                throw new IllegalArgumentException("The class [" + type.getInternalName() + "]" +
                    " could not be found to create type [" + name + "].");
            }

            sort = Sort.ARRAY;
        } else if ("def".equals(struct.name)) {
            sort = Sort.DEF;
        } else {
            sort = Sort.OBJECT;

            for (Sort value : Sort.values()) {
                if (value.clazz == null) {
                    continue;
                }

                if (value.clazz.equals(struct.clazz)) {
                    sort = value;

                    break;
                }
            }
        }

        return new Type(name, dimensions, struct, clazz, type, sort);
    }

    private int getDimensions(String name) {
        int dimensions = 0;
        int index = name.indexOf('[');

        if (index != -1) {
            int length = name.length();

            while (index < length) {
                if (name.charAt(index) == '[' && ++index < length && name.charAt(index++) == ']') {
                    ++dimensions;
                } else {
                    throw new IllegalArgumentException("Invalid array braces in canonical name [" + name + "].");
                }
            }
        }

        return dimensions;
    }
}
