/*
 * Copyright 2012-2014 headius.
 *
 * Licensed 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 com.headius.invokebinder;

import com.headius.invokebinder.transform.Cast;
import com.headius.invokebinder.transform.Catch;
import com.headius.invokebinder.transform.Collect;
import com.headius.invokebinder.transform.Convert;
import com.headius.invokebinder.transform.Drop;
import com.headius.invokebinder.transform.Filter;
import com.headius.invokebinder.transform.FilterReturn;
import com.headius.invokebinder.transform.Fold;
import com.headius.invokebinder.transform.Insert;
import com.headius.invokebinder.transform.Permute;
import com.headius.invokebinder.transform.Spread;
import com.headius.invokebinder.transform.Transform;
import com.headius.invokebinder.transform.TryFinally;
import com.headius.invokebinder.transform.Varargs;

import java.io.PrintStream;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.invoke.VarHandle;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.logging.Logger;

/**
 * The Binder class provides a DSL for building a chain of MethodHandles using
 * various of the adaptations provided by java.lang.invoke.MethodHandles. The
 * transformations are pushed into a stack, allowing the DSL to operate forward
 * from an incoming signature rather than backward from a target handle. This
 * is often conceptually easier to understand, and certainly easier to read.
 *
 * The transformations are also applied simultaneously to the starting
 * java.lang.invoke.MethodType, allowing Binder to check at each step whether
 * the adaptation is valid.
 *
 * Here's a typical use, starting with a signature that takes two Strings and
 * returns a String, dropping and inserting arguments, casting to a target
 * signature, and finally calling a target handle with that signature.
 *
 * <pre>
 * MethodHandle mh = Binder
 *     .from(String.class, String.class, String.class) // String w(String, String)
 *     .drop(1, String.class) // String x(String)
 *     .insert(0, 'hello') // String y(String, String)
 *     .cast(String.class, CharSequence.class, Object.class) // String z(CharSequence, Object)String
 *     .invoke(someTargetHandle);
 * </pre>
 */
public class Binder {

    private static final Logger logger = Logger.getLogger("Invoke Binder");
    private final List<Transform> transforms = new LinkedList<>();
    private final List<MethodType> types = new LinkedList<>();
    private final MethodType start;
    private final MethodHandles.Lookup lookup;

    /**
     * Construct a new Binder, starting from a given MethodType. Use a public
     * java.lang.invoke.MethodHandles.Lookup for retrieving direct handles.
     *
     * @param start the starting MethodType, for calls entering the eventual chain
     */
    public Binder(MethodType start) {
        this.start = start;
        this.types.add(0, start);
        this.lookup = MethodHandles.publicLookup();
    }

    /**
     * Construct a new Binder, starting from a given Lookup and MethodType.
     *
     * @param lookup the Lookup context to use for direct handles
     * @param start  the starting MethodType, for calls entering the eventual chain
     */
    public Binder(MethodHandles.Lookup lookup, MethodType start) {
        this.start = start;
        this.types.add(0, start);
        this.lookup = lookup;
    }

    /**
     * Construct a new Binder using the given invokebinder.
     *
     * @param source a Binder to duplicate
     */
    public Binder(Binder source) {
        this.start = source.start;
        this.types.addAll(source.types);
        this.transforms.addAll(source.transforms);
        this.lookup = source.lookup;
    }

    /**
     * Construct a new Binder using the given Lookup and invokebinder.
     *
     * @param lookup the Lookup context to use for direct handles
     * @param source the source Binder
     */
    public Binder(MethodHandles.Lookup lookup, Binder source) {
        this.start = source.start;
        this.types.addAll(source.types);
        this.transforms.addAll(source.transforms);
        this.lookup = lookup;
    }

    /**
     * Construct a new Binder using the given invokebinder plus an additional transform
     *
     * @param source    the source Binder
     * @param transform the additional Transform
     */
    public Binder(Binder source, Transform transform) {
        this.start = source.start;
        this.types.addAll(source.types);
        this.transforms.addAll(source.transforms);
        add(transform);
        this.lookup = source.lookup;
    }

    /**
     * Construct a new Binder using the given invokebinder plus an additional transform and current type
     *
     * @param source    the source Binder
     * @param transform the additional Transform
     * @param type      the new current type resulting from the transform
     */
    public Binder(Binder source, Transform transform, MethodType type) {
        this.start = source.start;
        this.types.addAll(source.types);
        this.transforms.addAll(source.transforms);
        add(transform, type);
        this.lookup = source.lookup;
    }

    /**
     * Construct a new Binder, starting from a given MethodType.
     *
     * @param start the starting MethodType, for calls entering the eventual chain
     * @return the Binder object
     */
    public static Binder from(MethodType start) {
        return new Binder(start);
    }

    /**
     * Construct a new Binder, starting from a given MethodType.
     *
     * @param lookup the Lookup context to use for direct handles
     * @param start  the starting MethodType, for calls entering the eventual chain
     * @return the Binder object
     */
    public static Binder from(MethodHandles.Lookup lookup, MethodType start) {
        return new Binder(lookup, start);
    }

    /**
     * Construct a new Binder using a return type.
     *
     * @param returnType the return type of the incoming signature
     * @return the Binder object
     */
    public static Binder from(Class<?> returnType) {
        return from(MethodType.methodType(returnType));
    }

    /**
     * Construct a new Binder using a return type.
     *
     * @param lookup     the Lookup context to use for direct handles
     * @param returnType the return type of the incoming signature
     * @return the Binder object
     */
    public static Binder from(MethodHandles.Lookup lookup, Class<?> returnType) {
        return from(lookup, MethodType.methodType(returnType));
    }

    /**
     * Construct a new Binder using a return type and argument types.
     *
     * @param returnType the return type of the incoming signature
     * @param argTypes   the argument types of the incoming signature
     * @return the Binder object
     */
    public static Binder from(Class<?> returnType, Class<?>[] argTypes) {
        return from(MethodType.methodType(returnType, argTypes));
    }

    /**
     * Construct a new Binder using a return type and argument types.
     *
     * @param lookup     the Lookup context to use for direct handles
     * @param returnType the return type of the incoming signature
     * @param argTypes   the argument types of the incoming signature
     * @return the Binder object
     */
    public static Binder from(MethodHandles.Lookup lookup, Class<?> returnType, Class<?>[] argTypes) {
        return from(lookup, MethodType.methodType(returnType, argTypes));
    }

    /**
     * Construct a new Binder using a return type and argument types.
     *
     * @param returnType the return type of the incoming signature
     * @param argType0   the first argument type of the incoming signature
     * @param argTypes   the remaining argument types of the incoming signature
     * @return the Binder object
     */
    public static Binder from(Class<?> returnType, Class<?> argType0, Class<?>... argTypes) {
        return from(MethodType.methodType(returnType, argType0, argTypes));
    }

    /**
     * Construct a new Binder using a return type and argument types.
     *
     * @param lookup     the Lookup context to use for direct handles
     * @param returnType the return type of the incoming signature
     * @param argType0   the first argument type of the incoming signature
     * @param argTypes   the remaining argument types of the incoming signature
     * @return the Binder object
     */
    public static Binder from(MethodHandles.Lookup lookup, Class<?> returnType, Class<?> argType0, Class<?>... argTypes) {
        return from(lookup, MethodType.methodType(returnType, argType0, argTypes));
    }

    /**
     * Construct a new Binder, starting from a given invokebinder.
     *
     * @param start the starting invokebinder; the new one will start with the current endpoint type
     *              of the given invokebinder
     * @return the Binder object
     */
    public static Binder from(Binder start) {
        return new Binder(start);
    }

    /**
     * Construct a new Binder, starting from a given invokebinder.
     *
     * @param lookup the Lookup context to use for direct handles
     * @param start  the starting invokebinder; the new one will start with the current endpoint type
     *               of the given invokebinder
     * @return the Binder object
     */
    public static Binder from(MethodHandles.Lookup lookup, Binder start) {
        return new Binder(lookup, start);
    }

