/*
 * Copyright 2004,2004 The Apache Software Foundation.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.bsf.engines.netrexx;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Hashtable;
import java.util.Vector;

import org.apache.bsf.BSFDeclaredBean;
import org.apache.bsf.BSFException;
import org.apache.bsf.BSFManager;
import org.apache.bsf.util.BSFEngineImpl;
import org.apache.bsf.util.BSFFunctions;
import org.apache.bsf.util.EngineUtils;
import org.apache.bsf.util.MethodUtils;
import org.apache.bsf.util.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * This is the interface to NetRexx from the
 * Bean Scripting Framework.
 * <p>
 * The NetRexx code must be written script-style, without a "class" or
 * "properties" section preceeding the executable code. The NetRexxEngine will
 * generate a prefix for this code:
 * <pre>
 * <code>
 * class $$CLASSNAME$$;
 * method BSFNetRexxEngineEntry(bsf=org.apache.bsf.BSFManager) public static;
 * </code>
 * </pre>
 * $$CLASSNAME$$ will be replaced by a generated classname of the form
 * BSFNetRexx*, and the bsf parameter can be used to retrieve application
 * objects registered with the Bean Scripting Framework.
 * <p>
 * If you use the placeholder string $$CLASSNAME$$ elsewhere
 * in your script -- including within text strings -- BSFNetRexxEngine will
 * replace it with the generated name of the class before the NetRexx code
 * is compiled.
 * <p>
 * If you need to use full NetRexx functionality, we recommend that your
 * NetRexx script define and invoke a "minor class", with or without the
 * "dependent" keyword as suits your needs. You'll have to use $$CLASSNAME$$
 * in naming the minor class, since the name of the main class is synthesized;
 * for example, to create the minor class "bar" you'd write
 * "class $$CLASSNAME$$.Bar".
 * <p>
 * <h2>Hazards:</h2>
 * <p>
 * Since NetRexx has to be _compiled_ to a Java classfile, invoking it involves
 * a fair amount of computation to load and execute the compiler. We are
 * currently making an attempt to manage that by caching the class
 * after it has been loaded, but the indexing is fairly primitive; we
 * hash against the script string to find the class for it.
 * <p>
 * Minor-class .class files are now being deleted after the major class loads.
 * This coould potentially cause problems.
 *
 * @author  Joe Kesselman
 * @author  Sanjiva Weerawarana
 */
public class NetRexxEngine extends BSFEngineImpl
{
	BSFFunctions mgrfuncs;
	static Hashtable codeToClass=new Hashtable();
	static String serializeCompilation="";
	static String placeholder="$$CLASSNAME$$";
	String minorPrefix;
	
	private Log logger = LogFactory.getLog(this.getClass().getName());
	  
	/**
	 * Create a scratchfile, open it for writing, return its name.
	 * Relies on the filesystem to provide us with uniqueness testing.
	 * NOTE THAT uniqueFileOffset continues to count; we don't want to
	 * risk reusing a classname we have previously loaded in this session
	 * even if the classfile has been deleted.
	 *
	 * I've made the offset static, due to concerns about reuse/reentrancy
	 * of the NetRexx engine.
	 */
  private static int uniqueFileOffset=0;
  private class GeneratedFile 
  {
	File file=null;
	FileOutputStream fos=null;
	String className=null;
	GeneratedFile(File file,FileOutputStream fos,String className) 
	  {
		  this.file=file;
		  this.fos=fos;
		  this.className=className;
	  }
  }
	
	// rexxclass used to be an instance variable, on the theory that
	// each NetRexxEngine was an instance of a specific script.
	// BSF is currently reusing Engines, so caching the class
	// no longer makes sense.
	// Class rexxclass;
	
