/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (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.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jasper.compiler;

/**
 * This class implements a parser for EL expressions.
 *
 * It takes strings of the form xxx${..}yyy${..}zzz etc, and turn it into
 * a ELNode.Nodes.
 *
 * Currently, it only handles text outside ${..} and functions in ${ ..}.
 *
 * @author Kin-man Chung
 */

public class ELParser {

    private Token curToken;        // current token
    private ELNode.Nodes expr;
    private ELNode.Nodes ELexpr;
    private int index;                // Current index of the expression
    private String expression;        // The EL expression
    private boolean escapeBS;        // is '\' an escape char in text outside EL?

    private static final String reservedWords[] = {
        "and", "div", "empty", "eq", "false",
        "ge", "gt", "instanceof", "le", "lt", "mod",
        "ne", "not", "null", "or", "true"};

    public ELParser(String expression) {
        index = 0;
        this.expression = expression;
        expr = new ELNode.Nodes();
    }

    /**
     * Parse an EL expression
     * @param expression The input expression string of the form
     *                   Char* ('${' Char* '}')* Char*
     * @return Parsed EL expression in ELNode.Nodes
     */
    public static ELNode.Nodes parse(String expression) {
        ELParser parser = new ELParser(expression);
        while (parser.hasNextChar()) {
            String text = parser.skipUntilEL();
            if (text.length() > 0) {
                parser.expr.add(new ELNode.Text(text));
            }
            ELNode.Nodes elexpr = parser.parseEL();
            if (! elexpr.isEmpty()) {
                parser.expr.add(new ELNode.Root(elexpr));
            }
        }
        return parser.expr;
    }

    /**
     * Parse an EL expression string '${...}'
     *@return An ELNode.Nodes representing the EL expression
     * TODO: Currently only parsed into functions and text strings.  This
     *       should be rewritten for a full parser.
     */
    private ELNode.Nodes parseEL() {

        StringBuffer buf = new StringBuffer();
        ELexpr = new ELNode.Nodes();
        while (hasNext()) {
            curToken = nextToken();
            if (curToken instanceof Char) {
                if (curToken.toChar() == '}') {
                    break;
                }
                buf.append(curToken.toChar());
            } else {
                // Output whatever is in buffer
                if (buf.length() > 0) {
                    ELexpr.add(new ELNode.ELText(buf.toString()));
                }
                if (!parseFunction()) {
                    ELexpr.add(new ELNode.ELText(curToken.toString()));
                }
            }
        }
        if (buf.length() > 0) {
            ELexpr.add(new ELNode.ELText(buf.toString()));
        }

        return ELexpr;
    }

    /**
     * Parse for a function
     * FunctionInvokation ::= (identifier ':')? identifier '('
     *                              (Expression (,Expression)*)? ')'
     * Note: currently we don't parse arguments
     */
    private boolean parseFunction() {
        if (! (curToken instanceof Id) || isELReserved(curToken.toString())) {
            return false;
        }
        String s1 = null;                 // Function prefix
        String s2 = curToken.toString();  // Function name
        int mark = getIndex();
        if (hasNext()) {
            Token t = nextToken();
            if (t.toChar() == ':') {
                if (hasNext()) {
                    Token t2 = nextToken();
                    if (t2 instanceof Id) {
                        s1 = s2;
                        s2 = t2.toString();
                        if (hasNext()) {
                            t = nextToken();
                        }
                    }
                }
            }
            if (t.toChar() == '(') {
                ELexpr.add(new ELNode.Function(s1, s2));
                return true;
            }
        }
        setIndex(mark);
        return false;
    }

    /**
     * Test if an id is a reserved word in EL
     */
    private boolean isELReserved(String id) {
        int i = 0;
        int j = reservedWords.length;
        while (i < j) {
            int k = (i+j)/2;
            int result = reservedWords[k].compareTo(id);
            if (result == 0) {
                return true;
            }
            if (result < 0) {
                i = k+1;
            } else {
                j = k;
            }
        }
        return false;
    }

    /**
     * Skip until an EL expression ('${') is reached, allowing escape sequences
     * '\\' and '\$'.
     * @return The text string up to the EL expression
     */
    private String skipUntilEL() {
        char prev = 0;
        StringBuffer buf = new StringBuffer();
        while (hasNextChar()) {
            char ch = nextChar();
            if (prev == '\\') {
                prev = 0;
                if (ch == '\\') {
                    buf.append('\\');
                    if (!escapeBS)
                        prev = '\\';
                } else if (ch == '$') {
                    buf.append('$');
                }
                // else error!
            } else if (prev == '$') {
                if (ch == '{') {
                    prev = 0;
                    break;
                } 
                buf.append('$');
                buf.append(ch);
                prev = 0;
            } else if (ch == '\\' || ch == '$') {
                prev = ch;
            } else {
                buf.append(ch);
            }
        }
        if (prev != 0) {
            buf.append(prev);
        }
        return buf.toString();
    }

    /*
     * @return true if there is something left in EL expression buffer other
     *         than white spaces.
     */
    private boolean hasNext() {
        skipSpaces();
        return hasNextChar();
    }

    /*
     * @return The next token in the EL expression buffer.
     */
    private Token nextToken() {
        skipSpaces();
        if (hasNextChar()) {
            char ch = nextChar();
            if (Character.isJavaIdentifierStart(ch)) {
                StringBuffer buf = new StringBuffer();
                buf.append(ch);
                while ((ch = peekChar()) != -1 &&
                                Character.isJavaIdentifierPart(ch)) {
                    buf.append(ch);
                    nextChar();
                }
                return new Id(buf.toString());
            }

            if (ch == '\'' || ch == '"') {
                return parseQuotedChars(ch);
            } else {
                // For now...
                return new Char(ch);
            }
        }
        return null;
    }

    /*
     * Parse a string in single or double quotes, allowing for escape sequences
     * '\\', and ('\"', or "\'")
     */
    private Token parseQuotedChars(char quote) {
        StringBuffer buf = new StringBuffer();
        buf.append(quote);
        while (hasNextChar()) {
            char ch = nextChar();
            if (ch == '\\') {
                ch = nextChar();
                if (ch == '\\' || ch == quote) {
                    buf.append(ch);
                }
                // else error!
            } else if (ch == quote) {
                buf.append(ch);
                break;
            } else {
                buf.append(ch);
            }
        }
        return new QuotedString(buf.toString());
    }

    /*
     * A collection of low level parse methods dealing with character in
     * the EL expression buffer.
     */

    private void skipSpaces() {
        while (hasNextChar()) {
            if (expression.charAt(index) > ' ')
                break;
            index++;
        }
    }

    private boolean hasNextChar() {
        return index < expression.length();
    }

    private char nextChar() {
        if (index >= expression.length()) {
            return (char)-1;
        }
        return expression.charAt(index++);
    }

    private char peekChar() {
        if (index >= expression.length()) {
            return (char)-1;
        }
        return expression.charAt(index);
    }

    private int getIndex() {
        return index;
    }

    private void setIndex(int i) {
        index = i;
    }

    /*
     * Represents a token in EL expression string
     */
    private static class Token {

        char toChar() {
            return 0;
        }

        public String toString() {
            return "";
        }
    }

    /*
     * Represents an ID token in EL
     */
    private static class Id extends Token {
        String id;

        Id(String id) {
            this.id = id;
        }

        public String toString() {
            return id;
        }
    }

    /*
     * Represents a character token in EL
     */
    private static class Char extends Token {

        private char ch;

        Char(char ch) {
            this.ch = ch;
        }

        char toChar() {
            return ch;
        }

        public String toString() {
            return (new Character(ch)).toString();
        }
    }

    /*
     * Represents a quoted (single or double) string token in EL
     */
    private static class QuotedString extends Token {

        private String value;

        QuotedString(String v) {
            this.value = v;
        }

        public String toString() {
            return value;
        }
    }
}

