package org.perl.inline.java ;

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


public class InlineJavaPerlNatives extends InlineJavaPerlCaller {
	static private boolean inited = false ;
	static private Map<Class, Class> registered_classes = Collections.synchronizedMap(new HashMap<Class, Class>()) ;
	static private Map<String, String> registered_methods = Collections.synchronizedMap(new HashMap<String, String>()) ;


	protected InlineJavaPerlNatives() throws InlineJavaException {
		init() ;
		RegisterPerlNatives(this.getClass()) ;
	}


	static protected void init() throws InlineJavaException {
		init("install") ;
	}


	synchronized static protected void init(String mode) throws InlineJavaException {
		InlineJavaPerlCaller.init() ;
		if (! inited){
			try {
				String perlnatives_so = GetBundle().getString("inline_java_perlnatives_so_" + mode) ;
				File f = new File(perlnatives_so) ;
				if (! f.exists()){
					throw new InlineJavaException("Can't initialize PerlNatives " +
						"functionnality: PerlNatives extension (" + perlnatives_so + 
						") can't be found") ;
				}

				try {
					Class ste_class = Class.forName("java.lang.StackTraceElement") ;
				}
				catch (ClassNotFoundException cnfe){
					throw new InlineJavaException("Can't initialize PerlNatives " +
                        "functionnality: Java 1.4 or higher required (current is " +
						System.getProperty("java.version") + ").") ;
				}      	

				// Load the Natives shared object
				InlineJavaUtils.debug(2, "loading shared library " + perlnatives_so) ;
				System.load(perlnatives_so) ;

				inited = true ;
			}                                   
			catch (MissingResourceException mre){
				throw new InlineJavaException("Error loading InlineJava.properties resource: " + mre.getMessage()) ;
			}
		}
	}


	// This method actually does the real work of registering the methods.
	synchronized private void RegisterPerlNatives(Class c) throws InlineJavaException {
		if (registered_classes.get(c) == null){
			InlineJavaUtils.debug(3, "registering natives for class " + c.getName()) ;

			Constructor constructors[] = c.getDeclaredConstructors() ;
			Method methods[] = c.getDeclaredMethods() ;

			registered_classes.put(c, c) ;
			for (int i = 0 ; i < constructors.length ; i++){
				Constructor x = constructors[i] ;
				if (Modifier.isNative(x.getModifiers())){
					RegisterMethod(c, "new", x.getParameterTypes(), c) ;
				}
			}

			for (int i = 0 ; i < methods.length ; i++){
				Method x = methods[i] ;
				if (Modifier.isNative(x.getModifiers())){
					RegisterMethod(c, x.getName(), x.getParameterTypes(), x.getReturnType()) ;
				}
			}
		}
	}


	private void RegisterMethod(Class c, String mname, Class params[], Class rt) throws InlineJavaException {
		String cname = c.getName() ;
		InlineJavaUtils.debug(3, "registering native method " + mname + " for class " + cname) ;

		// Check return type
		if ((! Object.class.isAssignableFrom(rt))&&(rt != void.class)){
			throw new InlineJavaException("Perl native method " + mname + " of class " + cname + " can only have Object or void return types (not " + rt.getName() + ")") ;
		}

		// fmt starts with the return type, which for now is Object only (or void).
		StringBuffer fmt = new StringBuffer("L") ;
		StringBuffer sign = new StringBuffer("(") ;
		for (int i = 0 ; i < params.length ; i++){
			String code = InlineJavaClass.FindJNICode(params[i]) ;
			sign.append(code) ;
			char ch = code.charAt(0) ;
			char f = ch ;
			if (f == '['){
				// Arrays are Objects...
				f = 'L' ;
			}
			fmt.append(new String(new char [] {f})) ;
		}
		sign.append(")") ;

		sign.append(InlineJavaClass.FindJNICode(rt)) ;
		InlineJavaUtils.debug(3, "signature is " + sign) ;
		InlineJavaUtils.debug(3, "format is " + fmt) ;

		// For now, no method overloading so no signature necessary
		String meth = cname + "." + mname ;
		String prev = (String)registered_methods.get(meth) ;
		if (prev != null){
			throw new InlineJavaException("There already is a native method '" + mname + "' registered for class '" + cname + "'") ;
		}
		registered_methods.put(meth, fmt.toString()) ;

		// call the native method to hook it up
		RegisterMethod(c, mname, sign.toString()) ;
	}


