package com.googlecode.htmlcompressor.compressor;

/*
 * 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.
 */

import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import com.google.common.collect.Lists;
import com.google.common.io.LimitInputStream;
import com.google.javascript.jscomp.CompilationLevel;
import com.google.javascript.jscomp.Compiler;
import com.google.javascript.jscomp.CompilerOptions;
import com.google.javascript.jscomp.JSSourceFile;
import com.google.javascript.jscomp.Result;
import com.google.javascript.jscomp.WarningLevel;

/**
 * Basic JavaScript compressor implementation using <a href="http://code.google.com/closure/compiler/">Google Closure Compiler</a> 
 * that could be used by {@link HtmlCompressor} for inline JavaScript compression.
 * 
 * @author <a href="mailto:[email protected]">Sergiy Kovalchuk</a>
 * 
 * @see HtmlCompressor#setJavaScriptCompressor(Compressor)
 * @see <a href="http://code.google.com/closure/compiler/">Google Closure Compiler</a>
 */
public class ClosureJavaScriptCompressor implements Compressor {
	
	public static final String COMPILATION_LEVEL_SIMPLE = "simple";
	public static final String COMPILATION_LEVEL_ADVANCED = "advanced";
	public static final String COMPILATION_LEVEL_WHITESPACE = "whitespace";

	//Closure compiler default settings
	private CompilerOptions compilerOptions = new CompilerOptions();
	private CompilationLevel compilationLevel = CompilationLevel.SIMPLE_OPTIMIZATIONS;
	private Level loggingLevel = Level.SEVERE;
	private WarningLevel warningLevel = WarningLevel.DEFAULT;
	private boolean customExternsOnly = false;
	private List<JSSourceFile> externs = null;

	public ClosureJavaScriptCompressor() {
	}

	public ClosureJavaScriptCompressor(CompilationLevel compilationLevel) {
		this.compilationLevel = compilationLevel;
	}

