// Created by Lawrence PC Dol.  Released into the public domain.
//
// Source is licensed for any use, provided this copyright notice is retained.
// No warranty for any purpose whatsoever is implied or expressed.  The author
// is not liable for any losses of any kind, direct or indirect, which result
// from the use of this software.

package <your-package-here>;

import java.lang.reflect.*;

/**
 * Implements a method callback using reflection, avoiding definition of an interface for simple callbacks.  Both the
 * object and the method must be publicly accessible. In certain contexts, using Method.setAccessible() may be
 * appropriate.
 * <p>
 * Usually, the API which is utilizing callbacks, will take a Callback and create from it a Callback.WithParms using the
 * latter to invoke the callback method. This delegates the tedium of assigning the parameter elements for each
 * invocation.
 * <p>
 * Because method reflection is name-based, care will normally be needed to ensure that the methods used in a Callback
 * are not obfuscated in the final distribution JAR.
 * <p>
 * Threading Design : [ ] Single Threaded  [x] Threadsafe  [ ] Immutable  [ ] Isolated
 * <p>
 * </b>THREAD SAFETY NOTE</b>: In order for invocations of the callback to be threadsafe, the invoker must allocate the
 * parameter array either off the stack or each the specific invocation - the former is recommended when efficiency
 * matters.  In some cases it may be necessary to use an instance or static array - <i>in such cases the method which
 * takes the callback <b>must</b> clearly document this single-threading restriction</i>.  Further, in those cases it is
 * strongly recommended that Callback.WithParms be used instead of Callback, to further reinforce that the invocation is
 * not threadsafe.
 *
 * @author          Lawrence Dol
 * @since           Build 2008.0709.1456
 */

