package org.kne.cloud.network;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.StandardSocketOptions;
import java.nio.channels.SocketChannel;
import java.util.Map;

public class SocketToSocketProxy extends Proxy {
	private SocketListener sl;
	private SocketChannelListener scl;
	private DatagramSocketListener dsl;
	private MultiProtocolSocketAddress listen, cbind, defaultConnect;
	private Map<String, MultiProtocolSocketAddress> detectedConnect;
	private SocketBridgeFactory defaultFactory;
	private SocketChannelBridgeFactory defaultChannelFactory;
	private Map<String, SocketBridgeFactory> detectedFactory;
	private int defaultSoTimeout=120*60*1000;
	
	public int getDefaultSoTimeout() {
		return defaultSoTimeout;
	}

	public void setDefaultSoTimeout(int defaultSoTimeout) {
		this.defaultSoTimeout = defaultSoTimeout;
	}

	public SocketToSocketProxy(MultiProtocolSocketAddress listen, MultiProtocolSocketAddress connect) throws IOException {
		this(listen, new MultiProtocolSocketAddress(new InetSocketAddress(0)), connect);
	}

	public SocketToSocketProxy(String l, String r) throws IOException {
		this(new MultiProtocolSocketAddress(l), new MultiProtocolSocketAddress(r));
	}

	public SocketToSocketProxy(MultiProtocolSocketAddress listen, MultiProtocolSocketAddress cbind,
                               MultiProtocolSocketAddress defaultConnect, Map<String, MultiProtocolSocketAddress> detectedConnect)
			throws IOException {
		this(listen,cbind,defaultConnect,detectedConnect,new DefaultSocketBridgeFactory(),null);
	}
	
	public SocketToSocketProxy(MultiProtocolSocketAddress listen, MultiProtocolSocketAddress cbind,
                               MultiProtocolSocketAddress defaultConnect, Map<String, MultiProtocolSocketAddress> detectedConnect, SocketBridgeFactory defaultFactory, Map<String, SocketBridgeFactory> detectedFactory)
			throws IOException {
		this(listen, cbind, defaultConnect, detectedConnect, defaultFactory, detectedFactory, new DefaultSocketChannelBridgeFactory());
	}
	
	public SocketToSocketProxy(MultiProtocolSocketAddress listen, MultiProtocolSocketAddress cbind,
                               MultiProtocolSocketAddress defaultConnect, Map<String, MultiProtocolSocketAddress> detectedConnect, SocketBridgeFactory defaultFactory, Map<String, SocketBridgeFactory> detectedFactory, SocketChannelBridgeFactory defaultChannelFactory)
			throws IOException {
		this.listen = listen;
		this.cbind = cbind;
		this.defaultConnect = defaultConnect;
		this.detectedConnect = detectedConnect;
		
		this.defaultFactory=defaultFactory;
		this.detectedFactory=detectedFactory;
		
		this.defaultChannelFactory=defaultChannelFactory;
		
		open();
	}

	public SocketToSocketProxy(MultiProtocolSocketAddress listen, MultiProtocolSocketAddress cbind,
                               MultiProtocolSocketAddress defaultConnect) throws IOException {
		this(listen, cbind, defaultConnect,null);
	}

