//----------------------------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.CharArrayWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.util.Hashtable;

import org.huihoo.workflow.WorkflowException;
import org.huihoo.workflow.script.Constants;
import org.huihoo.workflow.script.ScriptCompilationContext;
import org.huihoo.workflow.util.StringManager;

public class ScriptReader
{
	private static StringManager sm = StringManager.getManager(Constants.RESOURCE_BUNDLE_BASE);
	private Mark current = null;
	private ScriptCompilationContext context;
	private String encoding = null;
	protected ScriptReader(String file, ScriptCompilationContext ctx, String encoding)
		throws WorkflowException, FileNotFoundException
	{
		this.context = ctx;
		this.encoding = encoding;
		if (this.encoding == null)
		{
			this.encoding = "ISO-8859-1";
		}
		loadFile(file, this.encoding);
	}

	protected ScriptReader(String content, ScriptCompilationContext ctx) throws WorkflowException, FileNotFoundException
	{
		this.context = ctx;
		loadContent(content);
	}

	protected ScriptReader(InputStream istream, ScriptCompilationContext ctx, String encoding)
		throws WorkflowException, FileNotFoundException
	{
		this.context = ctx;
		this.encoding = encoding;
		if (this.encoding == null)
		{
			this.encoding = "ISO-8859-1";
		}
		loadStream(istream, this.encoding);
	}

	public static ScriptReader createScriptReader(String file, ScriptCompilationContext ctx, String encoding)
		throws WorkflowException, FileNotFoundException
	{
		return new ScriptReader(file, ctx, encoding);
	}

	public static ScriptReader createScriptReader(String content, ScriptCompilationContext ctx)
		throws WorkflowException, FileNotFoundException
	{
		return new ScriptReader(content, ctx);
	}

	public static ScriptReader createScriptReader(InputStream istream, ScriptCompilationContext ctx, String encoding)
		throws WorkflowException, FileNotFoundException
	{
		return new ScriptReader(istream, ctx, encoding);
	}

