package hh.dejsem.net;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLSession;

import hh.dejsem.K;
import hh.dejsem.Prefs;
import hh.lib.D;

public abstract class NetSslChIO implements NetIO, GetPut {
	/**----------------------------------------------------------
	 * base class for SSL engine operations
	 * ----------------------------------------------------------*/
	public D d;
	/*long timeOut = 10*1000L;*/
	SSLEngine engine;
	SSLSession session;
	SSLEngineResult result;		// results from last engine operation
	SSLEngineResult.Status rStatus;				// engine result progress
	SSLEngineResult.HandshakeStatus hsStatus;	// engine handshake progress
	int bytesProduced;			// data bytes produced by engine
	boolean peerMayBeGone = false;
	ServerSocket ss;
	ServerSocketChannel ssc;
	public SocketChannel sc;
	public Selector selector = null;
	public SelectionKey sk;
	public ByteBuffer sslOut;			// SSL outbound
	ByteBuffer sslIn;			// SSL inbound
	ByteBuffer out;				// data outbound
	ByteBuffer in;				// data inbound

	public NetSslChIO(D d) throws Exception {
//		super(d);
		this.d = d.klon(this);
		if(Prefs.sslDebug) { System.setProperty("javax.net.debug", "all"); }
	}

	public void bind(String host, int port) throws Exception {
		ssc = ServerSocketChannel.open();
		ss = ssc.socket();
		ss.bind(new InetSocketAddress(host, port), 1024);
	}

	public boolean isBound() { return ss.isBound(); }

	public void accept() throws Exception {
		createSSLEngine();
		engine.setUseClientMode(false);
		engine.setNeedClientAuth(true);
		selector = Selector.open();
		ssc.configureBlocking(false);
		int selection = 0;
		sk = ssc.register(selector, SelectionKey.OP_ACCEPT);

		selection = selector.select(K.acceptTO);

		selector.selectedKeys().remove(sk);
		if(d.getAct().isFinishing()) return;
		if(selection == 0) throw new SocketTimeoutException("on accept");
		sc = ssc.accept();
		configureSocket();
	}

	public void connect(InetSocketAddress inetAdr) throws Exception {
		createSSLEngine();
		engine.setUseClientMode(true);
		sc = SocketChannel.open();
		sc.socket().setSoTimeout(K.socketTO);
		sc.configureBlocking(true);
		sc.socket().connect(inetAdr, K.socketTO);
		configureSocket();
		wrapAndPut(ByteBuffer.allocate(0), in);		// handshaking
	}

	public boolean isConnected() { return sc != null && sc.isConnected(); }

	public boolean get(ByteBuffer buf) throws Exception {		// false on EOD
		if(d.ll(5)) d.l("get from net, buf=" + D.bufStat(buf) + " ...");
		getAndUnwrap(buf, out);
		if(d.ll(5)) d.l("got from net, buf=" + D.bufStat(buf) + " ...");
		return buf.position() > 0;
	}

	public int put(ByteBuffer buf) throws Exception {
		if(d.ll(5)) d.l("put to net, buf=" + D.bufStat(buf));
		while(buf.hasRemaining()) {
			wrapAndPut(buf, in); }
		int n = buf.position();
		buf.clear();
		return n;
	}

	public void closeSc() throws Exception { terminateClient(in); }

	public void closeSsc() throws Exception { terminateServer(in); }

	abstract void wrapAndPut(ByteBuffer out, ByteBuffer in) throws Exception;

	public void wrap(ByteBuffer out) throws Exception {
		if(d.ll(5)) d.l(String.format("%-16s %-32s %-32s", "before wrap",
				"sslOut=" + D.bufStat(sslOut), "dataOut=" + D.bufStat(out)));
		result = engine.wrap(out, sslOut);
		chkStat("wrap: ");
		if(d.ll(5)) d.l(String.format("%-16s %-32s %-32s %-32s", "after wrap",
				"sslOut=" + D.bufStat(sslOut), "dataOut=" + D.bufStat(out),
				"close=" + engine.isOutboundDone() + "/" + engine.isInboundDone()));
	}

	public void putSslOut() throws Exception {
		sslOut.flip();
		int len = sslOut.remaining();
		while(sslOut.remaining() > 0)
			try { sc.write(sslOut); }
			catch(Exception e) {
				if(isClosed() && e.getMessage().equals("Broken pipe")) break;		// peer is gone after close
				else throw new Exception(e); }
		if(d.ll(4)) d.l(String.format("%-16s %-32s", "written=" + len, "sslOut=" + D.bufStat(sslOut)));
		sslOut.compact();
	}

	public void getAndUnwrap(ByteBuffer in, ByteBuffer out) throws Exception {
		if(d.ll(5)) d.l("reading...");
		getSslIn();
		if(sslIn.position() == 0) return;
		do { unwrap(in); } while(needUnwrap());
		if(needWrap()) wrapAndPut(out, in);
	}

