/*
 * Copyright 2013-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 java.io.PrintStream;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;

/**
 * Maintains both a Binder, for building a series of transformations, and a
 * current Signature that maps symbolic names to arguments. Transformations
 * normally performed with Binder using argument indices can be done instead
 * using argument names and wildcards.
 *
 * TODO: Examples, or links to wiki examples.
 *
 * @author headius
 */
public class SmartBinder {
    private final Signature start;
    private final List<Signature> signatures = new ArrayList<>();
    private final Binder binder;

    private SmartBinder(Signature start, Binder binder) {
        this.start = start;
        this.signatures.add(start);
        this.binder = binder;
    }

    private SmartBinder(SmartBinder original, Signature next, Binder binder) {
        this.start = original.start;
        this.signatures.add(0, next);
        this.signatures.addAll(original.signatures);
        this.binder = binder;
    }

    /**
     * Get the current Signature for this binder.
     *
     * @return this binder's current Signature
     */
    public Signature signature() {
        return signatures.get(0);
    }

    /**
     * Get the signature this binder started with.
     *
     * @return the signature this binder started with.
     */
    public Signature baseSignature() {
        return signatures.get(signatures.size() - 1);
    }

    /**
     * Get the Binder instance associated with this SmartBinder.
     *
     * @return this SmartBinder's Binder instance
     */
    public Binder binder() {
        return binder;
    }

    /**
     * Create a new SmartBinder from the given Signature.
     *
     * @param inbound the Signature to start from
     * @return a new SmartBinder
     */
    public static SmartBinder from(Signature inbound) {
        return new SmartBinder(inbound, Binder.from(inbound.type()));
    }

    /**
     * Create a new SmartBinder from the given types and argument names.
     *
     * @param retType the type of the return value to start with
     * @param names   the names of arguments
     * @param types   the argument types
     * @return a new SmartBinder
     */
    public static SmartBinder from(Class<?> retType, String[] names, Class<?>... types) {
        return from(Signature.returning(retType).appendArgs(names, types));
    }

    /**
     * Create a new SmartBinder with from the given return type.
     *
     * @param retType the type of the return value to start with
     * @return a new SmartBinder
     */
    public static SmartBinder from(Class<?> retType) {
        return from(Signature.returning(retType));
    }

    /**
     * Create a new SmartBinder with from the given types and argument name.
     *
     * @param retType the type of the return value to start with
     * @param name    the name of the sole argument
     * @param type    the sole argument's type
     * @return a new SmartBinder
     */
    public static SmartBinder from(Class<?> retType, String name, Class<?> type) {
        return from(Signature.returning(retType).appendArg(name, type));
    }

    /**
     * Create a new SmartBinder from the given Signature, using the given
     * Lookup for any handle lookups.
     *
     * @param lookup  the Lookup to use for handle lookups
     * @param inbound the Signature to start from
     * @return a new SmartBinder
     */
    public static SmartBinder from(Lookup lookup, Signature inbound) {
        return new SmartBinder(inbound, Binder.from(lookup, inbound.type()));
    }

    /**
     * Create a new SmartBinder from the given types and argument names,
     * using the given Lookup for any handle lookups.
     *
     * @param lookup  the Lookup to use for handle lookups
     * @param retType the type of the return value to start with
     * @param names   the names of arguments
     * @param types   the argument types
     * @return a new SmartBinder
     */
    public static SmartBinder from(Lookup lookup, Class<?> retType, String[] names, Class<?>... types) {
        return from(lookup, Signature.returning(retType).appendArgs(names, types));
    }

    /**
     * Create a new SmartBinder from the given types and argument name,
     * using the given Lookup for any handle lookups.
     *
     * @param lookup  the Lookup to use for handle lookups
     * @param retType the type of the return value to start with
     * @param name    the name of  the sole arguments
     * @param type    the sole argument's type
     * @return a new SmartBinder
     */
    public static SmartBinder from(Lookup lookup, Class<?> retType, String name, Class<?> type) {
        return from(lookup, Signature.returning(retType).appendArg(name, type));
    }

    ///////////////////////////////////////////////////////////////////////////
    // FOLDS, based on MethodHandles.foldArguments.
    ///////////////////////////////////////////////////////////////////////////

    /**
     * Pass all arguments to the given function and insert the resulting value
     * as newName into the argument list.
     *
     * @param newName  the name of the new first argument where the fold
     *                 function's result will be passed
     * @param function a function which will receive all arguments and have its
     *                 return value inserted into the call chain
     * @return a new SmartBinder with the fold applied
     */
    public SmartBinder fold(String newName, MethodHandle function) {
        return new SmartBinder(this, signature().prependArg(newName, function.type().returnType()), binder.fold(function));
    }

