// -*-Java-*-
// Copyright (c) 2001, 2002  Per M.A. Bothner and Brainfood Inc.
// This is free software;  for terms and warranty disclaimer see ./COPYING.

package gnu.xml;
import java.io.*;
import java.net.*;

/** Reads XML from a @CHAR@ array.
 * Assumes a state-less character encoding containing ascii as a sub-set,
 * and where no byte in a multi-byte character is the same as a xml special
 * character.  Any bytes with high-order bit set are treated as if they
 * are letters, and can be part of names.
 *
 * Handles CR/LF, CDATA, entity references, processing instructions, DOCTYPE,
 * as well as the obvious (text, element, and attributes).
 *
 * @author Per Bothner
 */

public class @XMLParser@
{
  protected @CHAR@[] buffer;
  protected int pos;
  protected int limit;

  @ParsedXMLHandler@ out;

  private static final int EXPECT_NAME_MODIFIER = 1;
  private static final int SKIP_SPACES_MODIFIER = 2;
  private static final int INIT_STATE = 0;
  private static final int TEXT_STATE = 1;
  private static final int BEGIN_ELEMENT_STATE = 2;
  private static final int END_ELEMENT_STATE = 4;
  private static final int SAW_ENTITY_REF = 6;  // Saw '&'.  
  private static final int ATTRIBUTE_SEEN_NAME_STATE = 8;
  private static final int MAYBE_ATTRIBUTE_STATE = 10;
  private static final int ATTRIBUTE_SEEN_EQ_STATE = 11;
  private static final int DOCTYPE_SEEN_STATE = 13;
  private static final int DOCTYPE_NAME_SEEN_STATE = 16;
  private static final int SAW_LEFT_STATE = 14;
  private static final int SAW_LEFT_SLASH_STATE = 19; // Seen '</'
  private static final int SAW_LEFT_EXCL_STATE = 20;
  private static final int SAW_LEFT_QUEST_STATE = 21; // Seen '<?'
  private static final int SAW_LEFT_EXCL_MINUS_STATE = 22;
  private static final int SAW_AMP_STATE = 25;  // Saw '&'.  
  private static final int SAW_AMP_SHARP_STATE = 26;  // Saw '&#'.  
  private static final int EXPECT_RIGHT_STATE = 27;
  private static final int SAW_ERROR = 28;
  private static final int SAW_EOF_ERROR = 30;  // Unexpected end-of-file.

