/* Copyright (c) 2001-2018, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * 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.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS 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 HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR 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.
 */


package org.hsqldb;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;

import org.hsqldb.error.Error;
import org.hsqldb.error.ErrorCode;
import org.hsqldb.lib.ArrayUtil;
import org.hsqldb.lib.StringUtil;
import org.hsqldb.types.DTIType;
import org.hsqldb.types.TimestampData;
import org.hsqldb.types.Types;

/**
 * collection of static methods to convert Date and Timestamp strings
 * into corresponding Java objects and perform other Calendar related
 * operation.<p>
 *
 * Was reviewed for 1.7.2 resulting in centralising all DATETIME related
 * operations.<p>
 *
 * From version 2.0.0, HSQLDB supports TIME ZONE with datetime types. The
 * values are stored internally as UTC seconds from 1970, regardless of the
 * time zone of the JVM, and converted as and when required, to the local
 * timezone.
 *
 * @author Fred Toussi (fredt@users dot sourceforge.net)
 * @version 2.4.1
 * @since 1.7.0
 */
public class HsqlDateTime {

    /**
     * A reusable static value for today's date. Should only be accessed
     * by getToday()
     */
    public static Locale         defaultLocale  = Locale.UK;
    public static final Calendar tempCalDefault = new GregorianCalendar();
    public static final Calendar tempCalGMT =
        new GregorianCalendar(TimeZone.getTimeZone("GMT"), defaultLocale);
    private static final Date   tempDate        = new Date(0);
    private static final String sdfdPattern     = "yyyy-MM-dd";
    static SimpleDateFormat     sdfd = new SimpleDateFormat(sdfdPattern);
    private static final String sdftPattern     = "HH:mm:ss";
    static SimpleDateFormat     sdft = new SimpleDateFormat(sdftPattern);
    private static final String sdftsPattern    = "yyyy-MM-dd HH:mm:ss";
    static SimpleDateFormat     sdfts = new SimpleDateFormat(sdftsPattern);
    private static final String sdftsSysPattern = "yyyy-MM-dd HH:mm:ss.SSS";
    static SimpleDateFormat sdftsSys = new SimpleDateFormat(sdftsSysPattern);

    static {
        tempCalGMT.setLenient(false);
        sdfd.setCalendar(new GregorianCalendar(TimeZone.getTimeZone("GMT"),
                                               defaultLocale));
        sdfd.setLenient(false);
        sdft.setCalendar(new GregorianCalendar(TimeZone.getTimeZone("GMT"),
                                               defaultLocale));
        sdft.setLenient(false);
        sdfts.setCalendar(new GregorianCalendar(TimeZone.getTimeZone("GMT"),
                defaultLocale));
        sdfts.setLenient(false);
    }

    public static long getDateSeconds(String s) {

        try {
            synchronized (sdfd) {
                java.util.Date d = sdfd.parse(s);

                return d.getTime() / 1000;
            }
        } catch (Exception e) {
            throw Error.error(ErrorCode.X_22007);
        }
    }

    public static String getDateString(long seconds) {

        synchronized (sdfd) {
            sysDate.setTime(seconds * 1000);

            return sdfd.format(sysDate);
        }
    }

    public static long getTimestampSeconds(String s) {

        try {
            synchronized (sdfts) {
                java.util.Date d = sdfts.parse(s);

                return d.getTime() / 1000;
            }
        } catch (Exception e) {
            throw Error.error(ErrorCode.X_22007);
        }
    }

    public static void getTimestampString(StringBuffer sb, long seconds,
                                          int nanos, int scale) {

        synchronized (sdfts) {
            tempDate.setTime(seconds * 1000);
            sb.append(sdfts.format(tempDate));

            if (scale > 0) {
                sb.append('.');
                sb.append(StringUtil.toZeroPaddedString(nanos, 9, scale));
            }
        }
    }

    public static String getTimestampString(long millis) {

        synchronized (sdfts) {
            sysDate.setTime(millis);

            return sdfts.format(sysDate);
        }
    }

    private static java.util.Date sysDate = new java.util.Date();

    public static String getSystemTimeString() {

        synchronized (sdftsSys) {
            sysDate.setTime(System.currentTimeMillis());

            return sdftsSys.format(sysDate);
        }
    }

