package org.muellerware.jsml;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Member;
import java.util.HashMap;
import java.util.Map;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.EcmaError;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.FunctionObject;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;

//-------------------------------------------------------------------
// Provides methods to load and execute a module
//-------------------------------------------------------------------
public class ModuleLoader {
	
	//---------------------------------------------------------------
	// map of filename -> module
	//---------------------------------------------------------------
	static private Map<String, Object> ModuleMap = new HashMap<String, Object>();

	//---------------------------------------------------------------
	// the scope which contains reusable functions
	//---------------------------------------------------------------
	static private ScriptableObject BaseModuleScope = null;

	//---------------------------------------------------------------
	// arguments of a JavaScript method implementation in Java
	//---------------------------------------------------------------
	@SuppressWarnings("unchecked")
	static private Class[] JS_FUNCTION_ARGUMENT_CLASSES = new Class[] {
		Context.class,
		Scriptable.class,
		Object[].class,
		Function.class
	};
	
	//---------------------------------------------------------------
	// throw an exception
	//---------------------------------------------------------------
	static private void error(String message) {
		throw Context.throwAsScriptRuntimeEx(new RuntimeException(message));
	}

	//---------------------------------------------------------------
	// register a function
	//---------------------------------------------------------------
	@SuppressWarnings("unchecked")
	static private void registerFunction(Scriptable scope, Class clazz, String function) {
		Member func = null;
		
		// get the method
		try {
			func = clazz.getMethod("js_" + function, JS_FUNCTION_ARGUMENT_CLASSES);
		} 
		catch (Exception e) {
			error("error loading function: " + clazz.getName() + "." + function + ": " + e);
		}
		
		// create the JS wrapper for it
		FunctionObject js_func = new FunctionObject("loadModule", func, scope);
		
		// add it to the scope
		ScriptableObject.putProperty(scope, function, js_func);
	}

	//---------------------------------------------------------------
	// create base module scope
	//---------------------------------------------------------------
	static public ScriptableObject getBaseModuleScope(Context context) {
		
		// if the base module scope has already been created, just return it
		if (null != BaseModuleScope) return BaseModuleScope;
		
		// create the base module scope
		BaseModuleScope = context.initStandardObjects();

		// add the loadModule() function to the scope
		registerFunction(BaseModuleScope, ModuleLoader.class, "loadModule");

		// add the print() function to the scope
		registerFunction(BaseModuleScope, ModuleLoader.class, "print");
		
		// make the scope immutable
		BaseModuleScope.sealObject();
		
		return BaseModuleScope;
	}
	
	//---------------------------------------------------------------
	// load a module
	//---------------------------------------------------------------
	static public Object load(Context context, String scriptFileName) {
		
		// get the script file set up
		File       sFile       = new File(scriptFileName);
		String     sFileName   = sFile.getAbsolutePath();
		FileReader sFileReader = null;
		
		try {
			sFileReader = new FileReader(sFileName);
		}
		catch (FileNotFoundException e) {
			error("File not found: '" + sFileName + "'");
		}
		
		// create the module object, see
		//    http://www.mozilla.org/rhino/scopes.html
		// for more details on sharing scopes
		Scriptable baseModuleScope = getBaseModuleScope(context);
		Scriptable moduleScope     = context.newObject(baseModuleScope);
		moduleScope.setPrototype(baseModuleScope);
		moduleScope.setParentScope(null);
	    
		// add the __FILE__ variable
		Object __FILE__ = context.newObject(moduleScope, "String", new Object[] { scriptFileName });
		ScriptableObject.putProperty(moduleScope, "__FILE__", __FILE__);
		
		// invoke the script
		try {
			context.evaluateReader(moduleScope, sFileReader, sFileName, 1, null);
		} 
		catch (IOException e) {
			error("Error running script: '" + sFileName + "': " + e);
			return null;
		}
		catch (EcmaError e) {
			String message = "Exception " + e.getName() + " in " +
				e.sourceName() + ":" + e.lineNumber() + ": " +
				e.getErrorMessage();
			System.err.println(message);
			System.err.println(e.getScriptStackTrace());
		}
		finally {
			try { sFileReader.close(); } catch(Exception e) {}
		}

		return moduleScope; 
	}
	
	//---------------------------------------------------------------
	// the implementation of the loadModule() function
	//---------------------------------------------------------------
	static public Object js_loadModule(
			Context    context, 
			Scriptable thisObj, 
			Object[]   args, 
			Function   function) {
		
		// script name is the first argument
		String scriptName = Context.toString(args[0]);
		
		// do we already have the module loaded?
		// if so, just return it
		Object module     = ModuleMap.get(scriptName);
		if (null != module) return module;
		
		// otherwise, load it
		module = load(context, scriptName);
		
		// save it in our loaded map
		ModuleMap.put(scriptName, module);
		
		// return it
		return module;
	}
	
	//---------------------------------------------------------------
	// the implementation of the print() function
	//---------------------------------------------------------------
	static public Object js_print(
			Context    context, 
			Scriptable thisObj, 
			Object[]   args, 
			Function   function) {
		
	    for (int i=0; i<args.length; i++) {
	        if (i > 0) System.out.print(" ");

	        System.out.print(Context.toString(args[i]));
	    }
	    
	    System.out.println();
	    
	    return Context.getUndefinedValue();
	}

}
