/*******************************************************************************
 * Copyright 2012 The MITRE Corporation
 * 
 * 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.mitre.jdbc.datasource.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.Resource;

/**
 * @author Matt Franklin
 * 
 *         Parses a file looking for create, alter, insert, update, delete or
 *         drop commands and appends them to an output string or follows @@
 *         syntax to read child scripts.
 * 
 */
public class SqlFileParser {
	
	private static Log logger = LogFactory.getLog(SqlFileParser.class);
	
	private static final Pattern WORD_PATTERN = Pattern
			.compile("^([a-zA-Z]*)[ ;]");
	private static final String CHILD_SCRIPT_INDICATOR = "@@";
	private static final Set<String> commandSet;

	static {
		commandSet = new HashSet<String>();
		commandSet.add("create");
		commandSet.add("alter");
		commandSet.add("insert");
		commandSet.add("update");
		commandSet.add("drop");
		commandSet.add("delete");
		commandSet.add("commit");
		commandSet.add("set");
		commandSet.add("truncate");
		commandSet.add("rollback");
	}

	private enum State {
		INIT, READFILE, READSQL
	}

	private Stack<State> stateStack;
	private Resource resource;

	/**
	 * Constructor takes a Spring {@link Resource}
	 * 
	 * @param resource
	 *            the initial file to parse
	 */
	public SqlFileParser(Resource resource) {
		stateStack = new Stack<State>();
		this.resource = resource;
	}

	/**
	 * Gets the executable SQL statements from the resource passed to the
	 * constructor and its children
	 * 
	 * @return a valid executable string containing SQL statements
	 * @throws IOException
	 *             if the resource or its children are not found
	 */
	public String getSQL() throws IOException {
		return processResource(resource);
	}

	private String processResource(Resource res) throws IOException {
		StringBuilder sql = new StringBuilder();
		File resourceFile = res.getFile();
		stateStack.push(State.INIT);
		processFile(resourceFile, sql);
		stateStack.pop();
		
		logger.debug(" SQL:: " + sql);

		return sql.toString();
	}

	private void processFile(File file, StringBuilder sql) throws IOException {
		BufferedReader fileReader = new BufferedReader(new FileReader(
				file.getAbsoluteFile()));
		String line = null;
		while ((line = fileReader.readLine()) != null) {
			processLine(sql, line);
		}
	}

	private void processLine(StringBuilder sql, String line) throws IOException {
		String lowerLine = line.toLowerCase().trim();
		switch (stateStack.peek()) {
		case INIT: {
			if (lowerLine.startsWith(CHILD_SCRIPT_INDICATOR)) {
				// replace the current element in the stack with the new state
				stateStack.pop();
				stateStack.push(State.READFILE);
				processLine(sql, line);
			} else if (commandSet.contains(getFirstWord(lowerLine))) {

				// replace the current element in the stack with the new state
				stateStack.pop();
				stateStack.push(State.READSQL);
				processLine(sql, line);
			}
			break;
		}
		case READFILE: {
			stateStack.push(State.INIT);
			Resource child = resource.createRelative(line.replace(
					CHILD_SCRIPT_INDICATOR, ""));
			sql.append(processResource(child));
			// Read File lines do not have a terminal character but are by
			// definition only one line Long
			stateStack.pop();
			stateStack.push(State.INIT);
			break;
		}
		case READSQL: {
			sql.append(line);
			// add a space to accommodate line breaks. Not a big deal if
			// extraneous spaces are added
			sql.append(" ");
			if (lowerLine.endsWith(";")) {
				stateStack.pop();
				stateStack.push(State.INIT);
			}
			break;
		}
		default: {
			throw new RuntimeException("Invalid State");
		}
		}
	}

	private static String getFirstWord(String line) {
		Matcher match = WORD_PATTERN.matcher(line);
		return match.find() ? match.group(1) : null;
	}
}
