/*
 * Copyright 2004,2004 The Apache Software Foundation.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.bsf.util.cf;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;

import org.apache.bsf.util.IndentWriter;
import org.apache.bsf.util.StringUtils;

/**
 * A <code>CodeFormatter</code> bean is used to format raw Java code. It
 * indents, word-wraps, and replaces tab characters with an amount of space
 * characters equal to the size of the <code>indentationStep</code> property.
 * To create and use a <code>CodeFormatter</code>, you simply instantiate a
 * new <code>CodeFormatter</code> bean, and invoke
 * <code>formatCode(Reader source, Writer target)</code> with appropriate
 * arguments.
 *
 * @version 1.0
 * @author Matthew J. Duftler
 */
public class CodeFormatter
{
  /**
   * The default maximum line length.
   */
  public static final int     DEFAULT_MAX      = 74;
  /**
   * The default size of the indentation step.
   */
  public static final int     DEFAULT_STEP     = 2;
  /**
   * The default set of delimiters.
   */
  public static final String  DEFAULT_DELIM    = "(+";
  /**
   * The default set of sticky delimiters.
   */
  public static final String  DEFAULT_S_DELIM  = ",";

  // Configurable Parameters
  private             int     maxLineLength    = DEFAULT_MAX;
  private             int     indentationStep  = DEFAULT_STEP;
  private             String  delimiters       = DEFAULT_DELIM;
  private             String  stickyDelimiters = DEFAULT_S_DELIM;

  // Global Variables
  private             int     indent;
  private             int     hangingIndent;
  private             int     origIndent;
  private             boolean inCPP_Comment;

