/*
 * 作成日: 2007/11/27
 */
package blanco.commons.sql.format;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

import blanco.commons.sql.format.valueobject.BlancoSqlToken;

/**
 * BlancoSqlFormatter2: SQL整形ツール. SQL文を決められたルールに従い整形します。
 * 
 * フォーマットを実施するためには、入力されるSQLがSQL文として妥当であることが前提条件となります。
 * そのため、SQL文が妥当でない可能性がある場合には、呼び出し側でtry/catch等で例外処理を行うことを勧めます。
 * このクラスが準拠するSQL整形のルールについては、下記URLを参照ください。
 * http://homepage2.nifty.com/igat/igapyon/diary/2005/ig050613.html
 * 
 * 2005.08.08 Tosiki Iga: ( ) については (*) のようにひっつけるように変更。 <br>
 * 2005.08.03 Tosiki Iga: 決められた例外をスローするように変更しました。
 * 
 * @author Yoshinori WATANABE (a-san) : original version at 2005.07.04.
 * @author Tosiki Iga : marge into blanc Framework at 2005.07.04
 * @deprecated		改造中
 */
public class BlancoSqlFormatter2 {
    private final BlancoSqlParser fParser = new BlancoSqlParser();

    private BlancoSqlRule fRule = null;

    /** 丸カッコが関数のものかどうかを覚える。 */
    private Stack functionBracket = new Stack();

    /**
     * SQL整形ツールのインスタンスを作成します。
     * 
     * @param argRule
     *            SQL変換ルール。
     */
    public BlancoSqlFormatter2(final BlancoSqlRule argRule) {
        fRule = argRule;
    }

    /**
     * 与えられたSQLを整形します。
     * 
     * 1.改行で終了するSQL文は、整形後も改行付きであるようにします。
     * 
     * @param argSql
     *            整形前のSQL文
     * @return 整形後のSQL文
     * @throws BlancoSqlFormatterException
     *             内部的に発生した例外は全てこのクラスになります。
     */
    public String format(final String argSql)
            throws BlancoSqlFormatterException {
        functionBracket.clear();
        try {
        	// 末尾が改行で終わっているかどうか？
            boolean isSqlEndsWithNewLine = false;
            if (argSql.endsWith("\n")) {
                isSqlEndsWithNewLine = true;
            }

            // SQL文を字句ごとに区切って字句のリストにする。
            List list = fParser.parse(argSql);

            // 字句のリストを整形し、字句のリストを返す。ココがスゴイ☆
            list = format(list);

            // 変換結果を１つの文字列に戻す。
            StringBuffer after = new StringBuffer();
            for (int index = 0; index < list.size(); index++) {
                BlancoSqlToken token = (BlancoSqlToken) list.get(index);
                after.append(token.getString());
            }

        	// 最初のSQLが改行で終わっていれば、改行を加える。
            if (isSqlEndsWithNewLine) {
                after.append("\n");
            }
            
            // 整形後のSQLを返す。
            return after.toString();

        } catch (Exception ex) {
            final BlancoSqlFormatterException sqlException = new BlancoSqlFormatterException(
                    ex.toString());
            sqlException.initCause(ex);
            throw sqlException;
        }
    }

