package net.happygiraffe.jslint;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;

/**
 * A utility class to check JavaScript source code for potential problems.
 * 
 * @author dom
 * @version $Id$
 */
public class JSLint {

    private static final String JSLINT_FILE = "fulljslint.js";

    private Context ctx;

    private Set<Option> options = EnumSet.noneOf(Option.class);

    private ScriptableObject scope;

    /**
     * Create a new {@link JSLint} object. This reads in the jslint JavaScript
     * source as a resource.
     * 
     * @throws IOException
     *                 if something went wrong reading jslint.js.
     */
    public JSLint() throws IOException {
        ctx = Context.enter();
        scope = ctx.initStandardObjects();
        Reader reader = new BufferedReader(new InputStreamReader(getClass()
                .getResourceAsStream(JSLINT_FILE)));
        ctx.evaluateReader(scope, reader, JSLINT_FILE, 1, null);
    }

    /**
     * Add an option to change the behaviour of the lint.
     * 
     * @param o
     *                Any {@link Option}.
     */
    public void addOption(Option o) {
        options.add(o);
    }

    /**
     * Check for problems in a {@link Reader} which contains JavaScript source.
     * 
     * @param systemId
     *                a filename
     * @param reader
     *                a {@link Reader} over JavaScript source code.
     * 
     * @return a {@link List} of {@link Issue}s describing any problems.
     * @throws IOException
     */
    public List<Issue> lint(String systemId, Reader reader) throws IOException {
        return lint(systemId, Util.readerToString(reader));
    }

    /**
     * Check for problems in JavaScript source.
     * 
     * @param systemId
     *                a filename
     * @param javaScript
     *                a String of JavaScript source code.
     * 
     * @return a {@link List} of {@link Issue}s describing any problems.
     */
    public List<Issue> lint(String systemId, String javaScript) {
        List<Issue> issues = new ArrayList<Issue>();
        scope.put("input", scope, javaScript == null ? "" : javaScript);
        setJavaScriptOptions("options");
        String check = "JSLINT(input, options)";
        Boolean ok = (Boolean) ctx.evaluateString(scope, check, "lint()", 1,
                null);
        if (!ok.booleanValue()) {
            readErrors(systemId, issues);
        }
        return issues;
    }

    private void readErrors(String systemId, List<Issue> issues) {
        Scriptable JSLINT = (Scriptable) scope.get("JSLINT", scope);
        Scriptable errors = (Scriptable) JSLINT.get("errors", JSLINT);
        int count = Util.intValue("length", errors);
        for (int i = 0; i < count; i++) {
            Scriptable err = (Scriptable) errors.get(i, errors);
            issues.add(new Issue(systemId, err));
        }
    }

    /**
     * Clear out all options that have been set with {@link #addOption(Option)}.
     */
    public void resetOptions() {
        options = EnumSet.noneOf(Option.class);
    }

    private void setJavaScriptOptions(String name) {
        String js = "var " + name + " = "
                + Util.optionSetToObjectLiteral(options);
        ctx.evaluateString(scope, js, "-", 1, null);
    }

}

@@ -282,16 +282,56 @@
 ontext;%0A
+import org.mozilla.javascript.Function;%0A
 import o
@@ -2372,86 +2372,20 @@
     
-List%3CIssue%3E issues = new ArrayList%3CIssue%3E();%0A        scope.put(%22input%22, scope,
+String src =
  jav
@@ -2417,17 +2417,16 @@
 vaScript
-)
 ;%0A      
@@ -2431,296 +2431,741 @@
     
-setJavaScriptOptions(%22options%22);%0A        String check = %22JSLINT(input, options)%22;%0A        Boolean ok = (Boolean) ctx.evaluateString(scope, check, %22lint()%22, 1,%0A                null);%0A        if (!ok.booleanValue()) %7B%0A            readErrors(systemId, issues);%0A        %7D%0A        return issue
+Object%5B%5D args = new Object%5B%5D %7B src, optionsAsJavaScriptObject() %7D;%0A        Function lintFunc = (Function) scope.get(%22JSLINT%22, scope);%0A        Boolean ok = (Boolean) lintFunc.call(ctx, scope, scope, args);%0A        List%3CIssue%3E issues = new ArrayList%3CIssue%3E();%0A        if (!ok.booleanValue()) %7B%0A            readErrors(systemId, issues);%0A        %7D%0A        return issues;%0A    %7D%0A%0A    /**%0A     * Turn the set of options into a JavaScript object, where the key is the%0A     * name of the option and the value is true.%0A     */%0A    private Scriptable optionsAsJavaScriptObject() %7B%0A        Scriptable opts = ctx.newObject(scope);%0A        for (Option o : options) %7B%0A            opts.put(o.getLowerName(), opts, true);%0A        %7D%0A        return opt
 s;%0A 
@@ -3793,219 +3793,6 @@
  %7D%0A%0A
-    private void setJavaScriptOptions(String name) %7B%0A        String js = %22var %22 + name + %22 = %22%0A                + Util.optionSetToObjectLiteral(options);%0A        ctx.evaluateString(scope, js, %22-%22, 1, null);%0A    %7D%0A%0A
 %7D%0A

Call the JSLINT function inside JavaScript directly, instead of via eval(). This means less clutter in the global namespace.