    /**
     * Join this binder to an existing one by applying its transformations after
     * this one.
     *
     * @param other the Binder containing the set of transformations to append
     * @return a new Binder combining this Binder with the target Binder
     */
    public Binder to(Binder other) {
        assert type().equals(other.start);

        Binder newBinder = new Binder(this);

        for (ListIterator<Transform> iter = other.transforms.listIterator(other.transforms.size()); iter.hasPrevious(); ) {
            Transform t = iter.previous();
            newBinder.add(t);
        }

        return newBinder;
    }

    /**
     * Use an alternate java.lang.invoke.MethodHandles.Lookup as the default for
     * any direct handles created.
     *
     * @param lookup the new Lookup context to use
     * @return a new Binder with the given lookup
     */
    public Binder withLookup(MethodHandles.Lookup lookup) {
        return from(lookup, this);
    }

    /**
     * Add a Transform to the chain.
     *
     * @param transform
     */
    private void add(Transform transform) {
        add(transform, transform.down(types.get(0)));
    }

    /**
     * Add a Transform with an associated MethodType target to the chain.
     *
     * @param transform
     * @param target
     */
    private void add(Transform transform, MethodType target) {
        types.add(0, target);
        transforms.add(0, transform);
    }

    /**
     * The current MethodType, were the handle chain to terminate at this point.
     *
     * @return the current MethodType
     */
    public MethodType type() {
        return types.get(0);
    }

    /**
     * Println the current MethodType to the given stream.
     *
     * @param ps a PrintStream to which to println the current MethodType
     * @return this Binding
     */
    public Binder printType(PrintStream ps) {
        ps.println(types.get(0));
        return this;
    }

    /**
     * Println the current MethodType to stdout.
     *
     * @return this Binding
     */
    public Binder printType() {
        return printType(System.out);
    }

    /**
     * Log the current MethodType as info.
     *
     * @return this Binding
     */
    public Binder logType() {
        logger.info(types.get(0).toString());
        return this;
    }

    /**
     * Insert at the given index the given boolean value.
     *
     * @param index the index at which to insert the argument value
     * @param value the value to insert
     * @return a new Binder
     */
    public Binder insert(int index, boolean value) {
        return new Binder(this, new Insert(index, value));
    }

    /**
     * Insert at the given index the given byte value.
     *
     * @param index the index at which to insert the argument value
     * @param value the value to insert
     * @return a new Binder
     */
    public Binder insert(int index, byte value) {
        return new Binder(this, new Insert(index, value));
    }

    /**
     * Insert at the given index the given short value.
     *
     * @param index the index at which to insert the argument value
     * @param value the value to insert
     * @return a new Binder
     */
    public Binder insert(int index, short value) {
        return new Binder(this, new Insert(index, value));
    }

    /**
     * Insert at the given index the given char value.
     *
     * @param index the index at which to insert the argument value
     * @param value the value to insert
     * @return a new Binder
     */
    public Binder insert(int index, char value) {
        return new Binder(this, new Insert(index, value));
    }

    /**
     * Insert at the given index the given int value.
     *
     * @param index the index at which to insert the argument value
     * @param value the value to insert
     * @return a new Binder
     */
    public Binder insert(int index, int value) {
        return new Binder(this, new Insert(index, value));
    }

    /**
     * Insert at the given index the given long value.
     *
     * @param index the index at which to insert the argument value
     * @param value the value to insert
     * @return a new Binder
     */
    public Binder insert(int index, long value) {
        return new Binder(this, new Insert(index, value));
    }

    /**
     * Insert at the given index the given float value.
     *
     * @param index the index at which to insert the argument value
     * @param value the value to insert
     * @return a new Binder
     */
    public Binder insert(int index, float value) {
        return new Binder(this, new Insert(index, value));
    }

    /**
     * Insert at the given index the given double value.
     *
     * @param index the index at which to insert the argument value
     * @param value the value to insert
     * @return a new Binder
     */
    public Binder insert(int index, double value) {
        return new Binder(this, new Insert(index, value));
    }

    /**
     * Insert at the given index the given argument value(s).
     *
     * @param index  the index at which to insert the argument value
     * @param values the value(s) to insert
     * @return a new Binder
     */
    public Binder insert(int index, Object... values) {
        return new Binder(this, new Insert(index, values));
    }

    /**
     * Insert at the given index the given argument value.
     *
     * @param index the index at which to insert the argument value
     * @param type  the actual type to use, rather than getClass
     * @param value the value to insert
     * @return a new Binder
     */
    public Binder insert(int index, Class<?> type, Object value) {
        return new Binder(this, new Insert(index, new Class[]{type}, value));
    }

    /**
     * Insert at the given index the given argument value(s).
     *
     * @param index  the index at which to insert the argument value
     * @param types  the actual types to use, rather than getClass
     * @param values the value(s) to insert
     * @return a new Binder
     */
    public Binder insert(int index, Class<?>[] types, Object... values) {
        return new Binder(this, new Insert(index, types, values));
    }

    /**
     * Append to the argument list the given boolean value.
     *
     * @param value the value to append
     * @return a new Binder
     */
    public Binder append(boolean value) {
        return new Binder(this, new Insert(type().parameterCount(), value));
    }

    /**
     * Append to the argument list the given boolean values.
     *
     * @param values the value to append
     * @return a new Binder
     */
    public Binder appendBooleans(boolean... values) {
        return new Binder(this, new Insert(type().parameterCount(), values));
    }

    /**
     * Append to the argument list the given byte value.
     *
     * @param value the value to append
     * @return a new Binder
     */
    public Binder append(byte value) {
        return new Binder(this, new Insert(type().parameterCount(), value));
    }

    /**
     * Append to the argument list the given byte values.
     *
     * @param values the values to append
     * @return a new Binder
     */
    public Binder appendBytes(byte... values) {
        return new Binder(this, new Insert(type().parameterCount(), values));
    }

    /**
     * Append to the argument list the given short value.
     *
     * @param value the value to append
     * @return a new Binder
     */
    public Binder append(short value) {
        return new Binder(this, new Insert(type().parameterCount(), value));
    }

    /**
     * Append to the argument list the given short values.
     *
     * @param values the values to append
     * @return a new Binder
     */
    public Binder appendShorts(short... values) {
        return new Binder(this, new Insert(type().parameterCount(), values));
    }

    /**
     * Append to the argument list the given char value.
     *
     * @param value the value to append
     * @return a new Binder
     */
    public Binder append(char value) {
        return new Binder(this, new Insert(type().parameterCount(), value));
    }

    /**
     * Append to the argument list the given char values.
     *
     * @param values the values to append
     * @return a new Binder
     */
    public Binder appendChars(char... values) {
        return new Binder(this, new Insert(type().parameterCount(), values));
    }

    /**
     * Append to the argument list the given int value.
     *
     * @param value the value to append
     * @return a new Binder
     */
    public Binder append(int value) {
        return new Binder(this, new Insert(type().parameterCount(), value));
    }

    /**
     * Append to the argument list the given int values.
     *
     * @param values the values to append
     * @return a new Binder
     */
    public Binder appendInts(int... values) {
        return new Binder(this, new Insert(type().parameterCount(), values));
    }

    /**
     * Append to the argument list the given long value.
     *
     * @param value the value to append
     * @return a new Binder
     */
    public Binder append(long value) {
        return new Binder(this, new Insert(type().parameterCount(), value));
    }

    /**
     * Append to the argument list the given long values.
     *
     * @param values the values to append
     * @return a new Binder
     */
    public Binder appendLongs(long... values) {
        return new Binder(this, new Insert(type().parameterCount(), values));
    }

    /**
     * Append to the argument list the given float value.
     *
     * @param value the value to append
     * @return a new Binder
     */
    public Binder append(float value) {
        return new Binder(this, new Insert(type().parameterCount(), value));
    }

    /**
     * Append to the argument list the given float values.
     *
     * @param values the values to append
     * @return a new Binder
     */
    public Binder appendFloats(float... values) {
        return new Binder(this, new Insert(type().parameterCount(), values));
    }

