package com.tabnine;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.application.ApplicationInfo;
import com.intellij.util.PlatformUtils;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

class TabNineProcess {
    Process proc;
    BufferedReader procLineReader;
    int restartCount = 0;

    TabNineProcess() throws IOException {
        this.startTabNine();
    }

    void startTabNine() throws IOException {
        if (this.proc != null) {
            this.proc.destroy();
            this.proc = null;
        }
        // When we tell TabNine that it's talking to IntelliJ, it won't suggest language server
        // setup since we assume it's already built into the IDE
        List<String> command = new ArrayList<>();
        command.add(TabNineFinder.getTabNinePath());
        List<String> metadata = new ArrayList<>();
        metadata.add("--client-metadata");
        metadata.add("pluginVersion=" + Utils.getPluginVersion());
        metadata.add("clientIsUltimate=" + PlatformUtils.isIdeaUltimate());
        final ApplicationInfo applicationInfo = ApplicationInfo.getInstance();
        if (applicationInfo != null) {
            command.add("--client");
            command.add(applicationInfo.getVersionName());
            metadata.add("clientVersion=" + applicationInfo.getFullVersion());
            metadata.add("clientApiVersion=" + applicationInfo.getApiVersion());
        }
        command.addAll(metadata);
        ProcessBuilder builder = new ProcessBuilder(command);
        this.proc = builder.start();
        this.procLineReader = new BufferedReader(new InputStreamReader(this.proc.getInputStream(), StandardCharsets.UTF_8));
    }

    void restartTabNine(boolean checkCount) throws IOException {
        if (checkCount) {
            if (this.restartCount >= 5) {
                return;
            }
            ++this.restartCount;
        }
        this.startTabNine();
    }

    <T> T request(Request<T> r) {
        Gson gson = new GsonBuilder().create();
        Map<String, Object> jsonObject = new HashMap<>();
        Map<String, Object> requestMap = new HashMap<>();
        requestMap.put(r.name(), r);
        jsonObject.put("version", "2.0.2");
        jsonObject.put("request", requestMap);
        String rJson = gson.toJson(jsonObject) + "\n";
        String responseJson = this.communicateLine(rJson);
        if (responseJson == null) {
            return null;
        } else {
            T response = gson.fromJson(responseJson, r.response());
            if (r.validate(response)) {
                return response;
            } else {
                try {
                    this.restartTabNine(false);
                } catch (IOException e) {
                    Logger.getInstance(getClass()).error("Error restarting TabNine: " + e);
                }
                return null;
            }
        }
    }

    boolean isDead() {
        return this.proc == null;
    }

    String communicateLine(String req) {
        synchronized (this) {
            if (this.isDead()) {
                Logger.getInstance(getClass()).info("TabNine cannot respond to the request because the process is dead.");
                return null;
            }
            try {
                byte[] toWrite = req.getBytes(StandardCharsets.UTF_8);
                synchronized (this) {
                    this.proc.getOutputStream().write(toWrite);
                    this.proc.getOutputStream().flush();
                    return this.procLineReader.readLine();
                }
            } catch (IOException e) {
                Logger.getInstance(getClass()).info("Exception communicating with TabNine: " + e);
                try {
                    this.restartTabNine(true);
                } catch (IOException e2) {
                    Logger.getInstance(getClass()).error("Error restarting TabNine: " + e2);
                    this.proc = null;
                }
            }
        }
        return null;
    }

    static interface Request<T> {
        String name();
        Class<T> response();
        boolean validate(T response);
    }

    static class AutocompleteRequest implements Request<AutocompleteResponse> {
        String before;
        String after;
        String filename;
        boolean region_includes_beginning;
        boolean region_includes_end;
        int max_num_results;

        public String name() {
            return "Autocomplete";
        }

        public Class<AutocompleteResponse> response() {
            return AutocompleteResponse.class;
        }

        public boolean validate(AutocompleteResponse response) {
            return this.before.endsWith(response.old_prefix);
        }
    }

    static class AutocompleteResponse {
        String old_prefix;
        ResultEntry[] results;
        String[] user_message;
    }

    static class ResultEntry {
        String new_prefix;
        String old_suffix;
        String new_suffix;

        String detail;
        Boolean deprecated;
        // TODO other lsp types
    }
}