//----------------------------BEGIN LICENSE----------------------------
/* 
 * Willow : the Open Source WorkFlow Project
 * Distributable under GNU LGPL license by gun.org
 *
 * Copyright (C) 2004-2010 huihoo.org 
 * Copyright (C) 2004-2010  ZosaTapo <dertyang@hotmail.com> 
 * 
 * ====================================================================
 * Project Homepage : http://www.huihoo.org/willow
 * Source Forge     : http://sourceforge.net/projects/huihoo
 * Mailing list     : willow@lists.sourceforge.net 
 */
//----------------------------END  LICENSE-----------------------------
package org.huihoo.workflow.script;

import java.io.File;
import java.io.FileNotFoundException;
import java.net.URL;
import java.net.URLClassLoader;

import org.huihoo.workflow.WorkflowException;
import org.huihoo.workflow.script.compiler.Compiler;
import org.huihoo.workflow.script.compiler.JavaCompiler;
import org.huihoo.workflow.script.compiler.ScriptCompiler;
import org.huihoo.workflow.script.compiler.ScriptReader;
import org.huihoo.workflow.script.compiler.ScriptRuntimeContext;
import org.huihoo.workflow.script.compiler.ScriptWriter;
import org.huihoo.workflow.script.compiler.SunJavaCompiler;
import org.huihoo.workflow.script.selector.ScriptLoader;
import org.huihoo.workflow.script.selector.ScriptSelectorWrapper;
import org.huihoo.workflow.selector.SelectorException;
import org.huihoo.workflow.server.WorkflowContext;
import org.huihoo.workflow.util.StringManager;
import org.huihoo.workflow.xpdl.WorkflowCondition;
import org.huihoo.workflow.xpdl.WorkflowPackage;
import org.huihoo.workflow.xpdl.WorkflowProcess;
import org.huihoo.workflow.xpdl.WorkflowTransition;

public class ScriptCompilationContext
{
	protected static StringManager sm = StringManager.getManager(Constants.RESOURCE_BUNDLE_BASE);

	private String className;

	private String basePackageName;
	private String derivedPackageName;

	private String scriptJavaFileName;
	private String classFileName;
	private ScriptWriter writer;
	private ScriptReader reader;

	private Options options;
	private ScriptSelectorWrapper ssw;
	private Compiler scriptCompiler;
	private String classPath;

	private String outputDir;
	private WorkflowContext workflowContext;
	private URLClassLoader loader;

	private ScriptRuntimeContext rctxt;

	private int removed = 0;

	private URLClassLoader selectorClassLoader;
	private URL baseUrl;
	private Class selectorClass;

	private WorkflowTransition workflowTransition;
	private WorkflowCondition workflowCondition;

	public ScriptCompilationContext(
	WorkflowTransition workflowTransition,
	WorkflowCondition workflowCondition,
		Options options,
		WorkflowContext workflowContext,
		ScriptSelectorWrapper ssw,
		ScriptRuntimeContext rctxt)
	{

		this.workflowTransition = workflowTransition;
		this.workflowCondition = workflowCondition;
		this.options = options;
		this.workflowContext = workflowContext;

		this.ssw = ssw;
		this.rctxt = rctxt;
		this.basePackageName = Constants.SCRIPT_PACKAGE_NAME;
	}

	/**
	 * The classpath that is passed off to the Java compiler. 
	 */
	public String getClassPath()
	{
		if (classPath != null)
		{
			return classPath;
		}
		return rctxt.getClassPath();
	}

	/**
	 * The classpath that is passed off to the Java compiler. 
	 */
	public void setClassPath(String classPath)
	{
		this.classPath = classPath;
	}

	/**
	 * What class loader to use for loading classes while compiling
	 * this script?
	 */
	public ClassLoader getClassLoader()
	{
		if (loader != null)
		{
			return loader;
		}
		return rctxt.getParentClassLoader();
	}

	public void setClassLoader(URLClassLoader loader)
	{
		this.loader = loader;
	}

	/** ---------- Input/Output  ---------- */

	/**
	 * The output directory to generate code into.  The output directory
	 * is make up of the scratch directory, which is provide in Options,
	 * plus the directory derived from the package name.
	 */
	public String getOutputDir()
	{
		if (outputDir == null)
		{
			createOutputDir();
		}

		return outputDir;
	}