    /**
     * Append to the argument list the given double value.
     *
     * @param value the value to append
     * @return a new Binder
     */
    public Binder append(double value) {
        return new Binder(this, new Insert(type().parameterCount(), value));
    }

    /**
     * Append to the argument list the given double values.
     *
     * @param values the values to append
     * @return a new Binder
     */
    public Binder appendDoubles(double... values) {
        return new Binder(this, new Insert(type().parameterCount(), values));
    }

    /**
     * Append to the argument list the given argument value(s).
     *
     * @param values the value(s) to append
     * @return a new Binder
     */
    public Binder append(Object... values) {
        return new Binder(this, new Insert(type().parameterCount(), values));
    }

    /**
     * Append to the argument list the given argument type(s) and value(s), provided as (Class, value) pairs of
     * arguments.
     *
     * @param typesAndValues the value(s) to append
     * @return a new Binder
     */
    public Binder appendWithTypes(Object... typesAndValues) {
        Class[] types = new Class[typesAndValues.length / 2];
        Object[] values = new Object[typesAndValues.length / 2];

        for (int i = 0; i < typesAndValues.length; i++) {
            if (i % 2 == 0) types[i / 2] = (Class) typesAndValues[i];
            if (i % 2 == 1) values[i / 2] = typesAndValues[i];
        }

        return append(types, values);
    }

    /**
     * Prepend to the argument list the given boolean value.
     *
     * @param value the value to prepend
     * @return a new Binder
     */
    public Binder prepend(boolean value) {
        return new Binder(this, new Insert(0, value));
    }

    /**
     * Prepend to the argument list the given boolean values.
     *
     * @param values the values to prepend
     * @return a new Binder
     */
    public Binder prependBooleans(boolean... values) {
        return new Binder(this, new Insert(0, values));
    }

    /**
     * Prepend to the argument list the given byte value.
     *
     * @param value the value to prepend
     * @return a new Binder
     */
    public Binder prepend(byte value) {
        return new Binder(this, new Insert(0, value));
    }

    /**
     * Prepend to the argument list the given byte values.
     *
     * @param values the values to prepend
     * @return a new Binder
     */
    public Binder prependBytes(byte... values) {
        return new Binder(this, new Insert(0, values));
    }

    /**
     * Prepend to the argument list the given short value.
     *
     * @param value the value to prepend
     * @return a new Binder
     */
    public Binder prepend(short value) {
        return new Binder(this, new Insert(0, value));
    }

    /**
     * Prepend to the argument list the given short values.
     *
     * @param values the values to prepend
     * @return a new Binder
     */
    public Binder prependShorts(short... values) {
        return new Binder(this, new Insert(0, values));
    }

    /**
     * Prepend to the argument list the given char value.
     *
     * @param value the value to prepend
     * @return a new Binder
     */
    public Binder prepend(char value) {
        return new Binder(this, new Insert(0, value));
    }

    /**
     * Prepend to the argument list the given char values.
     *
     * @param values the values to prepend
     * @return a new Binder
     */
    public Binder prependChars(char... values) {
        return new Binder(this, new Insert(0, values));
    }

    /**
     * Prepend to the argument list the given int value.
     *
     * @param value the value to prepend
     * @return a new Binder
     */
    public Binder prepend(int value) {
        return new Binder(this, new Insert(0, value));
    }

    /**
     * Prepend to the argument list the given int values.
     *
     * @param values the values to prepend
     * @return a new Binder
     */
    public Binder prependInts(int... values) {
        return new Binder(this, new Insert(0, values));
    }

    /**
     * Prepend to the argument list the given long value.
     *
     * @param value the value to prepend
     * @return a new Binder
     */
    public Binder prepend(long value) {
        return new Binder(this, new Insert(0, value));
    }

    /**
     * Prepend to the argument list the given long values.
     *
     * @param values the values to prepend
     * @return a new Binder
     */
    public Binder prependLongs(long... values) {
        return new Binder(this, new Insert(0, values));
    }

    /**
     * Prepend to the argument list the given float value.
     *
     * @param value the value to prepend
     * @return a new Binder
     */
    public Binder prepend(float value) {
        return new Binder(this, new Insert(0, value));
    }

    /**
     * Prepend to the argument list the given float values.
     *
     * @param values the values to prepend
     * @return a new Binder
     */
    public Binder prependFloats(float... values) {
        return new Binder(this, new Insert(0, values));
    }

    /**
     * Prepend to the argument list the given double value.
     *
     * @param value the value to prepend
     * @return a new Binder
     */
    public Binder prepend(double value) {
        return new Binder(this, new Insert(0, value));
    }

    /**
     * Prepend to the argument list the given double values.
     *
     * @param values the values to prepend
     * @return a new Binder
     */
    public Binder prependDoubles(double... values) {
        return new Binder(this, new Insert(0, values));
    }

    /**
     * Prepend to the argument list the given argument value(s).
     *
     * @param values the value(s) to prepend
     * @return a new Binder
     */
    public Binder prepend(Object... values) {
        return new Binder(this, new Insert(0, values));
    }

    /**
     * Prepend to the argument list the given argument type(s) and value(s), provided as (Class, value) pairs of
     * arguments.
     *
     * @param typesAndValues the value(s) to append
     * @return a new Binder
     */
    public Binder prependWithTypes(Object... typesAndValues) {
        Class[] types = new Class[typesAndValues.length / 2];
        Object[] values = new Object[typesAndValues.length / 2];

        for (int i = 0; i < typesAndValues.length; i++) {
            if (i % 2 == 0) types[i / 2] = (Class) typesAndValues[i];
            if (i % 2 == 1) values[i / 2] = typesAndValues[i];
        }

        return prepend(types, values);
    }

    /**
     * Append to the argument list the given argument value with the specified type.
     *
     * @param type  the actual type to use, rather than getClass
     * @param value the value to append
     * @return a new Binder
     */
    public Binder append(Class<?> type, Object value) {
        return new Binder(this, new Insert(type().parameterCount(), new Class[]{type}, value));
    }

    /**
     * Append to the argument list the given argument values with the specified types.
     *
     * @param types  the actual types to use, rather than getClass
     * @param values the value(s) to append
     * @return a new Binder
     */
    public Binder append(Class<?>[] types, Object... values) {
        return new Binder(this, new Insert(type().parameterCount(), types, values));
    }

    /**
     * Prepend to the argument list the given argument value with the specified type
     *
     * @param type  the actual type to use, rather than getClass
     * @param value the value(s) to prepend
     * @return a new Binder
     */
    public Binder prepend(Class<?> type, Object value) {
        return new Binder(this, new Insert(0, new Class[]{type}, value));
    }

    /**
     * Prepend to the argument list the given argument values with the specified types.
     *
     * @param types  the actual types to use, rather than getClass
     * @param values the value(s) to prepend
     * @return a new Binder
     */
    public Binder prepend(Class<?>[] types, Object... values) {
        return new Binder(this, new Insert(0, types, values));
    }

    /**
     * Drop a single argument at the given index.
     *
     * @param index the index at which to drop an argument
     * @return a new Binder
     */
    public Binder drop(int index) {
        return drop(index, 1);
    }

    /**
     * Drop from the given index a number of arguments.
     *
     * @param index the index at which to start dropping
     * @param count the number of arguments to drop
     * @return a new Binder
     */
    public Binder drop(int index, int count) {
        return new Binder(this, new Drop(index, Arrays.copyOfRange(type().parameterArray(), index, index + count)));
    }

    /**
     * Drop a single argument at the end of the argument list.
     *
     * @return a new Binder
     */
    public Binder dropLast() {
        return dropLast(1);
    }

    /**
     * Drop from the end of the argument list a number of arguments.
     *
     * @param count the number of arguments to drop
     * @return a new Binder
     */
    public Binder dropLast(int count) {
        assert count <= type().parameterCount();

        return drop(type().parameterCount() - count, count);
    }

