/*
 * Copyright (c) 2008, Aaron Digulla
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 * 
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in
 *       the documentation and/or other materials provided with the
 *       distribution.
 *     * Neither the name of Aaron Digulla nor the names of its
 *       contributors may be used to endorse or promote products derived
 *       from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 */
package de.pdark.decentxml.dtd;

import de.pdark.decentxml.Token;
import de.pdark.decentxml.XMLParseException;
import de.pdark.decentxml.XMLSource;
import de.pdark.decentxml.XMLTokenizer;

/** A parser for a DTD (inline or reference). */
public class DTDTokenizer extends XMLTokenizer
{
    /** >= 0 as long as we're inside of a DOCTYPE */
    protected int docTypeLevel;

    /** Create an DTD tokenizer. startPosition must point at "<!DOCTYPE" in the source. */
    public DTDTokenizer (XMLSource source, int startPosition)
    {
        super (source);
        this.pos = startPosition;
    }
    
    /** The current position in the XML source */
    public int getOffset ()
    {
        return pos;
    }
    
    /** Fetch the next token from the source. Returns <code>null</code> if
     * there the complete doctype declaration has been read.
     * 
     * <p>If {@code next()} returns {@code null}, the position of the
     * tokenizer will be just after the end of the DTD declaration.
     * 
     * @return The next token or <code>null</code> if the DTD has been read.
     */
    public Token next ()
    {
        if (pos >= source.length () || docTypeLevel < 0)
            return null;

        Token token = createToken ();
        
        char c = source.charAt (pos);
        switch (c)
        {
        case '[':
            pos ++;
            token.setType (Type.DOCTYPE_BEGIN_SUBSET);
            docTypeLevel ++;
            break;

        case ']':
            pos ++;
            token.setType (Type.DOCTYPE_END_SUBSET);
            docTypeLevel --;
            break;

        case '(':
            pos ++;
            token.setType (Type.DOCTYPE_BEGIN_GROUP);
            docTypeLevel ++;
            break;
            
        case ')':
            pos ++;
            token.setType (Type.DOCTYPE_END_GROUP);
            docTypeLevel --;
            break;
            
        case '?':
            pos ++;
            token.setType (Type.DOCTYPE_ZERO_OR_ONE);
            break;
            
        case '*':
            pos ++;
            token.setType (Type.DOCTYPE_ZERO_OR_MORE);
            break;
            
        case '+':
            pos ++;
            token.setType (Type.DOCTYPE_ONE_OR_MORE);
            break;
            
        case '|':
            pos ++;
            token.setType (Type.DOCTYPE_ALTERNATIVE);
            break;
            
        case ',':
            pos ++;
            token.setType (Type.DOCTYPE_SEQUENCE);
            break;
            
        case '<':
            pos ++;
            parseDocTypeMarkupDeclaration (token);
            if (token.getType () != Type.DOCTYPE)
                docTypeLevel ++;
            break;

        case '>':
            pos ++;
            token.setType (Type.DOCTYPE_END);
            docTypeLevel --;
            break;

        case '"':
        case '\'':
            pos ++;
            parseDocTypeQuotedText (token);
            break;

        case '-':
            pos ++;
            parseDocTypeComment (token);
            break;
            
        case '#':
            pos ++;
            parseDocTypeConstant (token);
            break;
            
        case '%':
            pos ++;
            token.setType (Type.DOCTYPE_PARAMETER_ENTITY);
            break;
            
        case ';':
            pos ++;
            token.setType (Type.DOCTYPE_PARAMETER_ENTITY_END);
            break;
            
        default:
            if (Character.isWhitespace (c))
            {
                token.setType (Type.DTD_WHITESPACE);
                skipWhiteSpace ();
            }
            else
            {
                pos ++;
                parseDocTypeText (token);
            }
        break;
        }
        
        token.setEndOffset (pos);
        return token;
    }
    
    /** #implied, #pcdata, ... */
    protected void parseDocTypeConstant (Token token)
    {
        int errorOffset = pos - 1;
        
        char c = 0;
        if (pos < source.length ())
            c = source.charAt (pos);
        
        String expected = null;
        switch (c)
        {
        case 'i':
        case 'I':
            expected = "#IMPLIED";
            token.setType (Type.DOCTYPE_IMPLIED);
            break;
            
        case 'p':
        case 'P':
            expected = "#PCDATA";
            token.setType (Type.DOCTYPE_PCDATA);
            break;
        
        case 'r':
        case 'R':
            expected = "#REQUIRED";
            token.setType (Type.DOCTYPE_REQUIRED);
            break;
            
        case 'f':
        case 'F':
            expected = "#FIXED";
            token.setType (Type.DOCTYPE_FIXED);
            break;
            
        }
        
        String s = null;
        if (expected != null)
        {
            int pos2 = errorOffset + expected.length ();
            if (pos2 < source.length ())
            {
                pos = pos2;
                s = source.substring (errorOffset, pos2);
            }
        }
        
        if (expected == null || s == null)
            throw new XMLParseException ("Expected '#IMPLIED' or '#PCDATA'"+lookAheadForErrorMessage ("but found", errorOffset, 20), source, errorOffset);
        
        if (!expected.equalsIgnoreCase (s))
            throw new XMLParseException ("Expected '"+expected+"'", source, errorOffset);
    }

