/*
 * ReflectObject.java --
 *
 *	Implements the Tcl internal representation of Java
 *	reflection object.
 *
 * See the file "license.terms" for information on usage and
 * redistribution of this file, and for a DISCLAIMER OF ALL
 * WARRANTIES.
 *
 * RCS: @(#) $Id: ReflectObject.java,v 1.1.1.1 1998/10/14 21:09:14 cvsadmin Exp $
 *
 */

package tcl.lang;

import java.lang.reflect.*;
import java.util.*;
import java.beans.*;

/*
 * A ReflectObject is used to create and access arbitrary Java objects
 * using the Java Reflection API. It wraps around a Java object (i.e.,
 * an instance of any Java class) and expose it to Tcl scripts. The
 * object is registered inside the interpreter and is given a string
 * name. Tcl scripts can manipulate this object as long as the the
 * reference count of the object is greater than zero.
 */

public class ReflectObject extends InternalRep implements CommandWithDispose {

//The java.lang.Object wrapped by the ReflectObject representation.

Object javaObj;
Class  javaClass;

/*
 * The interpreter in which the java.lang.Object is registered in.
 * ReflectObject's are not shared among interpreters for safety
 * reasons.
 */

Interp ownerInterp;

/*
 * The reference ID of this object.
 */

String refID;

/*
 * This variables records how many TclObject's are using
 * this ReflectObject internal rep. In this example:
 *
 *     set x [new java.lang.Integer 1]
 *     set y [format %s $x]
 *     java::info methods $y
 *
 * The two objects $x and $y share the same ReflectObject instance.
 * useCount is 2 when the java::info command has just translated the
 * string $y into a ReflectObject.
 *
 * useCount is initially 1. It will be more than 1 only when the
 * script tries to refer to the object using its string form, or when
 * the same object is returned by the Reflection API more than once.
 *
 * This variable is called useCount rather than refCount to avoid
 * confusion with TclObject.refCount.
 */

private int useCount;

/*
 * This variable marks whether the object is still considered "valid"
 * in Tcl scripts. An object is no longer valid if its object command
 * has been explicitly deleted from the interpreter.
 */

private boolean isValid;

/*
 * Stores the bindings of this ReflectObject. This member variable is used
 * in the BeanEventMgr class.
 */

Hashtable bindings;


//the string representation of the null reflect object

protected static final String NULL_REP = JavaNullCmd.getNullString();

protected static final String NOCONVERT = "-noconvert";


/*
 *----------------------------------------------------------------------
 *
 * makeReflectObject --
 *
 *	Wraps an Java Object in a ReflectObject. If the same Java
 *	Object has already been wrapped in a ReflectObject, return
 *	that ReflectObject. Otherwise, create a new ReflectObject to
 *	wrap the Java Object.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The object is unregistered (and thus no longer accessible from
 *	Tcl scripts) if no other if no other TclObjects are
 *	still using this internal rep.
 *
 *----------------------------------------------------------------------
 */

private static ReflectObject
makeReflectObject(
    Interp interp,
    Class  cl,
    Object obj)
  throws TclException //if a null class with a non null object is passed in
{

    if (obj == null) {
        //this is the null reflect object case, just call initReflectObject
        //with no hashtable and no key

        //System.out.println("null object");

        return initReflectObject(interp,null,null,null,null);
    }

    if (cl == null) {
        //we have no way to deal with a non null object that has a
        //null class reference type, we must give up in this case

        throw new TclException(interp,"non null reflect object with null class is not valid");
    }
    
    
    //apply builtin type conversion rules

    if (cl == Integer.TYPE)
      cl = Integer.class;
    else if (cl == Boolean.TYPE)
      cl = Boolean.class;
    else if (cl == Long.TYPE)
      cl = Long.class;
    else if (cl == Float.TYPE)
      cl = Float.class;
    else if (cl == Double.TYPE)
      cl = Double.class;
    else if (cl == Byte.TYPE)
      cl = Byte.class;
    else if (cl == Short.TYPE)
      cl = Short.class;
    else if (cl == Character.TYPE)
      cl = Character.class;    
    else if (cl == Void.TYPE)
      throw new TclException(interp,"void object type can not be reflected");



    ReflectObject roRep = null;

    //now we hash the object id to find the ReflectObject or 
    //ReflectObjects that represent this object. The case where
    //a single object is referenced as only one type will be common
    //so the object we hash to can be either a ReflectObject or a
    //Hashtable. At runtime we determine which one it is. While
    //this may take a little longer it will save a lot of space
    //because an object will not need to have a hashtable
    //allocated to it unless it is getting referenced multiple
    //time and by different object reference types.
    
    Object refOrTable = interp.reflectObjTable.get(obj);


    if (refOrTable == null) {
      //there is no reflect object for this java object, just add
      //a single reflect object to the reflectObjTable in the interp
      
      //System.out.println("new object for " + cl);

      return initReflectObject(interp,cl,obj,interp.reflectObjTable,obj);
      
    } else {

      //found something in the table and now we must find out
      //if it is a ReflectObject or a HashTable

      if (refOrTable instanceof ReflectObject) {
	//we found a single reflect object, if the new type is the
	//same as the current ref type then we are ok. If not
	//then we need to make a hashtable for the ReflectObjects
	
	roRep = (ReflectObject) refOrTable;
	
	//the easy types match case, just return the ReflectObject
	if (roRep.javaClass == cl) {
	    //System.out.println("type match for " + cl);
	    roRep.useCount++;
	    return roRep;
	} else {
	  //we need to allocate a hashtable and add the old ref
	  //in with the new ref that will be added to the hash table
	  
	  //System.out.println("type mismatch allocating typetable for " + cl);

	  Hashtable h = new Hashtable(3);
	  
	  //make new hashtable into the interp reflect hash for the object
	  interp.reflectObjTable.put(obj,h);

	  //hash the existing class to the existing ReflectObject
	  h.put(roRep.javaClass,roRep);
	  
	  //now init a new ReflectObject and add it to the hashtable
	  //we just created with the new reference class as the key
	  
	  return initReflectObject(interp,cl,obj,h,cl);
	}
	
      } else {
	//it must be a hash table if it is not a ReflectObject
	//we now hash the class type into this hashtable to
	//find the ReflectObject for the ref type we care about
		
	Hashtable h = (Hashtable) refOrTable;
	
	roRep = (ReflectObject) h.get(cl);

	//add a new reflect object if it is not in the table
	//map the class to the ReflectObject in the table

	if (roRep == null) {
	    //System.out.println("type mismatch in typetable " + cl);
	    return initReflectObject(interp,cl,obj,h,cl);
	} else {
	    //System.out.println("type match in typetable for " + cl);
	    //if the rep is already in the table then just return it
	    roRep.useCount++;
	    return roRep;
	}

      }
    }
}



/*
 *----------------------------------------------------------------------
 *
 * initReflectObject --
 *
 *      Helper for makeReflectObject, it prepares a ReflectObject so
 *      that it can be used inside the interp.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The object is unregistered (and thus no longer accessible from
 *	Tcl scripts) if no other if no other TclObjects are
 *	still using this internal rep.
 *
 *----------------------------------------------------------------------
 */

private static ReflectObject
initReflectObject(
    Interp interp,          //the interp we are running inside of
    Class  cl,              //the class of the object to reflect
    Object obj,             //the java object to reflect
    Hashtable addTable,     //the hashtable we should add the ReflectObject to
    Object    addKey)       //the key that the ReflectObject is added under
throws TclException
{
  ReflectObject roRep;

  if ((cl != null) && (cl.isArray())) {
    roRep = new ArrayObject();
  } else {
    roRep = new ReflectObject();
  }

  roRep.ownerInterp = interp;
  roRep.javaObj = obj;
  roRep.javaClass = cl;

  if (roRep.javaObj != null) {

      //make sure the object can be represented by the given Class
      Class obj_class = roRep.javaObj.getClass();
      if (! roRep.javaClass.isAssignableFrom(obj_class)) {
	throw new TclException(interp,"object of type " + obj_class.getName()
	   + " can not be referenced as type " + roRep.javaClass.getName());
      }


      //Register the object in the interp.
	    
      interp.reflectObjCount++;
      roRep.refID = "java0x" + Long.toHexString(interp.reflectObjCount);

      interp.createCommand(roRep.refID,roRep);
      interp.reflectIDTable.put(roRep.refID,roRep);
      
      //interp.reflectObjTable.put(obj, roRep);
      addTable.put(addKey,roRep);

  } else {
      //The null object is not registered (hence it can't be deleted).  
      roRep.refID = NULL_REP;
  }

  roRep.useCount = 1;
  roRep.isValid = true;
  
  return roRep;
}


/*
 *----------------------------------------------------------------------
 *
 * dispose --
 *
 *	Called when a TclObject no longers uses this internal rep. We
 *	unregister the java.lang.Object if no other TclObjects are
 *	still using this internal rep.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The object is unregistered (and thus no longer accessible from
 *	Tcl scripts) if no other if no other TclObjects are
 *	still using this internal rep.
 *
 *----------------------------------------------------------------------
 */

protected void
dispose()
{
    useCount--;
    if ((useCount == 0) && (refID != NULL_REP)) {
        //No TclObject is using this internal rep anymore. Free it.

        ownerInterp.deleteCommand(refID);

        ownerInterp.reflectIDTable.remove(refID);

	disposeOfTableRefrences();

	ownerInterp = null;
	javaObj = null;
	javaClass = null;
	bindings = null;
	refID = NULL_REP;
    }
}



//helper function for dispose, it will clean up reprences inside
//the interp.reflectObjTable which currently holds either a
//ReflectObject or a table of reflect objects

private void
disposeOfTableRefrences() {

  Object refOrTable = ownerInterp.reflectObjTable.get(javaObj);

  if (refOrTable instanceof ReflectObject) {
      //this is the easy case we just need to remove the key
      //that maps the java object to this reflect object

      //System.out.println("remove object");

      ownerInterp.reflectObjTable.remove(javaObj);
  } else {
      //in this case the main table hashed to a hash table
      //so we need remove the current ref type from the hashtable

      Hashtable h = (Hashtable) refOrTable;

      //System.out.println("remove typetable entry " + javaClass);

      //remove the entry for the current refrencing class
      h.remove(javaClass);

      //now if there is only one key left in the hashtable
      //we can remap the original table so that it maps
      //the java object to the ReflectObject

      if (h.size() == 1) {
          //get the only value out of the table
	  Object value = h.elements().nextElement();

          //put the original single mapping back into the
	  //interp reflect table which also frees up the
	  //reftype hashtable for garbage collection
      
          ownerInterp.reflectObjTable.put(javaObj,value);

	  //System.out.println("remove typetable");
      }
  }
}



/*
 *----------------------------------------------------------------------
 *
 * duplicate --
 *
 *	Get a copy of this ReflectObject for copy-on-write
 *	operations. We just increment its useCount and return the same
 *	ReflectObject because ReflectObject's cannot be modified, so
 *	they don't need copy-on-write protections.
 *
 * Results:
 *	The same internal rep.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

protected InternalRep
duplicate()
{
    useCount++;
    return this;
}

/*
 *----------------------------------------------------------------------
 *
 * setReflectObjectFromAny --
 *
 *	Called to convert an TclObject's internal rep to ReflectObject.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	When successful, the internal representation of tobj is
 *	changed to ReflectObject, if it is not already so.
 *
 *----------------------------------------------------------------------
 */

private static void
setReflectObjectFromAny(
    Interp interp,		// Current interpreter. Must be non-null.
    TclObject tobj)		// The TclObject to convert.
throws
    TclException		// If the object's internal rep is not
				// already ReflectObject, and the string rep
				// is not the name of a java.lang.Object
				// registered in the given interpreter.
				// Error message is left inside interp.
{
    InternalRep rep = tobj.getInternalRep();
    ReflectObject roRep;

    if (rep instanceof ReflectObject) {
	roRep = (ReflectObject) rep;
	if (roRep.isValid && (roRep.ownerInterp == interp)) {
	    return;
	}
    }

    String s = tobj.toString();
      if (s.startsWith("java")) {
	if (s.equals(NULL_REP)) {
	  tobj.setInternalRep(makeReflectObject(interp,null,null));
	  return;
	} else {
	  roRep = (ReflectObject) interp.reflectIDTable.get(s);
	  if ((roRep != null) && (roRep.isValid)) {
	    roRep.useCount++;
	    tobj.setInternalRep(roRep);
	    return;
	  }
	}
      }

    throw new TclException(interp, "unknown java object \"" + 
	    tobj + "\"");
}

/*
 *----------------------------------------------------------------------
 *
 * newInstance --
 *
 *	Creates a new instance of a TclObject that wraps a
 *	java.lang.Object.
 *
 * Results:
 *	The newly created TclObject.
 *
 * Side effects:
 *	The java.lang.Object will be registered in the interpreter.
 *
 *----------------------------------------------------------------------
 */

public static TclObject
newInstance(
    Interp interp,		// Current interpreter.
    Class cl,                   // class of the reflect instance
    Object obj)			// java.lang.Object to wrap.
throws TclException
{
    return new TclObject(makeReflectObject(interp, cl, obj));
}

/*
 *----------------------------------------------------------------------
 *
 * get --
 *
 *	Returns a java.lang.Object represented by tobj. tobj must have a
 *	ReflectObject internal rep, or its string rep must be one of the
 *	currently registered objects.
 *
 * Results:
 *	The Java object represented by tobj.
 *
 * Side effects:
 *	When successful, the internal representation of tobj is
 *	changed to ReflectObject, if it is not already so.
 *
 *----------------------------------------------------------------------
 */

public static Object
get(
    Interp interp,		// Current interpreter. Must be non-null.
    TclObject tobj)		// The TclObject to query.
throws
    TclException		// If the internal rep of tobj cannot
				// be converted to a ReflectObject. 
				// Error message is left inside interp.
{
    setReflectObjectFromAny(interp, tobj);
    ReflectObject rep = (ReflectObject)(tobj.getInternalRep());
    return rep.javaObj;
}


/*
 *----------------------------------------------------------------------
 *
 * getClass --
 *
 *	Returns a java.lang.Class object that is the ref type of this
 *      reflect object. This is not always the same class as is returned
 *      by a call to ((Object) o).getClass().
 *
 * Results:
 *	The Java class object used to reference tobj.
 *
 * Side effects:
 *	When successful, the internal representation of tobj is
 *	changed to ReflectObject, if it is not already so.
 *
 *----------------------------------------------------------------------
 */

public static Class
getClass(
    Interp interp,		// Current interpreter. Must be non-null.
    TclObject tobj)		// The TclObject to query.
throws
    TclException		// If the internal rep of tobj cannot
				// be converted to a ReflectObject. 
				// Error message is left inside interp.
{
    setReflectObjectFromAny(interp, tobj);
    ReflectObject rep = (ReflectObject)(tobj.getInternalRep());
    return rep.javaClass;
}


/*
 *----------------------------------------------------------------------
 *
 * getReflectObject --
 *
 *	Returns the InternalRep of a the ReflectObject represented by
 *	tobj. Only the java:: commands should call this
 *	method. (java::bind, java::call, etc).
 *
 * Results:
 *	The Java object represented by tobj.
 *
 * Side effects:
 *	When successful, the internal representation of tobj is
 *	changed to ReflectObject, if it is not already so.
 *
 *----------------------------------------------------------------------
 */

static ReflectObject
getReflectObject(
    Interp interp,		// Current interpreter. Must be non-null
    TclObject tobj)		// The TclObject to query.
throws
    TclException		// If the internal rep of tobj cannot
				// be converted to a ReflectObject.
				// Error message is left inside interp.
{
    setReflectObjectFromAny(interp, tobj);
    return (ReflectObject)(tobj.getInternalRep());
}

/*
 *----------------------------------------------------------------------
 *
 * cmdProc --
 *
 *	This cmdProc implements the Tcl command used to invoke methods
 *	of the java.lang.Object stored in this ReflectObject internal
 *	rep. For example, this method is called to process the "$v"
 *	command at the second line of this script:
 *
 *	    set v [java::new java.util.Vector]
 *	    $v addElement "foo"
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	If the given method returns a value, it is converted into a
 *	TclObject and stored as the result of the interpreter.
 *
 *----------------------------------------------------------------------
 */

public void
cmdProc(
    Interp interp,			// Current interpreter.
    TclObject argv[])			// Argument list.
throws
    TclException			// Standard Tcl exception;
{
    boolean convert;
    int sigIdx;

    if (argv.length < 2) {
	throw new TclNumArgsException(interp, 1, argv, 
		"?-noconvert? signature ?arg arg ...?");
    }

    String arg1 = argv[1].toString();
    if ((arg1.length() >= 2) && (NOCONVERT.startsWith(arg1))) {
	convert = false;
	sigIdx = 2;
    } else {
	convert = true;
	sigIdx = 1;
    }

    if (argv.length < sigIdx + 1) {
	throw new TclNumArgsException(interp, 1, argv, 
		"?-noconvert? signature ?arg arg ...?");
    }

    int startIdx = sigIdx + 1;
    int count = argv.length - startIdx;

    interp.setResult(JavaInvoke.callMethod(interp, argv[0],
	    argv[sigIdx], argv, startIdx, count, convert));
}

/*
 *----------------------------------------------------------------------
 *
 * disposeCmd --
 *
 * 	This method is called when the object command has been deleted
 * 	from an interpreter. It marks the ReflectObject no longer
 * 	accessible from Tcl scripts.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The ReflectObject is no longer accessible from Tcl scripts.
 *
 *----------------------------------------------------------------------
 */

public void
disposeCmd()
{
    isValid = false;
}

/*
 *----------------------------------------------------------------------
 *
 * toString --
 *
 *	Called to query the string representation of the Tcl
 *	object. This method is called only by TclObject.toString()
 *	when TclObject.stringRep is null.
 *
 * Results:
 * 	Returns the string representation of this ReflectObject.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

public String
toString()
{
  //return "java0x" + Integer.toHexString(refID);
  return refID;
}

/*
 *----------------------------------------------------------------------
 *
 * init --
 *
 *	Initializes fields inside the Interp that are used by
 *	ReflectObject's.
 *
 * Results:
 * 	None.
 *
 * Side effects:
 *	The fields are initialized.
 *
 *----------------------------------------------------------------------
 */

static void
init(
    Interp interp)
{
    interp.reflectIDTable = new Hashtable();
    interp.reflectObjTable = new Hashtable();
    interp.reflectObjCount = 0;
}

} // end ReflectObject