	void unwrap(ByteBuffer in) throws Exception {
		do {
			sslIn.flip();
			if(d.ll(4)) d.l(String.format("%-16s %-32s %-32s", "before unwrap", "sslIn=" + D.bufStat(sslIn), "dataIn=" + D.bufStat(in)));
			result = engine.unwrap(sslIn, in);
			chkStat("unwrap: ");
			sslIn.compact();
			if(d.ll(4)) d.l(String.format("%-16s %-32s %-32s %-32s", "after unwrap", "sslIn=" + D.bufStat(sslIn), "dataIn=" + D.bufStat(in),
					"close=" + engine.isOutboundDone() + "/" + engine.isInboundDone()));
		} while(sslIn.position() > 0 && rStatus == SSLEngineResult.Status.OK);
	}

	void getSslIn() throws Exception {
		int len;
		try {
			if((len = sc.read(sslIn)) < 0 && !peerMayBeGone) {
				if(d.ll(5)) d.l("getSslIn, len read=" + len + ", peerMayBeGone=" + peerMayBeGone);
				throw new IOException("Connection reset by peer");
			}
		}
		catch(Exception e) {
			if(e.getMessage().equals("Connection reset by peer") && peerMayBeGone) len = 0;
			else throw e; }
		if(d.ll(4)) d.l(String.format("%-16s %-32s", "read=" + len, "sslIn=" + D.bufStat(sslIn)));
	}

	void createSSLEngine() throws Exception {
		engine = K.netNode.ctx.createSSLEngine();
		session = engine.getSession();
		getBuffers();
	}

	abstract void configureSocket() throws Exception;

	void getBuffers() {
		int netBufferMax = session.getPacketBufferSize();
		sslOut = ByteBuffer.allocateDirect(netBufferMax);
		sslIn = ByteBuffer.allocateDirect(netBufferMax);
		out = createNetBuffer();
		in = createNetBuffer();
	}

	ByteBuffer createDataBuffer() {
		int appBufferMax = session.getApplicationBufferSize();
		return ByteBuffer.allocate(appBufferMax);
	}

	ByteBuffer createNetBuffer() {
		int netBufferMax = session.getPacketBufferSize();
		return ByteBuffer.allocate(netBufferMax);
	}

	public boolean needWrap() { return rOK() && hsStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP; }

	public boolean needUnwrap() { return rOK() && hsStatus == SSLEngineResult.HandshakeStatus.NEED_UNWRAP; }

	boolean rOK() { return (rStatus == SSLEngineResult.Status.OK || rStatus == SSLEngineResult.Status.CLOSED); }

	boolean isClosed() { return (engine.isOutboundDone() && engine.isInboundDone()); }

	boolean isNotHandshaking() {
		return hsStatus == SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING || hsStatus == SSLEngineResult.HandshakeStatus.FINISHED; }

	boolean isHandshaking() { return !isNotHandshaking(); }

	void closeIn() throws Exception {
		engine.closeInbound();
		if(d.ll(4)) d.l(String.format("%-82s %-32s",
				"SSL engine's inbound closed", "close=" + engine.isOutboundDone() + "/" + engine.isInboundDone())); }

	public void closeOut() {
		if(d.ll(4)) d.l(String.format("%-82s %-32s",
				"SSL engine before close", "close=" + engine.isOutboundDone() + "/" + engine.isInboundDone()));
		engine.closeOutbound();
		if(d.ll(4)) d.l(String.format("%-82s %-32s",
				"SSL engine's outbound closed", "close=" + engine.isOutboundDone() + "/" + engine.isInboundDone())); }

	public void closeSocket() throws Exception {
		if(sc != null) sc.socket().close();
		if(ss != null) ss.close();
	}

	void terminateClient(ByteBuffer dataIn) throws Exception {
		closeOut();
		if(isConnected()) wrapAndPut(ByteBuffer.allocate(0), dataIn);
		closeSocket();
	}

	abstract void terminateServer(ByteBuffer dataIn) throws Exception;

	private void chkStat(String tag) throws Exception {
		rStatus = result.getStatus();
		if(rStatus == SSLEngineResult.Status.CLOSED) peerMayBeGone = true;
		hsStatus = engine.getHandshakeStatus();
		logRes(tag);
		if (result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_TASK) {
			Runnable runnable;
			while ((runnable = engine.getDelegatedTask()) != null) {
				if(d.ll(3)) d.l("\trunning delegated task...");
				runnable.run();
			}
			rStatus = result.getStatus();
			hsStatus = engine.getHandshakeStatus();
			if (hsStatus == SSLEngineResult.HandshakeStatus.NEED_TASK)
				throw new Exception("handshake shouldn't need additional tasks");
			logRes(" ");
		}
	}

	void logRes(String str) {
		if(!d.ll(3)) return;
		String s = String.format("%-8s", str) +
				rStatus + "/" + hsStatus + ", " +
				result.bytesConsumed() + "/" + result.bytesProduced() + " bytes";
		if (hsStatus == SSLEngineResult.HandshakeStatus.FINISHED) s += "\t...ready for application data";
		d.l(s);
	}
}