    /**
     * Pass all arguments to the given function and insert the resulting value
     * as newName into the argument list.
     *
     * @param newName  the name of the new first argument where the fold
     *                 function's result will be passed
     * @param function a function which will receive all arguments and have its
     *                 return value inserted into the call chain
     * @return a new SmartBinder with the fold applied
     */
    public SmartBinder fold(String newName, SmartHandle function) {
        if (Arrays.equals(signature().argNames(), function.signature().argNames())) {
            return fold(newName, function.handle());
        } else {
            return fold(newName, signature().changeReturn(function.signature().type().returnType()).permuteWith(function).handle());
        }
    }

    /**
     * Pass all arguments to the given function and drop any result.
     *
     * @param function a function which will receive all arguments and have its
     *                 return value inserted into the call chain
     * @return a new SmartBinder with the fold applied
     */
    public SmartBinder foldVoid(MethodHandle function) {
        return new SmartBinder(this, signature(), binder.foldVoid(function));
    }

    /**
     * Pass all arguments to the given function and drop any result.
     *
     * @param function a function which will receive all arguments and have its
     *                 return value inserted into the call chain
     * @return a new SmartBinder with the fold applied
     */
    public SmartBinder foldVoid(SmartHandle function) {
        if (Arrays.equals(signature().argNames(), function.signature().argNames())) {
            return foldVoid(function.handle());
        } else {
            return foldVoid(signature().asFold(void.class).permuteWith(function).handle());
        }
    }

    /**
     * Acquire a static folding function from the given target class, using the
     * given name and Lookup. Pass all arguments to that function and insert
     * the resulting value as newName into the argument list.
     *
     * @param newName the name of the new first argument where the fold
     *                function's result will be passed
     * @param lookup  the Lookup to use for acquiring a folding function
     * @param target  the class on which to find the folding function
     * @param method  the name of the method to become a folding function
     * @return a new SmartBinder with the fold applied
     */
    public SmartBinder foldStatic(String newName, Lookup lookup, Class<?> target, String method) {
        Binder newBinder = binder.foldStatic(lookup, target, method);
        return new SmartBinder(this, signature().prependArg(newName, newBinder.type().parameterType(0)), binder);
    }

    /**
     * Acquire a public static folding function from the given target class,
     * using the given name. Pass all arguments to that function and insert
     * the resulting value as newName into the argument list.
     *
     * @param newName the name of the new first argument where the fold
     *                function's result will be passed
     * @param target  the class on which to find the folding function
     * @param method  the name of the method to become a folding function
     * @return a new SmartBinder with the fold applied
     */
    public SmartBinder foldStatic(String newName, Class<?> target, String method) {
        Binder newBinder = binder.foldStatic(target, method);
        return new SmartBinder(this, signature().prependArg(newName, newBinder.type().parameterType(0)), newBinder);
    }

    /**
     * Acquire a virtual folding function from the first argument's class,
     * using the given name and Lookup. Pass all arguments to that function and
     * insert the resulting value as newName into the argument list.
     *
     * @param newName the name of the new first argument where the fold
     *                function's result will be passed
     * @param lookup  the Lookup to use for acquiring a folding function
     * @param method  the name of the method to become a folding function
     * @return a new SmartBinder with the fold applied
     */
    public SmartBinder foldVirtual(String newName, Lookup lookup, String method) {
        Binder newBinder = binder.foldVirtual(lookup, method);
        return new SmartBinder(this, signature().prependArg(newName, newBinder.type().parameterType(0)), newBinder);
    }

    /**
     * Acquire a public virtual folding function from the first argument's
     * class, using the given name and Lookup. Pass all arguments to that
     * function and insert the resulting value as newName into the argument
     * list.
     *
     * @param newName the name of the new first argument where the fold
     *                function's result will be passed
     * @param method  the name of the method to become a folding function
     * @return a new SmartBinder with the fold applied
     */
    public SmartBinder foldVirtual(String newName, String method) {
        Binder newBinder = binder.foldVirtual(method);
        return new SmartBinder(this, signature().prependArg(newName, newBinder.type().parameterType(0)), newBinder);
    }

    ///////////////////////////////////////////////////////////////////////////
    // PERMUTES, based on MethodHandles.permuteArguments
    ///////////////////////////////////////////////////////////////////////////

    /**
     * Using the argument names and order in the target Signature, permute the
     * arguments in this SmartBinder. Arguments may be duplicated or omitted
     * in the target Signature, but all arguments in the target must be defined
     * in this SmartBinder .
     *
     * @param target the Signature from which to derive a new argument list
     * @return a new SmartBinder with the permute applied
     */
    public SmartBinder permute(Signature target) {
        return new SmartBinder(this, target, binder.permute(signature().to(target)));
    }

