package org.kne.cloud.network.te;

import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

import org.kne.cloud.network.srv6.DijkstraAlgorithm;

public class BandwidthDistributer<T> {
	private long totalBandwidth;
	private AtomicLong totalBandwidthRequest=new AtomicLong();
	private long totalBandwidthRequestOld;
	private volatile Map<T,Long> distrmap=new ConcurrentHashMap<T,Long>();
	private Map<T,Long> reqmap=new ConcurrentHashMap<T,Long>();
	private Map<T,Consumer<Long>> distrUpdateConsumerMap=new ConcurrentHashMap<T,Consumer<Long>>();
	private BiConsumer<T,Long> distUpdateConsumer;
	private Consumer<Long> totalRequestUpdateConsumer;
	private Lock rlock=new ReentrantLock();
	
	public void setDistrUpdateConsumer(T address,Consumer<Long>con) {
		if(con==null) {
			distrUpdateConsumerMap.remove(address);
		}else {
			distrUpdateConsumerMap.put(address, con);
		}
	}

	public Map<T, Consumer<Long>> getDistrUpdateConsumerMap() {
		return distrUpdateConsumerMap;
	}


	public Consumer<Long> getTotalRequestUpdateConsumer() {
		return totalRequestUpdateConsumer;
	}


	public void setTotalRequestUpdateConsumer(Consumer<Long> totalRequestUpdateConsumer) {
		this.totalRequestUpdateConsumer = totalRequestUpdateConsumer;
	}


	public BiConsumer<T, Long> getDistUpdateConsumer() {
		return distUpdateConsumer;
	}


	public void setDistUpdateConsumer(BiConsumer<T, Long> distUpdateConsumer) {
		this.distUpdateConsumer = distUpdateConsumer;
	}


	private class BandwidthDistributeInfo implements Comparable<BandwidthDistributeInfo>{
		private T address;
		private long distBandwidth;
		@Override
		public int compareTo(BandwidthDistributeInfo o) {
			if(distBandwidth>o.distBandwidth) {
				return 1;
			}else if(distBandwidth<o.distBandwidth){
				return -1;
			}else{
				return 0;
			}
		}
		public BandwidthDistributeInfo(T address, long distBandwidth) {
			super();
			this.address = address;
			this.distBandwidth = distBandwidth;
		}
		@Override
		public String toString() {
			return "BandwidthDistributeInfo [address=" + address + ", distBandwidth=" + distBandwidth + "]";
		}
		
	}
	
	public BandwidthDistributer() {
		super();
	}


	public BandwidthDistributer(long totalBandwidth) {
		super();
		this.totalBandwidth = totalBandwidth;
	}
	public long getTotalBandwidth() {
		return totalBandwidth;
	}


	public void setTotalBandwidth(long totalBandwidth) {
		rlock.lock();
		try {
		if(totalBandwidth!=this.totalBandwidth) {
		this.totalBandwidth = totalBandwidth;
		applyNewDistribute(redistribute(reqmap));
		}
		}finally {
			rlock.unlock();
		}
	}

	public void setBandwidthRequest(T addr,long bandwidth){
		if(bandwidth<0)
			throw new IllegalArgumentException("bandwidth<0");
		rlock.lock();
		boolean changed=false;
		try {
		if(bandwidth==0) {
			Long ol=reqmap.remove(addr);
			if(ol!=null) {
			totalBandwidthRequest.addAndGet(- ol);
			changed=true;
			}
		}else {
			totalBandwidthRequest.addAndGet(bandwidth);
			Long ol=reqmap.put(addr, bandwidth);
			if(ol!=null) {
			totalBandwidthRequest.addAndGet(- ol);
			if(bandwidth!=ol)
				changed=true;
			}else {
				changed=true;
			}
		}
		if(totalBandwidthRequest.get()!= totalBandwidthRequestOld) {
			totalBandwidthRequestOld=totalBandwidthRequest.get();
			if(totalRequestUpdateConsumer!=null)
				totalRequestUpdateConsumer.accept(totalBandwidthRequest.get());
			applyNewDistribute(redistribute(reqmap));
		}
		//if(changed)
		}finally {
			rlock.unlock();
		}
	}
	
