/*******************************************************************************
 * Copyright (c) 2019 EclipseSource and others.
 *  
 *   This program and the accompanying materials are made available under the
 *   terms of the Eclipse Public License v. 2.0 which is available at
 *   http://www.eclipse.org/legal/epl-2.0.
 *  
 *   This Source Code may also be made available under the following Secondary
 *   Licenses when the conditions for such availability set forth in the Eclipse
 *   Public License v. 2.0 are satisfied: GNU General Public License, version 2
 *   with the GNU Classpath Exception which is available at
 *   https://www.gnu.org/software/classpath/license.html.
 *  
 *   SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 ******************************************************************************/
package com.eclipsesource.glsp.server.launch;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.Channels;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Consumer;
import java.util.function.Function;

import org.apache.log4j.Logger;
import org.eclipse.lsp4j.jsonrpc.Launcher;
import org.eclipse.lsp4j.jsonrpc.MessageConsumer;

import com.eclipsesource.glsp.api.di.GLSPModule;
import com.eclipsesource.glsp.api.json.GsonConfigurator;
import com.eclipsesource.glsp.api.jsonrpc.GLSPClient;
import com.eclipsesource.glsp.api.jsonrpc.GLSPServer;
import com.google.gson.GsonBuilder;
import com.google.inject.Guice;
import com.google.inject.Injector;

public class DefaultGLSPServerLauncher extends GLSPServerLauncher {
	private static Logger log = Logger.getLogger(DefaultGLSPServerLauncher.class);

	private ExecutorService threadPool;
	private AsynchronousServerSocketChannel serverSocket;
	private CompletableFuture<Void> onShutdown;

	public DefaultGLSPServerLauncher(GLSPModule module) {
		super(module);
	}

	@Override
	public void run(String hostname, int port) {
		Future<Void> onClose;
		try {
			onClose = asyncRun(hostname, port);
			onClose.get();
			log.info("Stopped language server");
		} catch (IOException | InterruptedException | ExecutionException e) {
			log.error(e.getMessage());
			e.printStackTrace();
		}

	}

	public Future<Void> asyncRun(String hostname, int port)
			throws IOException, InterruptedException, ExecutionException {
		onShutdown = new CompletableFuture<Void>();

		serverSocket = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(hostname, port));
		threadPool = Executors.newCachedThreadPool();

		CompletionHandler<AsynchronousSocketChannel, Void> handler = new CompletionHandler<AsynchronousSocketChannel, Void>() {
			@Override
			public void completed(AsynchronousSocketChannel result, Void attachment) {
				serverSocket.accept(null, this); // Prepare for the next connection
				DefaultGLSPServerLauncher.this.createClientConnection(result);
			}

			@Override
			public void failed(Throwable exc, Void attachment) {
				log.error("Client Connection Failed: " + exc.getMessage(), exc);
			}
		};

		serverSocket.accept(null, handler);
		log.info("The graphical server launcher is ready to accept new client requests");

		return onShutdown;
	}

	private void createClientConnection(AsynchronousSocketChannel socketChannel) {
		Injector injector = Guice.createInjector(getGLSPModule());
		GsonConfigurator gsonConf = injector.getInstance(GsonConfigurator.class);

		InputStream in = Channels.newInputStream(socketChannel);
		OutputStream out = Channels.newOutputStream(socketChannel);

		Consumer<GsonBuilder> configureGson = (GsonBuilder builder) -> gsonConf.configureGsonBuilder(builder);
		Function<MessageConsumer, MessageConsumer> wrapper = Function.identity();
		GLSPServer languageServer = injector.getInstance(GLSPServer.class);

		Launcher<GLSPClient> launcher = Launcher.createIoLauncher(languageServer, GLSPClient.class, in, out, threadPool,
				wrapper, configureGson);
		languageServer.connect(launcher.getRemoteProxy());
		launcher.startListening();

		try {
			SocketAddress remoteAddress = socketChannel.getRemoteAddress();
			log.info("Started language server for client " + remoteAddress);
		} catch (IOException ex) {
			log.error("Failed to get the remoteAddress for the new client connection: " + ex.getMessage(), ex);
		}
	}

	@Override
	public void shutdown() {
		log.info("Stopping all connections to the language server...");
		if (serverSocket.isOpen()) {
			try {
				serverSocket.close();
			} catch (IOException e) {
				log.error("Failed to close serverSocket: " + e.getMessage(), e);

			}
		}

		threadPool.shutdown();
		onShutdown.complete(null);
		log.info("Stopped language server");
	}
}