    /**
     * 字句の配列を、指定されたSQL書式規則に従って変換します。
     * 
     * @param argList
     *            変換前の字句の配列。ArrayList <Token>
     * @return 変換後の字句の配列。ArrayList <Token>
     */
    private List format(final List list0) {

        // TODO:SQL整形のカスタマイズが必要な方は、ここから、あなたの行いたい書式に変換して下さい。
        // なるべくシンプルで明確な整形ルールにしてください。
        // また、誰も使わないような不必要な選択肢はやめましょう。
    	
    	// 整形とはある規則に従って空白のみを置き換えることである。まず空白を削除する。あとで適切に追加する。
    	ArrayList list1 = new ArrayList();
    	for (int i=0; i<list0.size(); i++) {
        	BlancoSqlToken token = (BlancoSqlToken) list0.get(i);
        	switch (token.getType()) {
        	case BlancoSqlTokenConstants.SPACE:
        		// 空白は除く。
        		break;
         	default:
        		// それ以外のとき
        		list1.add(token);
        	}
    	}
    	
    	// キーワード、および名前(テーブル名、カラム名など)を大文字、小文字に変換する。
    	ArrayList list2 = new ArrayList();
    	for (int i=0; i<list1.size(); i++) {
        	BlancoSqlToken token = (BlancoSqlToken) list1.get(i);
        	switch (token.getType()) {
            case BlancoSqlTokenConstants.KEYWORD:
            	// キーワードのとき、変換規則により、大文字/小文字等に変換する。
                switch (fRule.keyword) {
                case BlancoSqlRule.KEYWORD_NONE:
                	list2.add(token);
                    break;
                case BlancoSqlRule.KEYWORD_UPPER_CASE:
                	token = new BlancoSqlToken(token.getType(), token.getString().toUpperCase());
                	list2.add(token);
                    break;
                case BlancoSqlRule.KEYWORD_LOWER_CASE:
                	token = new BlancoSqlToken(token.getType(), token.getString().toLowerCase());
                	list2.add(token);
                    break;
                default:
                	assert false: fRule.keyword;
                }
                break;
            case BlancoSqlTokenConstants.NAME:
            	// 名前(テーブル名、カラム名など)のとき、変換規則により、大文字/小文字等に変換する。
                switch (fRule.name) {
                case BlancoSqlRule.NAME_NONE:
                	list2.add(token);
                    break;
                case BlancoSqlRule.NAME_UPPER_CASE:
                	token = new BlancoSqlToken(token.getType(), token.getString().toUpperCase());
                    list2.add(token);
                    break;
                case BlancoSqlRule.NAME_LOWER_CASE:
                	token = new BlancoSqlToken(token.getType(), token.getString().toLowerCase());
                    list2.add(token);
                    break;
                default:
                	assert false: fRule.name;
                }
            	break;
        	default:
        		// それ以外のとき
        		list2.add(token);
        	}
    	}


        // ２つ並んだキーワードは１つのキーワードとみなす。(ex."INSERT INTO", "ORDER BY")
        // 近代の言語はキーワードが２つ並ぶことはない。古い言語では、自然言語（つまり人間の言語）に
        // 近づけるため、キーワードが２つ並ぶことがあった。しかし、近代では"ORDER_BY"、あるいは"OrderBy"
        // のように、可読性を損なうことなく、解析しやすい文法を採用している。
    	ArrayList list3 = new ArrayList();
        for (int index = 0; index < list2.size() - 2; index++) {
            BlancoSqlToken t0 = (BlancoSqlToken) list2.get(index);
            BlancoSqlToken t1 = (BlancoSqlToken) list2.get(index + 1);
            BlancoSqlToken t2 = (BlancoSqlToken) list2.get(index + 2);

            if (t0.getType() == BlancoSqlTokenConstants.KEYWORD
                    && t1.getType() == BlancoSqlTokenConstants.KEYWORD &&
                (t0.getString().equalsIgnoreCase("ORDER") ||
                	  t0.getString().equalsIgnoreCase("GROUP")) &&
                	  t1.getString().equalsIgnoreCase("BY")) {
                	BlancoSqlToken t = new BlancoSqlToken(
                			BlancoSqlTokenConstants.KEYWORD, 
                			t0.getString() + " " + t1.getString());
                    list3.add(t);
                    index++;
           }
            // Oracle対応 begin 2007/10/24 A.Watanabe
			// Oracleの外部結合演算子"(+)"を１つの演算子とする。
            else if (t0.getString().equals("(") && t1.getString().equals("+")
					&& t2.getString().equals(")")) {
            	BlancoSqlToken t = new BlancoSqlToken(
            			BlancoSqlTokenConstants.SYMBOL, "(+)");
				list3.add(t);
			}
			// Oracle対応 end
            else {
         	   list3.add(t0);
            }
            // 最後なら、t1,t2も追加
            if (index == list2.size() - 3) {
          	   list3.add(t1);
         	   list3.add(t2);            	
            }
        }



        // インデントを整える。
        int indent = 0;
        // 丸カッコのインデント位置を覚える。
        Stack bracketIndent = new Stack();
        BlancoSqlToken prev = new BlancoSqlToken(BlancoSqlTokenConstants.SPACE, " ");
        boolean encounterBetween = false;
        for (int index = 0; index < list3.size(); index++) {
        	BlancoSqlToken token = (BlancoSqlToken) list3.get(index);
            // System.out.println("@@@"+i+"@"+t);
            if (token.getType() == BlancoSqlTokenConstants.SYMBOL) {
                // indentを１つ増やし、'('のあとで改行。
                if (token.getString().equals("(")) {
                    functionBracket.push(
                            fRule.isFunction(prev.getString()) ? 
                                    Boolean.TRUE :
                                    Boolean.FALSE);
                    bracketIndent.push(new Integer(indent));
                    indent++;
                    index += insertReturnAndIndent(list3, index + 1, indent);
                }
                // indentを１つ増やし、')'の前と後ろで改行。
                else if (token.getString().equals(")")) {
                    indent = ((Integer) bracketIndent.pop()).intValue();
                    // indent--;
                    index += insertReturnAndIndent(list3, index, indent);
                    // i += insertReturnAndIndent(list, i+1, indent);
                    functionBracket.pop();
                }
                // ','の前で改行
                else if (token.getString().equals(",")) {
                    index += insertReturnAndIndent(list3, index, indent);
                } else if (token.getString().equals(";")) {
                    // 2007/11/27 A-san ADD begin 直前にブランクがあればそれを除く。
                	if (prev.getType() == BlancoSqlTokenConstants.SPACE) {
                		list3.remove(index);
                	}
                	// 2007/11/27 A-san ADD end
                    // 2005.07.26 Tosiki Iga とりあえずセミコロンでSQL文がつぶれないように改良
                    indent = 0;
                    index += insertReturnAndIndent(list3, index, indent);
                }
            } else if (token.getType() == BlancoSqlTokenConstants.KEYWORD) {
                // indentを２つ増やし、キーワードの後ろで改行
                if (token.getString().equalsIgnoreCase("DELETE")
                        || token.getString().equalsIgnoreCase("SELECT")
                        || token.getString().equalsIgnoreCase("UPDATE")) {
                    indent += 2;
                    index += insertReturnAndIndent(list3, index + 1, indent);
                }
                // indentを１つ増やし、キーワードの後ろで改行
                if (token.getString().equalsIgnoreCase("INSERT")
                        || token.getString().equalsIgnoreCase("INTO")
                        || token.getString().equalsIgnoreCase("CREATE")
                        || token.getString().equalsIgnoreCase("DROP")
                        || token.getString().equalsIgnoreCase("TRUNCATE")
                        || token.getString().equalsIgnoreCase("TABLE")
                        || token.getString().equalsIgnoreCase("CASE")) {
                    indent++;
                    index += insertReturnAndIndent(list3, index + 1, indent);
                }
                // キーワードの前でindentを１つ減らして改行、キーワードの後ろでindentを戻して改行。
                if (token.getString().equalsIgnoreCase("FROM")
                        || token.getString().equalsIgnoreCase("WHERE")
                        || token.getString().equalsIgnoreCase("SET")
                        || token.getString().equalsIgnoreCase("ORDER BY")
                        || token.getString().equalsIgnoreCase("GROUP BY")
                        || token.getString().equalsIgnoreCase("HAVING")) {
                    index += insertReturnAndIndent(list3, index, indent - 1);
                    index += insertReturnAndIndent(list3, index + 1, indent);
                }
                // キーワードの前でindentを１つ減らして改行、キーワードの後ろでindentを戻して改行。
                if (token.getString().equalsIgnoreCase("VALUES")) {
                    indent--;
                    index += insertReturnAndIndent(list3, index, indent);
                }
                // キーワードの前でindentを１つ減らして改行
                if (token.getString().equalsIgnoreCase("END")) {
                    indent--;
                    index += insertReturnAndIndent(list3, index, indent);
                }
                // キーワードの前で改行
                if (token.getString().equalsIgnoreCase("OR")
                        //|| token.getString().equalsIgnoreCase("AND")
                        || token.getString().equalsIgnoreCase("THEN")
                        || token.getString().equalsIgnoreCase("ELSE")) {
                    index += insertReturnAndIndent(list3, index, indent);
                }
                // キーワードの前で改行
                if (token.getString().equalsIgnoreCase("ON")
                        || token.getString().equalsIgnoreCase("USING")) {
                    index += insertReturnAndIndent(list3, index, indent + 1);
                }
                // キーワードの前で改行。indentを強制的に０にする。
                if (token.getString().equalsIgnoreCase("UNION")
                        || token.getString().equalsIgnoreCase("INTERSECT")
                        || token.getString().equalsIgnoreCase("EXCEPT")) {
                    indent -= 2;
                    index += insertReturnAndIndent(list3, index, indent);
                    index += insertReturnAndIndent(list3, index + 1, indent);
                }
                if (token.getString().equalsIgnoreCase("BETWEEN")) {
                    encounterBetween = true;
                }
                if (token.getString().equalsIgnoreCase("AND")) {
                    // BETWEEN のあとのANDは改行しない。
                    if (! encounterBetween) {
                        index += insertReturnAndIndent(list3, index, indent);
                    }
                    encounterBetween = false;
                }
            } else if (token.getType() == BlancoSqlTokenConstants.COMMENT) {
                // System.out.println(">>>"+t.s);
                if (token.getString().startsWith("/*")) {
                    // マルチラインコメントの後に改行を入れる。
                    index += insertReturnAndIndent(list3, index + 1, indent);
                }
            }
            prev = token;
        }

//        // 丸カッコで囲まれた (ひとつの項目)については特別扱いを行う。 @author tosiki iga
//		for (int index = argList.size() - 1; index >= 4; index--) {
//			if (index >= argList.size()) {
//				continue;
//			}
//
//			BlancoSqlToken t0 = (BlancoSqlToken) argList.get(index);
//			BlancoSqlToken t1 = (BlancoSqlToken) argList.get(index - 1);
//			BlancoSqlToken t2 = (BlancoSqlToken) argList.get(index - 2);
//			BlancoSqlToken t3 = (BlancoSqlToken) argList.get(index - 3);
//			BlancoSqlToken t4 = (BlancoSqlToken) argList.get(index - 4);
//
//			if (t4.getString().equalsIgnoreCase("(")
//					&& t3.getString().trim().equalsIgnoreCase("")
//					&& t1.getString().trim().equalsIgnoreCase("")
//					&& t0.getString().equalsIgnoreCase(")")) {
//				t4.setString(t4.getString() + t2.getString() + t0.getString());
//				argList.remove(index);
//				argList.remove(index - 1);
//				argList.remove(index - 2);
//				argList.remove(index - 3);
//			}
//		}
//
//        // 前後にスペースを入れる
//        for (int index = 1; index < argList.size(); index++) {
//            prev = (BlancoSqlToken) argList.get(index - 1);
//            token = (BlancoSqlToken) argList.get(index);
//
//            if (prev.getType() != BlancoSqlTokenConstants.SPACE
//                    && token.getType() != BlancoSqlTokenConstants.SPACE) {
//                // カンマの後にはスペース入れない
//                if (prev.getString().equals(",")) {
//                    continue;
//                }
//                // 関数名の後ろにはスペースは入れない
//                if (fRule.isFunction(prev.getString())
//                        && token.getString().equals("(")) {
//                    continue;
//                }
//                // コメントの後にはスペースは入れない。
//                if (prev.getType() == BlancoSqlTokenConstants.COMMENT) {
//                	continue;
//                }
//                // スペースを入れる
//                argList.add(index, new BlancoSqlToken(
//                        BlancoSqlTokenConstants.SPACE, " "));
//            }
//        }

        return list3;
    }