public class Callback
extends Object
{

// *****************************************************************************
// INSTANCE PROPERTIES
// *****************************************************************************

private final Object                    target;                                 // target object
private final Method                    method;                                 // target method

// *****************************************************************************
// INSTANCE CREATE/DELETE
// *****************************************************************************

/**
 * Create a callback for a static target object.
 */
public Callback(Method mth) {
    this(null,mth);
    }

/**
 * Create a callback into the specified object for the specified method.
 * If the object is of type java.lang.Class, the method is assumed to be static, otherwise the method is assumed instance.  The object may not be null.
 * If the method has no arguments, prms may be null.
 */
public Callback(Object obj, String mth, Class... prms) {
    this(((obj instanceof Class) ? null : obj),getMethod(((obj instanceof Class) ? (Class)obj : obj.getClass()),mth,false,prms));
    }

/**
 * Create a callback into the specified object for the specified existing callback.
 */
public Callback(Object obj, Callback cbk) {
    this(obj,cbk.method);
    }

/**
 * Create a callback into the specified object for the specified method.
 * If the method is static, the object is ignored and may be null.
 */
public Callback(Object obj, Method mth) {
    super();

    target=obj;
    method=mth;

    if(target==null           && !Modifier.isStatic(method.getModifiers()) ) { throw new IllegalArgumentException("Callback conflict - method is not static and object reference is null"); }
    if(target!=null           &&  Modifier.isStatic(method.getModifiers()) ) { throw new IllegalArgumentException("Callback conflict - method is static and object reference is not null"); }
    if(!method.isAccessible() && !Modifier.isPublic(method.getModifiers()) ) { throw new IllegalArgumentException("Callback conflict - method is neither public nor marked accessible"   ); }
    if(target!=null           && target.getClass()!=mth.getDeclaringClass()) { throw new IllegalArgumentException("Callback conflict - target object and method classes are not the same"); }
    }

// *****************************************************************************
// INSTANCE METHODS - ACCESSORS
// *****************************************************************************

public String toString() {
    return method.toString();
    }

public int getParmCount() {
    return getParmTypes().length;
    }

public Class[] getParmTypes() {
    return method.getParameterTypes();
    }

public Class getReturnType() {
    return method.getReturnType();
    }

// *****************************************************************************
// INSTANCE METHODS
// *****************************************************************************

/** Invoke the callback method with no arguments.  This simple calls invoke(null). */
public Object invoke() {
    return invoke(null);
    }

/** Invoke the callback method using the supplied arguments. If the method has no arguments, args may be null. */
public Object invoke(Object[] args) {
    try { return method.invoke(target,args); }
    catch(java.lang.IllegalAccessException thr) {
        throw new IllegalArgumentException("Callback method inaccessible: " +method.toString()+" - "+thr);
        }
    catch(java.lang.reflect.InvocationTargetException thr) {
        Throwable targetthr=thr.getTargetException();
        if(targetthr instanceof RuntimeException) { throw (RuntimeException)targetthr;                                                                    }
        else                                   { throw new IllegalArgumentException("Callback Invocation Exception: "+method.toString()+" - "+targetthr); }
        }
    }

// *****************************************************************************
// INSTANCE INNER CLASSES
// *****************************************************************************

// *****************************************************************************
// STATIC NESTED CLASSES
// *****************************************************************************

    /**
     * A Callback which includes an internal parameter array for invocations.  This implementation is not thread-safe,
     * and is separated from the base Callback for this reason.  An API writer should consider carefully the choice of
     * whether to use this or Callback - this is more convenient, but Callback can be made thread-safe by declaring the
     * parameter array on the call stack.
     * <p>
     * This variation for a callback should only be used where the context precludes multithreaded use by design.
     * <p>
     * Use of the resulting callback must use the invocation with the same number of parameters as is required by the
     * supplied callback's method.  Anything else will throw an exception.
     */
    static public class WithParms
    extends Object
    {
    private final Callback                  callback;                               // callback to invoke
    private final Object[]                  parameters;                             // default arguments array

    // *****************************************************************************
    // INSTANCE CREATE/DELETE
    // *****************************************************************************

    /**
     * Create a callback with parameters from the supplied callback.
     */
    public WithParms(Callback cbk) {
        super();

        callback=cbk;
        parameters=new Object[cbk.getParmCount()];
        }

    /**
     * Create a callback with parameters from the supplied callback and verify the callback method's parameter count matches the supplied expected count.
     */
    public WithParms(Callback cbk, int exp) {
        this(cbk);
        if(parameters.length!=exp) { throw new IllegalArgumentException("Supplied Callback is not valid - expected a callback with "+exp+" parameters, but the supplied callback has "+parameters.length+" parameters"); }
        }

    // *****************************************************************************
    // INSTANCE METHODS - ACCESSORS
    // *****************************************************************************

    /** Invoke the callback with 1 parameter. */
    public Object invoke(Object p1) {
        if(parameters.length!=1) { throw new IllegalArgumentException("Illegal invocation of Callback.WithParms - 1 parameters were supplied, but "+parameters.length+" are required"); }
        parameters[0]=p1;
        return callback.invoke(parameters);
        }

    /** Invoke the callback with 1 parameter. */
    public Object invoke(Object p1, Object p2) {
        if(parameters.length!=2) { throw new IllegalArgumentException("Illegal invocation of Callback.WithParms - 2 parameters were supplied, but "+parameters.length+" are required"); }
        parameters[0]=p1;
        parameters[1]=p2;
        return callback.invoke(parameters);
        }

    /** Invoke the callback with 1 parameter. */
    public Object invoke(Object p1, Object p2, Object p3) {
        if(parameters.length!=3) { throw new IllegalArgumentException("Illegal invocation of Callback.WithParms - 3 parameters were supplied, but "+parameters.length+" are required"); }
        parameters[0]=p1;
        parameters[1]=p2;
        parameters[2]=p3;
        return callback.invoke(parameters);
        }

    /** Invoke the callback with 1 parameter. */
    public Object invoke(Object p1, Object p2, Object p3, Object p4) {
        if(parameters.length!=4) { throw new IllegalArgumentException("Illegal invocation of Callback.WithParms - 4 parameters were supplied, but "+parameters.length+" are required"); }
        parameters[0]=p1;
        parameters[1]=p2;
        parameters[2]=p3;
        parameters[3]=p4;
        return callback.invoke(parameters);
        }

    /** Invoke the callback with 1 parameter. */
    public Object invoke(Object p1, Object p2, Object p3, Object p4, Object p5) {
        if(parameters.length!=5) { throw new IllegalArgumentException("Illegal invocation of Callback.WithParms - 5 parameters were supplied, but "+parameters.length+" are required"); }
        parameters[0]=p1;
        parameters[1]=p2;
        parameters[2]=p3;
        parameters[3]=p4;
        parameters[4]=p5;
        return callback.invoke(parameters);
        }

    /** Invoke the callback with 1 parameter. */
    public Object invoke(Object p1, Object p2, Object p3, Object p4, Object p5, Object p6) {
        if(parameters.length!=6) { throw new IllegalArgumentException("Illegal invocation of Callback.WithParms - 6 parameters were supplied, but "+parameters.length+" are required"); }
        parameters[0]=p1;
        parameters[1]=p2;
        parameters[2]=p3;
        parameters[3]=p4;
        parameters[4]=p5;
        parameters[5]=p6;
        return callback.invoke(parameters);
        }

    /** Invoke the callback with 1 parameter. */
    public Object invoke(Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7) {
        if(parameters.length!=7) { throw new IllegalArgumentException("Illegal invocation of Callback.WithParms - 7 parameters were supplied, but "+parameters.length+" are required"); }
        parameters[0]=p1;
        parameters[1]=p2;
        parameters[2]=p3;
        parameters[3]=p4;
        parameters[4]=p5;
        parameters[5]=p6;
        parameters[6]=p7;
        return callback.invoke(parameters);
        }

    /** Invoke the callback with 1 parameter. */
    public Object invoke(Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7, Object p8) {
        if(parameters.length!=8) { throw new IllegalArgumentException("Illegal invocation of Callback.WithParms - 8 parameters were supplied, but "+parameters.length+" are required"); }
        parameters[0]=p1;
        parameters[1]=p2;
        parameters[2]=p3;
        parameters[3]=p4;
        parameters[4]=p5;
        parameters[5]=p6;
        parameters[6]=p7;
        parameters[7]=p8;
        return callback.invoke(parameters);
        }

    /** Invoke the callback with 1 parameter. */
    public Object invoke(Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7, Object p8, Object p9) {
        if(parameters.length!=9) { throw new IllegalArgumentException("Illegal invocation of Callback.WithParms - 9 parameters were supplied, but "+parameters.length+" are required"); }
        parameters[0]=p1;
        parameters[1]=p2;
        parameters[2]=p3;
        parameters[3]=p4;
        parameters[4]=p5;
        parameters[5]=p6;
        parameters[6]=p7;
        parameters[7]=p8;
        parameters[8]=p9;
        return callback.invoke(parameters);
        }

    /** Invoke the callback with 1 parameter. */
    public Object invoke(Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7, Object p8, Object p9, Object p10) {
        if(parameters.length!=10) { throw new IllegalArgumentException("Illegal invocation of Callback.WithParms - 10 parameters were supplied, but "+parameters.length+" are required"); }

        parameters[0]=p1;
        parameters[1]=p2;
        parameters[2]=p3;
        parameters[3]=p4;
        parameters[4]=p5;
        parameters[5]=p6;
        parameters[6]=p7;
        parameters[7]=p8;
        parameters[8]=p9;
        parameters[9]=p10;
        return callback.invoke(parameters);
        }
    } // END CLASS

// *****************************************************************************
// STATIC PROPERTIES
// *****************************************************************************

// *****************************************************************************
// STATIC INIT & MAIN
// *****************************************************************************

// *****************************************************************************
// STATIC METHODS
// *****************************************************************************

/**
 * Obtain a public method object.
 * <p>
 * Because method reflection is name-based, care will normally be needed to ensure that the methods used in a Callback
 * are not obfuscated in the final distribution JAR.
 */
static public Method getMethod(Class cls, String nam, Class... prms) {
    return getMethod(cls,nam,false,prms);
    }

/**
 * Obtain a method object, optionally making it publicly accessible.
 * <p>
 * Because method reflection is name-based, care will normally be needed to ensure that the methods used in a Callback
 * are not obfuscated in the final distribution JAR.
 *
 * @throws          IllegalArgumentException If the specified method cannot be located or accessed.
 */
static public Method getMethod(Class cls, String nam, boolean setacc, Class... prms) {
    Method                              mth=null;

    try {
        mth=cls.getDeclaredMethod(nam,prms);
        if     (setacc                                ) { mth.setAccessible(true);                                                                   }
        else if(!Modifier.isPublic(mth.getModifiers())) { throw new IllegalArgumentException("Callback method not public: method "+nam+" in class "+cls.getName()); }
        else if(!Modifier.isPublic(cls.getModifiers())) { throw new IllegalArgumentException("Callback class not public: method "+nam+" in class "+cls.getName());  }
        }
    catch(NoSuchMethodException thr) { throw new IllegalArgumentException("Callback method not found: "      +nam+" in class "+cls.getName()+" - "+thr); }
    catch(SecurityException     thr) { throw new IllegalArgumentException("Callback method not accessible: " +nam+" in class "+cls.getName()+" - "+thr); }
    catch(Throwable             thr) { throw new IllegalArgumentException("Callback method not reflected: "  +nam+" in class "+cls.getName()+" - "+thr); }
    return mth;
    }

} // END CLASS

