/*
 * 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.jython;

import java.beans.PropertyChangeEvent;
import java.io.ByteArrayInputStream;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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.python.core.Py;
import org.python.core.PyException;
import org.python.core.PyJavaInstance;
import org.python.core.PyObject;
import org.python.core.PySystemState;
import org.python.util.InteractiveInterpreter;

/**
 * This is the interface to Jython (http://www.jython.org/) from BSF.
 * It's derived from the JPython 1.x engine
 *
 * @author   Sanjiva Weerawarana
 * @author   Finn Bock <bckfnn@worldonline.dk>
 * @author   Chuck Murcko
 * @author   Sonny To" <son.c.to@gmail.com>, 2006-10-30
 */

public class JythonEngine extends BSFEngineImpl {
  BSFPythonInterpreter interp;
  private final static Pattern fromRegExp = Pattern.compile("from ([.^\\S]*)");
  
  /**
   * call the named method of the given object.
   */
  public Object call (Object object, String method, Object[] args) 
      throws BSFException {
      try {
          PyObject[] pyargs = Py.EmptyObjects;

          if (args != null) {
              pyargs = new PyObject[args.length];
              for (int i = 0; i < pyargs.length; i++)
                  pyargs[i] = Py.java2py(args[i]);
          }

          if (object != null) {
              PyObject o = Py.java2py(object);
              return unwrap(o.invoke(method, pyargs));
          }

          PyObject m = interp.get(method);

          if (m == null)
              m = interp.eval(method);
          if (m != null) {
              return unwrap(m.__call__(pyargs));
          }

          return null;
      } catch (PyException e) {
          throw new BSFException (BSFException.REASON_EXECUTION_ERROR,
                                  "exception from Jython:\n" + e, e);
      }
  }

  /**
   * Declare a bean
   */
  public void declareBean (BSFDeclaredBean bean) throws BSFException {
	interp.set (bean.name, bean.bean);
  }

  /**
   * Evaluate an anonymous function (differs from eval() in that apply() 
   * handles multiple lines).
   */
  public Object apply (String source, int lineNo, int columnNo, 
                       Object funcBody, Vector paramNames,
                       Vector arguments) throws BSFException {
      try {
          /* We wrapper the original script in a function definition, and
           * evaluate the function. A hack, no question, but it allows
           * apply() to pretend to work on Jython.
           */
          StringBuffer script = new StringBuffer(byteify(funcBody.toString()));
          int index = 0;
          script.insert(0, "def bsf_temp_fn():\n");
         
          while (index < script.length()) {
              if (script.charAt(index) == '\n') {
                  script.insert(index+1, '\t');
              }
              index++;
          }

          String scriptStr = script.toString ();
          importPackage(scriptStr);
          interp.exec (scriptStr);
          
          Object result = interp.eval ("bsf_temp_fn()");
          
          if (result != null && result instanceof PyJavaInstance)
              result = ((PyJavaInstance)result).__tojava__(Object.class);
          return result;
      } catch (PyException e) {
          throw new BSFException (BSFException.REASON_EXECUTION_ERROR,
                                  "exception from Jython:\n" + e, e);
      }
  }

  /**
   * Evaluate an expression.
   */
  public Object eval (String source, int lineNo, int columnNo, 
		      Object script) throws BSFException {
	try {
	  String scriptStr = byteify(script.toString ());
	  importPackage(scriptStr);
	  Object result = interp.eval (scriptStr);
	  if (result != null && result instanceof PyJavaInstance)
		result = ((PyJavaInstance)result).__tojava__(Object.class);
	  return result;
	} catch (PyException e) {
	  throw new BSFException (BSFException.REASON_EXECUTION_ERROR,
			      "exception from Jython:\n" + e, e);
	}
  }

  /**
   * Execute a script. 
   */
  public void exec (String source, int lineNo, int columnNo,
		    Object script) throws BSFException {
	try {
	  String scriptStr = byteify(script.toString());
	  importPackage(scriptStr);
	  interp.exec (scriptStr);
	} catch (PyException e) {
	  throw new BSFException (BSFException.REASON_EXECUTION_ERROR,
			      "exception from Jython:\n" + e, e);
	}
  }

  private void importPackage(String script) {
	Matcher matcher = fromRegExp.matcher(script);
	while (matcher.find()) {
		String packageName = matcher.group(1);
		PySystemState.add_package(packageName);
	}
  }

  /**
   * Execute script code, emulating console interaction.
   */
  public void iexec (String source, int lineNo, int columnNo,
                     Object script) throws BSFException {
      String scriptStr = byteify(script.toString());
      importPackage(scriptStr);
      int newline = scriptStr.indexOf("\n");

      if (newline > -1)
          scriptStr = scriptStr.substring(0, newline);

      try {
          if (interp.buffer.length() > 0)
              interp.buffer.append("\n");
          interp.buffer.append(scriptStr);
          if (!(interp.runsource(interp.buffer.toString())))
              interp.resetbuffer();
      } catch (PyException e) {
          interp.resetbuffer();
          throw new BSFException(BSFException.REASON_EXECUTION_ERROR, 
                                 "exception from Jython:\n" + e, e);
      }
  }

  /**
   * Initialize the engine.
   */
  public void initialize (BSFManager mgr, String lang,
						  Vector declaredBeans) throws BSFException {
	super.initialize (mgr, lang, declaredBeans);

	// create an interpreter
	interp = new BSFPythonInterpreter ();

    // ensure that output and error streams are re-directed correctly
    interp.setOut(System.out);
    interp.setErr(System.err);
    
	// register the mgr with object name "bsf"
	interp.set ("bsf", new BSFFunctions (mgr, this));

    // Declare all declared beans to the interpreter
	int size = declaredBeans.size ();
	for (int i = 0; i < size; i++) {
	  declareBean ((BSFDeclaredBean) declaredBeans.elementAt (i));
	}
  }

  /**
   * Undeclare a previously declared bean.
   */
  public void undeclareBean (BSFDeclaredBean bean) throws BSFException {
	interp.set (bean.name, null);
  }

  public Object unwrap(PyObject result) {
	if (result != null) {
	   Object ret = result.__tojava__(Object.class);
	   if (ret != Py.NoConversion)
		  return ret;
	}
	return result;
  }
  
  private String byteify (String orig) {
      // Ugh. Jython likes to be fed bytes, rather than the input string.
      ByteArrayInputStream bais = 
          new ByteArrayInputStream(orig.getBytes());
      StringBuffer s = new StringBuffer();
      int c;
      
      while ((c = bais.read()) >= 0) {
          s.append((char)c);
      }

      return s.toString();
  }
  
  private class BSFPythonInterpreter extends InteractiveInterpreter {

      public BSFPythonInterpreter() {
          super();
      }

      // Override runcode so as not to print the stack dump
      public void runcode(PyObject code) {
          try {
              this.exec(code);
          } catch (PyException exc) {
              throw exc;
          }
      }
  }


  public void propertyChange(PropertyChangeEvent e) {
	  super.propertyChange(e);
	  String name = e.getPropertyName();
      Object value = e.getNewValue();
      if (name.equals("classLoader")) {
		Py.getSystemState().setClassLoader((ClassLoader) value);
      }

  }
}
