/*-*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * $Id: Library.java,v 1.5 2000/08/15 21:37:55 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 -- 2000 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 gnu.jel.reflect.Field;
import gnu.jel.reflect.LocalField;
import gnu.jel.reflect.Method;
import gnu.jel.reflect.LocalMethod;
import gnu.jel.reflect.Member;
import java.util.Hashtable;
import java.util.Vector;
import java.util.Enumeration;

/**
 * A namespace for JEL expressions.
 * <P> There are two types of members in the library, those which are stateless
 * (i.e. their value depends only on their arguments, if there are any) and
 * stateful (also called here dynamic). The result of evaluation of 
 * stateful members may depend on other factors besides their arguments.
 *
 * <P>Examples of possible stateless members are : <TT>Math.sin(double)</TT>,
 * <TT>Math.PI</TT>.
 * <P>Examples of possible stateful members are : <TT>Math.random()</TT>,
 * <TT>System.currentTimeMillis()</TT>.
 *
 * <P>Stateless members of this library are always static members of the 
 * classes, which define them. The inverse is generally not true. However,
 * this library goes as far as assuming that static members are stateless, 
 * if this assumption does not hold for some of Your members it is possible to 
 * mark them as stateful using the <TT>markStateDependent()</TT> method of
 * this class.
 *
 * <P>The most crucial difference between the two kind of members of this
 * library is that evaluation of stateless methods is attempted by JEL at
 * a compile time during the constants folding phase.
 */
public class Library {

  private Hashtable names; // Hashtable(names, Hashtable(signatures,members))
  
  private Hashtable dynIDs; // Hashtable(members, integers)

  private Hashtable stateless; // Hashtable(members, booleans)

  private Hashtable dotClasses; // Hashtable(classes, Hashtable(names, Hashtable(signatures, members)))
  private boolean noDotSecurity=false;
  
  /**
   * Creates a library for JEL.
   * <P> See the three argument constructor for more info.
   * @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,dynamicLib,null);
  };

  /**
   * 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.
   * <P>The array (dotClasses), which is the third argument of 
   * this constructor determines how (and whether) to compile the 
   * dot operators encountered in expressions. These operators are
   * of the form <object>.(<method>|<field>), 
   * which means to call method (access field)
   * of an <object>. There can be three types of the behaviour: 
   * <BR>1) dot operators are prohibited (<TT>dotClasses==null</TT>),
   * this is behaviour of older version of JEL.
   * <BR>2) dot operators are allowed on all classes
   * (<TT>dotClasses==new Class[0], an empty array).
   * Depending on the types of objects returned by the static/dynamic library
   * classes this may pose a security risk.
   * <BR>3) dot operators are allowed only on some classes. This is achieved 
   * by listing these classes in the dotClasses array.
   * @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.
   * @param dotClasses is the array of classes on which the dot ('.')
   * operation is allowed.
   */
  public Library(Class[] staticLib,Class[] dynamicLib,Class[] dotClasses) {
    if (dotClasses==null) {
      this.dotClasses=null;
    } else {
      noDotSecurity=(dotClasses.length==0);
      this.dotClasses=new Hashtable();
      // hash the names
      Class[] temp=new Class[1];
      for(int i=0;i<dotClasses.length;i++) 
        rehashDotClass(dotClasses[i]);
    };

    names = new Hashtable();
    dynIDs = new Hashtable();
    stateless=new Hashtable();

    if (staticLib!=null)
      rehash(staticLib,names,null,stateless);
    if (dynamicLib!=null)
      rehash(dynamicLib,names,dynIDs,null);

  };

  private void rehashDotClass(Class cls) {
    Hashtable tempNames=new Hashtable();
    Class[] temp=new Class[1];
    temp[0]=cls;
    //    rehash(temp,tempNames,null,new Hashtable());
    rehash(temp,tempNames,new Hashtable(),null);
    dotClasses.put(cls,tempNames);
  };