	/**
	 * Constructor.
	 */
	public NetRexxEngine ()
	{
		/*
		  The following line is intended to cause the constructor to
		  throw a NoClassDefFoundError if the NetRexxC.zip dependency
		  is not resolved.
		  
		  If this line was not here, the problem would not surface until
		  the actual processing of a script. We want to know all is well
		  at the time the engine is instantiated, not when we attempt to
		  process a script.
		  */
		
		new netrexx.lang.BadArgumentException();
	}
	/**
	 * Return an object from an extension.
	 * @param object object from which to call our static method
	 * @param method The name of the method to call.
	 * @param args an array of arguments to be
	 * passed to the extension, which may be either
	 * Vectors of Nodes, or Strings.
	 */
	public Object call (Object object, String method, Object[] args) 
	throws BSFException
	{
		throw new BSFException(BSFException.REASON_UNSUPPORTED_FEATURE,
							   "NetRexx doesn't currently support call()",
							   null);
	}
	/**
	 * Invoke a static method.
	 * @param rexxclass Class to invoke the method against
	 * @param method The name of the method to call.
	 * @param args an array of arguments to be
	 * passed to the extension, which may be either
	 * Vectors of Nodes, or Strings.
	 */
	Object callStatic(Class rexxclass, String method, Object[] args) 
	throws BSFException
	{
		//***** ISSUE: Currently supports only static methods
		Object retval = null;
		try
		{
			if (rexxclass != null)
			{
				//***** This should call the lookup used in BML, for typesafety
				Class[] argtypes=new Class[args.length];
				for(int i=0;i<args.length;++i)
					argtypes[i]=args[i].getClass();
				
				Method m=MethodUtils.getMethod(rexxclass, method, argtypes);
				retval=m.invoke(null,args);
			}
			else
			{
				logger.error("NetRexxEngine: ERROR: rexxclass==null!");
			}
		}
		catch(Exception e)
		{
			e.printStackTrace ();
			if (e instanceof InvocationTargetException)
			{
				Throwable t = ((InvocationTargetException)e).getTargetException ();
				t.printStackTrace ();
			}
			throw new BSFException (BSFException.REASON_IO_ERROR,
									e.getMessage (),
									e);
		}
		return retval;
	}
	public void declareBean (BSFDeclaredBean bean) throws BSFException {}
	/**
	 * Override impl of execute. In NetRexx, methods which do not wish
	 * to return a value should be invoked via exec, which will cause them
	 * to be generated without the "returns" clause.
	 * Those which wish to return a value should call eval instead.
	 * which will add "returns java.lang.Object" to the header.
	 *
	 * Note: It would be nice to have the "real" return type avaialable, so
	 * we could do something more type-safe than Object, and so we could
	 * return primitive types without having to enclose them in their
	 * object wrappers. BSF does not currently support that concept.
	 */
	public Object eval (String source, int lineNo, int columnNo,
					Object script)
	throws BSFException
	{
		return execEvalShared(source, lineNo, columnNo, script,true);
	}
	/**
	 * Override impl of execute. In NetRexx, methods which do not wish
	 * to return a value should be invoked via exec, which will cause them
	 * to be generated without the "returns" clause.
	 * Those which wish to return a value should call eval instead.
	 * which will add "returns java.lang.Object" to the header.
	 */
	public void exec (String source, int lineNo, int columnNo,
				  Object script)
	throws BSFException
	{
		 execEvalShared(source, lineNo, columnNo, script,false);
	}
	/**
	 * This is shared code for the exec() and eval() operations. It will
	 * evaluate a string containing a NetRexx method body -- which may be
	 * as simple as a single return statement.
	 * It should store the "bsf" handle where the
	 * script can get to it, for callback purposes.
	 * <p>
	 * Note that NetRexx compilation imposes serious overhead -- 11 seconds for
	 * the first compile, about 3 thereafter -- but in exchange you get
	 * Java-like speeds once the classes have been created (minus the cache
	 * lookup cost).
	 * <p>
	 * Nobody knows whether javac is threadsafe.
	 * I'm going to serialize access to the compilers to protect it.
	 */
	public Object execEvalShared (String source, int lineNo, int columnNo, 
							  Object oscript,boolean returnsObject)
	throws BSFException
	{
		Object retval=null;
		String classname=null;
		GeneratedFile gf=null;
		
		// Moved into the exec process; see comment above.
		Class rexxclass=null;
		
		String basescript=oscript.toString();
		String script=basescript; // May be altered by $$CLASSNAME$$ expansion
		
		try {
                    // Do we already have a class exactly matching this code?
                    rexxclass=(Class)codeToClass.get(basescript);
                    
                    if(rexxclass!=null)
                    	
			{
                            logger.debug("NetRexxEngine: Found pre-compiled class" +
                                                   " for script '" + basescript + "'");
                            classname=rexxclass.getName();
			}
                    else
			{
                            gf=openUniqueFile(tempDir,"BSFNetRexx",".nrx");
                            if(gf==null)
                                throw new BSFException("couldn't create NetRexx scratchfile");
                            
                            // Obtain classname
                            classname=gf.className;
                            
                            // Decide whether to declare a return type
                            String returnsDecl="";
                            if(returnsObject)
                                returnsDecl="returns java.lang.Object";
                            
                            // Write the kluge header to the file.
                            // ***** By doing so we give up the ability to use Property blocks.
                            gf.fos.write(("class "+classname+";\n")
                                         .getBytes());
                            gf.fos.write(
                                         ("method BSFNetRexxEngineEntry(bsf=org.apache.bsf.util.BSFFunctions) "+
                                          " public static "+returnsDecl+";\n")
								 .getBytes());
				
                            // Edit the script to replace placeholder with the generated
                            // classname. Note that this occurs _after_ the cache was
                            // checked!
                            int startpoint,endpoint;
                            if((startpoint=script.indexOf(placeholder))>=0)
				{
                                    StringBuffer changed=new StringBuffer();
                                    for(;
                                        startpoint>=0;
                                        startpoint=script.indexOf(placeholder,startpoint))
					{
                                            changed.setLength(0);   // Reset for 2nd pass or later
                                            if(startpoint>0)
                                                changed.append(script.substring(0,startpoint));
                                            changed.append(classname);
                                            endpoint=startpoint+placeholder.length();
                                            if(endpoint<script.length())
                                                changed.append(script.substring(endpoint));
                                            script=changed.toString();
					}
				}
                            
                            BSFDeclaredBean tempBean;
                            String          className;
                            
                            for (int i = 0; i < declaredBeans.size (); i++)
				{
                                    tempBean  = (BSFDeclaredBean) declaredBeans.elementAt (i);
                                    className = StringUtils.getClassName (tempBean.type);
                                    
                                    gf.fos.write ((tempBean.name + " =" + className + "   bsf.lookupBean(\"" +
                                                   tempBean.name + "\");").getBytes());
				}
                            
                            if(returnsObject)
                                gf.fos.write("return ".getBytes());
                            
                            // Copy the input to the file.
                            // Assumes all available -- probably mistake, but same as
                            // other engines.
                            gf.fos.write(script.getBytes());
                            gf.fos.close();
                            
                            logger.debug("NetRexxEngine: wrote temp file " + 
                                                   gf.file.getPath () + ", now compiling");
                            
                            // Compile through Java to .class file
                    String command=gf.file.getPath(); //classname;
                    if (logger.isDebugEnabled()) {  
                    	command += " -verbose4";
                    } else {
                        command += " -noverbose";
                        command += " -noconsole";
                    }
                    
                    netrexx.lang.Rexx cmdline= new netrexx.lang.Rexx(command);
                    int retValue;
                    
                    // May not be threadsafe. Serialize access on static object:
                    synchronized(serializeCompilation)
                        {
                            // compile to a .java file
                            retValue =
                                COM.ibm.netrexx.process.NetRexxC.main(cmdline,
                                                                      new PrintWriter(System.err)); 
                        }

				// Check if there were errors while compiling the Rexx code.
				if (retValue == 2)
				{
				  throw new BSFException(BSFException.REASON_EXECUTION_ERROR,
										 "There were NetRexx errors.");
				}

				// Load class.
                logger.debug("NetRexxEngine: loading class "+classname);
				rexxclass=EngineUtils.loadClass (mgr, classname);

				// Stash class for reuse
				codeToClass.put(basescript,rexxclass);
                        }

			Object[] args={mgrfuncs};
			retval=callStatic(rexxclass, "BSFNetRexxEngineEntry",args);
                }
                catch (BSFException e)
                    {
                        // Just forward the exception on.
                        throw e;
                    }
                catch(Exception e)
                    {
			e.printStackTrace ();
			if (e instanceof InvocationTargetException)
			{
				Throwable t = ((InvocationTargetException)e).getTargetException ();
				t.printStackTrace ();
			}
			throw new BSFException (BSFException.REASON_IO_ERROR,
									e.getMessage (), e);
		}
		finally
		{
			// Cleanup: delete the .nrx and .class files
			// (if any) generated by NetRexx Trace requests.
			
			if(gf!=null && gf.file!=null && gf.file.exists())
				gf.file.delete();  // .nrx file
			
			if(classname!=null)
			{
				// Generated src
				File file=new File(tempDir+File.separatorChar+classname+".java");
				if(file.exists())
					file.delete();
				
				// Generated class
				file=new File(classname+".class");
				if(file.exists())
					file.delete();
				
				// Can this be done without disrupting trace?
				file=new File(tempDir+File.separatorChar+classname+".crossref");
				if(file.exists())
					file.delete();
				
				// Search for and clean up minor classes, classname$xxx.class
				file=new File(tempDir);
				minorPrefix=classname+"$"; // Indirect arg to filter
				String[] minor_classfiles=
					file.list(
						// ANONYMOUS CLASS for filter:
						new FilenameFilter()
						{
							// Starts with classname$ and ends with .class
							public boolean accept(File dir,String name)
							{
								return
									(0==name.indexOf(minorPrefix))
									&&
									(name.lastIndexOf(".class")==name.length()-6)
									;
							}
						}
						);
				if(minor_classfiles!=null)
					for(int i=minor_classfiles.length;i>0;)
					{
						file=new File(minor_classfiles[--i]);
						file.delete();
					}
			}
		}
		
		return retval;
	}
	public void initialize(BSFManager mgr, String lang,Vector declaredBeans)
	throws BSFException
	{
		super.initialize(mgr, lang, declaredBeans);
		mgrfuncs = new BSFFunctions (mgr, this);
	}
private GeneratedFile openUniqueFile(String directory,String prefix,String suffix)
	{
		File file=null,obj=null;
		FileOutputStream fos=null;
		int max=1000;           // Don't try forever
		GeneratedFile gf=null;
		int i;
		String className = null;
		for(i=max,++uniqueFileOffset;
			fos==null && i>0;
			--i,++uniqueFileOffset)     
		{
			// Probably a timing hazard here... ***************
			try
				{
					className = prefix+uniqueFileOffset;
					file=new File(directory+File.separatorChar+className+suffix);
					obj=new File(directory+File.separatorChar+className+".class");
					if(file!=null && !file.exists() & obj!=null & !obj.exists())
						fos=new FileOutputStream(file);
				}
			catch(Exception e)
				{
					// File could not be opened for write, or Security Exception
					// was thrown. If someone else created the file before we could
					// open it, that's probably a threading conflict and we don't
					// bother reporting it.
					if(!file.exists())
					{
						logger.error("openUniqueFile: unexpected "+e);
					}
				}
		}
		if(fos==null)
			logger.error("openUniqueFile: Failed "+max+"attempts.");
		else
			gf=new GeneratedFile(file,fos,className);
		return gf;
	}

	public void undeclareBean (BSFDeclaredBean bean) throws BSFException {}
}
