/*
 * Copyright 2004-2019 H2 Group. Multiple-Licensed under the MPL 2.0,
 * and the EPL 1.0 (http://h2database.com/html/license.html).
 * Initial Developer: H2 Group
 */
package org.h2.util;

import java.io.Closeable;
import java.io.IOException;
import java.io.Reader;
import java.util.Arrays;
import org.h2.engine.Constants;
import org.h2.message.DbException;

/**
 * This class can split SQL scripts to single SQL statements.
 * Each SQL statement ends with the character ';', however it is ignored
 * in comments and quotes.
 */
public class ScriptReader implements Closeable {

    private final Reader reader;
    private char[] buffer;

    /**
     * The position in the buffer of the next char to be read
     */
    private int bufferPos;

    /**
     * The position in the buffer of the statement start
     */
    private int bufferStart = -1;

    /**
     * The position in the buffer of the last available char
     */
    private int bufferEnd;

    /**
     * True if we have read past the end of file
     */
    private boolean endOfFile;

    /**
     * True if we are inside a comment
     */
    private boolean insideRemark;

    /**
     * Only valid if insideRemark is true. True if we are inside a block
     * comment, false if we are inside a line comment
     */
    private boolean blockRemark;

    /**
     * True if comments should be skipped completely by this reader.
     */
    private boolean skipRemarks;

    /**
     * The position in buffer of start of comment
     */
    private int remarkStart;

    /**
     * Create a new SQL script reader from the given reader
     *
     * @param reader the reader
     */
    public ScriptReader(Reader reader) {
        this.reader = reader;
        buffer = new char[Constants.IO_BUFFER_SIZE * 2];
    }

    /**
     * Close the underlying reader.
     */
    @Override
    public void close() {
        try {
            reader.close();
        } catch (IOException e) {
            throw DbException.convertIOException(e, null);
        }
    }

    /**
     * Read a statement from the reader. This method returns null if the end has
     * been reached.
     *
     * @return the SQL statement or null
     */
    public String readStatement() {
        if (endOfFile) {
            return null;
        }
        try {
            return readStatementLoop();
        } catch (IOException e) {
            throw DbException.convertIOException(e, null);
        }
    }

    private String readStatementLoop() throws IOException {
        bufferStart = bufferPos;
        int c = read();
        while (true) {
            if (c < 0) {
                endOfFile = true;
                if (bufferPos - 1 == bufferStart) {
                    return null;
                }
                break;
            } else if (c == ';') {
                break;
            }
            switch (c) {
            case '$': {
                c = read();
                if (c == '$' && (bufferPos - bufferStart < 3 || buffer[bufferPos - 3] <= ' ')) {
                    // dollar quoted string
                    while (true) {
                        c = read();
                        if (c < 0) {
                            break;
                        }
                        if (c == '$') {
                            c = read();
                            if (c < 0) {
                                break;
                            }
                            if (c == '$') {
                                break;
                            }
                        }
                    }
                    c = read();
                }
                break;
            }
            case '\'':
                while (true) {
                    c = read();
                    if (c < 0) {
                        break;
                    }
                    if (c == '\'') {
                        break;
                    }
                }
                c = read();
                break;
            case '"':
                while (true) {
                    c = read();
                    if (c < 0) {
                        break;
                    }
                    if (c == '\"') {
                        break;
                    }
                }
                c = read();
                break;
            case '/': {
                c = read();
                if (c == '*') {
                    // block comment
                    startRemark(true);
                    while (true) {
                        c = read();
                        if (c < 0) {
                            break;
                        }
                        if (c == '*') {
                            c = read();
                            if (c < 0) {
                                clearRemark();
                                break;
                            }
                            if (c == '/') {
                                endRemark();
                                break;
                            }
                        }
                    }
                    c = read();
                } else if (c == '/') {
                    // single line comment
                    startRemark(false);
                    while (true) {
                        c = read();
                        if (c < 0) {
                            clearRemark();
                            break;
                        }
                        if (c == '\r' || c == '\n') {
                            endRemark();
                            break;
                        }
                    }
                    c = read();
                }
                break;
            }
            case '-': {
                c = read();
                if (c == '-') {
                    // single line comment
                    startRemark(false);
                    while (true) {
                        c = read();
                        if (c < 0) {
                            clearRemark();
                            break;
                        }
                        if (c == '\r' || c == '\n') {
                            endRemark();
                            break;
                        }
                    }
                    c = read();
                }
                break;
            }
            default: {
                c = read();
            }
            }
        }
        return new String(buffer, bufferStart, bufferPos - 1 - bufferStart);
    }

    private void startRemark(boolean block) {
        blockRemark = block;
        remarkStart = bufferPos - 2;
        insideRemark = true;
    }

    private void endRemark() {
        clearRemark();
        insideRemark = false;
    }

    private void clearRemark() {
        if (skipRemarks) {
            Arrays.fill(buffer, remarkStart, bufferPos, ' ');
        }
    }

    private int read() throws IOException {
        if (bufferPos >= bufferEnd) {
            return readBuffer();
        }
        return buffer[bufferPos++];
    }

    private int readBuffer() throws IOException {
        if (endOfFile) {
            return -1;
        }
        int keep = bufferPos - bufferStart;
        if (keep > 0) {
            char[] src = buffer;
            if (keep + Constants.IO_BUFFER_SIZE > src.length) {
                // protect against NegativeArraySizeException
                if (src.length >= Integer.MAX_VALUE / 2) {
                    throw new IOException("Error in parsing script, " +
                            "statement size exceeds 1G, " +
                            "first 80 characters of statement looks like: " +
                            new String(buffer, bufferStart, 80));
                }
                buffer = new char[src.length * 2];
            }
            System.arraycopy(src, bufferStart, buffer, 0, keep);
        }
        remarkStart -= bufferStart;
        bufferStart = 0;
        bufferPos = keep;
        int len = reader.read(buffer, keep, Constants.IO_BUFFER_SIZE);
        if (len == -1) {
            // ensure bufferPos > bufferEnd
            bufferEnd = -1024;
            endOfFile = true;
            // ensure the right number of characters are read
            // in case the input buffer is still used
            bufferPos++;
            return -1;
        }
        bufferEnd = keep + len;
        return buffer[bufferPos++];
    }

    /**
     * Check if this is the last statement, and if the single line or block
     * comment is not finished yet.
     *
     * @return true if the current position is inside a remark
     */
    public boolean isInsideRemark() {
        return insideRemark;
    }

    /**
     * If currently inside a remark, this method tells if it is a block comment
     * (true) or single line comment (false)
     *
     * @return true if inside a block comment
     */
    public boolean isBlockRemark() {
        return blockRemark;
    }

    /**
     * If comments should be skipped completely by this reader.
     *
     * @param skipRemarks true if comments should be skipped
     */
    public void setSkipRemarks(boolean skipRemarks) {
        this.skipRemarks = skipRemarks;
    }

}
