package freenet.node;

import static java.util.concurrent.TimeUnit.HOURS;

import java.net.InetAddress;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Random;

import freenet.l10n.NodeL10n;
import freenet.support.HTMLNode;
import freenet.support.LRUMap;
import freenet.support.io.InetAddressComparator;

/** Tracks announcements by IP address to identify nodes that announce repeatedly. */
public class SeedAnnounceTracker {
	
	private final LRUMap<InetAddress, TrackerItem> itemsByIP = 
		LRUMap.createSafeMap(InetAddressComparator.COMPARATOR);
	
	// This should be plenty for now and limits memory usage to something reasonable.
	private static final int MAX_SIZE = 100*1000;

	/** A single IP address's behaviour */
	private class TrackerItem {
		
		private TrackerItem(InetAddress addr) {
			this.addr = addr;
		}
		
		private final InetAddress addr;
		private int totalSeedConnects;
		private int totalAnnounceRequests;
		private int totalAcceptedAnnounceRequests;
		private int totalCompletedAnnounceRequests;
		private int totalSentRefs;
		private int lastVersion;
		
		public void acceptedAnnounce() {
			totalAnnounceRequests++;
			totalAcceptedAnnounceRequests++;
		}

		public void rejectedAnnounce() {
			totalAnnounceRequests++;
		}

		public void connected() {
			totalSeedConnects++;
		}

		public void setVersion(int ver) {
			if(ver <= 0) return;
			lastVersion = ver;
		}

		public void completed(int forwardedRefs) {
			totalCompletedAnnounceRequests++;
			totalSentRefs += forwardedRefs;
		}
		
	}
	
	// Reset every 2 hours.
	// FIXME implement something smoother.
	static final long RESET_TIME = HOURS.toMillis(2);

	private long lastReset;

	/** If the IP has had at least 5 noderefs, and is out of date, 80% chance of rejection.
	 * If the IP has had 10 noderefs, 75% chance of rejection. */
	public boolean acceptAnnounce(SeedClientPeerNode source, Random fastRandom) {
		InetAddress addr = source.getPeer().getAddress();
		int ver = source.getVersionNumber();
		boolean badVersion = source.isUnroutableOlderVersion();
		long now = System.currentTimeMillis();
		synchronized(this) {
			if(lastReset - now > RESET_TIME) {
				itemsByIP.clear();
				lastReset = now;
			}
			TrackerItem item = itemsByIP.get(addr);
			if(item == null) {
				item = new TrackerItem(addr);
			} else {
				if(item.totalSentRefs > 5 && badVersion) {
					if(fastRandom.nextInt(5) != 0)
						return false;
				} else if(item.totalSentRefs > 10) {
					if(fastRandom.nextInt(4) != 0)
						return false;
				}
			}
			item.acceptedAnnounce();
			item.setVersion(ver);
			itemsByIP.push(addr, item);
			while(itemsByIP.size() > MAX_SIZE)
				itemsByIP.popKey();
			return true;
		}
	}
	
	public void rejectedAnnounce(SeedClientPeerNode source) {
		InetAddress addr = source.getPeer().getAddress();
		int ver = source.getVersionNumber();
		synchronized(this) {
			TrackerItem item = itemsByIP.get(addr);
			if(item == null)
				item = new TrackerItem(addr);
			item.rejectedAnnounce();
			item.setVersion(ver);
			itemsByIP.push(addr, item);
			while(itemsByIP.size() > MAX_SIZE)
				itemsByIP.popKey();
		}
	}

	public void onConnectSeed(SeedClientPeerNode source) {
		InetAddress addr = source.getPeer().getAddress();
		int ver = source.getVersionNumber();
		synchronized(this) {
			TrackerItem item = itemsByIP.get(addr);
			if(item == null)
				item = new TrackerItem(addr);
			item.connected();
			item.setVersion(ver);
			itemsByIP.push(addr, item);
			while(itemsByIP.size() > MAX_SIZE)
				itemsByIP.popKey();
		}
	}
	
	public void completedAnnounce(SeedClientPeerNode source, int forwardedRefs) {
		InetAddress addr = source.getPeer().getAddress();
		int ver = source.getVersionNumber();
		synchronized(this) {
			TrackerItem item = itemsByIP.get(addr);
			if(item == null)
				item = new TrackerItem(addr);
			item.completed(forwardedRefs);
			item.setVersion(ver);
			itemsByIP.push(addr, item);
			while(itemsByIP.size() > MAX_SIZE)
				itemsByIP.popKey();
		}
	}
	
	public void drawSeedStats(HTMLNode content) {
		TrackerItem[] topItems = getTopTrackerItems(20);
		if(topItems.length == 0) return;
		HTMLNode table = content.addChild("table", "border", "0");
		HTMLNode row = table.addChild("tr");
		row.addChild("th", l10nStats("seedTableIP"));
		row.addChild("th", l10nStats("seedTableConnections"));
		row.addChild("th", l10nStats("seedTableAnnouncements"));
		row.addChild("th", l10nStats("seedTableAccepted"));
		row.addChild("th", l10nStats("seedTableCompleted"));
		row.addChild("th", l10nStats("seedTableForwarded"));
		row.addChild("th", l10nStats("seedTableVersion"));
		for(TrackerItem item : topItems) {
			row = table.addChild("tr");
			row.addChild("td", item.addr.getHostAddress());
			row.addChild("td", Integer.toString(item.totalSeedConnects));
			row.addChild("td", Integer.toString(item.totalAnnounceRequests));
			row.addChild("td", Integer.toString(item.totalAcceptedAnnounceRequests));
			row.addChild("td", Integer.toString(item.totalCompletedAnnounceRequests));
			row.addChild("td", Integer.toString(item.totalSentRefs));
			row.addChild("td", Integer.toString(item.lastVersion));
		}
	}

	private synchronized TrackerItem[] getTopTrackerItems(int count) {
		TrackerItem[] items = new TrackerItem[itemsByIP.size()];
		itemsByIP.valuesToArray(items);
		Arrays.sort(items, new Comparator<TrackerItem>() {
			
			@Override
			public int compare(TrackerItem arg0, TrackerItem arg1) {
				int a = Math.max(arg0.totalAnnounceRequests, arg0.totalSeedConnects);
				int b = Math.max(arg1.totalAnnounceRequests, arg1.totalSeedConnects);
				if(a > b) return 1;
				if(b > a) return -1;
				if(arg0.totalAcceptedAnnounceRequests > arg1.totalAcceptedAnnounceRequests)
					return 1;
				else if(arg0.totalAcceptedAnnounceRequests < arg1.totalAcceptedAnnounceRequests)
					return -1;
				return 0;
			}
			
		});
		int topLength = Math.min(count, items.length);
		return Arrays.copyOfRange(items, items.length - topLength, items.length);
	}

	private String l10nStats(String key) {
		return NodeL10n.getBase().getString("StatisticsToadlet."+key);
	}

}
