/*
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (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.mozilla.org/MPL/>.
 * 
 * Software distributed under the License is distributed on an "AS IS" basis, WITHOUT
 * WARRANTY OF ANY KIND, either express or implied. See the License for the specific
 * language governing rights and limitations under the License.
 * 
 * The Original Code is the Venice Web Communities System.
 * 
 * The Initial Developer of the Original Code is Eric J. Bowersox <erbo@silcom.com>,
 * for Silverwrist Design Studios.  Portions created by Eric J. Bowersox are
 * Copyright (C) 2001-02 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.venice.htmlcheck.impl;

import java.util.*;
import org.apache.log4j.*;
import com.silverwrist.venice.htmlcheck.*;

class HTMLCheckerImpl implements HTMLChecker, HTMLCheckerBackend, RewriterServices
{
  /*--------------------------------------------------------------------------------
   * Wrapper for Rewriters to implement counting where necessary
   *--------------------------------------------------------------------------------
   */

  static class CountingRewriter implements Rewriter
  {
    private Rewriter inner;
    private int count = 0;

    public CountingRewriter(Rewriter inner)
    {
      this.inner = inner;

    } // end constructor
    
    public String getName()
    {
      return inner.getName();

    } // end getName

    public MarkupData rewrite(String data, RewriterServices svc)
    {
      MarkupData rc = inner.rewrite(data,svc);
      if (rc!=null)
	count++;
      return rc;

    } // end rewrite

    public int getCount()
    {
      return count;

    } // end getCount

    public void reset()
    {
      count = 0;

    } // end reset

  } // end class CountingRewriter

  /*--------------------------------------------------------------------------------
   * State machine constants
   *--------------------------------------------------------------------------------
   */

  private static final short ST_WHITESPACE = 0;
  private static final short ST_CHARS = 1;
  private static final short ST_LEFTANGLE = 2;
  private static final short ST_TAG = 3;
  private static final short ST_PAREN = 4;
  private static final short ST_TAGQUOTE = 5;
  private static final short ST_NEWLINE = 6;

  /*--------------------------------------------------------------------------------
   * Internal constants
   *--------------------------------------------------------------------------------
   */

  private static final int MARGIN_SLOP = 5;

  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

  private static Category logger = Category.getInstance(HTMLCheckerImpl.class);

  /*--------------------------------------------------------------------------------
   * Attributes
   *--------------------------------------------------------------------------------
   */

  private HTMLCheckerConfigImpl config;             // the configuration we're working from
  private boolean started = false;                  // have we started parsing?
  private boolean finished = false;                 // have we finished parsing?
  private short state = ST_WHITESPACE;              // current parser state
  private char quote_char;                          // quote character to match
  private int paren_level = 0;                      // parenthesization level
  private short columns = 0;                        // current number of columns
  private int lines = 0;                            // current number of lines
  private int nobreak_count = 0;                    // "no-break" count
  private boolean trigger_WBR = false;              // word break trigger
  private StringBuffer output_buffer = null;        // output buffer for characters
  private StringBuffer temp_buffer = null;          // temporary buffer used within one state
  private ArrayList tag_stack = null;               // stack of tags that have been opened
  private HashMap counters = new HashMap();         // the CountingRewriter instances
  private ArrayList string_rewriters = new ArrayList();   // string rewriter instances
  private ArrayList word_rewriters = new ArrayList();     // word rewriter instances
  private ArrayList tag_rewriters = new ArrayList();      // tag rewriter instances
  private ArrayList paren_rewriters = new ArrayList();    // paren rewriter instances
  private HashMap context_data = new HashMap();     // context variables

  /*--------------------------------------------------------------------------------
   * Constructor
   *--------------------------------------------------------------------------------
   */

  HTMLCheckerImpl(HTMLCheckerConfigImpl config)
  {
    this.config = config;
    copyRewriters(string_rewriters,config.getStringRewriters());
    copyRewriters(word_rewriters,config.getWordRewriters());
    copyRewriters(tag_rewriters,config.getTagRewriters());
    copyRewriters(paren_rewriters,config.getParenRewriters());

  } // end constructor

  /*--------------------------------------------------------------------------------
   * Internal functions
   *--------------------------------------------------------------------------------
   */

  /**
   * Returns <CODE>true</CODE> if this character belongs as part of a word, <CODE>false</CODE> if not.
   *
   * @param ch Character to be tested.
   * @return See above.
   */
  private static final boolean isWordChar(char ch)
  {
    return (Character.isUpperCase(ch) || Character.isLowerCase(ch) || (ch=='-') || (ch=='\''));

  } // end isWordChar

  /**
   * Returns the maximum length of a &quot;run&quot; of word characters or non-word characters in the
   * buffer, beginning at the specified start point, before a character of the opposite classification.
   *
   * @param buf The buffer to look through.
   * @param start The start position to look at.
   * @return See above.
   */
  private static final int getRunLength(StringBuffer buf, int start)
  {
    boolean word_char = isWordChar(buf.charAt(start));
    int l = 1;
    while ((start+l)<buf.length())
    { // see if there's a break from word characters to non-word characters, or vice versa
      if (isWordChar(buf.charAt(start+l))!=word_char)
	break;
      l++;

    } // end while

    return l;

  } // end getRunLength

  /**
   * Returns the maximum length of a &quot;run&quot; of word characters or non-word characters in the
   * buffer, counting from the start of the buffer, before a character of the opposite classification.
   *
   * @param buf The buffer to look through.
   * @return See above.
   */
  private static final int getRunLength(StringBuffer buf)
  {
    return getRunLength(buf,0);

  } // end getRunLength

  /**
   * Copies the <CODE>Rewriter</CODE> objects from an outside list to an internal list, wrapping
   * named rewriters in <CODE>CountingRewriter</CODE> objects as appropriate.
   *
   * @param dest Destination to copy rewriters to.
   * @param source List to copy rewriters from.
   */
  private final void copyRewriters(ArrayList dest, List source)
  {
    Iterator it = source.iterator();
    while (it.hasNext())
    { // get each rewriter, and wrap it if it has a name
      Rewriter r = (Rewriter)(it.next());
      String name = r.getName();
      if (name!=null)
      { // wrap it in a CountingRewriter and hash it...
	CountingRewriter cr = new CountingRewriter(r);
	counters.put(name,cr);
	r = cr;

      } // end if

      dest.add(r);

    } // end while

    dest.trimToSize();

  } // end copyRewriters

  /**
   * Use the specified list of rewriters to attempt to rewrite the specified string data.
   * The first rewriter in the list that returns a valid <CODE>MarkupData</CODE> object takes
   * precedence.
   *
   * @param rewriters List of rewriters to try against the rewriter data.
   * @param data String data to attempt to rewrite.
   * @return A <CODE>MarkupData</CODE> object that contains the marked-up data to output, or
   *         <CODE>null</CODE> if no rewriter handled the data.
   */
  private final MarkupData attemptRewrite(List rewriters, String data)
  {
    Iterator it = rewriters.iterator();
    MarkupData rc = null;
    while ((rc==null) && it.hasNext())
    { // look at each rewriter in turn and try seeing if it will mark this text up
      Rewriter r = (Rewriter)(it.next());
      rc = r.rewrite(data,this);

    } // end while

    return rc;

  } // end attemptRewrite

  /**
   * Initializes the internal state of the parser.
   */
  private final void initState()
  {
    output_buffer = new StringBuffer(1024);
    temp_buffer = new StringBuffer(64);
    tag_stack = new ArrayList();

  } // end initState

  /**
   * Erases any temporary data that is no longer needed after the parser finishes.
   */
  private final void killState()
  {
    temp_buffer = null;
    if (tag_stack!=null)
      tag_stack.clear();
    tag_stack = null;

  } // end killState

  /**
   * Emits one character to the output of the HTML checker, running it through a list of defined filters.
   *
   * @param ch Character to output.
   * @param filters List of filters to use to attempt to process the character.
   * @param count_cols <CODE>true</CODE> if the character output adds to the column counter,
   *                   <CODE>false</CODE> if not.
   */
  private final void emitChar(char ch, List filters, boolean count_cols)
  {
    boolean handled = false;
    if (filters.size()>0)
    { // look through all the output filters to see what we can do
      Iterator it = filters.iterator();
      while (!handled && it.hasNext())
      { // look for an output filter that matches this character
	OutputFilter of = (OutputFilter)(it.next());
	handled = of.tryOutputCharacter(output_buffer,ch);
	
      } // end while

    } // end if

    if (!handled)  // output the character
      output_buffer.append(ch);
    if (count_cols && (config.getWordWrapLength()>0))
      columns++;  // adjust column indicator

  } // end emitChar

  /**
   * Emits a character string to the output of the HTML checker, running it through a list of defined filters.
   *
   * @param str String to output.
   * @param filters List of filters to use to attempt to process the string.
   * @param count_cols <CODE>true</CODE> if the characters output add to the column counter,
   *                   <CODE>false</CODE> if not.
   */
  private final void emitString(String str, List filters, boolean count_cols)
  {
    boolean real_count_cols = count_cols && (config.getWordWrapLength()>0);

    if (filters.size()==0)
    { // if there are no filters, just append the entire string directly
      output_buffer.append(str);
      if (real_count_cols)
	columns += (short)(str.length());
      return;

    } // end if

    String temp = str;
    while ((temp!=null) && (temp.length()>0))
    { // We output as much of the string as we possibly can at once.  For starters,
      // assume we're going to output the whole thing.
      int output_len = temp.length();

      // Now look at each of the output filters to see if we should try outputting
      // a lesser amount (i.e., does the string contain a "stopper" that one of the
      // output filters would like to mogrify?)
      Iterator it = filters.iterator();
      OutputFilter stopper = null;
      while (it.hasNext() && (output_len>0))
      { // look to find the length of characters that doesn't match this filter
	OutputFilter of = (OutputFilter)(it.next());
	int lnm = of.lengthNoMatch(temp);
	if ((lnm>=0) && (lnm<output_len))
	{ // we've found a new stopper - record the length and the filter
	  output_len = lnm;
	  stopper = of;

	} // end if

      } // end while (looking through filters)

      if (output_len>0)
      { // At least this many unaltered characters can be output, so copy them.
	output_buffer.append(temp.substring(0,output_len));
	if (real_count_cols)
	  columns += (short)output_len;

      } // end if

      if (stopper!=null)
      { // one of the output filters stopped us, so invoke it
	char tmpch = temp.charAt(output_len++);
	if (!(stopper.tryOutputCharacter(output_buffer,tmpch)))
	  output_buffer.append(tmpch);
	if (real_count_cols)
	  columns++;

      } // end if

      // Chop the string down the middle and go around again.
      if (output_len==temp.length())
	temp = null;
      else if (output_len>0)
	temp = temp.substring(output_len);

    } // end while (still data left to append)

  } // end emitString

  /**
   * Emits a line break to the output of the HTML checker, resetting the column counter and advancing
   * the line counter.
   */
  private final void emitLineBreak()
  {
    emitString("\r\n",config.getRawOutputFilters(),false);
    if (config.getWordWrapLength()>0)
      columns = 0;
    lines++;

  } // end emitLineBreak

  /**
   * If the current line of text contains more characters than will fit on the line, emit a line break.
   * Disabled if the checker does not word-wrap.
   */
  private final void emitPossibleLineBreak()
  {
    if ((config.getWordWrapLength()>0) && (nobreak_count<=0) && (columns>=config.getWordWrapLength()))
      emitLineBreak();

  } // end emitPossibleLineBreak

  /**
   * Ensure that the current line has a certain number of characters of space left on it; if it does not,
   * emit a line break.
   *
   * @param nchars Number of characters to reserve on the line.
   */
  private final void ensureSpaceOnLine(int nchars)
  {
    if ((config.getWordWrapLength()>0) && (nobreak_count<=0))
    { // line break might be required here
      int remain_space = (int)(config.getWordWrapLength() - columns);
      if (remain_space<nchars)
	emitLineBreak();
      
    } // end if

  } // end ensureSpaceOnLine

  private final void emitMarkupData(MarkupData md)
  {
    ensureSpaceOnLine(md.getText().length());
    List raw_filters = config.getRawOutputFilters();
    emitString(md.getBeginMarkup(),raw_filters,false);
    emitString(md.getText(),config.getOutputFilters(),true);
    emitString(md.getEndMarkup(),raw_filters,false);

  } // end emitMarkupData

  private final void emitMarkupData(MarkupData md, char prefix, char suffix)
  {
    ensureSpaceOnLine(md.getText().length() + 2);
    List raw_filters = config.getRawOutputFilters();
    List cooked_filters = config.getOutputFilters();
    emitChar(prefix,cooked_filters,true);
    emitString(md.getBeginMarkup(),raw_filters,false);
    emitString(md.getText(),cooked_filters,true);
    emitString(md.getEndMarkup(),raw_filters,false);
    emitChar(suffix,cooked_filters,true);

  } // end emitMarkupData

  private final void doFlushWhitespace()
  {
    int output_len = temp_buffer.length();
    if (output_len<=0)
      return;
    boolean force_line_break = false;

    if ((config.getWordWrapLength()>0) && (nobreak_count<=0))
    { // adjust output if necessary for word wrapping
      int remain_space = (int)(config.getWordWrapLength() - columns);
      if (remain_space<output_len)
	output_len = remain_space;
      if (output_len<=0)
      { // this means that NONE of the whitespace would fit on this line...
	force_line_break = true;  // we need a line break to fill in!
	output_len = 0;

      } // end if

    } // end if

    if (force_line_break)
      emitLineBreak();
    if (output_len>0)
      emitString(temp_buffer.substring(0,output_len),config.getOutputFilters(),true);

    // clear out the buffer
    temp_buffer.setLength(0);

  } // end doFlushWhitespace

  private final void doFlushNewlines()
  {
    //logger.debug("FlushNewLines");
    int line_breaks = 0, crs = 0;
    for (int i=0; i<temp_buffer.length(); i++)
    { // convert the \r and \n string into a line break count
      char ch = temp_buffer.charAt(i);
      if (ch=='\r')
      { // count carriage returns
	//logger.debug("CR");
	crs++;

      } // end if
      else if (ch=='\n')
      { // count line breaks...
	//logger.debug("LF");
	crs = 0;
	line_breaks++;

      } // end else if

    } // end for

    if (crs>0)
      line_breaks++;
    //logger.debug("Total line breaks: " + line_breaks);

    if (config.getRewrapLines())
    { // rewrap lines forces a lot of adjustment in lines...
      if (line_breaks<2)
      { // convert a single line break to whitespace
	temp_buffer.setLength(0);
	temp_buffer.append(' ');
	state = ST_WHITESPACE;
	return;

      } // end if
      else
	line_breaks = 2;  // compress out multiple blank lines

    } // end if

    while ((line_breaks--)>0)
      emitLineBreak();  // emit the line breaks

    temp_buffer.setLength(0);  // clear out the buffer
    state = ST_WHITESPACE;

  } // end doFlushNewlines

  private final void emitFromStartOfTempBuffer(int nchars)
  {
    if (nchars<=0)
      return;  // can't emit less than 1 character!

    if ((config.getWordWrapLength()>0) && (nobreak_count<=0))
    { // we can output the line break anywhere in the subsequence...
      while (nchars>0)
      { // figure out how many characters we can output WITHOUT causing a line break
	int curlen = nchars;
	int remaining_space = (int)(config.getWordWrapLength() - columns);
	if (curlen>remaining_space)
	  curlen = remaining_space;

	// EJB 10/2/2001 - only output if we have something to output (and there is space for it)
	if (curlen>0)
	{ // output those characters
	  emitString(temp_buffer.substring(0,curlen),config.getOutputFilters(),true);
	  temp_buffer.delete(0,curlen);
	  nchars -= curlen;

	} // end if

	if (columns>=config.getWordWrapLength())  // EJB 10/2/2001 - might be greater
	  emitLineBreak();  // and line break us to get to the next line

      } // end while

    } // end if
    else
    { // just output the run of characters straight
      emitString(temp_buffer.substring(0,nchars),config.getOutputFilters(),true);
      temp_buffer.delete(0,nchars);
  
    } // end else

  } // end emitFromStartOfTempBuffer

  private final void doFlushString()
  {
    MarkupData md = attemptRewrite(string_rewriters,temp_buffer.toString());
    if (md!=null)
    { // we've got something marked up!  output it...
      emitMarkupData(md);
      temp_buffer.setLength(0);
      return;

    } // end if

    boolean first = true;
    while (temp_buffer.length()>0)
    { // find the length of the initial string of word or non-word characters
      int sublen = getRunLength(temp_buffer);

      if (isWordChar(temp_buffer.charAt(0)))
      { // we need to check the word...but first, we must eliminate leading hyphens and apostrophes
	int hyph_count = 0;
	final String hyph_apos = "-\'";
	while ((hyph_count<sublen) && (hyph_apos.indexOf(temp_buffer.charAt(hyph_count))>=0))
	  hyph_count++;
	emitFromStartOfTempBuffer(hyph_count);
	sublen -= hyph_count;

	// now determine how many hyphens/apostrophes there are at the end of the word...
	int word_len = sublen;
	hyph_count = 0;
	while ((word_len>0) && (hyph_apos.indexOf(temp_buffer.charAt(word_len-1))>=0))
	{ // decrement word length, increment hyphen/apostrophe count
	  hyph_count++;
	  word_len--;

	} // end while

	if (word_len>0)
	{ // extract the word from the start of the buffer
	  String word = temp_buffer.substring(0,word_len);
	  temp_buffer.delete(0,word_len);

	  // try to rewrite this word...
	  md = attemptRewrite(word_rewriters,word);
	  if (md!=null)
	    emitMarkupData(md);
	  else
	  { // just output the word normally
	    ensureSpaceOnLine(word.length());
	    emitString(word,config.getOutputFilters(),true);

	  } // end else

	} // end if

	// now emit the rest of the hyphens/apostrophes
	emitFromStartOfTempBuffer(hyph_count);

      } // end if
      else
      { // just emit this many characters, line-breaking where required
	if ((sublen==temp_buffer.length()) && !first && (sublen<=MARGIN_SLOP))
        { // This is intended to handle a small run of non-word characters at the end of a string (i.e.
	  // followed by whitespace) that should stay on the same line with its preceding word, to
	  // eliminate "funnies" in punctuation formatting.
	  emitString(temp_buffer.toString(),config.getOutputFilters(),true);
	  temp_buffer.setLength(0);
	  break;

	} // end if

	// This is kind of the inverse of the above check; if we have a small run of non-word
	// characters at the START of a word (preceded by whitespace and followed by at least
	// one word character), then ensure that we can keep that word and its prefixing non-word
	// characters on the same line (again, avoiding "funnies" in formatting).
	if ((sublen<temp_buffer.length()) && first && (sublen<=MARGIN_SLOP))
	{ // get the length of the word and ensure they can be kept together
	  int fw_len = getRunLength(temp_buffer,sublen);
	  ensureSpaceOnLine(sublen+fw_len);

        } // end if

	emitFromStartOfTempBuffer(sublen);

      } // end else

      first = false;

    } // end while

  } // end doFlushString

  private final boolean handleAsHTML()
  {
    if (logger.isDebugEnabled())
      logger.debug("handleAsHTML(): candidate buffer = [" + temp_buffer.toString() + "]");
    trigger_WBR = false;  // initialize

    // Figure out the place in the buffer where the command word starts.
    int start_cmd = 0;
    boolean closing_tag = false;
    if ((start_cmd<temp_buffer.length()) && (temp_buffer.charAt(start_cmd)=='/'))
    { // this is a closing tag - move the command start pointer...
      if (logger.isDebugEnabled())
	logger.debug("closing tag detected");
      start_cmd++;
      closing_tag = true;

    } // end if

    // Now figure out where it ends.
    int end_cmd = start_cmd;
    while ((end_cmd<temp_buffer.length()) && !(Character.isWhitespace(temp_buffer.charAt(end_cmd))))
      end_cmd++;

    if ((end_cmd==start_cmd) || ((end_cmd-start_cmd)>TagRepository.getMaxTagLength()))
    { // the command word is empty or is just too long to be an HTML tag
      if (logger.isDebugEnabled())
	logger.debug("HTML tag rejected due to length (" + String.valueOf(end_cmd-start_cmd) + ")");
      return false;

    } // end if

    // Look up the tag name to get a tag index from the repository.
    String poss_tag_name = temp_buffer.substring(start_cmd,end_cmd);
    int tag_index = TagRepository.tagNameToIndex(poss_tag_name);
    if (tag_index<0)
    { // not a known HTML tag
      if (logger.isDebugEnabled())
	logger.debug("<" + poss_tag_name + "> is not valid HTML");
      return false;

    } // end if

    // Look up the tag object that corresponds to the tag index.
    SimpleTag tagobj = TagRepository.tagIndexToObject(tag_index);
    if (closing_tag && !(tagobj.allowClose()))
    { // this is a closing tag, and this tag doesn't permit the "close" form
      if (logger.isDebugEnabled())
	logger.debug("</" + poss_tag_name + "> is not a permitted tag form (no close)");
      return false;

    } // end if

    // Get the HTML tag set index for this tag, and see if we allow that set.
    int tag_set_id = TagRepository.tagIndexToSet(tag_index);
    if (!(config.isTagSetAllowed(tag_set_id)))
    { // the tag is not allowed - either discard it or leave it in verbatim
      if (config.getDiscardHTMLTags() || config.getDiscardRejectedHTML())
      { // throw this tag the hell away!
	if (logger.isDebugEnabled())
	  logger.debug("<" + poss_tag_name + "> tag rejected and discarded");
	return true;

      } // end if
      else
      { // kick the tag out and let some other code deal with it
	if (logger.isDebugEnabled())
	  logger.debug("<" + poss_tag_name + "> is not allowed in this context");
	return false;

      } // end if

    } // end if (tag rejected by HTML rules)

    if (!(config.getDiscardHTMLTags()) && tagobj.balanceTags())
    { // this tag needs to be balanced - here is where we manipulate the stack
      boolean valid = false;

      if (closing_tag)
      { // hunt through the list to find the most recently-opened tag of this type
	int i = tag_stack.size();
	while ((--i)>=0)
	{ // look through the stack...
	  Integer foo = (Integer)(tag_stack.get(i));
	  if (foo.intValue()==tag_index)
	  { // found it - remove it from the tag stack
	    if (logger.isDebugEnabled())
	      logger.debug("balanced closure of <" + poss_tag_name + ">");
	    tag_stack.remove(i);
	    valid = true;
	    break;

	  } // end if

	} // end while

      } // end if
      else
      { // push a new opening tag!
	if (logger.isDebugEnabled())
	  logger.debug("open form of <" + poss_tag_name + ">");
	tag_stack.add(new Integer(tag_index));
	valid = true;

      } // end else

      if (!valid)
      { // not validated by the stack code
	if (logger.isDebugEnabled())
	  logger.debug("stack checking code rejects <" + poss_tag_name + ">");
	return false;

      } // end if

    } // end if
    // else tag doesn't need to be auto-balanced, or is being discarded
    
    // Give the tag object one last chance to dictate what we do with the tag.
    String real_tag_data = tagobj.rewriteTagContents(temp_buffer.toString(),closing_tag,this);
    if ((real_tag_data==null) || config.getDiscardHTMLTags())
    { // tag is being erased by rewriter, or is being discarded anyway
      if (logger.isDebugEnabled())
	logger.debug("rewriter erasure or discard of <" + poss_tag_name + ">");
      return true;

    } // end if

    if (logger.isDebugEnabled())
      logger.debug("real tag data = [" + real_tag_data + "]");

    // Emit the tag to the output.
    List filters = config.getRawOutputFilters();
    emitChar('<',filters,false);
    emitString(real_tag_data,filters,false);
    emitChar('>',filters,false);

    // Determine whether this tag causes a "logical line break."
    boolean logical_line_break = false;
    if (trigger_WBR && !closing_tag && (nobreak_count>0))
    { // a word break is a logical line break (but only if within <NOBR>...</NOBR>)
      if (logger.isDebugEnabled())
	logger.debug("<" + poss_tag_name + "> WORD BREAKS");
      logical_line_break = true;

    } // end if
    else
      logical_line_break = tagobj.causeLineBreak(closing_tag);
    if (logical_line_break)
    { // logical line break is triggered
      if (logger.isDebugEnabled())
	logger.debug("<" + poss_tag_name + "> triggers logical line break");
      columns = 0;

    } // end if

    return true;  // handled!

  } // end handleAsHTML

  /**
   * Returns <CODE>true</CODE> if the temporary buffer contains the start of an HTML comment.  (The
   * leading and trailing angle brackets are assumed.)
   *
   * @return See above.
   */
  private final boolean containsHTMLComment()
  {
    return ((temp_buffer.length()>=3) && (temp_buffer.substring(0,3).equals("!--")));

  } // end containsHTMLComment

  /**
   * Returns <CODE>true</CODE> if the temporary buffer contains a complete HTML comment.  (The leading
   * and trailing angle brackets are assumed.)
   *
   * @return See above.
   */
  private final boolean containsCompleteHTMLComment()
  {
    int l = temp_buffer.length();
    // note that a minimum HTML comment is <!---->, i.e. "<!--" followed by "-->" with no characters
    // in between...
    return ((l>=5) && (temp_buffer.substring(0,3).equals("!--"))
	    && (temp_buffer.substring(l-2,l).equals("--")));

  } // end containsCompleteHTMLComment

  /**
   * Returns <CODE>true</CODE> if the temporary buffer contains an XML construct, i.e. a tag that
   * contains a ':', and may or may not have a leading '/'.  (The leading and trailing angle brackets
   * are assumed.)
   *
   * @return See above.
   */
  private final boolean containsXMLConstruct()
  {
    int ptr = 0;
    if ((temp_buffer.length()>1) && (temp_buffer.charAt(0)=='/'))
      ptr++;
    while (ptr<temp_buffer.length())
    { // look for a ':' in the "tag name" portion of the HTML tag
      char ch = temp_buffer.charAt(ptr++);
      if (ch==':')
	return true;
      if (Character.isWhitespace(ch))
	break;

    } // end while

    return false;

  } // end containsXMLConstruct

  private final void finishTag()
  {
    if (containsHTMLComment())
    { // we are dealing with at least the start of an HTML comment - is it a complete one?
      if (!(containsCompleteHTMLComment()))
	return;  // we have an HTML comment but it's not complete yet - bail out

      if (!(config.getDiscardHTMLComments()))
      { // output the comment in the raw
	List filters = config.getRawOutputFilters();
	emitChar('<',filters,false);
	emitString(temp_buffer.toString(),filters,false);
	emitChar('>',filters,false);

      } // end if

      // clear our state and return to parsing
      temp_buffer.setLength(0);
      state = ST_WHITESPACE;
      return;

    } // end if

    if (handleAsHTML())
    { // the tag has been handled as an HTML tag - bail out immediately
      temp_buffer.setLength(0);
      state = ST_WHITESPACE;
      return;

    } // end if

    // now try to handle it using a tag rewriter
    MarkupData md = attemptRewrite(tag_rewriters,temp_buffer.toString());
    if (md!=null)
    { // we've got something marked up!  output it...
      emitMarkupData(md,'<','>');
      temp_buffer.setLength(0);
      state = ST_WHITESPACE;
      return;

    } // end if

    if (config.getDiscardXMLConstructs() && containsXMLConstruct())
    { // the tag is an XML construct, and is to be discarded
      temp_buffer.setLength(0);
      state = ST_WHITESPACE;
      return;

    } // end if

    // This tag has been rejected!  We need to process it normally, as character data.
    String rejection = temp_buffer.toString();
    temp_buffer.setLength(0);
    temp_buffer.append('<');
    state = ST_CHARS;
    if (rejection.length()>0)
      parse(rejection);   // just run it through the parser, now that we've fixed up the state
    parse(">");

  } // end finishTag

  private final void finishParen()
  {
    // Try to handle the paren element using a paren rewriter.
    MarkupData md = attemptRewrite(paren_rewriters,temp_buffer.toString());
    if (md!=null)
    { // we've got something marked up!  output it...
      emitMarkupData(md,'(',')');
      temp_buffer.setLength(0);
      state = ST_WHITESPACE;
      paren_level = 0;
      return;

    } // end if

    // This tag has been rejected!  We need to process it normally, as character data.
    String rejection = temp_buffer.toString();
    temp_buffer.setLength(0);
    temp_buffer.append('(');
    state = ST_CHARS;
    paren_level = 0;
    if (rejection.length()>0)
      parse(rejection);   // just run it through the parser, now that we've fixed up the state
    parse(")");

  } // end finishParen

  private final void parse(String str)
  {
    int i = 0;
    while (i<str.length())
    { // get the character from the string
      char ch = str.charAt(i);

      // now process differently based on the current parser state
      switch (state)
      {
	case ST_WHITESPACE:
	  { // Whitespace handling - look at the character
	    switch (ch)
	    {
	      case ' ':     // append spaces and tabs verbatim to the temp buffer
	      case '\t':
		temp_buffer.append(ch);
		i++;
		break;

	      case '\r':    // flush the whitespace and go to NEWLINE state
	      case '\n':
		doFlushWhitespace();
		state = ST_NEWLINE;
		temp_buffer.append(ch);
		i++;
		break;

	      case '<':
		doFlushWhitespace();  // flush the whitespace we've already got
		if (config.getProcessAngles())
		  state = ST_LEFTANGLE;
		else
		{ // just process as an ordinary character
		  state = ST_CHARS;
		  temp_buffer.append(ch);

		} // end else
		i++;
		break;

	      case '(':
		doFlushWhitespace();  // flush the whitespace we've already got
		if (config.getProcessParens())
		  state = ST_PAREN;
		else
		{ // just process as an ordinary character
		  state = ST_CHARS;
		  temp_buffer.append(ch);

		} // end else
		i++;
		break;

	      case '\\':  // backslash processing is complex - shift to ST_CHARS state to handle it
		doFlushWhitespace();
		state = ST_CHARS;
		break;

	      default:
		doFlushWhitespace();  // flush the whitespace we've already got
		state = ST_CHARS;
		temp_buffer.append(ch);
		i++;
		break;

	    } // end switch

	  } // end case
	  break;

	case ST_CHARS:
	  { // Character data handling - look at the character
	    switch (ch)
	    {
	      case ' ':   // whitespace - drop back to whitespace mode
	      case '\t':
		doFlushString();
		state = ST_WHITESPACE;
		temp_buffer.append(ch);
		i++;
		break;

	      case '\r':  // newline characters - flush out the buffer and go to NEWLINE state
	      case '\n':
		doFlushString();
		state = ST_NEWLINE;
		temp_buffer.append(ch);
		i++;
		break;

	      case '<':   // left angle bracket - may be a start-of-tag
		if (config.getProcessAngles())
		{ // this is a tag start - go to LEFTANGLE state
		  doFlushString();
		  state = ST_LEFTANGLE;

		} // end if
		else  // just handle it normally
		  temp_buffer.append(ch);
		i++;
		break;

	      case '(':   // left parenthesis - may be a start-of-paren
		if (config.getProcessParens())
		{ // we're going into Parens mode...
		  doFlushString();
		  paren_level = 0;
		  state = ST_PAREN;

		} // end if
		else  // just handle it normally
		  temp_buffer.append(ch);
		i++;
		break;

	      case '\\':
		if (i<(str.length()-1))
		{ // look at the character following the backslash
		  ch = str.charAt(++i);
		  if (((ch=='(') && config.getProcessParens()) || ((ch=='<') && config.getProcessAngles()))
		  { // append the escaped character, omitting the backslash
		    temp_buffer.append(ch);
		    i++;

		  } // end if
		  else // append the backslash and hit the new character
		    temp_buffer.append('\\');

		} // end if
		else
		{ // just append the backslash normally
		  temp_buffer.append('\\');
		  i++;

		} // end else
		break;

	      default:  // just append the next non-white character
		temp_buffer.append(ch);
		i++;
		break;
	      
	    } // end switch

	  } // end case
	  break;

	case ST_LEFTANGLE:
	  { // Left Angle processing - this isn't very difficult
	    switch (ch)
	    {
	      case ' ':
	      case '\t':
	      case '\r':
	      case '\n':  // output the < and click back to whitespace mode
		emitChar('<',config.getOutputFilters(),true);
		state = ST_WHITESPACE;
		break;

	      case '<':   // output the < and stay in this mode
		emitChar('<',config.getOutputFilters(),true);
		i++;
		break;

	      default:    // click over to TAG mode
		state = ST_TAG;
		temp_buffer.append(ch);
		i++;
		break;
	      
	    } // end switch

	  } // end case
	  break;

	case ST_TAG:
	  { // inside a tag - process the character here
	    switch (ch)
	    {
	      case '>':  // end tag
		finishTag();  // this changes the state, and maybe calls parse() recursively
		i++;
		break;

	      case '\'':  // go into "quote string" mode inside tag
	      case '\"':
		temp_buffer.append(ch);
		state = ST_TAGQUOTE;
		quote_char = ch;
		i++;
		break;

	      default:   // just append more data to the temp buffer
		temp_buffer.append(ch);
		i++;
		break;

	    } // end switch

	  } // end case
	  break;

	case ST_PAREN:
	  { // inside parentheses - try to build something up
	    switch (ch)
	    {
	      case '(':  // append the open parenthesis and kick it up a notch!
		temp_buffer.append(ch);
		paren_level++;
		i++;
		break;

	      case ')':
		if (paren_level==0)
		  finishParen();  // will change the parser state
		else
		{ // append the close parenthesis and kick it DOWN a notch
		  temp_buffer.append(ch);
		  paren_level--;

		} // end else
		i++;
		break;

	      default:
		temp_buffer.append(ch);
		i++;
		break;

	    } // end switch

	  } // end case
	  break;

	case ST_TAGQUOTE:
	  temp_buffer.append(ch);
	  if (ch==quote_char) // close quote seen - go back to ST_TAG state
	    state = ST_TAG;
	  i++;
	  break;

	case ST_NEWLINE:
	  if ((ch=='\r') || (ch=='\n'))
	  { // handle newlines
	    temp_buffer.append(ch);
	    i++;

	  } // end if
	  else // flush out the newlines and click back to whitespace mode
	    doFlushNewlines();
	  break;

	default:
	  throw new IllegalStateException("invalid parser state value");

      } // end switch

    } // end while (looking through string)

  } // end parse

  /*--------------------------------------------------------------------------------
   * Implementations from interface HTMLChecker
   *--------------------------------------------------------------------------------
   */

  public void append(String str) throws AlreadyFinishedException
  {
    if (finished)
      throw new AlreadyFinishedException();
    if (!started)
    { // initialize the parser state
      initState();
      started = true;

    } // end if

    if (str!=null)
      parse(str);  // parse things

  } // end append

  public void finish() throws AlreadyFinishedException
  {
    if (finished)
      throw new AlreadyFinishedException();
    if (!started)
    { // set up the initial parser state (so we don't kill ourselves later)
      initState();
      started = true;

    } // end if

    // This is the "end parse" loop, in which we resolve any funny state the parser has
    // found itself in and clear out the internal buffers.
    boolean running;
    do
    { // what we do depends on the parser state...
      running = false;  // make sure we stop unless running is set to true here

      switch (state)
      {
	case ST_WHITESPACE:
	case ST_NEWLINE:
	  break;    // discard any whitespace or newlines at the end of output

	case ST_CHARS:
	  doFlushString();  // flush out the temporary buffer
	  break;

	case ST_LEFTANGLE:  // just emit a '<' character
	  emitPossibleLineBreak();
	  emitChar('<',config.getOutputFilters(),true);
	  break;

	case ST_TAG:
	case ST_TAGQUOTE:
	  { // we won't finish this tag, so it's automagically rejected
	    String rejection = temp_buffer.toString();
	    temp_buffer.setLength(0);
	    temp_buffer.append('<');
	    state = ST_CHARS;

	    // now parse the tag contents again
	    if (rejection.length()>0)
	      parse(rejection);
	    
	    running = true;  // go back around for another try

	  } // end case
	  break;

	case ST_PAREN:
	  { // we won't finish this paren tag, so it's automagically rejected
	    String rejection = temp_buffer.toString();
	    temp_buffer.setLength(0);
	    temp_buffer.append('(');
	    state = ST_CHARS;
	    paren_level = 0;

	    // now parse the parenthesis contents again
	    if (rejection.length()>0)
	      parse(rejection);
	    
	    running = true;  // go back around for another try

	  } // end case
	  break;

      } // end switch

    } while (running);  // end do

    // Now close all the HTML tags that were left open.
    for (int i=(tag_stack.size()-1); i>=0; i--)
    { // get each element in the tag stack and append the appropriate closing tag
      Integer foo = (Integer)(tag_stack.get(i));
      SimpleTag tagobj = TagRepository.tagIndexToObject(foo.intValue());
      output_buffer.append(tagobj.makeClosingTag());

    } // end for

    // deallocate some excess crap and mark the object as finished
    killState();
    lines++;  // there's one extra line at the end
    finished = true;

  } // end finish

  public void reset()
  {
    started = false;
    finished = false;
    trigger_WBR = false;
    state = ST_WHITESPACE;
    quote_char = '\0';
    columns = 0;
    lines = 0;
    paren_level = 0;
    output_buffer = null;
    killState();

    // Also reset all the counters.
    Iterator it = counters.values().iterator();
    while (it.hasNext())
    { // reset each counter in turn
      CountingRewriter cr = (CountingRewriter)(it.next());
      cr.reset();

    } // end while

  } // end reset

  public String getValue() throws NotYetFinishedException
  {
    if (!finished)
      throw new NotYetFinishedException();
    return output_buffer.toString();

  } // end getValue

  public int getLength() throws NotYetFinishedException
  {
    if (!finished)
      throw new NotYetFinishedException();
    return output_buffer.length();

  } // end getLength

  public int getLines() throws NotYetFinishedException
  {
    if (!finished)
      throw new NotYetFinishedException();
    return lines;

  } // end getLines

  public int getCounter(String name) throws NotYetFinishedException
  {
    if (!finished)
      throw new NotYetFinishedException();
    CountingRewriter cr = (CountingRewriter)(counters.get(name));
    if (cr==null)
      return 0;
    else
      return cr.getCount();

  } // end getCounter

  public Object getContextValue(String name)
  {
    return context_data.get(name);

  } // end getContextValue

  public void setContextValue(String name, Object val)
  {
    context_data.put(name,val);

  } // end setContextValue

  /*--------------------------------------------------------------------------------
   * Implementations from interface HTMLCheckerBackend
   *--------------------------------------------------------------------------------
   */

  public String getCheckerAttrValue(String name)
  {
    if (name=="ANCHORTAIL")
      return config.getAnchorTail();

    throw new IllegalArgumentException("attribute \"" + name + "\" is not defined");

  } // end getCheckerAttrValue

  public void sendTagMessage(String msg)
  {
    if (msg=="NOBR")
    { // increment the no-break count
      nobreak_count++;
      return;

    } // end if

    if (msg=="/NOBR")
    { // decrement the no-break count
      nobreak_count--;
      return;

    } // end if

    if (msg=="WBR")
    { // trigger a word break
      trigger_WBR = true;
      return;

    } // end if

    throw new IllegalArgumentException("message \"" + msg + "\" is not defined");

  } // end sendTagMessage

  public Object getCheckerContextValue(String name)
  {
    return context_data.get(name);

  } // end getCheckerContextValue

  /*--------------------------------------------------------------------------------
   * Implementations from interface RewriterServices
   *--------------------------------------------------------------------------------
   */

  public String getRewriterAttrValue(String name)
  {
    return getCheckerAttrValue(name);

  } // end getRewriterAttrValue

  public Object getRewriterContextValue(String name)
  {
    return context_data.get(name);

  } // end getRewriterContextValue

} // end class HTMLCheckerImpl
