package com.interpress_project.modernshare.client.controller.delegate.secureguard;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.concurrent.ThreadPoolExecutor;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

import com.interpress_project.modernshare.AppKeys;
import com.interpress_project.modernshare.ipcommon.SystemBase;

public class WorkerThread implements Runnable {
	private final SystemBase sb = SystemBase.getInstance();
	private Socket requestSocket = null;
	private SSLSocket tgtSocket = null;
	private BufferedInputStream sock_bis = null, svnsock_bis = null;
	private BufferedOutputStream sock_bos = null, svnsock_bos = null;
	private String targethost;
	private int targetport, tcpbufsz, conntimeout;
	private final int bufsz = AppKeys.BUFFER_SZ;
	private boolean bDoneIn = false, bDoneOut = false;
	private ThreadPoolExecutor workThreadPool = null;
	private static SSLContext context = null;

	/**
	 * WorkerThread
	 * @param requestSocket
	 * @param targethost
	 * @param targetport
	 * @param tcpbufsz
	 * @param conntimeout
	 * @param keycode
	 */
	public WorkerThread(Socket socket, String host, int port, ThreadPoolExecutor workThreadPool, int tcpbufsz,
	    int conntimeout) {
		this.requestSocket = socket;
		this.targethost = host;
		this.targetport = port;
		this.workThreadPool = workThreadPool;
		this.tcpbufsz = tcpbufsz;
		this.conntimeout = conntimeout;

		try {
			this.requestSocket.setSendBufferSize(tcpbufsz); // No need to setup RecievedBufferSize.
			this.requestSocket.setKeepAlive(true);
			this.requestSocket.setTcpNoDelay(true);
		}
		catch (SocketException ex) {
			// Just ignore. 
		}
	}

	/**
	 * getSSLContext
	 * @param trustedKeyStoreFile
	 * @param keyStoreType
	 * @param trustedKeyStorePass
	 * @return
	 * @throws KeyStoreException
	 * @throws IOException
	 * @throws CertificateException
	 * @throws NoSuchAlgorithmException
	 * @throws UnrecoverableKeyException
	 * @throws KeyManagementException
	 */
	private SSLContext getSSLContext(String trustedKeyStoreFile, String keyStoreType, String trustedKeyStorePass)
	    throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException,
	    KeyManagementException {
		KeyStore keyStore = KeyStore.getInstance((keyStoreType != null) ? keyStoreType : KeyStore.getDefaultType());
		FileInputStream in = new FileInputStream(trustedKeyStoreFile);
		try {
			keyStore.load(new FileInputStream(trustedKeyStoreFile), trustedKeyStorePass.toCharArray());
		}
		finally {
			in.close();
		}
		// 使用するキーストアを変更したTrustManagerを作成
		TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
		tmf.init(keyStore);
		TrustManager[] tm = tmf.getTrustManagers();

		// 自前TrustManagerを使うSSLContextの生成
		SSLContext context = SSLContext.getInstance("SSL");
		context.init(null, tm, null);
		return context;
	}

	/**
	 * connect_server
	 * @param hostname
	 * @param port
	 * @throws SocketException
	 * @throws IOException
	 * @throws UnrecoverableKeyException 
	 * @throws NoSuchAlgorithmException 
	 * @throws CertificateException 
	 * @throws KeyStoreException 
	 * @throws KeyManagementException 
	 */
	private void connect_server(String hostname, int port) throws SocketException, IOException, KeyManagementException,
	    KeyStoreException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException {

		if (context == null) {
			context = getSSLContext("jsse_client_ks", null, "jsse_client_ks_pass");
		}

		/**
		 * アプリケーションが RFC1323 で定義されている 64K バイトを超える受信ウィンドウを
		 * 使用可能にする必要がある場合には、ローカルアドレスにバインドする前に 推奨値を
		 * ServerSocket で設定する必要があります。 
		 */
		InetSocketAddress endpoint = new InetSocketAddress(hostname, port);
		tgtSocket = (SSLSocket) context.getSocketFactory().createSocket();
		tgtSocket.setReceiveBufferSize(tcpbufsz);
		tgtSocket.setSendBufferSize(tcpbufsz);
		tgtSocket.setKeepAlive(true);
		tgtSocket.setTcpNoDelay(true);

		tgtSocket.connect(endpoint, this.conntimeout);

		svnsock_bis = new BufferedInputStream(tgtSocket.getInputStream(), tcpbufsz);
		svnsock_bos = new BufferedOutputStream(tgtSocket.getOutputStream(), tcpbufsz);
	}

	/**
	 * run
	 */
	public void run() {
		try {
			_run();
		}
		finally {
			try {
				if (sock_bis != null) {
					sock_bis.close();
				}
				if (sock_bos != null) {
					sock_bos.close();
				}
				requestSocket.close();
				requestSocket = null;
			}
			catch (IOException ex) {
				sb.getLogger().error("requestSocket closing failed: " + ex.toString());
			}

			try {
				if (svnsock_bis != null) {
					svnsock_bis.close();
				}
				if (svnsock_bos != null) {
					svnsock_bos.close();
				}
				tgtSocket.close();
				tgtSocket = null;
			}
			catch (IOException ex) {
				sb.getLogger().error("tgtSocket closing failed: " + ex.toString());
			}
			sb.getLogger().debug("Connection accepted from SVNAdapter done!");
		}
	}