    /**
     * Using the argument names and order in the given targetNames, permute the
     * arguments in this SmartBinder. Arguments may be duplicated or omitted
     * in the targetNames array, but all arguments in the target must be
     * defined in this SmartBinder.
     *
     * @param targetNames the array of names from which to derive a new argument
     *                    list
     * @return a new SmartBinder with the permute applied
     */
    public SmartBinder permute(String... targetNames) {
        return permute(signature().permute(targetNames));
    }

    /**
     * Permute all parameters except the names given. Blacklisting to #permute's
     * whitelisting.
     *
     * @param excludeNames parameter patterns to exclude
     * @return a new SmartBinder with the exclude applied
     */
    public SmartBinder exclude(String... excludeNames) {
        return permute(signature().exclude(excludeNames));
    }

    ///////////////////////////////////////////////////////////////////////////
    // SPREADS, based on MethodHandle#asSpreader.
    ///////////////////////////////////////////////////////////////////////////

    /**
     * Spread a trailing array into the specified argument types.
     *
     * @param spreadNames the names for the spread out arguments
     * @param spreadTypes the types as which to spread the incoming array
     * @return a new SmartBinder with the spread applied
     */
    public SmartBinder spread(String[] spreadNames, Class<?>... spreadTypes) {
        return new SmartBinder(this, signature().spread(spreadNames, spreadTypes), binder.spread(spreadTypes));
    }

    /**
     * Spread a trailing array into count number of arguments, using the
     * natural component type for the array. Build names for the arguments
     * using the given baseName plus the argument's index.
     *
     * Example:
     * Current binder has a signature of (int, String[])void. We want
     * to spread the strings into five arguments named "str".
     *
     * <code>binder = binder.spread("str", 5)</code>
     *
     * The resulting signature will have five trailing arguments named
     * "arg0" through "arg4".
     *
     * @param baseName the base name from which to create the new argument names
     * @param count    the count of arguments to spread
     * @return a new SmartBinder with the spread applied
     */
    public SmartBinder spread(String baseName, int count) {
        return new SmartBinder(this, signature().spread(baseName, count), binder.spread(count));
    }

    ///////////////////////////////////////////////////////////////////////////
    // INSERTS, based on MethodHandles.insertArguments.
    ///////////////////////////////////////////////////////////////////////////

    /**
     * Insert an argument into the argument list at the given index with the
     * given name and value.
     *
     * @param index the index at which to insert the argument
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the insert applied
     */
    public SmartBinder insert(int index, String name, Object value) {
        return new SmartBinder(this, signature().insertArg(index, name, value.getClass()), binder.insert(index, value));
    }

    /**
     * Insert an argument into the argument list at the given index with the
     * given name and value.
     *
     * @param index the index at which to insert the argument
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the insert applied
     */
    public SmartBinder insert(int index, String name, boolean value) {
        return new SmartBinder(this, signature().insertArg(index, name, boolean.class), binder.insert(index, value));
    }

    /**
     * Insert an argument into the argument list at the given index with the
     * given name and value.
     *
     * @param index the index at which to insert the argument
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the insert applied
     */
    public SmartBinder insert(int index, String name, byte value) {
        return new SmartBinder(this, signature().insertArg(index, name, byte.class), binder.insert(index, value));
    }

    /**
     * Insert an argument into the argument list at the given index with the
     * given name and value.
     *
     * @param index the index at which to insert the argument
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the insert applied
     */
    public SmartBinder insert(int index, String name, short value) {
        return new SmartBinder(this, signature().insertArg(index, name, short.class), binder.insert(index, value));
    }

    /**
     * Insert an argument into the argument list at the given index with the
     * given name and value.
     *
     * @param index the index at which to insert the argument
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the insert applied
     */
    public SmartBinder insert(int index, String name, char value) {
        return new SmartBinder(this, signature().insertArg(index, name, char.class), binder.insert(index, value));
    }

    /**
     * Insert an argument into the argument list at the given index with the
     * given name and value.
     *
     * @param index the index at which to insert the argument
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the insert applied
     */
    public SmartBinder insert(int index, String name, int value) {
        return new SmartBinder(this, signature().insertArg(index, name, int.class), binder.insert(index, value));
    }

    /**
     * Insert an argument into the argument list at the given index with the
     * given name and value.
     *
     * @param index the index at which to insert the argument
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the insert applied
     */
    public SmartBinder insert(int index, String name, long value) {
        return new SmartBinder(this, signature().insertArg(index, name, long.class), binder.insert(index, value));
    }

