/*
 * Created on 2004-6-13
 *
 * To change the template for this generated file go to
 * Window - Preferences - Java - Code Generation - Code and Comments
 */
package workflow.script;

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

import workflow.script.compiler.Compiler;
import workflow.script.compiler.ScriptRuntimeContext;
import workflow.script.interpretor.ScriptWrapper;
import workflow.script.interpretor.ScriptWriter;
import workflow.script.loader.ScriptClassLoader;
import workflow.script.core.ScriptContext;
import workflow.script.util.StringManager;
import workflow.wfmc.xpdl.WorkflowPackage;
import workflow.wfmc.xpdl.WorkflowProcess;
import workflow.wfmc.xpdl.WorkflowTransition;


/**
 * @author reic
 *
 * To change the template for this generated type comment go to
 * Window - Preferences - Java - Code Generation - Code and Comments
 */
public class ScriptCompilationContext
{
	protected static StringManager sm = StringManager.getManager(Constants.RESOURCE_BUNDLE_BASE);

	private String className;

	private String basePackageName;
	private String derivedPackageName;

	private String servletJavaFileName;
	private String classFileName;
	private ScriptWriter writer;

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

	private String outputDir;
	private ScriptContext servletContext;
	private URLClassLoader loader;

	private ScriptRuntimeContext rctxt;

	private int removed = 0;

	private URLClassLoader servletClassLoader;
	private URL baseUrl;
	private Class servletClass;

	private WorkflowTransition workflowTransition;

	public ScriptCompilationContext(
		WorkflowTransition workflowTransition,
		Options options,
		ScriptContext servletContext,
		ScriptWrapper ssw,
		ScriptRuntimeContext rctxt)
	{

		this.workflowTransition = workflowTransition;
		this.options = options;
		this.servletContext = servletContext;
		
		this.ssw=ssw;
		this.rctxt = rctxt;
		this.basePackageName = Constants.SERVLET_PACKAGE_NAME;
	}

	/* ==================== Methods to override ==================== */

	/** ---------- Class path and loader ---------- */

	/**
	 * 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 ScriptException
	{
		if (scriptCompiler != null)
		{
			return scriptCompiler;
		}

		scriptCompiler = new Compiler(this);

		return scriptCompiler;
	}

	public Compiler getCompiler()
	{
		return scriptCompiler;
	}

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

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

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

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

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

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

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

		return className;
	}

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

	public String getScriptFilePath()
	{
		return options.getScriptDir() + workflowTransition.getScriptFileName();
	}

	/**
	 * 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 JSP page.
	 */
	public String getServletPackageName()
	{
		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 setServletPackageName(String scriptPackageName)
	{
		this.basePackageName = formatJavaID(scriptPackageName);
	}

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

		if (servletJavaFileName == null)
		{
			servletJavaFileName = getOutputDir() + getServletClassName() + ".java";
		}
		else
		{
			// Make sure output dir exists
			makeOutputDir();
		}
		return servletJavaFileName;
	}

	public void setServletJavaFileName(String servletJavaFileName)
	{
		this.servletJavaFileName = formatJavaID(servletJavaFileName);
	}

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

	public ScriptContext getServletContext()
	{
		return servletContext;
	}

	public ScriptRuntimeContext getRuntimeContext()
	{
		return rctxt;
	}

	public String getServletClassFileName()
	{

		if (classFileName == null)
		{
			classFileName = getOutputDir() + getServletClassName() + ".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;
	}

	/**
	 * 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.removeScriptWrapper(workflowTransition);
			}
		}
		removed++;
	}

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

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

	public Class load() throws ScriptException,FileNotFoundException
	{
		try
		{
			URL baseUrl = options.getScratchDir().toURL();
			servletClassLoader = new ScriptClassLoader(new URL[] { baseUrl }, rctxt.getParentClassLoader());

			String name = getServletPackageName() + "." + getServletClassName();
			servletClass = servletClassLoader.loadClass(name);
		}
		catch (Exception ex)
		{
			if(ex instanceof ScriptException)
			{
				throw (ScriptException)ex;
			}
			throw new ScriptException(sm.getString("script.error.unable.load"), ex);
		}
		removed = 0;
		return servletClass;
	}

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

	static Object outputDirLock = new Object();

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

	private void createOutputDir()
	{
		String path = getServletPackageName().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 (Exception 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();
	}
}