    /**
     * Drop a single argument at the beginning of the argument list.
     *
     * @return a new Binder
     */
    public Binder dropFirst() {
        return dropFirst(1);
    }

    /**
     * Drop from the end of the argument list a number of arguments.
     *
     * @param count the number of arguments to drop
     * @return a new Binder
     */
    public Binder dropFirst(int count) {
        assert count <= type().parameterCount();

        return drop(0, count);
    }

    /**
     * Drop all arguments from this handle chain
     *
     * @return a new Binder
     */
    public Binder dropAll() {
        return drop(0, type().parameterCount());
    }

    /**
     * Convert the incoming arguments to the given MethodType. The conversions
     * applied are equivalent to those in MethodHandle.asType(MethodType).
     *
     * @param target the target MethodType
     * @return a new Binder
     */
    public Binder convert(MethodType target) {
        return new Binder(this, new Convert(type()), target);
    }

    /**
     * Convert the incoming arguments to the given MethodType. The conversions
     * applied are equivalent to those in MethodHandle.asType(MethodType).
     *
     * @param returnType the target return type
     * @param argTypes   the target argument types
     * @return a new Binder
     */
    public Binder convert(Class<?> returnType, Class<?>... argTypes) {
        return new Binder(this, new Convert(type()), MethodType.methodType(returnType, argTypes));
    }

    /**
     * Cast the incoming arguments to the given MethodType. The casts
     * applied are equivalent to those in MethodHandles.explicitCastArguments(mh, MethodType).
     *
     * @param type the target MethodType
     * @return a new Binder
     */
    public Binder cast(MethodType type) {
        return new Binder(this, new Cast(type()), type);
    }

    /**
     * Cast the incoming arguments to the given MethodType. The casts
     * applied are equivalent to those in MethodHandle.explicitCastArguments(MethodType).
     *
     * @param returnType the target return type
     * @param argTypes   the target argument types
     * @return a new Binder
     */
    public Binder cast(Class<?> returnType, Class<?>... argTypes) {
        return new Binder(this, new Cast(type()), MethodType.methodType(returnType, argTypes));
    }

    /**
     * Cast the incoming arguments to the given MethodType. The casts
     * applied are equivalent to those in MethodHandle.explicitCastArguments(MethodType).
     *
     * @param returnType the target return type
     * @param firstType  the first argument type, usually a target type
     * @param restTypes  the remaining target argument types
     * @return a new Binder
     */
    public Binder castVirtual(Class<?> returnType, Class<?> firstType, Class<?>... restTypes) {
        return new Binder(this, new Cast(type()), MethodType.methodType(returnType, firstType, restTypes));
    }

    /**
     * Spread a trailing array argument into the specified argument types.
     *
     * @param spreadTypes the types into which to spread the incoming Object[]
     * @return a new Binder
     */
    public Binder spread(Class<?>... spreadTypes) {
        if (spreadTypes.length == 0) {
            return dropLast();
        }

        return new Binder(this, new Spread(type(), spreadTypes));
    }

    /**
     * Spread a trailing array argument into the given number of arguments of
     * the type of the array.
     *
     * @param count the new count of arguments to spread from the trailing array
     * @return a new Binder
     */
    public Binder spread(int count) {
        if (count == 0) {
            return dropLast();
        }

        Class<?> aryType = type().parameterType(type().parameterCount() - 1);

        assert aryType.isArray();

        Class<?>[] spreadTypes = new Class[count];
        Arrays.fill(spreadTypes, aryType.getComponentType());

        return spread(spreadTypes);
    }

    /**
     * Box all incoming arguments from the given position onward into the given array type.
     *
     * @param index the index from which to start boxing args
     * @param type  the array type into which the args will be boxed
     * @return a new Binder
     */
    public Binder collect(int index, Class<?> type) {
        return new Binder(this, new Collect(type(), index, type));
    }

    /**
     * Box all incoming arguments from the given position onward into the given array type.
     *
     * @param index the index from which to start boxing args
     * @param type  the array type into which the args will be boxed
     * @param collector a function to use for collecting the arguments
     * @return a new Binder
     */
    public Binder collect(int index, Class<?> type, MethodHandle collector) {
        return new Binder(this, new Collect(type(), index, type, collector));
    }

    /**
     * Box a range of incoming arguments into the given array type.
     *
     * @param index the index from which to start boxing args
     * @param count the count of arguments to box
     * @param type  the array type into which the args will be boxed
     * @return a new Binder
     */
    public Binder collect(int index, int count, Class<?> type) {
        return new Binder(this, new Collect(type(), index, count, type));
    }

    /**
     * Box a range of incoming arguments into the given array type using the given constructor to construct the array.
     *
     * The collector signature should match (T1, ..., Tn) where T is the type of the arguments being collected and n
     * is the number of arguments being collected.
     *
     * @param index the index from which to start boxing args
     * @param count the count of arguments to box
     * @param type  the array type into which the args will be boxed
     * @param collector a function to use for collecting the arguments
     * @return a new Binder
     */
    public Binder collect(int index, int count, Class<?> type, MethodHandle collector) {
        return new Binder(this, new Collect(type(), index, count, type, collector));
    }

    /**
     * Box all incoming arguments from the given position onward into the given array type.
     * This version accepts a variable number of incoming arguments.
     *
     * @param index the index from which to start boxing args
     * @param type  the array type into which the args will be boxed
     * @return a new Binder
     */
    public Binder varargs(int index, Class<?> type) {
        return new Binder(this, new Varargs(type(), index, type));
    }

    /**
     * Permute the incoming arguments to a new sequence specified by the given values.
     *
     * Arguments may be duplicated or dropped in this sequence.
     *
     * @param reorder the int offsets of the incoming arguments in the desired permutation
     * @return a new Binder
     */
    public Binder permute(int... reorder) {
        return new Binder(this, new Permute(type(), reorder));
    }

    /**
     * Process the incoming arguments using the given handle, inserting the result
     * as the first argument.
     *
     * @param function the function that will process the incoming arguments. Its
     *                 signature must match the current signature's arguments exactly.
     * @return a new Binder
     */
    public Binder fold(MethodHandle function) {
        return new Binder(this, new Fold(function));
    }

    /**
     * Process the incoming arguments using the given handle, leaving the argument list
     * unmodified.
     *
     * @param function the function that will process the incoming arguments. Its
     *                 signature must match the current signature's arguments exactly.
     * @return a new Binder
     */
    public Binder foldVoid(MethodHandle function) {
        if (type().returnType() == void.class) {
            return fold(function);
        } else {
            return fold(function.asType(function.type().changeReturnType(void.class)));
        }
    }

    /**
     * Process the incoming arguments by calling the given static method on the
     * given class, inserting the result as the first argument.
     *
     * @param lookup the java.lang.invoke.MethodHandles.Lookup to use
     * @param target the class on which the method is defined
     * @param method the method to invoke on the first argument
     * @return a new Binder
     */
    public Binder foldStatic(MethodHandles.Lookup lookup, Class<?> target, String method) {
        return fold(Binder.from(type()).invokeStaticQuiet(lookup, target, method));
    }

    /**
     * Process the incoming arguments by calling the given static method on the
     * given class, inserting the result as the first argument.
     *
     * @param target the class on which the method is defined
     * @param method the method to invoke on the first argument
     * @return a new Binder
     */
    public Binder foldStatic(Class<?> target, String method) {
        return foldStatic(lookup, target, method);
    }

    /**
     * Process the incoming arguments by calling the given method on the first
     * argument, inserting the result as the first argument.
     *
     * @param lookup the java.lang.invoke.MethodHandles.Lookup to use
     * @param method the method to invoke on the first argument
     * @return a new Binder
     */
    public Binder foldVirtual(MethodHandles.Lookup lookup, String method) {
        return fold(Binder.from(type()).invokeVirtualQuiet(lookup, method));
    }

