/*
 * $Id: Library.java,v 1.1.1.1 1998/05/11 18:43:02 metlov Exp $
 *
 * This file is part of the Java Expressions Library (JEL).
 *   For more information about JEL visit :
 *    http://galaxy.fzu.cz/JEL/
 *
 * (c) 1998 by Konstantin Metlov(metlov@fzu.cz);
 *
 * JEL is Distributed under the terms of GNU General Public License.
 *    This code comes with ABSOLUTELY NO WARRANTY.
 *  For license details see COPYING file in this directory.
 */

package gnu.jel;

import gnu.jel.debug.Debug;
import gnu.jel.debug.Tester;
import java.util.Hashtable;
import java.util.Vector;
import java.util.Enumeration;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

/**
 * A Library of the methods, callable from compiled expressions.
 * <P> There are following constraints on the functions in the library
 * classes:
 * <UL> 
 * <LI>Functions can not return objects which are changeable by other
 * functions in the library. The best is if the functions do not return
 * mutable objects at all.
 * <LI>If a static function has the internal state it should be explicitly
 * marked as such (see markStateDependent(String name,Class[] params)). 
 * Evaluation of stateless functions of the constant arguments will be
 * performed at compile time. All static library functions are initially
 * assumed to be state independent. The example of the function which will
 * certainly have to be made state dependent is <TT>java.lang.Math.random</TT>
 * </UL>
 */
public class Library {

  /**
   * Classes, whose static methods are exported
   */
  private Class[] staticLib;
  
  /**
   * Classes, whose virtual methods are exported
   */
  private Class[] dynamicLib;
  

  private Hashtable names;

  private Hashtable dynIDs;

  private Hashtable stateless;
  
  /**
   * Creates a library for JEL.
   * <P> The following should be kept in mind when constructing a library:
   * <OL>
   * <LI>This constructor may throw IllegalArgumentException if it does not
   * like something in Your library. The requirements to the method names
   * are somewhat more strict, than in Java as the <TT>class.method(..)</TT>
   * syntax is not used.
   * <LI>When calling the 
   * <TT>CompiledExpression.evaluate(Object[] dynalib)</TT> of the
   * expression, using dynamic library methods it is needed to pass as
   *  <TT>dynalib</TT> parameter the array of objects, of the classes 
   * _exactly_ in the same order as they are appearing in the
   * <TT>dynamicLib</TT> parameter of this constructor. If You do not
   * allow to call dynamic methods (there is no sense, then, to use a compiler)
   * it is possible to pass <TT>null</TT> istead of <TT>dynalib</TT>.
   * <LI> Generally speaking, this class should not let You to create wrong
   * libraries. It's methods will throw exceptions, return <TT>false</TT>'s , 
   * ignore Your actions,... ;)
   * </OL>
   * If methods in the library classes conflict with each other, the last
   * conflicting method will be skipped. You will not get any messages unless
   * debugging is ON (see <TT>gnu.jel.debug.Debug.enabled</TT>). This is
   * done to avoid unnecessary error messages in the production code of the
   * compiler.
   * @param staticLib is the array of classes, whose public static 
   *  methods are exported.
   * @param dynamicLib is the array of classes, whose public virutal
   *  methods are exported.
   */
  public Library(Class[] staticLib,Class[] dynamicLib) {
    this.staticLib=staticLib;
    this.dynamicLib=dynamicLib;
    rehash();
  };
  
  private void rehash() {
    names = new Hashtable();
    dynIDs = new Hashtable();
    stateless=new Hashtable();
    if (staticLib!=null) {
      for (int i=0; i<staticLib.length; i++) {
	Method[] ma=staticLib[i].getMethods();
	for(int j=0; j<ma.length; j++) {
	  if (Modifier.isStatic(ma[j].getModifiers())) {
	    if (rehash(ma[j])) stateless.put(ma[j],Boolean.TRUE);
	  };
	};
      };
    };
    if (dynamicLib!=null) {
      for (int i=0; i<dynamicLib.length; i++) {
	Integer dynID=new Integer(i);
	Method[] ma=dynamicLib[i].getMethods();
	for(int j=0; j<ma.length; j++) {
	  if (!Modifier.isStatic(ma[j].getModifiers())) {
	    if (rehash(ma[j])) dynIDs.put(ma[j],dynID); // Register ID
	  };
	};
      };
    };
  };
  
