/*
 * 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.
 */

/* $Id$ */

package org.apache.fop.fo.expr;



/**
 * Class to tokenize XSL FO property expression.
 * This class is heavily based on the epxression tokenizer in James Clark's
 * XT, an XSLT processor.
 */
class PropertyTokenizer {

    static final int TOK_EOF = 0;
    static final int TOK_NCNAME = 1;
    static final int TOK_MULTIPLY = TOK_NCNAME + 1;
    static final int TOK_LPAR = TOK_MULTIPLY + 1;
    static final int TOK_RPAR = TOK_LPAR + 1;
    static final int TOK_LITERAL = TOK_RPAR + 1;
    static final int TOK_NUMBER = TOK_LITERAL + 1;
    static final int TOK_FUNCTION_LPAR = TOK_NUMBER + 1;
    static final int TOK_PLUS = TOK_FUNCTION_LPAR + 1;
    static final int TOK_MINUS = TOK_PLUS + 1;
    static final int TOK_MOD = TOK_MINUS + 1;
    static final int TOK_DIV = TOK_MOD + 1;
    static final int TOK_NUMERIC = TOK_DIV + 1;
    static final int TOK_COMMA = TOK_NUMERIC + 1;
    static final int TOK_PERCENT = TOK_COMMA + 1;
    static final int TOK_COLORSPEC = TOK_PERCENT + 1;
    static final int TOK_FLOAT = TOK_COLORSPEC + 1;
    static final int TOK_INTEGER = TOK_FLOAT + 1;

    protected int currentToken = TOK_EOF;
    protected String currentTokenValue;
    protected int currentUnitLength;

    private int currentTokenStartIndex;
    private /* final */ String expr;
    private int exprIndex;
    private int exprLength;

    /**
     * Construct a new PropertyTokenizer object to tokenize the passed
     * String.
     * @param s The Property expressio to tokenize.
     */
    PropertyTokenizer(String s) {
        this.expr = s;
        this.exprLength = s.length();
    }

    /**
     * Parse the next token in the expression string.
     * This sets the following package visible variables:
     * currentToken  An enumerated value identifying the recognized token
     * currentTokenValue  A String containing the token contents
     * currentUnitLength  If currentToken = TOK_NUMERIC, the number of
     * characters in the unit name.
     * @throws PropertyException If un unrecognized token is encountered.
     */
    void next() throws PropertyException {
        currentTokenValue = null;
        currentTokenStartIndex = exprIndex;
        boolean bSawDecimal;
        while (true) {
            if (exprIndex >= exprLength) {
                currentToken = TOK_EOF;
                return;
            }
            char c = expr.charAt(exprIndex++);
            switch (c) {
            case ' ':
            case '\t':
            case '\r':
            case '\n':
                currentTokenStartIndex = exprIndex;
                break;
            case ',':
                currentToken = TOK_COMMA;
                return;
            case '+':
                currentToken = TOK_PLUS;
                return;
            case '-':
                currentToken = TOK_MINUS;
                return;
            case '(':
                currentToken = TOK_LPAR;
                return;
            case ')':
                currentToken = TOK_RPAR;
                return;
            case '"':
            case '\'':
                exprIndex = expr.indexOf(c, exprIndex);
                if (exprIndex < 0) {
                    exprIndex = currentTokenStartIndex + 1;
                    throw new PropertyException("missing quote");
                }
                currentTokenValue = expr.substring(currentTokenStartIndex
                                                   + 1, exprIndex++);
                currentToken = TOK_LITERAL;
                return;
            case '*':
                /*
                 * if (currentMaybeOperator) {
                 * recognizeOperator = false;
                 */
                currentToken = TOK_MULTIPLY;
                /*
                 * }
                 * else
                 * throw new PropertyException("illegal operator *");
                 */
                return;
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
                scanDigits();
                if (exprIndex < exprLength && expr.charAt(exprIndex) == '.') {
                    exprIndex++;
                    bSawDecimal = true;
                    if (exprIndex < exprLength
                            && isDigit(expr.charAt(exprIndex))) {
                        exprIndex++;
                        scanDigits();
                    }
                } else {
                    bSawDecimal = false;
                }
                if (exprIndex < exprLength && expr.charAt(exprIndex) == '%') {
                    exprIndex++;
                    currentToken = TOK_PERCENT;
                } else {
                    // Check for possible unit name following number
                    currentUnitLength = exprIndex;
                    scanName();
                    currentUnitLength = exprIndex - currentUnitLength;
                    currentToken = (currentUnitLength > 0) ? TOK_NUMERIC
                                   : (bSawDecimal ? TOK_FLOAT : TOK_INTEGER);
                }
                currentTokenValue = expr.substring(currentTokenStartIndex,
                                                   exprIndex);
                return;

            case '.':
                nextDecimalPoint();
                return;

            case '#':    // Start of color value
                nextColor();
                return;

            default:
                --exprIndex;
                scanName();
                if (exprIndex == currentTokenStartIndex) {
                    throw new PropertyException("illegal character");
                }
                currentTokenValue = expr.substring(currentTokenStartIndex, exprIndex);
                if (currentTokenValue.equals("mod")) {
                    currentToken = TOK_MOD;
                    return;
                } else if (currentTokenValue.equals("div")) {
                    currentToken = TOK_DIV;
                    return;
                }
                if (followingParen()) {
                    currentToken = TOK_FUNCTION_LPAR;
                } else {
                    currentToken = TOK_NCNAME;
                }
                return;
            }
        }
    }

