/*

   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.batik.css.parser;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;

import org.apache.batik.util.io.NormalizingReader;
import org.apache.batik.util.io.StreamNormalizingReader;
import org.apache.batik.util.io.StringNormalizingReader;

/**
 * This class represents a CSS scanner - an object which decodes CSS lexical
 * units.
 *
 * @author <a href="mailto:stephane@hillion.org">Stephane Hillion</a>
 * @version $Id$
 */
public class Scanner {

    /**
     * The reader.
     */
    protected NormalizingReader reader;

    /**
     * The current char.
     */
    protected int current;

    /**
     * The recording buffer.
     */
    protected char[] buffer = new char[128];

    /**
     * The current position in the buffer.
     */
    protected int position;

    /**
     * The type of the current lexical unit.
     */
    protected int type;

    /**
     * The start offset of the last lexical unit.
     */
    protected int start;

    /**
     * The end offset of the last lexical unit.
     */
    protected int end;

    /**
     * The characters to skip to create the string which represents the
     * current token.
     */
    protected int blankCharacters;

    /**
     * Creates a new Scanner object.
     * @param r The reader to scan.
     */
    public Scanner(Reader r) throws ParseException {
        try {
            reader = new StreamNormalizingReader(r);
            current = nextChar();
        } catch (IOException e) {
            throw new ParseException(e);
        }
    }

    /**
     * Creates a new Scanner object.
     * @param is The input stream to scan.
     * @param enc The encoding to use to decode the input stream, or null.
     */
    public Scanner(InputStream is, String enc) throws ParseException {
        try {
            reader = new StreamNormalizingReader(is, enc);
            current = nextChar();
        } catch (IOException e) {
            throw new ParseException(e);
        }
    }

    /**
     * Creates a new Scanner object.
     * @param s The string to scan.
     */
    public Scanner(String s) throws ParseException {
        try {
            reader = new StringNormalizingReader(s);
            current = nextChar();
        } catch (IOException e) {
            throw new ParseException(e);
        }
    }

    /**
     * Returns the current line.
     */
    public int getLine() {
        return reader.getLine();
    }

    /**
     * Returns the current column.
     */
    public int getColumn() {
        return reader.getColumn();
    }

    /**
     * Returns the buffer used to store the chars.
     */
    public char[] getBuffer() {
        return buffer;
    }

    /**
     * Returns the start offset of the last lexical unit.
     */
    public int getStart() {
        return start;
    }

    /**
     * Returns the end offset of the last lexical unit.
     */
    public int getEnd() {
        return end;
    }

    /**
     * Clears the buffer.
     */
    public void clearBuffer() {
        if (position <= 0) {
            position = 0;
        } else {
            buffer[0] = buffer[position-1];
            position = 1;
        }
    }

    /**
     * The current lexical unit type like defined in LexicalUnits.
     */
    public int getType() {
        return type;
    }

    /**
     * Returns the string representation of the current lexical unit.
     */
    public String getStringValue() {
        return new String(buffer, start, end - start);
    }

    /**
     * Scans a @rule value. This method assumes that the current
     * lexical unit is a at keyword.
     */
    public void scanAtRule() throws ParseException {
        try {
            // waiting for EOF, ';' or '{'
            loop: for (;;) {
                switch (current) {
                case '{':
                    int brackets = 1;
                    for (;;) {
                        nextChar();
                        switch (current) {
                        case '}':
                            if (--brackets > 0) {
                                break;
                            }
                        case -1:
                            break loop;
                        case '{':
                            brackets++;
                        }
                    }
                case -1:
                case ';':
                    break loop;
                }
                nextChar();
            }
            end = position;
        } catch (IOException e) {
            throw new ParseException(e);
        }
    }

    /**
     * Returns the next token.
     */
    public int next() throws ParseException {
        blankCharacters = 0;
        start = position - 1;
        nextToken();
        end = position - endGap();
        return type;
    }

