/*
 * (C) Copyright IBM Corp. 1997-1998  All rights reserved.
 *
 * US Government Users Restricted Rights Use, duplication or
 * disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
 *
 * The program is provided "as is" without any warranty express or
 * implied, including the warranty of non-infringement and the implied
 * warranties of merchantibility and fitness for a particular purpose.
 * IBM will not be liable for any damages suffered by you as a result
 * of using the Program. In no event will IBM be liable for any
 * special, indirect or consequential damages or lost profits even if
 * IBM has been advised of the possibility of their occurrence. IBM
 * will not be liable for any third party claims against you.
 */

package com.ibm.xml.xpointer;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import com.ibm.xml.parser.XMLChar;

/**
 * XPointer parser.
 * (Based on <a href="http://www.w3.org/TR/1998/WD-xptr-19980303">WD-xptr-19980303</a>)
 *
 * @version 1.1
 * @author TAMURA Kent &lt;kent@trl.ibm.co.jp&gt;
 */
public class XPointerParser {
    static Hashtable s_keys = new Hashtable(14);
    static {
        s_keys.put(XPointer.literals[XPointer.ST_ROOT], new Integer(XPointer.ST_ROOT));
        s_keys.put(XPointer.literals[XPointer.ST_ORIGIN], new Integer(XPointer.ST_ORIGIN));
        s_keys.put(XPointer.literals[XPointer.ST_ID], new Integer(XPointer.ST_ID));
        s_keys.put(XPointer.literals[XPointer.ST_HTML], new Integer(XPointer.ST_HTML));
        s_keys.put(XPointer.literals[XPointer.ST_CHILD], new Integer(XPointer.ST_CHILD));
        s_keys.put(XPointer.literals[XPointer.ST_DESCENDANT], new Integer(XPointer.ST_DESCENDANT));
        s_keys.put(XPointer.literals[XPointer.ST_ANCESTOR], new Integer(XPointer.ST_ANCESTOR));
        s_keys.put(XPointer.literals[XPointer.ST_PRECEDING], new Integer(XPointer.ST_PRECEDING));
        s_keys.put(XPointer.literals[XPointer.ST_PSIBLING], new Integer(XPointer.ST_PSIBLING));
        s_keys.put(XPointer.literals[XPointer.ST_FOLLOWING], new Integer(XPointer.ST_FOLLOWING));
        s_keys.put(XPointer.literals[XPointer.ST_FSIBLING], new Integer(XPointer.ST_FSIBLING));
        s_keys.put(XPointer.literals[XPointer.ST_SPAN], new Integer(XPointer.ST_SPAN));
        s_keys.put(XPointer.literals[XPointer.ST_ATTR], new Integer(XPointer.ST_ATTR));
        s_keys.put(XPointer.literals[XPointer.ST_STRING], new Integer(XPointer.ST_STRING));
    }

    String xpointer;
    int index = 0;

    AbsTerm parseAbsTerm(int ide) throws XPointerParseException {
        String param = null;
        if ('(' != next()) {
            error("Syntax error: expect `(': "+left());
        } else {
            get();                              // '('
            if (XPointer.ST_ID == ide) {
                String name = getName();
                if (null == name)
                    error("Syntax error: expect ID name: "+left());
                else
                    param = name;
            } else if (XPointer.ST_HTML == ide) {
                param = getSkipLit();
            }
            if (')' != next()) {
                error("Syntax error: expect `)'");
            } else
                get();
        }
        return new AbsTerm(ide, param);
    }