	public boolean hasMoreInput() throws WorkflowException
	{
		if (current.cursor >= current.stream.length)
		{
			return false;
		}
		return true;
	}
	public int nextChar() throws WorkflowException
	{
		if (!hasMoreInput())
			return -1;
		int ch = current.stream[current.cursor];
		current.cursor++;
		if (ch == '\n')
		{
			current.line++;
			current.col = 0;
		}
		else
		{
			current.col++;
		}
		return ch;
	}
	/**
	 * Gets Content until the next potential RULE element.  Because all elements
	 * begin with a '&lt;' we can just move until we see the next one.
	 */
	String nextContent()
	{
		int cur_cursor = current.cursor;
		int len = current.stream.length;
		char ch;
		if (peekChar() == '\n')
		{
			current.line++;
			current.col = 0;
		}
		else
		{
			current.col++;
		}
		// pure obsfuscated genius!
		while ((++current.cursor < len) && ((ch = current.stream[current.cursor]) != '<'))
		{
			if (ch == '\n')
			{
				current.line++;
				current.col = 0;
			}
			else
			{
				current.col++;
			}
		}
		return new String(current.stream, cur_cursor, current.cursor - cur_cursor);
	}
	char[] getChars(Mark start, Mark stop) throws WorkflowException
	{
		Mark oldstart = mark();
		reset(start);
		CharArrayWriter caw = new CharArrayWriter();
		while (!stop.equals(mark()))
			caw.write(nextChar());
		caw.close();
		reset(oldstart);
		return caw.toCharArray();
	}
	public int peekChar()
	{
		return current.stream[current.cursor];
	}
	public Mark mark()
	{
		return new Mark(current);
	}
	public void reset(Mark mark)
	{
		current = new Mark(mark);
	}
	public boolean matchesIgnoreCase(String string) throws WorkflowException
	{
		Mark mark = mark();
		int ch = 0;
		int i = 0;
		do
		{
			ch = nextChar();
			if (Character.toLowerCase((char) ch) != string.charAt(i++))
			{
				reset(mark);
				return false;
			}
		}
		while (i < string.length());
		reset(mark);
		return true;
	}
	public boolean matches(String string) throws WorkflowException
	{
		Mark mark = mark();
		int ch = 0;
		int i = 0;
		do
		{
			ch = nextChar();
			if (((char) ch) != string.charAt(i++))
			{
				reset(mark);
				return false;
			}
		}
		while (i < string.length());
		reset(mark);
		return true;
	}
	public void advance(int n) throws WorkflowException
	{
		while (--n >= 0)
			nextChar();
	}
	public int skipSpaces() throws WorkflowException
	{
		int i = 0;
		while (isSpace())
		{
			i++;
			nextChar();
		}
		return i;
	}
	/**
	 * Skip until the given string is matched in the stream.
	 * When returned, the context is positioned past the end of the match.
	 * @param s The String to match.
	 * @return A non-null <code>Mark</code> instance if found,
	 * <strong>null</strong> otherwise.
	 */
	public Mark skipUntil(String limit) throws WorkflowException
	{
		Mark ret = null;
		int limlen = limit.length();
		int ch;
		skip : for (ret = mark(), ch = nextChar(); ch != -1; ret = mark(), ch = nextChar())
		{
			if (ch == limit.charAt(0))
			{
				for (int i = 1; i < limlen; i++)
				{
					if (Character.toLowerCase((char) nextChar()) != limit.charAt(i))
						continue skip;
				}
				return ret;
			}
		}
		return null;
	}
	final boolean isSpace()
	{
		return peekChar() <= ' ';
	}
	/**
	 * Parse a space delimited token.
	 * If quoted the token will consume all characters up to a matching quote,
	 * otherwise, it consumes up to the first delimiter character.
	 * @param quoted If <strong>true</strong> accept quoted strings.
	 */
	public String parseToken(boolean quoted) throws WorkflowException
	{
		StringBuffer stringBuffer = new StringBuffer();
		skipSpaces();
		stringBuffer.setLength(0);
		int ch = peekChar();
		if (quoted)
		{
			if (ch == '"' || ch == '\'')
			{
				char endQuote = ch == '"' ? '"' : '\'';
				// Consume the open quote: 
				ch = nextChar();
				for (ch = nextChar(); ch != -1 && ch != endQuote; ch = nextChar())
				{
					if (ch == '\\')
						ch = nextChar();
					stringBuffer.append((char) ch);
				}
				// Check end of quote, skip closing quote:
				if (ch == -1)
				{
					throw new WorkflowException(mark() + " " + sm.getString("script.error.quotes.unterminated"));
				}
			}
			else
			{
				throw new WorkflowException(mark() + " " + sm.getString("script.error.attr.quoted"));
			}
		}
		else
		{
			if (!isDelimiter()) // Read value until delimiter is found:
				do
				{
					ch = nextChar();
					// Take care of the quoting here.
					if (ch == '\\')
					{
						if (peekChar() == '"' || peekChar() == '\'' || peekChar() == '>' || peekChar() == '%')
							ch = nextChar();
					}
					stringBuffer.append((char) ch);
				}
				while (!isDelimiter());
		}
		return stringBuffer.toString();
	}
	/**
	 * Parse an attribute/value pair, and store it in provided hash table.
	 * The attribute/value pair is defined by:
	 * <pre>
	 * av := spaces token spaces '=' spaces token spaces
	 * </pre>
	 * Where <em>token</em> is defined by <code>parseToken</code> and
	 * <em>spaces</em> is defined by <code>skipSpaces</code>.
	 * The name is always considered case insensitive, hence stored in its
	 * lower case version.
	 * @param into The Hashtable instance to save the result to.
	 */
	private void parseAttributeValue(Hashtable into) throws WorkflowException
	{
		// Get the attribute name:
		skipSpaces();
		String name = parseToken(false);
		// Check for an equal sign:
		skipSpaces();
		if (peekChar() != '=')
		{
			throw new WorkflowException(mark() + " " + sm.getString("script.error.attr.novalue", new Object[] { name }));
		}
		char ch = (char) nextChar();
		// Get the attribute value:
		skipSpaces();
		String value = parseToken(true);
		skipSpaces();
		// Add the binding to the provided hashtable:
		into.put(name, value);
		return;
	}
	/**
	 * Parse some tag attributes.
	 * The stream is assumed to be positioned right after the tag name. The
	 * syntax recognized is:
	 * <pre>
	 * tag-attrs := empty | attr-list ("&gt;" | "--&gt;" | %&gt;)
	 * attr-list := empty | av spaces attr-list
	 * empty     := spaces 
	 * </pre>
	 * Where <em>av</em> is defined by <code>parseAttributeValue</code>.
	 * @return A Hashtable mapping String instances (variable names) into
	 * String instances (variable values).
	 */
	public Hashtable parseTagAttributes() throws WorkflowException
	{
		Hashtable values = new Hashtable(11);
		while (true)
		{
			skipSpaces();
			int ch = peekChar();
			if (ch == '>')
			{
				return values;
			}
			if (ch == '-')
			{
				Mark mark = mark();
				nextChar();
				// Close NCSA like attributes "->"
				try
				{
					if (nextChar() == '-' && nextChar() == '>')
						return values;
				}
				finally
				{
					reset(mark);
				}
			}
			else if (ch == '%')
			{
				Mark mark = mark();
				nextChar();
				// Close variable like attributes "%>"
				try
				{
					if (nextChar() == '>')
						return values;
				}
				finally
				{
					reset(mark);
				}
			}
			else if (ch == '/')
			{
				Mark mark = mark();
				nextChar();
				// XMLesque Close tags 
				try
				{
					if (nextChar() == '>')
						return values;
				}
				finally
				{
					reset(mark);
				}
			}
			if (ch == -1)
				break;
			// Parse as an attribute=value:
			parseAttributeValue(values);
		}
		// Reached EOF:
		throw new WorkflowException(mark() + " " + sm.getString("script.error.tag.attr.unterminated"));
	}
	/**
	 * Parse utils - Is current character a token delimiter ?
	 * Delimiters are currently defined to be =, &gt;, &lt;, ", and ' or any
	 * any space character as defined by <code>isSpace</code>.
	 * @return A boolean.
	 */
	private boolean isDelimiter() throws WorkflowException
	{
		if (!isSpace())
		{
			int ch = peekChar();
			// Look for a single-char work delimiter:
			if (ch == '=' || ch == '>' || ch == '"' || ch == '\'' || ch == '/')
				return true;
			// Look for an end-of-comment or end-of-tag:		
			if (ch == '-')
			{
				Mark mark = mark();
				if (((ch = nextChar()) == '>') || ((ch == '-') && (nextChar() == '>')))
				{
					reset(mark);
					return true;
				}
				else
				{
					reset(mark);
					return false;
				}
			}
			return false;
		}
		else
		{
			return true;
		}
	}
	private void loadFile(String file, String encoding) throws WorkflowException, FileNotFoundException
	{
		loadFile(new File(file), encoding);
	}
	private void loadFile(File file, String encoding) throws WorkflowException, FileNotFoundException
	{
		// Default encoding if needed:
		if (encoding == null)
		{
			encoding = this.encoding;
		}
		InputStreamReader reader = null;
		try
		{
			reader = new InputStreamReader(new FileInputStream(file), encoding);
			CharArrayWriter caw = new CharArrayWriter();
			char buf[] = new char[1024];
			for (int i = 0;(i = reader.read(buf)) != -1;)
				caw.write(buf, 0, i);
			caw.close();
			current = new Mark(this, caw.toCharArray(), file.getPath());
		}
		catch (FileNotFoundException fnfe)
		{
			throw fnfe;
		}
		catch (Throwable ex)
		{
			throw new WorkflowException(sm.getString("script.error.file.cannot.read", new Object[] { file }));
		}
		finally
		{
			if (reader != null)
			{
				try
				{
					reader.close();
				}
				catch (Exception any)
				{
				}
			}
		}
	}
	private void loadContent(String content)
	{
		StringReader reader = new StringReader(content);
		try
		{
			CharArrayWriter caw = new CharArrayWriter();
			char buf[] = new char[1024];
			for (int i = 0;(i = reader.read(buf)) != -1;)
				caw.write(buf, 0, i);
			caw.close();
			current = new Mark(this, caw.toCharArray(), null);
		}
		catch (IOException ex)
		{
		}
		finally
		{
			try
			{
				reader.close();
			}
			catch (Exception any)
			{
			}
		}
	}

	private void loadStream(InputStream istream, String encoding) throws WorkflowException, FileNotFoundException
	{
		InputStreamReader reader = null;
		try
		{
			reader = new InputStreamReader(istream, encoding);
			CharArrayWriter caw = new CharArrayWriter();
			char buf[] = new char[1024];
			for (int i = 0;(i = reader.read(buf)) != -1;)
				caw.write(buf, 0, i);
			caw.close();
			current = new Mark(this, caw.toCharArray(), "");
		}
		catch (Throwable ex)
		{
			throw new WorkflowException(sm.getString("script.error.file.cannot.read", new Object[] { "InputStream" }));
		}
		finally
		{
			if (reader != null)
			{
				try
				{
					reader.close();
				}
				catch (Exception any)
				{
				}
			}
		}
	}
}