  private boolean rehash(Method m) {
    String name=m.getName();
    String signature=ExpressionImage.getSignature(m);
    Hashtable signatures=(Hashtable)names.get(name);
    if (signatures==null) { 
      // No method with this name was added
      Hashtable signatures_new=new Hashtable();
      signatures_new.put(signature,m);
      names.put(name,signatures_new);
      return true;
    };
    // Name exists in the library, check for possible signature conflict.
    Method conflicting_method=(Method)signatures.get(signature);
    if (conflicting_method==null) { // No conflict
      signatures.put(signature,m);
      return true;
    };
    if (Debug.enabled) {
      Debug.println("Conflict was detected during the library initialization."+
		    " Conflicting method \""+name+signature+"\", conflicting "+
		    " classes : "+
		    conflicting_method.getDeclaringClass().getName()+" and "+
		    m.getDeclaringClass().getName()+" .");
    };
    // If no debug then the method is ignored.
    return false;
  };

  /**
   * This function is itended for marking static function as having the internal state.
   * <P> If You include <TT>java.lang.Math</TT> into the library it is
   * necessary to mark <TT>java.lang.random()</TT> as having the state. This
   * can be done by calling <TT>markStateDependent("random",null)</TT>
   * <P> Please specify parameters as close as possible, otherwise You can
   * accidentally mark another function.
   * @param name is the function name.
   * @param params are the possible invocation parameters of the function.
   * @exception NoSuchMethodException if the method can't be resolved   
   */
  public void markStateDependent(String name, Class[] params) 
       throws NoSuchMethodException {
    Method m=getMethod(name,params);
    Object removed=stateless.remove(m);
    if (Debug.enabled)
      Debug.assert(removed!=null,"State dependent function \""+m.toString()+
		   "\"is made state dependend again.");
  };
  
  /**
   * Used to check if the given method is stateless.
   * @param m is the method to check.
   * @return true if the method is stateless and can be invoked at
   *    compile time.
   */
  public boolean isStateless(Method m) {
    return stateless.containsKey(m);
  };
  