    private static void resetToDate(Calendar cal) {

        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
    }

    private static void resetToTime(Calendar cal) {

        cal.set(Calendar.YEAR, 1970);
        cal.set(Calendar.MONTH, 0);
        cal.set(Calendar.DATE, 1);
        cal.set(Calendar.MILLISECOND, 0);
    }

    public static long convertMillisToCalendar(Calendar calendar,
            long millis) {

        synchronized (tempCalGMT) {
            synchronized (calendar) {
                calendar.clear();
                tempCalGMT.setTimeInMillis(millis);
                calendar.set(tempCalGMT.get(Calendar.YEAR),
                             tempCalGMT.get(Calendar.MONTH),
                             tempCalGMT.get(Calendar.DAY_OF_MONTH),
                             tempCalGMT.get(Calendar.HOUR_OF_DAY),
                             tempCalGMT.get(Calendar.MINUTE),
                             tempCalGMT.get(Calendar.SECOND));

                return calendar.getTimeInMillis();
            }
        }
    }

    public static long convertMillisFromCalendar(Calendar calendar,
            long millis) {
        return convertMillisFromCalendar(tempCalGMT, calendar, millis);
    }

    public static long convertMillisFromCalendar(Calendar clendarGMT,
            Calendar calendar, long millis) {

        synchronized (clendarGMT) {
            synchronized (calendar) {
                clendarGMT.clear();
                calendar.setTimeInMillis(millis);
                clendarGMT.set(calendar.get(Calendar.YEAR),
                               calendar.get(Calendar.MONTH),
                               calendar.get(Calendar.DAY_OF_MONTH),
                               calendar.get(Calendar.HOUR_OF_DAY),
                               calendar.get(Calendar.MINUTE),
                               calendar.get(Calendar.SECOND));

                return clendarGMT.getTimeInMillis();
            }
        }
    }

    /**
     * Sets the time in the given Calendar using the given milliseconds value; wrapper method to
     * allow use of more efficient JDK1.4 method on JDK1.4 (was protected in earlier versions).
     *
     * @param       cal                             the Calendar
     * @param       millis                  the time value in milliseconds
     */
    public static void setTimeInMillis(Calendar cal, long millis) {
        cal.setTimeInMillis(millis);
    }

    public static long convertToNormalisedTime(long t) {
        return convertToNormalisedTime(t, tempCalGMT);
    }

    public static long convertToNormalisedTime(long t, Calendar cal) {

        synchronized (cal) {
            setTimeInMillis(cal, t);
            resetToDate(cal);

            long t1 = cal.getTimeInMillis();

            return t - t1;
        }
    }

    public static long getNormalisedTime(long t) {

        Calendar cal = tempCalGMT;

        synchronized (cal) {
            setTimeInMillis(cal, t);
            resetToTime(cal);

            return cal.getTimeInMillis();
        }
    }

    public static long getNormalisedTime(Calendar cal, long t) {

        synchronized (cal) {
            setTimeInMillis(cal, t);
            resetToTime(cal);

            return cal.getTimeInMillis();
        }
    }

    public static long getNormalisedDate(long d) {

        synchronized (tempCalGMT) {
            setTimeInMillis(tempCalGMT, d);
            resetToDate(tempCalGMT);

            return tempCalGMT.getTimeInMillis();
        }
    }

    public static long getNormalisedDate(Calendar cal, long t) {

        synchronized (cal) {
            setTimeInMillis(cal, t);
            resetToDate(cal);

            return cal.getTimeInMillis();
        }
    }