	// This native method will call RegisterNative to hook up the magic
	// method implementation for the method.
	native private void RegisterMethod(Class c, String name, String signature) throws InlineJavaException ;


	// This method will be called from the native side. We need to figure
	// out who this method is and then look in up in the
	// registered method list and return the format.
	private String LookupMethod() throws InlineJavaException {
		InlineJavaUtils.debug(3, "entering LookupMethod") ;

		String caller[] = GetNativeCaller() ;
		String meth = caller[0] + "." + caller[1]  ;

		String fmt = (String)registered_methods.get(meth) ;
		if (fmt == null){
			throw new InlineJavaException("Native method " + meth + " is not registered") ;
		}

		InlineJavaUtils.debug(3, "exiting LookupMethod") ;

		return fmt ;
	}


	private Object InvokePerlMethod(Object args[]) throws InlineJavaException, InlineJavaPerlException {
		InlineJavaUtils.debug(3, "entering InvokePerlMethod") ;

		String caller[] = GetNativeCaller() ;
		String pkg = caller[0] ;
		String method = caller[1] ;

		// Transform the Java class name into the Perl package name
		StringTokenizer st = new StringTokenizer(pkg, ".") ;
		StringBuffer perl_sub = new StringBuffer() ;
		// Starting with "::" means that the package is relative to the caller package
		while (st.hasMoreTokens()){
			perl_sub.append("::" + st.nextToken()) ;
		}
		perl_sub.append("::" + method) ;

		for (int i = 0 ; i < args.length ; i++){
			InlineJavaUtils.debug(3, "InvokePerlMethod argument " + i + " = " + args[i]) ;
		}

		Object ret = CallPerlSub(perl_sub.toString(), args) ;

		InlineJavaUtils.debug(3, "exiting InvokePerlMethod") ;

		return ret ;
	}


	// This method must absolutely be called by a method DIRECTLY called
	// by generic_perl_native
	private String[] GetNativeCaller() throws InlineJavaException {
		InlineJavaUtils.debug(3, "entering GetNativeCaller") ;

		Class ste_class = null ;
		try {
			ste_class = Class.forName("java.lang.StackTraceElement") ;
		}
		catch (ClassNotFoundException cnfe){
			throw new InlineJavaException("Can't load class java.lang.StackTraceElement") ;
		}      	

		Throwable exec_point = new Throwable() ;
		try {
			Method m = exec_point.getClass().getMethod("getStackTrace", new Class [] {}) ;
			Object stack = m.invoke(exec_point, new Object [] {}) ;
			if (Array.getLength(stack) <= 2){
				throw new InlineJavaException("Improper use of InlineJavaPerlNatives.GetNativeCaller (call stack too short)") ;
			}

			Object ste = Array.get(stack, 2) ;
			m = ste.getClass().getMethod("isNativeMethod", new Class [] {}) ;
			Boolean is_nm = (Boolean)m.invoke(ste, new Object [] {}) ;
			if (! is_nm.booleanValue()){
				throw new InlineJavaException("Improper use of InlineJavaPerlNatives.GetNativeCaller (caller is not native)") ;
			}

			m = ste.getClass().getMethod("getClassName", new Class [] {}) ;
			String cname = (String)m.invoke(ste, new Object [] {}) ;
			m = ste.getClass().getMethod("getMethodName", new Class [] {}) ;
			String mname = (String)m.invoke(ste, new Object [] {}) ;

			InlineJavaUtils.debug(3, "exiting GetNativeCaller") ;

			return new String [] {cname, mname} ;
		}
		catch (NoSuchMethodException nsme){
			throw new InlineJavaException("Error manipulating java.lang.StackTraceElement classes: " +
				nsme.getMessage()) ;
		}
		catch (IllegalAccessException iae){
			throw new InlineJavaException("Error manipulating java.lang.StackTraceElement classes: " +
				iae.getMessage()) ;
		}
		catch (InvocationTargetException ite){
			// None of the methods invoked throw exceptions, so...
			throw new InlineJavaException("Exception caught while manipulating java.lang.StackTraceElement classes: " +
				ite.getTargetException()) ;
		}
	}
}
