/* Copyright 2007 Freenet Project Inc.
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */
package freenet.io;

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

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Iterator;

import freenet.io.comm.Peer;
import freenet.l10n.NodeL10n;
import freenet.node.FSParseException;
import freenet.node.ProgramDirectory;
import freenet.support.Logger;
import freenet.support.SimpleFieldSet;
import freenet.support.io.FileUtil;

/**
 * Track packet traffic to/from specific peers and IP addresses, in order to
 * determine whether we are open to the internet.
 *
 * Normally there would be one tracker per port i.e. per UdpSocketHandler.
 * @author toad
 */
public class AddressTracker {

	/** PeerAddressTrackerItem's by Peer */
	private final HashMap<Peer, PeerAddressTrackerItem> peerTrackers;

	/** InetAddressAddressTrackerItem's by InetAddress */
	private final HashMap<InetAddress, InetAddressAddressTrackerItem> ipTrackers;

	/** Maximum number of Item's of either type */
	private int MAX_ITEMS = DEFAULT_MAX_ITEMS;
	
	static final int DEFAULT_MAX_ITEMS = 1000;
	static final int SEED_MAX_ITEMS = 10000;

	private long timeDefinitelyNoPacketsReceivedIP;
	private long timeDefinitelyNoPacketsSentIP;

	private long timeDefinitelyNoPacketsReceivedPeer;
	private long timeDefinitelyNoPacketsSentPeer;

	private long brokenTime;