    /**
     * Process the incoming arguments by calling the given method on the first
     * argument, inserting the result as the first argument.
     *
     * @param method the method to invoke on the first argument
     * @return a new Binder
     */
    public Binder foldVirtual(String method) {
        return foldVirtual(lookup, method);
    }

    /**
     * Filter incoming arguments, from the given index, replacing each with the
     * result of calling the associated function in the given list. Note that
     * the order in which the filters are applied is undefined; OpenJDK produces
     * handles that execute them in reverse order.
     *
     * @see #filterForward(int, MethodHandle...)
     *
     * @param index     the index of the first argument to filter
     * @param functions the array of functions to transform the arguments
     * @return a new Binder
     */
    public Binder filter(int index, MethodHandle... functions) {
        return new Binder(this, new Filter(index, functions));
    }

    /**
     * Filter incoming arguments, from the given index, replacing each with the
     * result of calling the associated function in the given list. This version
     * guarantees left-to-right evaluation of filter functions, potentially at
     * the cost of a more complex handle tree.
     *
     * @param index     the index of the first argument to filter
     * @param functions the array of functions to transform the arguments
     * @return a new Binder
     */
    public Binder filterForward(int index, MethodHandle... functions) {
        Binder filtered = this;

        for (int i = 0; i < functions.length; i++) {
            filtered = filtered.filter(index + i, functions[i]);
        }

        return filtered;
    }

    /**
     * Filter return value, using a function that produces the current return type
     * from another type. The new endpoint will have the return value that the
     * filter function accepts as an argument.
     *
     * @param function the array of functions to transform the arguments
     * @return a new Binder
     */
    public Binder filterReturn(MethodHandle function) {
        return new Binder(this, new FilterReturn(function));
    }

    /**
     * Apply transforms to run the given handle's logic as a "finally" block.
     *
     * try {
     * some_code // your eventual endpoint
     * } finally {
     * finally_logic // the given handle
     * }
     *
     * The layering uses a combination of catch and fold to reuse the same target
     * handle for both exceptional and non-exceptional paths. In essence, the
     * result is equivalent to using the given post logic as both an exception
     * handler (using catchException) and a "post fold" that runs after the main
     * downstream handles have run.
     *
     * @param post the logic that would live inside the "finally" block
     * @return a new Binder
     */
    public Binder tryFinally(MethodHandle post) {
        return new Binder(this, new TryFinally(post));
    }

    /**
     * Catch the given exception type from the downstream chain and handle it with the
     * given function.
     *
     * @param throwable the exception type to catch
     * @param function  the function to use for handling the exception
     * @return a new Binder
     */
    public Binder catchException(Class<? extends Throwable> throwable, MethodHandle function) {
        return new Binder(this, new Catch(throwable, function));
    }

    /**
     * Apply all transforms to an endpoint that does absolutely nothing. Useful for
     * creating exception handlers in void methods that simply ignore the exception.
     *
     * @return a handle that has all transforms applied and does nothing at its endpoint
     */
    public MethodHandle nop() {
        if (type().returnType() != void.class) {
            throw new InvalidTransformException("must have void return type to nop: " + type());
        }
        return invoke(Binder
                .from(type())
                .drop(0, type().parameterCount())
                .cast(Object.class)
                .constant(null));
    }

    /**
     * Throw the current signature's sole Throwable argument. Return type
     * does not matter, since it will never return.
     *
     * @return a handle that has all transforms applied and which will eventually throw an exception
     */
    public MethodHandle throwException() {
        if (type().parameterCount() != 1 || !Throwable.class.isAssignableFrom(type().parameterType(0))) {
            throw new InvalidTransformException("incoming signature must have one Throwable type as its sole argument: " + type());
        }
        return invoke(MethodHandles.throwException(type().returnType(), type().parameterType(0).asSubclass(Throwable.class)));
    }

    /**
     * Apply the tranforms, binding them to a constant value that will
     * propagate back through the chain. The chain's expected return type
     * at that point must be compatible with the given value's type.
     *
     * @param value the constant value to put at the end of the chain
     * @return a handle that has all transforms applied in sequence up to the constant
     */
    public MethodHandle constant(Object value) {
        return invoke(MethodHandles.constant(type().returnType(), value));
    }

    /**
     * Apply the tranforms, binding them to a handle that will simply return its sole
     * argument as its return value. The endpoint signature must have a single argument
     * of the same type as its return type.
     *
     * @return a handle that has all transforms applied in sequence
     */
    public MethodHandle identity() {
        return invoke(MethodHandles.identity(type().parameterType(0)));
    }

    /**
     * Apply the chain of transforms with the target method handle as the final
     * endpoint. Produces a handle that has the transforms in given sequence.
     *
     * If the final handle's type does not exactly match the initial type for
     * this Binder, an additional cast will be attempted.
     *
     * @param target the endpoint handle to bind to
     * @return a handle that has all transforms applied in sequence up to endpoint
     */
    public MethodHandle invoke(MethodHandle target) {
        MethodHandle current = target;
        for (Transform t : transforms) {
            current = t.up(current);
        }

        // if resulting handle's type does not match start, attempt one more cast
        current = MethodHandles.explicitCastArguments(current, start);

        return current;
    }

    /**
     * Apply the chain of transforms and bind them to a static method specified
     * using the end signature plus the given class and method. The method will
     * be retrieved using the given Lookup and must match the end signature
     * exactly.
     *
     * If the final handle's type does not exactly match the initial type for
     * this Binder, an additional cast will be attempted.
     *
     * @param lookup the MethodHandles.Lookup to use to unreflect the method
     * @param method the Method to unreflect
     * @return the full handle chain, bound to the given method
     * @throws java.lang.IllegalAccessException if the method is not accessible
     */
    public MethodHandle invoke(MethodHandles.Lookup lookup, Method method) throws IllegalAccessException {
        return invoke(lookup.unreflect(method));
    }

    /**
     * Same as {@link #invoke(MethodHandles.Lookup, Method method)} but using the default lookup for this
     * binder.
     */
    public MethodHandle invoke(Method method) throws IllegalAccessException {
        return invoke(lookup.unreflect(method));
    }

    /**
     * Apply the chain of transforms and bind them to a static method specified
     * using the end signature plus the given class and method. The method will
     * be retrieved using the given Lookup and must match the end signature
     * exactly.
     *
     * If the final handle's type does not exactly match the initial type for
     * this Binder, an additional cast will be attempted.
     *
     * This version is "quiet" in that it throws an unchecked InvalidTransformException
     * if the target method does not exist or is inaccessible.
     *
     * @param lookup the MethodHandles.Lookup to use to unreflect the method
     * @param method the Method to unreflect
     * @return the full handle chain, bound to the given method
     */
    public MethodHandle invokeQuiet(MethodHandles.Lookup lookup, Method method) {
        try {
            return invoke(lookup, method);
        } catch (IllegalAccessException iae) {
            throw new InvalidTransformException(iae);
        }
    }

    /**
     * Same as {@link #invokeQuiet(MethodHandles.Lookup, Method method)} but using the default lookup for this
     * binder.
     */
    public MethodHandle invokeQuiet(Method method) {
        try {
            return invoke(lookup, method);
        } catch (IllegalAccessException iae) {
            throw new InvalidTransformException(iae);
        }
    }

    /**
     * Apply the chain of transforms and bind them to a static method specified
     * using the end signature plus the given class and name. The method will
     * be retrieved using the given Lookup and must match the end signature
     * exactly.
     *
     * If the final handle's type does not exactly match the initial type for
     * this Binder, an additional cast will be attempted.
     *
     * @param lookup the MethodHandles.Lookup to use to unreflect the method
     * @param target the class in which to find the method
     * @param name   the name of the method to invoke
     * @return the full handle chain, bound to the given method
     * @throws java.lang.NoSuchMethodException if the method does not exist
     * @throws java.lang.IllegalAccessException if the method is not accessible
     */
    public MethodHandle invokeStatic(MethodHandles.Lookup lookup, Class<?> target, String name) throws NoSuchMethodException, IllegalAccessException {
        return invoke(lookup.findStatic(target, name, type()));
    }