  private void addTok(StringBuffer targetBuf, StringBuffer tokBuf,
					  IndentWriter out)
  {
	int tokLength    = tokBuf.length(),
		targetLength = targetBuf.length();

	if (indent + targetLength + tokLength > maxLineLength)
	{
	  if (targetLength == 0)
	  {
		out.println(indent, tokBuf.toString());
		indent = hangingIndent;
		targetBuf.setLength(0);

		return;
	  }
	  else
	  {
		out.println(indent, targetBuf.toString().trim());
		indent = hangingIndent;
		targetBuf.setLength(0);
	  }
	}

	targetBuf.append(tokBuf.toString());

	return;
  }
  /**
   * Formats the code read from <code>source</code>, and writes the formatted
   * code to <code>target</code>.
   *
   * @param source where to read the unformatted code from.
   * @param target where to write the formatted code to.
   */
  public void formatCode(Reader source, Writer target)
  {
	String         line;
	BufferedReader in  = new BufferedReader(source);
	IndentWriter   out = new IndentWriter(new BufferedWriter(target), true);

	try
	{
	  origIndent    = 0;
	  inCPP_Comment = false;

	  while ((line = in.readLine()) != null)
	  {
		line = line.trim();

		if (line.length() > 0)
		{
		  indent        = origIndent;
		  hangingIndent = indent + indentationStep;
		  printLine(line, out);
		}
		else
		  out.println();
	  }
	}
	catch (IOException e)
	{
	  e.printStackTrace();
	}
  }
  /**
   * Gets the set of delimiters.
   *
   * @return the set of delimiters.
   * @see #setDelimiters
   */
  public String getDelimiters()
  {
	return delimiters;
  }
  /**
   * Gets the size of the indentation step.
   *
   * @return the size of the indentation step.
   * @see #setIndentationStep
   */
  public int getIndentationStep()
  {
	return indentationStep;
  }
  /**
   * Gets the maximum line length.
   *
   * @return the maximum line length.
   * @see #setMaxLineLength
   */
  public int getMaxLineLength()
  {
	return maxLineLength;
  }
  /**
   * Gets the set of sticky delimiters.
   *
   * @return the set of sticky delimiters.
   * @see #setStickyDelimiters
   */
  public String getStickyDelimiters()
  {
	return stickyDelimiters;
  }
  private void printLine(String line, IndentWriter out)
  {
	char[]       source           = line.toCharArray();
	char         ch;
	char         quoteChar        = ' ';
	boolean      inEscapeSequence = false;
	boolean      inString         = false;
	StringBuffer tokBuf           = new StringBuffer(),
				 targetBuf        = new StringBuffer(hangingIndent + line.length());

	for (int i = 0; i < source.length; i++)
	{
	  ch = source[i];

	  if (inEscapeSequence)
	  {
		tokBuf.append(ch);
		inEscapeSequence = false;
	  }
	  else
	  {
		if (inString)
		{
		  switch (ch)
		  {
			case '\\' :
			  tokBuf.append('\\');
			  inEscapeSequence = true;
			  break;
			case '\'' :
			case '\"' :
			  tokBuf.append(ch);

			  if (ch == quoteChar)
			  {
				addTok(targetBuf, tokBuf, out);
				tokBuf.setLength(0);
				inString = false;
			  }
			  break;
			case 9 :  // pass thru tab characters...
			  tokBuf.append(ch);
			  break;
			default :
			  if (ch > 31)
				tokBuf.append(ch);
			  break;
		  }
		}
		else  // !inString
		{
		  if (inCPP_Comment)
		  {
			tokBuf.append(ch);

			if (ch == '/' && i > 0 && source[i - 1] == '*')
			  inCPP_Comment = false;
		  }
		  else
		  {
			switch (ch)
			{
			  case '/' :
				tokBuf.append(ch);

				if (i > 0 && source[i - 1] == '/')
				{
				  String tokStr = tokBuf.append(source,
												i + 1,
												source.length - (i + 1)).toString();

				  out.println(indent, targetBuf.append(tokStr).toString());

				  return;
				}
				break;
			  case '*' :
				tokBuf.append(ch);

				if (i > 0 && source[i - 1] == '/')
				  inCPP_Comment = true;
				break;
			  case '\'' :
			  case '\"' :
				addTok(targetBuf, tokBuf, out);
				tokBuf.setLength(0);
				tokBuf.append(ch);
				quoteChar = ch;
				inString  = true;
				break;
			  case 9 :  // replace tab characters...
				tokBuf.append(StringUtils.getChars(indentationStep, ' '));
				break;
			  case '{' :
				tokBuf.append(ch);
				origIndent += indentationStep;
				break;
			  case '}' :
				tokBuf.append(ch);
				origIndent -= indentationStep;

				if (i == 0)
				  indent = origIndent;
				break;
			  default :
				if (ch > 31)
				{
				  if (delimiters.indexOf(ch) != -1)
				  {
					addTok(targetBuf, tokBuf, out);
					tokBuf.setLength(0);
					tokBuf.append(ch);
				  }
				  else if (stickyDelimiters.indexOf(ch) != -1)
				  {
					tokBuf.append(ch);
					addTok(targetBuf, tokBuf, out);
					tokBuf.setLength(0);
				  }
				  else
					tokBuf.append(ch);
				}
				break;
			}
		  }
		}
	  }
	}

	if (tokBuf.length() > 0)
	  addTok(targetBuf, tokBuf, out);

	String lastLine = targetBuf.toString().trim();

	if (lastLine.length() > 0)
	  out.println(indent, lastLine);
  }
  /**
   * Sets the set of delimiters; default set is <code>"(+"</code>.
   * <p>
   * Each character represents one delimiter. If a line is ready to be
   * word-wrapped and a delimiter is encountered, the delimiter will
   * appear as the <em>first character on the following line</em>.
   * A quotation mark, <code>"</code> or <code>'</code>, opening a string
   * is always a delimiter, whether you specify it or not.
   *
   * @param newDelimiters the new set of delimiters.
   * @see #getDelimiters
   */
  public void setDelimiters(String newDelimiters)
  {
	delimiters = newDelimiters;
  }
  /**
   * Sets the size of the indentation step; default size is <code>2</code>.
   * <p>
   * This is the number of spaces that lines will be indented (when appropriate).
   * 
   * @param newIndentationStep the new size of the indentation step.
   * @see #getIndentationStep
   */
  public void setIndentationStep(int newIndentationStep)
  {
	indentationStep = (newIndentationStep < 0 ? 0 : newIndentationStep);
  }
  /**
   * Sets the (desired) maximum line length; default length is
   * <code>74</code>.
   * <p>
   * If a token is longer than the requested maximum line length,
   * then the line containing that token will obviously be longer
   * than the desired maximum.
   *
   * @param newMaxLineLength the new maximum line length.
   * @see #getMaxLineLength
   */
  public void setMaxLineLength(int newMaxLineLength)
  {
	maxLineLength = (newMaxLineLength < 0 ? 0 : newMaxLineLength);
  }
  /**
   * Sets the set of sticky delimiters; default set is <code>","</code>.
   * <p>
   * Each character represents one sticky delimiter. If a line is ready
   * to be word-wrapped and a sticky delimiter is encountered, the sticky
   * delimiter will appear as the <em>last character on the current line</em>.
   * A quotation mark, <code>"</code> or <code>'</code>, closing a string
   * is always a sticky delimiter, whether you specify it or not.
   *
   * @param newStickyDelimiters the new set of sticky delimiters.
   * @see #getStickyDelimiters
   */
  public void setStickyDelimiters(String newStickyDelimiters)
  {
	stickyDelimiters = newStickyDelimiters;
  }
}
