/*
 * BeanShell.java - BeanShell scripting support
 * :tabSize=4:indentSize=4:noTabs=false:
 * :folding=explicit:collapseFolds=1:
 *
 * Copyright (C) 2000, 2004 Slava Pestov
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package org.gjt.sp.jedit;

//{{{ Imports
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;

import org.gjt.sp.jedit.bsh.BshMethod;
import org.gjt.sp.jedit.bsh.EvalError;
import org.gjt.sp.jedit.bsh.Interpreter;
import org.gjt.sp.jedit.bsh.NameSpace;
import org.gjt.sp.jedit.bsh.UtilEvalError;
import org.gjt.sp.jedit.gui.BeanShellErrorDialog;
import org.gjt.sp.jedit.io.VFSManager;
import org.gjt.sp.jedit.textarea.JEditTextArea;
import org.gjt.sp.jedit.textarea.Selection;
import org.gjt.sp.util.Log;
import org.gjt.sp.util.TaskManager;
//}}}

/**
 * BeanShell is jEdit's extension language.<p>
 *
 * When run from jEdit, BeanShell code has access to the following predefined
 * variables:
 *
 * <ul>
 * <li><code>view</code> - the currently active {@link View}.</li>
 * <li><code>editPane</code> - the currently active {@link EditPane}.</li>
 * <li><code>textArea</code> - the edit pane's {@link JEditTextArea}.</li>
 * <li><code>buffer</code> - the edit pane's {@link Buffer}.</li>
 * <li><code>wm</code> - the view's {@link
 * org.gjt.sp.jedit.gui.DockableWindowManager}.</li>
 * <li><code>scriptPath</code> - the path name of the currently executing
 * BeanShell script.</li>
 * </ul>
 *
 * @author Slava Pestov
 * @version $Id: BeanShell.java 25350 2020-07-02 06:54:09Z kpouer $
 */
public class BeanShell
{
	private static final BeanShellFacade<View> bsh = new MyBeanShellFacade();

	static void init()
	{
		Log.log(Log.MESSAGE, BeanShell.class, "Beanshell Init");
	}

	//{{{ evalSelection() method
	/**
	 * Evaluates the text selected in the specified text area.
	 * @param view The view
	 * @param textArea The text area
	 * @since jEdit 2.7pre2
	 */
	public static void evalSelection(View view, JEditTextArea textArea)
	{
		bsh.evalSelection(view, textArea);
	} //}}}

	//{{{ showEvaluateDialog() method
	/**
	 * Prompts for a BeanShell expression to evaluate.
	 * @param view the View
	 * @since jEdit 2.7pre2
	 */
	public static void showEvaluateDialog(View view)
	{
		String command = GUIUtilities.input(view,"beanshell-eval-input",null);
		if(command != null)
		{
			if(!command.endsWith(";"))
				command += ";";

			int repeat = view.getInputHandler().getRepeatCount();

			if(view.getMacroRecorder() != null)
			{
				view.getMacroRecorder().record(repeat,command);
			}

			Object returnValue = null;
			try
			{
				for(int i = 0; i < repeat; i++)
				{
					returnValue = bsh._eval(view,bsh.getNameSpace(),command);
				}
			}
			catch(Throwable e)
			{
				Log.log(Log.ERROR,BeanShell.class,e);

				bsh.handleException(view,null,e);
			}

			if(returnValue != null)
			{
				String[] args = { returnValue.toString() };
				GUIUtilities.message(view,"beanshell-eval",args);
			}
		}
	} //}}}

	//{{{ showEvaluateLinesDialog() method
	/**
	 * Evaluates the specified script for each selected line.
	 * @param view The view
	 * @since jEdit 4.0pre1
	 */
	public static void showEvaluateLinesDialog(View view)
	{
		String command = GUIUtilities.input(view,"beanshell-eval-line",null);

		JEditTextArea textArea = view.getTextArea();
		Buffer buffer = view.getBuffer();

		if(command == null || command.isEmpty())
			return;

		Selection[] selection = textArea.getSelection();
		if(selection.length == 0)
		{
			javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null); 
			return;
		}

		if(!command.endsWith(";"))
			command += ";";