	/**
	 * _run
	 */
	private void _run() {
		try {
			connect_server(targethost, targetport);
		}
		catch (SocketTimeoutException ex) {
			sb.getLogger().error("データセンタへの接続時にタイムアウトが発生しました。(セキュアガード) (ホスト名: " + targethost + ", ポート: " + targetport + ")");
			return;
		}
		catch (Exception ex) {
			sb.getLogger().error(
			  "データセンタへの接続に失敗しました。(セキュアガード) (ホスト名: " + targethost + ", ポート: " + targetport + ":" + ex.toString() + ")");
			return;
		}

		try {
			sock_bis = new BufferedInputStream(requestSocket.getInputStream(), tcpbufsz);
			sock_bos = new BufferedOutputStream(requestSocket.getOutputStream(), tcpbufsz);
		}
		catch (IOException ex) {
			sb.getLogger().error("WorkerThread() caught exception : ", ex);
			return;
		}
		workThreadPool.execute(new StreamGobblerSrc2Dest(sock_bis, svnsock_bos));
		workThreadPool.execute(new StreamGobblerDest2Src(svnsock_bis, sock_bos));

		while ((bDoneIn == false) || (bDoneOut == false)) {
			try {
				Thread.sleep(100);
			}
			catch (InterruptedException ex) {
			}
		}
	}

	/**
	 * StreamGobblerSrc2Dest
	 * @author mshimada
	 */
	private class StreamGobblerSrc2Dest implements Runnable {
		private BufferedInputStream bis = null;
		private BufferedOutputStream bos = null;

		/**
		 * StreamGobblerSrc2Dest
		 * @param bis
		 * @param bos
		 */
		public StreamGobblerSrc2Dest(BufferedInputStream bis, BufferedOutputStream bos) {
			this.bis = bis;
			this.bos = bos;
		}

		/**
		 * setStreams
		 * @param bis
		 * @param bos
		 */
		public void setStreams(BufferedInputStream bis, BufferedOutputStream bos) {
			this.bis = bis;
			this.bos = bos;
		}

		/**
		 * flushBuffer
		 * @param buffer
		 * @param sz
		 * @throws Exception 
		 */
		private void flushBuffer(ByteBuffer buffer, int sz) throws Exception {
			if (sz > 0) {
				bos.write(buffer.array(), 0, sz);
				bos.flush();

				buffer.clear();
			}
		}

		/**
		 * run
		 * SVNAdapterクライアントからのデータはブロック化されているわけではないので
		 * 受け取り側がbufszの大きさのバッファリングをする必要がある。
		 */
		public void run() {
			boolean bClosed = false;
			ByteBuffer buffer = ByteBuffer.allocate(bufsz);
			/**
			 * Bufferingによって3倍程度のパフォーマンスを得られた。
			 */
			int i = 0, c = 0;
			while (true) {
				for (i = 0; i < bufsz; i++) {
					try {
						if (bis.available() == 0) {
							flushBuffer(buffer, i);
							i = 0;
						}
						c = bis.read();
					}
					catch (SocketException ex) {
						bClosed = true;
						sb.getLogger().debug("(IGNORABLE) " + ex.toString());
						break;
					}
					catch (Exception ex) {
						bClosed = true;
						sb.getLogger().error("Exception caught in StreamGobblerSrc2Dest(): ", ex);
						break;
					}
					if (c == -1) {
						bClosed = true;
						break;
					}
					buffer.put(i, (byte) c);
				} /* End Of for loop */

				try {
					flushBuffer(buffer, i);
				}
				catch (Exception ex) {
					bClosed = true;
					sb.getLogger().error(ex);
				}
				if (bClosed) {
					break;
				}
			} /* End Of while(true) loop. */

			try {
				bos.close();
			}
			catch (IOException ex) {
				sb.getLogger().error(ex);
			}
			bDoneIn = true;
		}
	}

	/**
	 * StreamGobblerDest2Src
	 * @author mshimada
	 */
	private class StreamGobblerDest2Src implements Runnable {
		private BufferedInputStream bis = null;
		private BufferedOutputStream bos = null;

		/**
		 * StreamGobblerDest2Src
		 * @param bis
		 * @param bos
		 */
		public StreamGobblerDest2Src(BufferedInputStream bis, BufferedOutputStream bos) {
			this.bis = bis;
			this.bos = bos;
		}

		/**
		 * setStreams
		 * @param bis
		 * @param bos
		 */
		public void setStreams(BufferedInputStream bis, BufferedOutputStream bos) {
			this.bis = bis;
			this.bos = bos;
		}

		/**
		 * run
		 */
		public void run() {
			byte[] buffer = new byte[bufsz];

			try {
				while (true) {
					int len = bis.read(buffer, 0, bufsz); // とりあえず32kbyteの大きさで読んでみる。実際に読み込まれたデータ長が返る。
					if (len == -1) {
						break;
					}
					bos.write(buffer, 0, len);
					bos.flush();
				} /* End Of while(true) loop. */
			}
			catch (SocketException ex) {
				sb.getLogger().debug("(IGNORABLE) " + ex.toString());
			}
			catch (IOException ex) {
				sb.getLogger().error("通信エラーが発生しました。: " + ex.getCause().getMessage());
			}
			bDoneOut = true;
		}
	}
}
