//----------------------------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.compiler;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.URL;
import java.util.Hashtable;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.huihoo.workflow.WorkflowException;
import org.huihoo.workflow.script.Constants;
import org.huihoo.workflow.script.Options;
import org.huihoo.workflow.script.ScriptCompilationContext;
import org.huihoo.workflow.util.StringManager;
import org.huihoo.workflow.xpdl.WorkflowCondition;
import org.huihoo.workflow.xpdl.condition.ScriptCondition;
import org.huihoo.workflow.xpdl.condition.XCDATACondition;

public class Compiler
{
	private static StringManager sm = StringManager.getManager(Constants.RESOURCE_BUNDLE_BASE);
	private static Log log = LogFactory.getLog(Compiler.class);
	// ----------------------------------------------------- Instance Variables
	protected JavaCompiler javac;
	protected ScriptCompilationContext ctxt;
	protected Options options;
	// ------------------------------------------------------------ Constructor
	public Compiler(ScriptCompilationContext ctxt)
	{
		this.ctxt = ctxt;
		this.options = ctxt.getOptions();
	}
	// --------------------------------------------------------- Public Methods
	public ScriptCompilationContext getCompilationContext()
	{
		return ctxt;
	}
	/** 
	 * Compile the script file from the current engine context
	 */
	public boolean compile() throws WorkflowException, FileNotFoundException, Exception
	{
		return compile(true);
	}
	/**
	 * Compile the script file from the current engine context.  As an side-
	 * effect, tag files that are referenced by this page are also compiled.
	 * @param compileClass If true, generate both .java and .class file
	 *                     If false, generate only .java file
	 */
	public boolean compile(boolean compileClass) throws WorkflowException, FileNotFoundException, Exception
	{
		if (!isOutDated(compileClass))
		{
			return true;
		}
		WorkflowCondition condition = ctxt.getWorkflowCondition();
		if (condition != null && condition instanceof ScriptCondition)
		{
			return compileFile((ScriptCondition) condition, compileClass);
		}
		else if (condition != null && condition instanceof XCDATACondition)
		{
			return compileContent((XCDATACondition) condition, compileClass);
		}
		return true;
	}
	/**
	 * This is a protected method intended to be overridden by 
	 * subclasses of Compiler. This is used by the compile method
	 * to do all the compilation. 
	 */
	public boolean isOutDated()
	{
		return isOutDated(true);
	}
	/**
	 * Determine if a compilation is necessary by checking the time stamp
	 * of the script page with that of the corresponding .class or .java file.
	 * 
	 * This method can by overidden by a subclasses of Compiler.
	 * @param checkClass If true, check against .class file,
	 *                   if false, check against .java file.
	 */
	public boolean isOutDated(boolean checkClass)
	{
		return true;
	}
	/**
			* Set java compiler info
			*/
	public void setJavaCompiler(JavaCompiler javac)
	{
		this.javac = javac;
	}
	/**
		* Change the encoding for the reader if specified.
		*/
	public String changeEncodingIfNecessary(ScriptReader tmpReader) throws WorkflowException
	{
		// A lot of code replicated from Parser.java
		// Main aim is to "get-it-to-work".
		while (tmpReader.skipUntil("<%@") != null)
		{
			tmpReader.skipSpaces();
			// check if it is a page directive.
			if (tmpReader.matches("script"))
			{
				tmpReader.advance(4);
				tmpReader.skipSpaces();
				Hashtable attrs = tmpReader.parseTagAttributes();
				String ct = (String) attrs.get("charset");
				if (ct != null)
				{
					return ct;
				}
			}
		}
		return null;
	}
	/**
	 * Remove generated files
	 */
	public void removeGeneratedFiles()
	{
		try
		{
			String classFileName = ctxt.getScriptClassFileName();
			if (classFileName != null)
			{
				File classFile = new File(classFileName);
				if (log.isDebugEnabled())
				{
					log.debug("Deleting " + classFile);
				}
				classFile.delete();
			}
		}
		catch (Exception e)
		{
			// Remove as much as possible, ignore possible exceptions
		}
		try
		{
			String javaFileName = ctxt.getScriptJavaFileName();
			if (javaFileName != null)
			{
				File javaFile = new File(javaFileName);
				log.debug("Deleting " + javaFile);
				javaFile.delete();
			}
		}
		catch (Exception e)
		{
			// Remove as much as possible, ignore possible exceptions
		}
	}
	protected boolean compileFile(ScriptCondition condition, boolean compileClass)
		throws WorkflowException, FileNotFoundException, Exception
	{
		String javaFileName = ctxt.getScriptJavaFileName();
		String scriptEncoding = "ISO-8859-1"; // default per Script spec
		String javaEncoding = "UTF8";
		ScriptReader reader = null;

		if (options.getScriptDir() == null || options.getScriptDir().startsWith("/WEB-INF/script/"))
		{
			String filePath = "/WEB-INF/script/" + condition.getContent();
			URL fileURL = ctxt.getTemplateContext().getResource(filePath);

			InputStream istream = null;

			try
			{
				istream = fileURL.openStream();
				ScriptReader tmpReader = ScriptReader.createScriptReader(istream, ctxt, scriptEncoding);
				String newEncode = changeEncodingIfNecessary(tmpReader);
				if (newEncode != null)
				{
					scriptEncoding = newEncode;
				}
				reader = ScriptReader.createScriptReader(istream, ctxt, scriptEncoding);
			}
			finally
			{
				if (istream != null)
				{
					try
					{
						istream.close();
					}
					catch (Exception ex)
					{
					}
				}
			}
		}
		else
		{

			String filePath = options.getScriptDir() + File.separator + condition.getContent();

			ScriptReader tmpReader = ScriptReader.createScriptReader(filePath, ctxt, scriptEncoding);
			String newEncode = changeEncodingIfNecessary(tmpReader);
			if (newEncode != null)
			{
				scriptEncoding = newEncode;
			}
			reader = ScriptReader.createScriptReader(filePath, ctxt, scriptEncoding);

		}

		OutputStreamWriter osw;
		try
		{
			osw = new OutputStreamWriter(new FileOutputStream(javaFileName), javaEncoding);
		}
		catch (java.io.UnsupportedEncodingException ex)
		{
			javaEncoding = ctxt.getOptions().getJavaEncoding();
			if (javaEncoding != null)
			{
				try
				{
					osw = new OutputStreamWriter(new FileOutputStream(javaFileName), javaEncoding);
				}
				catch (java.io.UnsupportedEncodingException ex2)
				{
					// no luck :-(
					throw new WorkflowException(
						sm.getString("script.error.invalid.javaEncoding", new Object[] { "UTF8", javaEncoding, }));
				}
			}
			else
			{
				throw new WorkflowException(sm.getString("script.error.needAlternateJavaEncoding", new Object[] { "UTF8" }));
			}
		}

		ScriptWriter writer = new ScriptWriter(new PrintWriter(osw));
		ctxt.setReader(reader);
		ctxt.setWriter(writer);
		ParseEventListener listener = new ScriptParseEventListener(ctxt);
		Parser p = new Parser(reader, listener);
		listener.beginScriptProcessing();
		p.parse();
		listener.endScriptProcessing();
		writer.close();
		ByteArrayOutputStream out = new ByteArrayOutputStream(256);
		String classpath = ctxt.getClassPath();
		javac.setClassDebugInfo(options.getClassDebugInfo());
		javac.setClassPath(classpath);
		javac.setJavaFilePath(javaFileName);
		javac.setJavaEncoding(options.getJavaEncoding());
		javac.setOutputDir(options.getScratchDir().getAbsolutePath());
		javac.setMsgOutput(out);
		/**
		 * Execute the compiler
		 */
		boolean status = javac.compile();
		if (!ctxt.keepGenerated())
		{
			File javaFile = new File(javaFileName);
			javaFile.delete();
		}
		if (status == false)
		{
			String msg = out.toString();
			throw new WorkflowException(sm.getString("script.error.unable.compile") + msg);
		}
		return true;
	}
	protected boolean compileContent(XCDATACondition condition, boolean compileClass)
		throws WorkflowException, FileNotFoundException, Exception
	{
		String javaFileName = ctxt.getScriptJavaFileName();
		String scriptEncoding = "ISO-8859-1"; // default per Script spec
		String javaEncoding = "UTF8";
		ScriptReader reader = ScriptReader.createScriptReader(condition.getContent(), ctxt);
		OutputStreamWriter osw;
		try
		{
			osw = new OutputStreamWriter(new FileOutputStream(javaFileName), javaEncoding);
		}
		catch (java.io.UnsupportedEncodingException ex)
		{
			// Try to get the java encoding from the "javaEncoding"
			// init parameter for JspServlet.
			javaEncoding = ctxt.getOptions().getJavaEncoding();
			if (javaEncoding != null)
			{
				try
				{
					osw = new OutputStreamWriter(new FileOutputStream(javaFileName), javaEncoding);
				}
				catch (java.io.UnsupportedEncodingException ex2)
				{
					// no luck :-(
					throw new WorkflowException(
						sm.getString("script.error.invalid.javaEncoding", new Object[] { "UTF8", javaEncoding, }));
				}
			}
			else
			{
				throw new WorkflowException(sm.getString("script.error.needAlternateJavaEncoding", new Object[] { "UTF8" }));
			}
		}
		ScriptWriter writer = new ScriptWriter(new PrintWriter(osw));
		ctxt.setReader(reader);
		ctxt.setWriter(writer);
		ParseEventListener listener = new ScriptParseEventListener(ctxt);
		Parser p = new Parser(reader, listener);
		listener.beginScriptProcessing();
		p.parse();
		listener.endScriptProcessing();
		writer.close();
		ByteArrayOutputStream out = new ByteArrayOutputStream(256);
		String classpath = ctxt.getClassPath();
		javac.setClassDebugInfo(options.getClassDebugInfo());
		javac.setClassPath(classpath);
		javac.setJavaFilePath(javaFileName);
		javac.setJavaEncoding(options.getJavaEncoding());
		javac.setOutputDir(options.getScratchDir().getAbsolutePath());
		javac.setMsgOutput(out);
		/**
		 * Execute the compiler
		 */
		boolean status = javac.compile();
		if (!ctxt.keepGenerated())
		{
			File javaFile = new File(javaFileName);
			javaFile.delete();
		}
		if (status == false)
		{
			String msg = out.toString();
			throw new WorkflowException(sm.getString("script.error.unable.compile") + msg);
		}
		return true;
	}
}
