/* This code is part of Freenet. It is distributed under the GNU General
 * Public License, version 2 (or at your option any later version). See
 * http://www.gnu.org/ for further details of the GPL. */
package freenet.node;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import freenet.io.comm.FreenetInetAddress;
import freenet.io.comm.Peer;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.transport.ip.IPUtil;

/**
 * Combine the detected IP address with the NodeCrypto's port number and the port numbers we have
 * on connections in the given class to get a list of Peer's.
 * @author toad
 */
public class NodeIPPortDetector {

	/** The Node object */
	final Node node;
	/** The NodeIPDetector which determines the node's IP address but not its port number */
	final NodeIPDetector ipDetector;
	/** The NodeCrypto with the node's port number and list of peers */
	final NodeCrypto crypto;
	/** ARK inserter. */
	private final NodeARKInserter arkPutter;
	/** Last detected IP address */
	Peer[] lastPeers;

        private static volatile boolean logMINOR;
	static {
		Logger.registerLogThresholdCallback(new LogThresholdCallback(){
			@Override
			public void shouldUpdate(){
				logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
			}
		});
	}
	
	NodeIPPortDetector(Node node, NodeIPDetector ipDetector, NodeCrypto crypto, boolean enableARKs) {
		this.node = node;
		this.ipDetector = ipDetector;
		this.crypto = crypto;
		arkPutter = new NodeARKInserter(node, crypto, this, enableARKs);
		ipDetector.addPortDetector(this);
	}

	/**
	 * Combine the NodeIPDetector's output with any per-port information we may have to get a definitive
	 * (for that port/NodeCrypto) list of IP addresses (still without port numbers).
	 */
	FreenetInetAddress[] detectPrimaryIPAddress() {
		FreenetInetAddress addr = crypto.getBindTo();
		if(addr.isRealInternetAddress(false, true, ipDetector.allowBindToLocalhost)) {
			// Binding to a real internet address => don't want us to use the others, most likely
			// he is on a multi-homed box where only one IP can be used for Freenet.
			return new FreenetInetAddress[] { addr };
		}
		return ipDetector.detectPrimaryIPAddress(!crypto.getConfig().includeLocalAddressesInNoderefs);
	}

	/**
	 * Get our Peer's. This is a list of IP:port's at which we might be contactable. Some of them
	 * will have the same port as the listenPort, but if we are behind a NAT which rewrites our
	 * port number, some of them may not. (If we're behind a symmetric NAT which rewrites it 
	 * differently for each connection, we're stuffed, and we tell the user).
	 */
	Peer[] detectPrimaryPeers() {
		final boolean logMINOR = NodeIPPortDetector.logMINOR;
		ArrayList<Peer> addresses = new ArrayList<Peer>();
		FreenetInetAddress[] addrs = detectPrimaryIPAddress();
		for(FreenetInetAddress addr: addrs) {
			addresses.add(new Peer(addr, crypto.getPortNumber()));
			if(logMINOR)
				Logger.minor(this, "Adding "+addr);
		}
		// Now try to get the rewritten port number from our peers.
		// Only considering those within this crypto port, this time.
		
		PeerNode[] peerList = crypto.getPeerNodes();
		
		if(peerList != null) {
			HashMap<Peer,Integer> countsByPeer = new HashMap<Peer,Integer>();
			// FIXME use a standard mutable int object, we have one somewhere
			for(PeerNode pn: peerList) {
				Peer p = pn.getRemoteDetectedPeer();
				if((p == null) || p.isNull()) continue;
				// DNSRequester doesn't deal with our own node
				if(!IPUtil.isValidAddress(p.getAddress(true), false)) continue;
				if(logMINOR)
					Logger.minor(this, "Peer "+pn.getPeer()+" thinks we are "+p);
				if(countsByPeer.containsKey(p)) {
					countsByPeer.put(p, countsByPeer.get(p) + 1);
				} else {
					countsByPeer.put(p, 1);
				}
			}
			if(countsByPeer.size() == 1) {
				Iterator<Peer> it = countsByPeer.keySet().iterator();
				Peer p = (it.next());
				Logger.minor(this, "Everyone agrees we are "+p);
				if(!addresses.contains(p)) {
					addresses.add(p);
				}
			} else if(countsByPeer.size() > 1) {
				// Take two most popular addresses.
				Peer best = null;
				Peer secondBest = null;
				int bestPopularity = 0;
				int secondBestPopularity = 0;
				for (Map.Entry<Peer,Integer> entry : countsByPeer.entrySet()) {
					Peer cur = entry.getKey();
					int curPop = entry.getValue();
					Logger.normal(this, "Detected peer: "+cur+" popularity "+curPop);
					if(curPop >= bestPopularity) {
						secondBestPopularity = bestPopularity;
						bestPopularity = curPop;
						secondBest = best;
						best = cur;
					}
				}
				if(best != null) {
					if((bestPopularity > 1) || (addrs.length == 0)) {
 						if(!addresses.contains(best)) {
							Logger.normal(this, "Adding best peer "+best+" ("+bestPopularity+ ')');
							addresses.add(best);
						}
						if((secondBest != null) && (secondBestPopularity > 1)) {
							if(!addresses.contains(secondBest)) {
								Logger.normal(this, "Adding second best peer "+secondBest+" ("+secondBest+ ')');
								addresses.add(secondBest);
							}
							if(best.getAddress().equals(secondBest.getAddress()) && bestPopularity == 1) {
								Logger.error(this, "Hrrrm, maybe this is a symmetric NAT? Expect trouble connecting!");
								System.err.println("Hrrrm, maybe this is a symmetric NAT? Expect trouble connecting!");
								
								ipDetector.setMaybeSymmetric();
								
								Peer p = new Peer(best.getFreenetAddress(), crypto.getPortNumber());
								if(!addresses.contains(p))
									addresses.add(p);

							}
						}
					}
				}
			}
		}
		lastPeers = addresses.toArray(new Peer[addresses.size()]);
		if(logMINOR)
			Logger.minor(this, "Returning for port "+crypto.getPortNumber()+" : "+Arrays.toString(lastPeers));
		return lastPeers;
	}
	
	void update() {
		arkPutter.update();
	}

	void startARK() {
		arkPutter.start();
	}

	public Peer[] getPrimaryPeers() {
		if(lastPeers == null)
			return detectPrimaryPeers();
		else
			return lastPeers;
	}

	public boolean includes(FreenetInetAddress addr) {
		FreenetInetAddress[] a = detectPrimaryIPAddress();
		for(FreenetInetAddress ai: a)
			if(ai.equals(addr)) return true;
		return false;
	}
}
