package org.kne.cloud.network.klalb;

import java.net.BindException;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import com.google.gson.internal.Pair;

public class PortBinder {
	private static Inet6Address ANYLA;
	static {
		try {
			ANYLA=(Inet6Address) Inet6Address.getByAddress(new byte[16]);
		} catch (UnknownHostException e) {
		}
	}
	private KLALBController controller;
	public PortBinder(KLALBController controller) {
		super();
		this.controller = controller;
	}
	private ReentrantReadWriteLock bindMaplock=new ReentrantReadWriteLock();
	private Map<InetSocketAddress,BindableKLALBPacketConsumer>bindMap=new ConcurrentHashMap<>();
	private ReentrantReadWriteLock listenMaplock=new ReentrantReadWriteLock();
	private Map<InetSocketAddress,BindableKLALBPacketConsumer>listenMap=new ConcurrentHashMap<>();
	private ReentrantReadWriteLock connectMaplock=new ReentrantReadWriteLock();
	private Map<Pair<InetSocketAddress,InetSocketAddress>,BindableKLALBPacketConsumer>connectMap=new ConcurrentHashMap<>();
	private volatile int portn=65535;
	public void bind(BindableKLALBPacketConsumer ks) throws BindException {
		bindMaplock.writeLock().lock();
		try {
			int pold=ks.getLocalPort();
			if(pold==0) {
				int portnod=portn+1;
				if(portnod>65535) {
					portnod=1;
				}
				int cnt=0;
			while(true) {
				portn++;
				if(portn>65535) {
					portn=1;
				}
				if(portn==portnod) {
					cnt++;
					if(cnt>1) {
					throw new BindException("can't alloc port");
					}
				}
				boolean flag=true;
				for (Iterator<InetSocketAddress> iterator = bindMap.keySet().iterator(); iterator.hasNext();) {
					InetSocketAddress bindableKLALBPacketConsumer = (InetSocketAddress) iterator
							.next();
					if(Objects.equals(ks.getLocalInetAddress(), bindableKLALBPacketConsumer.getAddress())&&portn==bindableKLALBPacketConsumer.getPort()) {
						flag=false;
						break;
					}
				}
				if(flag)
					break;
			}
			ks.setLocalPort(portn);
			}else {
				for (Iterator<InetSocketAddress> iterator = bindMap.keySet().iterator(); iterator.hasNext();) {
					InetSocketAddress bindableKLALBPacketConsumer = (InetSocketAddress) iterator
							.next();
					if(Objects.equals(ks.getLocalInetAddress(), bindableKLALBPacketConsumer.getAddress())&&pold==bindableKLALBPacketConsumer.getPort()) {
						throw new BindException("port " + pold + " is already bind!");
					}
				}
			}
			bindMap.put(new InetSocketAddress(ks.getLocalInetAddress(), ks.getLocalPort()),ks);
		}finally {
			bindMaplock.writeLock().unlock();
		}
	}
	
	public void unbind(BindableKLALBPacketConsumer ks) {
		bindMaplock.writeLock().lock();
		try {
		removeAsValue(bindMap, ks);
	}finally {
		bindMaplock.writeLock().unlock();
	}
	}
	public void connect(BindableKLALBPacketConsumer ks) throws BindException {
		connectMaplock.writeLock().lock();
		try {
		connectMap.put(new Pair<InetSocketAddress, InetSocketAddress>( new InetSocketAddress(ks.getLocalInetAddress(), ks.getLocalPort()), new InetSocketAddress(ks.getRemoteInetAddress(), ks.getPort())),ks);
		}finally {
			connectMaplock.writeLock().unlock();
		}
		}
	public void disconnect(BindableKLALBPacketConsumer ks) throws BindException {
		connectMaplock.writeLock().lock();
		try {
		removeAsValue(connectMap, ks);
		}finally {
			connectMaplock.writeLock().unlock();
		}
	}

