/*
 * 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.catalina.ssi;


/**
 * Parses an expression string to return the individual tokens. This is patterned similar to the StreamTokenizer in the
 * JDK but customized for SSI conditional expression parsing.
 *
 * @author Paul Speed
 */
public class ExpressionTokenizer {
    public static final int TOKEN_STRING = 0;
    public static final int TOKEN_AND = 1;
    public static final int TOKEN_OR = 2;
    public static final int TOKEN_NOT = 3;
    public static final int TOKEN_EQ = 4;
    public static final int TOKEN_NOT_EQ = 5;
    public static final int TOKEN_RBRACE = 6;
    public static final int TOKEN_LBRACE = 7;
    public static final int TOKEN_GE = 8;
    public static final int TOKEN_LE = 9;
    public static final int TOKEN_GT = 10;
    public static final int TOKEN_LT = 11;
    public static final int TOKEN_END = 12;
    private final char[] expr;
    private String tokenVal = null;
    private int index;
    private final int length;


    /**
     * Creates a new parser for the specified expression.
     *
     * @param expr The expression
     */
    public ExpressionTokenizer(String expr) {
        this.expr = expr.trim().toCharArray();
        this.length = this.expr.length;
    }


    /**
     * @return <code>true</code> if there are more tokens.
     */
    public boolean hasMoreTokens() {
        return index < length;
    }


    /**
     * @return the current index for error reporting purposes.
     */
    public int getIndex() {
        return index;
    }


    protected boolean isMetaChar(char c) {
        return Character.isWhitespace(c) || c == '(' || c == ')' || c == '!' || c == '<' || c == '>' || c == '|' ||
                c == '&' || c == '=';
    }


    /**
     * @return the next token type and initializes any state variables accordingly.
     */
    public int nextToken() {
        // Skip any leading white space
        while (index < length && Character.isWhitespace(expr[index])) {
            index++;
        }
        // Clear the current token val
        tokenVal = null;
        if (index == length) {
            return TOKEN_END; // End of string
        }
        int start = index;
        char currentChar = expr[index];
        char nextChar = (char) 0;
        index++;
        if (index < length) {
            nextChar = expr[index];
        }
        // Check for a known token start
        switch (currentChar) {
            case '(':
                return TOKEN_LBRACE;
            case ')':
                return TOKEN_RBRACE;
            case '=':
                return TOKEN_EQ;
            case '!':
                if (nextChar == '=') {
                    index++;
                    return TOKEN_NOT_EQ;
                }
                return TOKEN_NOT;
            case '|':
                if (nextChar == '|') {
                    index++;
                    return TOKEN_OR;
                }
                break;
            case '&':
                if (nextChar == '&') {
                    index++;
                    return TOKEN_AND;
                }
                break;
            case '>':
                if (nextChar == '=') {
                    index++;
                    return TOKEN_GE; // Greater than or equal
                }
                return TOKEN_GT; // Greater than
            case '<':
                if (nextChar == '=') {
                    index++;
                    return TOKEN_LE; // Less than or equal
                }
                return TOKEN_LT; // Less than
            default:
                // Otherwise it's a string
                break;
        }
        int end = index;
        if (currentChar == '"' || currentChar == '\'') {
            // It's a quoted string and the end is the next unescaped quote
            char endChar = currentChar;
            boolean escaped = false;
            start++;
            for (; index < length; index++) {
                if (expr[index] == '\\' && !escaped) {
                    escaped = true;
                    continue;
                }
                if (expr[index] == endChar && !escaped) {
                    break;
                }
                escaped = false;
            }
            end = index;
            index++; // Skip the end quote
        } else if (currentChar == '/') {
            // It's a regular expression and the end is the next unescaped /
            char endChar = currentChar;
            boolean escaped = false;
            for (; index < length; index++) {
                if (expr[index] == '\\' && !escaped) {
                    escaped = true;
                    continue;
                }
                if (expr[index] == endChar && !escaped) {
                    break;
                }
                escaped = false;
            }
            end = ++index;
        } else {
            // End is the next whitespace character
            for (; index < length; index++) {
                if (isMetaChar(expr[index])) {
                    break;
                }
            }
            end = index;
        }
        // Extract the string from the array
        this.tokenVal = new String(expr, start, end - start);
        return TOKEN_STRING;
    }


    /**
     * @return the String value of the token if it was type TOKEN_STRING. Otherwise null is returned.
     */
    public String getTokenValue() {
        return tokenVal;
    }
}