  /**
   * Used to search library for method.
   * <P> The method with the same name, and closest (convertible) parameter
   * types is returned. If there are several methods the most specific one
   * is used. 
   * Ambiguities are detected. Example of detectable ambiguity:<BR>
   * You want to call : <TT>someName(int, int)<TT> <BR>
   * There are two applicable methods : <TT>someName(int, double)<TT>,
   * <TT>someName(double, int)<TT> , requirements to parameter types of each
   *  of those can be satisfied by _widening_ conversions.<BR>
   * Those methods are same specific, there is no most specific method in 
   * terms of Java Language Specification (15.11.2.2). Thus there is ambiguity
   * and null will be returned.
   * <P> Java compiler normally would not allow to define such ambiguous 
   * methods in the same class. However, as this library is assembled from
   * several Java classes such ambiguities can happen, and should be
   * detected.
   * @param name is the name of the method to find.
   * @param params are the types of formal parameters in the method invocation.
   * @return the method object of the resolved method in this library.
   * @exception NoSuchMethodException if the method can't be resolved
   */
  Method getMethod(String name,Class[] params) throws NoSuchMethodException {
    Hashtable signatures=(Hashtable) names.get(name);

    if (signatures==null)
      throw new NoSuchMethodException("There is no function \""+name+
				      "\" defined.");
    
    // Choose applicable methods
    Vector applicable_methods=new Vector();
    for(Enumeration e=signatures.elements();e.hasMoreElements();) {
      Method cm=(Method) e.nextElement();
      Class[] cp=cm.getParameterTypes();
      
      boolean applicable=false;
      if (params!=null) { // if no parameters requested
	if (cp.length==params.length) {  // If number of arguments matches
	  applicable=true;
	  for(int i=0;((i<cp.length) && applicable);i++) {
	    applicable=ExpressionImage.canConvertByWidening(params[i],cp[i]);
	  };
	};
      } else {
	applicable=(cp.length==0);
      };
      
      if (applicable) applicable_methods.addElement(cm);
    };
    
    if (applicable_methods.size()==0) {
      throw new NoSuchMethodException("Function \""+name+"\" exists,"+
				      " but parameters "+
				      describe(name,params)+
				      " can not be accepted by it.");
    };
    
    if (applicable_methods.size()==1) 
      return (Method) applicable_methods.firstElement();
    
    // Search for the most specific method
    Enumeration e=applicable_methods.elements();
    Method most_specific=(Method)e.nextElement();
    Class[] most_specific_params=most_specific.getParameterTypes();
    while (e.hasMoreElements()) {
      Method cm=(Method) e.nextElement();
      Class[] cp=cm.getParameterTypes();
      boolean moreSpecific=true;
      boolean lessSpecific=true;
      
      for(int i=0; i<cp.length; i++) {
	moreSpecific = moreSpecific && 
	  ExpressionImage.canConvertByWidening(cp[i],most_specific_params[i]);
	lessSpecific = lessSpecific &&
	  ExpressionImage.canConvertByWidening(most_specific_params[i],cp[i]);
      };
      
      if (moreSpecific && (!lessSpecific)) {
	
	most_specific=cm;
	most_specific_params=cp;
      };
     
      if (! (moreSpecific ^ lessSpecific))
	throw new NoSuchMethodException("Ambiguity detected between \""+
					describe(name,most_specific_params) +
					"\" and \""+ describe(name,cp) +
					"\" on invocation \""+
					describe(name,params)+"\" .");
      
    };
    
    return most_specific;
  };

  private String describe(String name,Class[] params) {
    String res="";
    StringBuffer invp=new StringBuffer();
    invp.append(name); 
    invp.append('(');
    
    if (params!=null)
      for(int k=0;k<params.length;k++) {
	if (k!=0) invp.append(',');
	invp.append(params[k].toString());
      };
    invp.append(')');
    res=invp.toString();
    return res;
  };
  
  /**
   * Returns ID(position in the object array) of the dynamic Method.
   * <P> ID's are used to locate the pointers to the objects, implementing
   * dynamic methods, in the array, argument of evaluate(Object[]) function.
   * @param m method to get an ID of.
   * @return the ID of the method or -1 if the method is static.
   * @exception NullPointerException if method is not a dynamic method of
   *            this library.
   */
  int getDynamicMethodClassID(Method m) {
    
    if (Modifier.isStatic(m.getModifiers())) return -1;
    
    Integer id=(Integer)dynIDs.get(m);
    return id.intValue();
  };
  
  
  // ------------------------- TESTSUITE -------------------------------
  
  /**
   * Performs unitary test of the library.
   * @param args ignored.
   */
  public static void main(String[] args) {
    Tester t=new Tester(System.out);
    test(t);
    t.summarize();
  };

