/**
 * Copyright (C) 2010-2011 the original author or authors.
 *
 * 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 com.googlecode.flyway.core.dbsupport.h2;

import com.googlecode.flyway.core.migration.sql.PlaceholderReplacer;
import com.googlecode.flyway.core.migration.sql.SqlScript;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * SqlScript supporting Hsql-specific delimiter changes.
 */
public class H2SqlScript extends SqlScript {
    /**
     * Creates a new sql script from this source with these placeholders to replace.
     *
     * @param sqlScriptSource     The sql script as a text block with all placeholders still present.
     * @param placeholderReplacer The placeholder replacer to apply to sql migration scripts.
     *
     * @throws IllegalStateException Thrown when the script could not be read from this resource.
     */
    public H2SqlScript(String sqlScriptSource, PlaceholderReplacer placeholderReplacer) {
        super(sqlScriptSource, placeholderReplacer);
    }

    /**
     * For testing only.
     */
    /* private -> for testing */ H2SqlScript() {
        super();
    }

    @Override
    protected String changeDelimiterIfNecessary(String statement, String line, String delimiter) {
        if (endsWithOpenMultilineStringLiteral(statement)) {
            return null;
        }

        return DEFAULT_STATEMENT_DELIMITER;
    }

    /**
     * Checks whether the statement we have assembled so far ends with an open multi-line string literal (which will be
     * continued on the next line).
     *
     * @param statement The current statement, assembled from the lines we have parsed so far. May not yet be complete.
     *
     * @return {@code true} if the statement is unfinished and the end is currently in the middle of a multi-line string
     *         literal. {@code false} if not.
     */
    /* private -> for testing */ boolean endsWithOpenMultilineStringLiteral(String statement) {
        String[] tokens = StringUtils.tokenizeToStringArray(statement, " ;=|()");

        List<Set<TokenType>> delimitingTokens = extractStringLiteralDelimitingTokens(tokens);

        boolean insideQuoteStringLiteral = false;
        boolean insideDollarStringLiteral = false;

        for (Set<TokenType> delimitingToken : delimitingTokens) {
            if (!insideDollarStringLiteral && !insideQuoteStringLiteral && delimitingToken.contains(TokenType.QUOTE_OPEN)) {
                insideQuoteStringLiteral = true;
                continue;
            }
            if (insideQuoteStringLiteral && delimitingToken.contains(TokenType.QUOTE_CLOSE)) {
                insideQuoteStringLiteral = false;
                continue;
            }
            if (!insideDollarStringLiteral && !insideQuoteStringLiteral && delimitingToken.contains(TokenType.DOLLAR_OPEN)) {
                insideDollarStringLiteral = true;
                continue;
            }
            if (insideDollarStringLiteral && delimitingToken.contains(TokenType.DOLLAR_CLOSE)) {
                insideDollarStringLiteral = false;
            }
        }

        return insideQuoteStringLiteral || insideDollarStringLiteral;
    }

    /**
     * Extract the type of all tokens that potentially delimit string literals.
     *
     * @param tokens The tokens to analyse.
     *
     * @return The list of potentially delimiting string literals token types per token. Tokens that do not have any
     *         impact on string delimiting are discarded.
     */
    private List<Set<TokenType>> extractStringLiteralDelimitingTokens(String[] tokens) {
        List<Set<TokenType>> delimitingTokens = new ArrayList<Set<TokenType>>();
        for (String token : tokens) {
            //Remove escaped quotes as they do not form a string literal delimiter
            String cleanToken = StringUtils.replace(token, "''", "");

            Set<TokenType> tokenTypes = new HashSet<TokenType>();

            if (cleanToken.startsWith("'")) {
                if ((cleanToken.length() > 1) && cleanToken.endsWith("'")) {
                    // Ignore. ' string literal is opened and closed inside the same token.
                    continue;
                }
                tokenTypes.add(TokenType.QUOTE_OPEN);
            }

            if (cleanToken.endsWith("'")) {
                tokenTypes.add(TokenType.QUOTE_CLOSE);
            }

            if (cleanToken.startsWith("$$")) {
                if ((cleanToken.length() > 2) && cleanToken.endsWith("$$")) {
                    // Ignore. $$ string literal is opened and closed inside the same token.
                    continue;
                }
                tokenTypes.add(TokenType.DOLLAR_OPEN);
            }

            if (cleanToken.endsWith("$$")) {
                tokenTypes.add(TokenType.DOLLAR_CLOSE);
            }

            if (!tokenTypes.isEmpty()) {
                delimitingTokens.add(tokenTypes);
            }
        }

        return delimitingTokens;
    }

    /**
     * The types of tokens relevant for string delimiter related parsing.
     */
    private static enum TokenType {
        /**
         * Token opens ' string literal
         */
        QUOTE_OPEN,

        /**
         * Token closes ' string literal
         */
        QUOTE_CLOSE,

        /**
         * Token opens $$ string literal
         */
        DOLLAR_OPEN,

        /**
         * Token closes $$ string literal
         */
        DOLLAR_CLOSE
    }
}

@@ -700,131 +700,106 @@
 ort 
-com.googlecode.flyway.core.migration.sql.PlaceholderReplacer;%0D%0Aimport com.googlecode.flyway.core.migration.sql.SqlScrip
+java.util.ArrayList;%0D%0Aimport java.util.HashSet;%0D%0Aimport java.util.List;%0D%0Aimport java.util.Se
 t;%0D%0A
+%0D%0A
 impo
@@ -853,100 +853,127 @@
 ort 
-java.util.ArrayList;%0D%0Aimport java.util.HashSet;%0D%0Aimport java.util.List;%0D%0Aimport java.util.Se
+com.googlecode.flyway.core.migration.sql.PlaceholderReplacer;%0D%0Aimport com.googlecode.flyway.core.migration.sql.SqlScrip
 t;%0D%0A
@@ -2629,24 +2629,18 @@
 ent) %7B%0D%0A
-        
+%09%09
 String%5B%5D
@@ -2701,16 +2701,17 @@
  %22 ;=%7C()
+,
 %22);%0D%0A%0D%0A 

fix for bug http://code.google.com/p/flyway/issues/detail?id=128