	/**
	 * Create a "Compiler" object based on some init param data. This
	 * is not done yet. Right now we're just hardcoding the actual
	 * compilers that are created. 
	 */
	public Compiler createCompiler() throws SelectorException
	{
		if (scriptCompiler != null)
		{
			return scriptCompiler;
		}

		JavaCompiler javac = null;
		String javaCompilerClass = options.getJavaCompiler();
		if (javaCompilerClass != null)
		{
			ClassLoader loader = Thread.currentThread().getContextClassLoader();
			try
			{
				javac = (JavaCompiler) loader.loadClass(javaCompilerClass).newInstance();
			}
			catch (Throwable throwable)
			{
				throwable.printStackTrace();
			}
		}

		if (javac == null)
		{
			javac = new SunJavaCompiler();
		}

		scriptCompiler = new ScriptCompiler(this);
		scriptCompiler.setJavaCompiler(javac);

		return scriptCompiler;
	}

	public Compiler getCompiler()
	{
		return scriptCompiler;
	}

	/** ---------- Access resources in the workflowapp ---------- */

	/**
	 * Gets a resource as a stream, relative to the meanings of this
	 * workflowContext's implementation.
	 * @return a null if the resource cannot be found or represented 
	 *         as an InputStream.
	 */
	public java.io.InputStream getResourceAsStream(String res)
	{
		return workflowContext.getResourceAsStream(canonicalURI(res));
	}

	/** 
	 * Gets the actual path of a URI relative to the workflowContext of
	 * the compilation.
	 */
	public String getRealPath(String path)
	{
		if (workflowContext != null)
		{
			return workflowContext.getRealPath(path);
		}
		return path;
	}

	/* ==================== Common implementation ==================== */

	/**
	 * Just the class name (does not include package name) of the
	 * generated class. 
	 */
	public String getScriptClassName()
	{

		if (className != null)
		{
			return className;
		}

		className = formatJavaID("_script_" + workflowTransition.getUUID());

		return className;
	}

	public void setScriptClassName(String className)
	{
		this.className = formatJavaID(className);
	}

	public WorkflowCondition getWorkflowCondition()
	{
		return workflowCondition;
	}
	/**
	 * Package name for the generated class is make up of the base package
	 * name, which is user settable, and the derived package name.  The
	 * derived package name directly mirrors the file heirachy of the script page.
	 */
	public String getScriptPackageName()
	{
		String dPackageName = getDerivedPackageName();
		if (dPackageName.length() == 0)
		{
			return basePackageName;
		}
		return basePackageName + '.' + getDerivedPackageName();
	}

	private String getDerivedPackageName()
	{
		if (derivedPackageName != null)
		{
			return derivedPackageName;
		}
		
		WorkflowProcess workflowProcess = workflowTransition.getWorkflowProcess();
		WorkflowPackage workflowPackage = workflowProcess.getWorkflowPackage();
		derivedPackageName = formatJavaID("_" + workflowPackage.getUUID() + "._" + workflowProcess.getUUID());

		return derivedPackageName;
	}

	/**
	 * The package name into which the script class is generated.
	 */
	public void setScriptPackageName(String scriptPackageName)
	{
		this.basePackageName = formatJavaID(scriptPackageName);
	}

	/**
	 * Full path name of the Java file into which the script is being
	 * generated. 
	 */
	public String getScriptJavaFileName()
	{

		if (scriptJavaFileName == null)
		{
			scriptJavaFileName = getOutputDir() + File.separator + getScriptClassName() + ".java";
		}
		else
		{
			// Make sure output dir exists
			makeOutputDir();
		}
		return scriptJavaFileName;
	}

	public void setScriptJavaFileName(String scriptJavaFileName)
	{
		this.scriptJavaFileName = formatJavaID(scriptJavaFileName);
	}

	/**
	 * Get hold of the Options object for this workflowContext. 
	 */
	public Options getOptions()
	{
		return options;
	}

	public WorkflowContext getTemplateContext()
	{
		return workflowContext;
	}

	public ScriptRuntimeContext getRuntimeContext()
	{
		return rctxt;
	}

	public String getScriptClassFileName()
	{

		if (classFileName == null)
		{
			classFileName = getOutputDir() + File.separator + getScriptClassName() + ".class";
		}
		else
		{
			// Make sure output dir exists
			makeOutputDir();
		}
		return classFileName;
	}

	/**
	 * Where is the script being generated?
	 */
	public ScriptWriter getWriter()
	{
		return writer;
	}

	public void setWriter(ScriptWriter writer)
	{
		this.writer = writer;
	}

	/**
	 * Get the input reader for the SCRIPT text. 
	 */
	public ScriptReader getReader()
	{
		return this.reader;
	}

