/*
 * Copyright (c) 2003 The Visigoth Software Society. All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowledgement:
 *       "This product includes software developed by the
 *        Visigoth Software Society (http://www.visigoths.org/)."
 *    Alternately, this acknowledgement may appear in the software
 *    itself, if and wherever such third-party acknowledgements
 *    normally appear.
 *
 * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names
 *    of the project contributors may be used to endorse or promote
 *    products derived from this software without prior written
 *    permission. For written permission, please contact
 *    visigoths@visigoths.org.
 *
 * 5. Products derived from this software may not be called
 *    "FreeMarker" or "Visigoth" nor may "FreeMarker" or "Visigoth"
 *    appear in their names without prior written permission of the
 *    Visigoth Software Society.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Visigoth Software Society. For more
 * information on the Visigoth Software Society, please see
 * http://www.visigoths.org/
 */

package freemarker.eclipse.editors;

import org.eclipse.jface.text.rules.ICharacterScanner;

/**
 * A class containing utility methods. In particular, this class
 * contains metods for performing the lexical analysis needed for
 * correct syntax highlighting.
 *
 * @author  <a href="mailto:stephan@chaquotay.net">Stephan Mueller</a>
 * @author  <a href="mailto:per&#64;percederberg.net">Per Cederberg</a>
 * @version $Id: FreemarkerTools.java,v 1.3 2004/03/03 21:55:43 stephanmueller Exp $
 */
public class FreemarkerTools {

    /**
     * The FreeMarker comment start character sequence.
     */
    private static final char[] COMMENT_START = "<#--".toCharArray();

    /**
     * The FreeMarker comment end character sequence.
     */
    private static final char[] COMMENT_END = "-->".toCharArray();

    /**
     * The XML comment start character sequence.
     */
    private static final char[] XML_COMMENT_START = "<!--".toCharArray();

    /**
     * The XML comment end character sequence.
     */
    private static final char[] XML_COMMENT_END = "-->".toCharArray();

    /**
     * Checks for a FreeMarker comment start. If a matching character
     * sequence was detected, it will be read. If no match was found
     * starting at the first character returned by the scanner, false
     * will be returned and any characters read will be unread
     * (leaving the scanner in the original state).
     *
     * @param scanner        the character scanner
     *
     * @return true if a comment start was read, or
     *         false otherwise
     */
    public static boolean readCommentStart(ICharacterScanner scanner) {
        return readSequence(scanner, COMMENT_START);
    }

    /**
     * Reads characters until a FreeMarker comment end is found.
     *
     * @param scanner        the character scanner
     */
    public static void readCommentEnd(ICharacterScanner scanner) {
        int  c;

        do {
            c = scanner.read();
            if (c == '-') {
                scanner.unread();
                if (readSequence(scanner, COMMENT_END)) {
                    break;
                } else {
                    c = scanner.read();
                }
            }
        } while (c != ICharacterScanner.EOF);
    }

    /**
     * Checks for a FreeMarker directive start. If a matching
     * character sequence was detected, it will be read. If no match
     * was found starting at the first character returned by the
     * scanner, false will be returned and any characters read will
     * be unread (leaving the scanner in the original state).
     *
     * @param scanner        the character scanner
     *
     * @return true if a directive start was read, or
     *         false otherwise
     */
    public static boolean readDirectiveStart(ICharacterScanner scanner) {
        int  c;

        c = scanner.read();
        if (c != '<') {
            scanner.unread();
            return false;
        }
        c = scanner.read();
        if (c != '#' && c != '@' && c != '/') {
            scanner.unread();
            scanner.unread();
            return false;
        }
        if (c == '/') {
            c = scanner.read();
            if (c != '#' && c != '@') {
                scanner.unread();
                scanner.unread();
                scanner.unread();
                return false;
            }
        }
        return true;
    }

    /**
     * Reads characters until a FreeMarker directive end is found.
     * This method attempts to detect and handle '>' characters used
     * inside parenthesized expressions, as well as quoted strings
     * and FreeMarker comments.
     *
     * @param scanner        the character scanner
     */
    public static void readDirectiveEnd(ICharacterScanner scanner) {
        int  parens = 0;
        int  c;

        do {
            c = scanner.read();
            if (c == '<') {
                scanner.unread();
                if (readCommentStart(scanner)) {
                    readCommentEnd(scanner);
                } else {
                    c = scanner.read();
                }
            } else if (c == '"') {
                readQuoteEnd(scanner);
            } else if (c == '(') {
                parens++;
            } else if (c == ')') {
                parens--;
            }
        } while (c != ICharacterScanner.EOF
              && (c != '>' || parens > 0));
    }

    /**
     * Checks for a FreeMarker interpolation start. If a matching
     * character sequence was detected, it will be read. If no match
     * was found starting at the first character returned by the
     * scanner, false will be returned and any characters read will
     * be unread (leaving the scanner in the original state).
     *
     * @param scanner        the character scanner
     *
     * @return true if an interpolation start was read, or
     *         false otherwise
     */
    public static boolean readInterpolationStart(ICharacterScanner scanner) {
        int  c;

        c = scanner.read();
        if (c != '$' && c != '#') {
            scanner.unread();
            return false;
        }
        c = scanner.read();
        if (c != '{') {
            scanner.unread();
            scanner.unread();
            return false;
        }
        return true;
    }

    /**
     * Reads characters until a FreeMarker comment end is found. This
     * method attempts to detect and handle quoted strings and hash
     * literals inside the interpolation.
     *
     * @param scanner        the character scanner
     */
    public static void readInterpolationEnd(ICharacterScanner scanner) {
        int  c;

        do {
            c = scanner.read();
            if (c == '"') {
                readQuoteEnd(scanner);
            } else if (c == '{') {
                readHashEnd(scanner);
            }
        } while (c != ICharacterScanner.EOF && c != '}');
    }

