/*
 * 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) 2003 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.dynamo.htmlcheck;

import java.io.*;
import java.util.*;
import java.util.regex.*;
import org.apache.log4j.Logger;
import com.silverwrist.dynamo.except.*;
import com.silverwrist.dynamo.htmlcheck.tags.*;
import com.silverwrist.dynamo.iface.*;
import com.silverwrist.dynamo.util.*;

class CheckerObject implements HTMLChecker, HTMLRewriterSite, HTMLTagSite
{
  /*--------------------------------------------------------------------------------
   * Internal class implementing counters for rewriters
   *--------------------------------------------------------------------------------
   */

  private static class CountingRewriter implements HTMLRewriter
  {
    /*====================================================================
     * Attributes
     *====================================================================
     */

    private HTMLRewriter m_inner;
    private int m_count = 0;

    /*====================================================================
     * Constructor
     *====================================================================
     */

    CountingRewriter(HTMLRewriter inner)
    {
      m_inner = inner;

    } // end constructor

    /*====================================================================
     * Implementations from interface HTMLRewriter
     *====================================================================
     */

    public QualifiedNameKey getName()
    {
      return m_inner.getName();

    } // end getName

    public MarkupData rewrite(String data, String prefix, String suffix, HTMLRewriterSite site)
    {
      MarkupData rc = m_inner.rewrite(data,prefix,suffix,site);
      if (rc!=null)
	m_count++;
      return rc;

    } // end rewrite

    /*====================================================================
     * External operations
     *====================================================================
     */

    int getCount()
    {
      return m_count;

    } // end getCount

    void reset()
    {
      m_count = 0;

    } // end reset

  } // end class CountingRewriter

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

  private static Logger logger = Logger.getLogger(CheckerObject.class);

  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;
  private static final short ST_BRACKET = 7;
  private static final short ST_BRACE = 8;

  private static final int MARGIN_SLOP = 5;

  private static Pattern P_INITIAL_COMMENT;
  private static Pattern P_ALL_COMMENT;
  private static Pattern P_OPEN_HTML;
  private static Pattern P_CLOSE_HTML;
  private static Pattern P_XML_CONSTRUCT;

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

  private FrozenProfileObject m_profile;            // the profile object we use
  private MemoryObjectStore m_objstore;             // memory object store (for context data)
  private boolean m_started = false;                // have we started the current parse?
  private boolean m_finished = false;               // have we finished the current parse?
  private short m_state = ST_WHITESPACE;            // current parser state
  private short m_columns = 0;                      // current number of columns
  private int m_lines = 0;                          // current number of lines
  private int m_nobreak_count = 0;                  // "no-break" count
  private int m_nesting = 0;                        // nesting level
  private char m_quote_char = '\0';                 // quote match character
  private boolean m_wordbreak = false;              // word break triggered?
  private StringBuffer m_output_buffer = null;      // the output buffer containing the checked text
  private StringBuffer m_state_buffer = null;       // intra-state buffer containing processed text
  private ArrayList m_tag_stack = null;             // HTML tag stack
  private Reader m_do_reader = null;                // reader data to insert
  private HashMap m_counters = new HashMap();       // defined counters on rewriters
  private HTMLRewriter[] m_string_rewriters;        // the string rewriters
  private HTMLRewriter[] m_word_rewriters;          // the word rewriters
  private HTMLRewriter[] m_tag_rewriters;           // the tag rewriters
  private HTMLRewriter[] m_paren_rewriters;         // the parenthesis rewriters
  private HTMLRewriter[] m_bracket_rewriters;       // the bracket rewriters
  private HTMLRewriter[] m_brace_rewriters;         // the brace rewriters

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

  CheckerObject(FrozenProfileObject profile)
  {
    m_profile = profile;
    m_objstore = new MemoryObjectStore("HTML Checker");
    m_string_rewriters = processRewriterList(profile.getStringRewriters());
    m_word_rewriters = processRewriterList(profile.getWordRewriters());
    m_tag_rewriters = processRewriterList(profile.getTagRewriters());
    m_paren_rewriters = processRewriterList(profile.getParenRewriters());
    m_bracket_rewriters = processRewriterList(profile.getBracketRewriters());
    m_brace_rewriters = processRewriterList(profile.getBraceRewriters());

  } // end constructor

  /*--------------------------------------------------------------------------------
   * Internal operations
   *--------------------------------------------------------------------------------
   */

  /**
   * 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.isLetter(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

  private static final boolean isHTMLComment(String s)
  {
    return P_INITIAL_COMMENT.matcher(s).matches();

  } // end isHTMLComment

  private static final boolean isCompleteHTMLComment(String s)
  {
    return P_ALL_COMMENT.matcher(s).matches();

  } // end isCompleteHTMLComment

  private static final boolean isXMLConstruct(String s)
  {
    return P_XML_CONSTRUCT.matcher(s).matches();

  } // end isXMLConstruct

  private final HTMLRewriter[] processRewriterList(HTMLRewriter[] list)
  {
    boolean nocopy = true;
    int i;
    for (i=0; nocopy && (i<list.length); i++)
      nocopy = (list[i].getName()==null);
    if (nocopy)
      return list;
    HTMLRewriter[] rc = new HTMLRewriter[list.length];
    for (i=0; i<list.length; i++)
    { // get the key...
      QualifiedNameKey key = list[i].getName();
      if (key==null)
	rc[i] = list[i];
      else
      { // wrap this in a CountingRewriter
	CountingRewriter cr = new CountingRewriter(list[i]);
	m_counters.put(key,cr);
	rc[i] = cr;

      } // end else

    } // end for

    return rc;

  } // end processRewriterList

  /**
   * Initializes the internal state of the parser.
   */
  private final void initState()
  {
    m_output_buffer = new StringBuffer(1024);
    m_state_buffer = new StringBuffer(64);
    m_tag_stack = new ArrayList();

  } // end initState

  /**
   * Erases any temporary data that is no longer needed after the parser finishes.
   */
  private final void killState()
  {
    m_state_buffer = null;
    if (m_tag_stack.size()>0)
      m_tag_stack.clear();
    m_tag_stack = null;

  } // end killState

  /**
   * Use the specified list of rewriters to attempt to rewrite the specified string data. The first rewriter in the
   * list that returns a valid {@link com.silverwrist.dynamo.htmlcheck.MarkupData MarkupData} object takes precedence.
   *
   * @param rewriters Array 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(HTMLRewriter[] rewriters, String data, String prefix, String suffix)
  {
    MarkupData rc = null;
    for (int i=0; (rc==null) && (i<rewriters.length); i++)
      rc = rewriters[i].rewrite(data,prefix,suffix,this);
    return rc;

  } // end attemptRewrite

  /**
   * 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 Array 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 emit(char ch, HTMLOutputFilter[] filters, boolean count_cols)
  {
    boolean handled = false;
    if (filters.length>0)
    { // look through all the output filters to see what we can do
      for (int i=0; !handled && (i<filters.length); i++)
	handled = filters[i].tryOutputCharacter(m_output_buffer,ch);

    } // end if

    if (!handled)  // output the character
      m_output_buffer.append(ch);
    if (count_cols && (m_profile.getWordWrapLength()>0))
      m_columns++;  // adjust column indicator

  } // end emit

  /**
   * Emits a character string to the output of the HTML checker, running it through a list of defined filters.
   *
   * @param s String to output.
   * @param filters Array 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 emit(String s, HTMLOutputFilter[] filters, boolean count_cols)
  {
    if ((s==null) || (s.length()==0))
      return;

    boolean real_count_cols = (count_cols && (m_profile.getWordWrapLength()>0));

    if (filters.length==0)
    { // if there are no filters, just append the entire string directly
      m_output_buffer.append(s);
      if (real_count_cols)
	m_columns += (short)(s.length());
      return;

    } // end if

    String temp = s;
    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?)
      int stopper = -1;
      for (int i=0; (output_len>0) && (i<filters.length); i++)
      { // look to find the length of characters that doesn't match this filter
	int lnm = filters[i].lengthNoMatch(temp);
	if ((lnm>=0) && (lnm<output_len))
	{ // we've found a new stopper - record the length and the filter
	  output_len = lnm;
	  stopper = i;

	} // end if

      } // end for

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

      } // end if

      if (stopper>=0)
      { // one of the output filters stopped us, so invoke it
	char tmpch = temp.charAt(output_len++);
	if (!(filters[stopper].tryOutputCharacter(m_output_buffer,tmpch)))
	  m_output_buffer.append(tmpch);
	if (real_count_cols)
	  m_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 emit

  /**
   * Emits a line break to the output of the HTML checker, resetting the column counter and advancing
   * the line counter.
   */
  private final void emitLineBreak()
  {
    emit("\r\n",m_profile.getRawOutputFilters(),false);
    if (m_profile.getWordWrapLength()>0)
      m_columns = 0;
    m_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()
  {
    short wwl = m_profile.getWordWrapLength();
    if ((wwl>0) && (m_nobreak_count<=0) && (m_columns>=wwl))
      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)
  {
    short wwl = m_profile.getWordWrapLength();
    if ((wwl>0) && (m_nobreak_count<=0))
    { // line break might be required here
      int remain_space = (int)(wwl - m_columns);
      if (remain_space<nchars)
	emitLineBreak();
      
    } // end if

  } // end ensureSpaceOnLine

  private final void emit(MarkupData md)
  {
    ensureSpaceOnLine(md.getTotalLength());
    HTMLOutputFilter[] raw_filters = m_profile.getRawOutputFilters();
    HTMLOutputFilter[] cooked_filters = m_profile.getOutputFilters();
    if (md.getPrefix()!=null)
      emit(md.getPrefix(),cooked_filters,true);
    if (md.getOpenTag()!=null)
      emit(md.getOpenTag(),raw_filters,false);
    if (md.getBody()!=null)
      emit(md.getBody(),cooked_filters,true);
    if (md.getCloseTag()!=null)
      emit(md.getCloseTag(),raw_filters,false);
    if (md.getSuffix()!=null)
      emit(md.getSuffix(),cooked_filters,true);

  } // end emit

  private final boolean handleHTML(String tag)
  {
    m_wordbreak = false;

    // Get the tag name and whether or not it's a closing tag.
    String tagname = null;
    boolean is_closing = false;
    Matcher openmatch = P_OPEN_HTML.matcher(tag);
    Matcher closematch = P_CLOSE_HTML.matcher(tag);
    if (openmatch.matches())
      tagname = openmatch.group(1);
    else if (closematch.matches())
    { // matched the "close tag" sequence
      tagname = closematch.group(1);
      is_closing = true;

    } // end else if
    else  // not a valid open or close tag
      return false;

    TagsList tagslist = TagsList.get();
    if (tagname.length()>tagslist.getMaxTagLength())
      return false;  // tagname too long

    HTMLTag ht = tagslist.getTag(tagname);
    if (ht==null)
      return false;  // tag not found!

    if (is_closing && !(ht.allowClose()))
      return false;  // tag does not allow the "close" form

    if (!(m_profile.isAllowed(ht.getTagSet())))
      // this tag is not allowed!  but, if we're discarding ALL HTML tags or rejected HTML, then we're in the
      // clear...
      return (m_profile.getDiscardHTMLTags() || m_profile.getDiscardRejectedHTML());

    if (!(m_profile.getDiscardHTMLTags()) && ht.balanceTags())
    { // manipulate the tag stack to properly balance tags
      boolean valid = false;  // is the tag balance valid?
      if (is_closing)
      { // hunt backwards through the stack to find a valid open for this close
	int i = m_tag_stack.size();
	while ((--i)>=0)
	{ // look back through the stack...
	  HTMLTag foo = (HTMLTag)(m_tag_stack.get(i));
	  if (foo.equals(ht))
	  { // found a match!  remove the object from the tag stack
	    m_tag_stack.remove(i);
	    valid = true;
	    break;

	  } // end if

	} // end while

      } // end if
      else
      { // push this tag on the stack
	m_tag_stack.add(ht);
	valid = true;

      } // end else

      if (!valid)
	return false;  // tag couldn't balance properly - bug the hell out

    } // end if

    // Give the tag an opportunity to do its own rewriting.
    String out_tag = ht.rewrite(tag,is_closing,this);
    if ((out_tag==null) || m_profile.getDiscardHTMLTags())
      return true;  // tag erased, or discarded

    // Emit the actual tag.
    HTMLOutputFilter[] raw = m_profile.getRawOutputFilters();
    emit('<',raw,false);
    emit(out_tag,raw,false);
    emit('>',raw,false);

    // Does this tag cause a logical line break?
    boolean llb = false;
    if (m_wordbreak && !is_closing && (m_nobreak_count>0))
      llb = true;
    else
      llb = ht.causeLineBreak(is_closing);
    if (llb)
      m_columns = 0;  // now at the start of a new line

    return true;  // all handled!

  } // end handleHTML

  private final void doFlushChars()
  {
    if (m_state_buffer.length()>0)
    { // look to see if we can rewrite the string
      MarkupData md = attemptRewrite(m_string_rewriters,m_state_buffer.toString(),null,null);
      if (md==null)
      { // Break the big string into words and non-word characters.
	boolean first = true;
	while (m_state_buffer.length()>0)
	{ // get the initial run length of word/non-word characters
	  int sublen = getRunLength(m_state_buffer);
	  if (isWordChar(m_state_buffer.charAt(0)))
	  { // Strip off leading and trailing hyphens/apostrophes from the word.
	    int word_len = sublen;
	    int lead_len = 0, trail_len = 0;
	    final String hyph_apos = "'-";
	    while ((word_len>0) && (hyph_apos.indexOf(m_state_buffer.charAt(lead_len))>=0))
	    { // skip leading apostrophes/hyphens
	      lead_len++;
	      word_len--;

	    } // end while

	    while ((word_len>0) && (hyph_apos.indexOf(m_state_buffer.charAt(sublen - (trail_len + 1)))>=0))
	    { // skip trailing apostrophes/hyphens
	      trail_len++;
	      word_len--;

	    } // end while

	    if (lead_len>0)
	    { // emit leading apostrophes and hyphens
	      if (first && (lead_len<=MARGIN_SLOP))
		ensureSpaceOnLine(lead_len + word_len);  // keep initial run with word
	      else
		ensureSpaceOnLine(lead_len);
	      emit(m_state_buffer.substring(0,lead_len),m_profile.getOutputFilters(),true);
	      m_state_buffer.delete(0,lead_len);

	    } // end if

	    if (word_len>0)
	    { // get the word off the front of the buffer
	      String word = m_state_buffer.substring(0,word_len);
	      m_state_buffer.delete(0,word_len);

	      // try to rewrite the word
	      md = attemptRewrite(m_word_rewriters,word,null,null);
	      if (md==null)
	      { // output the word as usual
		ensureSpaceOnLine(word_len);
		emit(word,m_profile.getOutputFilters(),true);

	      } // end if
	      else // output the marked-up data
		emit(md);

	    } // end if

	    if (trail_len>0)
	    { // emit trailing apostrophes and hyphens
	      if ((trail_len<m_state_buffer.length()) || (trail_len>MARGIN_SLOP))
		ensureSpaceOnLine(trail_len);  // keep trailing apostrophe/hyphen with word in some cases
	      emit(m_state_buffer.substring(0,trail_len),m_profile.getOutputFilters(),true);
	      m_state_buffer.delete(0,trail_len);

	    } // end if

	  } // end if
	  else
	  { // output the substring of non-word characters
	    if ((sublen==m_state_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.
	      emit(m_state_buffer.toString(),m_profile.getOutputFilters(),true);
	      m_state_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<m_state_buffer.length()) && first && (sublen<=MARGIN_SLOP))
	    { // get the length of the word and ensure they can be kept together
	      int fw_len = getRunLength(m_state_buffer,sublen);
	      ensureSpaceOnLine(sublen + fw_len);

	    } // end if

	    // Output the whole string.
	    ensureSpaceOnLine(sublen);
	    emit(m_state_buffer.substring(0,sublen),m_profile.getOutputFilters(),true);
	    m_state_buffer.delete(0,sublen);

	  } // end else

	  first = false;

	} // end while

      } // end if
      else // write the markup data
	emit(md);

      m_state_buffer.setLength(0);  // clear the state buffer

    } // end if

  } // end doFlushChars

  private final short parseWhitespace(PushbackReader r) throws IOException
  {
    int ch = r.read();
    while ((ch!=-1) && Character.isWhitespace((char)ch) && !((ch=='\r') || (ch=='\n')))
    { // while we're still reading non-newline whitespace characters...
      m_state_buffer.append((char)ch);
      ch = r.read();

    } // end while

    if (ch==-1)
      return ST_WHITESPACE;  // end of input - stay in this state
    r.unread(ch);  // push back the character we just read

    // Flush our whitespace.
    int output_len = m_state_buffer.length();
    if (output_len>0)
    { // need to output some whitespace
      boolean force_line_break = false;
      short wwl = m_profile.getWordWrapLength();
      if ((wwl>0) && (m_nobreak_count<=0))
      { // adjust output if necessary for word-wrapping
	int remain_space = (int)(wwl - m_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();  // emit a line break
      if (output_len>0)   // emit the spaces
	emit(m_state_buffer.substring(0,output_len),m_profile.getOutputFilters(),true);
      m_state_buffer.setLength(0);

    } // end if

    // Determine the next state.
    if ((ch=='\r') || (ch=='\n'))
      return ST_NEWLINE;
    if ((ch=='<') && m_profile.getProcessAngles())
      return ST_LEFTANGLE;
    if ((ch=='(') && m_profile.getProcessParens())
    { // start parenthesis
      m_nesting = 0;
      return ST_PAREN;

    } // end if

    if ((ch=='[') && m_profile.getProcessBrackets())
    { // start bracket
      m_nesting = 0;
      return ST_BRACKET;

    } // end if

    if ((ch=='{') && m_profile.getProcessBraces())
    { // start brace
      m_nesting = 0;
      return ST_BRACE;

    } // end if

    return ST_CHARS;

  } // end parseWhitespace

  private final short parseChars(PushbackReader r) throws IOException
  {
    String openers = m_profile.getOpeners(); 
    int ch = r.read();
    while ((ch!=-1) && !(Character.isWhitespace((char)ch)) && (openers.indexOf((char)ch)<0))
    { // while we're still reading non-newline whitespace characters...
      if (ch=='\\')
      { // allow backslash to escape the opening character of a special sequence
	int ch2 = r.read();
	if ((ch2!=-1) && (openers.indexOf((char)ch2)>=0))
	  m_state_buffer.append((char)ch2);
	else
	{ // append the backslash and push back the second character we read
	  m_state_buffer.append('\\');
	  if (ch2!=-1)
	    r.unread(ch2);

	} // end else

      } // end if
      else // append the character
	m_state_buffer.append((char)ch);
      ch = r.read();

    } // end while

    if (ch==-1)
      return ST_CHARS;  // end of input - stay in this state
    r.unread(ch);  // push back the character we just read

    // Flush the characters we've just read.
    doFlushChars();

    // Determine the next state.
    if ((ch=='\r') || (ch=='\n'))
      return ST_NEWLINE;
    if ((ch=='<') && m_profile.getProcessAngles())
      return ST_LEFTANGLE;
    if ((ch=='(') && m_profile.getProcessParens())
    { // start parenthesis
      m_nesting = 0;
      return ST_PAREN;

    } // end if

    if ((ch=='[') && m_profile.getProcessBrackets())
    { // start bracket
      m_nesting = 0;
      return ST_BRACKET;

    } // end if

    if ((ch=='{') && m_profile.getProcessBraces())
    { // start brace
      m_nesting = 0;
      return ST_BRACE;

    } // end if

    return ST_WHITESPACE;

  } // end parseChars

  private final short parseLeftAngle(PushbackReader r) throws IOException
  {
    // Read off the initial left angle bracket.
    int ch = r.read();
    assert (ch=='<');

    for (;;)
    { // look for the character following the angle bracket
      ch = r.read();
      if ((ch==-1) || (Character.isWhitespace((char)ch)))
      { // switch back to whitespace mode
	emit('<',m_profile.getOutputFilters(),true);
	if (ch!=-1)
	  r.unread(ch);
	return ST_WHITESPACE;

      } // end if
      else if (ch=='<')  // left angle - emit it and stay in this mode
	emit('<',m_profile.getOutputFilters(),true);
      else
	break;  // found the start of a tag

    } // end for (ever)

    // now hitting the start of the tag
    r.unread(ch);
    return ST_TAG;

  } // end parseLeftAngle

  private final short parseTag(PushbackReader r) throws IOException
  {
    int ch = r.read();
    while ((ch!=-1) && (ch!='>'))
    { // append characters until we hit the end of the tag
      m_state_buffer.append(ch);
      if ((ch=='"') || (ch=='\''))
      { // we're inside a quote - do your number
	m_quote_char = (char)ch;
	return ST_TAGQUOTE;

      } // end if

      ch = r.read();

    } // end while

    if (ch==-1)
      return ST_TAG;  // remain in same state
    assert (ch=='>');

    // Lift the completed tag out of the buffer.
    String the_tag = m_state_buffer.toString();
    m_state_buffer.setLength(0);

    if (the_tag.length()==0)
    { // pretend we've already read the <> and click to the characters state
      m_state_buffer.append("<>");
      return ST_CHARS;

    } // end if

    // see if this is a comment
    if (isHTMLComment(the_tag))
    { // must be complete if we are to process it
      if (!isCompleteHTMLComment(the_tag))
      { // dump tag back into the buffer and try again
	m_state_buffer.append(the_tag).append('>');
	return ST_TAG;

      } // end if

      if (!(m_profile.getDiscardHTMLComments()))
      { // emit the comment
	HTMLOutputFilter[] raw = m_profile.getRawOutputFilters();
	emit('<',raw,false);
	emit(the_tag,raw,false);
	emit('>',raw,false);

      } // end if

      return ST_WHITESPACE;  // treat the comment as if it were whitespace

    } // end if

    // See if the tag can be handled as valid HTML.
    if (handleHTML(the_tag))
      return ST_WHITESPACE;

    // See if one of our tag rewriters will handle it.
    MarkupData md = attemptRewrite(m_tag_rewriters,the_tag,"<",">");
    if (md!=null)
    { // emit the "tag" and click to whitespace mode
      emit(md);
      return ST_WHITESPACE;

    } // end if

    if (m_profile.getDiscardXMLConstructs() && isXMLConstruct(the_tag))
      return ST_WHITESPACE;  // discard the XML constructs

    // Tag has been rejected...fake the initial character in the buffer, push back the last character, arrange the
    // rejected string as a new StringReader
    m_state_buffer.append('<');
    r.unread('>');
    m_do_reader = new StringReader(the_tag);
    return ST_CHARS;

  } // end parseTag

  private final short parseDelimited(PushbackReader r, short state, HTMLRewriter[] rewriters, String prefix,
				     String suffix) throws IOException
  {
    char start_char = prefix.charAt(0);
    char end_char = suffix.charAt(0);

    // Read off the initial delimiter.
    int ch = r.read();
    assert (((char)ch)==start_char);

    ch = r.read();
    while (ch!=-1)
    { // determine how to append data to the buffer
      if (start_char==(char)ch)
      { // open nesting
	m_state_buffer.append((char)ch);
	m_nesting++;

      } // end if
      else if (end_char==(char)ch)
      { // close nesting, maybe close the sequence
	if (m_nesting==0)
	  break;
	m_state_buffer.append((char)ch);
	m_nesting--;

      } // end else if
      else // just append the character to the buffer
	m_state_buffer.append((char)ch);

      ch = r.read();

    } // end while

    if (ch==-1)
      return state;  // keep it in the same state
    assert (((char)ch)==end_char);

    String data = m_state_buffer.toString();  // lift the data clear
    m_state_buffer.setLength(0);

    // can we rewrite this string?
    MarkupData md = attemptRewrite(rewriters,data,prefix,suffix);
    if (md!=null)
    { // output the marked-up data and click to whitespace mode
      emit(md);
      m_nesting = 0;
      return ST_WHITESPACE;

    } // end if

    // fake the initial character in the buffer, push back the last character, arrange the rejected string
    // as a new StringReader
    m_state_buffer.append(start_char);
    r.unread(ch);
    if (data.length()>0)
      m_do_reader = new StringReader(data);
    m_nesting = 0;
    return ST_CHARS;

  } // end parseDelimited

  private final short parseTagQuote(PushbackReader r) throws IOException
  {
    for (;;)
    { // copy characters across, scanning for EOF and the quote character
      int ch = r.read();
      if (ch==-1)
	return ST_TAGQUOTE;
      m_state_buffer.append((char)ch);
      if (m_quote_char==(char)ch)
	break;

    } // end for (ever)

    m_quote_char = '\0';
    return ST_TAG;

  } // end parseTagQuote

  private final short parseNewlines(PushbackReader r) throws IOException
  {
    int ch = r.read();
    while ((ch=='\r') || (ch=='\n'))
    { // append newlines to the state buffer
      m_state_buffer.append((char)ch);
      ch = r.read();

    } // end while

    if (ch==-1)
      return ST_NEWLINE;  // EOF - stay in this state
    r.unread(ch);  // pushback character

    if (m_state_buffer.length()>0)
    { // flush out all the newlines
      char[] arr = m_state_buffer.toString().toCharArray();
      m_state_buffer.setLength(0);

      int crs = 0, line_breaks = 0;
      for (int i=0; i<arr.length; i++)
      { // loop across the contents of the state buffer
	if (arr[i]=='\r')
	  crs++;
	else if (arr[i]=='\n')
	{ // found an actual line break
	  crs = 0;
	  line_breaks++;

	} // end else if

      } // end for

      if (crs>0)
	line_breaks++;

      if (m_profile.getRewrapLines())
      { // if we're rewrapping lines, do some fancy maneuvering
	if (line_breaks<2)
	{ // single line breaks become just spaces
	  m_state_buffer.append(' ');
	  return ST_WHITESPACE;

	} // end if
	else // compress all greater numbers down to 2 line breaks
	  line_breaks = 2;

      } // end if

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

    } // end if

    // Figure out the next state to go to.
    if (Character.isWhitespace((char)ch))
      return ST_WHITESPACE;
    if ((ch=='<') && m_profile.getProcessAngles())
      return ST_LEFTANGLE;
    if ((ch=='(') && m_profile.getProcessParens())
    { // start parenthesis
      m_nesting = 0;
      return ST_PAREN;

    } // end if

    if ((ch=='[') && m_profile.getProcessBrackets())
    { // start bracket
      m_nesting = 0;
      return ST_BRACKET;

    } // end if

    if ((ch=='{') && m_profile.getProcessBraces())
    { // start brace
      m_nesting = 0;
      return ST_BRACE;

    } // end if

    return ST_CHARS;

  } // end parseNewlines

  private final void parse(Reader r)
  {
    try
    { // wrap the reader in a PushbackReader
      PushbackReader pr = new PushbackReader(r,4);
      boolean looping = true;
      while (looping)
      { // loop around on the current state
	switch (m_state)
	{ // process for this state, get the next state
	  case ST_WHITESPACE:
	    m_state = parseWhitespace(pr);
	    break;

	  case ST_CHARS:
	    m_state = parseChars(pr);
	    break;

	  case ST_LEFTANGLE:
	    m_state = parseLeftAngle(pr);
	    break;

	  case ST_TAG:
	    m_state = parseTag(pr);
	    break;

	  case ST_PAREN:
	    m_state = parseDelimited(pr,ST_PAREN,m_paren_rewriters,"(",")");
	    break;

	  case ST_TAGQUOTE:
	    m_state = parseTagQuote(pr);
	    break;

	  case ST_NEWLINE:
	    m_state = parseNewlines(pr);
	    break;

	  case ST_BRACKET:
	    m_state = parseDelimited(pr,ST_BRACKET,m_bracket_rewriters,"[","]");
	    break;

	  case ST_BRACE:
	    m_state = parseDelimited(pr,ST_BRACE,m_brace_rewriters,"{","}");
	    break;

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

	} // end switch

	// If there's a "next" reader that's been set up, process the data it contains.
	Reader next = m_do_reader;
	m_do_reader = null;
	if (next!=null)
	  parse(next);

	// See if we're at the end of input.
	int ch = pr.read();
	if (ch==-1)
	  looping = false;  // end of input
	else
	  pr.unread(ch);

      } // end while

    } // end try
    catch (IOException e)
    { // just log the error...
      logger.error("IO error parsing HTML",e);

    } // end catch

  } // end parse

  private final void recoverPartialTag(char opener)
  {
    String data = m_state_buffer.toString();
    m_state_buffer.setLength(0);
    m_state_buffer.append(opener);
    m_state = ST_CHARS;
    m_nesting = 0;
    if (data.length()>0)
      parse(new StringReader(data));

  } // end recoverPartialTag

  /*--------------------------------------------------------------------------------
   * Implementations from interface ObjectProvider
   *--------------------------------------------------------------------------------
   */

  /**
   * Retrieves an object from this <CODE>ObjectProvider</CODE>.
   *
   * @param namespace The namespace to interpret the name relative to.
   * @param name The name of the object to be retrieved.
   * @return The object reference specified.
   */
  public Object getObject(String namespace, String name)
  {
    return m_objstore.getObject(namespace,name);

  } // end getObject

  /*--------------------------------------------------------------------------------
   * Implementations from interface ObjectStore
   *--------------------------------------------------------------------------------
   */

  /**
   * Sets an object into this <CODE>ObjectStore</CODE>.
   *
   * @param namespace The namespace to interpret the name relative to.
   * @param name The name of the object to be set.
   * @param value The object to set into the <CODE>ObjectStore</CODE>.
   * @return The previous object that was set into the <CODE>ObjectStore</CODE> under this namespace and name, or
   *         <CODE>null</CODE> if there was no such object.
   */
  public Object setObject(String namespace, String name, Object value)
  {
    return m_objstore.setObject(namespace,name,value);

  } // end setObject

  /**
   * Removes an object from this <CODE>ObjectStore</CODE>.
   *
   * @param namespace The namespace to interpret the name relative to.
   * @param name The name of the object to be removed.
   * @return The previous object that was set into the <CODE>ObjectStore</CODE> under this namespace and name, or
   *         <CODE>null</CODE> if there was no such object.
   */
  public Object removeObject(String namespace, String name)
  {
    return m_objstore.removeObject(namespace,name);

  } // end removeObject

  /**
   * Returns a collection of all object namespaces that have been set into this <CODE>ObjectStore</CODE>.
   *
   * @return A <CODE>java.util.Collection</CODE> containing <CODE>String</CODE> objects specifying all the
   *         object namespaces.
   */
  public Collection getNamespaces()
  {
    return m_objstore.getNamespaces();

  } // end getNamespaces

  /**
   * Returns a collection of all object names that have been set into this <CODE>ObjectStore</CODE> under a given
   * namespace.
   *
   * @param namespace The namespace to look for names under.
   * @return A {@link java.util.Collection Collection} containing {@link java.lang.String String} objects specifying
   *         all the object names for this namespace.
   */
  public Collection getNamesForNamespace(String namespace)
  {
    return m_objstore.getNamesForNamespace(namespace);

  } // end getNamesForNamespace

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

  public void append(String str)
  {
    if (str!=null)
      this.append(new StringReader(str));

  } // end append

  public void append(Reader r)
  {
    if (m_finished)
      throw new AlreadyFinishedException();
    if (!m_started)
    { // initialize the parser state
      initState();
      m_started = true;

    } // end if

    parse(r);  // parse this reader's contents

  } // end append

  public void finish()
  {
    if (m_finished)
      throw new AlreadyFinishedException();
    if (!m_started)
    { // initialize the parser state
      initState();
      m_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 (m_state)
      {
	case ST_WHITESPACE:
	case ST_NEWLINE:
	  break;    // discard any whitespace or newlines at the end of output

	case ST_CHARS:
	  doFlushChars();  // flush the character buffer
	  break;

	case ST_LEFTANGLE:  // just emit a '<' character
	  ensureSpaceOnLine(1);
	  emit('<',m_profile.getOutputFilters(),true);
	  break;

	case ST_TAG:
	case ST_TAGQUOTE:
	  recoverPartialTag('<');
	  running = true;
	  break;

	case ST_PAREN:
	  recoverPartialTag('(');
	  running = true;
	  break;

	case ST_BRACKET:
	  recoverPartialTag('[');
	  running = true;
	  break;

	case ST_BRACE:
	  recoverPartialTag('{');
	  running = true;
	  break;

      } // end switch

    } while (running);  // end do

    // Now close all the HTML tags that were left open.
    for (int i=(m_tag_stack.size()-1); i>=0; i--)
    { // get each tag object off the stack and close it
      HTMLTag ht = (HTMLTag)(m_tag_stack.get(i));
      m_output_buffer.append(ht.makeClosingTag());

    } // end for

    // deallocate some excess crap and mark the object as finished
    killState();
    m_lines++;
    m_finished = true;

  } // end finish

  public void reset()
  {
    // Reset all the items on our "worry list."
    m_started = false;
    m_finished = false;
    m_state = ST_WHITESPACE;
    m_columns = 0;
    m_lines = 0;
    m_nobreak_count = 0;
    m_nesting = 0;
    m_quote_char = '\0';
    m_wordbreak = false;
    m_output_buffer = null;
    killState();

    // Also reset all the counters.
    Iterator it = m_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()
  {
    if (!m_finished)
      throw new NotYetFinishedException();
    return m_output_buffer.toString();

  } // end getValue

  public int getLength()
  {
    if (!m_finished)
      throw new NotYetFinishedException();
    return m_output_buffer.length();

  } // end getLength

  public int getLines()
  {
    if (!m_finished)
      throw new NotYetFinishedException();
    return m_lines;

  } // end getLines

  public int getCounter(String namespace, String name)
  {
    if (!m_finished)
      throw new NotYetFinishedException();
    CountingRewriter cr = (CountingRewriter)(m_counters.get(new QualifiedNameKey(namespace,name)));
    return (cr==null) ? 0 : cr.getCount();

  } // end getCounter

  /*--------------------------------------------------------------------------------
   * Implementations from interface HTMLRewriterSite
   *--------------------------------------------------------------------------------
   */

  public String getAttribute(String namespace, String name)
  {
    return m_profile.getAttribute(namespace,name);

  } // end getAttribute

  /*--------------------------------------------------------------------------------
   * Implementations from interface HTMLTagSite
   *--------------------------------------------------------------------------------
   */

  public void controlMessage(String message, Object data)
  {
    if (message.equals("NOBR"))
      m_nobreak_count++;
    else if (message.equals("/NOBR"))
      m_nobreak_count--;
    else if (message.equals("WBR"))
      m_wordbreak = true;
    else
      logger.warn("CheckerObject.controlMessage(): unknown message " + message);

  } // end controlMessage

  /*--------------------------------------------------------------------------------
   * Static initializer
   *--------------------------------------------------------------------------------
   */

  static
  {
    try
    { // pre-compile all our regex patterns
      P_INITIAL_COMMENT = Pattern.compile("^!--");
      P_ALL_COMMENT = Pattern.compile("^!--.*--$");
      P_OPEN_HTML = Pattern.compile("^([A-Za-z]+)(?:\\s+.*)?$");
      P_CLOSE_HTML = Pattern.compile("^/([A-Za-z]+)$");
      P_XML_CONSTRUCT = Pattern.compile("^/?[-\\w]+:[-\\w]+(?:\\s+.*)?$");

    } // end try
    catch (PatternSyntaxException pe)
    { // log an error, all we can do
      logger.fatal("Pattern compilation error in CheckerObject",pe);

    } // end catch

  } // end static initializer

} // end class CheckerObject