    protected void parseDocTypeComment (Token token)
    {
        expect ('-');
        
        while (pos < source.length ())
        {
            char c = source.charAt (pos);
            if (c == '-')
            {
                c = nextChar ("Expected '--'");
                if (c == '-')
                {
                    pos ++;
                    break;
                }
            }
            pos ++;
        }
        
        token.setType (Type.DOCTYPE_COMMENT);
    }

    protected void parseDocTypeText (Token token)
    {
        token.setType (Type.TEXT);

        pos --;
        while (pos < source.length () && getCharValidator ().isNameChar (source.charAt (pos)))
            pos ++;
        
        String s = source.substring (token.getStartOffset (), pos);
        if (s.length () == 0)
            throw new XMLParseException ("Expected some text"+lookAheadForErrorMessage ("but found", token.getStartOffset (), 20), token);
        
        // TODO How about "<!DOCTYPE SYSTEM ..."?
        if ("SYSTEM".equals (s))
            token.setType (Type.DOCTYPE_SYSTEM);
        else if ("PUBLIC".equals (s))
            token.setType (Type.DOCTYPE_PUBLIC);
        else if ("NDATA".equals (s))
            token.setType (Type.DOCTYPE_NDATA);
    }

    protected void parseDocTypeQuotedText (Token token)
    {
        token.setType (Type.DOCTYPE_QUOTED_TEXT);
        int errorPos = pos - 1;
        char quoteChar = source.charAt (errorPos);
        boolean insideEntity = false;
        int entityStartPos = pos - 1;
        
        while (pos < source.length ())
        {
            char c = source.charAt (pos);
            if (c == quoteChar) break;
            
            if (c == '&')
            {
                entityStartPos = pos;
                insideEntity = true;
            }
            else if (c == ';')
            {
                if (insideEntity)
                {
                    verifyEntity (entityStartPos, pos + 1);
                }
                
                insideEntity = false;
            }
            else
            {
                String msg = getCharValidator ().isValid (source, pos);
                if (msg != null)
                    throw new XMLParseException ("Illegal character found in quoted text. "+msg, source, pos);
            }
            
            skipChar (c);
        }
        
        if (insideEntity)
        {
            throw new XMLParseException ("Missing ';' after '&': "+lookAheadForErrorMessage (null, entityStartPos, 20), source, entityStartPos);
        }

        if (pos >= source.length ())
        {
            throw new XMLParseException ("Couldn't find closing quote", getSource (), errorPos);
        }
        else
        {
            // Skip closing quote
            pos ++;
        }
    }

    /** "<!DOCTYPE", "<!ELEMENT", "<!ATTLIST", "<!ENTITY", "<!--...-->" */
    protected void parseDocTypeMarkupDeclaration (Token token)
    {
        int errorPos = pos - 1;
        expect ('!');
        
        char c = nextChar ("Unexpeted end of file while reading doctype markup declaration");
        if (c  == 'E')
        {
            c = nextChar ("Unexpeted end of file while reading doctype markup declaration");
            if (c == 'L')
            {
                nextChars ("<!ELEMENT", errorPos, "Expected '<!ELEMENT'");
                token.setType (Type.DOCTYPE_ELEMENT);
            }
            else if (c == 'N')
            {
                nextChars ("<!ENTITY", errorPos, "Expected '<!ENTITY'");
                token.setType (Type.DOCTYPE_ENTITY);
            }
            else
                throw new XMLParseException ("Expected '<!ELEMENT' or '<!ENTITY'", source, errorPos);
        }
        else if (c == 'A')
        {
            nextChars ("<!ATTLIST", errorPos, "Expected '<!ATTLIST'");
            token.setType (Type.DOCTYPE_ATTLIST);
        }
        else if (c == 'N')
        {
            nextChars ("<!NOTATION", errorPos, "Expected '<!NOTATION'");
            token.setType (Type.DOCTYPE_NOTATION);
        }
        else if (c == '-') // Comment
        {
            // Very ugly; the level will be incremented in the calling routine :(
            docTypeLevel --;
            parseComment (token);
            return;
        }
        else if (c == 'D')
        {
            nextChars ("<!DOCTYPE", errorPos, "Expected '<!DOCTYPE'");
            token.setType (Type.DOCTYPE);
        }
        else
            throw new XMLParseException ("Expected '<!ATTLIST', '<!DOCTYPE', '<!ELEMENT' or '<!ENTITY'", source, errorPos);
    }


}
