package com.asksunny.odbc;

import java.io.IOException;
import java.io.Reader;
import java.util.Arrays;

public class SQLScriptReader implements AutoCloseable {

	private final int IO_BUFFER_SIZE = 4 * 1024;
	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 SQLScriptReader(Reader reader) {
		this.reader = reader;
		buffer = new char[IO_BUFFER_SIZE * 2];
	}

	/**
	 * Close the underlying reader.
	 */
	@Override
	public void close() {
		if (reader != null)
			try {
				reader.close();
			} catch (IOException e) {
				throw new SQLProxyException(
						"Failed to close sql script reader", e);
			}
	}

	/**
	 * 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 new SQLProxyException("Failed to read next sql statement", e);
		}
	}

	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 + 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, 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;
	}

}