	@Override
	public String compress(String source) {
		
		StringWriter writer = new StringWriter();
		
		//prepare source
		List<JSSourceFile> input = new ArrayList<JSSourceFile>();
		input.add(JSSourceFile.fromCode("source.js", source));
		
		//prepare externs
		List<JSSourceFile> externsList = new ArrayList<JSSourceFile>();
		if(compilationLevel.equals(CompilationLevel.ADVANCED_OPTIMIZATIONS)) {
			//default externs
			if(!customExternsOnly) {
				try {
					externsList = getDefaultExterns();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			//add user defined externs
			if(externs != null) {
				for(JSSourceFile extern : externs) {
					externsList.add(extern);
				}
			}
			//add empty externs
			if(externsList.size() == 0) {
				externsList.add(JSSourceFile.fromCode("externs.js", ""));
			}
		} else {
			//empty externs
			externsList.add(JSSourceFile.fromCode("externs.js", ""));
		}
		
		Compiler.setLoggingLevel(loggingLevel);
		
		Compiler compiler = new Compiler();
		compiler.disableThreads();
		
		compilationLevel.setOptionsForCompilationLevel(compilerOptions);
		warningLevel.setOptionsForWarningLevel(compilerOptions);
		
		Result result = compiler.compile(externsList, input, compilerOptions);
		
		if (result.success) {
			writer.write(compiler.toSource());
		} else {
			writer.write(source);
		}

		return writer.toString();

	}
	
	//read default externs from closure.jar
	private List<JSSourceFile> getDefaultExterns() throws IOException {
		InputStream input = ClosureJavaScriptCompressor.class.getResourceAsStream("/externs.zip");
		ZipInputStream zip = new ZipInputStream(input);
		List<JSSourceFile> externs = Lists.newLinkedList();
		for (ZipEntry entry = null; (entry = zip.getNextEntry()) != null;) {
			LimitInputStream entryStream = new LimitInputStream(zip, entry.getSize());
			externs.add(JSSourceFile.fromInputStream(entry.getName(), entryStream));
		}
		return externs;
	}

	/**
	 * Returns level of optimization that is applied when compiling JavaScript code.
	 * 
	 * @return <code>CompilationLevel</code> that is applied when compiling JavaScript code.
	 * 
	 * @see <a href="http://closure-compiler.googlecode.com/svn/trunk/javadoc/com/google/javascript/jscomp/CompilationLevel.html">CompilationLevel</a>
	 */
	public CompilationLevel getCompilationLevel() {
		return compilationLevel;
	}

	/**
	 * Sets level of optimization that should be applied when compiling JavaScript code. 
	 * If none is provided, <code>CompilationLevel.SIMPLE_OPTIMIZATIONS</code> will be used by default. 
	 * 
	 * <p><b>Warning:</b> Using <code>CompilationLevel.ADVANCED_OPTIMIZATIONS</code> could 
	 * break inline JavaScript if externs are not set properly.
	 * 
	 * @param compilationLevel Optimization level to use, could be set to <code>CompilationLevel.ADVANCED_OPTIMIZATIONS</code>, <code>CompilationLevel.SIMPLE_OPTIMIZATIONS</code>, <code>CompilationLevel.WHITESPACE_ONLY</code> 
	 * 
	 * @see <a href="http://code.google.com/closure/compiler/docs/api-tutorial3.html">Advanced Compilation and Externs</a>
	 * @see <a href="http://code.google.com/closure/compiler/docs/compilation_levels.html">Closure Compiler Compilation Levels</a>
	 * @see <a href="http://closure-compiler.googlecode.com/svn/trunk/javadoc/com/google/javascript/jscomp/CompilationLevel.html">CompilationLevel</a>
	 */
	public void setCompilationLevel(CompilationLevel compilationLevel) {
		this.compilationLevel = compilationLevel;
	}

	/**
	 * Returns options that are used by the Closure compiler.
	 * 
	 * @return <code>CompilerOptions</code> that are used by the compiler
	 * 
	 * @see <a href="http://closure-compiler.googlecode.com/svn/trunk/javadoc/com/google/javascript/jscomp/CompilerOptions.html">CompilerOptions</a>
	 */
	public CompilerOptions getCompilerOptions() {
		return compilerOptions;
	}

	/**
	 * Sets options that will be used by the Closure compiler. 
	 * If none is provided, default options constructor will be used: <code>new CompilerOptions()</code>.
	 *   
	 * @param compilerOptions <code>CompilerOptions</code> that will be used by the compiler
	 * 
	 * @see <a href="http://closure-compiler.googlecode.com/svn/trunk/javadoc/com/google/javascript/jscomp/CompilerOptions.html">CompilerOptions</a>
	 */
	public void setCompilerOptions(CompilerOptions compilerOptions) {
		this.compilerOptions = compilerOptions;
	}

	/**
	 * Returns logging level used by the Closure compiler.
	 * 
	 * @return <code>Level</code> of logging used by the Closure compiler
	 */
	public Level getLoggingLevel() {
		return loggingLevel;
	}

	/**
	 * Sets logging level for the Closure compiler.
	 * 
	 * @param loggingLevel logging level for the Closure compiler.
	 * 
	 * @see java.util.logging.Level
	 */
	public void setLoggingLevel(Level loggingLevel) {
		this.loggingLevel = loggingLevel;
	}

	/**
	 * Returns <code>JSSourceFile</code> used as a reference during the compression 
	 * at <code>CompilationLevel.ADVANCED_OPTIMIZATIONS</code> level.
	 * 
	 * @return <code>JSSourceFile</code> used as a reference during compression
	 */
	public List<JSSourceFile> getExterns() {
		return externs;
	}

	/**
	 * Sets external JavaScript files that are used as a reference for function declarations if 
	 * <code>CompilationLevel.ADVANCED_OPTIMIZATIONS</code> compression level is used. 
	 * 
	 * <p>A number of default externs defined inside Closure's jar will be used besides user defined ones, 
	 * to use only user defined externs set {@link #setCustomExternsOnly(boolean) setCustomExternsOnly(true)}
	 * 
	 * <p><b>Warning:</b> Using <code>CompilationLevel.ADVANCED_OPTIMIZATIONS</code> could 
	 * break inline JavaScript if externs are not set properly.
	 * 
	 * @param externs <code>JSSourceFile</code> to use as a reference during compression
	 * 
	 * @see #setCompilationLevel(CompilationLevel)
	 * @see #setCustomExternsOnly(boolean)
	 * @see <a href="http://code.google.com/closure/compiler/docs/api-tutorial3.html">Advanced Compilation and Externs</a>
	 * @see <a href="http://closure-compiler.googlecode.com/svn/trunk/javadoc/com/google/javascript/jscomp/JSSourceFile.html">JSSourceFile</a>
	 */
	public void setExterns(List<JSSourceFile> externs) {
		this.externs = externs;
	}

	/**
	 * Returns <code>WarningLevel</code> used by the Closure compiler
	 *  
	 * @return <code>WarningLevel</code> used by the Closure compiler
	 */
	public WarningLevel getWarningLevel() {
		return warningLevel;
	}

	/**
	 * Indicates the amount of information you want from the compiler about possible problems in your code. 
	 * 
	 * @param warningLevel <code>WarningLevel</code> to use
	 * 
	 * @see <a href="http://code.google.com/closure/compiler/docs/api-ref.html">
	 */
	public void setWarningLevel(WarningLevel warningLevel) {
		this.warningLevel = warningLevel;
	}

	/**
	 * Returns <code>true</code> if default externs defined inside Closure's jar are ignored 
	 * and only user defined ones are used.
	 * 
	 * @return <code>true</code> if default externs defined inside Closure's jar are ignored 
	 * and only user defined ones are used
	 */
	public boolean isCustomExternsOnly() {
		return customExternsOnly;
	}

	/**
	 * If set to <code>true</code>, default externs defined inside Closure's jar will be ignored 
	 * and only user defined ones will be used.
	 *  
	 * @param customExternsOnly <code>true</code> to skip default externs and use only user defined ones
	 * 
	 * @see #setExterns(List)
	 * @see #setCompilationLevel(CompilationLevel)
	 */
	public void setCustomExternsOnly(boolean customExternsOnly) {
		this.customExternsOnly = customExternsOnly;
	}

}