    /**
     * 改行とインデントを挿入する.
     * 
     * @param argList
     * @param argIndex
     * @param argIndent
     * @return 空白を挿入した場合は１を、空白を置き換えた場合は０を返す。
     */
    private int insertReturnAndIndent(final List argList, final int argIndex,
            final int argIndent) {
        // 関数内では改行は挿入しない
        if (functionBracket.contains(Boolean.TRUE))
            return 0;
        try {
            // 挿入する文字列を作成する。
            String s = "\n";
            // もし１つ前にシングルラインコメントがあるなら、改行は不要。
            BlancoSqlToken prevToken = (BlancoSqlToken) (argList
                    .get(argIndex - 1));
            if (prevToken.getType() == BlancoSqlTokenConstants.COMMENT
                    && prevToken.getString().startsWith("--")) {
                s = "";
            }
            // インデントをつける。
            for (int index = 0; index < argIndent; index++) {
                s += fRule.indentString;
            }

            // 前後にすでにスペースがあれば、それを置き換える。
            BlancoSqlToken token = (BlancoSqlToken) argList.get(argIndex);
            if (token.getType() == BlancoSqlTokenConstants.SPACE) {
                token.setString(s);
                return 0;
            }

            token = (BlancoSqlToken) argList.get(argIndex - 1);
            if (token.getType() == BlancoSqlTokenConstants.SPACE) {
                token.setString(s);
                return 0;
            }
            // 前後になければ、新たにスペースを追加する。
            argList.add(argIndex, new BlancoSqlToken(
                    BlancoSqlTokenConstants.SPACE, s));
            return 1;
        } catch (IndexOutOfBoundsException e) {
            // e.printStackTrace();
            return 0;
        }
    }