    /**
     * Same as {@link #invokeStaticQuiet(MethodHandles.Lookup, Class, String)} but using the default lookup for this
     * binder.
     */
    public MethodHandle invokeStatic(Class<?> target, String name) throws NoSuchMethodException, IllegalAccessException {
        return invoke(lookup.findStatic(target, name, type()));
    }

    /**
     * Apply the chain of transforms and bind them to a static method specified
     * using the end signature plus the given class and name. The method will
     * be retrieved using the given Lookup and must match the end signature
     * exactly.
     *
     * If the final handle's type does not exactly match the initial type for
     * this Binder, an additional cast will be attempted.
     *
     * This version is "quiet" in that it throws an unchecked InvalidTransformException
     * if the target method does not exist or is inaccessible.
     *
     * @param lookup the MethodHandles.Lookup to use to look up the method
     * @param target the class in which to find the method
     * @param name   the name of the method to invoke
     * @return the full handle chain, bound to the given method
     */
    public MethodHandle invokeStaticQuiet(MethodHandles.Lookup lookup, Class<?> target, String name) {
        try {
            return invokeStatic(lookup, target, name);
        } catch (IllegalAccessException | NoSuchMethodException e) {
            throw new InvalidTransformException(e);
        }
    }

    /**
     * Same as {@link #invokeStaticQuiet(MethodHandles.Lookup, Class, String)} but using the default lookup for this
     * binder.
     */
    public MethodHandle invokeStaticQuiet(Class<?> target, String name) {
        try {
            return invokeStatic(lookup, target, name);
        } catch (IllegalAccessException | NoSuchMethodException e) {
            throw new InvalidTransformException(e);
        }
    }

    /**
     * Apply the chain of transforms and bind them to a virtual method specified
     * using the end signature plus the given class and name. The method will
     * be retrieved using the given Lookup and must match the end signature
     * exactly.
     *
     * If the final handle's type does not exactly match the initial type for
     * this Binder, an additional cast will be attempted.
     *
     * @param lookup the MethodHandles.Lookup to use to look up the method
     * @param name   the name of the method to invoke
     * @return the full handle chain, bound to the given method
     * @throws java.lang.NoSuchMethodException if the method does not exist
     * @throws java.lang.IllegalAccessException if the method is not accessible
     */
    public MethodHandle invokeVirtual(MethodHandles.Lookup lookup, String name) throws NoSuchMethodException, IllegalAccessException {
        return invoke(lookup.findVirtual(type().parameterType(0), name, type().dropParameterTypes(0, 1)));
    }

    /**
     * Same as {@link #invokeVirtual(MethodHandles.Lookup, String)} but using the default lookup for this
     * binder.
     */
    public MethodHandle invokeVirtual(String name) throws NoSuchMethodException, IllegalAccessException {
        return invoke(lookup.findVirtual(type().parameterType(0), name, type().dropParameterTypes(0, 1)));
    }

    /**
     * Apply the chain of transforms and bind them to a virtual method specified
     * using the end signature plus the given class and name. The method will
     * be retrieved using the given Lookup and must match the end signature
     * exactly.
     *
     * If the final handle's type does not exactly match the initial type for
     * this Binder, an additional cast will be attempted.
     *
     * This version is "quiet" in that it throws an unchecked InvalidTransformException
     * if the target method does not exist or is inaccessible.
     *
     * @param lookup the MethodHandles.Lookup to use to look up the method
     * @param name   the name of the method to invoke
     * @return the full handle chain, bound to the given method
     */
    public MethodHandle invokeVirtualQuiet(MethodHandles.Lookup lookup, String name) {
        try {
            return invokeVirtual(lookup, name);
        } catch (IllegalAccessException | NoSuchMethodException e) {
            throw new InvalidTransformException(e);
        }
    }

    /**
     * Same as {@link #invokeVirtualQuiet(MethodHandles.Lookup, String)} but using the default lookup for this
     * binder.
     */
    public MethodHandle invokeVirtualQuiet(String name) {
        try {
            return invokeVirtual(lookup, name);
        } catch (IllegalAccessException | NoSuchMethodException e) {
            throw new InvalidTransformException(e);
        }
    }

    /**
     * Apply the chain of transforms and bind them to a special method specified
     * using the end signature plus the given class and name. The method will
     * be retrieved using the given Lookup and must match the end signature
     * exactly.
     *
     * If the final handle's type does not exactly match the initial type for
     * this Binder, an additional cast will be attempted.
     *
     * @param lookup the MethodHandles.Lookup to use to look up the method
     * @param name   the name of the method to invoke
     * @param caller the calling class
     * @return the full handle chain, bound to the given method
     * @throws java.lang.NoSuchMethodException if the method does not exist
     * @throws java.lang.IllegalAccessException if the method is not accessible
     */
    public MethodHandle invokeSpecial(MethodHandles.Lookup lookup, String name, Class<?> caller) throws NoSuchMethodException, IllegalAccessException {
        return invoke(lookup.findSpecial(type().parameterType(0), name, type().dropParameterTypes(0, 1), caller));
    }

    /**
     * Same as {@link #invokeSpecial(MethodHandles.Lookup, String, Class)} but using the default lookup for this
     * binder.
     */
    public MethodHandle invokeSpecial(String name, Class<?> caller) throws NoSuchMethodException, IllegalAccessException {
        return invoke(lookup.findSpecial(type().parameterType(0), name, type().dropParameterTypes(0, 1), caller));
    }

    /**
     * Apply the chain of transforms and bind them to a special method specified
     * using the end signature plus the given class and name. The method will
     * be retrieved using the given Lookup and must match the end signature
     * exactly.
     *
     * If the final handle's type does not exactly match the initial type for
     * this Binder, an additional cast will be attempted.
     *
     * This version is "quiet" in that it throws an unchecked InvalidTransformException
     * if the target method does not exist or is inaccessible.
     *
     * @param lookup the MethodHandles.Lookup to use to look up the method
     * @param name   the name of the method to invoke
     * @param caller the calling class
     * @return the full handle chain, bound to the given method
     */
    public MethodHandle invokeSpecialQuiet(MethodHandles.Lookup lookup, String name, Class<?> caller) {
        try {
            return invokeSpecial(lookup, name, caller);
        } catch (IllegalAccessException | NoSuchMethodException e) {
            throw new InvalidTransformException(e);
        }
    }

    /**
     * Same as {@link #invokeSpecialQuiet(MethodHandles.Lookup, String, Class)} but using the default lookup for this
     * binder.
     */
    public MethodHandle invokeSpecialQuiet(String name, Class<?> caller) {
        try {
            return invokeSpecial(lookup, name, caller);
        } catch (IllegalAccessException | NoSuchMethodException e) {
            throw new InvalidTransformException(e);
        }
    }

    /**
     * Apply the chain of transforms and bind them to a constructor specified
     * using the end signature plus the given class. The constructor will
     * be retrieved using the given Lookup and must match the end signature's
     * arguments exactly.
     *
     * If the final handle's type does not exactly match the initial type for
     * this Binder, an additional cast will be attempted.
     *
     * @param lookup the MethodHandles.Lookup to use to look up the constructor
     * @param target the constructor's class
     * @return the full handle chain, bound to the given constructor
     * @throws java.lang.NoSuchMethodException if the constructor does not exist
     * @throws java.lang.IllegalAccessException if the constructor is not accessible
     */
    public MethodHandle invokeConstructor(MethodHandles.Lookup lookup, Class<?> target) throws NoSuchMethodException, IllegalAccessException {
        return invoke(lookup.findConstructor(target, type().changeReturnType(void.class)));
    }

    /**
     * Same as {@link #invokeConstructor(MethodHandles.Lookup, Class)} but using the default lookup for this
     * binder.
     */
    public MethodHandle invokeConstructor(Class<?> target) throws NoSuchMethodException, IllegalAccessException {
        return invoke(lookup.findConstructor(target, type().changeReturnType(void.class)));
    }