	public long getMaxRequestableBandwidth(T addr) {
		rlock.lock();
		try {
			Map<T, Long> reqmapclo=new HashMap<>( reqmap);
			reqmapclo.put(addr, DijkstraAlgorithm.MAX_WEIGHT);
			Map<T, Long> result=redistribute(reqmapclo);
			return result.get(addr);
		}finally {
			rlock.unlock();
		}
	}
	
	
	private Map<T,Long> redistribute(Map<T,Long> reqmap) {
		List<BandwidthDistributeInfo>ndistrlist=new  ArrayList<BandwidthDistributeInfo>(reqmap.size());
		
		for (Entry<T, Long> entry : reqmap.entrySet()) {
			T key = entry.getKey();
			Long val = entry.getValue();
			ndistrlist.add(new BandwidthDistributeInfo(key, val));
		}
		
		Collections.sort(ndistrlist);
		int sumb=0;
		for (int i = 0; i < ndistrlist.size(); i++) {
			int avgingCount=ndistrlist.size()-i;
			long ibw=(totalBandwidth-sumb)/(long)avgingCount;
			if(ibw<=ndistrlist.get(i).distBandwidth) {
				for (int j = i; j < ndistrlist.size(); j++) {
					ndistrlist.get(j).distBandwidth=ibw;
				}
				break;
			}
			sumb+=ndistrlist.get(i).distBandwidth;
		}
		//System.out.println(ndistrlist);
		Map<T,Long> newdistrmap=new HashMap<T,Long>();
		for (int i = 0; i < ndistrlist.size(); i++) {
			BandwidthDistributeInfo bwi=ndistrlist.get(i);
			newdistrmap.put(bwi.address, bwi.distBandwidth);
		}
		return newdistrmap;
	}

	

	private void applyNewDistribute(Map<T, Long> newdistrmap) {
		for (Entry<T, Long> entry : newdistrmap.entrySet()) {
			T key = entry.getKey();
			Long val = entry.getValue();
			Long oval=distrmap.get(key);
			if(oval!=null) {
				if(oval.longValue()!=val.longValue()) {
					noticeDistributeUpdate(key, val);
					
					distrmap.put(key, val);
				}
			}else {
				noticeDistributeUpdate(key, val);
				
				distrmap.put(key, val);
			}
		}
		for (Iterator<Entry<T, Long>> iterator = distrmap.entrySet().iterator(); iterator.hasNext();) {
			Entry<T, Long> entry = (Entry<T, Long>) iterator.next();
			if(!newdistrmap.containsKey(entry.getKey())) {
				noticeDistributeUpdate(entry.getKey(), 0L);
				
				iterator.remove();
			}
		}
	}


	private void noticeDistributeUpdate(T key, Long val) {
		if(distUpdateConsumer!=null) {
			distUpdateConsumer.accept(key, val);
		}
		Consumer<Long>conl= distrUpdateConsumerMap.get(key);
		if(conl!=null) {
			conl.accept(val);
		}
	}

	
	public Map<T, Long> getDistrmap() {
		return distrmap;
	}


	


	public Map<T, Long> getReqmap() {
		return reqmap;
	}


	@Override
	public String toString() {
		return "BandwidthDistributer [totalBandwidth=" + totalBandwidth + ", distrmap=" + distrmap + ", reqmap="
				+ reqmap + "]";
	}


	public static void main(String[] args) throws UnknownHostException {
		BandwidthDistributer<Inet6Address> bwd=new BandwidthDistributer<Inet6Address>(100*1000000L);
		bwd.setDistUpdateConsumer((k,v)->{System.out.println(((Inet6Address) k).getHostAddress()+" "+v);});
		bwd.setTotalRequestUpdateConsumer((l)->{System.out.println(l);});
		bwd.setBandwidthRequest((Inet6Address) InetAddress.getByName("::2"),1*1000000L );
		bwd.setBandwidthRequest((Inet6Address) InetAddress.getByName("::4"),10*1000000L );
		System.out.println("max:"+bwd.getMaxRequestableBandwidth((Inet6Address) InetAddress.getByName("::5")));
		bwd.setBandwidthRequest((Inet6Address) InetAddress.getByName("::5"),100*1000000L );
		bwd.setTotalBandwidth(110*1000000L);
		bwd.setBandwidthRequest((Inet6Address) InetAddress.getByName("::2"),0L );
		
	}
}