    RelTerm parseRelTerm(int ide) throws XPointerParseException {
        if ('(' != next()) {
            error("Syntax error: expect `(': "+left());
            return null;
        } 

        boolean allp = false;
        int instance = -1;
        String nodename = null;
        int nodetype = XPointer.NT_NONE;
        Vector attributes = new Vector();
        get();                                  // '('
        String n = getName();
        if (null != n && XPointer.S_ALL.equals(n)) {
            allp = true;
        } else if (null != n) {
            error("Syntax error: [relterm] expect a name or number: "+n);
        } else
            instance = getDigit();
        if (',' == next()) {
            get();                              // ','
                                                // Nodetype (','|')')
            if ('#' == next()) {
                get();                          // '#'
                n = getName();
                if (null != n) {
                    if (n.equals(XPointer.S_ELEMENT))
                        nodetype = XPointer.NT_ELEMENT;
                    else if (n.equals(XPointer.S_PI))
                        nodetype = XPointer.NT_PI;
                    else if (n.equals(XPointer.S_COMMENT))
                        nodetype = XPointer.NT_COMMENT;
                    else if (n.equals(XPointer.S_TEXT))
                        nodetype = XPointer.NT_TEXT;
                    else if (n.equals(XPointer.S_CDATA))
                        nodetype = XPointer.NT_CDATA;
                    else if (n.equals(XPointer.S_ALL))
                        nodetype = XPointer.NT_ALL;
                }
            } else {
                nodename = getName();
                nodetype = XPointer.NT_NAME;
            }
            if (nodetype == XPointer.NT_NONE)
                error("Syntax error: [relterm] expect a name/#element/#pi/#comment/#text/#cdata/#all: #"+n);
                        
                                                // ',' or ')'
            while (in() && ')' != next()) {
                if (',' == next())
                    attributes.addElement(parseAttribute());
                else
                    break;
            }
        }
        if (')' != next()) {
            error("Syntax error: [relterm] expect `)': "+left());
        } else
            get();                              // ')'
        return new RelTerm(ide, allp, instance, nodetype, nodename, attributes);
    }

    RelTermAttribute parseAttribute() throws XPointerParseException {
        String attr;
        int type = -1;
        String value = null;
        get();                                  // ','
        if ('*' == next()) {
            attr = "*";
            get();                              // '*'
        } else {
            attr = getName();
            if (null == attr)
                error("Syntax error: expect `*' or Name: "+left());
        }
        if (',' != next()) {
            error("Syntax error: expect `,': "+left());
        } else {
            get();                              // ','
            if ('*' == next()) {
                get();                          // '*'
                type = XPointer.T_ANY;
                value = "*";
            } else if ('#' == next()) {
                get();                          // '#'
                String s = getName();
                if (null == s || !XPointer.S_IMPLIED.equals(s)) {
                    error("Syntax error: [relterm] expect `#IMPLIED': "+left());
                } else {
                    type = XPointer.T_IMPLIED;
                    value = XPointer.S_NIMPLIED;
                }
            } else if ('"' == next() || '\'' == next()) {
                type = XPointer.T_EXACT;
                value = getSkipLit();
            } else {
                type = XPointer.T_NAME;
                value = getName();
                if (null == value) {
                    error("Syntax error: [relterm] expect a name: "+left());
                }
            }
        }
        return new RelTermAttribute(type, attr, value);
    }

    StringTerm parseStringTerm() throws XPointerParseException {
        boolean allp = false;
        int instance = -1;
        String str = null;
        boolean isposition = false;
        boolean endp = false;
        int position = -1;
        int length = -1;
        if ('(' != next()) {
            error("Syntax error: [string] expect `(': "+left());
        } else {
            get();                              // '('
            String n = getName();               // 'ALL' ?
            if (n.equals(XPointer.S_ALL)) {
                allp = true;
            } else {                            // digit
                instance = getDigit();
            }
            if (',' != next())
                error("Syntax error: [string] expect `,':"+left());
            else {
                get();                          // ','
                str = getSkipLit();
                if (',' == next()) {
                    get();                      // ','
                    isposition = true;
                    n = getName();
                    if (n.equals(XPointer.S_END)) {
                        endp = true;
                    } else {
                        position = getDigit();
                    }
                    if (',' == next()) {
                        get();                  // ','
                        length = getNaturalNumber();
                    }
                }
                if (')' != next())
                    error("Syntax error: [string] expect `)':"+left());
                else
                    get();                      // ')'
            }
        }
        return new StringTerm(allp, instance, str, isposition, endp, position, length);
    }

    AttrTerm parseAttrTerm() throws XPointerParseException {
        String attrname = null;
        if ('(' != next()) {
            error("Syntax error: [attr] expect `(': "+left());
        } else {
            get();                              // '('
            attrname = getName();
            if (null == attrname) {
                error("Syntax error: [attr] expect a name: "+left());
            }
            if (')' != next()) {
                error("Syntax error: [attr] expect `)': "+left());
            } else {
                get();                          // ')'
            }
        }
        return new AttrTerm(attrname);
    }