    /**
     * Checks for a literal hash start. If a matching character
     * sequence was detected, it will be read. If no match was found
     * starting at the first character returned by the scanner, false
     * will be returned and any characters read will be unread
     * (leaving the scanner in the original state).
     *
     * @param scanner        the character scanner
     *
     * @return true if a literal hash start was read, or
     *         false otherwise
     */
    public static boolean readHashStart(ICharacterScanner scanner) {
        if (scanner.read() != '{') {
            scanner.unread();
            return false;
        } else {
            return true;
        }
    }

    /**
     * Reads characters until a literal hash end is found.
     *
     * @param scanner        the character scanner
     */
    public static void readHashEnd(ICharacterScanner scanner) {
        int  c;

        do {
            c = scanner.read();
            if (c == '"') {
                readQuoteEnd(scanner);
            } else if (c == '{') {
                readHashEnd(scanner);
            }

        } while (c != ICharacterScanner.EOF && c != '}');
    }

    /**
     * Checks for a quoted string start. If a matching character
     * sequence was detected, it will be read. If no match was found
     * starting at the first character returned by the scanner, false
     * will be returned and any characters read will be unread
     * (leaving the scanner in the original state).
     *
     * @param scanner        the character scanner
     *
     * @return true if a quoted string start was read, or
     *         false otherwise
     */
    public static boolean readQuoteStart(ICharacterScanner scanner) {
        if (scanner.read() != '"') {
            scanner.unread();
            return false;
        } else {
            return true;
        }
    }

    /**
     * Reads characters until a quote end is found. This method
     * attempts to detect and handle character escapes inside the
     * quoted string.
     *
     * @param scanner        the character scanner
     */
    public static void readQuoteEnd(ICharacterScanner scanner) {
        int  c;

        do {
            c = scanner.read();
            if (c == '\\') {
                scanner.read();
            }
        } while (c != ICharacterScanner.EOF && c != '"');
    }

    /**
     * Checks for an XML comment start. If a matching character
     * sequence was detected, it will be read. If no match was found
     * starting at the first character returned by the scanner, false
     * will be returned and any characters read will be unread
     * (leaving the scanner in the original state).
     *
     * @param scanner        the character scanner
     *
     * @return true if an XML comment start was read, or
     *         false otherwise
     */
    public static boolean readXmlCommentStart(ICharacterScanner scanner) {
        return readSequence(scanner, XML_COMMENT_START);
    }

    /**
     * Reads characters until an XML comment end is found. This
     * method attempts to detect and handle FreeMarker comments,
     * directives and interpolations inside the XML comment.
     *
     * @param scanner        the character scanner
     */
    public static void readXmlCommentEnd(ICharacterScanner scanner) {
        int  c;

        do {
            c = scanner.read();
            if (c == '$' || c == '#') {
                scanner.unread();
                if (readInterpolationStart(scanner)) {
                    readInterpolationEnd(scanner);
                } else {
                    c = scanner.read();
                }
            } else if (c == '<') {
                scanner.unread();
                if (readCommentStart(scanner)) {
                    readCommentEnd(scanner);
                } else if (readDirectiveStart(scanner)) {
                    readDirectiveEnd(scanner);
                } else {
                    c = scanner.read();
                }
            } else if (c == '-') {
                scanner.unread();
                if (readSequence(scanner, XML_COMMENT_END)) {
                    break;
                } else {
                    c = scanner.read();
                }
            }
        } while (c != ICharacterScanner.EOF);
    }

    /**
     * Checks for an XML tag start. If a matching character sequence
     * was detected, it will be read. If no match was found starting
     * at the first character returned by the scanner, false will be
     * returned and any characters read will be unread (leaving the
     * scanner in the original state).
     *
     * @param scanner        the character scanner
     *
     * @return true if an XML tag start was read, or
     *         false otherwise
     */
    public static boolean readXmlTagStart(ICharacterScanner scanner) {
        if (scanner.read() != '<') {
            scanner.unread();
            return false;
        } else {
            return true;
        }
    }

    /**
     * Reads characters until an XML tag end is found. This method
     * attempts to detect and handle FreeMarker comments, directives
     * and interpolations inside the tag.
     *
     * @param scanner        the character scanner
     */
    public static void readXmlTagEnd(ICharacterScanner scanner) {
        int  c;

        do {
            c = scanner.read();
            if (c == '$' || c == '#') {
                scanner.unread();
                if (readInterpolationStart(scanner)) {
                    readInterpolationEnd(scanner);
                } else {
                    c = scanner.read();
                }
            } else if (c == '<') {
                scanner.unread();
                if (readCommentStart(scanner)) {
                    readCommentEnd(scanner);
                } else if (readDirectiveStart(scanner)) {
                    readDirectiveEnd(scanner);
                } else {
                    c = scanner.read();
                }
            }
        } while (c != ICharacterScanner.EOF && c != '>');
    }

    /**
     * Checks for a character sequence. If a matching sequence was
     * detected, it will be read. If no match was found starting at
     * the first character returned by the scanner, false will be
     * returned and any characters read will be unread (leaving the
     * scanner in the original state).
     *
     * @param scanner        the character scanner
     *
     * @return true if a character sequence was read, or
     *         false otherwise
     */
    private static boolean readSequence(ICharacterScanner scanner,
                                        char[] chars) {

        int  pos = 0;
        int  c;

        do {
            c = scanner.read();
            if (chars[pos] != c) {
                while (pos-- >= 0) {
                    scanner.unread();
                }
                return false;
            }
            pos++;
        } while (pos < chars.length);

        return true;
    }
}
