package org.kne.cloud.network.klalb;

import java.net.BindException;
import java.net.InetSocketAddress;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.kne.cloud.network.IPv6SocketAddress;
import org.kne.cloud.network.ipv6.IPv6Address;

import com.google.gson.internal.Pair;

public class PortBinder<T> {
	private IPv6Address selfAddress;
	public PortBinder(IPv6Address selfAddress) {
		super();
		this.selfAddress = selfAddress;
	}
	private ReentrantReadWriteLock bindMaplock=new ReentrantReadWriteLock();
	private Map<IPv6SocketAddress,BindableConsumer<T>>bindMap=new ConcurrentHashMap<>(2048);
	private ReentrantReadWriteLock listenMaplock=new ReentrantReadWriteLock();
	private Map<IPv6SocketAddress,BindableConsumer<T>>listenMap=new ConcurrentHashMap<>(2048);
	private ReentrantReadWriteLock connectMaplock=new ReentrantReadWriteLock();
	private Map<Pair<IPv6SocketAddress,IPv6SocketAddress>,BindableConsumer<T>>connectMap=new ConcurrentHashMap<>(2048);
	private volatile int portn=65535;
	public void bind(BindableConsumer<T> 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<IPv6SocketAddress> iterator = bindMap.keySet().iterator(); iterator.hasNext();) {
					IPv6SocketAddress bindableKLALBPacketConsumer = (IPv6SocketAddress) iterator
							.next();
					if(Objects.equals(IPv6Address.valueOf( ks.getLocalInetAddress()), bindableKLALBPacketConsumer.getAddress())&&portn==bindableKLALBPacketConsumer.getPort()) {
						flag=false;
						break;
					}
				}
				if(flag)
					break;
			}
			ks.setLocalPort(portn);
			}else {
				for (Iterator<IPv6SocketAddress> iterator = bindMap.keySet().iterator(); iterator.hasNext();) {
					IPv6SocketAddress bindableKLALBPacketConsumer = (IPv6SocketAddress) iterator
							.next();
					if(Objects.equals(IPv6Address.valueOf( ks.getLocalInetAddress()), bindableKLALBPacketConsumer.getAddress())&&pold==bindableKLALBPacketConsumer.getPort()) {
						throw new BindException("port " + pold + " is already bind!");
					}
				}
			}
			bindMap.put(new IPv6SocketAddress(IPv6Address.valueOf( ks.getLocalInetAddress()), ks.getLocalPort()),ks);
		}finally {
			bindMaplock.writeLock().unlock();
		}
	}
	
	public void unbind(BindableConsumer<T> ks) {
		bindMaplock.writeLock().lock();
		try {
		removeAsValue(bindMap, ks);
	}finally {
		bindMaplock.writeLock().unlock();
	}
	}
	public void connect(BindableConsumer<T> ks) throws BindException {
		connectMaplock.writeLock().lock();
		try {
		connectMap.put(new Pair<IPv6SocketAddress, IPv6SocketAddress>( new IPv6SocketAddress(IPv6Address.valueOf(ks.getLocalInetAddress()), ks.getLocalPort()), new IPv6SocketAddress(IPv6Address.valueOf(ks.getRemoteInetAddress()), ks.getPort())),ks);
		}finally {
			connectMaplock.writeLock().unlock();
		}
		}
	public void disconnect(BindableConsumer<T> ks)  {
		connectMaplock.writeLock().lock();
		try {
		removeAsValue(connectMap, ks);
		}finally {
			connectMaplock.writeLock().unlock();
		}
	}

	public void listen(BindableConsumer<T> ks) throws BindException {
		listenMaplock.writeLock().lock();
		try {
		listenMap.put(new IPv6SocketAddress(IPv6Address.valueOf(ks.getLocalInetAddress()), ks.getLocalPort()),ks);
		}finally {
			listenMaplock.writeLock().unlock();
		}
	}
	public void unlisten(BindableConsumer<T> ks)  {
		/*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 BindableConsumer<T> distributePacketToConsumer(IPv6Address srcAddr, PortPacket packet)  {
		int dst=packet.getDstPort();
		int src=packet.getSrcPort();
		IPv6SocketAddress local=new IPv6SocketAddress( selfAddress,dst);
		IPv6SocketAddress remote=new IPv6SocketAddress(srcAddr, src);
		BindableConsumer<T> bkc=connectMap.get(new Pair<IPv6SocketAddress, IPv6SocketAddress>(local, remote));
		if(bkc!=null) {
			return bkc;
		}
		IPv6SocketAddress localany=new IPv6SocketAddress(IPv6Address.UNSPECIFIED, dst);
		bkc=connectMap.get(new Pair<IPv6SocketAddress, IPv6SocketAddress>(localany, remote));
		if(bkc!=null) {
			return bkc;
		}

		 bkc=listenMap.get(local);
		if(bkc!=null) {
			return bkc;
		}
		bkc=listenMap.get(localany);
		if(bkc!=null) {
			return bkc;
		}
		
		
		return null;
	}
	public boolean checkIsBind(BindableConsumer<T> klalbVirtualSocketImpl) {
			return bindMap.containsValue(klalbVirtualSocketImpl);
	}
	public boolean checkIsConnect(BindableConsumer<T> kservers,InetSocketAddress isaf) throws BindException {
			return connectMap.containsKey(new Pair<IPv6SocketAddress, IPv6SocketAddress>( new IPv6SocketAddress(IPv6Address.valueOf( kservers.getLocalInetAddress()), kservers.getLocalPort()),IPv6SocketAddress.valueOf( isaf)));
		}
	public String toString() {
		StringBuilder sb=new StringBuilder();
		sb.append("connectMap： Local<->Remote\n");
		for(Entry<Pair<IPv6SocketAddress, IPv6SocketAddress>, BindableConsumer<T>> p:connectMap.entrySet()) {
		sb.append(p.getKey().first);
		sb.append("<->");
		sb.append(p.getKey().second);
		sb.append(" ");
		sb.append(p.getValue());
		sb.append('\n');
		}
		
		sb.append("bindMap： Local\n");
		for(Entry<IPv6SocketAddress, BindableConsumer<T>> p:bindMap.entrySet()) {
			sb.append(p);
			sb.append(" ");
			sb.append(p.getValue());
		sb.append('\n');
		}
		
		sb.append("listenMap： Local\n");
		for(Entry<IPv6SocketAddress, BindableConsumer<T>> p:listenMap.entrySet()) {
		sb.append(p);
		sb.append(" ");
		sb.append(p.getValue());
		sb.append('\n');
		}
		return sb.toString();
		
	}

}