    /**
     * Insert an argument into the argument list at the given index with the
     * given name and value.
     *
     * @param index the index at which to insert the argument
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the insert applied
     */
    public SmartBinder insert(int index, String name, float value) {
        return new SmartBinder(this, signature().insertArg(index, name, float.class), binder.insert(index, value));
    }

    /**
     * Insert an argument into the argument list at the given index with the
     * given name and value.
     *
     * @param index the index at which to insert the argument
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the insert applied
     */
    public SmartBinder insert(int index, String name, double value) {
        return new SmartBinder(this, signature().insertArg(index, name, double.class), binder.insert(index, value));
    }

    /**
     * Insert an argument into the argument list at the given index with the
     * given name and value.
     *
     * @param index the index at which to insert the argument
     * @param name  the name of the new argument
     * @param type  the type to use in the new signature
     * @param value the value of the new argument
     * @return a new SmartBinder with the insert applied
     */
    public SmartBinder insert(int index, String name, Class<?> type, Object value) {
        return new SmartBinder(this, signature().insertArg(index, name, type), binder.insert(index, type, value));
    }

    /**
     * Insert arguments into the argument list at the given index with the
     * given names and values.
     *
     * @param index  the index at which to insert the arguments
     * @param names  the names of the new arguments
     * @param types  the types of the new arguments
     * @param values the values of the new arguments
     * @return a new SmartBinder with the insert applied
     */
    public SmartBinder insert(int index, String[] names, Class<?>[] types, Object... values) {
        return new SmartBinder(this, signature().insertArgs(index, names, types), binder.insert(index, types, values));
    }

    /**
     * Append the given argument to the argument list, assigning it the
     * given name.
     *
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the append applied
     */
    public SmartBinder append(String name, Object value) {
        return new SmartBinder(this, signature().appendArg(name, value.getClass()), binder.append(value));
    }

    /**
     * Append the given argument to the argument list, assigning it the
     * given name.
     *
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the append applied
     */
    public SmartBinder append(String name, boolean value) {
        return new SmartBinder(this, signature().appendArg(name, boolean.class), binder.append(value));
    }

    /**
     * Append the given argument to the argument list, assigning it the
     * given name.
     *
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the append applied
     */
    public SmartBinder append(String name, byte value) {
        return new SmartBinder(this, signature().appendArg(name, byte.class), binder.append(value));
    }

    /**
     * Append the given argument to the argument list, assigning it the
     * given name.
     *
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the append applied
     */
    public SmartBinder append(String name, short value) {
        return new SmartBinder(this, signature().appendArg(name, short.class), binder.append(value));
    }

    /**
     * Append the given argument to the argument list, assigning it the
     * given name.
     *
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the append applied
     */
    public SmartBinder append(String name, char value) {
        return new SmartBinder(this, signature().appendArg(name, char.class), binder.append(value));
    }

    /**
     * Append the given argument to the argument list, assigning it the
     * given name.
     *
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the append applied
     */
    public SmartBinder append(String name, int value) {
        return new SmartBinder(this, signature().appendArg(name, int.class), binder.append(value));
    }

    /**
     * Append the given argument to the argument list, assigning it the
     * given name.
     *
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the append applied
     */
    public SmartBinder append(String name, long value) {
        return new SmartBinder(this, signature().appendArg(name, long.class), binder.append(value));
    }

    /**
     * Append the given argument to the argument list, assigning it the
     * given name.
     *
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the append applied
     */
    public SmartBinder append(String name, float value) {
        return new SmartBinder(this, signature().appendArg(name, float.class), binder.append(value));
    }

    /**
     * Append the given argument to the argument list, assigning it the
     * given name.
     *
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the append applied
     */
    public SmartBinder append(String name, double value) {
        return new SmartBinder(this, signature().appendArg(name, double.class), binder.append(value));
    }

    /**
     * Append the given argument to the argument list, assigning it the
     * given name.
     *
     * @param name  the name of the new argument
     * @param type  the type to use in the new signature
     * @param value the value of the new argument
     * @return a new SmartBinder with the append applied
     */
    public SmartBinder append(String name, Class<?> type, Object value) {
        return new SmartBinder(this, signature().appendArg(name, type), binder.append(new Class<?>[]{type}, value));
    }

    /**
     * Append the given arguments to the argument list, assigning them the
     * given names.
     *
     * @param names  the names of the new arguments
     * @param types  the types to use in the new signature
     * @param values the values of the new arguments
     * @return a new SmartBinder with the append applied
     */
    public SmartBinder append(String[] names, Class<?>[] types, Object... values) {
        return new SmartBinder(this, signature().appendArgs(names, types), binder.append(types, values));
    }