  /**
   * Performs unitary test of the library.
   * <p> Used if all package is being tested and not just codegen.
   * @param t Tester to report test results.
   */
  public static void test(Tester t) {
    Library ll=null;
    Class math=null;
    try {
      math=Class.forName("java.lang.Math");
    } catch(ClassNotFoundException e) {
      Debug.println("It is IMPOSSIBLE :)");
    };

    t.startTest("Creating the library of java.lang.Math");
    try {
      Class[] sl=new Class[1];
      sl[0]=math;
      Library l=new Library(sl,null);
      ll=l;
      t.testOK();
    } catch (Throwable e) {
      Debug.reportThrowable(e);
      t.testFail();
    };

    t.startTest("Attempt of invocation round(double)");
    try {
      Class[] par=new Class[1];
      par[0]=Double.TYPE;
      Method mf=ll.getMethod("round",par);
      if ((mf!=null) && (mf.equals(math.getMethod("round",par))))
	  t.testOK(); else t.testFail();
    } catch (Throwable e) {
      Debug.reportThrowable(e);
      t.testFail();
    };

    t.startTest("Attempt of invocation round(float)");
    try {
      Class[] par=new Class[1];
      par[0]=Float.TYPE;
      Method mf=ll.getMethod("round",par);
      if ((mf!=null) && (mf.equals(math.getMethod("round",par))))
	  t.testOK(); else t.testFail();
    } catch (Throwable e) {
      Debug.reportThrowable(e);
      t.testFail();
    };

    t.startTest("Attempt of invocation round(int) best is round(float)");
    try {
      Class[] par=new Class[1];
      par[0]=Integer.TYPE;
      Method mf=ll.getMethod("round",par);

      Class[] par1=new Class[1];
      par1[0]=Float.TYPE;


      if ((mf!=null) && (mf.equals(math.getMethod("round",par1))))
	  t.testOK(); else t.testFail();
    } catch (Throwable e) {
      Debug.reportThrowable(e);
      t.testFail();
    };

    t.startTest("Attempt of invocation abs(int) best is abs(int)");
    try {
      Class[] par=new Class[1];
      par[0]=Integer.TYPE;
      Method mf=ll.getMethod("abs",par);

      Class[] par1=new Class[1];
      par1[0]=Integer.TYPE;

      if ((mf!=null) && (mf.equals(math.getMethod("abs",par1))))
	  t.testOK(); else t.testFail();
    } catch (Throwable e) {
      Debug.reportThrowable(e);
      t.testFail();
    };

    t.startTest("Attempt of invocation abs(byte) best is abs(int)");
    try {
      Class[] par=new Class[1];
      par[0]=Byte.TYPE;
      Method mf=ll.getMethod("abs",par);
      
      Class[] par1=new Class[1];
      par1[0]=Integer.TYPE;
      
      if ((mf!=null) && (mf.equals(math.getMethod("abs",par1))))
	  t.testOK(); else t.testFail();
    } catch (Throwable e) {
      Debug.reportThrowable(e);
      t.testFail();
    };

    t.startTest("Attempt of invocation abs(char) best is abs(int)");
    try {
      Class[] par=new Class[1];
      par[0]=Character.TYPE;
      Method mf=ll.getMethod("abs",par);
      
      Class[] par1=new Class[1];
      par1[0]=Integer.TYPE;
      
      if ((mf!=null) && (mf.equals(math.getMethod("abs",par1))))
	  t.testOK(); else t.testFail();
    } catch (Throwable e) {
      Debug.reportThrowable(e);
      t.testFail();
    };
    
    t.startTest("Attempt of invocation min(char,float) best is min(float,float)");
    try {
      Class[] par=new Class[2];
      par[0]=Character.TYPE;
      par[1]=Float.TYPE;
      Method mf=ll.getMethod("min",par);
      
      Class[] par1=new Class[2];
      par1[0]=Float.TYPE;
      par1[1]=Float.TYPE;
      
      if ((mf!=null) && (mf.equals(math.getMethod("min",par1))))
	  t.testOK(); else t.testFail();
    } catch (Throwable e) {
      Debug.reportThrowable(e);
      t.testFail();
    };

    t.startTest("Checking assignment of state dependence ");
    try {
      ll.markStateDependent("random",null);
      if (!ll.isStateless(ll.getMethod("random",null)))
	t.testOK();
      else
	t.testFail();
    } catch (Throwable e) {
      Debug.reportThrowable(e);
      t.testFail();
    };


  };

};