    /**
     * Apply the chain of transforms and bind them to a constructor specified
     * using the end signature plus the given class. The constructor will
     * be retrieved using the given Lookup and must match the end signature's
     * arguments exactly.
     *
     * If the final handle's type does not exactly match the initial type for
     * this Binder, an additional cast will be attempted.
     *
     * This version is "quiet" in that it throws an unchecked InvalidTransformException
     * if the target method does not exist or is inaccessible.
     *
     * @param lookup the MethodHandles.Lookup to use to look up the constructor
     * @param target the constructor's class
     * @return the full handle chain, bound to the given constructor
     */
    public MethodHandle invokeConstructorQuiet(MethodHandles.Lookup lookup, Class<?> target) {
        try {
            return invokeConstructor(lookup, target);
        } catch (IllegalAccessException | NoSuchMethodException e) {
            throw new InvalidTransformException(e);
        }
    }

    /**
     * Same as {@link #invokeConstructorQuiet(MethodHandles.Lookup, Class)} but using the default lookup for this
     * binder.
     */
    public MethodHandle invokeConstructorQuiet(Class<?> target) {
        try {
            return invokeConstructor(lookup, target);
        } catch (IllegalAccessException | NoSuchMethodException e) {
            throw new InvalidTransformException(e);
        }
    }

    /**
     * Apply the chain of transforms and bind them to an object field retrieval specified
     * using the end signature plus the given class and name. The field must
     * match the end signature's return value and the end signature must take
     * the target class or a subclass as its only argument.
     *
     * If the final handle's type does not exactly match the initial type for
     * this Binder, an additional cast will be attempted.
     *
     * @param lookup the MethodHandles.Lookup to use to look up the field
     * @param name   the field's name
     * @return the full handle chain, bound to the given field access
     * @throws java.lang.NoSuchFieldException if the field does not exist
     * @throws java.lang.IllegalAccessException if the field is not accessible
     * @throws java.lang.NoSuchFieldException if the field does not exist
     * @throws java.lang.IllegalAccessException if the field is not accessible
     */
    public MethodHandle getField(MethodHandles.Lookup lookup, String name) throws NoSuchFieldException, IllegalAccessException {
        return invoke(lookup.findGetter(type().parameterType(0), name, type().returnType()));
    }

    /**
     * Same as {@link #getField(MethodHandles.Lookup, String)}  but using the default lookup for this
     * binder.
     */
    public MethodHandle getField(String name) throws NoSuchFieldException, IllegalAccessException {
        return invoke(lookup.findGetter(type().parameterType(0), name, type().returnType()));
    }

    /**
     * Apply the chain of transforms and bind them to an object field retrieval specified
     * using the end signature plus the given class and name. The field must
     * match the end signature's return value and the end signature must take
     * the target class or a subclass as its only argument.
     *
     * If the final handle's type does not exactly match the initial type for
     * this Binder, an additional cast will be attempted.
     *
     * This version is "quiet" in that it throws an unchecked InvalidTransformException
     * if the target method does not exist or is inaccessible.
     *
     * @param lookup the MethodHandles.Lookup to use to look up the field
     * @param name   the field's name
     * @return the full handle chain, bound to the given field access
     */
    public MethodHandle getFieldQuiet(MethodHandles.Lookup lookup, String name) {
        try {
            return getField(lookup, name);
        } catch (IllegalAccessException | NoSuchFieldException e) {
            throw new InvalidTransformException(e);
        }
    }

    /**
     * Same as {@link #getFieldQuiet(MethodHandles.Lookup, String)}  but using the default lookup for this
     * binder.
     */
    public MethodHandle getFieldQuiet(String name) {
        try {
            return getField(lookup, name);
        } catch (IllegalAccessException | NoSuchFieldException e) {
            throw new InvalidTransformException(e);
        }
    }

    /**
     * Apply the chain of transforms and bind them to a static field retrieval specified
     * using the end signature plus the given class and name. The field must
     * match the end signature's return value and the end signature must take
     * no arguments.
     *
     * If the final handle's type does not exactly match the initial type for
     * this Binder, an additional cast will be attempted.
     *
     * @param lookup the MethodHandles.Lookup to use to look up the field
     * @param target the class in which the field is defined
     * @param name   the field's name
     * @return the full handle chain, bound to the given field access
     * @throws java.lang.NoSuchFieldException if the field does not exist
     * @throws java.lang.IllegalAccessException if the field is not accessible or cannot be modified
     */
    public MethodHandle getStatic(MethodHandles.Lookup lookup, Class<?> target, String name) throws NoSuchFieldException, IllegalAccessException {
        return invoke(lookup.findStaticGetter(target, name, type().returnType()));
    }

    /**
     * Same as {@link #getStatic(MethodHandles.Lookup, Class, String)}  but using the default lookup for this
     * binder.
     */
    public MethodHandle getStatic(Class<?> target, String name) throws NoSuchFieldException, IllegalAccessException {
        return invoke(lookup.findStaticGetter(target, name, type().returnType()));
    }

    /**
     * Apply the chain of transforms and bind them to a static field retrieval specified
     * using the end signature plus the given class and name. The field must
     * match the end signature's return value and the end signature must take
     * no arguments.
     *
     * If the final handle's type does not exactly match the initial type for
     * this Binder, an additional cast will be attempted.
     *
     * This version is "quiet" in that it throws an unchecked InvalidTransformException
     * if the target method does not exist or is inaccessible.
     *
     * @param lookup the MethodHandles.Lookup to use to look up the field
     * @param target the class in which the field is defined
     * @param name   the field's name
     * @return the full handle chain, bound to the given field access
     */
    public MethodHandle getStaticQuiet(MethodHandles.Lookup lookup, Class<?> target, String name) {
        try {
            return getStatic(lookup, target, name);
        } catch (IllegalAccessException | NoSuchFieldException e) {
            throw new InvalidTransformException(e);
        }
    }

    /**
     * Same as {@link #getStaticQuiet(MethodHandles.Lookup, Class, String)}  but using the default lookup for this
     * binder.
     */
    public MethodHandle getStaticQuiet(Class<?> target, String name) {
        try {
            return getStatic(lookup, target, name);
        } catch (IllegalAccessException | NoSuchFieldException e) {
            throw new InvalidTransformException(e);
        }
    }

    /**
     * Apply the chain of transforms and bind them to an object field assignment specified
     * using the end signature plus the given class and name. The end signature must take
     * the target class or a subclass and the field's type as its arguments, and its return
     * type must be compatible with void.
     *
     * If the final handle's type does not exactly match the initial type for
     * this Binder, an additional cast will be attempted.
     *
     * @param lookup the MethodHandles.Lookup to use to look up the field
     * @param name   the field's name
     * @return the full handle chain, bound to the given field assignment
     * @throws java.lang.NoSuchFieldException if the field does not exist
     * @throws java.lang.IllegalAccessException if the field is not accessible or cannot be modified
     */
    public MethodHandle setField(MethodHandles.Lookup lookup, String name) throws NoSuchFieldException, IllegalAccessException {
        return invoke(lookup.findSetter(type().parameterType(0), name, type().parameterType(1)));
    }

    /**
     * Same as {@link #setField(MethodHandles.Lookup, String)}  but using the default lookup for this
     * binder.
     */
    public MethodHandle setField(String name) throws NoSuchFieldException, IllegalAccessException {
        return invoke(lookup.findSetter(type().parameterType(0), name, type().parameterType(1)));
    }

    /**
     * Apply the chain of transforms and bind them to an object field assignment specified
     * using the end signature plus the given class and name. The end signature must take
     * the target class or a subclass and the field's type as its arguments, and its return
     * type must be compatible with void.
     *
     * If the final handle's type does not exactly match the initial type for
     * this Binder, an additional cast will be attempted.
     *
     * This version is "quiet" in that it throws an unchecked InvalidTransformException
     * if the target method does not exist or is inaccessible.
     *
     * @param lookup the MethodHandles.Lookup to use to look up the field
     * @param name   the field's name
     * @return the full handle chain, bound to the given field assignment
     */
    public MethodHandle setFieldQuiet(MethodHandles.Lookup lookup, String name) {
        try {
            return setField(lookup, name);
        } catch (IllegalAccessException | NoSuchFieldException e) {
            throw new InvalidTransformException(e);
        }
    }