    /**
     * Prepend the given argument to the argument list, assigning it the
     * given name.
     *
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the prepend applied
     */
    public SmartBinder prepend(String name, Object value) {
        return new SmartBinder(this, signature().prependArg(name, value.getClass()), binder.prepend(value));
    }

    /**
     * Prepend the given argument to the argument list, assigning it the
     * given name.
     *
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the prepend applied
     */
    public SmartBinder prepend(String name, boolean value) {
        return new SmartBinder(this, signature().prependArg(name, boolean.class), binder.prepend(value));
    }

    /**
     * Prepend the given argument to the argument list, assigning it the
     * given name.
     *
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the prepend applied
     */
    public SmartBinder prepend(String name, byte value) {
        return new SmartBinder(this, signature().prependArg(name, byte.class), binder.prepend(value));
    }

    /**
     * Prepend the given argument to the argument list, assigning it the
     * given name.
     *
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the prepend applied
     */
    public SmartBinder prepend(String name, short value) {
        return new SmartBinder(this, signature().prependArg(name, short.class), binder.prepend(value));
    }

    /**
     * Prepend the given argument to the argument list, assigning it the
     * given name.
     *
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the prepend applied
     */
    public SmartBinder prepend(String name, char value) {
        return new SmartBinder(this, signature().prependArg(name, char.class), binder.prepend(value));
    }

    /**
     * Prepend the given argument to the argument list, assigning it the
     * given name.
     *
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the prepend applied
     */
    public SmartBinder prepend(String name, int value) {
        return new SmartBinder(this, signature().prependArg(name, int.class), binder.prepend(value));
    }

    /**
     * Prepend the given argument to the argument list, assigning it the
     * given name.
     *
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the prepend applied
     */
    public SmartBinder prepend(String name, long value) {
        return new SmartBinder(this, signature().prependArg(name, long.class), binder.prepend(value));
    }

    /**
     * Prepend the given argument to the argument list, assigning it the
     * given name.
     *
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the prepend applied
     */
    public SmartBinder prepend(String name, float value) {
        return new SmartBinder(this, signature().prependArg(name, float.class), binder.prepend(value));
    }

    /**
     * Prepend the given argument to the argument list, assigning it the
     * given name.
     *
     * @param name  the name of the new argument
     * @param value the value of the new argument
     * @return a new SmartBinder with the prepend applied
     */
    public SmartBinder prepend(String name, double value) {
        return new SmartBinder(this, signature().prependArg(name, double.class), binder.prepend(value));
    }

    /**
     * Prepend the given argument to the argument list, assigning it the
     * given name.
     *
     * @param name  the name of the new argument
     * @param type  the type to use in the new signature
     * @param value the value of the new argument
     * @return a new SmartBinder with the prepend applied
     */
    public SmartBinder prepend(String name, Class<?> type, Object value) {
        return new SmartBinder(this, signature().prependArg(name, type), binder.prepend(type, value));
    }

    /**
     * Prepend the given arguments to the argument list, assigning them the
     * given name.
     *
     * @param names  the names of the new arguments
     * @param types  the types to use in the new signature
     * @param values the values of the new arguments
     * @return a new SmartBinder with the prepend applied
     */
    public SmartBinder prepend(String[] names, Class<?>[] types, Object... values) {
        return new SmartBinder(this, signature().prependArgs(names, types), binder.prepend(types, values));
    }

    ///////////////////////////////////////////////////////////////////////////
    // DROPS, based on MethodHandles.dropArguments.
    ///////////////////////////////////////////////////////////////////////////

    /**
     * Drop the argument with the given name.
     *
     * @param name the name of the argument to drop
     * @return a new SmartBinder with the drop applied
     */
    public SmartBinder drop(String name) {
        int index = signature().argOffset(name);
        return new SmartBinder(this, signature().dropArg(index), binder.drop(index));
    }

    /**
     * Drop the last argument.
     *
     * @return a new SmartBinder with the drop applied
     */
    public SmartBinder dropLast() {
        return dropLast(1);
    }

    /**
     * Drop the last N arguments.
     *
     * @param count the count of arguments to drop
     * @return a new SmartBinder with the drop applied
     */
    public SmartBinder dropLast(int count) {
        return new SmartBinder(this, signature().dropLast(count), binder.dropLast(count));
    }

    /**
     * Drop the first argument.
     *
     * @return a new SmartBinder with the drop applied
     */
    public SmartBinder dropFirst() {
        return dropFirst(1);
    }

