/*
 * Copyright 2004-2013 the Seasar Foundation and the Others.
 *
 * Licensed 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.seasar.extension.sql.parser;

import org.seasar.extension.sql.SqlTokenizer;
import org.seasar.extension.sql.TokenNotClosedRuntimeException;

/**
 * {@link SqlTokenizer}の実装クラスです。
 * 
 * @author higa
 * 
 */
public class SqlTokenizerImpl implements SqlTokenizer {

    private String sql;

    private int position = 0;

    private String token;

    private int tokenType = SQL;

    private int nextTokenType = SQL;

    private int bindVariableNum = 0;

    /**
     * <code>SqlTokenizerImpl</code>を作成します。
     * 
     * @param sql
     */
    public SqlTokenizerImpl(String sql) {
        this.sql = sql;
    }

    public String getSql() {
        return sql;
    }

    public int getPosition() {
        return position;
    }

    public String getToken() {
        return token;
    }

    public String getBefore() {
        return sql.substring(0, position);
    }

    public String getAfter() {
        return sql.substring(position);
    }

    public int getTokenType() {
        return tokenType;
    }

    public int getNextTokenType() {
        return nextTokenType;
    }

    public int next() {
        if (position >= sql.length()) {
            token = null;
            tokenType = EOF;
            nextTokenType = EOF;
            return tokenType;
        }
        switch (nextTokenType) {
        case SQL:
            parseSql();
            break;
        case COMMENT:
            parseComment();
            break;
        case ELSE:
            parseElse();
            break;
        case BIND_VARIABLE:
            parseBindVariable();
            break;
        default:
            parseEof();
            break;
        }
        return tokenType;
    }

    /**
     * SQLを解析します。
     */
    protected void parseSql() {
        int commentStartPos = sql.indexOf("/*", position);
        int commentStartPos2 = sql.indexOf("#*", position);
        if (0 < commentStartPos2 && commentStartPos2 < commentStartPos) {
            commentStartPos = commentStartPos2;
        }
        int lineCommentStartPos = sql.indexOf("--", position);
        int bindVariableStartPos = sql.indexOf("?", position);
        int elseCommentStartPos = -1;
        int elseCommentLength = -1;
        if (lineCommentStartPos >= 0) {
            int skipPos = skipWhitespace(lineCommentStartPos + 2);
            if (skipPos + 4 < sql.length()
                    && "ELSE".equals(sql.substring(skipPos, skipPos + 4))) {
                elseCommentStartPos = lineCommentStartPos;
                elseCommentLength = skipPos + 4 - lineCommentStartPos;
            }
        }
        int nextStartPos = getNextStartPos(commentStartPos,
                elseCommentStartPos, bindVariableStartPos);
        if (nextStartPos < 0) {
            token = sql.substring(position);
            nextTokenType = EOF;
            position = sql.length();
            tokenType = SQL;
        } else {
            token = sql.substring(position, nextStartPos);
            tokenType = SQL;
            boolean needNext = nextStartPos == position;
            if (nextStartPos == commentStartPos) {
                nextTokenType = COMMENT;
                position = commentStartPos + 2;
            } else if (nextStartPos == elseCommentStartPos) {
                nextTokenType = ELSE;
                position = elseCommentStartPos + elseCommentLength;
            } else if (nextStartPos == bindVariableStartPos) {
                nextTokenType = BIND_VARIABLE;
                position = bindVariableStartPos;
            }
            if (needNext) {
                next();
            }
        }
    }

    /**
     * 次の開始位置を返します。
     * 
     * @param commentStartPos
     *            コメントの開始位置
     * @param elseCommentStartPos
     *            ELSEコメントの開始位置
     * @param bindVariableStartPos
     *            バインド変数の開始位置
     * @return 次の開始位置
     */
    protected int getNextStartPos(int commentStartPos, int elseCommentStartPos,
            int bindVariableStartPos) {

        int nextStartPos = -1;
        if (commentStartPos >= 0) {
            nextStartPos = commentStartPos;
        }
        if (elseCommentStartPos >= 0
                && (nextStartPos < 0 || elseCommentStartPos < nextStartPos)) {
            nextStartPos = elseCommentStartPos;
        }
        if (bindVariableStartPos >= 0
                && (nextStartPos < 0 || bindVariableStartPos < nextStartPos)) {
            nextStartPos = bindVariableStartPos;
        }
        return nextStartPos;
    }

    /**
     * コメントを解析します。
     */
    protected void parseComment() {
        int commentEndPos = sql.indexOf("*/", position);
        int commentEndPos2 = sql.indexOf("*#", position);
        if (0 < commentEndPos2 && commentEndPos2 < commentEndPos) {
            commentEndPos = commentEndPos2;
        }
        if (commentEndPos < 0) {
            throw new TokenNotClosedRuntimeException("*/", sql
                    .substring(position));
        }
        token = sql.substring(position, commentEndPos);
        nextTokenType = SQL;
        position = commentEndPos + 2;
        tokenType = COMMENT;
    }

    /**
     * バインド変数を解析します。
     */
    protected void parseBindVariable() {
        token = nextBindVariableName();
        nextTokenType = SQL;
        position += 1;
        tokenType = BIND_VARIABLE;
    }

    /**
     * ELSEを解析します。
     */
    protected void parseElse() {
        token = null;
        nextTokenType = SQL;
        tokenType = ELSE;
    }

    /**
     * SQLの終わりを解析します。
     */
    protected void parseEof() {
        token = null;
        tokenType = EOF;
        nextTokenType = EOF;
    }

    /**
     * 位置パラメータ用のバインド変数名を返します。
     * 
     * @return 位置パラメータ用のバインド変数名
     */
    protected String nextBindVariableName() {
        return "$" + ++bindVariableNum;
    }

    public String skipToken() {
        int index = sql.length();
        char quote = position < sql.length() ? sql.charAt(position) : '\0';
        boolean quoting = quote == '\'' || quote == '(';
        if (quote == '(') {
            quote = ')';
        }
        for (int i = quoting ? position + 1 : position; i < sql.length(); ++i) {
            char c = sql.charAt(i);
            if ((Character.isWhitespace(c) || c == ',' || c == ')' || c == '(')
                    && !quoting) {
                index = i;
                break;
            } else if (c == '/' && i + 1 < sql.length()
                    && sql.charAt(i + 1) == '*') {
                index = i;
                break;
            } else if (c == '-' && i + 1 < sql.length()
                    && sql.charAt(i + 1) == '-') {
                index = i;
                break;
            } else if (quoting && quote == '\'' && c == '\''
                    && (i + 1 >= sql.length() || sql.charAt(i + 1) != '\'')) {
                index = i + 1;
                break;
            } else if (quoting && c == quote) {
                index = i + 1;
                break;
            }
        }
        token = sql.substring(position, index);
        tokenType = SQL;
        nextTokenType = SQL;
        position = index;
        return token;
    }

    public String skipWhitespace() {
        int index = skipWhitespace(position);
        token = sql.substring(position, index);
        position = index;
        return token;
    }

    private int skipWhitespace(int position) {
        int index = sql.length();
        for (int i = position; i < sql.length(); ++i) {
            char c = sql.charAt(i);
            if (!Character.isWhitespace(c)) {
                index = i;
                break;
            }
        }
        return index;
    }
}