  public void parse()
  {
    // Cache fields in local variables, for speed.
    @CHAR@[] buffer = this.buffer;
    int pos = this.pos;
    int limit = this.limit;
    @ParsedXMLHandler@ out = this.out;
    
    // The flow logic of this method is unusual.  It is one big state machine,
    // but with two "subroutines": SKIP_SPACES_MODIFIER and EXPECT_NAME_MODIFIER.
    // There is also a "subroutine" to get a new character (and leave it in 'ch')
    // when 'break handleChar' is executed, except this has the hard-wired
    // continuation of switching on the 'state'.
    //
    // The justification for this rather usual design is performance.
    // As long as the input is contained within 'buffer', we don't need
    // to call input methods (only methods for emitting parsed data is
    // called).  We also maximize use of local variables - we do not
    // access any object fields (including fields of 'this') except
    // for getting the next char from 'buffer'.  These properties mean
    // this method can be compiled to very tight efficient code.

    int state = INIT_STATE;
    // 0: normal - in character context.
    // 1: seen '&'

    // The next two varibles are only relevant if state==INIT_STATE:
    @CHAR@ terminator = (@CHAR@) '<';
    int continue_state = SAW_LEFT_STATE;
    @CHAR@ ch = (@CHAR@) ' '; // ???
    int length = 0;
    int dstart = -1;
    String message = null;

    int start = limit;
  mainLoop:
    for (;;)
      {
        handleChar:  // When done get next character.
        switch (state)
          {
          case INIT_STATE:
            state = TEXT_STATE;
            break handleChar;

          case SAW_ERROR:
            this.pos = pos;
            error('e', message);
            for (;;)
              {
                if (pos >= limit)
                  break mainLoop;
                ch = buffer[pos++];
                if (ch == '>')
                  {
                    state = TEXT_STATE;
                    break handleChar;
                  }
              }

          case SAW_EOF_ERROR:
            this.pos = pos;
            error('f', "unexpected end-of-file");
            return;

          case TEXT_STATE:
            // This state handle text not inside tags (in which case
            // terminator=='<').  It also handles attribute values (in
            // which case terminator is '\'' or '"').
            start = pos - 1;
            length = pos;
            for (;;)
              {
                if (ch == terminator)
                  {
                    state = continue_state;
                    break;
                  }
                if (ch == '&')
                  {
                    state = SAW_AMP_STATE;
                    break;
                  }
                if (pos == limit)
                  {
                    length--;
                    break;
                  }
                ch = buffer[pos++];
              }
            length = pos - length;
            if (length > 0)
              out.emitCharacters(buffer, start, length);
	    start = buffer.length;
            break handleChar;

          case SKIP_SPACES_MODIFIER + EXPECT_RIGHT_STATE:
          case SKIP_SPACES_MODIFIER + MAYBE_ATTRIBUTE_STATE:
          case SKIP_SPACES_MODIFIER + SAW_LEFT_QUEST_STATE:
          case SKIP_SPACES_MODIFIER + DOCTYPE_SEEN_STATE:
            // "Subroutine" for skipping whitespace.
            if (ch == ' ' || ch == '\t'|| ch == '\n' || ch == '\r'
		|| ch == '\u0085' || ch == '\u2028')
              break handleChar;
            // Not a space, so "return" to next state.
            state -= SKIP_SPACES_MODIFIER;
            continue mainLoop;

          case EXPECT_NAME_MODIFIER + BEGIN_ELEMENT_STATE:
          case EXPECT_NAME_MODIFIER + END_ELEMENT_STATE:
          case EXPECT_NAME_MODIFIER + ATTRIBUTE_SEEN_NAME_STATE:
          case EXPECT_NAME_MODIFIER + SAW_ENTITY_REF:
          case EXPECT_NAME_MODIFIER + DOCTYPE_NAME_SEEN_STATE:
          case EXPECT_NAME_MODIFIER + SKIP_SPACES_MODIFIER + SAW_LEFT_QUEST_STATE:
            // "Subroutine" for reading a Name.
            for (;;)
              {
		// XML 1.1 candidate recommendation:
		// [2] Char    ::=    #x9 | #xA | #xD | [#x20-#x7E] | #x85
		//   | [#xA0-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
		// [4]  NameStartChar := ":" | [A-Z] | "_" | [a-z] |
		//   [#xC0-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] |
		//   [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] |
		//   [#x3001-#xD7FF] | [#xF900-#xEFFFF]
		// [4a] NameChar := NameStartChar | "-" | "." | [0-9] | #xB7 |
		//   [#x0300-#x036F] | [#x203F-#x2040]
                if ((ch >= 'a' && ch <= 'z') ||
		    (ch >= 'A' && ch <= 'Z') ||
		    ch == '_' || ch == ':' ||
		    (ch >= 0xC0 && (ch <= 0x2FF ||
				    (ch >= 0x370 &&
				     ((ch <= 0x1FFF && ch != 0x37E) ||
				      (ch >= 0x200C &&
				       (ch <= 0x200D ||
					(ch >= 0x2070 && ch <= 0x218F)||
					(ch >= 0x2C00 && ch <= 0x2FEF) ||
					(ch >= 0x3001 && ch <= 0xD7FF) ||
					(ch >= 0xF900 && ch <= 0xFFFD))))))) ||
		    (pos > start &&
		     (ch >= '0' && ch <= '9') ||
		      ch == '.' || ch == '-' ||
		     ch == 0xB7 ||
		     (ch > 0x300 &&
		      (ch <= 0x36F || (ch >= 0x203F && ch <= 0x2040)))))
		  {
                  }
                else
                  {
		    state -= EXPECT_NAME_MODIFIER;
		    length = pos - 1 - start;
		    if (length == 0)
		      {
			if (state == ATTRIBUTE_SEEN_NAME_STATE)
			  message = "missing attribute name";
			else
			  message = "missing name";
			state = SAW_ERROR;
		      }
                    continue mainLoop;
                  }
                if (pos < limit)
		  ch = buffer[pos++];
		else
		  break handleChar;
              }
          case SAW_AMP_SHARP_STATE:
	    for (;;)
	      {
		if (ch == ';')
		  {
		    out.emitCharacterReference(length,
					       buffer, start, pos-1-start);
		    state = TEXT_STATE;
		    break handleChar;
		  }
		if (ch == 'x' && dstart == 0)
		  dstart = 16;
		else if (length >= 0x8000000)
		  break; // Overflow likely.
		else
		  {
		    int base = dstart == 0 ? 10 : dstart;
		    int digit = Character.digit((char) ch, base);
		    if (digit < 0)
		      break;
		    length = length * base + digit;
		  }
                if (pos < limit)
		  ch = buffer[pos++];
		else
		  break handleChar;
	      }
	    message = "Invalid character reference";
	    state = SAW_ERROR;
	    continue mainLoop;

          case SAW_AMP_STATE:
            if (ch == '#')
              {
                state = SAW_AMP_SHARP_STATE;
		start = pos;
		length = 0;  // accumulated value; -1 means error, -2 overflow
		dstart = 0;  // base - 0 means not seen yet
                break handleChar;
              }
	    start = pos - 1;
            state = EXPECT_NAME_MODIFIER + SAW_ENTITY_REF;
            continue mainLoop;

          case SAW_ENTITY_REF:
            if (ch != ';')
              {
                this.pos = pos;
                error('w', "missing ';'");
              }
            out.emitEntityReference(buffer, start, length);
	    start = limit;
            state = TEXT_STATE;
            break handleChar;

          case SAW_LEFT_STATE: // Saw '<'
            if (ch == '/')
              {
                state = SAW_LEFT_SLASH_STATE;
                break handleChar;
              }
            if (ch == '?')
              {
		start = pos;
                state = EXPECT_NAME_MODIFIER + SKIP_SPACES_MODIFIER + SAW_LEFT_QUEST_STATE;
                break handleChar;
              }
            if (ch == '!')
              {
                state = SAW_LEFT_EXCL_STATE;
		start = pos;
                break handleChar;
              }
            // Read Name then goto BEGIN_ELEMENT_STATE.
	    start = pos - 1;
            state = EXPECT_NAME_MODIFIER + BEGIN_ELEMENT_STATE;
            continue mainLoop;
          case BEGIN_ELEMENT_STATE:
            this.pos = pos;
            out.emitBeginElement(buffer, start, length);
            state = SKIP_SPACES_MODIFIER + MAYBE_ATTRIBUTE_STATE;
	    start = limit;
            continue mainLoop;

          case SAW_LEFT_QUEST_STATE: // Seen '<?' Name Spaces
	    if (dstart < 0)
	      dstart = pos - 1;
            for (;;)
              {
		int end;
		if (ch == '>'
		    && buffer[end = pos - 2] == '?'
		    && end >= dstart)
		  {
		    out.emitProcessingInstruction(buffer, start, length,
						  dstart, end - dstart);
		    start = limit;
		    dstart = -1;
		    state = TEXT_STATE;
		    break handleChar;
		  }
                if (pos < limit)
		  ch = buffer[pos++];
		else
		  break handleChar;
              }

          case SAW_LEFT_EXCL_STATE: // Seen '<!'
	  exclLoop:
	    for (;;)
	      {
		if (ch == '>')
		  {
		    length = pos - 1 - start;
		    if (length >= 4
			&& buffer[start] == '-'
			&& buffer[start+1] == '-')
		      {
			if (buffer[pos-2] == '-'
			    && buffer[pos-3] == '-')
			  {
			    out.emitComment(buffer, start + 2, length - 4);
			    break exclLoop;
			  }
		      }
		    else if (length >= 6
			     && buffer[start] == '['
			     && buffer[start+1] == 'C'
			     && buffer[start+2] == 'D'
			     && buffer[start+3] == 'A'
			     && buffer[start+4] == 'T'
			     && buffer[start+5] == 'A'
			     && buffer[start+6] == '[')
		      {
			if (buffer[pos-2] == ']'
			    && buffer[pos-3] == ']')
			  {
			    out.emitCDATA(buffer, start + 7, pos - 10 - start);
			    break exclLoop;
			  }
		      }
		    else
		      {
			// FIXME ignoreing <!ELEMENT ... > etc.
			break exclLoop;
		      }
		  }
		else if (pos == start+7
			 &&  buffer[start] == 'D'
			 &&  buffer[start+1] == 'O'
			 &&  buffer[start+2] == 'C'
			 &&  buffer[start+3] == 'T'	
			 &&  buffer[start+4] == 'Y'
			 &&  buffer[start+5] == 'P'
			 &&  ch == 'E')
		  {
		    start = limit;
		    state = SKIP_SPACES_MODIFIER + DOCTYPE_SEEN_STATE;
		    break handleChar;
		  }
                if (pos < limit)
		  ch = buffer[pos++];
		else
		  break handleChar;
	      }
	    start = limit;
	    state = TEXT_STATE;
	    break handleChar;

          case DOCTYPE_SEEN_STATE:  /* Seen '<!DOCTYPE' S* */
	    state = EXPECT_NAME_MODIFIER + DOCTYPE_NAME_SEEN_STATE;
	    start = pos - 1;
	    continue mainLoop;

          case DOCTYPE_NAME_SEEN_STATE:  /* Seen '<!DOCTYPE' S* Name */
	    if (dstart < 0)
	      {
		dstart = pos - 1;
		terminator = 0;
	      }
            for (;;)
              {
		if (ch == '\'' || ch == '\"')
                  {
                    if (terminator == 0)
                      terminator = ch;
                    else if (terminator == ch)
                      terminator = 0;
                  }
                if (ch == '>' && terminator == 0)
                  {
                    out.emitDoctypeDecl(buffer, start, length,
                                        dstart, pos - 1 - dstart);
                    terminator = (@CHAR@) '<';
		    start = limit;
		    dstart = -1;
                    state = TEXT_STATE;
                    break handleChar;
                  }
                if (pos < limit)
		  ch = buffer[pos++];
		else
		  break handleChar;
              }

          case MAYBE_ATTRIBUTE_STATE:
            terminator = (@CHAR@) '<';
            continue_state = SAW_LEFT_STATE;
            if (ch == '/')
              {
                out.emitEndAttributes();
                out.emitEndElement(null, 0, 0);
                state = EXPECT_RIGHT_STATE;
                break handleChar;
              }
            if (ch == '>')
              {
                out.emitEndAttributes();
                state = TEXT_STATE;
                break handleChar;
              }
	    start = pos - 1;
            state = EXPECT_NAME_MODIFIER + ATTRIBUTE_SEEN_NAME_STATE;
            continue mainLoop;
          case ATTRIBUTE_SEEN_NAME_STATE:
            if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'
		|| ch == '\u0085' || ch == '\u2028')
              break handleChar;
            out.emitBeginAttribute(buffer, start, length);
	    start = limit;
            if (ch == '=')
              {
                state = ATTRIBUTE_SEEN_EQ_STATE;
                break handleChar;
              }
            message = "missing or misplaced '=' after attribute name";
            state = SAW_ERROR;
            continue mainLoop;
          case ATTRIBUTE_SEEN_EQ_STATE:
            if (ch == '\'' || ch == '\"')
              {
                terminator = ch;
                continue_state = SKIP_SPACES_MODIFIER + MAYBE_ATTRIBUTE_STATE;
                state = TEXT_STATE;
                break handleChar;
              }
            if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'
		|| ch == '\u0085' || ch == '\u2028')
              break handleChar;
            message = "missing or unquoted attribute value";
            state = SAW_ERROR;
            continue mainLoop;

          case SAW_LEFT_SLASH_STATE: // Seen '</'.
            // Do "Name" subroutine, then goto END_ELEMENT_STATE.
	    start = pos - 1;
            state = EXPECT_NAME_MODIFIER + END_ELEMENT_STATE;
            continue mainLoop;

          case END_ELEMENT_STATE:  // Seen '</' Name.
            this.pos = pos;
            out.emitEndElement(buffer, start, length);
	    start = limit;
            // Skip spaces then goto EXPECT_RIGHT_STATE.
            state = SKIP_SPACES_MODIFIER + EXPECT_RIGHT_STATE;
            continue mainLoop;
          
          case EXPECT_RIGHT_STATE: // Looking for '>'.
            if (ch != '>')
              {
                message = "missing '>'";
                state = SAW_ERROR;
                continue mainLoop;
              }
            state = TEXT_STATE;
            break handleChar;
          }