    /**
     * Drop the first N arguments.
     *
     * @param count the count of arguments to drop
     * @return a new SmartBinder with the drop applied
     */
    public SmartBinder dropFirst(int count) {
        return new SmartBinder(this, signature().dropFirst(count), binder.dropFirst(count));
    }

    ///////////////////////////////////////////////////////////////////////////
    // COLLECTS, based on MethodHandle#asCollector
    ///////////////////////////////////////////////////////////////////////////

    /**
     * Collect arguments matching namePattern into an trailing array argument
     * named outName.
     *
     * The namePattern is a standard regular expression.
     *
     * @param outName     the name of the new array argument
     * @param namePattern a pattern with which to match arguments for collecting
     * @return a new SmartBinder with the collect applied
     */
    public SmartBinder collect(String outName, String namePattern) {
        int index = signature().argOffsets(namePattern);

        assert index >= 0 : "no arguments matching " + namePattern + " found in signature " + signature();

        Signature newSignature = signature().collect(outName, namePattern);

        return new SmartBinder(this, newSignature, binder.collect(index, signature().argCount() - (newSignature.argCount() - 1), Array.newInstance(signature().argType(index), 0).getClass()));
    }

    /**
     * Collect arguments matching namePattern into an trailing array argument
     * named outName.
     *
     * The namePattern is a standard regular expression.
     *
     * @param outName     the name of the new array argument
     * @param namePattern a pattern with which to match arguments for collecting
     * @return a new SmartBinder with the collect applied
     */
    public SmartBinder collect(String outName, Class<?> outType, String namePattern) {
        int index = signature().argOffsets(namePattern);

        assert index >= 0 : "no arguments matching " + namePattern + " found in signature " + signature();

        Signature newSignature = signature().collect(outName, namePattern);

        return new SmartBinder(this, newSignature, binder.collect(index, signature().argCount() - (newSignature.argCount() - 1), Array.newInstance(signature().argType(index), 0).getClass()));
    }

    /**
     * Collect arguments matching namePattern into an trailing array argument
     * named outName, using collector to construct the array object.
     *
     * 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.
     *
     * The namePattern is a standard regular expression.
     *
     * @param outName     the name of the new array argument
     * @param namePattern a pattern with which to match arguments for collecting
     * @param collector a function to use for collecting the arguments
     * @return a new SmartBinder with the collect applied
     */
    public SmartBinder collect(String outName, String namePattern, MethodHandle collector) {
        int index = signature().argOffsets(namePattern);

        assert index >= 0 : "no arguments matching " + namePattern + " found in signature " + signature();

        Signature newSignature = signature().collect(outName, namePattern);

        return new SmartBinder(this, newSignature, binder.collect(index, signature().argCount() - (newSignature.argCount() - 1), Array.newInstance(signature().argType(index), 0).getClass(), collector));
    }

    ///////////////////////////////////////////////////////////////////////////
    // CASTS, based on MethodHandles.explicitCastArguments and MethodHandle.asType.
    ///////////////////////////////////////////////////////////////////////////

    /**
     * Cast the incoming arguments to the types in the given signature. The
     * argument count must match, but the names in the target signature are
     * ignored.
     *
     * @param target the Signature to which arguments should be cast
     * @return a new SmartBinder with the cast applied
     */
    public SmartBinder cast(Signature target) {
        return new SmartBinder(target, binder.cast(target.type()));
    }

    /**
     * Cast the incoming arguments to the return and argument types given. The
     * argument count must match.
     *
     * @param returnType the return type for the casted signature
     * @param argTypes   the types of the arguments for the casted signature
     * @return a new SmartBinder with the cast applied
     */
    public SmartBinder cast(Class<?> returnType, Class<?>... argTypes) {
        return new SmartBinder(this, new Signature(returnType, argTypes, signature().argNames()), binder.cast(returnType, argTypes));
    }

    /**
     * Cast the incoming arguments to the return, first argument type, and
     * remaining argument types. Provide for convenience when dealing with
     * virtual method argument lists, which frequently omit the target
     * object.
     *
     * @param returnType the return type for the casted signature
     * @param firstArg   the type of the first argument for the casted signature
     * @param restArgs   the types of the remaining arguments for the casted signature
     * @return a new SmartBinder with the cast applied.
     */
    public SmartBinder castVirtual(Class<?> returnType, Class<?> firstArg, Class<?>... restArgs) {
        return new SmartBinder(this, new Signature(returnType, firstArg, restArgs, signature().argNames()), binder.castVirtual(returnType, firstArg, restArgs));
    }