    /**
     * Closes the underlying stream used by the scanner.
     */
    public void close() {
        try {
            reader.close();
        } catch(IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Returns the end gap of the current lexical unit.
     */
    protected int endGap() {
        int result = (current == -1) ? 0 : 1;
        switch (type) {
        case LexicalUnits.FUNCTION:
        case LexicalUnits.STRING:
        case LexicalUnits.S:
        case LexicalUnits.PERCENTAGE:
            result += 1;
            break;
        case LexicalUnits.COMMENT:
        case LexicalUnits.HZ:
        case LexicalUnits.EM:
        case LexicalUnits.EX:
        case LexicalUnits.PC:
        case LexicalUnits.PT:
        case LexicalUnits.PX:
        case LexicalUnits.CM:
        case LexicalUnits.MM:
        case LexicalUnits.IN:
        case LexicalUnits.MS:
            result += 2;
            break;
        case LexicalUnits.KHZ:
        case LexicalUnits.DEG:
        case LexicalUnits.RAD:
            result += 3;
            break;
        case LexicalUnits.GRAD:
            result += 4;
        }
        return result + blankCharacters;
    }

    /**
     * Returns the next token.
     */
    protected void nextToken() throws ParseException {
        try {
            switch (current) {
            case -1:
                type = LexicalUnits.EOF;
                return;
            case '{':
                nextChar();
                type = LexicalUnits.LEFT_CURLY_BRACE;
                return;
            case '}':
                nextChar();
                type = LexicalUnits.RIGHT_CURLY_BRACE;
                return;
            case '=':
                nextChar();
                type = LexicalUnits.EQUAL;
                return;
            case '+':
                nextChar();
                type = LexicalUnits.PLUS;
                return;
            case ',':
                nextChar();
                type = LexicalUnits.COMMA;
                return;
            case ';':
                nextChar();
                type = LexicalUnits.SEMI_COLON;
                return;
            case '>':
                nextChar();
                type = LexicalUnits.PRECEDE;
                return;
            case '[':
                nextChar();
                type = LexicalUnits.LEFT_BRACKET;
                return;
            case ']':
                nextChar();
                type = LexicalUnits.RIGHT_BRACKET;
                return;
            case '*':
                nextChar();
                type = LexicalUnits.ANY;
                return;
            case '(':
                nextChar();
                type = LexicalUnits.LEFT_BRACE;
                return;
            case ')':
                nextChar();
                type = LexicalUnits.RIGHT_BRACE;
                return;
            case ':':
                nextChar();
                type = LexicalUnits.COLON;
                return;
            case ' ':
            case '\t':
            case '\r':
            case '\n':
            case '\f':
                do {
                    nextChar();
                } while (ScannerUtilities.isCSSSpace((char)current));
                type = LexicalUnits.SPACE;
                return;
            case '/':
                nextChar();
                if (current != '*') {
                    type = LexicalUnits.DIVIDE;
                    return;
                }
                // Comment
                nextChar();
                start = position - 1;
                do {
                    while (current != -1 && current != '*') {
                        nextChar();
                    }
                    do {
                        nextChar();
                    } while (current != -1 && current == '*');
                } while (current != -1 && current != '/');
                if (current == -1) {
                    throw new ParseException("eof",
                                             reader.getLine(),
                                             reader.getColumn());
                }
                nextChar();
                type = LexicalUnits.COMMENT;
                return;
            case '\'': // String1
                type = string1();
                return;
            case '"': // String2
                type = string2();
                return;
            case '<':
                nextChar();
                if (current != '!') {
                    throw new ParseException("character",
                                             reader.getLine(),
                                             reader.getColumn());
                }
                nextChar();
                if (current == '-') {
                    nextChar();
                    if (current == '-') {
                        nextChar();
                        type = LexicalUnits.CDO;
                        return;
                    }
                }
                throw new ParseException("character",
                                         reader.getLine(),
                                         reader.getColumn());
            case '-':
                nextChar();
                if (current == 'i') {
                    return;
                }
                if (current != '-') {
                    type = LexicalUnits.MINUS;
                    return;
                }
                nextChar();
                if (current == '>') {
                    nextChar();
                    type = LexicalUnits.CDC;
                    return;
                }
                throw new ParseException("character",
                                         reader.getLine(),
                                         reader.getColumn());
            case '|':
                nextChar();
                if (current == '=') {
                    nextChar();
                    type = LexicalUnits.DASHMATCH;
                    return;
                }
                throw new ParseException("character",
                                         reader.getLine(),
                                         reader.getColumn());
            case '~':
                nextChar();
                if (current == '=') {
                    nextChar();
                    type = LexicalUnits.INCLUDES;
                    return;
                }
                throw new ParseException("character",
                                         reader.getLine(),
                                         reader.getColumn());
            case '#':
                nextChar();
                if (ScannerUtilities.isCSSNameCharacter((char)current)) {
                    start = position - 1;
                    do {
                        nextChar();
                        while (current == '\\') {
                            nextChar();
                           escape();
                        }
                    } while (current != -1 &&
                             ScannerUtilities.isCSSNameCharacter
                                 ((char)current));
                    type = LexicalUnits.HASH;
                    return;
                }
                throw new ParseException("character",
                                         reader.getLine(),
                                         reader.getColumn());
            case '@':
                nextChar();
                switch (current) {
                case 'c':
                case 'C':
                    start = position - 1;
                    if (isEqualIgnoreCase(nextChar(), 'h') &&
                        isEqualIgnoreCase(nextChar(), 'a') &&
                        isEqualIgnoreCase(nextChar(), 'r') &&
                        isEqualIgnoreCase(nextChar(), 's') &&
                        isEqualIgnoreCase(nextChar(), 'e') &&
                        isEqualIgnoreCase(nextChar(), 't')) {
                        nextChar();
                        type = LexicalUnits.CHARSET_SYMBOL;
                        return;
                    }
                    break;
                case 'f':
                case 'F':
                    start = position - 1;
                    if (isEqualIgnoreCase(nextChar(), 'o') &&
                        isEqualIgnoreCase(nextChar(), 'n') &&
                        isEqualIgnoreCase(nextChar(), 't') &&
                        isEqualIgnoreCase(nextChar(), '-') &&
                        isEqualIgnoreCase(nextChar(), 'f') &&
                        isEqualIgnoreCase(nextChar(), 'a') &&
                        isEqualIgnoreCase(nextChar(), 'c') &&
                        isEqualIgnoreCase(nextChar(), 'e')) {
                        nextChar();
                        type = LexicalUnits.FONT_FACE_SYMBOL;
                        return;
                    }
                    break;
                case 'i':
                case 'I':
                    start = position - 1;
                    if (isEqualIgnoreCase(nextChar(), 'm') &&
                        isEqualIgnoreCase(nextChar(), 'p') &&
                        isEqualIgnoreCase(nextChar(), 'o') &&
                        isEqualIgnoreCase(nextChar(), 'r') &&
                        isEqualIgnoreCase(nextChar(), 't')) {
                        nextChar();
                        type = LexicalUnits.IMPORT_SYMBOL;
                        return;
                    }
                    break;
                case 'm':
                case 'M':
                    start = position - 1;
                    if (isEqualIgnoreCase(nextChar(), 'e') &&
                        isEqualIgnoreCase(nextChar(), 'd') &&
                        isEqualIgnoreCase(nextChar(), 'i') &&
                        isEqualIgnoreCase(nextChar(), 'a')) {
                        nextChar();
                        type = LexicalUnits.MEDIA_SYMBOL;
                        return;
                    }
                    break;
                case 'p':
                case 'P':
                    start = position - 1;
                    if (isEqualIgnoreCase(nextChar(), 'a') &&
                        isEqualIgnoreCase(nextChar(), 'g') &&
                        isEqualIgnoreCase(nextChar(), 'e')) {
                        nextChar();
                        type = LexicalUnits.PAGE_SYMBOL;
                        return;
                    }
                    break;
                default:
                    if (!ScannerUtilities.isCSSIdentifierStartCharacter
                        ((char)current)) {
                        throw new ParseException("identifier.character",
                                                 reader.getLine(),
                                                 reader.getColumn());
                    }
                    start = position - 1;
                }
                do {
                    nextChar();
                    while (current == '\\') {
                        nextChar();
                        escape();
                    }
                } while (current != -1 &&
                         ScannerUtilities.isCSSNameCharacter((char)current));
                type = LexicalUnits.AT_KEYWORD;
                return;
            case '!':
                do {
                    nextChar();
                } while (current != -1 &&
                         ScannerUtilities.isCSSSpace((char)current));
                if (isEqualIgnoreCase(current, 'i') &&
                    isEqualIgnoreCase(nextChar(), 'm') &&
                    isEqualIgnoreCase(nextChar(), 'p') &&
                    isEqualIgnoreCase(nextChar(), 'o') &&
                    isEqualIgnoreCase(nextChar(), 'r') &&
                    isEqualIgnoreCase(nextChar(), 't') &&
                    isEqualIgnoreCase(nextChar(), 'a') &&
                    isEqualIgnoreCase(nextChar(), 'n') &&
                    isEqualIgnoreCase(nextChar(), 't')) {
                    nextChar();
                    type = LexicalUnits.IMPORTANT_SYMBOL;
                    return;
                }
                if (current == -1) {
                    throw new ParseException("eof",
                                             reader.getLine(),
                                             reader.getColumn());
                } else {
                    throw new ParseException("character",
                                             reader.getLine(),
                                             reader.getColumn());
                }
            case '0': case '1': case '2': case '3': case '4':
            case '5': case '6': case '7': case '8': case '9':
                type = number();
                return;
            case '.':
                switch (nextChar()) {
                case '0': case '1': case '2': case '3': case '4':
                case '5': case '6': case '7': case '8': case '9':
                    type = dotNumber();
                    return;
                default:
                    type = LexicalUnits.DOT;
                    return;
                }
            case 'u':
            case 'U':
                nextChar();
                switch (current) {
                case '+':
                    boolean range = false;
                    for (int i = 0; i < 6; i++) {
                        nextChar();
                        switch (current) {
                        case '?':
                            range = true;
                            break;
                        default:
                            if (range &&
                            !ScannerUtilities.isCSSHexadecimalCharacter
                                ((char)current)) {
                                throw new ParseException("character",
                                                         reader.getLine(),
                                                         reader.getColumn());
                            }
                        }
                    }
                    nextChar();
                    if (range) {
                        type = LexicalUnits.UNICODE_RANGE;
                        return;
                    }
                    if (current == '-') {
                        nextChar();
                        if (!ScannerUtilities.isCSSHexadecimalCharacter
                            ((char)current)) {
                            throw new ParseException("character",
                                                     reader.getLine(),
                                                     reader.getColumn());
                        }
                        nextChar();
                        if (!ScannerUtilities.isCSSHexadecimalCharacter
                            ((char)current)) {
                            type = LexicalUnits.UNICODE_RANGE;
                            return;
                        }
                        nextChar();
                        if (!ScannerUtilities.isCSSHexadecimalCharacter
                            ((char)current)) {
                            type = LexicalUnits.UNICODE_RANGE;
                            return;
                        }
                        nextChar();
                        if (!ScannerUtilities.isCSSHexadecimalCharacter
                            ((char)current)) {
                            type = LexicalUnits.UNICODE_RANGE;
                            return;
                        }
                        nextChar();
                        if (!ScannerUtilities.isCSSHexadecimalCharacter
                            ((char)current)) {
                            type = LexicalUnits.UNICODE_RANGE;
                            return;
                        }
                        nextChar();
                        if (!ScannerUtilities.isCSSHexadecimalCharacter
                            ((char)current)) {
                            type = LexicalUnits.UNICODE_RANGE;
                            return;
                        }
                        nextChar();
                        type = LexicalUnits.UNICODE_RANGE;
                        return;
                    }
                case 'r':
                case 'R':
                    nextChar();
                    switch (current) {
                    case 'l':
                    case 'L':
                        nextChar();
                        switch (current) {
                        case '(':
                            do {
                                nextChar();
                            } while (current != -1 &&
                                     ScannerUtilities.isCSSSpace
                                     ((char)current));
                            switch (current) {
                            case '\'':
                                string1();
                                blankCharacters += 2;
                                while (current != -1 &&
                                       ScannerUtilities.isCSSSpace
                                       ((char)current)) {
                                    blankCharacters++;
                                    nextChar();
                                }
                                if (current == -1) {
                                    throw new ParseException
                                        ("eof",
                                         reader.getLine(),
                                         reader.getColumn());
                                }
                                if (current != ')') {
                                    throw new ParseException
                                        ("character",
                                         reader.getLine(),
                                         reader.getColumn());
                                }
                                nextChar();
                                type = LexicalUnits.URI;
                                return;
                            case '"':
                                string2();
                                blankCharacters += 2;
                                while (current != -1 &&
                                       ScannerUtilities.isCSSSpace
                                       ((char)current)) {
                                    blankCharacters++;
                                    nextChar();
                                }
                                if (current == -1) {
                                    throw new ParseException
                                        ("eof",
                                         reader.getLine(),
                                         reader.getColumn());
                                }
                                if (current != ')') {
                                    throw new ParseException
                                        ("character",
                                         reader.getLine(),
                                         reader.getColumn());
                                }
                                nextChar();
                                type = LexicalUnits.URI;
                                return;
                            case ')':
                                throw new ParseException("character",
                                                         reader.getLine(),
                                                         reader.getColumn());
                            default:
                                if (!ScannerUtilities.isCSSURICharacter
                                    ((char)current)) {
                                    throw new ParseException
                                        ("character",
                                         reader.getLine(),
                                         reader.getColumn());
                                }
                                start = position - 1;
                                do {
                                    nextChar();
                                } while (current != -1 &&
                                      ScannerUtilities.isCSSURICharacter
                                         ((char)current));
                                blankCharacters++;
                                while (current != -1 &&
                                       ScannerUtilities.isCSSSpace
                                       ((char)current)) {
                                    blankCharacters++;
                                    nextChar();
                                }
                                if (current == -1) {
                                    throw new ParseException
                                        ("eof",
                                         reader.getLine(),
                                         reader.getColumn());
                                }
                                if (current != ')') {
                                    throw new ParseException
                                        ("character",
                                         reader.getLine(),
                                         reader.getColumn());
                                }
                                nextChar();
                                type = LexicalUnits.URI;
                                return;
                            }
                        }
                    }
                }
                while (current != -1 &&
                       ScannerUtilities.isCSSNameCharacter((char)current)) {
                    nextChar();
                }
                if (current == '(') {
                    nextChar();
                    type = LexicalUnits.FUNCTION;
                    return;
                }
                type = LexicalUnits.IDENTIFIER;
                return;
            default:
                if (current == '\\') {
                    do {
                        nextChar();
                        escape();
                    } while(current == '\\');
                } else if (!ScannerUtilities.isCSSIdentifierStartCharacter
                           ((char)current)) {
                    nextChar();
                    throw new ParseException("identifier.character",
                                             reader.getLine(),
                                             reader.getColumn());
                }
                // Identifier
                while ((current != -1) &&
                       ScannerUtilities.isCSSNameCharacter((char)current)) {
                    nextChar();
                    while (current == '\\') {
                        nextChar();
                        escape();
                    }
                }
                if (current == '(') {
                    nextChar();
                    type = LexicalUnits.FUNCTION;
                    return;
                }
                type = LexicalUnits.IDENTIFIER;
                return;
            }
        } catch (IOException e) {
            throw new ParseException(e);
        }
    }

    /**
     * Scans a single quoted string.
     */
    protected int string1() throws IOException {
        start = position;  // fix bug #29416
        loop: for (;;) {
            switch (nextChar()) {
            case -1:
                throw new ParseException("eof",
                                         reader.getLine(),
                                         reader.getColumn());
            case '\'':
                break loop;
            case '"':
                break;
            case '\\':
                switch (nextChar()) {
                case '\n':
                case '\f':
                    break;
                default:
                    escape();
                }
                break;
            default:
                if (!ScannerUtilities.isCSSStringCharacter((char)current)) {
                    throw new ParseException("character",
                                             reader.getLine(),
                                             reader.getColumn());
                }
            }
        }
        nextChar();
        return LexicalUnits.STRING;
    }

    /**
     * Scans a double quoted string.
     */
    protected int string2() throws IOException {
        start = position;  // fix bug #29416
        loop: for (;;) {
            switch (nextChar()) {
            case -1:
                throw new ParseException("eof",
                                         reader.getLine(),
                                         reader.getColumn());
            case '\'':
                break;
            case '"':
                break loop;
            case '\\':
                switch (nextChar()) {
                case '\n':
                case '\f':
                    break;
                default:
                    escape();
                }
                break;
            default:
                if (!ScannerUtilities.isCSSStringCharacter((char)current)) {
                    throw new ParseException("character",
                                             reader.getLine(),
                                             reader.getColumn());
                }
            }
        }
        nextChar();
        return LexicalUnits.STRING;
    }

    /**
     * Scans a number.
     */
    protected int number() throws IOException {
        loop: for (;;) {
            switch (nextChar()) {
            case '.':
                switch (nextChar()) {
                case '0': case '1': case '2': case '3': case '4':
                case '5': case '6': case '7': case '8': case '9':
                    return dotNumber();
                }
                throw new ParseException("character",
                                         reader.getLine(),
                                         reader.getColumn());
            default:
                break loop;
            case '0': case '1': case '2': case '3': case '4':
            case '5': case '6': case '7': case '8': case '9':
            }
        }
        return numberUnit(true);
    }

    /**
     * Scans the decimal part of a number.
     */
    protected int dotNumber() throws IOException {
        loop: for (;;) {
            switch (nextChar()) {
            default:
                break loop;
            case '0': case '1': case '2': case '3': case '4':
            case '5': case '6': case '7': case '8': case '9':
            }
        }
        return numberUnit(false);
    }

    /**
     * Scans the unit of a number.
     */
    protected int numberUnit(boolean integer) throws IOException {
        switch (current) {
        case '%':
            nextChar();
            return LexicalUnits.PERCENTAGE;
        case 'c':
        case 'C':
            switch(nextChar()) {
            case 'm':
            case 'M':
                nextChar();
                if (current != -1 &&
                    ScannerUtilities.isCSSNameCharacter((char)current)) {
                    do {
                        nextChar();
                    } while (current != -1 &&
                             ScannerUtilities.isCSSNameCharacter
                             ((char)current));
                    return LexicalUnits.DIMENSION;
                }
                return LexicalUnits.CM;
            default:
                while (current != -1 &&
                       ScannerUtilities.isCSSNameCharacter((char)current)) {
                    nextChar();
                }
                return LexicalUnits.DIMENSION;
            }
        case 'd':
        case 'D':
            switch(nextChar()) {
            case 'e':
            case 'E':
                switch(nextChar()) {
                case 'g':
                case 'G':
                    nextChar();
                    if (current != -1 &&
                        ScannerUtilities.isCSSNameCharacter((char)current)) {
                        do {
                            nextChar();
                        } while (current != -1 &&
                                 ScannerUtilities.isCSSNameCharacter
                                 ((char)current));
                        return LexicalUnits.DIMENSION;
                    }
                    return LexicalUnits.DEG;
                }
            default:
                while (current != -1 &&
                       ScannerUtilities.isCSSNameCharacter((char)current)) {
                    nextChar();
                }
                return LexicalUnits.DIMENSION;
            }
        case 'e':
        case 'E':
            switch(nextChar()) {
            case 'm':
            case 'M':
                nextChar();
                if (current != -1 &&
                    ScannerUtilities.isCSSNameCharacter((char)current)) {
                    do {
                        nextChar();
                    } while (current != -1 &&
                             ScannerUtilities.isCSSNameCharacter
                             ((char)current));
                    return LexicalUnits.DIMENSION;
                }
                return LexicalUnits.EM;
            case 'x':
            case 'X':
                nextChar();
                if (current != -1 &&
                    ScannerUtilities.isCSSNameCharacter((char)current)) {
                    do {
                        nextChar();
                    } while (current != -1 &&
                             ScannerUtilities.isCSSNameCharacter
                             ((char)current));
                    return LexicalUnits.DIMENSION;
                }
                return LexicalUnits.EX;
            default:
                while (current != -1 &&
                       ScannerUtilities.isCSSNameCharacter((char)current)) {
                    nextChar();
                }
                return LexicalUnits.DIMENSION;
            }
        case 'g':
        case 'G':
            switch(nextChar()) {
            case 'r':
            case 'R':
                switch(nextChar()) {
                case 'a':
                case 'A':
                    switch(nextChar()) {
                    case 'd':
                    case 'D':
                        nextChar();
                        if (current != -1 &&
                            ScannerUtilities.isCSSNameCharacter
                            ((char)current)) {
                            do {
                                nextChar();
                            } while (current != -1 &&
                                     ScannerUtilities.isCSSNameCharacter
                                     ((char)current));
                            return LexicalUnits.DIMENSION;
                        }
                        return LexicalUnits.GRAD;
                    }
                }
            default:
                while (current != -1 &&
                       ScannerUtilities.isCSSNameCharacter((char)current)) {
                    nextChar();
                }
                return LexicalUnits.DIMENSION;
            }
        case 'h':
        case 'H':
            nextChar();
            switch(current) {
            case 'z':
            case 'Z':
                nextChar();
                if (current != -1 &&
                    ScannerUtilities.isCSSNameCharacter((char)current)) {
                    do {
                        nextChar();
                    } while (current != -1 &&
                             ScannerUtilities.isCSSNameCharacter
                             ((char)current));
                    return LexicalUnits.DIMENSION;
                }
                return LexicalUnits.HZ;
            default:
                while (current != -1 &&
                       ScannerUtilities.isCSSNameCharacter((char)current)) {
                    nextChar();
                }
                return LexicalUnits.DIMENSION;
            }
        case 'i':
        case 'I':
            switch(nextChar()) {
            case 'n':
            case 'N':
                nextChar();
                if (current != -1 &&
                    ScannerUtilities.isCSSNameCharacter((char)current)) {
                    do {
                        nextChar();
                    } while (current != -1 &&
                             ScannerUtilities.isCSSNameCharacter
                             ((char)current));
                    return LexicalUnits.DIMENSION;
                }
                return LexicalUnits.IN;
            default:
                while (current != -1 &&
                       ScannerUtilities.isCSSNameCharacter((char)current)) {
                    nextChar();
                }
                return LexicalUnits.DIMENSION;
            }
        case 'k':
        case 'K':
            switch(nextChar()) {
            case 'h':
            case 'H':
                switch(nextChar()) {
                case 'z':
                case 'Z':
                    nextChar();
                    if (current != -1 &&
                        ScannerUtilities.isCSSNameCharacter((char)current)) {
                        do {
                            nextChar();
                        } while (current != -1 &&
                                 ScannerUtilities.isCSSNameCharacter
                                 ((char)current));
                        return LexicalUnits.DIMENSION;
                    }
                    return LexicalUnits.KHZ;
                }
            default:
                while (current != -1 &&
                       ScannerUtilities.isCSSNameCharacter((char)current)) {
                    nextChar();
                }
                return LexicalUnits.DIMENSION;
            }
        case 'm':
        case 'M':
            switch(nextChar()) {
            case 'm':
            case 'M':
                nextChar();
                if (current != -1 &&
                    ScannerUtilities.isCSSNameCharacter((char)current)) {
                    do {
                        nextChar();
                    } while (current != -1 &&
                             ScannerUtilities.isCSSNameCharacter
                             ((char)current));
                    return LexicalUnits.DIMENSION;
                }
                return LexicalUnits.MM;
            case 's':
            case 'S':
                nextChar();
                if (current != -1 &&
                    ScannerUtilities.isCSSNameCharacter((char)current)) {
                    do {
                        nextChar();
                    } while (current != -1 &&
                             ScannerUtilities.isCSSNameCharacter
                             ((char)current));
                    return LexicalUnits.DIMENSION;
                }
                return LexicalUnits.MS;
            default:
                while (current != -1 &&
                       ScannerUtilities.isCSSNameCharacter((char)current)) {
                    nextChar();
                }
                return LexicalUnits.DIMENSION;
            }
        case 'p':
        case 'P':
            switch(nextChar()) {
            case 'c':
            case 'C':
                nextChar();
                if (current != -1 &&
                    ScannerUtilities.isCSSNameCharacter((char)current)) {
                    do {
                        nextChar();
                    } while (current != -1 &&
                             ScannerUtilities.isCSSNameCharacter
                             ((char)current));
                    return LexicalUnits.DIMENSION;
                }
                return LexicalUnits.PC;
            case 't':
            case 'T':
                nextChar();
                if (current != -1 &&
                    ScannerUtilities.isCSSNameCharacter((char)current)) {
                    do {
                        nextChar();
                    } while (current != -1 &&
                             ScannerUtilities.isCSSNameCharacter
                             ((char)current));
                    return LexicalUnits.DIMENSION;
                }
                return LexicalUnits.PT;
            case 'x':
            case 'X':
                nextChar();
                if (current != -1 &&
                    ScannerUtilities.isCSSNameCharacter((char)current)) {
                    do {
                        nextChar();
                    } while (current != -1 &&
                             ScannerUtilities.isCSSNameCharacter
                             ((char)current));
                    return LexicalUnits.DIMENSION;
                }
                return LexicalUnits.PX;
            default:
                while (current != -1 &&
                       ScannerUtilities.isCSSNameCharacter((char)current)) {
                    nextChar();
                }
                return LexicalUnits.DIMENSION;
            }
        case 'r':
        case 'R':
            switch(nextChar()) {
            case 'a':
            case 'A':
                switch(nextChar()) {
                case 'd':
                case 'D':
                    nextChar();
                    if (current != -1 &&
                        ScannerUtilities.isCSSNameCharacter((char)current)) {
                        do {
                            nextChar();
                        } while (current != -1 &&
                                 ScannerUtilities.isCSSNameCharacter
                                 ((char)current));
                        return LexicalUnits.DIMENSION;
                    }
                    return LexicalUnits.RAD;
                }
            default:
                while (current != -1 &&
                       ScannerUtilities.isCSSNameCharacter((char)current)) {
                    nextChar();
                }
                return LexicalUnits.DIMENSION;
            }
        case 's':
        case 'S':
            nextChar();
            return LexicalUnits.S;
        default:
            if (current != -1 &&
                ScannerUtilities.isCSSIdentifierStartCharacter
                ((char)current)) {
                do {
                    nextChar();
                } while (current != -1 &&
                         ScannerUtilities.isCSSNameCharacter((char)current));
                return LexicalUnits.DIMENSION;
            }
            return (integer) ? LexicalUnits.INTEGER : LexicalUnits.REAL;
        }
    }

    /**
     * Scans an escape sequence, if one.
     */
    protected void escape() throws IOException {
        if (ScannerUtilities.isCSSHexadecimalCharacter((char)current)) {
            nextChar();
            if (!ScannerUtilities.isCSSHexadecimalCharacter((char)current)) {
                if (ScannerUtilities.isCSSSpace((char)current)) {
                    nextChar();
                }
                return;
            }
            nextChar();
            if (!ScannerUtilities.isCSSHexadecimalCharacter((char)current)) {
                if (ScannerUtilities.isCSSSpace((char)current)) {
                    nextChar();
                }
                return;
            }
            nextChar();
            if (!ScannerUtilities.isCSSHexadecimalCharacter((char)current)) {
                if (ScannerUtilities.isCSSSpace((char)current)) {
                    nextChar();
                }
                return;
            }
            nextChar();
            if (!ScannerUtilities.isCSSHexadecimalCharacter((char)current)) {
                if (ScannerUtilities.isCSSSpace((char)current)) {
                    nextChar();
                }
                return;
            }
            nextChar();
            if (!ScannerUtilities.isCSSHexadecimalCharacter((char)current)) {
                if (ScannerUtilities.isCSSSpace((char)current)) {
                    nextChar();
                }
                return;
            }
        }
        if ((current >= ' ' && current <= '~') || current >= 128) {
            nextChar();
            return;
        }
        throw new ParseException("character",
                                 reader.getLine(),
                                 reader.getColumn());
    }

    /**
     * Compares the given int with the given character, ignoring case.
     */
    protected static boolean isEqualIgnoreCase(int i, char c) {
        return (i == -1) ? false : Character.toLowerCase((char)i) == c;
    }

    /**
     * Sets the value of the current char to the next character or -1 if the
     * end of stream has been reached.
     */
    protected int nextChar() throws IOException {
        current = reader.read();

        if (current == -1) {
            return current;
        }

        if (position == buffer.length) {
            // list is full, grow to 1.5 * size
            char[] t = new char[ 1 + position + position / 2];
            System.arraycopy( buffer, 0, t, 0, position );
            buffer = t;
        }

        return buffer[position++] = (char)current;
    }
}