    public static void main(final String[] args) throws Exception {
        // ルールを設定する
        final BlancoSqlRule rule = new BlancoSqlRule();
        rule.keyword = BlancoSqlRule.KEYWORD_UPPER_CASE;
        rule.indentString = "    ";
        String[] mySqlFuncs = {
                // getNumericFunctions
                "ABS", "ACOS", "ASIN", "ATAN", "ATAN2", "BIT_COUNT", "CEILING",
                "COS", "COT", "DEGREES", "EXP", "FLOOR", "LOG", "LOG10", "MAX",
                "MIN", "MOD", "PI", "POW", "POWER", "RADIANS", "RAND", "ROUND",
                "SIN", "SQRT", "TAN", "TRUNCATE",
                // getStringFunctions
                "ASCII", "BIN", "BIT_LENGTH", "CHAR", "CHARACTER_LENGTH",
                "CHAR_LENGTH", "CONCAT", "CONCAT_WS", "CONV", "ELT",
                "EXPORT_SET", "FIELD", "FIND_IN_SET", "HEX,INSERT", "INSTR",
                "LCASE", "LEFT", "LENGTH", "LOAD_FILE", "LOCATE", "LOCATE",
                "LOWER", "LPAD", "LTRIM", "MAKE_SET", "MATCH", "MID", "OCT",
                "OCTET_LENGTH", "ORD", "POSITION", "QUOTE", "REPEAT",
                "REPLACE", "REVERSE", "RIGHT", "RPAD", "RTRIM", "SOUNDEX",
                "SPACE", "STRCMP", "SUBSTRING", "SUBSTRING", "SUBSTRING",
                "SUBSTRING", "SUBSTRING_INDEX", "TRIM", "UCASE", "UPPER",
                // getSystemFunctions
                "DATABASE", "USER", "SYSTEM_USER", "SESSION_USER", "PASSWORD",
                "ENCRYPT", "LAST_INSERT_ID", "VERSION",
                // getTimeDateFunctions
                "DAYOFWEEK", "WEEKDAY", "DAYOFMONTH", "DAYOFYEAR", "MONTH",
                "DAYNAME", "MONTHNAME", "QUARTER", "WEEK", "YEAR", "HOUR",
                "MINUTE", "SECOND", "PERIOD_ADD", "PERIOD_DIFF", "TO_DAYS",
                "FROM_DAYS", "DATE_FORMAT", "TIME_FORMAT", "CURDATE",
                "CURRENT_DATE", "CURTIME", "CURRENT_TIME", "NOW", "SYSDATE",
                "CURRENT_TIMESTAMP", "UNIX_TIMESTAMP", "FROM_UNIXTIME",
                "SEC_TO_TIME", "TIME_TO_SEC"
        };
        rule.setFunctionNames(mySqlFuncs);
        final BlancoSqlFormatter2 formatter = new BlancoSqlFormatter2(rule);

        // テストディレクトリ内のファイルを一括で変換する。
        final File[] files = new File("Test").listFiles();
        for (int i = 0; i < files.length; i++) {
            System.out.println("-- " + files[i]);
            // ファイルのSQLを読み込む.
            final BufferedReader reader = new BufferedReader(new FileReader(
                    files[i]));
            String before = "";
            while (reader.ready()) {
                String line = reader.readLine();
                if (line == null)
                    break;
                before += line + "\n";
            }
            reader.close();

            // 整形
            System.out.println("[before]\n" + before);
            String after = formatter.format(before);
            System.out.println("[after]\n" + after);
        }
    }
}
