/**
 * 
 */
package codemining.js.codeutils;

import java.io.File;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Map;

import org.apache.commons.io.FileUtils;
import org.eclipse.wst.jsdt.core.JavaScriptCore;
import org.eclipse.wst.jsdt.core.dom.AST;
import org.eclipse.wst.jsdt.core.dom.ASTNode;
import org.eclipse.wst.jsdt.core.dom.ASTParser;
import org.eclipse.wst.jsdt.core.dom.ASTVisitor;
import org.eclipse.wst.jsdt.core.dom.FunctionDeclaration;
import org.eclipse.wst.jsdt.core.dom.JavaScriptUnit;

import codemining.languagetools.ParseType;

/**
 * A utility class to retrieve an Eclipse AST.
 * 
 * @author Miltos Allamanis <[email protected]>
 * 
 */
public class JavascriptASTExtractor {

	private static final class TopFunctionRetriever extends ASTVisitor {
		public FunctionDeclaration topDcl;

		@Override
		public boolean visit(final FunctionDeclaration node) {
			topDcl = node;
			return false;
		}
	}

	/**
	 * Remembers if the given Extractor will calculate the bindings.
	 */
	private final boolean useBindings;

	private final boolean useJavadocs;

	/**
	 * Constructor.
	 * 
	 * @param useBindings
	 *            calculate bindings on the extracted AST.
	 */
	public JavascriptASTExtractor(final boolean useBindings) {
		this.useBindings = useBindings;
		useJavadocs = false;
	}

	public JavascriptASTExtractor(final boolean useBindings,
			final boolean useJavadocs) {
		this.useBindings = useBindings;
		this.useJavadocs = useJavadocs;
	}

	/**
	 * Get the AST of a file. It is assumed that a JavaScriptUnit will be
	 * returned. An heuristic is used to set the path variables.
	 * 
	 * @param file
	 * @return the compilation unit of the file
	 * @throws IOException
	 */
	public final JavaScriptUnit getAST(final File file) throws IOException {
		final String sourceFile = FileUtils.readFileToString(file);
		final ASTParser parser = ASTParser.newParser(AST.JLS3);
		parser.setKind(ASTParser.K_COMPILATION_UNIT);

		final Map<String, String> options = new Hashtable<String, String>();
		options.put(JavaScriptCore.COMPILER_CODEGEN_TARGET_PLATFORM,
				JavaScriptCore.VERSION_1_7);
		options.put(JavaScriptCore.COMPILER_SOURCE, JavaScriptCore.VERSION_1_7);
		if (useJavadocs) {
			options.put(JavaScriptCore.COMPILER_DOC_COMMENT_SUPPORT,
					JavaScriptCore.ENABLED);
		}
		parser.setCompilerOptions(options);
		parser.setSource(sourceFile.toCharArray()); // set source
		parser.setResolveBindings(useBindings);
		parser.setBindingsRecovery(useBindings);

		parser.setStatementsRecovery(true);

		parser.setUnitName(file.getAbsolutePath());

		// FIXME Need file's project loaded into Eclipse to get bindings
		// which is only possible automatically if this were an Eclipse plugin
		// cf. https://bugs.eclipse.org/bugs/show_bug.cgi?id=206391
		// final IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
		// final IProject project = root.getProject(projectName);
		// parser.setProject(JavaScriptCore.create(project));

		final JavaScriptUnit compilationUnit = (JavaScriptUnit) parser
				.createAST(null);

		return compilationUnit;
	}

	/**
	 * Get a compilation unit of the given file content.
	 * 
	 * @param fileContent
	 * @param parseType
	 * @return the compilation unit
	 */
	public final ASTNode getAST(final String fileContent,
			final ParseType parseType) {
		return (ASTNode) getASTNode(fileContent, parseType);
	}

	/**
	 * Return an ASTNode given the content
	 * 
	 * @param content
	 * @return
	 */
	public final ASTNode getASTNode(final char[] content,
			final ParseType parseType) {
		final ASTParser parser = ASTParser.newParser(AST.JLS3);
		final int astKind;
		switch (parseType) {
		case CLASS_BODY:
		case METHOD:
			astKind = ASTParser.K_CLASS_BODY_DECLARATIONS;
			break;
		case COMPILATION_UNIT:
			astKind = ASTParser.K_COMPILATION_UNIT;
			break;
		case EXPRESSION:
			astKind = ASTParser.K_EXPRESSION;
			break;
		case STATEMENTS:
			astKind = ASTParser.K_STATEMENTS;
			break;
		default:
			astKind = ASTParser.K_COMPILATION_UNIT;
		}
		parser.setKind(astKind);

		final Map<String, String> options = new Hashtable<String, String>();
		options.put(JavaScriptCore.COMPILER_CODEGEN_TARGET_PLATFORM,
				JavaScriptCore.VERSION_1_7);
		options.put(JavaScriptCore.COMPILER_SOURCE, JavaScriptCore.VERSION_1_7);
		if (useJavadocs) {
			options.put(JavaScriptCore.COMPILER_DOC_COMMENT_SUPPORT,
					JavaScriptCore.ENABLED);
		}
		parser.setCompilerOptions(options);
		parser.setSource(content); // set source
		parser.setResolveBindings(useBindings);
		parser.setBindingsRecovery(useBindings);

		parser.setStatementsRecovery(true);

		if (parseType != ParseType.METHOD) {
			return parser.createAST(null);
		} else {
			final ASTNode cu = parser.createAST(null);
			return getFirstFunctionDeclaration(cu);
		}
	}

	/**
	 * Get the AST of a string. Path variables cannot be set.
	 * 
	 * @param file
	 * @param parseType
	 * @return an AST node for the given file content
	 * @throws IOException
	 */
	public final ASTNode getASTNode(final String fileContent,
			final ParseType parseType) {
		return getASTNode(fileContent.toCharArray(), parseType);
	}

	/**
	 * Get the AST and assume the type of the node is a compilation unit.
	 * 
	 * @throws Exception
	 */
	public final ASTNode getCompilationUnitAstNode(final char[] content)
			throws Exception {
		return getASTNode(content, ParseType.COMPILATION_UNIT);
	}

	/**
	 * Get the AST of a string. Path variables cannot be set.
	 * 
	 * @param file
	 * @return an AST node for the given file content
	 * @throws Exception
	 * @throws IOException
	 */
	public final ASTNode getCompilationUnitAstNode(final String fileContent)
			throws Exception {
		return getCompilationUnitAstNode(fileContent.toCharArray());
	}

	private final FunctionDeclaration getFirstFunctionDeclaration(
			final ASTNode node) {
		final TopFunctionRetriever visitor = new TopFunctionRetriever();
		node.accept(visitor);
		return visitor.topDcl;
	}

}