    /**
     * Cast the named argument to the given type.
     *
     * @param name the name of the argument to cast
     * @param type the type to which that argument will be cast
     * @return a new SmartBinder with the cast applied
     */
    public SmartBinder castArg(String name, Class<?> type) {
        Signature newSig = signature().replaceArg(name, name, type);
        return new SmartBinder(this, newSig, binder.cast(newSig.type()));
    }

    /**
     * Cast the return value to the given type.
     *
     * Example: Our current signature is (String)String but the method this
     * handle will eventually call returns CharSequence.
     *
     * <code>binder = binder.castReturn(CharSequence.class);</code>
     *
     * Our handle will now successfully find and call the target method and
     * propagate the returned CharSequence as a String.
     *
     * @param type the new type for the return value
     * @return a new SmartBinder
     */
    public SmartBinder castReturn(Class<?> type) {
        return new SmartBinder(this, signature().changeReturn(type), binder.cast(type, binder.type().parameterArray()));
    }

    ///////////////////////////////////////////////////////////////////////////
    // FILTERS, based on MethodHandles.filterReturnValue and filterArguments.
    ///////////////////////////////////////////////////////////////////////////

    /**
     * Use the given filter function to transform the return value at this
     * point in the binder. The filter will be inserted into the handle, and
     * return values will pass through it before continuing.
     *
     * The filter's argument must match the expected return value downstream
     * from this point in the binder, and the return value must match the
     * return value at this point in the binder.
     *
     * @param filter the function to use to transform the return value at this point
     * @return a new SmartBinder with the filter applied
     */
    public SmartBinder filterReturn(MethodHandle filter) {
        return new SmartBinder(this, signature().changeReturn(filter.type().returnType()), binder.filterReturn(filter));
    }

    /**
     * Use the given filter function to transform the return value at this
     * point in the binder. The filter will be inserted into the handle, and
     * return values will pass through it before continuing.
     *
     * The filter's argument must match the expected return value downstream
     * from this point in the binder, and the return value must match the
     * return value at this point in the binder.
     *
     * @param filter the function to use to transform the return value at this point
     * @return a new SmartBinder with the filter applied
     */
    public SmartBinder filterReturn(SmartHandle filter) {
        return new SmartBinder(this, signature().changeReturn(filter.signature().type().returnType()), binder.filterReturn(filter.handle()));
    }

    ///////////////////////////////////////////////////////////////////////////
    // INVOKES, terminating the handle chain at a concrete target
    ///////////////////////////////////////////////////////////////////////////

    /**
     * Terminate this binder by looking up the named virtual method on the
     * first argument's type. Perform the actual method lookup using the given
     * Lookup object.
     *
     * @param lookup the Lookup to use for handle lookups
     * @param name   the name of the target virtual method
     * @return a SmartHandle with this binder's starting signature, bound
     * to the target method
     * @throws NoSuchMethodException  if the named method with current signature's types does not exist
     * @throws IllegalAccessException if the named method is not accessible to the given Lookup
     */
    public SmartHandle invokeVirtual(Lookup lookup, String name) throws NoSuchMethodException, IllegalAccessException {
        return new SmartHandle(start, binder.invokeVirtual(lookup, name));
    }

    /**
     * Terminate this binder by looking up the named virtual method on the
     * first argument's type. Perform the actual method lookup using the given
     * Lookup object. If the lookup fails, a RuntimeException will be raised,
     * containing the actual reason. This method is for convenience in (for
     * example) field declarations, where checked exceptions noise up code
     * that can't recover anyway.
     *
     * Use this in situations where you would not expect your library to be
     * usable if the target method can't be acquired.
     *
     * @param lookup the Lookup to use for handle lookups
     * @param name   the name of the target virtual method
     * @return a SmartHandle with this binder's starting signature, bound
     * to the target method
     */
    public SmartHandle invokeVirtualQuiet(Lookup lookup, String name) {
        return new SmartHandle(start, binder.invokeVirtualQuiet(lookup, name));
    }

    /**
     * Terminate this binder by looking up the named static method on the
     * given target type. Perform the actual method lookup using the given
     * Lookup object.
     *
     * @param lookup the Lookup to use for handle lookups
     * @param target the type on which to find the static method
     * @param name   the name of the target static method
     * @return a SmartHandle with this binder's starting signature, bound
     * to the target method
     * @throws NoSuchMethodException  if the named method with current signature's types does not exist
     * @throws IllegalAccessException if the named method is not accessible to the given Lookup
     */
    public SmartHandle invokeStatic(Lookup lookup, Class<?> target, String name) throws NoSuchMethodException, IllegalAccessException {
        return new SmartHandle(start, binder.invokeStatic(lookup, target, name));
    }