        // After 'break handleChar', we get here.
        if (pos < limit)
          ch = buffer[pos++];
        else
          {
	    int saved = pos - start;
	    int count = fill(buffer, start, pos);
	    if (count > 0)
	      {
		pos = this.pos;
		buffer = this.buffer;
		limit = pos + count;
		start = saved >= 0 ? pos - saved : limit;
		ch = buffer[pos++];
	      }
	    else
	      {
		if (state == TEXT_STATE)
		  return;
		state = SAW_EOF_ERROR;
	      }
          }
      }
  }

@if CHAR is byte@
  protected InputStream in;
@endif CHAR is byte@
@if CHAR is char@
  protected Reader in;
@endif CHAR is char@

  /** Fill the buffer with more data.
   * @param start start of current token.
   * @param pos index of current read position
   */
  public int fill(@CHAR@[] buffer, int start, int pos)
  {
    if (in == null)
      return -1;
    int saved = pos - start;
    if (saved > 0)
      {
	if (saved >= buffer.length)
	  {
	    @CHAR@[] tmp = new @CHAR@[saved+100];
	    System.arraycopy(buffer, start, tmp, 0, saved);
	    buffer = tmp;
	    this.buffer = tmp;
	  }
	else if (start > 0)
	  System.arraycopy(buffer, start, buffer, 0, saved);
	pos = saved;
      }
    else
      {
	pos = 0;
      }
    try
      {	
	this.pos = pos;
	return in.read(buffer, pos, buffer.length - pos);
      }
    catch (java.io.IOException ex)
      {
	throw new RuntimeException(ex.getMessage());
      }
  }

