package ro.isdc.wro.extensions.processor.support.typescript;

import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;

import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.NativeObject;
import org.mozilla.javascript.ScriptableObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ro.isdc.wro.WroRuntimeException;
import ro.isdc.wro.extensions.script.RhinoScriptBuilder;
import ro.isdc.wro.util.StopWatch;
import ro.isdc.wro.util.WroUtil;


/**
 * Uses rhino to compile a TypeScript into javascript.
 *
 * @author Alex Objelean
 * @since 1.6.3
 * @created 18 Jan 2013
 */
public class TypeScriptCompiler {
  private static final String PARAM_ERRORS = "errors";
  private static final String PARAM_SOURCE = "source";
  private static final Logger LOG = LoggerFactory.getLogger(TypeScriptCompiler.class);
  private static final String TYPESCRIPT_JS = "typescript-0.8.js";
  private static final String TYPESCRIPT_COMPILE_JS = "typescript.compile-0.3.js";
  private final String ecmaScriptVersion = "TypeScript.CodeGenTarget.ES5";
  private ScriptableObject scope;

  public String compile(final String typeScript) {
    final StopWatch stopWatch = new StopWatch();
    stopWatch.start("initContext");
    final RhinoScriptBuilder builder = initScriptBuilder();
    stopWatch.stop();

    stopWatch.start("compile");
    try {
      final String execute = getCompilationCommand(typeScript);
      final NativeObject compilationResult = (NativeObject) builder.evaluate(execute, "compile");
      final NativeArray errors = (NativeArray) compilationResult.get(PARAM_ERRORS);
      if (errors.size() > 0) {
        throwCompilationError(errors);
      }
      return compilationResult.get(PARAM_SOURCE).toString();
    } finally {
      stopWatch.stop();
      LOG.debug(stopWatch.prettyPrint());
    }
  }

  private void throwCompilationError(final NativeArray errors) {
    final StringBuilder sb = new StringBuilder();
    for (final Object error : errors) {
      sb.append(error.toString()).append("\n");
    }
    LOG.debug("Compilation errors: {}", sb);
    throw new WroRuntimeException(sb.toString());
  }

  /**
   * Creates compilation command for provided typescript input.
   *
   * @param input
   *          the TypeScript to compile into js.
   * @return compilation command.
   */
  private String getCompilationCommand(final String input) {
    return String.format("compilerWrapper.compile(%s, %s)", WroUtil.toJSMultiLineString(input),
        ecmaScriptVersion);
  }

  /**
   * @return the stream of the compiler resource (javascript) used to compile templates.
   * @throws IOException
   *           if the stream could not be located.
   */
  private InputStream getCompilerAsStream()
      throws IOException {
    return new SequenceInputStream(getCompilerStream(), getCompilerWrapperStream());
  }

  /**
   * @return the stream for the script performing actual compilation wrapper.
   */
  protected InputStream getCompilerWrapperStream() {
    return TypeScriptCompiler.class.getResourceAsStream(TYPESCRIPT_COMPILE_JS);
  }

  /**
   * @return the stream for the script for TypeScript compiler.
   */
  protected InputStream getCompilerStream() {
    return TypeScriptCompiler.class.getResourceAsStream(TYPESCRIPT_JS);
  }

  private RhinoScriptBuilder initScriptBuilder() {
    try {
      RhinoScriptBuilder builder;
      if (scope == null) {
        builder = RhinoScriptBuilder.newClientSideAwareChain().evaluateChain(getCompilerAsStream(), "templateCompiler.js");
        scope = builder.getScope();
      } else {
        builder = RhinoScriptBuilder.newChain(scope);
      }
      return builder;
    } catch (final IOException ex) {
      throw new WroRuntimeException("Failed reading init script", ex);
    }
  }
}