	private void open() throws IOException {
		if (listen.isStream()) {
			if ((detectedConnect == null || detectedConnect.isEmpty())&&(detectedFactory==null||detectedFactory.isEmpty())) {
				if(listen.supportNIO()&&defaultConnect.supportNIO()&&defaultChannelFactory!=null) {
					scl=new SocketChannelListener(listen);
				}else {
				sl = new SocketListener(listen);
				}
			} else {
				sl = new SocketListener(new ProtocolDetectorServerSocket(listen.listenServerSocket()));
			}
			if(scl!=null) {
				scl.setCon((socx)->{
					SocketChannel sck=null;
					try {
						sck=defaultConnect.connectSocketChannel(InetAddress.getByName(cbind.getHost()), cbind.getPort());
						runChannelBridge(defaultChannelFactory, socx, sck);
					} catch (IOException e) {
						e.printStackTrace();
					} finally {
						if (sck != null)
							try {
								sck.close();
							} catch (IOException e) {
								e.printStackTrace();
							}
						try {
							socx.close();
						} catch (IOException e) {
							e.printStackTrace();
						}
					}
				});
			}else {
			sl.setCon((sox) -> {
				Socket sk = null;
				try {
					if (sox instanceof ProtocolDetectorSocket) {
						ProtocolStack ps = ((ProtocolDetectorSocket) sox).getProtocolStack();
						if (!ps.isEmpty()) {
							String pname=ps.pop().getName();
						MultiProtocolSocketAddress pmsa = detectedConnect.get(pname);
						if(pmsa!=null) {
							sk = pmsa.connectSocket(InetAddress.getByName(cbind.getHost()), cbind.getPort());
						}else {
							sk = defaultConnect.connectSocket(InetAddress.getByName(cbind.getHost()), cbind.getPort());
							
						}
						SocketBridgeFactory sbf=detectedFactory.get(pname);
						if(sbf!=null) {
							runBridge(sbf,sox,sk);
						}else {
							runBridge(defaultFactory,sox,sk);
						}
						runBridge(defaultFactory,sox,sk);
						} else {
							sk = defaultConnect.connectSocket(InetAddress.getByName(cbind.getHost()), cbind.getPort());
							runBridge(defaultFactory,sox,sk);
						}
					} else {
						sk = defaultConnect.connectSocket(InetAddress.getByName(cbind.getHost()), cbind.getPort());
						runBridge(defaultFactory,sox,sk);
					}
				} catch (IOException e) {
					e.printStackTrace();
				} finally {
					if (sk != null)
						try {
							sk.close();
						} catch (IOException e) {
							e.printStackTrace();
						}
					try {
						sox.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			});
			}
		} else {
			dsl = new DatagramSocketListener(listen);
			dsl.setCon((dox) -> {
				
			});
		}
	}

	protected void runBridge(SocketBridgeFactory sbf,Socket sk, Socket sox) throws IOException {

		sk.setSoTimeout(defaultSoTimeout);
		sox.setSoTimeout(defaultSoTimeout);
		sk.setTcpNoDelay(true);
		sox.setTcpNoDelay(true);
		SocketBridge sb = sbf.createBridge(sk, sox);
		sb.run();
	}
	
	protected void runChannelBridge(SocketChannelBridgeFactory sbf,SocketChannel sk, SocketChannel sox) throws IOException {
		//sk.setSoTimeout(defaultSoTimeout);
		//sox.setSoTimeout(defaultSoTimeout);
		sk.setOption(StandardSocketOptions.TCP_NODELAY, true);
		sox.setOption(StandardSocketOptions.TCP_NODELAY, true);
		SocketChannelBridge sb = sbf.createBridge(sk, sox);
		sb.run();
	}

	public MultiProtocolSocketAddress getListen() {
		return listen;
	}

	public MultiProtocolSocketAddress getCbind() {
		return cbind;
	}

	public MultiProtocolSocketAddress getDefaultConnect() {
		return defaultConnect;
	}
	
	public Map<String, MultiProtocolSocketAddress> getDetectedConnect() {
		return detectedConnect;
	}

	public SocketBridgeFactory getDefaultFactory() {
		return defaultFactory;
	}

	public Map<String, SocketBridgeFactory> getDetectedFactory() {
		return detectedFactory;
	}

	
	
	public SocketChannelBridgeFactory getDefaultChannelFactory() {
		return defaultChannelFactory;
	}

	@Override
	public void close() throws IOException {
		if (dsl != null)
			dsl.close();
		if (sl != null)
			sl.close();
		if(scl!=null)
			scl.close();
	}

	public InetAddress getListenAddress() {
		if (sl != null) {
			return sl.getServerSocket().getInetAddress();
		} else if(scl!=null){
			try {
				return ((InetSocketAddress)scl.getServerSocketChannel().getLocalAddress()).getAddress();
			} catch (IOException e) {
				return null;
			}
		}else {
			return dsl.getDatagramServerSocket().getLocalAddress();
		}
	}

	public int getListenPort() {
		if (sl != null) {
			return sl.getServerSocket().getLocalPort();
		} else if(scl!=null){
			try {
				return ((InetSocketAddress)scl.getServerSocketChannel().getLocalAddress()).getPort();
			} catch (IOException e) {
				return -1;
			}
		} else {
			return dsl.getDatagramServerSocket().getLocalPort();
		}
	}
}
