package com.charego.lingo.client.multiplayer;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.Consumer;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.core.task.TaskExecutor;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompFrameHandler;
import org.springframework.messaging.simp.stomp.StompHeaders;
import org.springframework.messaging.simp.stomp.StompSession;
import org.springframework.messaging.simp.stomp.StompSession.Subscription;
import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.WebSocketStompClient;

@Component
public class StompTemplate {

	private static final Logger log = LoggerFactory.getLogger(StompTemplate.class);

	@Autowired
	private Environment env;

	@Autowired
	private TaskExecutor taskExecutor;

	@Autowired
	private WebSocketStompClient stompClient;

	private StompSession stompSession;

	private final BlockingQueue<SubscriptionRequest> subscriptionRequests = new LinkedBlockingQueue<>();

	public StompSession getSession() {
		/*
		 * TODO: If STOMP session is null or disconnected, create a new
		 * connection before returning this field.
		 */
		return stompSession;
	}

	@PostConstruct
	private void postConstruct() {
		String webServerUrl = env.getRequiredProperty("lingo.url");
		String webSocketUrl = webServerUrl.replace("http", "ws") + "/sockjs";
		log.info("Connecting to STOMP endpoint: " + webSocketUrl);
		taskExecutor.execute(() -> stompClient.connect(webSocketUrl, new WebSocketSessionHandler()));
		taskExecutor.execute(new WebSocketSessionListener());
	}

	@PreDestroy
	private void preDestroy() {
		if (stompSession != null) {
			log.info("Disconnecting from STOMP endpoint...");
			stompSession.disconnect();
		}
		stompClient.stop();
	}

	public void subscribe(String destination, StompFrameHandler handler) {
		subscribe(destination, handler, null);
	}

	public void subscribe(String destination, StompFrameHandler handler, Consumer<Subscription> callback) {
		try {
			subscriptionRequests.put(new SubscriptionRequest(destination, handler, callback));
		} catch (InterruptedException e) {
			log.error("Failed to subscribe to destination: {}", destination, e);
		}
	}

	private class SubscriptionRequest {
		final String destination;
		final StompFrameHandler handler;
		final Consumer<Subscription> callback;

		SubscriptionRequest(String destination, StompFrameHandler handler, Consumer<Subscription> callback) {
			this.destination = destination;
			this.handler = handler;
			this.callback = callback;
		}

		void onSubscribed(Subscription subscription) {
			if (callback != null) {
				callback.accept(subscription);
			}
		}
	}

	private class WebSocketSessionHandler extends StompSessionHandlerAdapter {
		@Override
		public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
			log.info("Connected to STOMP endpoint");
			stompSession = session;
		}

		@Override
		public void handleException(StompSession session, StompCommand command, StompHeaders headers, byte[] payload, Throwable exception) {
			log.error("STOMP session exception", exception);
			super.handleException(session, command, headers, payload, exception);
		}

		@Override
		public void handleTransportError(StompSession session, Throwable exception) {
			log.error("STOMP session transport error", exception);
			super.handleTransportError(session, exception);
		}
	}

	private class WebSocketSessionListener implements Runnable {

		@Override
		public void run() {
			while (true) {
				if (stompSession == null) {
					try {
						Thread.sleep(1000L);
					} catch (InterruptedException ok) {
						ok.printStackTrace();
					}
					continue;
				}
				try {
					final SubscriptionRequest request = subscriptionRequests.take();
					log.debug("Subscribing to destination: {}", request.destination);
					final Subscription subscription = stompSession.subscribe(request.destination, request.handler);
					request.onSubscribed(subscription);
				} catch (InterruptedException e) {
					log.error("Failed to subscribe", e);
				}
			}
		}
	}

}
