package jp.cssj.driver.ctip.v2;

import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

public class SSLSocketChannel extends SelectableChannel implements ByteChannel {
	private ByteBuffer peerAppData, appData, netData, peerNetData;
	private final SocketChannel channel;
	private SSLEngineResult res;
	private SSLEngine engine;

	public SSLSocketChannel(SocketChannel sc) {
		this.channel = sc;
	}

	public boolean connect(SocketAddress remote) throws IOException {
		if (!this.channel.connect(remote)) {
			return false;
		}
		SSLContext sslContext = null;
		try {
			sslContext = SSLContext.getInstance("TLS");
			TrustManager[] tm = { new X509TrustManager() {
				public void checkClientTrusted(X509Certificate[] chain,
						String authType) throws CertificateException {
				}

				public void checkServerTrusted(X509Certificate[] chain,
						String authType) throws CertificateException {
				}

				public X509Certificate[] getAcceptedIssuers() {
					return null;
				}
			} };
			sslContext.init(null, tm, null);
			this.engine = sslContext.createSSLEngine();
			this.engine.setUseClientMode(true);
			this.engine.setEnableSessionCreation(true);
			SSLSession session = this.engine.getSession();
			int appBufferMax = session.getApplicationBufferSize();
			int netBufferMax = session.getPacketBufferSize();

			this.appData = ByteBuffer.allocate(appBufferMax);
			this.netData = ByteBuffer.allocate(netBufferMax);
			this.peerAppData = ByteBuffer.allocate(appBufferMax);
			this.peerNetData = ByteBuffer.allocate(netBufferMax);
			this.appData.clear();

			this.write(this.appData);
			while (this.res.getHandshakeStatus() != SSLEngineResult.HandshakeStatus.FINISHED) {
				if (this.res.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_UNWRAP) {
					this.peerNetData.clear();
					while (this.channel.read(this.peerNetData) < 1) {
						Thread.sleep(30);
					}
					this.peerNetData.flip();
					this.unwrap(this.peerNetData);
					if (this.res.getHandshakeStatus() != SSLEngineResult.HandshakeStatus.FINISHED) {
						this.appData.clear();
						this.write(this.appData);
					}
				} else if (res.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_WRAP) {
					this.appData.clear();
					this.write(this.appData);
				} else {
					Thread.sleep(100);
				}
			}
			this.peerAppData.clear();
			this.peerAppData.flip();
		} catch (Exception e) {
			IOException ioe = new IOException();
			ioe.initCause(e);
			throw ioe;
		}
		return true;
	}

	private synchronized ByteBuffer unwrap(ByteBuffer b) throws SSLException {
		this.peerAppData.clear();
		while (b.hasRemaining()) {
			this.res = this.engine.unwrap(b, this.peerAppData);
			switch (this.res.getHandshakeStatus()) {
			case NEED_TASK:
				Runnable task;
				while ((task = this.engine.getDelegatedTask()) != null) {
					task.run();
				}
				break;
			case FINISHED:
				return this.peerAppData;
			default:
				switch (this.res.getStatus()) {
				case BUFFER_UNDERFLOW:
					return this.peerAppData;
				}
			}
		}
		return this.peerAppData;
	}

	public synchronized int write(ByteBuffer src) throws IOException {
		this.netData.clear();
		this.res = this.engine.wrap(src, this.netData);
		this.netData.flip();
		return this.channel.write(this.netData);
	}

	public synchronized int read(ByteBuffer dst) throws IOException {
		int amount = 0, limit;
		if (this.peerAppData.hasRemaining()) {
			limit = Math.min(this.peerAppData.remaining(), dst.remaining());
			for (int i = 0; i < limit; i++) {
				dst.put(this.peerAppData.get());
				amount++;
			}
			return amount;
		}
		if (this.peerNetData.hasRemaining()) {
			this.unwrap(this.peerNetData);
			this.peerAppData.flip();
			limit = Math.min(this.peerAppData.limit(), dst.remaining());
			for (int i = 0; i < limit; i++) {
				dst.put(this.peerAppData.get());
				amount++;
			}
			if (this.res.getStatus() != SSLEngineResult.Status.BUFFER_UNDERFLOW) {
				this.peerNetData.clear();
				this.peerNetData.flip();
				return amount;
			}
		}
		if (!this.peerNetData.hasRemaining()) {
			this.peerNetData.clear();
		} else {
			this.peerNetData.compact();
		}

		if (this.channel.read(this.peerNetData) == -1) {
			this.peerNetData.clear();
			this.peerNetData.flip();
			return -1;
		}
		this.peerNetData.flip();
		this.unwrap(this.peerNetData);
		this.peerAppData.flip();
		limit = Math.min(this.peerAppData.limit(), dst.remaining());
		for (int i = 0; i < limit; i++) {
			dst.put(this.peerAppData.get());
			amount++;
		}
		return amount;
	}

	public boolean isConnected() {
		return this.channel.isConnected();
	}

	public SelectableChannel configureBlocking(boolean b) throws IOException {
		return this.channel.configureBlocking(b);
	}

	public Object blockingLock() {
		return this.channel.blockingLock();
	}

	public boolean isBlocking() {
		return this.channel.isBlocking();
	}

	public boolean isRegistered() {
		return this.channel.isRegistered();
	}

	public SelectionKey keyFor(Selector sel) {
		return this.channel.keyFor(sel);
	}

	public SelectorProvider provider() {
		return this.channel.provider();
	}

	public SelectionKey register(Selector sel, int ops, Object att)
			throws ClosedChannelException {
		return this.channel.register(sel, ops, att);
	}

	public int validOps() {
		return this.channel.validOps();
	}

	protected synchronized void implCloseChannel() throws IOException {
		this.engine.closeOutbound();
		this.appData.clear();
		this.channel.write(this.appData);
		this.channel.close();
	}
}