/**
 * Copyright Alex Objelean
 */
package ro.isdc.wro.extensions.script;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.Validate;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.RhinoException;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.tools.ToolErrorReporter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ro.isdc.wro.WroRuntimeException;
import ro.isdc.wro.extensions.locator.WebjarUriLocator;


/**
 * Used to evaluate javascript on the serverside using rhino javascript engine. Encapsulate and hides all implementation
 * details used by rhino to evaluate javascript on the serverside.
 *
 * @author Alex Objelean
 */
public final class RhinoScriptBuilder {
  private static final Logger LOG = LoggerFactory.getLogger(RhinoScriptBuilder.class);
  private static final String SCRIPT_ENV = "env.rhino.min.js";
  private static final String SCRIPT_JSON = "json2.min.js";
  private static final String SCRIPT_CYCLE = "cycle.js";
  private final ScriptableObject scope;

  private RhinoScriptBuilder() {
    this(null);
  }


  private RhinoScriptBuilder(final ScriptableObject scope) {
    this.scope = createContext(scope);
  }


  private Context getContext() {
    initContext();
    return Context.getCurrentContext();
  }

  /**
   * @return the context
   */
  public ScriptableObject getScope() {
    return this.scope;
  }

  /**
   * Initialize the context.
   */
  private ScriptableObject createContext(final ScriptableObject initialScope) {
    final Context context = getContext();
    context.setOptimizationLevel(-1);
    // TODO redirect errors from System.err to LOG.error()
    context.setErrorReporter(new ToolErrorReporter(false));
    context.setLanguageVersion(Context.VERSION_1_8);
    InputStream script = null;
    final ScriptableObject scriptCommon = (ScriptableObject) context.initStandardObjects(initialScope);
    try {
      script = getClass().getResourceAsStream("commons.js");
      context.evaluateReader(scriptCommon, new InputStreamReader(script), "commons.js", 1, null);
    } catch (final IOException e) {
      throw new RuntimeException("Problem while evaluationg commons script.", e);
    } finally {
      IOUtils.closeQuietly(script);
    }
    return scriptCommon;
  }

  /**
   * Add a client side environment to the script context (client-side aware).
   *
   * @return {@link RhinoScriptBuilder} used to chain evaluation of the scripts.
   * @throws IOException
   */
  public RhinoScriptBuilder addClientSideEnvironment() {
    try {
      final InputStream scriptEnv = getClass().getResourceAsStream(SCRIPT_ENV);
      evaluateChain(scriptEnv, SCRIPT_ENV);
      return this;
    } catch (final IOException e) {
      throw new RuntimeException("Couldn't initialize env.rhino script", e);
    }
  }


  /**
   * This method will load JSON utility and aslo a Douglas Crockford's <a
   * href="https://github.com/douglascrockford/JSON-js/blob/master/cycle.js">utility</a> required for decycling objects
   * which would fail otherwise when using JSON.stringify.
   */
  public RhinoScriptBuilder addJSON() {
    try {
      final InputStream script = new WebjarUriLocator().locate(WebjarUriLocator.createUri("20110223/json2.js"));
      final InputStream scriptCycle = getClass().getResourceAsStream(SCRIPT_CYCLE);

      evaluateChain(script, SCRIPT_JSON);
      evaluateChain(scriptCycle, SCRIPT_CYCLE);
      return this;
    } catch (final IOException e) {
      throw new RuntimeException("Couldn't initialize json2.min.js script", e);
    }
  }

  /**
   * Evaluates a script and return {@link RhinoScriptBuilder} for a chained script evaluation.
   *
   * @param stream {@link InputStream} of the script to evaluate.
   * @param sourceName the name of the evaluated script.
   * @return {@link RhinoScriptBuilder} chain with required script evaluated.
   * @throws IOException if the script couldn't be retrieved.
   */
  public RhinoScriptBuilder evaluateChain(final InputStream stream, final String sourceName)
    throws IOException {
    Validate.notNull(stream);
    try {
      getContext().evaluateReader(scope, new InputStreamReader(stream), sourceName, 1, null);
      return this;
    } catch(final RhinoException e) {
      if (e instanceof RhinoException) {
        LOG.error("RhinoException: {}", RhinoUtils.createExceptionMessage(e));
      }
      throw e;
    } catch (final RuntimeException e) {
      LOG.error("Exception caught", e);
      throw e;
    } finally {
      stream.close();
    }
  }

  /**
   * Makes sure the context is properly initialized.
   */
  private void initContext() {
    if (Context.getCurrentContext() == null) {
      Context.enter();
    }
  }


  /**
   * Evaluates a script and return {@link RhinoScriptBuilder} for a chained script evaluation.
   *
   * @param script the string representation of the script to evaluate.
   * @param sourceName the name of the evaluated script.
   * @return evaluated object.
   * @throws IOException if the script couldn't be retrieved.
   */
  public RhinoScriptBuilder evaluateChain(final String script, final String sourceName) {
    Validate.notNull(script);
    getContext().evaluateString(scope, script, sourceName, 1, null);
    return this;
  }


  /**
   * Evaluates a script from a reader.
   *
   * @param reader {@link Reader} of the script to evaluate.
   * @param sourceName the name of the evaluated script.
   * @return evaluated object.
   * @throws IOException if the script couldn't be retrieved.
   */
  public Object evaluate(final Reader reader, final String sourceName)
    throws IOException {
    Validate.notNull(reader);
    try {
      return evaluate(IOUtils.toString(reader), sourceName);
    } finally {
      reader.close();
    }
  }


  /**
   * Evaluates a script.
   *
   * @param script string representation of the script to evaluate.
   * @param sourceName the name of the evaluated script.
   * @return evaluated object.
   * @throws IOException if the script couldn't be retrieved.
   */
  public Object evaluate(final String script, final String sourceName) {
    Validate.notNull(script);
    // make sure we have a context associated with current thread
    try {
      return getContext().evaluateString(scope, script, sourceName, 1, null);
    } catch (final RhinoException e) {
      final String message = RhinoUtils.createExceptionMessage(e);
      LOG.error("JavaScriptException occured: {}", message);
      throw new WroRuntimeException(message);
    } finally {
      // Rhino throws an exception when trying to exit twice. Make sure we don't get any exception
      if (Context.getCurrentContext() != null) {
        Context.exit();
      }
    }
  }

  /**
   * @return default {@link RhinoScriptBuilder} for script evaluation chaining.
   */
  public static RhinoScriptBuilder newChain() {
    return new RhinoScriptBuilder();
  }


  public static RhinoScriptBuilder newChain(final ScriptableObject scope) {
    return new RhinoScriptBuilder(scope);
  }


  /**
   * @return default {@link RhinoScriptBuilder} for script evaluation chaining.
   */
  public static RhinoScriptBuilder newClientSideAwareChain() {
    return new RhinoScriptBuilder().addClientSideEnvironment();
  }
}