    /**
     * Terminate this binder by looking up the named static method on the
     * given target type. Perform the actual method lookup using the given
     * Lookup object. If the lookup fails, a RuntimeException will be raised,
     * containing the actual reason. This method is for convenience in (for
     * example) field declarations, where checked exceptions noise up code
     * that can't recover anyway.
     *
     * Use this in situations where you would not expect your library to be
     * usable if the target method can't be acquired.
     *
     * @param lookup the Lookup to use for handle lookups
     * @param target the type on which to find the static method
     * @param name   the name of the target static method
     * @return a SmartHandle with this binder's starting signature, bound
     * to the target method
     */
    public SmartHandle invokeStaticQuiet(Lookup lookup, Class<?> target, String name) {
        return new SmartHandle(start, binder.invokeStaticQuiet(lookup, target, name));
    }

    /**
     * Terminate this binder by invoking the given target handle. The signature
     * of this binder is not compared to the signature of the given
     * SmartHandle.
     *
     * @param target the handle to invoke
     * @return a new SmartHandle with this binder's starting signature, bound
     * through to the given handle
     */
    public SmartHandle invoke(SmartHandle target) {
        return new SmartHandle(start, binder.invoke(target.handle()));
    }

    /**
     * Terminate this binder by invoking the given target handle.
     *
     * @param target the handle to invoke
     * @return a new SmartHandle with this binder's starting signature, bound
     * through to the given handle
     */
    public SmartHandle invoke(MethodHandle target) {
        return new SmartHandle(start, binder.invoke(target));
    }

    /**
     * Terminate this binder by setting an array element based on the current
     * signature. The signature should have the array as its first argument,
     * an integer index as its second, and an appropriately-typed value as its
     * third. It should have a void return value.
     *
     * Invoking the resulting handle will (eventually) perform the array
     * assignment.
     *
     * @return a new SmartHandle with this binder's starting signature, bound
     * to an array assignment operation
     */
    public SmartHandle arrayGet() {
        return new SmartHandle(start, binder.arrayGet());
    }

    /**
     * Terminate this binder by getting an array element based on the current
     * signature. The signature should have the array as its first argument and
     * an integer index as its second, and an appropriately-typed return value.
     *
     * Invoking the resulting handle will (eventually) perform the array
     * assignment.
     *
     * @return a new SmartHandle with this binder's starting signature, bound
     * to an array dereference operation
     */
    public SmartHandle arraySet() {
        return new SmartHandle(start, binder.arraySet());
    }

    /**
     * Terminate this binder by returning its sole remaining argument. The
     * signature must take only one argument whose type matches the return
     * type.
     *
     * Invoking the resulting handle will (eventually) return the argument
     * passed in at this point.
     *
     * @return a new SmartHandle with this binder's starting signature that
     * simply returns its sole received argument
     */
    public SmartHandle identity() {
        return new SmartHandle(start, binder.identity());
    }

    ///////////////////////////////////////////////////////////////////////////
    // OTHER UTILITIES
    ///////////////////////////////////////////////////////////////////////////

    /**
     * Print this binder's current signature to System.out.
     *
     * @return this SmartBinder
     */
    public SmartBinder printSignature() {
        return printSignature(System.out);
    }

    /**
     * Print this binder's current signature to the give PrintStream.
     *
     * @return this SmartBinder
     */
    public SmartBinder printSignature(PrintStream ps) {
        ps.println(signature().toString());
        return this;
    }

    /**
     * Produce a SmartHandle from this binder that invokes a leading
     * MethodHandle argument with the remaining arguments.
     *
     * @return a SmartHandle that invokes its leading MethodHandle argument
     */
    public SmartHandle invoker() {
        return new SmartHandle(start, binder.invoker());
    }

    /**
     * Filter the arguments matching the given pattern using the given filter function.
     *
     * @param pattern the regular expression pattern to match arguments
     * @param filter the MethodHandle to use to filter the arguments
     * @return a new SmartBinder with the filter applied
     */
    public SmartBinder filter(String pattern, MethodHandle filter) {
        String[] argNames = signature().argNames();
        Pattern pat = Pattern.compile(pattern);

        Binder newBinder = binder();
        Signature newSig = signature();
        for (int i = 0; i < argNames.length; i++) {
            if (pat.matcher(argNames[i]).matches()) {
                newBinder = newBinder.filter(i, filter);
                newSig = newSig.argType(i, filter.type().returnType());
            }
        }

        return new SmartBinder(newSig, newBinder);
    }

    /**
     * @see Binder#tryFinally(MethodHandle)
     */
    public SmartBinder tryFinally(MethodHandle post) {
        return new SmartBinder(this, signature(), binder.tryFinally(post));
    }
}
