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.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.reason.Platform;
import com.reason.ide.ReasonMLNotification;
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 m_objectMapper;
    private Process m_merlin;
    private BufferedWriter m_writer;
    private BufferedReader m_reader;
    private BufferedReader m_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);

        m_merlin = processBuilder.start();
        m_writer = new BufferedWriter(new OutputStreamWriter(m_merlin.getOutputStream()));
        m_reader = new BufferedReader(new InputStreamReader(m_merlin.getInputStream()));
        m_errorReader = new BufferedReader(new InputStreamReader(m_merlin.getErrorStream()));

        m_objectMapper = new ObjectMapper();
    }

    @Override
    public void close() throws IOException {
        if (m_merlin != null && m_merlin.isAlive()) {
            try {
                m_writer.close();
                m_reader.close();
                m_errorReader.close();
            } catch (IOException e) {
                // nothing to do
            }
            m_merlin.destroyForcibly();
            m_merlin = null;
        }
    }

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

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

            m_writer.write(request);
            m_writer.flush();

            // just a little tempo to have a chance to get data from merlin
            // Not perfect, but might be enough for now
            InterruptedSleep(50);

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

                try {
                    return m_objectMapper.convertValue(responseNode, type);
                } catch (RuntimeException e) {
                    System.err.println("!! Request conversion error");
                    System.err.println("        file: " + filename);
                    System.err.println("     request: " + query);
                    System.err.println("     content: " + content);
                    System.err.println("         msg: " + e.getMessage());
                    throw e;
                }
            }
        } catch (IOException e) {
            System.err.println("!! Request IO error");
            System.err.println("        file: " + filename);
            System.err.println("     request: " + query);
            System.err.println("         msg: " + e.getMessage());
            throw new UncheckedIOException(e);
        }
    }

    @Nullable
    private JsonNode extractResponse(JsonNode merlinResult) {
        JsonNode classField = merlinResult.get("class");
        if (classField == null) {
            return null;
        }
        JsonNode value = merlinResult.get("value");

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

        // Something went wrong with merlin, it can be: failure|error|exception
        // https://github.com/ocaml/merlin/blob/master/doc/dev/PROTOCOL.md#answers
        if ("error".equals(responseType)) {
            Notifications.Bus.notify(new ReasonMLNotification("Merlin", responseType, value.toString(), NotificationType.ERROR, null));
        }

        // failure or error should not be reported to the user
        return null;
    }

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

    private void InterruptedSleep(int timeToWait) {
        try {
            Thread.sleep(timeToWait);
        } catch (InterruptedException e) {
            // Do nothing
        }
    }
}

@@ -3196,32 +3196,66 @@
        %7D else %7B%0A
+                waitUntilReady();%0A
                 
@@ -4471,32 +4471,463 @@
        %7D%0A    %7D%0A%0A
+    private void waitUntilReady() throws IOException %7B%0A        long start = System.currentTimeMillis();%0A        boolean isReady = m_reader.ready();%0A        while (!isReady) %7B%0A            if (60000 %3C (System.currentTimeMillis() - start)) %7B%0A                // max 1s%0A                isReady = true;%0A            %7D else %7B%0A                InterruptedSleep(20);%0A                isReady = m_reader.ready();%0A            %7D%0A        %7D%0A    %7D%0A%0A
     @Nullable%0A  

Add more waiting if needed