/* 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.useralerts;

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

import freenet.l10n.NodeL10n;
import freenet.node.NodeStats;
import freenet.node.PeerManager;
import freenet.node.updater.NodeUpdateManager;
import freenet.support.HTMLNode;

public class PeerManagerUserAlert extends AbstractUserAlert {

	final NodeStats n;
	final NodeUpdateManager nodeUpdater;
	// FIXME see comments in update().
	public int conns = 0;
	public int peers = 0;
	public int neverConn = 0;
	public int clockProblem = 0;
	public int connError = 0;
	public int disconnDarknetPeers = 0;
	int bwlimitDelayTime = 1;
	int nodeAveragePingTime = 1;
	long oldestNeverConnectedPeerAge = 0;
	private boolean bwlimitDelayAlertRelevant;
	private boolean nodeAveragePingAlertRelevant;
	public int darknetConns = 0;
	public int darknetPeers = 0;
	public int tooNewPeersDarknet = 0;
	public int tooNewPeersTotal = 0;
	public boolean isOpennetEnabled;
	public boolean darknetDefinitelyPortForwarded;
	public boolean opennetDefinitelyPortForwarded;
	public boolean opennetAssumeNAT;
	public boolean darknetAssumeNAT;
	private boolean isOutdated;
	
	/** How many connected peers we need to not get alert about not enough */
	public static final int MIN_CONN_ALERT_THRESHOLD = 3;
	
	/** How many connected darknet peers we can have without getting alerted about too many */
	public static final int MAX_DARKNET_CONN_ALERT_THRESHOLD = 100;
	
	/** How many disconnected peers we can have without getting alerted about too many */
	public static final int MAX_DISCONN_PEER_ALERT_THRESHOLD = 50;
	
	/** How many never-connected peers can we have without getting alerted about too many */
	public static final int MAX_NEVER_CONNECTED_PEER_ALERT_THRESHOLD = 5;
	
	/** How many peers with clock problems can we have without getting alerted about too many */
	public static final int MIN_CLOCK_PROBLEM_PEER_ALERT_THRESHOLD = 5;
	
	/** How many peers with unknown connection errors can we have without getting alerted */
	public static final int MIN_CONN_ERROR_ALERT_THRESHOLD = 5;
	
	/** How high can oldestNeverConnectedPeerAge be before we alert (in milliseconds)*/
	public static final long MAX_OLDEST_NEVER_CONNECTED_PEER_AGE_ALERT_THRESHOLD = DAYS.toMillis(14); // 2 weeks

	public PeerManagerUserAlert(NodeStats n, NodeUpdateManager nodeUpdater) {
		super(false, null, null, null, null, (short) 0, true, NodeL10n.getBase().getString("UserAlert.hide"), false, null);
		this.n = n;
		this.nodeUpdater = nodeUpdater;
	}
	
	@Override
	public String getTitle() {
		synchronized(this) {
			if(isOutdated)
				return l10n("outdatedUpdateTitle");
			if(!isOpennetEnabled) {
				if(peers == 0)
					return l10n("noPeersTitle");
				if(conns == 0)
					return l10n("noConnsTitle");
				if(conns < MIN_CONN_ALERT_THRESHOLD)
					return l10n("onlyFewConnsTitle", "count", Integer.toString(conns));
			}
			if(bwlimitDelayAlertRelevant && (bwlimitDelayTime > NodeStats.MAX_BWLIMIT_DELAY_TIME_ALERT_THRESHOLD))
				return l10n("tooHighBwlimitDelayTimeTitle");
			if(nodeAveragePingAlertRelevant && (nodeAveragePingTime > NodeStats.MAX_NODE_AVERAGE_PING_TIME_ALERT_THRESHOLD))
				return l10n("tooHighPingTimeTitle");
			if(clockProblem > MIN_CLOCK_PROBLEM_PEER_ALERT_THRESHOLD)
				return l10n("clockProblemTitle");
			if(neverConn > MAX_NEVER_CONNECTED_PEER_ALERT_THRESHOLD)
				return l10n("tooManyNeverConnectedTitle");
			if(connError > MIN_CONN_ERROR_ALERT_THRESHOLD)
				return l10n("connErrorTitle");
			if(disconnDarknetPeers > MAX_DISCONN_PEER_ALERT_THRESHOLD && !darknetDefinitelyPortForwarded && !darknetAssumeNAT)
				return l10n("tooManyDisconnectedTitle");
			if(darknetConns > MAX_DARKNET_CONN_ALERT_THRESHOLD)
				return l10n("tooManyConnsTitle");
			if(oldestNeverConnectedPeerAge > MAX_OLDEST_NEVER_CONNECTED_PEER_AGE_ALERT_THRESHOLD)
				return l10n("tooOldNeverConnectedPeersTitle");
			else throw new IllegalArgumentException("Not valid");
		}
	}
	
	@Override
	public String getShortText() {
		return getTitle();
	}
	
	private String l10n(String key, String pattern, String value) {
		return NodeL10n.getBase().getString("PeerManagerUserAlert."+key, pattern, value);
	}

	private String l10n(String key, String[] pattern, String[] value) {
		return NodeL10n.getBase().getString("PeerManagerUserAlert."+key, pattern, value);
	}

	private String l10n(String key) {
		return NodeL10n.getBase().getString("PeerManagerUserAlert."+key);
	}

	@Override
	public String getText() {
		String s;
		synchronized(this) {
			if(isOutdated)
				return l10n("outdatedUpdate");
			if(peers == 0 && !isOpennetEnabled) {
				return l10n("noPeersDarknet"); 
			} else if(conns < 3 && clockProblem > MIN_CLOCK_PROBLEM_PEER_ALERT_THRESHOLD) {
				s = l10n("clockProblem", "count", Integer.toString(clockProblem));
			} else if(conns < 3 && connError > MIN_CONN_ERROR_ALERT_THRESHOLD && !isOpennetEnabled) {
				s = l10n("connError", "count", Integer.toString(connError));
			} else if(conns == 0 && !isOpennetEnabled) {
				return l10n("noConns");
			} else if(conns == 1 && !isOpennetEnabled) {
				return l10n("oneConn");
			} else if(conns == 2 && !isOpennetEnabled) {
				return l10n("twoConns");
			} else if(bwlimitDelayAlertRelevant && (bwlimitDelayTime > NodeStats.MAX_BWLIMIT_DELAY_TIME_ALERT_THRESHOLD)) {
				s = l10n("tooHighBwlimitDelayTime", new String[] { "delay", "max" },
						new String[] { Integer.toString(bwlimitDelayTime), Long.toString(NodeStats.MAX_BWLIMIT_DELAY_TIME_ALERT_THRESHOLD)});
				// FIXME I'm not convinced about the next one!
			} else if(nodeAveragePingAlertRelevant && (nodeAveragePingTime > NodeStats.MAX_NODE_AVERAGE_PING_TIME_ALERT_THRESHOLD)) {
				s = l10n("tooHighPingTime", new String[] { "ping", "max" },
						new String[] { Integer.toString(nodeAveragePingTime), Long.toString(NodeStats.MAX_NODE_AVERAGE_PING_TIME_ALERT_THRESHOLD) });
			} else if(clockProblem > MIN_CLOCK_PROBLEM_PEER_ALERT_THRESHOLD) {
				s = l10n("clockProblem", "count", Integer.toString(clockProblem));
			} else if(neverConn > MAX_NEVER_CONNECTED_PEER_ALERT_THRESHOLD) {
				s = l10n("tooManyNeverConnected", "count", Integer.toString(neverConn));
			} else if(connError > MIN_CONN_ERROR_ALERT_THRESHOLD) {
				s = l10n("connError", "count", Integer.toString(connError));
			} else if(disconnDarknetPeers > MAX_DISCONN_PEER_ALERT_THRESHOLD && !darknetDefinitelyPortForwarded && !darknetAssumeNAT){
				s = l10n("tooManyDisconnected", new String[] { "count", "max" }, 
						new String[] { Integer.toString(disconnDarknetPeers), Integer.toString(MAX_DISCONN_PEER_ALERT_THRESHOLD)});
			} else if(darknetConns > MAX_DARKNET_CONN_ALERT_THRESHOLD) {
				s = l10n("tooManyConns", new String[] { "count", "max" }, 
						new String[] { Integer.toString(conns), Integer.toString(MAX_DARKNET_CONN_ALERT_THRESHOLD)});
			} else if(oldestNeverConnectedPeerAge > MAX_OLDEST_NEVER_CONNECTED_PEER_AGE_ALERT_THRESHOLD) {
				return l10n("tooOldNeverConnectedPeers");
			} else throw new IllegalArgumentException("Not valid");
			return s;
		}
	}

	static public String replace(String text, String find, String replace) {
		return replaceCareful(text, find, replace);
	}
	
	static public String replaceAll(String text, String find, String replace) {
		int i;
		while((i = text.indexOf(find)) >= 0) {
			text = text.substring(0, i) + replace + text.substring(i + find.length());
		}
		return text;
	}

	static public String replaceCareful(String text, String find, String replace) {
		String[] split = text.split(find, -1);
		StringBuilder sb = new StringBuilder(text.length() + (split.length-1)*(replace.length() - find.length()));
		for(int i=0;i<split.length;i++) {
			sb.append(split[i]);
			if(i < split.length - 1)
				sb.append(replace);
		}
		return sb.toString();
	}
	
	@Override
	public HTMLNode getHTMLText() {
		HTMLNode alertNode = new HTMLNode("div");
		
		synchronized(this) {
			if(isOutdated)
				// Arguably we should provide a button to turn on auto-update,
				// but very few users will turn off auto-update completely.
				// This is useful to not lose those who do however. 
				alertNode.addChild("#", l10n("outdatedUpdate"));
			else if (peers == 0 && !isOpennetEnabled) {
				alertNode.addChild("#", l10n("noPeersDarknet")); 
			} else if(conns < 3 && clockProblem > MIN_CLOCK_PROBLEM_PEER_ALERT_THRESHOLD) {
				alertNode.addChild("#", l10n("clockProblem", "count", Integer.toString(clockProblem)));
			} else if(conns < 3 && connError > MIN_CONN_ERROR_ALERT_THRESHOLD) {
				alertNode.addChild("#", l10n("connError", "count", Integer.toString(connError)));
			} else if (conns == 0 && !isOpennetEnabled) {
				alertNode.addChild("#", l10n("noConns"));
			} else if (conns == 1 && !isOpennetEnabled) {
				alertNode.addChild("#", l10n("oneConn"));
			} else if (conns == 2 && !isOpennetEnabled) {
				alertNode.addChild("#", l10n("twoConns"));
			} else if (bwlimitDelayAlertRelevant && (bwlimitDelayTime > NodeStats.MAX_BWLIMIT_DELAY_TIME_ALERT_THRESHOLD)) {
				alertNode.addChild("#", l10n("tooHighBwlimitDelayTime", new String[] { "delay", "max" },
						new String[] { Integer.toString(bwlimitDelayTime), Long.toString(NodeStats.MAX_BWLIMIT_DELAY_TIME_ALERT_THRESHOLD)}));
			} else if (nodeAveragePingAlertRelevant && (nodeAveragePingTime > NodeStats.MAX_NODE_AVERAGE_PING_TIME_ALERT_THRESHOLD)) {
				alertNode.addChild("#", l10n("tooHighPingTime", new String[] { "ping", "max" },
						new String[] { Integer.toString(nodeAveragePingTime), Long.toString(NodeStats.MAX_NODE_AVERAGE_PING_TIME_ALERT_THRESHOLD) }));
			} else if (clockProblem > MIN_CLOCK_PROBLEM_PEER_ALERT_THRESHOLD) {
				alertNode.addChild("#", l10n("clockProblem", "count", Integer.toString(clockProblem)));
			} else if (neverConn > MAX_NEVER_CONNECTED_PEER_ALERT_THRESHOLD) {
				NodeL10n.getBase().addL10nSubstitution(alertNode, "PeerManagerUserAlert.tooManyNeverConnectedWithLink",
						new String[] { "link", "count" },
						new HTMLNode[] { HTMLNode.link("/friends/myref.fref"), HTMLNode.text(neverConn) });
			} else if(connError > MIN_CONN_ERROR_ALERT_THRESHOLD) {
				alertNode.addChild("#", l10n("connError", "count", Integer.toString(connError)));
			} else if (disconnDarknetPeers > MAX_DISCONN_PEER_ALERT_THRESHOLD && !darknetDefinitelyPortForwarded && !darknetAssumeNAT) {
				alertNode.addChild("#", l10n("tooManyDisconnected", new String[] { "count", "max" }, new String[] { Integer.toString(disconnDarknetPeers), Integer.toString(MAX_DISCONN_PEER_ALERT_THRESHOLD)}));
			} else if (darknetConns > MAX_DARKNET_CONN_ALERT_THRESHOLD) {
				alertNode.addChild("#", l10n("tooManyConns", new String[] { "count", "max" }, 
						new String[] { Integer.toString(conns), Integer.toString(MAX_DARKNET_CONN_ALERT_THRESHOLD)}));
			} else if (oldestNeverConnectedPeerAge > MAX_OLDEST_NEVER_CONNECTED_PEER_AGE_ALERT_THRESHOLD) {
				alertNode.addChild("#", l10n("tooOldNeverConnectedPeers"));
			} else throw new IllegalArgumentException("not valid");
		}
		return alertNode;
	}

	private boolean calculateIsOutdated() {
		// Do not show the message if updater is enabled.
		if(nodeUpdater.isEnabled()) return false;
		if(nodeUpdater.isBlown()) return false;
		synchronized(this) {
			if(tooNewPeersDarknet >= PeerManager.OUTDATED_MIN_TOO_NEW_DARKNET)
				return true;
			return conns < PeerManager.OUTDATED_MAX_CONNS && 
				tooNewPeersTotal >= PeerManager.OUTDATED_MIN_TOO_NEW_TOTAL;
		}
	}

	@Override
	public short getPriorityClass() {
		synchronized(this) {
			if(isOutdated) {
				if(conns == 0)
					return UserAlert.CRITICAL_ERROR;
				else
					return UserAlert.ERROR;
			}
			if(peers == 0 && !isOpennetEnabled)
				return UserAlert.CRITICAL_ERROR;
			if(conns == 0 && !isOpennetEnabled)
				return UserAlert.ERROR;
			if(conns < 3 && clockProblem > MIN_CLOCK_PROBLEM_PEER_ALERT_THRESHOLD)
				return ERROR;
			if(conns < 3 && connError > MIN_CONN_ERROR_ALERT_THRESHOLD)
				return ERROR;
			if(conns < 3 && !isOpennetEnabled)
				return ERROR;
			if(bwlimitDelayAlertRelevant && (bwlimitDelayTime > NodeStats.MAX_BWLIMIT_DELAY_TIME_ALERT_THRESHOLD))
				return ERROR;
			if(nodeAveragePingAlertRelevant && (nodeAveragePingTime > NodeStats.MAX_NODE_AVERAGE_PING_TIME_ALERT_THRESHOLD))
				return ERROR;
			if(clockProblem > MIN_CLOCK_PROBLEM_PEER_ALERT_THRESHOLD)
				return ERROR;
			if(neverConn > MAX_NEVER_CONNECTED_PEER_ALERT_THRESHOLD)
				return WARNING;
			if(connError > MIN_CONN_ERROR_ALERT_THRESHOLD)
				return WARNING;
			if(disconnDarknetPeers > MAX_DISCONN_PEER_ALERT_THRESHOLD && !darknetDefinitelyPortForwarded && !darknetAssumeNAT)
				return WARNING;
			if(darknetConns > MAX_DARKNET_CONN_ALERT_THRESHOLD)
				return WARNING;
			if(oldestNeverConnectedPeerAge > MAX_OLDEST_NEVER_CONNECTED_PEER_AGE_ALERT_THRESHOLD)
				return WARNING;
			return ERROR;
		}
	}

	@Override
	public boolean isValid() {
		// only update here so we don't get odd behavior with it fluctuating
		update();
		boolean ret;
		synchronized(this) {
			ret = ((peers == 0 && !isOpennetEnabled) ||
				(conns < 3 && !isOpennetEnabled) ||
				(neverConn > MAX_NEVER_CONNECTED_PEER_ALERT_THRESHOLD) ||
				(disconnDarknetPeers > MAX_DISCONN_PEER_ALERT_THRESHOLD && !darknetDefinitelyPortForwarded && !darknetAssumeNAT) ||
				(darknetConns > MAX_DARKNET_CONN_ALERT_THRESHOLD) ||
				(clockProblem > MIN_CLOCK_PROBLEM_PEER_ALERT_THRESHOLD) ||
				(connError > MIN_CONN_ERROR_ALERT_THRESHOLD) ||
				(bwlimitDelayAlertRelevant && (bwlimitDelayTime > NodeStats.MAX_BWLIMIT_DELAY_TIME_ALERT_THRESHOLD)) ||
				(nodeAveragePingAlertRelevant && (nodeAveragePingTime > NodeStats.MAX_NODE_AVERAGE_PING_TIME_ALERT_THRESHOLD)) ||
				(oldestNeverConnectedPeerAge > MAX_OLDEST_NEVER_CONNECTED_PEER_AGE_ALERT_THRESHOLD));
		}
		if(!ret)
			ret = isOutdated;
		return ret;
	}

	private void update() {
		bwlimitDelayTime = (int) n.getBwlimitDelayTime();
		nodeAveragePingTime = (int) n.getNodeAveragePingTime();
		oldestNeverConnectedPeerAge = (int) n.peers.getOldestNeverConnectedDarknetPeerAge();
		bwlimitDelayAlertRelevant = n.bwlimitDelayAlertRelevant;
		nodeAveragePingAlertRelevant = n.nodeAveragePingAlertRelevant;
		isOutdated = calculateIsOutdated();
		// FIXME move PeerManager.updatePMUserAlert here.
		// FIXME then make this subscribe to PeerManager's listener thingy.
	}
	
}