  private static void rehash(Class[] arr, Hashtable hashedNames, 
                             Hashtable dynIDs, Hashtable stateless) {
    for (int i=0; i<arr.length; i++) {
      Integer dynID=new Integer(i);

      java.lang.reflect.Method[] marr=arr[i].getMethods();
      java.lang.reflect.Field[] farr=arr[i].getFields();

      int totalMethods=marr.length;
      int totalMembers=totalMethods+farr.length;
      for(int j=0;j<totalMembers;j++) {
        Member m = j<totalMethods?
          (Member)new Method(marr[j]):
          (Member)new Field(farr[j-totalMethods]);
        if ((m.getModifiers() & 0x0008)>0) { // static
          if ((stateless!=null) && rehash(hashedNames,m)) 
            stateless.put(m,Boolean.TRUE);
        } else { // not static
          if ((dynIDs!=null) && rehash(hashedNames,m))
            dynIDs.put(m,dynID);
        };
      };
    };
  };

  private static boolean rehash(Hashtable hashedNames, Member m) {
    String name=m.getName();
    String signature=ClassFile.getSignature(m);

    // for the purpose of matching fields behave like methods with no
    // arguments
    if (m instanceof Field) signature="()"+signature;
    
    Hashtable signatures=(Hashtable)hashedNames.get(name);
    if (signatures==null) { 
      // No method with this name was added
      Hashtable signatures_new=new Hashtable();
      signatures_new.put(signature,m);
      hashedNames.put(name,signatures_new);
      return true;
    };
    // Name exists in the library, check for possible signature conflict.
    Object conflicting_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 "+"\""+name+signature+
                    "\", conflicting :"+ conflicting_method+" and "+m+" .");
    };
    // If no debug then the method is ignored.
    return false;
  };

  /**
   * This method marks a static member 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 {
    Object m=getMember(name,params);
    Object removed=stateless.remove(m);
    if (Debug.enabled)
      Debug.assert(removed!=null,"State dependent methos \""+m+
		   "\"is made state dependend again.");
  };
  
  /**
   * Used to check if the given method is stateless.
   * @param m is method or field to check.
   * @return true if the method is stateless and can be invoked at
   *    compile time.
   */
  public boolean isStateless(Member o) {
    return stateless.containsKey(o);
  };
  
  /**
   * Used to search library for method or field.
   * <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/field object of the resolved method/field.
   * @exception NoSuchMethodException if the method can't be resolved
   */
  public Member getMember(String name,Class[] params) 
    throws NoSuchMethodException {
    return getMember(names,null,name,params);
  };

  /**
   * Finds the member of a given class by name.
   * <P> This method is used in compiling the dot ('.') operators.
   * It does all necessary security checks and wraps the result
   * into one of JEL's reflection classes.
   * @param container the class to search the method within.
   * @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/field object of the resolved method/field.
   * @exception NoSuchMethodException if the method can't be resolved
   */
  public Member getMember(Class container,String name,Class[] params) 
    throws NoSuchMethodException {
    // security
    if (dotClasses==null)
      throw new NoSuchMethodException("The access to class members is "+
                                      "not allowed.");
    else if (! (noDotSecurity || dotClasses.containsKey(container)))
      throw new NoSuchMethodException("The access to members of "+
                                      container.toString()+
                                      " is not allowed.");
    Hashtable hashedMembers=(Hashtable)dotClasses.get(container);
    if (hashedMembers==null) {
      rehashDotClass(container);
      hashedMembers=(Hashtable)dotClasses.get(container);
    };
      
    return getMember(hashedMembers,container,name,params);;
  };


  // This matches a method to a given invocation pattern,
  // unlike Class.getMethod(...) tries various widening conversions.
  private static Member getMember(Hashtable hashedMembers, Class cls,
                                  String name,Class[] params) 
    throws NoSuchMethodException {
    Hashtable signatures=(Hashtable) hashedMembers.get(name);

    if (signatures==null)
      throw new NoSuchMethodException("The name \""+name+
                                      "\" is not defined "+
                                      (cls==null?"":"in the "+cls)+
                                      " .");
    
    // Choose applicable methods
    Vector applicable_methods=new Vector();
    for(Enumeration e=signatures.elements();e.hasMoreElements();) {
      Member cm = (Member)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=TypesStack.isWidening(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"+
                                      (cls==null?"":" in the "+cls)+
                                      ", but parameters "+
                                      describe(name,params)+
                                      " can not be accepted by it.");
    };
    
    if (applicable_methods.size()==1) 
      return (Member)applicable_methods.firstElement();
    
    // Search for the most specific method
    Enumeration e=applicable_methods.elements();
    Member most_specific=(Member)e.nextElement();
    Class[] most_specific_params=most_specific.getParameterTypes();
    while (e.hasMoreElements()) {
      Member cm= (Member)e.nextElement();
      Class[] cp=cm.getParameterTypes();
      boolean moreSpecific=true;
      boolean lessSpecific=true;
      
      for(int i=0; i<cp.length; i++) {
        moreSpecific = moreSpecific && 
          TypesStack.isWidening(cp[i],most_specific_params[i]);
        lessSpecific = lessSpecific &&
          TypesStack.isWidening(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)+"\""+
                                        (cls==null?"":" on the "+cls)+" .");
    };
    return most_specific;
  };

  private static 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.
   */
  public int getDynamicMethodClassID(Member m) {
    Integer id=(Integer)dynIDs.get(m);
    if (id==null) return -1;
    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;
      Member mf=ll.getMember("round",par);
      if ((mf!=null) && 
          (mf.equals(new Method(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;
      Member mf=ll.getMember("round",par);
      if ((mf!=null) && 
          (mf.equals(new Method(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;
      Member mf=ll.getMember("round",par);

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


      if ((mf!=null) && 
          (mf.equals(new Method(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;
      Member mf=ll.getMember("abs",par);

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

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

    t.startTest("Attempt of access to the field PI");
    try {
      Class[] par=new Class[0];
      Member f=ll.getMember("PI",par);
      if ((f!=null) && (f.getName().equals("PI")))
	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.getMember("random",null)))
	t.testOK();
      else
	t.testFail();
    } catch (Throwable e) {
      Debug.reportThrowable(e);
      t.testFail();
    };

    t.startTest("Creating a library with dot operations allowed ");
    Library ldot=null;
    try {
      Class[] stl=new Class[1];
      Class[] dynl=new Class[1];
      Class[] dotl=new Class[2];
      stl[0]=Class.forName("java.lang.Math");
      dynl[0]=Class.forName("java.util.Random");
      dotl[0]=Class.forName("java.util.Hashtable");
      dotl[1]=Class.forName("java.util.Vector");
      
      ldot=new Library(stl,dynl,dotl);
      t.testOK();
    } catch (Throwable e) {
      Debug.reportThrowable(e);
      t.testFail();
    };

    t.startTest("Resolving size() on Hashtable");
    try {
      Class[] params=new Class[0];
      Class htable=Class.forName("java.util.Hashtable");
      Member mf=ldot.getMember(htable,"size",params);
      if ((mf!=null) && 
          (mf.equals(new Method(htable.getMethod("size",params)))))
	  t.testOK(); else t.testFail();
    } catch (Throwable e) {
      Debug.reportThrowable(e);
      t.testFail();
    };

    t.startTest("Resolving size() on Vector");
    try {
      Class[] params=new Class[0];
      Class vctr=Class.forName("java.util.Vector");
      Member mf=ldot.getMember(vctr,"size",params);
      if ((mf!=null) && 
          (mf.equals(new Method(vctr.getMethod("size",params)))))
	  t.testOK(); else t.testFail();
    } catch (Throwable e) {
      Debug.reportThrowable(e);
      t.testFail();
    };

    t.startTest("Ensuring size() is not leaked to global context.");
    try {
      Class[] params=new Class[0];
      try {
        Member mf=ldot.getMember("size",params);
        t.testFail();
      } catch (NoSuchMethodException exc) {
        t.testOK();
      };
    } catch (Throwable e) {
      Debug.reportThrowable(e);
      t.testFail();
    };

  };

};