    SpanTerm parseSpanTerm() throws XPointerParseException {
        XPointer xbegin = null, xend = null;
        if ('(' != next()) {
            error("Syntax error: [span] expect `(': "+left());
        } else {
            get();                              // '('
            XPointerParser xpp = new XPointerParser();
            xbegin = xpp.parse(this.xpointer, this.index);
            this.index += xpp.xpointer.length();
            if (',' != next()) {
                error("Syntax error: [span] expect ',': "+left());
            } else {
                get();                          // ','
                xend = xpp.parse(this.xpointer, this.index);
                this.index += xpp.xpointer.length();
                if (')' != next()) {
                    error("Syntax error: [span] expect ')': "+left());
                } else {
                    get();                      // ')'
                }
            }
        }
        return new SpanTerm(xbegin, xend);
    }

    private OtherTerm parseOtherTerm(int ide, String name) throws XPointerParseException {
        OtherTerm ot = null;
        switch (ide) {
          case XPointer.ST_NONE: 
          case XPointer.ST_CHILD:  case XPointer.ST_DESCENDANT:  case  XPointer.ST_ANCESTOR:  case XPointer.ST_PRECEDING:
          case XPointer.ST_FOLLOWING:  case  XPointer.ST_PSIBLING:  case XPointer.ST_FSIBLING:
            ot = parseRelTerm(ide);
            break;
          case XPointer.ST_SPAN:
            ot = parseSpanTerm();
            break;
          case XPointer.ST_ATTR:
            ot = parseAttrTerm();
            break;
          case XPointer.ST_STRING:
            ot = parseStringTerm();
            break;
        }
        return ot;
    }

    static String makeSkipLit(String str) {
        if (0 <= str.indexOf('"'))
            return "'"+str+"'";
        else
            return "\""+str+"\"";
    }

    private final boolean in() {
        return this.index < this.xpointer.length();
    }
    private final int next() {
        return in() ? this.xpointer.charAt(this.index) : -1;
    }
    private final int get() {
        return in() ? this.xpointer.charAt(this.index++) : -1;
    }

    private void error(String str) throws XPointerParseException {
        throw new XPointerParseException(str);
    }

    private String getName() {
        int n = next();
        if (XMLChar.isLetter((char)n) || '_' == n || ':' == n) {
            StringBuffer sb = new StringBuffer(32);
            do {
                sb.append((char)get());
            } while (in() && XMLChar.isNameChar((char)next()));
            return sb.toString();
        }
        return null;
    }

    private String getSkipLit() throws XPointerParseException {
        StringBuffer sb = new StringBuffer(32);
        int st = next();
        if ('"' == st || '\'' == st) {
            get();
            while (in() && st != next()) {
                sb.append((char)get());
            }
            if (!in())
                error("Unexpected end.");
            get();
        } else {
            error("Syntax error: expect \" or ': "+left());
        }
        return sb.toString();
    }

    private int getDigit() throws XPointerParseException {
        int c = next();
        int sign = 1;
        if ('-' == c) {
            sign = -1;
            get();                              // '-'
        } else if ('+' == c) {
            sign = 1;
            get();                              // '+'
        }
        int v = 0;
        if (!in() || !Character.isDigit((char)(c = next()))) {
            error("Syntax error: expect digits: "+left());
        } else {
            get();
            v = Character.getNumericValue((char)c);
            while (in() && Character.isDigit((char)next()))
                v = v*10+Character.getNumericValue((char)get());
        }
        return v*sign;
    }

    private int getNaturalNumber() throws XPointerParseException {
        int v = 0;
        if (!in() || !Character.isDigit((char)next())) {
            error("Syntax error: expect digits: "+left());
        } else {
            v = Character.getNumericValue((char)get());
            while (in() && Character.isDigit((char)next()))
                v = v*10+Character.getNumericValue((char)get());
        }
        return v;
    }

    String left() {
        return this.xpointer.substring(this.index);
    }

    /**
     * Parse an XPointer string.
     * @exception XPointerParseException <var>str</var> has errors as XPointer.
     */
    public XPointer parse(String str) throws XPointerParseException {
        return parse(str, 0);
    }

