package p.studio.lsp.services;

import org.eclipse.lsp4j.jsonrpc.Launcher;
import org.eclipse.lsp4j.launch.LSPLauncher;
import org.eclipse.lsp4j.services.LanguageClient;
import p.studio.lsp.messages.LspProjectContext;
import p.studio.lsp.messages.LspServerConfiguration;
import p.studio.lsp.services.compiler.CompilerLanguageServiceBridge;
import p.studio.lsp.services.protocol.PrometeuLanguageServer;

import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public final class TcpLspServerHost {
    private final LspServerConfiguration serverConfiguration;
    private final LspProjectContext context;
    private final CompilerLanguageServiceBridge compilerBridge;
    private final ExecutorService clientExecutor;
    private volatile boolean closed;
    private volatile ServerSocket serverSocket;
    private volatile Thread acceptThread;

    public TcpLspServerHost(
            final LspServerConfiguration serverConfiguration,
            final LspProjectContext context,
            final CompilerLanguageServiceBridge compilerBridge) {
        this.serverConfiguration = Objects.requireNonNull(serverConfiguration, "serverConfiguration");
        this.context = Objects.requireNonNull(context, "context");
        this.compilerBridge = Objects.requireNonNull(compilerBridge, "compilerBridge");
        this.clientExecutor = Executors.newCachedThreadPool(runnable -> {
            final Thread thread = new Thread(runnable);
            thread.setName("prometeu-lsp-v1-client");
            thread.setDaemon(true);
            return thread;
        });
    }

    public synchronized void start() {
        if (serverSocket != null) {
            return;
        }
        try {
            final InetAddress bindAddress = InetAddress.getByName(serverConfiguration.getEndpoint().host());
            serverSocket = new ServerSocket(serverConfiguration.getEndpoint().port(), 50, bindAddress);
            acceptThread = new Thread(this::acceptLoop, "prometeu-lsp-v1-accept");
            acceptThread.setDaemon(true);
            acceptThread.start();
        } catch (IOException exception) {
            throw new IllegalStateException("failed to start TCP LSP host", exception);
        }
    }

    private void acceptLoop() {
        while (!closed) {
            try {
                final Socket socket = serverSocket.accept();
                clientExecutor.submit(() -> handleClient(socket));
            } catch (IOException exception) {
                if (!closed) {
                    throw new IllegalStateException("failed to accept LSP client", exception);
                }
            }
        }
    }

    private void handleClient(final Socket socket) {
        try (socket) {
            final var server = new PrometeuLanguageServer(context, compilerBridge);
            final Launcher<LanguageClient> launcher = LSPLauncher.createServerLauncher(
                    server,
                    socket.getInputStream(),
                    socket.getOutputStream());
            server.connect(launcher.getRemoteProxy());
            launcher.startListening().get();
        } catch (Exception exception) {
            throw new IllegalStateException("LSP client failed", exception);
        }
    }

    public synchronized void close() throws Exception {
        if (closed) {
            return;
        }
        closed = true;
        if (serverSocket != null) {
            serverSocket.close();
        }
        if (acceptThread != null) {
            acceptThread.interrupt();
        }
        clientExecutor.shutdownNow();
    }
}