	public void setReader(ScriptReader reader)
	{
		this.reader = reader;
	}

	/**
	 * Are we keeping generated code around?
	 */
	public boolean keepGenerated()
	{
		return getOptions().getKeepGenerated();
	}
	// ==================== Removal ==================== 

	public void incrementRemoved()
	{
		if (removed > 1)
		{
			scriptCompiler.removeGeneratedFiles();
			if (rctxt != null)
			{
				rctxt.removeWrapper(workflowCondition);
			}
		}
		removed++;
	}

	public boolean isRemoved()
	{
		if (removed > 1)
		{
			return true;
		}
		return false;
	}
	// ==================== Compile and reload ====================

	public void compile() throws FileNotFoundException, SelectorException
	{
		createCompiler();

		if (scriptCompiler.isOutDated())
		{
			try
			{
				scriptCompiler.compile();
				ssw.setReload(true);
			}
			catch (Exception ex)
			{
				ex.printStackTrace();
				throw new SelectorException(sm.getString("script.error.unable.compile"), ex);
			}
		}
	}
	// ==================== Manipulating the class ====================

	public Class load() throws WorkflowException, FileNotFoundException
	{
		try
		{
			URL baseUrl = options.getScratchDir().toURL();
			selectorClassLoader =
				new ScriptLoader(
					new URL[] { baseUrl },
					rctxt.getParentClassLoader(),
					rctxt.getPermissionCollection(),
					rctxt.getCodeSource());

			String name = getScriptPackageName() + "." + getScriptClassName();
			selectorClass = selectorClassLoader.loadClass(name);
		}
		catch (Exception ex)
		{
			if (ex instanceof WorkflowException)
			{
				throw (WorkflowException) ex;
			}
			throw new WorkflowException(sm.getString("script.error.unable.load"), ex);
		}
		removed = 0;
		return selectorClass;
	}

	// ==================== Private methods ==================== 

	static Object outputDirLock = new Object();

	private void makeOutputDir()
	{
		synchronized (outputDirLock)
		{
			File outDirFile = new File(outputDir);
			outDirFile.mkdirs();
		}
	}

	private void createOutputDir()
	{
		String path = getScriptPackageName().replace('.', '/');

		try
		{
			// Append script or tag handler path to scratch dir
			baseUrl = options.getScratchDir().toURL();

			String outUrlString = baseUrl.toString() + '/' + path;
			URL outUrl = new URL(outUrlString);
			outputDir = outUrl.getFile() + File.separator;
			makeOutputDir();
		}
		catch (Throwable e)
		{
			throw new IllegalStateException("no output directory: " + e.getMessage());
		}
	}

	private static final boolean isPathSeparator(char c)
	{
		return (c == '/' || c == '\\');
	}

	private static final String formatJavaID(String s)
	{
		if (s == null)
		{
			return null;
		}

		return s.replace('-', '_');
	}

	private static final String canonicalURI(String s)
	{
		if (s == null)
		{
			return null;
		}

		StringBuffer result = new StringBuffer();
		final int len = s.length();
		int pos = 0;
		while (pos < len)
		{
			char c = s.charAt(pos);
			if (isPathSeparator(c))
			{
				/*
					* multiple path separators.
					* 'foo///bar' -> 'foo/bar'
					*/
				while (pos + 1 < len && isPathSeparator(s.charAt(pos + 1)))
				{
					++pos;
				}

				if (pos + 1 < len && s.charAt(pos + 1) == '.')
				{
					/*
						* a single dot at the end of the path - we are done.
						*/
					if (pos + 2 >= len)
						break;

					switch (s.charAt(pos + 2))
					{
						/*
							* self directory in path
							* foo/./bar -> foo/bar
							*/
						case '/' :
						case '\\' :
							pos += 2;
							continue;

							/*
								* two dots in a path: go back one hierarchy.
								* foo/bar/../baz -> foo/baz
								*/
						case '.' :
							// only if we have exactly _two_ dots.
							if (pos + 3 < len && isPathSeparator(s.charAt(pos + 3)))
							{
								pos += 3;
								int separatorPos = result.length() - 1;
								while (separatorPos >= 0 && !isPathSeparator(result.charAt(separatorPos)))
								{
									--separatorPos;
								}
								if (separatorPos >= 0)
									result.setLength(separatorPos);
								continue;
							}
					}
				}
			}
			result.append(c);
			++pos;
		}
		return result.toString();
	}
}
