/*
 * Copyright wro4j@2011
 */
package ro.isdc.wro.extensions.processor.support.linter;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ro.isdc.wro.WroRuntimeException;
import ro.isdc.wro.extensions.locator.WebjarUriLocator;
import ro.isdc.wro.extensions.processor.support.csslint.CssLint;
import ro.isdc.wro.extensions.script.RhinoScriptBuilder;
import ro.isdc.wro.util.StopWatch;
import ro.isdc.wro.util.WroUtil;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;


/**
 * Encapsulates common code for utilities like jsHint or jsLint.
 *
 * @author Alex Objelean
 * @created 19 Sept 2011
 * @since 1.4.2
 */
public abstract class AbstractLinter {
  private static final Logger LOG = LoggerFactory.getLogger(AbstractLinter.class);
  /**
   * Required to make jshint not complain.
   */
  private static final String DEFINE_WINDOW = "var window = {};";
  private WebjarUriLocator webjarLocator;
  private final OptionsBuilder optionsBuilder = new OptionsBuilder();
  /**
   * Options to apply to js hint processing
   */
  private String options;
  private RhinoScriptBuilder builder;

  /**
   * Initialize script builder for evaluation.
   */
  private RhinoScriptBuilder initScriptBuilder() {
    try {
      if (builder == null) {
        builder = RhinoScriptBuilder.newChain().evaluateChain(DEFINE_WINDOW, "window").evaluateChain(
            getScriptAsStream(), "linter.js");
      }
      return builder;
    } catch (final IOException e) {
      throw new WroRuntimeException("Failed reading init script", e);
    }
  }

  /**
   * @return {@link WebjarUriLocator} instance to retrieve webjars.
   */
  protected final WebjarUriLocator getWebjarLocator() {
    if (webjarLocator == null) {
      webjarLocator = new WebjarUriLocator();
    }
    return webjarLocator;
  }

  /**
   * @return the stream of the linter script. Override this method to provide a different script version.
   * @throws IOException
   *           if the stream is invalid or unavailable.
   */
  protected abstract InputStream getScriptAsStream()
      throws IOException;

  /**
   * Validates a js using jsHint and throws {@link LinterException} if the js is invalid. If no exception is thrown, the
   * js is valid.
   *
   * @param data
   *          js content to process.
   */
  public void validate(final String data)
      throws LinterException {
    final StopWatch watch = new StopWatch();
    watch.start("init");
    final RhinoScriptBuilder builder = initScriptBuilder();
    watch.stop();
    watch.start("lint");
    final String packIt = buildLinterScript(WroUtil.toJSMultiLineString(data), getOptions());
    final boolean valid = Boolean.parseBoolean(builder.evaluate(packIt, "check").toString());
    if (!valid) {
      final String json = builder.addJSON().evaluate(
          String.format("JSON.stringify(JSON.decycle(%s.errors))", getLinterName()), "stringify errors").toString();
      LOG.debug("json {}", json);
      final Type type = new TypeToken<List<LinterError>>() {}.getType();
      final List<LinterError> errors = new Gson().fromJson(json, type);
      LOG.debug("errors {}", errors);
      throw new LinterException().setErrors(errors);
    }
    LOG.debug("result: {}", valid);
    watch.stop();
    LOG.debug(watch.prettyPrint());
  }

  /**
   * @return the name of the function used to perform the lint operation.
   */
  protected abstract String getLinterName();

  /**
   * TODO this method is duplicated in {@link CssLint}. Extract and reuse it.
   *
   * @param data
   *          script to process.
   * @param options
   *          options to set as true
   * @return Script used to pack and return the packed result.
   */
  private String buildLinterScript(final String data, final String options) {
    return String.format("%s(%s,%s);", getLinterName(), data, optionsBuilder.buildFromCsv(options));
  }

  /**
   * @param options
   *          the options to set
   */
  public AbstractLinter setOptions(final String... options) {
    this.options = StringUtils.join(options, ',');
    LOG.debug("setOptions: {}", this.options);
    return this;
  }

  /**
   * @return an options as CSV.
   */
  private String getOptions() {
    if (options == null) {
      options = createDefaultOptions();
    }
    return options;
  }

  /**
   * @return default options to use for linting.
   */
  protected String createDefaultOptions() {
    return "";
  }
}