	public static AddressTracker create(long lastBootID, ProgramDirectory runDir, int port) {
		File data = runDir.file("packets-"+port+".dat");
		File dataBak = runDir.file("packets-"+port+".bak");
		dataBak.delete();
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(data);
			BufferedInputStream bis = new BufferedInputStream(fis);
			InputStreamReader ir = new InputStreamReader(bis, StandardCharsets.UTF_8);
			BufferedReader br = new BufferedReader(ir);
			SimpleFieldSet fs = new SimpleFieldSet(br, false, true);
			return new AddressTracker(fs, lastBootID);
		} catch (IOException e) {
			// Fall through
		} catch (FSParseException e) {
			Logger.warning(AddressTracker.class, "Failed to load from disk for port "+port+": "+e, e);
			// Fall through
		} finally {
			if(fis != null)
				try {
					fis.close();
				} catch (IOException e) {
					// Ignore
				}
		}
		return new AddressTracker();
	}

	private AddressTracker() {
		timeDefinitelyNoPacketsReceivedIP = System.currentTimeMillis();
		timeDefinitelyNoPacketsSentIP = System.currentTimeMillis();
		timeDefinitelyNoPacketsReceivedPeer = System.currentTimeMillis();
		timeDefinitelyNoPacketsSentPeer = System.currentTimeMillis();
		peerTrackers = new HashMap<Peer, PeerAddressTrackerItem>();
		ipTrackers = new HashMap<InetAddress, InetAddressAddressTrackerItem>();
	}

	private AddressTracker(SimpleFieldSet fs, long lastBootID) throws FSParseException {
		int version = fs.getInt("Version");
		if(version != 2)
			throw new FSParseException("Unknown Version "+version);
		long savedBootID = fs.getLong("BootID");
		if(savedBootID != lastBootID) throw new FSParseException("Unable to load address tracker table, assuming an unclean shutdown: Last ID was " +
				lastBootID+" but stored "+savedBootID);
		// Sadly we don't know whether there were packets arriving during the gap,
		// and some insecure firewalls will use incoming packets to keep tunnels open
		//timeDefinitelyNoPacketsReceived = fs.getLong("TimeDefinitelyNoPacketsReceived");
		timeDefinitelyNoPacketsReceivedPeer = System.currentTimeMillis();
		timeDefinitelyNoPacketsReceivedIP = System.currentTimeMillis();
		timeDefinitelyNoPacketsSentPeer = fs.getLong("TimeDefinitelyNoPacketsSentPeer");
		timeDefinitelyNoPacketsSentIP = fs.getLong("TimeDefinitelyNoPacketsSentIP");
		peerTrackers = new HashMap<Peer, PeerAddressTrackerItem>();
		SimpleFieldSet peers = fs.subset("Peers");
		if(peers != null) {
		Iterator<String> i = peers.directSubsetNameIterator();
		if(i != null) {
		while(i.hasNext()) {
			SimpleFieldSet peer = peers.subset(i.next());
			PeerAddressTrackerItem item = new PeerAddressTrackerItem(peer);
			peerTrackers.put(item.peer, item);
		}
		}
		}
		ipTrackers = new HashMap<InetAddress, InetAddressAddressTrackerItem>();
		SimpleFieldSet ips = fs.subset("IPs");
		if(ips != null) {
		Iterator<String> i = ips.directSubsetNameIterator();
		if(i != null) {
		while(i.hasNext()) {
			SimpleFieldSet peer = ips.subset(i.next());
			InetAddressAddressTrackerItem item = new InetAddressAddressTrackerItem(peer);
			ipTrackers.put(item.addr, item);
		}
		}
		}
	}

	public void sentPacketTo(Peer peer) {
		packetTo(peer, true);
	}

	public void receivedPacketFrom(Peer peer) {
		packetTo(peer, false);
	}

	private void packetTo(Peer peer, boolean sent) {
		Peer peer2 = peer.dropHostName();
		if(peer2 == null) {
			Logger.error(this, "Impossible: No host name in AddressTracker.packetTo for "+peer);
			return;
		}
		peer = peer2;

		InetAddress ip = peer.getAddress();
		long now = System.currentTimeMillis();
		synchronized(this) {
			PeerAddressTrackerItem peerItem = peerTrackers.get(peer);
			if(peerItem == null) {
				peerItem = new PeerAddressTrackerItem(timeDefinitelyNoPacketsReceivedPeer, timeDefinitelyNoPacketsSentPeer, peer);
				if(peerTrackers.size() > MAX_ITEMS) {
					Logger.error(this, "Clearing peer trackers on "+this);
					peerTrackers.clear();
					ipTrackers.clear();
					timeDefinitelyNoPacketsReceivedPeer = now;
					timeDefinitelyNoPacketsSentPeer = now;
				}
				peerTrackers.put(peer, peerItem);
			}
			if(sent)
				peerItem.sentPacket(now);
			else
				peerItem.receivedPacket(now);
			InetAddressAddressTrackerItem ipItem = ipTrackers.get(ip);
			if(ipItem == null) {
				ipItem = new InetAddressAddressTrackerItem(timeDefinitelyNoPacketsReceivedIP, timeDefinitelyNoPacketsSentIP, ip);
				if(ipTrackers.size() > MAX_ITEMS) {
					Logger.error(this, "Clearing IP trackers on "+this);
					peerTrackers.clear();
					ipTrackers.clear();
					timeDefinitelyNoPacketsReceivedIP = now;
					timeDefinitelyNoPacketsSentIP = now;
				}
				ipTrackers.put(ip, ipItem);
			}
			if(sent)
				ipItem.sentPacket(now);
			else
				ipItem.receivedPacket(now);
		}
	}

	public synchronized void startReceive(long now) {
		timeDefinitelyNoPacketsReceivedIP = now;
		timeDefinitelyNoPacketsReceivedPeer = now;
	}

	public synchronized void startSend(long now) {
		timeDefinitelyNoPacketsSentIP = now;
		timeDefinitelyNoPacketsSentPeer = now;
	}

	public synchronized PeerAddressTrackerItem[] getPeerAddressTrackerItems() {
		PeerAddressTrackerItem[] items = new PeerAddressTrackerItem[peerTrackers.size()];
		return peerTrackers.values().toArray(items);
	}

	public synchronized InetAddressAddressTrackerItem[] getInetAddressTrackerItems() {
		InetAddressAddressTrackerItem[] items = new InetAddressAddressTrackerItem[ipTrackers.size()];
		return ipTrackers.values().toArray(items);
	}

	public enum Status {
		// Note: Order is important! We compare by ordinals in various places.
		// FIXME switch to using member methods.
		DEFINITELY_NATED,
		MAYBE_NATED,
		DONT_KNOW,
		MAYBE_PORT_FORWARDED,
		DEFINITELY_PORT_FORWARDED
	}
	
	/** If the minimum gap is at least this, we might be port forwarded.
	 * RFC 4787 requires at least 2 minutes, but many NATs have shorter timeouts. */
	public final static long MAYBE_TUNNEL_LENGTH = MINUTES.toMillis(5) + SECONDS.toMillis(1);
	/** If the minimum gap is at least this, we are almost certainly port forwarded.
	 * Some stateful firewalls do at least 30 minutes. Hopefully the below is
	 * sufficiently over the top! */
	public final static long DEFINITELY_TUNNEL_LENGTH = HOURS.toMillis(12) + MINUTES.toMillis(1);
	/** Time after which we ignore evidence that we are port forwarded */
	public static final long HORIZON = HOURS.toMillis(24);

	public long getLongestSendReceiveGap() {
		return getLongestSendReceiveGap(HORIZON);
	}

	/**
	 * Find the longest send/known-no-packets-sent ... receive gap.
	 * It is highly unlikely that we are behind a NAT or symmetric
	 * firewall with a timeout less than the returned length.
	 */
	public long getLongestSendReceiveGap(long horizon) {
		long longestGap = -1;
		long now = System.currentTimeMillis();
		PeerAddressTrackerItem[] items = getPeerAddressTrackerItems();
		for(PeerAddressTrackerItem item: items) {
			if(item.packetsReceived() <= 0) continue;
			if(!item.peer.isRealInternetAddress(false, false, false)) continue;
			longestGap = Math.max(longestGap, item.longestGap(horizon, now));
		}
		return longestGap;

	}

	public Status getPortForwardStatus() {
		long minGap = getLongestSendReceiveGap(HORIZON);

		if(minGap > DEFINITELY_TUNNEL_LENGTH)
			return Status.DEFINITELY_PORT_FORWARDED;
		if(minGap > MAYBE_TUNNEL_LENGTH)
			return Status.MAYBE_PORT_FORWARDED;
		// Only take isBroken into account if we're not sure.
		// Somebody could be playing with us by sending bogus FNPSentPackets...
		synchronized(this) {
			if(isBroken()) return Status.DEFINITELY_NATED;
			if(minGap == 0 && timePresumeGuilty > 0 && System.currentTimeMillis() > timePresumeGuilty)
				return Status.MAYBE_NATED;
		}
		return Status.DONT_KNOW;
	}

	private boolean isBroken() {
		return System.currentTimeMillis() - brokenTime < HORIZON;
	}

	public static String statusString(Status status) {
		return NodeL10n.getBase().getString("ConnectivityToadlet.status."+status);
	}

	/** Persist the table to disk */
	public void storeData(long bootID, ProgramDirectory runDir, int port) {
		// Don't write to disk if we know we're NATed anyway!
		if(isBroken()) return;
		File data = runDir.file("packets-"+port+".dat");
		File dataBak = runDir.file("packets-"+port+".bak");
		dataBak.delete();
		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(dataBak);
			BufferedOutputStream bos = new BufferedOutputStream(fos);
			OutputStreamWriter osw = new OutputStreamWriter(bos, StandardCharsets.UTF_8);
			BufferedWriter bw = new BufferedWriter(osw);
			SimpleFieldSet fs = getFieldset(bootID);
			fs.writeTo(bw);
			bw.flush();
			bw.close();
			fos = null;
			FileUtil.moveTo(dataBak, data);
		} catch (IOException e) {
			Logger.error(this, "Cannot store packet tracker to disk");
			return;
		} finally {
			if(fos != null)
				try {
					fos.close();
				} catch (IOException e) {
					// Ignore
				}
		}
	}

	private synchronized SimpleFieldSet getFieldset(long bootID) {
		SimpleFieldSet sfs = new SimpleFieldSet(true);
		sfs.put("Version", 2);
		sfs.put("BootID", bootID);
		sfs.put("TimeDefinitelyNoPacketsReceivedPeer", timeDefinitelyNoPacketsReceivedPeer);
		sfs.put("TimeDefinitelyNoPacketsReceivedIP", timeDefinitelyNoPacketsReceivedIP);
		sfs.put("TimeDefinitelyNoPacketsSentPeer", timeDefinitelyNoPacketsSentPeer);
		sfs.put("TimeDefinitelyNoPacketsSentIP", timeDefinitelyNoPacketsSentIP);
		PeerAddressTrackerItem[] peerItems = getPeerAddressTrackerItems();
		SimpleFieldSet items = new SimpleFieldSet(true);
		if(peerItems.length > 0) {
			for(int i = 0; i < peerItems.length; i++)
				items.put(Integer.toString(i), peerItems[i].toFieldSet());
			sfs.put("Peers", items);
		}
		InetAddressAddressTrackerItem[] inetItems = getInetAddressTrackerItems();
		items = new SimpleFieldSet(true);
		if(inetItems.length > 0) {
		    for(int i = 0; i < inetItems.length; i++)
			items.put(Integer.toString(i), inetItems[i].toFieldSet());
		    sfs.put("IPs", items);
		}
		return sfs;
	}

	/** Called when something changes at a higher level suggesting that the status may be wrong */
	public void rescan() {
		// Do nothing for now, as we don't maintain any final state yet.
	}

	public synchronized void setBroken() {
		brokenTime = System.currentTimeMillis();
	}

	private long timePresumeGuilty = -1;

	public synchronized void setPresumedGuiltyAt(long l) {
		if(timePresumeGuilty <= 0)
			timePresumeGuilty = l;
	}

	public synchronized void setPresumedInnocent() {
		timePresumeGuilty = -1;
	}

	public synchronized void setHugeTracker() {
		MAX_ITEMS = SEED_MAX_ITEMS;
	}
}