		String script = "int[] lines = textArea.getSelectedLines();\n"
			+ "for(int i = 0; i < lines.length; i++)\n"
			+ "{\n"
				+ "line = lines[i];\n"
				+ "index = line - lines[0];\n"
				+ "start = buffer.getLineStartOffset(line);\n"
				+ "end = buffer.getLineEndOffset(line);\n"
				+ "text = buffer.getText(start,end - start - 1);\n"
				+ "newText = " + command + '\n'
				+ "if(newText != null)\n"
				+ "{\n"
					+ "buffer.remove(start,end - start - 1);\n"
					+ "buffer.insert(start,String.valueOf(newText));\n"
				+ "}\n"
			+ "}\n";

		if(view.getMacroRecorder() != null)
			view.getMacroRecorder().record(1,script);

		try
		{
			buffer.beginCompoundEdit();

			bsh.eval(view,script);
		}
		finally
		{
			buffer.endCompoundEdit();
		}

		textArea.selectNone();
	} //}}}

	//{{{ runScript() method
	/**
	 * Runs a BeanShell script. Errors are shown in a dialog box.<p>
	 *
	 * If the <code>in</code> parameter is non-null, the script is
	 * read from that stream; otherwise it is read from the file identified
	 * by <code>path</code>.<p>
	 *
	 * The <code>scriptPath</code> BeanShell variable is set to the path
	 * name of the script.
	 *
	 * @param view The view. Within the script, references to
	 * <code>buffer</code>, <code>textArea</code> and <code>editPane</code>
	 * are determined with reference to this parameter.
	 * @param path The script file's VFS path.
	 * @param in The reader to read the script from, or <code>null</code>.
	 * @param ownNamespace If set to <code>false</code>, methods and
	 * variables defined in the script will be available to all future
	 * uses of BeanShell; if set to <code>true</code>, they will be lost as
	 * soon as the script finishes executing. jEdit uses a value of
	 * <code>false</code> when running startup scripts, and a value of
	 * <code>true</code> when running all other macros.
	 *
	 * @since jEdit 4.0pre7
	 */
	public static void runScript(View view, String path, Reader in,
		boolean ownNamespace)
	{
		try
		{
			_runScript(view,path,in,ownNamespace);
		}
		catch(Throwable e)
		{
			Log.log(Log.ERROR,BeanShell.class,e);

			bsh.handleException(view,path,e);
		}
	} //}}}

	//{{{ runScript() method
	/**
	 * Runs a BeanShell script. Errors are shown in a dialog box.<p>
	 *
	 * If the <code>in</code> parameter is non-null, the script is
	 * read from that stream; otherwise it is read from the file identified
	 * by <code>path</code>.<p>
	 *
	 * The <code>scriptPath</code> BeanShell variable is set to the path
	 * name of the script.
	 *
	 * @param view The view. Within the script, references to
	 * <code>buffer</code>, <code>textArea</code> and <code>editPane</code>
	 * are determined with reference to this parameter.
	 * @param path The script file's VFS path.
	 * @param in The reader to read the script from, or <code>null</code>.
	 * @param namespace The namespace to run the script in.
	 *
	 * @since jEdit 4.2pre5
	 */
	public static void runScript(View view, String path, Reader in,
		NameSpace namespace)
	{
		try
		{
			_runScript(view,path,in,namespace);
		}
		catch(Throwable e)
		{
			Log.log(Log.ERROR,BeanShell.class,e);

			bsh.handleException(view,path,e);
		}
	} //}}}

	//{{{ _runScript() method
	/**
	 * Runs a BeanShell script. Errors are passed to the caller.<p>
	 *
	 * If the <code>in</code> parameter is non-null, the script is
	 * read from that stream; otherwise it is read from the file identified
	 * by <code>path</code>.<p>
	 *
	 * The <code>scriptPath</code> BeanShell variable is set to the path
	 * name of the script.
	 *
	 * @param view The view. Within the script, references to
	 * <code>buffer</code>, <code>textArea</code> and <code>editPane</code>
	 * are determined with reference to this parameter.
	 * @param path The script file's VFS path.
	 * @param in The reader to read the script from, or <code>null</code>.
	 * @param ownNamespace If set to <code>false</code>, methods and
	 * variables defined in the script will be available to all future
	 * uses of BeanShell; if set to <code>true</code>, they will be lost as
	 * soon as the script finishes executing. jEdit uses a value of
	 * <code>false</code> when running startup scripts, and a value of
	 * <code>true</code> when running all other macros.
	 * @exception Exception instances are thrown when various BeanShell errors
	 * occur
	 * @since jEdit 4.0pre7
	 */
	public static void _runScript(View view, String path, Reader in,
		boolean ownNamespace) throws Exception
	{
		_runScript(view,path,in,ownNamespace
			? new NameSpace(bsh.getNameSpace(),"namespace")
			: bsh.getNameSpace());
	} //}}}

	//{{{ _runScript() method
	/**
	 * Runs a BeanShell script. Errors are passed to the caller.<p>
	 *
	 * If the <code>in</code> parameter is non-null, the script is
	 * read from that stream; otherwise it is read from the file identified
	 * by <code>path</code>.<p>
	 *
	 * The <code>scriptPath</code> BeanShell variable is set to the path
	 * name of the script.
	 *
	 * @param view The view. Within the script, references to
	 * <code>buffer</code>, <code>textArea</code> and <code>editPane</code>
	 * are determined with reference to this parameter.
	 * @param path The script file's VFS path.
	 * @param in The reader to read the script from, or <code>null</code>.
	 * @param namespace The namespace to run the script in.
	 * @exception Exception instances are thrown when various BeanShell errors
	 * occur
	 * @since jEdit 4.2pre5
	 */
	public static void _runScript(View view, String path, Reader in,
		NameSpace namespace) throws Exception
	{
		Log.log(Log.MESSAGE,BeanShell.class,"Running script " + path);

		Interpreter interp = BeanShellFacade.createInterpreter(namespace);

		try
		{
			if(in == null)
			{
				Buffer buffer = jEdit.openTemporary(null,
					null,path,false);

				if(!buffer.isLoaded())
					TaskManager.instance.waitForIoTasks();

				in = new StringReader(buffer.getText(0,
					buffer.getLength()));
			}

			bsh.setupDefaultVariables(namespace,view);
			interp.set("scriptPath",path);

			running = true;

			interp.eval(in,namespace,path);
		}
		catch(Exception e)
		{
			BeanShellFacade.unwrapException(e);
		}
		finally
		{
			running = false;
			try
			{
				// no need to do this for macros!
				if(namespace == bsh.getNameSpace())
				{
					bsh.resetDefaultVariables(namespace);
					interp.unset("scriptPath");
				}
			}
			catch(EvalError e)
			{
				// do nothing
			}
		}
	} //}}}

	//{{{ eval() method
	/**
	 * Evaluates the specified BeanShell expression. Errors are reported in
	 * a dialog box.
	 * @param view The view. Within the script, references to
	 * <code>buffer</code>, <code>textArea</code> and <code>editPane</code>
	 * are determined with reference to this parameter.
	 * @param namespace The namespace
	 * @param command The expression
	 * @return an object
	 * @since jEdit 4.0pre8
	 */
	public static Object eval(View view, NameSpace namespace, String command)
	{
		return bsh.eval(view, namespace, command);
	} //}}}

	//{{{ _eval() method
	/**
	 * Evaluates the specified BeanShell expression. Unlike
	 * <code>eval()</code>, this method passes any exceptions to the caller.
	 *
	 * @param view The view. Within the script, references to
	 * <code>buffer</code>, <code>textArea</code> and <code>editPane</code>
	 * are determined with reference to this parameter.
	 * @param namespace The namespace
	 * @param command The expression
	 * @return the object
	 * @exception Exception instances are thrown when various BeanShell
	 * errors occur
	 * @since jEdit 3.2pre7
	 */
	public static Object _eval(View view, NameSpace namespace, String command)
		throws Exception
	{
		return bsh._eval(view, namespace, command);
	} //}}}

	//{{{ cacheBlock() method
	/**
	 * Caches a block of code, returning a handle that can be passed to
	 * runCachedBlock().
	 * @param id An identifier. If null, a unique identifier is generated
	 * @param code The code
	 * @param namespace If true, the namespace will be set
	 * @return a bsh method
	 * @exception Exception instances are thrown when various BeanShell errors
	 * occur
	 * @since jEdit 4.1pre1
	 */
	public static BshMethod cacheBlock(String id, String code, boolean namespace)
		throws Exception
	{
		return bsh.cacheBlock(id, code, namespace);
	} //}}}

	//{{{ runCachedBlock() method
	/**
	 * Runs a cached block of code in the specified namespace. Faster than
	 * evaluating the block each time.
	 * @param method The method instance returned by cacheBlock()
	 * @param view The view
	 * @param namespace The namespace to run the code in
	 * @return an object
	 * @exception Exception instances are thrown when various BeanShell
	 * errors occur
	 * @since jEdit 4.1pre1
	 */
	public static Object runCachedBlock(BshMethod method, View view,
		NameSpace namespace) throws Exception
	{
		return bsh.runCachedBlock(method, view, namespace);
	} //}}}

	//{{{ isScriptRunning() method
	/**
	 * @return if a BeanShell script or macro is currently running.
	 * @since jEdit 2.7pre2
	 */
	public static boolean isScriptRunning()
	{
		return running;
	} //}}}

	//{{{ getNameSpace() method
	/**
	 * @return the global namespace.
	 * @since jEdit 3.2pre5
	 */
	public static NameSpace getNameSpace()
	{
		return bsh.getNameSpace();
	} //}}}

	//{{{ Package-private members

	//{{{ resetClassManager() method
	/**
	 * Causes BeanShell internal structures to drop references to cached
	 * Class instances.
	 */
	static void resetClassManager()
	{
		bsh.resetClassManager();
	} //}}}

	//}}}

	//{{{ Private members

	//{{{ Static variables
	private static boolean running;
	//}}}

	//}}}

	/**
	 * The BeanshellFacade that is used by jEdit.
	 */
	private static class MyBeanShellFacade extends BeanShellFacade<View>
	{
		private MyBeanShellFacade()
		{
			classManager.setClassLoader(new JARClassLoader());
		}

		@Override
		protected void init()
		{
			super.init();
			global.importPackage("org.gjt.sp.jedit.browser");
			global.importPackage("org.gjt.sp.jedit.bufferset");
			global.importPackage("org.gjt.sp.jedit.statusbar");
			global.importPackage("org.gjt.sp.jedit.gui");
			global.importPackage("org.gjt.sp.jedit.help");
			global.importPackage("org.gjt.sp.jedit.io");
			global.importPackage("org.gjt.sp.jedit.menu");
			global.importPackage("org.gjt.sp.jedit.msg");
			global.importPackage("org.gjt.sp.jedit.options");
			global.importPackage("org.gjt.sp.jedit.pluginmgr");
			global.importPackage("org.gjt.sp.jedit.print");
			global.importPackage("org.gjt.sp.jedit.search");
			global.importPackage("org.jedit.io");
		}

		@Override
		protected void setupDefaultVariables(NameSpace namespace, View view) throws UtilEvalError
		{
			if(view != null)
			{
				EditPane editPane = view.getEditPane();
				setVariable(namespace, "view", view);
				setVariable(namespace, "editPane",editPane);
				setVariable(namespace, "buffer",editPane.getBuffer());
				setVariable(namespace, "textArea",editPane.getTextArea());
				setVariable(namespace, "wm",view.getDockableWindowManager());
			}
		}

		@Override
		protected void resetDefaultVariables(NameSpace namespace) throws UtilEvalError
		{
			namespace.setVariable("view",null, false);
			namespace.setVariable("editPane",null, false);
			namespace.setVariable("buffer",null, false);
			namespace.setVariable("textArea",null, false);
			namespace.setVariable("wm",null, false);
		}

		@Override
		protected void handleException(View view, String path, Throwable t)
		{
			if(t instanceof IOException)
			{
				VFSManager.error(view,path,"ioerror.read-error",
					new String[] { t.toString() });
			}
			else
				new BeanShellErrorDialog(view,t);
		}

	}
}
