package org.kareha.hareka.server.game;

import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.kareha.hareka.annotation.GuardedBy;
import org.kareha.hareka.annotation.Private;
import org.kareha.hareka.key.SimpleTrustManager;
import org.kareha.hareka.packet.ParserTable;
import org.kareha.hareka.server.ResourceInternalClientPacketType;

public class InternalConnector {

	@Private
	static final Logger logger = Logger.getLogger(InternalConnector.class.getName());

	@Private
	final Context context;
	@GuardedBy("this")
	private ConnectorThread thread;

	public InternalConnector(final Context context) {
		this.context = context;
	}

	public synchronized void start() {
		if (thread != null) {
			return;
		}
		thread = new ConnectorThread();
		thread.start();
		logger.fine("started");
	}

	public synchronized boolean stop(final long millis) throws InterruptedException {
		if (thread == null) {
			return false;
		}
		thread.interrupt();
		try {
			thread.join(millis);
		} finally {
			thread = null;
		}
		logger.fine("stopped");
		return true;
	}

	public synchronized InternalSession getSession() {
		if (thread == null) {
			return null;
		}
		return thread.session;
	}

	@Private
	static int CHECK_INTERVAL = 4000;

	private class ConnectorThread extends Thread {

		private int retryCount;
		volatile InternalSession session;

		@Private
		ConnectorThread() {

		}

		@SuppressWarnings("resource")
		@Override
		public void run() {
			while (!Thread.currentThread().isInterrupted()) {
				if (session != null && !session.isDisconnected()) {
					retryCount = 0;
					try {
						Thread.sleep(CHECK_INTERVAL);
					} catch (final InterruptedException e) {
						Thread.currentThread().interrupt();
						continue;
					}
				}

				if (retryCount > 0) {
					try {
						Thread.sleep(1000 * (long) Math.pow(2, retryCount));
					} catch (final InterruptedException e) {
						Thread.currentThread().interrupt();
						continue;
					}
				}

				if (session == null || session.isDisconnected()) {
					final GameServerSettings.ConnectionEntry entry = context.getGameServerSettings()
							.getResourceServer();
					final Socket socket;
					if (entry.isConnectSecurely()) {
						final X509TrustManager trustManager = new SimpleTrustManager(
								context.getResourceSimpleKeyStore(), entry.getHost()) {
							@Override
							protected boolean acceptCertificate(final X509Certificate cert) {
								// XXX always accept certificate; temporal implementation
								return true;
							}
						};
						try {
							socket = connectSecurely(entry.getHost(), entry.getInternalPort(), trustManager);
						} catch (final KeyManagementException e) {
							logger.severe(e.getMessage());
							break;
						} catch (final UnknownHostException e) {
							logger.info(e.getMessage());
							retryCount++;
							continue;
						} catch (final SecurityException e) {
							logger.info(e.getMessage());
							retryCount++;
							continue;
						} catch (final SSLHandshakeException e) {
							logger.info(e.getMessage());
							retryCount++;
							continue;
						} catch (final IOException e) {
							logger.info(e.getMessage());
							retryCount++;
							continue;
						}
					} else {
						try {
							socket = connectPlainly(entry.getHost(), entry.getInternalPort());
						} catch (final UnknownHostException e) {
							logger.info(e.getMessage());
							retryCount++;
							continue;
						} catch (final IOException e) {
							logger.info(e.getMessage());
							retryCount++;
							continue;
						}
					}

					// handshake here
					// If this handshake is not done and the certificate is not
					// accepted, an
					// exception will be thrown later in PacketSocket.
					if (socket instanceof SSLSocket) {
						final SSLSocket sslSocket = (SSLSocket) socket;
						try {
							sslSocket.startHandshake();
						} catch (final SSLHandshakeException e) {
							logger.info(e.getMessage());
							try {
								sslSocket.close();
							} catch (final IOException e1) {
								logger.log(Level.WARNING, "", e1);
							}
							retryCount++;
							continue;
						} catch (final IOException e) {
							logger.info(e.getMessage());
							try {
								sslSocket.close();
							} catch (final IOException e1) {
								logger.log(Level.WARNING, "", e1);
							}
							retryCount++;
							continue;
						}
					}
					final ParserTable<ResourceInternalClientPacketType, InternalSession> parserTable = new ParserTable<>(
							"org.kareha.hareka.server.game.iparser", ResourceInternalClientPacketType.class, "Parser");
					session = InternalSession.newInstance(context, entry.getHost(), socket, parserTable);
					session.start();
					logger.info("Connected to " + entry.getHost() + "; internal session started");
					retryCount = 0;
				}
			}
		}

	}

	public static Socket connectPlainly(final String host, final int port) throws UnknownHostException, IOException {
		return new Socket(host, port);
	}

	public static Socket connectSecurely(final String host, final int port, final X509TrustManager trustManager)
			throws UnknownHostException, SecurityException, IOException, KeyManagementException {
		final SSLContext sslContext;
		try {
			sslContext = SSLContext.getInstance("TLS");
		} catch (final NoSuchAlgorithmException e) {
			throw new AssertionError(e);
		}
		final TrustManager[] tm = new TrustManager[] { trustManager };
		sslContext.init(new KeyManager[0], tm, new SecureRandom());
		final SSLSocketFactory socketFactory = sslContext.getSocketFactory();
		return socketFactory.createSocket(host, port);
	}

}