	public void listen(BindableKLALBPacketConsumer ks) throws BindException {
		listenMaplock.writeLock().lock();
		try {
		listenMap.put(new InetSocketAddress(ks.getLocalInetAddress(), ks.getLocalPort()),ks);
		}finally {
			listenMaplock.writeLock().unlock();
		}
	}
	public void unlisten(BindableKLALBPacketConsumer ks) throws BindException {
		/*System.out.println(listenMap);
		new RuntimeException("!").printStackTrace();*/
		listenMaplock.writeLock().lock();
		try {
		removeAsValue(listenMap, ks);
	}finally {
		listenMaplock.writeLock().unlock();
	}
	}
	private static void removeAsValue(Map m,Object obj) {
		Collection col= m.values();
		for (Iterator iterator = col.iterator(); iterator.hasNext();) {
			Object object = (Object) iterator.next();
			if(object.equals(obj)) {
				iterator.remove();
			}
		}
	}
	/*private ReadWriteLock rwl=new ReentrantReadWriteLock();
	private List<BindableKLALBPacketConsumer> bindlist=new ArrayList<>();
	public void bind(BindableKLALBPacketConsumer ks) throws BindException {
		rwl.writeLock().lock();
		try {
			int pold=ks.getLocalPort();
			if(pold==0) {
			int i=1;
			while(true) {
				if(i>65535) {
					throw new BindException("can't alloc port");
				}
				boolean flag=true;
				for (Iterator<BindableKLALBPacketConsumer> iterator = bindlist.iterator(); iterator.hasNext();) {
					BindableKLALBPacketConsumer bindableKLALBPacketConsumer = (BindableKLALBPacketConsumer) iterator
							.next();
					if(Objects.equals(ks.getLocalInetAddress(), bindableKLALBPacketConsumer.getLocalInetAddress())&&i==bindableKLALBPacketConsumer.getLocalPort()&&Objects.equals( ks.getRemoteInetAddress(), bindableKLALBPacketConsumer.getRemoteInetAddress())&&(ks.getPort()==bindableKLALBPacketConsumer.getPort())) {
						flag=false;
						break;
					}
				}
				if(flag)
					break;
				i++;
			}
			ks.setLocalPort(i);
			}else {
				for (Iterator<BindableKLALBPacketConsumer> iterator = bindlist.iterator(); iterator.hasNext();) {
					BindableKLALBPacketConsumer bindableKLALBPacketConsumer = (BindableKLALBPacketConsumer) iterator
							.next();
					if(Objects.equals(ks.getLocalInetAddress(), bindableKLALBPacketConsumer.getLocalInetAddress())&&pold==bindableKLALBPacketConsumer.getLocalPort()&&Objects.equals( ks.getRemoteInetAddress(), bindableKLALBPacketConsumer.getRemoteInetAddress())&&(ks.getPort()==bindableKLALBPacketConsumer.getPort())) {
						throw new BindException("port " + pold + " is already bind!");
					}
				}
			}
		bindlist.add(ks);
		}finally {
			rwl.writeLock().unlock();
		}
	}
	public boolean distributePacketToConsumer(KLALBRemoteLine krl,PortPacket packet) throws SocketTimeoutException {
		rwl.readLock().lock();
		boolean b=false;
		try {
			for (Iterator<BindableKLALBPacketConsumer> iterator = bindlist.iterator(); iterator.hasNext();) {
				BindableKLALBPacketConsumer bindableKLALBPacketConsumer = (BindableKLALBPacketConsumer) iterator.next();
				if(bindableKLALBPacketConsumer.getLocalPort()==packet.getDport()&&(controller.getSelf().equals(bindableKLALBPacketConsumer.getLocalInetAddress())||ANYLA.equals(bindableKLALBPacketConsumer.getLocalInetAddress()))) {
					if(bindableKLALBPacketConsumer.getRemoteInetAddress()==null||bindableKLALBPacketConsumer.getRemoteInetAddress().equals(ANYLA)) {
						bindableKLALBPacketConsumer.accept(krl, (KLALBPacket) packet);
						b=true;
					}else {
						if(bindableKLALBPacketConsumer.getPort()==packet.getSport()&&bindableKLALBPacketConsumer.getRemoteInetAddress().equals(krl.getRemoteVaddr())) {
							bindableKLALBPacketConsumer.accept(krl, (KLALBPacket) packet);
							b=true;
						}
					}
				}
			}
	}finally {
		rwl.readLock().unlock();
	}
		return b;
	}*/
	public boolean distributePacketToConsumer(KLALBRemoteLine krl,PortPacket packet) throws SocketTimeoutException {
		InetSocketAddress local=new InetSocketAddress(controller.getSelf(), packet.getDport());
		InetSocketAddress remote=new InetSocketAddress(krl.getRemoteVaddr(), packet.getSport());
		BindableKLALBPacketConsumer bkc=connectMap.get(new Pair<InetSocketAddress, InetSocketAddress>(local, remote));
		if(bkc!=null) {
			bkc.accept(krl, (KLALBPacket) packet);
			return true;
		}
		InetSocketAddress localany=new InetSocketAddress(ANYLA, packet.getDport());
		bkc=connectMap.get(new Pair<InetSocketAddress, InetSocketAddress>(localany, remote));
		if(bkc!=null) {
			bkc.accept(krl, (KLALBPacket) packet);
			return true;
		}

		 bkc=listenMap.get(local);
		if(bkc!=null) {
			bkc.accept(krl, (KLALBPacket) packet);
			return true;
		}
		bkc=listenMap.get(localany);
		if(bkc!=null) {
			bkc.accept(krl, (KLALBPacket) packet);
			return true;
		}
		
		
		return false;
	}
	public boolean checkIsBind(KLALBVirtualSocketImpl klalbVirtualSocketImpl) {
			return bindMap.containsValue(klalbVirtualSocketImpl);
	}
	public boolean checkIsConnect(BindableKLALBPacketConsumer kservers,InetSocketAddress isaf) throws BindException {
			return connectMap.containsKey(new Pair<InetSocketAddress, InetSocketAddress>( new InetSocketAddress(kservers.getLocalInetAddress(), kservers.getLocalPort()),isaf));
		}

}