package com.reason.merlin;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.reason.Platform;
import org.jetbrains.annotations.Nullable;

import java.io.*;
import java.util.List;

import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;

public class MerlinProcess implements Closeable {

    static final String NO_CONTEXT = null;

    private ObjectMapper objectMapper;
    private Process merlin;
    private BufferedWriter writer;
    private BufferedReader reader;
    private BufferedReader errorReader;

    MerlinProcess(String merlinBin) throws IOException {
        List<String> commands;

        if (Platform.isWindows()) {
            commands = singletonList(merlinBin);
        } else {
            String absolutePath = new File(merlinBin).getAbsoluteFile().getParent();
            commands = asList("bash", "-c", "export PATH=" + absolutePath + ":$PATH && ocamlmerlin");
        }

        ProcessBuilder processBuilder = new ProcessBuilder(commands)
                .redirectErrorStream(true);

        this.merlin = processBuilder.start();
        this.writer = new BufferedWriter(new OutputStreamWriter(merlin.getOutputStream()));
        this.reader = new BufferedReader(new InputStreamReader(merlin.getInputStream()));
        this.errorReader = new BufferedReader(new InputStreamReader(merlin.getErrorStream()));

        this.objectMapper = new ObjectMapper();
    }

    @Override
    public void close() throws IOException {
        if (merlin != null && merlin.isAlive()) {
            try {
                writer.close();
                reader.close();
                errorReader.close();
            } catch (IOException e) {
                // nothing to do
            }
            merlin.destroyForcibly();
            merlin = null;
        }
    }

    @Nullable
    <R> R makeRequest(TypeReference<R> type, @Nullable String filename, String query) {
        if (this.merlin == null) {
            return null;
        }

        try {
            String request;
            if (filename == NO_CONTEXT) {
                request = query;
            } else {
                request = "{\"context\": [\"auto\", " + this.objectMapper.writeValueAsString(filename) + "], " +
                        "\"query\": " + query + "}";
            }
            //System.out.println("=> " + request);

            this.writer.write(request);
            this.writer.flush();

            if (this.errorReader.ready()) {
                StringBuilder errorBuffer = new StringBuilder();
                this.errorReader.lines().forEach(l -> errorBuffer.append(l).append(System.lineSeparator()));
                throw new RuntimeException(errorBuffer.toString());
            } else {
                String content = this.reader.readLine();
                JsonNode jsonNode = this.objectMapper.readTree(content);
                JsonNode responseNode = extractResponse(jsonNode);
                //System.out.println("<= " + responseNode);

                try {
                    return this.objectMapper.convertValue(responseNode, type);
                } catch (RuntimeException e) {
                    System.err.println("Conversion error of request " + content + "\n  " + e.getMessage());
                    throw e;
                }
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private JsonNode extractResponse(JsonNode merlinResult) {
        JsonNode classField = merlinResult.get("class");
        JsonNode value = merlinResult.get("value");

        String responseType = classField.textValue();
        if ("return".equals(responseType)) {
            return value;
        }

        throw new RuntimeException(value.toString());
    }

    String writeValueAsString(Object value) {
        try {
            return this.objectMapper.writeValueAsString(value);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }
}

@@ -249,23 +249,166 @@
 com.
-reason.Platform
+intellij.notification.NotificationType;%0Aimport com.intellij.notification.Notifications;%0Aimport com.reason.Platform;%0Aimport com.reason.ide.ReasonMLNotification
 ;%0Aim
@@ -3249,24 +3249,119 @@
 (jsonNode);%0A
+                if (responseNode == null) %7B%0A                    return null;%0A                %7D%0A
             
@@ -3404,24 +3404,24 @@
 ponseNode);%0A
-
 %0A           
@@ -3820,24 +3820,38 @@
    %7D%0A    %7D%0A%0A
+    @Nullable%0A
     private 
@@ -4158,52 +4158,414 @@
     
-throw new RuntimeException(value.toString())
+// Something went wrong with merlin, it can be: failure%7Cerror%7Cexception%0A        // https://github.com/ocaml/merlin/blob/master/doc/dev/PROTOCOL.md#answers%0A        boolean isFailure = %22failure%22.equals(responseType);%0A        Notifications.Bus.notify(new ReasonMLNotification(%22Merlin%22, responseType, value.toString(), isFailure ? NotificationType.WARNING : NotificationType.ERROR, null));%0A%0A        return null
 ;%0A  

Use a notification to inform user when merlin fail instead of an exception