@if CHAR is byte@
  public @XMLParser@(InputStream in, @ParsedXMLHandler@ out)
  {
    this.in = in; 
    buffer = new byte[1024];
    pos = 0;
    limit = 0;

    this.out = out;
  }
@endif CHAR is byte@

@if CHAR is char@
  public @XMLParser@(Reader in, @ParsedXMLHandler@ out)
  {
    this.in = in;
    buffer = new char[1024];
    pos = 0;
    limit = 0;

    this.out = out;
  }
@endif CHAR is char@

  public @XMLParser@(@CHAR@[] buffer, int pos, int limit,
		     @ParsedXMLHandler@ out)
  {
    this.buffer = buffer;
    this.pos = pos;
    this.limit = limit;
    this.out = out;
  }

  public @XMLParser@(URL url, @ParsedXMLHandler@ out)  throws IOException
  {
    URLConnection conn = url.openConnection();
    InputStream in = conn.getInputStream();
@if CHAR is byte@
    this.in = in; 
    buffer = new byte[1024];
@endif CHAR is byte@
@if CHAR is char@
    this.in = new InputStreamReader(in);
    buffer = new char[1024];
@endif CHAR is char@
    pos = 0;
    limit = 0;

    this.out = out;
  }

  public void error(char severity, String message)
  {
    out.error(this, message);
  }
}