    /**
     * Parse an XPointer string from specified offset.
     * @exception XPointerParseException <var>str</var> has errors as XPointer.
     */
    public XPointer parse(String str, int offset) throws XPointerParseException {
        AbsTerm absterm = null;
        Vector otherterms = new Vector();

        //System.err.println("CONSTRUCT: "+s.substring(offset)+","+offset+","+s.length());
        this.xpointer = str;
        this.index = offset;
        int status = 0;
        boolean exit = false;

        while(in() && !exit) {
            String n;
            Integer ic;
            switch (status) {
              case 0:                           // initial state
                n = getName();
                if (null == n) {
                    error("Syntax error: [ROOT] Expect a name.");
                    exit = true;
                    break;
                }
                ic = (Integer)s_keys.get(n);
                if (null == ic) {
                    error("Syntax error: [ROOT] Unexpected name: "+n);
                    exit = true;
                    break;
                }
                switch (ic.intValue()) {
                  case XPointer.ST_ROOT:  case XPointer.ST_ORIGIN:  case XPointer.ST_ID:  case XPointer.ST_HTML:
                    absterm = parseAbsTerm(ic.intValue());
                    break;

                  case XPointer.ST_CHILD:  case XPointer.ST_DESCENDANT:  case  XPointer.ST_ANCESTOR:  case XPointer.ST_PRECEDING:
                  case XPointer.ST_FOLLOWING:  case  XPointer.ST_PSIBLING:  case XPointer.ST_FSIBLING:
                  case XPointer.ST_SPAN:  case XPointer.ST_ATTR:  case XPointer.ST_STRING:
                    otherterms.addElement(parseOtherTerm(ic.intValue(), n));
                    break;

                  default:
                    exit = true;
                    error("Internal Error: [ROOT]");
                    break;
                }
                status = 1;
                break;

              case 1:
                if ('.' != next()) {
                    exit = true;
                    break;
                }
                get();                          // '.'
                if ('(' == next()) {
                    otherterms.addElement(parseOtherTerm(XPointer.ST_NONE, null));
                } else {
                    n = getName();
                    if (null == n) {
                        error("Syntax error: [ROOT] Expect a name.");
                        exit = true;
                        break;
                    }
                    ic = (Integer)s_keys.get(n);
                    if (null == ic) {
                        error("Syntax error: [ROOT] Unexpected name: "+n);
                        exit = true;
                        break;
                    }
                    switch (ic.intValue()) {
                      case XPointer.ST_ROOT:  case XPointer.ST_ORIGIN:  case XPointer.ST_ID:  case XPointer.ST_HTML:
                        exit = true;
                        error("Syntax error: [ROOT] Unexpected name: "+n);
                        break;

                      case XPointer.ST_CHILD:  case XPointer.ST_DESCENDANT:  case  XPointer.ST_ANCESTOR:  case XPointer.ST_PRECEDING:
                      case XPointer.ST_FOLLOWING:  case  XPointer.ST_PSIBLING:  case XPointer.ST_FSIBLING:
                      case XPointer.ST_SPAN:  case XPointer.ST_ATTR:  case XPointer.ST_STRING:
                        otherterms.addElement(parseOtherTerm(ic.intValue(), n));
                        break;

                      default:
                        exit = true;
                        error("Internal Error: [ROOT]");
                        break;
                    }
                }
                break;

              default:
                exit = true;
                error("Internal Error: [ROOT] Invalid state: "+status);
                break;
            } // switch
        } // while
        this.xpointer = this.xpointer.substring(offset, this.index);
        //System.err.println("END: "+this.xpointer+","+this.index+","+offset);
        return new XPointer(absterm, otherterms);
    }

    /**
     * Return a String representation of this instance.
     */
    public String toString() {
        return this.xpointer;
    }

    /**
     * A test program for <CODE>XPointerParser</CODE>.
     */
    public static void main(String[] argv) {
        if (1 != argv.length) {
            System.err.println("Require 1 argument.");
            System.exit(1);
        }
        try {
            XPointer xp = new XPointerParser().parse(argv[0]);
            System.out.println("PARSED and RESTRUCTED: "+xp.toString());
        } catch (XPointerParseException e) {
            e.printStackTrace();
        }
    }
}