    public static int getZoneSeconds(Calendar cal) {
        return (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET))
               / 1000;
    }

    public static int getZoneMillis(Calendar cal, long millis) {
        return cal.getTimeZone().getOffset(millis);
    }

    /**
     * Returns the indicated part of the given millisecond date object.
     * @param m the millisecond time value from which to extract the indicated part
     * @param part an integer code corresponding to the desired date part
     * @return the indicated part of the given <code>java.util.Date</code> object
     */
    public static int getDateTimePart(long m, int part) {

        synchronized (tempCalGMT) {
            tempCalGMT.setTimeInMillis(m);

            return tempCalGMT.get(part);
        }
    }

    /**
     * truncates millisecond date object
     */
    public static long getTruncatedPart(long m, int part) {

        synchronized (tempCalGMT) {
            tempCalGMT.setTimeInMillis(m);

            switch (part) {

                case DTIType.WEEK_OF_YEAR : {
                    int dayWeek = tempCalGMT.get(Calendar.DAY_OF_WEEK);

                    tempCalGMT.add(Calendar.DAY_OF_YEAR, 1 - dayWeek);
                    resetToDate(tempCalGMT);

                    break;
                }
                default : {
                    zeroFromPart(tempCalGMT, part);

                    break;
                }
            }

            return tempCalGMT.getTimeInMillis();
        }
    }

    /**
     * rounded millisecond date object
     */
    public static long getRoundedPart(long m, int part) {

        synchronized (tempCalGMT) {
            tempCalGMT.setTimeInMillis(m);

            switch (part) {

                case Types.SQL_INTERVAL_YEAR :
                    if (tempCalGMT.get(Calendar.MONTH) > 6) {
                        tempCalGMT.add(Calendar.YEAR, 1);
                    }
                    break;

                case Types.SQL_INTERVAL_MONTH :
                    if (tempCalGMT.get(Calendar.DAY_OF_MONTH) > 15) {
                        tempCalGMT.add(Calendar.MONTH, 1);
                    }
                    break;

                case Types.SQL_INTERVAL_DAY :
                    if (tempCalGMT.get(Calendar.HOUR_OF_DAY) > 11) {
                        tempCalGMT.add(Calendar.DAY_OF_MONTH, 1);
                    }
                    break;

                case Types.SQL_INTERVAL_HOUR :
                    if (tempCalGMT.get(Calendar.MINUTE) > 29) {
                        tempCalGMT.add(Calendar.HOUR_OF_DAY, 1);
                    }
                    break;

                case Types.SQL_INTERVAL_MINUTE :
                    if (tempCalGMT.get(Calendar.SECOND) > 29) {
                        tempCalGMT.add(Calendar.MINUTE, 1);
                    }
                    break;

                case Types.SQL_INTERVAL_SECOND :
                    if (tempCalGMT.get(Calendar.MILLISECOND) > 499) {
                        tempCalGMT.add(Calendar.SECOND, 1);
                    }
                    break;

                case DTIType.WEEK_OF_YEAR : {
                    int dayYear = tempCalGMT.get(Calendar.DAY_OF_YEAR);
                    int year    = tempCalGMT.get(Calendar.YEAR);
                    int week    = tempCalGMT.get(Calendar.WEEK_OF_YEAR);
                    int day     = tempCalGMT.get(Calendar.DAY_OF_WEEK);

                    tempCalGMT.clear();
                    tempCalGMT.set(Calendar.YEAR, year);

                    if (day > 3) {
                        week++;
                    }

                    if (week == 1 && (dayYear > 356 || dayYear < 7)) {
                        tempCalGMT.set(Calendar.DAY_OF_YEAR, dayYear);

                        while (true) {
                            if (tempCalGMT.get(Calendar.DAY_OF_WEEK) == 1) {
                                return tempCalGMT.getTimeInMillis();
                            }

                            tempCalGMT.add(Calendar.DAY_OF_YEAR, -1);
                        }
                    }

                    tempCalGMT.set(Calendar.WEEK_OF_YEAR, week);

                    return tempCalGMT.getTimeInMillis();
                }
            }

            zeroFromPart(tempCalGMT, part);

            return tempCalGMT.getTimeInMillis();
        }
    }

    static void zeroFromPart(Calendar cal, int part) {

        switch (part) {

            case Types.SQL_INTERVAL_YEAR :
                cal.set(Calendar.MONTH, 0);
            case Types.SQL_INTERVAL_MONTH :
                cal.set(Calendar.DAY_OF_MONTH, 1);
            case Types.SQL_INTERVAL_DAY :
                cal.set(Calendar.HOUR_OF_DAY, 0);
            case Types.SQL_INTERVAL_HOUR :
                cal.set(Calendar.MINUTE, 0);
            case Types.SQL_INTERVAL_MINUTE :
                cal.set(Calendar.SECOND, 0);
            case Types.SQL_INTERVAL_SECOND :
                cal.set(Calendar.MILLISECOND, 0);
            default :
        }
    }

    //J-

    private static final char[][] dateTokens     = {
        { 'R', 'R', 'R', 'R' }, { 'I', 'Y', 'Y', 'Y' }, { 'Y', 'Y', 'Y', 'Y' },
        { 'I', 'Y' }, { 'Y', 'Y' },
        { 'B', 'C' }, { 'B', '.', 'C', '.' }, { 'A', 'D' }, { 'A', '.', 'D', '.' },
        { 'M', 'O', 'N' }, { 'M', 'O', 'N', 'T', 'H' },
        { 'M', 'M' },
        { 'D', 'A', 'Y' }, { 'D', 'Y' },
        { 'W', 'W' }, { 'I', 'W' }, { 'D', 'D' }, { 'D', 'D', 'D' },
        { 'W' },
        { 'H', 'H', '2', '4' }, { 'H', 'H', '1', '2' }, { 'H', 'H' },
        { 'M', 'I' },
        { 'S', 'S' },
        { 'A', 'M' }, { 'P', 'M' }, { 'A', '.', 'M', '.' }, { 'P', '.', 'M', '.' },
        { 'F', 'F' }
    };

    private static final String[] javaDateTokens = {
        "yyyy", "'*IYYY'", "yyyy",
        "'*IY'", "yy",
        "G", "G", "G", "G",
        "MMM", "MMMMM",
        "MM",
        "EEEE", "EE",
        "'*WW'", "'*IW'", "dd", "D",
        "'*W'",
        "HH", "KK", "KK",
        "mm", "ss",
        "aaa", "aaa", "aaa", "aaa",
        "S"
    };

    private static final int[] sqlIntervalCodes = {
        -1, -1, Types.SQL_INTERVAL_YEAR,
        -1, Types.SQL_INTERVAL_YEAR,
        -1, -1, -1, -1,
        Types.SQL_INTERVAL_MONTH, Types.SQL_INTERVAL_MONTH,
        Types.SQL_INTERVAL_MONTH,
        -1, -1,
        DTIType.WEEK_OF_YEAR, -1, Types.SQL_INTERVAL_DAY, Types.SQL_INTERVAL_DAY,
        -1,
        Types.SQL_INTERVAL_HOUR, -1, Types.SQL_INTERVAL_HOUR,
        Types.SQL_INTERVAL_MINUTE,
        Types.SQL_INTERVAL_SECOND,
        -1,-1,-1,-1,
        -1
    };

    //J+

    /** Indicates end-of-input */
    private static final char e = 0xffff;

    public static TimestampData toDate(String string, String pattern,
                                       SimpleDateFormat format,
                                       boolean fraction) {

        long   millis;
        int    nanos       = 0;
        String javaPattern = HsqlDateTime.toJavaDatePattern(pattern);
        String tempPattern = null;
        int    matchIndex  = javaPattern.indexOf("*IY");

        if (matchIndex >= 0) {
            throw Error.error(ErrorCode.X_22511);
        }

        matchIndex = javaPattern.indexOf("*WW");

        if (matchIndex >= 0) {
            throw Error.error(ErrorCode.X_22511);
        }

        matchIndex = javaPattern.indexOf("*W");

        if (matchIndex >= 0) {
            throw Error.error(ErrorCode.X_22511);
        }

        matchIndex = javaPattern.indexOf("S");

        if (matchIndex >= 0) {
            tempPattern = javaPattern;
            javaPattern = javaPattern.substring(0, matchIndex)
                          + javaPattern.substring(matchIndex + 1);
        }

        try {
            format.applyPattern(javaPattern);

            millis = format.parse(string).getTime();
        } catch (Exception e) {
            throw Error.error(ErrorCode.X_22007, e.toString());
        }

        if (matchIndex >= 0 && fraction) {
            javaPattern = tempPattern;

            try {
                format.applyPattern(javaPattern);

                long tempMillis = format.parse(string).getTime();
                int  factor     = 1;

                tempMillis -= millis;
                nanos      = (int) tempMillis;

                while (tempMillis > 1000) {
                    tempMillis /= 10;
                    factor     *= 10;
                }

                nanos *= (1000000 / factor);
            } catch (Exception e) {
                throw Error.error(ErrorCode.X_22007, e.toString());
            }
        }

        return new TimestampData(millis / 1000, nanos, 0);
    }

    public static String toFormattedDate(Date date, String pattern,
                                         SimpleDateFormat format) {

        String javaPattern = HsqlDateTime.toJavaDatePattern(pattern);

        try {
            format.applyPattern(javaPattern);
        } catch (Exception e) {
            throw Error.error(ErrorCode.X_22511);
        }

        String result     = format.format(date);
        int    matchIndex = result.indexOf("*IY");

        if (matchIndex >= 0) {
            Calendar cal         = format.getCalendar();
            int      matchLength = 3;
            int      temp        = result.indexOf("*IYYY");

            if (temp >= 0) {
                matchLength = 5;
                matchIndex  = temp;
            }

            int year       = cal.get(Calendar.YEAR);
            int weekOfYear = cal.get(Calendar.WEEK_OF_YEAR);

            if (weekOfYear == 1 && cal.get(Calendar.DAY_OF_YEAR) > 360) {
                year++;
            } else if (weekOfYear > 51 && cal.get(Calendar.DAY_OF_YEAR) < 4) {
                year--;
            }

            String yearString = String.valueOf(year);

            if (matchLength == 3) {
                yearString = yearString.substring(yearString.length() - 2);
            }

            StringBuilder sb = new StringBuilder(result);

            sb.replace(matchIndex, matchIndex + matchLength, yearString);

            result = sb.toString();
        }

        matchIndex = result.indexOf("*WW");

        if (matchIndex >= 0) {
            Calendar      cal         = format.getCalendar();
            int           matchLength = 3;
            int           dayOfYear   = cal.get(Calendar.DAY_OF_YEAR);
            int           weekOfYear  = ((dayOfYear - 1) / 7) + 1;
            StringBuilder sb          = new StringBuilder(result);

            sb.replace(matchIndex, matchIndex + matchLength,
                       String.valueOf(weekOfYear));

            result = sb.toString();
        }

        matchIndex = result.indexOf("*IW");

        if (matchIndex >= 0) {
            Calendar cal         = format.getCalendar();
            int      matchLength = 3;
            int weekOfYear = getDateTimePart(date.getTime(),
                                             Calendar.WEEK_OF_YEAR);
            StringBuilder sb = new StringBuilder(result);

            sb.replace(matchIndex, matchIndex + matchLength,
                       String.valueOf(weekOfYear));

            result = sb.toString();
        }

        matchIndex = result.indexOf("*W");

        if (matchIndex >= 0) {
            Calendar      cal         = format.getCalendar();
            int           matchLength = 2;
            int           dayOfMonth  = cal.get(Calendar.DAY_OF_MONTH);
            int           weekOfMonth = ((dayOfMonth - 1) / 7) + 1;
            StringBuilder sb          = new StringBuilder(result);

            sb.replace(matchIndex, matchIndex + matchLength,
                       String.valueOf(weekOfMonth));

            result = sb.toString();
        }

        return result;
    }

    /**
     * Converts the given format into a pattern accepted by <code>java.text.SimpleDataFormat</code>
     *
     * @param format
     */
    public static String toJavaDatePattern(String format) {

        int          len = format.length();
        char         ch;
        StringBuffer sb               = new StringBuffer(len);
        Tokenizer    tokenizer        = new Tokenizer();
        int          limitQuotedToken = -1;

        for (int i = 0; i <= len; i++) {
            ch = (i == len) ? e
                            : format.charAt(i);

            if (tokenizer.isInQuotes()) {
                if (tokenizer.isQuoteChar(ch)) {
                    ch = '\'';
                } else if (ch == '\'') {

                    // double the single quote
                    sb.append(ch);
                }

                sb.append(ch);

                continue;
            }

            if (!tokenizer.next(ch, i)) {
                if (tokenizer.consumed) {
                    int    index = tokenizer.getLastMatch();
                    String s     = javaDateTokens[index];

                    // consecutive quoted tokens
                    if (s.startsWith("\'") && s.endsWith("\'")) {
                        if (limitQuotedToken == sb.length()) {
                            sb.setLength(sb.length() - 1);

                            s = s.substring(1);
                        }

                        limitQuotedToken = sb.length() + s.length();
                    }

                    sb.append(s);

                    i = tokenizer.matchOffset;
                } else {
                    if (tokenizer.isQuoteChar(ch)) {
                        ch = '\'';

                        sb.append(ch);
                    } else if (tokenizer.isLiteral(ch)) {
                        sb.append(ch);
                    } else if (ch == e) {

                        //
                    } else {
                        throw Error.error(ErrorCode.X_22007,
                                          format.substring(i));
                    }
                }

                tokenizer.reset();
            }
        }

        if (tokenizer.isInQuotes()) {
            throw Error.error(ErrorCode.X_22007);
        }

        String javaPattern = sb.toString();

        return javaPattern;
    }

    public static int toStandardIntervalPart(String format) {

        int       len = format.length();
        char      ch;
        Tokenizer tokenizer = new Tokenizer();

        for (int i = 0; i <= len; i++) {
            ch = (i == len) ? e
                            : format.charAt(i);

            if (!tokenizer.next(ch, i)) {
                int index = tokenizer.getLastMatch();

                if (index >= 0) {
                    return sqlIntervalCodes[index];
                }

                return -1;
            }
        }

        return -1;
    }

    /**
     * This class can match 64 tokens at maximum.
     */
    static class Tokenizer {

        private int     lastMatched;
        private int     matchOffset;
        private int     offset;
        private long    state;
        private boolean consumed;
        private boolean isInQuotes;
        private boolean matched;

        //
        private final char    quoteChar;
        private final char[]  literalChars;
        private static char[] defaultLiterals = new char[] {
            ' ', ',', '-', '.', '/', ':', ';'
        };
        char[][]              tokens;

        public Tokenizer() {

            this.quoteChar    = '\"';
            this.literalChars = defaultLiterals;
            tokens            = dateTokens;

            reset();
        }

        /**
         * Resets for next reuse.
         *
         */
        public void reset() {

            lastMatched = -1;
            offset      = -1;
            state       = 0;
            consumed    = false;
            matched     = false;
        }

        /**
         * Returns the length of a token to match.
         */
        public int length() {
            return offset;
        }

        /**
         * Returns an index of the last matched token.
         */
        public int getLastMatch() {
            return lastMatched;
        }

        /**
         * Indicates whether the last character has been consumed by the matcher.
         */
        public boolean isConsumed() {
            return consumed;
        }

        /**
         * Indicates whether the last character has been consumed by the matcher.
         */
        public boolean wasMatched() {
            return matched;
        }

        /**
         * Indicates if tokenizing a quoted string
         */
        public boolean isInQuotes() {
            return isInQuotes;
        }

        /**
         * returns true if character is the quote char and sets state
         */
        public boolean isQuoteChar(char ch) {

            if (quoteChar == ch) {
                isInQuotes = !isInQuotes;

                return true;
            }

            return false;
        }

        /**
         * Returns true if ch is in the list of literals
         */
        public boolean isLiteral(char ch) {
            return ArrayUtil.isInSortedArray(ch, literalChars);
        }

        /**
         * Checks whether the specified bit is not set.
         *
         * @param bit
         */
        private boolean isZeroBit(int bit) {
            return (state & (1L << bit)) == 0;
        }

        /**
         * Sets the specified bit.
         * @param bit
         */
        private void setBit(int bit) {
            state |= (1L << bit);
        }

        /**
         * Matches the specified character against tokens.
         *
         * @param ch
         * @param position
         */
        public boolean next(char ch, int position) {

            int index = ++offset;
            int len   = offset + 1;
            int left  = 0;

            matched = false;

            for (int i = tokens.length; --i >= 0; ) {
                if (isZeroBit(i)) {
                    if (tokens[i][index] == Character.toUpperCase(ch)) {
                        if (tokens[i].length == len) {
                            setBit(i);

                            lastMatched = i;
                            consumed    = true;
                            matched     = true;
                            matchOffset = position;
                        } else {
                            ++left;
                        }
                    } else {
                        setBit(i);
                    }
                }
            }

            return left > 0;
        }
    }
}