    private void nextDecimalPoint() throws PropertyException {
        if (exprIndex < exprLength
                && isDigit(expr.charAt(exprIndex))) {
            ++exprIndex;
            scanDigits();
            if (exprIndex < exprLength
                    && expr.charAt(exprIndex) == '%') {
                exprIndex++;
                currentToken = TOK_PERCENT;
            } else {
                // Check for possible unit name following number
                currentUnitLength = exprIndex;
                scanName();
                currentUnitLength = exprIndex - currentUnitLength;
                currentToken = (currentUnitLength > 0) ? TOK_NUMERIC
                        : TOK_FLOAT;
            }
            currentTokenValue = expr.substring(currentTokenStartIndex,
                    exprIndex);
            return;
        }
        throw new PropertyException("illegal character '.'");
    }


    private void nextColor() throws PropertyException {
        if (exprIndex < exprLength) {
            ++exprIndex;
            scanHexDigits();
            int len = exprIndex - currentTokenStartIndex - 1;
            if (len % 3 == 0 || len == 8) {
                currentToken = TOK_COLORSPEC;
            } else {
                //Actually not a color at all, but an NCNAME starting with "#"
                scanRestOfName();
                currentToken = TOK_NCNAME;
            }
            currentTokenValue = expr.substring(currentTokenStartIndex,
                    exprIndex);
            return;
        } else {
            throw new PropertyException("illegal character '#'");
        }
    }

    /**
     * Attempt to recognize a valid NAME token in the input expression.
     */
    private void scanName() {
        if (exprIndex < exprLength && isNameStartChar(expr.charAt(exprIndex))) {
            scanRestOfName();
        }
    }

    private void scanRestOfName() {
        while (++exprIndex < exprLength) {
            if (!isNameChar(expr.charAt(exprIndex))) {
                break;
            }
        }
    }

    /**
     * Attempt to recognize a valid sequence of decimal DIGITS in the
     * input expression.
     */
    private void scanDigits() {
        while (exprIndex < exprLength && isDigit(expr.charAt(exprIndex))) {
            exprIndex++;
        }
    }

    /**
     * Attempt to recognize a valid sequence of hexadecimal DIGITS in the
     * input expression.
     */
    private void scanHexDigits() {
        while (exprIndex < exprLength && isHexDigit(expr.charAt(exprIndex))) {
            exprIndex++;
        }
    }

    /**
     * Return a boolean value indicating whether the following non-whitespace
     * character is an opening parenthesis.
     */
    private boolean followingParen() {
        for (int i = exprIndex; i < exprLength; i++) {
            switch (expr.charAt(i)) {
            case '(':
                exprIndex = i + 1;
                return true;
            case ' ':
            case '\r':
            case '\n':
            case '\t':
                break;
            default:
                return false;
            }
        }
        return false;
    }


    private static final String NAME_START_CHARS
        = "_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private static final String NAME_CHARS = ".-0123456789";
    private static final String DIGITS = "0123456789";
    private static final String HEX_CHARS = DIGITS + "abcdefABCDEF";

    /**
     * Return a boolean value indicating whether the argument is a
     * decimal digit (0-9).
     * @param c The character to check
     */
    private static boolean isDigit(char c) {
        return DIGITS.indexOf(c) >= 0;
    }

    /**
     * Return a boolean value indicating whether the argument is a
     * hexadecimal digit (0-9, A-F, a-f).
     * @param c The character to check
     */
    private static boolean isHexDigit(char c) {
        return HEX_CHARS.indexOf(c) >= 0;
    }

    /**
     * Return a boolean value indicating whether the argument is whitespace
     * as defined by XSL (space, newline, CR, tab).
     * @param c The character to check
     */
    /* not used
    private static boolean isSpace(char c) {
        switch (c) {
        case ' ':
        case '\r':
        case '\n':
        case '\t':
            return true;
        default:
            return false;
        }
    }
    */

    /**
     * Return a  boolean value indicating whether the argument is a valid name
     * start character, ie. can start a NAME as defined by XSL.
     * @param c The character to check
     */
    private static boolean isNameStartChar(char c) {
        return NAME_START_CHARS.indexOf(c) >= 0 || c >= 0x80;
    }

    /**
     * Return a  boolean value indicating whether the argument is a valid name
     * character, ie. can occur in a NAME as defined by XSL.
     * @param c The character to check
     */
    private static boolean isNameChar(char c) {
        return NAME_START_CHARS.indexOf(c) >= 0 || NAME_CHARS.indexOf(c) >= 0
               || c >= 0x80;
    }

}