    /**
     * Same as {@link #setFieldQuiet(MethodHandles.Lookup, String)}  but using the default lookup for this
     * binder.
     */
    public MethodHandle setFieldQuiet(String name) {
        try {
            return setField(lookup, name);
        } catch (IllegalAccessException | NoSuchFieldException e) {
            throw new InvalidTransformException(e);
        }
    }

    /**
     * Apply the chain of transforms and bind them to an object field assignment specified
     * using the end signature plus the given class and name. The end signature must take
     * the target class or a subclass and the field's type as its arguments, and its return
     * type must be compatible with void.
     *
     * If the final handle's type does not exactly match the initial type for
     * this Binder, an additional cast will be attempted.
     *
     * @param lookup the MethodHandles.Lookup to use to look up the field
     * @param target the class in which the field is defined
     * @param name   the field's name
     * @return the full handle chain, bound to the given field assignment
     * @throws java.lang.NoSuchFieldException if the field does not exist
     * @throws java.lang.IllegalAccessException if the field is not accessible or cannot be modified
     */
    public MethodHandle setStatic(MethodHandles.Lookup lookup, Class<?> target, String name) throws NoSuchFieldException, IllegalAccessException {
        return invoke(lookup.findStaticSetter(target, name, type().parameterType(0)));
    }

    /**
     * Same as {@link #setStatic(MethodHandles.Lookup, Class, String)}  but using the default lookup for this
     * binder.
     */
    public MethodHandle setStatic(Class<?> target, String name) throws NoSuchFieldException, IllegalAccessException {
        return invoke(lookup.findStaticSetter(target, name, type().parameterType(0)));
    }

    /**
     * Apply the chain of transforms and bind them to an object field assignment specified
     * using the end signature plus the given class and name. The end signature must take
     * the target class or a subclass and the field's type as its arguments, and its return
     * type must be compatible with void.
     *
     * If the final handle's type does not exactly match the initial type for
     * this Binder, an additional cast will be attempted.
     *
     * This version is "quiet" in that it throws an unchecked InvalidTransformException
     * if the target method does not exist or is inaccessible.
     *
     * @param lookup the MethodHandles.Lookup to use to look up the field
     * @param target the class in which the field is defined
     * @param name   the field's name
     * @return the full handle chain, bound to the given field assignment
     */
    public MethodHandle setStaticQuiet(MethodHandles.Lookup lookup, Class<?> target, String name) {
        try {
            return setStatic(lookup, target, name);
        } catch (IllegalAccessException | NoSuchFieldException e) {
            throw new InvalidTransformException(e);
        }
    }

    /**
     * Same as {@link #setStaticQuiet(MethodHandles.Lookup, Class, String)}  but using the default lookup for this
     * binder.
     */
    public MethodHandle setStaticQuiet(Class<?> target, String name) {
        try {
            return setStatic(lookup, target, name);
        } catch (IllegalAccessException | NoSuchFieldException e) {
            throw new InvalidTransformException(e);
        }
    }


    /**
     * Construct a new array. The signature at the endpoint must return the array type and accept an integer length.
     *
     * @return a handle that constructs an array
     */
    public MethodHandle newArray() {
        return invoke(MethodHandles.arrayConstructor(type().returnType()));
    }

    /**
     * Get the length of an array. The signature must accept an array and return an integer length.
     *
     * @return a handle that gets the size of an array
     */
    public MethodHandle arrayLength() {
        return invoke(MethodHandles.arrayLength(type().parameterType(0)));
    }

    /**
     * Apply the chain of transforms and bind them to an array element set. The signature
     * at the endpoint must return void and receive the array type, int index, and array
     * element type.
     *
     * @return the full handle chain, bound to an array element set.
     */
    public MethodHandle arraySet() {
        return invoke(MethodHandles.arrayElementSetter(type().parameterType(0)));
    }

    /**
     * Apply the chain of transforms and bind them to a volatile array element set.
     *
     * @see Binder#arraySet()
     * @see VarHandle#setVolatile(Object...)
     */
    public MethodHandle arraySetVolatile() {
        return arrayAccess(VarHandle.AccessMode.SET_VOLATILE);
    }

    /**
     * Apply the chain of transforms and bind them to a release-fenced array element set.
     *
     * @see Binder#arraySet()
     * @see VarHandle#setRelease(Object...)
     */
    public MethodHandle arraySetAcquire() {
        return arrayAccess(VarHandle.AccessMode.SET_RELEASE);
    }

    /**
     * Apply the chain of transforms and bind them to an opaque (no ordering guarantee) array element set.
     *
     * @see Binder#arraySet()
     * @see VarHandle#setVolatile(Object...)
     */
    public MethodHandle arraySetOpaque() {
        return arrayAccess(VarHandle.AccessMode.SET_OPAQUE);
    }


    /**
     * Apply the chain of transforms and bind them to an array element get. The signature
     * at the endpoint must return the array element type and receive the array type and
     * int index.
     *
     * @return the full handle chain, bound to an array element get.
     */
    public MethodHandle arrayGet() {
        return invoke(MethodHandles.arrayElementGetter(type().parameterType(0)));
    }

    /**
     * Apply the chain of transforms and bind them to a volatile array element get.
     *
     * @see Binder#arrayGet()
     * @see VarHandle#getVolatile(Object...)
     */
    public MethodHandle arrayGetVolatile() {
        return arrayAccess(VarHandle.AccessMode.GET_VOLATILE);
    }

    /**
     * Apply the chain of transforms and bind them to an acquire-fenced array element get.
     *
     * @see Binder#arrayGet()
     * @see VarHandle#getAcquire(Object...)
     */
    public MethodHandle arrayGetAcquire() {
        return arrayAccess(VarHandle.AccessMode.GET_ACQUIRE);
    }

    /**
     * Apply the chain of transforms and bind them to an opaque (no ordering guarantee) array element get.
     *
     * @see Binder#arrayGet()
     * @see VarHandle#getVolatile(Object...)
     */
    public MethodHandle arrayGetOpaque() {
        return arrayAccess(VarHandle.AccessMode.GET_OPAQUE);
    }

    /**
     * Apply the chain of transforms and bind them to an array varhandle operation. The
     * signature at the endpoint must match the VarHandle access type passed in.
     */
    public MethodHandle arrayAccess(VarHandle.AccessMode mode) {
        return invoke(MethodHandles.arrayElementVarHandle(type().parameterType(0)).toMethodHandle(mode));
    }

    /**
     * Apply the chain of transforms and bind them to a boolean branch as from
     * java.lang.invoke.MethodHandles.guardWithTest. As with GWT, the current endpoint
     * signature must match the given target and fallback signatures.
     *
     * @param test      the test handle
     * @param truePath  the target handle
     * @param falsePath the fallback handle
     * @return the full handle chain bound to a branch
     */
    public MethodHandle branch(MethodHandle test, MethodHandle truePath, MethodHandle falsePath) {
        return invoke(MethodHandles.guardWithTest(test, truePath, falsePath));
    }

    /**
     * Produce a MethodHandle that invokes its leading MethodHandle argument
     * with the remaining arguments, returning the result.
     *
     * @return a new handle that invokes its leading MethodHandle argument
     */
    public MethodHandle invoker() {
        return invoke(MethodHandles.invoker(start));
    }

    /**
     * Produce Java code that would perform equivalent operations to this binder.
     *
     * @return Java code for the handle adaptations this Binder would produce.
     */
    public String toJava(MethodType incoming) {
        StringBuilder builder = new StringBuilder();
        boolean second = false;
        for (Transform transform : transforms) {
            if (second) builder.append('\n');
            second = true;
            builder.append(transform.toJava(incoming));
        }
        return builder.toString();
    }

}
