package lukazitnik.jshint;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.LinkedList;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import lukazitnik.jshint.options.JSHintPanel;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.NativeObject;
import org.mozilla.javascript.Scriptable;
import org.netbeans.modules.editor.NbEditorUtilities;
import org.openide.modules.InstalledFileLocator;
import org.openide.util.NbPreferences;

public class JSHint {

    private static JSHint instance;
    private final Scriptable scope;
    private Function jshint;

    private JSHint() throws IOException {
        Context cx = Context.enter();
        scope = cx.initStandardObjects();

        try {
            jshint = evaluateJSHint(cx, scope);
        } finally {
            Context.exit();
        }
    }

    public static JSHint getInstance () throws IOException {
        if (instance == null) {
            instance = new JSHint();
        }

        return instance;
    }

    public LinkedList<JSHintError> lint(Document d) {
        Context cx = Context.enter();
        LinkedList<JSHintError> result = new LinkedList<>();
        FileObject fo = NbEditorUtilities.getFileObject(d);

        try {
            Scriptable config = jsonToScriptable(cx, scope, getConfig(fo));
            Object args[] = {d.getText(0, d.getLength()), config};
            NativeArray errors = callJSHint(cx, args);

            for (Object error : errors) {
                if (error == null) {
                    // Null is added to the end in case of an "Unrecoverable
                    // syntax error." or "Too many errors.", so we could break
                    // out of the loop just as well
                    continue;
                }

                result.push(new JSHintError((NativeObject) error));
            }
        } catch (IOException | ParseException | BadLocationException ex) {
            Exceptions.printStackTrace(ex);
        } finally {
            Context.exit();
        }

        return result;
    }

    public LinkedList<JSHintError> lint(FileObject fo) {
        Context cx = Context.enter();
        LinkedList<JSHintError> result = new LinkedList<>();

        try {
            Scriptable config = jsonToScriptable(cx, scope, getConfig(fo));
            Object args[] = {fo.asText(), config};
            NativeArray errors = callJSHint(cx, args);

            for (Object error : errors) {
                if (error == null) {
                    // Null is added to the end in case of an "Unrecoverable
                    // syntax error." or "Too many errors.", so we could break
                    // out of the loop just as well
                    continue;
                }

                result.push(new JSHintError((NativeObject) error));
            }
        } catch (IOException | ParseException ex) {
            Exceptions.printStackTrace(ex);
        } finally {
            Context.exit();
        }

        return result;
    }

    private JSONObject getConfig(FileObject fo) throws IOException, ParseException {
        JSONParser parser = new JSONParser();
        FileObject config = findConfig(fo);

        if (config == null) {
            return (JSONObject) parser.parse("{}");
        }

        return (JSONObject) parser.parse(config.asText());
    }

    private FileObject findConfig(FileObject fo) {
        return findFile(".jshintrc", fo.getParent());
    }

    public static FileObject findFile(String name, FileObject folder) {
        FileObject fo = folder.getFileObject(name, "");

        if (fo != null && fo.isData()) {
            return fo;
        }

        if (folder.isRoot()) {
            return null;
        }

        return findFile(name, folder.getParent());
    }

    private Scriptable jsonToScriptable(Context cx, Scriptable scope, JSONObject obj) {
        Scriptable scriptable = cx.newObject(scope);

        for (Object key : obj.keySet()) {
            scriptable.put((String) key, scriptable, obj.get(key));
        }

        return scriptable;
    }

    private Function evaluateJSHint(Context cx, Scriptable scope) throws IOException {
        Reader in = getReader();
        cx.evaluateReader(scope, in, "jshint.js", 1, null);
        return (Function) scope.get("JSHINT", scope);
    }

    private Reader getReader() throws IOException {

        // jshint.js is the web bundle of the JSHint, downloaded from
        // https://raw.githubusercontent.com/jshint/jshint/master/dist/jshint.js
        String defaultJSFile = InstalledFileLocator.getDefault().locate("jshint.js", "lukazitnik.jshint", false).getPath();

        String jSFile = NbPreferences.forModule(JSHintPanel.class).get("jshint.js", defaultJSFile);

        return new BufferedReader(new FileReader(new File(jSFile)));
    }

    // Async executions of this method cause org.mozilla.javascript.JavaScriptException:
    // TypeError: Cannot read property "id" from null (jshint.js#12315) and similar.
    // Maybe it would be cheaper to have multiple instances of this class.
    private synchronized NativeArray callJSHint(Context cx, Object[] args) {
        jshint.call(cx, scope, scope, args);
        return (NativeArray) jshint.get("errors", null);
    }
}

@@ -310,131 +310,8 @@
 el;%0A
-import org.json.simple.JSONObject;%0Aimport org.json.simple.parser.JSONParser;%0Aimport org.json.simple.parser.ParseException;%0A
 impo
@@ -589,24 +589,133 @@
 Scriptable;%0A
+import org.mozilla.javascript.json.JsonParser;%0Aimport org.mozilla.javascript.json.JsonParser.ParseException;%0A
 import org.n
@@ -1637,55 +1637,23 @@
     
-Scriptable config = jsonToScriptable(cx, scope,
+Object config =
  get
@@ -1651,36 +1651,39 @@
 fig = getConfig(
+cx, 
 fo)
-)
 ;%0A            Ob
@@ -2604,55 +2604,23 @@
     
-Scriptable config = jsonToScriptable(cx, scope,
+Object config =
  get
@@ -2630,12 +2630,15 @@
 fig(
+cx, 
 fo)
-)
 ;%0A  
@@ -3357,20 +3357,16 @@
 private 
-JSON
 Object g
@@ -3370,24 +3370,36 @@
 t getConfig(
+Context cx, 
 FileObject f
@@ -3400,34 +3400,37 @@
 ject fo) throws 
-IO
+Parse
 Exception, Parse
@@ -3424,21 +3424,18 @@
 eption, 
-Parse
+IO
 Exceptio
@@ -3447,19 +3447,19 @@
        J
-SON
+son
 Parser p
@@ -3475,18 +3475,27 @@
 ew J
-SON
+son
 Parser(
+cx, scope
 );%0A 
@@ -3537,25 +3537,24 @@
 ig(fo);%0A
-%0A
         
 if (conf
@@ -3549,149 +3549,100 @@
     
-if (config == null) %7B%0A            return (JSONObject) parser.parse(%22%7B%7D%22);%0A        %7D%0A%0A        return (JSONObject) parser.parse(config.asText()
+String json = config == null ? %22%7B%7D%22 : config.asText();%0A        return parser.parseValue(json
 );%0A 
@@ -4092,305 +4092,8 @@
  %7D%0A%0A
-    private Scriptable jsonToScriptable(Context cx, Scriptable scope, JSONObject obj) %7B%0A        Scriptable scriptable = cx.newObject(scope);%0A%0A        for (Object key : obj.keySet()) %7B%0A            scriptable.put((String) key, scriptable, obj.get(key));%0A        %7D%0A%0A        return scriptable;%0A    %7D%0A%0A
     

switch to mozilla's json parser