package freenet.clients.http;

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

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.DecimalFormat;
import java.util.*;

import freenet.client.FetchException;
import freenet.client.HighLevelSimpleClient;
import freenet.clients.fcp.AddPeer;
import freenet.clients.http.complexhtmlnodes.PeerTrustInputForAddPeerBoxNode;
import freenet.clients.http.complexhtmlnodes.PeerVisibilityInputForAddPeerBoxNode;
import freenet.clients.http.geoip.IPConverter;
import freenet.clients.http.geoip.IPConverter.Country;
import freenet.config.ConfigException;
import freenet.io.comm.PeerParseException;
import freenet.io.comm.ReferenceSignatureVerificationException;
import freenet.io.xfer.PacketThrottle;
import freenet.keys.FreenetURI;
import freenet.l10n.NodeL10n;
import freenet.node.DarknetPeerNode;
import freenet.node.DarknetPeerNode.FRIEND_VISIBILITY;
import freenet.node.DarknetPeerNode.FRIEND_TRUST;
import freenet.node.FSParseException;
import freenet.node.Node;
import freenet.node.NodeClientCore;
import freenet.node.NodeFile;
import freenet.node.NodeStats;
import freenet.node.PeerManager;
import freenet.node.PeerNode;
import freenet.node.PeerNode.IncomingLoadSummaryStats;
import freenet.node.PeerNodeStatus;
import freenet.node.Version;
import freenet.support.Fields;
import freenet.support.HTMLNode;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.MultiValueTable;
import freenet.support.SimpleFieldSet;
import freenet.support.SizeUtil;
import freenet.support.TimeUtil;
import freenet.support.api.HTTPRequest;
import freenet.support.io.Closer;
import freenet.support.io.FileUtil;

/** Base class for DarknetConnectionsToadlet and OpennetConnectionsToadlet */
public abstract class ConnectionsToadlet extends Toadlet {
	protected class ComparatorByStatus implements Comparator<PeerNodeStatus> {		
		protected final String sortBy;
		protected final boolean reversed;
		
		ComparatorByStatus(String sortBy, boolean reversed) {
			this.sortBy = sortBy;
			this.reversed = reversed;
		}
		
		@Override
		public int compare(PeerNodeStatus firstNode, PeerNodeStatus secondNode) {
			int result = 0;
			boolean isSet = true;
			
			if(sortBy != null){
				result = customCompare(firstNode, secondNode, sortBy);
				isSet = (result != 0);
				
			}else
				isSet=false;
			
			if(!isSet){
				int statusDifference = firstNode.getStatusValue() - secondNode.getStatusValue();
				if (statusDifference != 0) 
					result = (statusDifference < 0 ? -1 : 1);
				else
					result = lastResortCompare(firstNode, secondNode);
			}

			if(result == 0){
				return 0;
			}else if(reversed){
				isReversed = true;
				return result > 0 ? -1 : 1;
			}else{
				isReversed = false;
				return result < 0 ? -1 : 1;
			}
		}
		
		// xor: check why we do not just return the result of (long1-long2)
		// j16sdiz: (Long.MAX_VALUE - (-1) ) would overflow and become negative
		private int compareLongs(long long1, long long2) {
			int diff = Long.valueOf(long1).compareTo(long2);
			if(diff == 0)
				return 0;
			else
				return (diff > 0 ? 1 : -1);
		}
		
		private int compareInts(int int1, int int2) {
			int diff = Integer.valueOf(int1).compareTo(int2);
			if(diff == 0)
				return 0;
			else
				return (diff > 0 ? 1 : -1);
		}

		protected int customCompare(PeerNodeStatus firstNode, PeerNodeStatus secondNode, String sortBy2) {
			switch (sortBy) {
				case "address":
					return firstNode.getPeerAddress().compareToIgnoreCase(secondNode.getPeerAddress());
				case "location":
					return compareLocations(firstNode, secondNode);
				case "version":
					return Version.getArbitraryBuildNumber(firstNode.getVersion(), -1) - Version.getArbitraryBuildNumber(secondNode.getVersion(), -1);
				case "backoffRT":
					return Double.compare(firstNode.getBackedOffPercent(true), secondNode.getBackedOffPercent(true));
				case "backoffBulk":
					return Double.compare(firstNode.getBackedOffPercent(false), secondNode.getBackedOffPercent(false));
				case "overload_p":
					return Double.compare(firstNode.getPReject(), secondNode.getPReject());
				case "idle":
					return compareLongs(firstNode.getTimeLastConnectionCompleted(), secondNode.getTimeLastConnectionCompleted());
				case "time_routable":
					return Double.compare(firstNode.getPercentTimeRoutableConnection(), secondNode.getPercentTimeRoutableConnection());
				case "total_traffic": {
					long total1 = firstNode.getTotalInputBytes() + firstNode.getTotalOutputBytes();
					long total2 = secondNode.getTotalInputBytes() + secondNode.getTotalOutputBytes();
					return compareLongs(total1, total2);
				}
				case "total_traffic_since_startup": {
					long total1 = firstNode.getTotalInputSinceStartup() + firstNode.getTotalOutputSinceStartup();
					long total2 = secondNode.getTotalInputSinceStartup() + secondNode.getTotalOutputSinceStartup();
					return compareLongs(total1, total2);
				}
				case "selection_percentage":
					return Double.compare(firstNode.getSelectionRate(), secondNode.getSelectionRate());
				case "time_delta":
					return compareLongs(firstNode.getClockDelta(), secondNode.getClockDelta());
				case "uptime":
					return compareInts(firstNode.getReportedUptimePercentage(), secondNode.getReportedUptimePercentage());
				default:
					return 0;
			}
		}

		private int compareLocations(PeerNodeStatus firstNode, PeerNodeStatus secondNode) {
			double diff = firstNode.getLocation() - secondNode.getLocation(); // Can occasionally be the same, and we must have a consistent sort order
			if(Double.MIN_VALUE*2 > Math.abs(diff)) return 0;
			return diff > 0 ? 1 : -1;
		}

		/** Default comparison, after taking into account status */
		protected int lastResortCompare(PeerNodeStatus firstNode, PeerNodeStatus secondNode) {
			return compareLocations(firstNode, secondNode);
		}
	}

	protected final Node node;
	protected final NodeClientCore core;
	protected final NodeStats stats;
	protected final PeerManager peers;
	protected boolean isReversed = false;
	protected boolean showTrivialFoafConnections = false;

	public enum PeerAdditionReturnCodes{ OK, WRONG_ENCODING, CANT_PARSE, INTERNAL_ERROR, INVALID_SIGNATURE, TRY_TO_ADD_SELF, ALREADY_IN_REFERENCE}

	protected ConnectionsToadlet(Node n, NodeClientCore core, HighLevelSimpleClient client) {
		super(client);
		this.node = n;
		this.core = core;
		this.stats = n.getNodeStats();
		this.peers = n.getPeers();
	    REF_LINK = HTMLNode.link(path()+"myref.fref").setReadOnly();
	    REFTEXT_LINK = HTMLNode.link(path()+"myref.txt").setReadOnly();
	}

	abstract SimpleColumn[] endColumnHeaders(boolean advancedModeEnabled);
	
	abstract class SimpleColumn {
		abstract protected void drawColumn(HTMLNode peerRow, PeerNodeStatus peerNodeStatus);
		abstract public String getSortString();
		abstract public String getTitleKey();
		abstract public String getExplanationKey();
	}

	public void handleMethodGET(URI uri, final HTTPRequest request, ToadletContext ctx) throws ToadletContextClosedException, IOException, RedirectException {
        if(!ctx.checkFullAccess(this))
            return;

	    String path = uri.getPath();
		if(path.endsWith("myref.fref")) {
			SimpleFieldSet fs = getNoderef();
			String noderefString = fs.toOrderedStringWithBase64();
			MultiValueTable<String, String> extraHeaders = MultiValueTable.from(
				// Force download to disk
				"Content-Disposition", "attachment; filename=myref.fref"
			);
			writeReply(ctx, 200, "application/x-freenet-reference", "OK", extraHeaders, noderefString);
			return;
		}

		if(path.endsWith("myref.txt")) {
			SimpleFieldSet fs = getNoderef();
            String noderefString = fs.toOrderedStringWithBase64();
			writeTextReply(ctx, 200, "OK", noderefString);
			return;
		}
		
		final DecimalFormat fix1 = new DecimalFormat("##0.0%");
				
		final boolean fProxyJavascriptEnabled = node.isFProxyJavascriptEnabled();
		boolean drawMessageTypes = path.endsWith("displaymessagetypes.html");
		
		/* gather connection statistics */
		PeerNodeStatus[] peerNodeStatuses = getPeerNodeStatuses(!drawMessageTypes);
		Arrays.sort(peerNodeStatuses, comparator(request.getParam("sortBy", null), request.isParameterSet("reversed")));
		
		int numberOfConnected = PeerNodeStatus.getPeerStatusCount(peerNodeStatuses, PeerManager.PEER_NODE_STATUS_CONNECTED);
		int numberOfRoutingBackedOff = PeerNodeStatus.getPeerStatusCount(peerNodeStatuses, PeerManager.PEER_NODE_STATUS_ROUTING_BACKED_OFF);
		int numberOfTooNew = PeerNodeStatus.getPeerStatusCount(peerNodeStatuses, PeerManager.PEER_NODE_STATUS_TOO_NEW);
		int numberOfTooOld = PeerNodeStatus.getPeerStatusCount(peerNodeStatuses, PeerManager.PEER_NODE_STATUS_TOO_OLD);
		int numberOfDisconnected = PeerNodeStatus.getPeerStatusCount(peerNodeStatuses, PeerManager.PEER_NODE_STATUS_DISCONNECTED);
		int numberOfNeverConnected = PeerNodeStatus.getPeerStatusCount(peerNodeStatuses, PeerManager.PEER_NODE_STATUS_NEVER_CONNECTED);
		int numberOfDisabled = PeerNodeStatus.getPeerStatusCount(peerNodeStatuses, PeerManager.PEER_NODE_STATUS_DISABLED);
		int numberOfBursting = PeerNodeStatus.getPeerStatusCount(peerNodeStatuses, PeerManager.PEER_NODE_STATUS_BURSTING);
		int numberOfListening = PeerNodeStatus.getPeerStatusCount(peerNodeStatuses, PeerManager.PEER_NODE_STATUS_LISTENING);
		int numberOfListenOnly = PeerNodeStatus.getPeerStatusCount(peerNodeStatuses, PeerManager.PEER_NODE_STATUS_LISTEN_ONLY);
		int numberOfClockProblem = PeerNodeStatus.getPeerStatusCount(peerNodeStatuses, PeerManager.PEER_NODE_STATUS_CLOCK_PROBLEM);
		int numberOfConnError = PeerNodeStatus.getPeerStatusCount(peerNodeStatuses, PeerManager.PEER_NODE_STATUS_CONN_ERROR);
		int numberOfDisconnecting = PeerNodeStatus.getPeerStatusCount(peerNodeStatuses, PeerManager.PEER_NODE_STATUS_DISCONNECTING);
		int numberOfRoutingDisabled = PeerNodeStatus.getPeerStatusCount(peerNodeStatuses, PeerManager.PEER_NODE_STATUS_ROUTING_DISABLED);
		int numberOfNoLoadStats = PeerNodeStatus.getPeerStatusCount(peerNodeStatuses, PeerManager.PEER_NODE_STATUS_NO_LOAD_STATS);
		
		int numberOfSimpleConnected = numberOfConnected + numberOfRoutingBackedOff;
		int numberOfNotConnected = numberOfTooNew + numberOfTooOld +  numberOfNoLoadStats + numberOfDisconnected + numberOfNeverConnected + numberOfDisabled + numberOfBursting + numberOfListening + numberOfListenOnly + numberOfClockProblem + numberOfConnError;
		String titleCountString = null;
		if(node.isAdvancedModeEnabled()) {
			titleCountString = "(" + numberOfConnected + '/' + numberOfRoutingBackedOff + '/' + numberOfTooNew + '/' + numberOfTooOld + '/' + numberOfNoLoadStats + '/' + numberOfRoutingDisabled + '/' + numberOfNotConnected + ')';
		} else {
			titleCountString = (numberOfNotConnected + numberOfSimpleConnected)>0 ? String.valueOf(numberOfSimpleConnected) : "";
		}

		PageNode page = ctx.getPageMaker().getPageNode(getPageTitle(titleCountString), ctx);
		final boolean advancedMode = ctx.isAdvancedModeEnabled();
		HTMLNode pageNode = page.outer;
		HTMLNode contentNode = page.content;
		
		// FIXME! We need some nice images
		long now = System.currentTimeMillis();
	
		if(ctx.isAllowedFullAccess())
			contentNode.addChild(ctx.getAlertManager().createSummary());
		
		if(peerNodeStatuses.length>0){
			
			if(advancedMode) {

				/* node status values */
				long nodeUptimeSeconds = SECONDS.convert(now - node.getStartupTime(), MILLISECONDS);
				int bwlimitDelayTime = (int) stats.getBwlimitDelayTime();
				int nodeAveragePingTime = (int) stats.getNodeAveragePingTime();
				int networkSizeEstimateSession = stats.getDarknetSizeEstimate(-1);
				int networkSizeEstimateRecent = 0;
				if(nodeUptimeSeconds > HOURS.toSeconds(48)) {
					networkSizeEstimateRecent = stats.getDarknetSizeEstimate(now - HOURS.toMillis(48));
				}
				DecimalFormat fix4 = new DecimalFormat("0.0000");
				double routingMissDistanceLocal =  stats.routingMissDistanceLocal.currentValue();
				double routingMissDistanceRemote =  stats.routingMissDistanceRemote.currentValue();
				double routingMissDistanceOverall =  stats.routingMissDistanceOverall.currentValue();
				double routingMissDistanceBulk =  stats.routingMissDistanceBulk.currentValue();
				double routingMissDistanceRT =  stats.routingMissDistanceRT.currentValue();
				double backedOffPercent =  stats.backedOffPercent.currentValue();
				String nodeUptimeString = TimeUtil.formatTime(MILLISECONDS.convert(nodeUptimeSeconds, SECONDS));

				// BEGIN OVERVIEW TABLE
				HTMLNode overviewTable = contentNode.addChild("table", "class", "column");
				HTMLNode overviewTableRow = overviewTable.addChild("tr");
				HTMLNode nextTableCell = overviewTableRow.addChild("td", "class", "first");
				
				HTMLNode overviewInfobox = nextTableCell.addChild("div", "class", "infobox");
				overviewInfobox.addChild("div", "class", "infobox-header", "Node status overview");
				HTMLNode overviewInfoboxContent = overviewInfobox.addChild("div", "class", "infobox-content");
				HTMLNode overviewList = overviewInfoboxContent.addChild("ul");
				overviewList.addChild("li", "bwlimitDelayTime:\u00a0" + bwlimitDelayTime + "ms");
				overviewList.addChild("li", "nodeAveragePingTime:\u00a0" + nodeAveragePingTime + "ms");
				overviewList.addChild("li", "darknetSizeEstimateSession:\u00a0" + networkSizeEstimateSession + "\u00a0nodes");
				if(nodeUptimeSeconds > HOURS.toSeconds(48)) {
					overviewList.addChild("li", "darknetSizeEstimateRecent:\u00a0" + networkSizeEstimateRecent + "\u00a0nodes");
				}
				overviewList.addChild("li", "nodeUptime:\u00a0" + nodeUptimeString);
				overviewList.addChild("li", "routingMissDistanceLocal:\u00a0" + fix4.format(routingMissDistanceLocal));
				overviewList.addChild("li", "routingMissDistanceRemote:\u00a0" + fix4.format(routingMissDistanceRemote));
				overviewList.addChild("li", "routingMissDistanceOverall:\u00a0" + fix4.format(routingMissDistanceOverall));
				overviewList.addChild("li", "routingMissDistanceBulk:\u00a0" + fix4.format(routingMissDistanceBulk));
				overviewList.addChild("li", "routingMissDistanceRT:\u00a0" + fix4.format(routingMissDistanceRT));
				overviewList.addChild("li", "backedOffPercent:\u00a0" + fix1.format(backedOffPercent));
				overviewList.addChild("li", "pInstantReject:\u00a0" + fix1.format(stats.pRejectIncomingInstantly()));
				nextTableCell = overviewTableRow.addChild("td");
				
				// Activity box
				int numARKFetchers = node.getNumARKFetchers();
				
				HTMLNode activityInfobox = nextTableCell.addChild("div", "class", "infobox");
				activityInfobox.addChild("div", "class", "infobox-header", l10n("activityTitle"));
				HTMLNode activityInfoboxContent = activityInfobox.addChild("div", "class", "infobox-content");
				HTMLNode activityList = StatisticsToadlet.drawActivity(activityInfoboxContent, node);
				if (advancedMode && (activityList != null)) {
					if (numARKFetchers > 0) {
						activityList.addChild("li", "ARK\u00a0Fetch\u00a0Requests:\u00a0" + numARKFetchers);
					}
					StatisticsToadlet.drawBandwidth(activityList, node, nodeUptimeSeconds, advancedMode);
				}
				
				nextTableCell = overviewTableRow.addChild("td", "class", "last");
				
				// Peer statistics box
				HTMLNode peerStatsInfobox = nextTableCell.addChild("div", "class", "infobox");
				StatisticsToadlet.drawPeerStatsBox(peerStatsInfobox, advancedMode, numberOfConnected, numberOfRoutingBackedOff, numberOfTooNew, numberOfTooOld, numberOfDisconnected, numberOfNeverConnected, numberOfDisabled, numberOfBursting, numberOfListening, numberOfListenOnly, 0, 0, numberOfRoutingDisabled, numberOfClockProblem, numberOfConnError, numberOfDisconnecting, numberOfNoLoadStats, node);
				
				// Peer routing backoff reason box
				if(advancedMode) {
					HTMLNode backoffReasonInfobox = nextTableCell.addChild("div", "class", "infobox");
					HTMLNode title = backoffReasonInfobox.addChild("div", "class", "infobox-header", "Peer backoff reasons (realtime)");
					HTMLNode backoffReasonContent = backoffReasonInfobox.addChild("div", "class", "infobox-content");
					String [] routingBackoffReasons = peers.getPeerNodeRoutingBackoffReasons(true);
					int total = 0;
					if(routingBackoffReasons.length == 0) {
						backoffReasonContent.addChild("#", NodeL10n.getBase().getString("StatisticsToadlet.notBackedOff"));
					} else {
						HTMLNode reasonList = backoffReasonContent.addChild("ul");
						for(String routingBackoffReason: routingBackoffReasons) {
							int reasonCount = peers.getPeerNodeRoutingBackoffReasonSize(routingBackoffReason, true);
							if(reasonCount > 0) {
								total += reasonCount;
								reasonList.addChild("li", routingBackoffReason + '\u00a0' + reasonCount);
							}
						}
					}
					if(total > 0)
						title.addChild("#", ": "+total);
					backoffReasonInfobox = nextTableCell.addChild("div", "class", "infobox");
					title = backoffReasonInfobox.addChild("div", "class", "infobox-header", "Peer backoff reasons (bulk)");
					backoffReasonContent = backoffReasonInfobox.addChild("div", "class", "infobox-content");
					routingBackoffReasons = peers.getPeerNodeRoutingBackoffReasons(false);
					total = 0;
					if(routingBackoffReasons.length == 0) {
						backoffReasonContent.addChild("#", NodeL10n.getBase().getString("StatisticsToadlet.notBackedOff"));
					} else {
						HTMLNode reasonList = backoffReasonContent.addChild("ul");
						for(String routingBackoffReason: routingBackoffReasons) {
							int reasonCount = peers.getPeerNodeRoutingBackoffReasonSize(routingBackoffReason, false);
							if(reasonCount > 0) {
								total += reasonCount;
								reasonList.addChild("li", routingBackoffReason + '\u00a0' + reasonCount);
							}
						}
					}
					if(total > 0)
						title.addChild("#", ": "+total);
				}
				// END OVERVIEW TABLE
			}
			
			boolean enablePeerActions = showPeerActionsBox();
			
			// BEGIN PEER TABLE
			if(fProxyJavascriptEnabled) {
				String js =
						"  function peerNoteChange() {\n" +
						"    document.getElementById(\"action\").value = \"update_notes\";" +
						"    document.getElementById(\"peersForm\").doAction.click();\n" +
						"  }\n";
				contentNode
						.addChild("script", "type", "text/javascript")
						.addChild("%", js);
				contentNode.addChild("script",
								new String[] {"type", "src"},
								new String[] {"text/javascript",  "/static/js/checkall.js"});
			}
			HTMLNode peerTableInfobox = contentNode.addChild("div", "class", "infobox infobox-normal");
			HTMLNode peerTableInfoboxHeader = peerTableInfobox.addChild("div", "class", "infobox-header");
			peerTableInfoboxHeader.addChild("#", getPeerListTitle());
			if (advancedMode) {
				if (!path.endsWith("displaymessagetypes.html")) {
					peerTableInfoboxHeader.addChild("#", " ");
					peerTableInfoboxHeader.addChild("a", "href", "displaymessagetypes.html", l10n("bracketedMoreDetailed"));
				}
			}
			HTMLNode peerTableInfoboxContent = peerTableInfobox.addChild("div", "class", "infobox-content");

			if (!isOpennet()) {
				HTMLNode myName = peerTableInfoboxContent.addChild("p");
				myName.addChild("span",
						NodeL10n.getBase().getString("DarknetConnectionsToadlet.myName", "name", node.getMyName()));
				myName.addChild("span", " [");
				myName.addChild("span").addChild("a", "href", "/config/node#name",
						NodeL10n.getBase().getString("DarknetConnectionsToadlet.changeMyName"));
				myName.addChild("span", "]");
			}

			if (peerNodeStatuses.length == 0) {
				NodeL10n.getBase().addL10nSubstitution(peerTableInfoboxContent, "DarknetConnectionsToadlet.noPeersWithHomepageLink", 
						new String[] { "link" }, new HTMLNode[] { HTMLNode.link("/") });
			} else {
				HTMLNode peerForm = null;
				HTMLNode peerTable;
				if(enablePeerActions) {
					peerForm = ctx.addFormChild(peerTableInfoboxContent, ".", "peersForm");
					peerTable = peerForm.addChild("table", "class", "darknet_connections");
				} else {
					peerTable = peerTableInfoboxContent.addChild("table", "class", "darknet_connections");
				}
				HTMLNode peerTableHeaderRow = peerTable.addChild("tr");
				if(enablePeerActions) {
					if(fProxyJavascriptEnabled) {
						peerTableHeaderRow.addChild("th").addChild("input", new String[] { "type", "onclick" }, new String[] { "checkbox", "checkAll(this, 'darknet_connections')" });
					} else {
						peerTableHeaderRow.addChild("th");
					}
				}
				peerTableHeaderRow.addChild("th").addChild("a", "href", sortString(isReversed, "status")).addChild("#", l10n("statusTitle"));
				if(hasNameColumn())
					peerTableHeaderRow.addChild("th").addChild("a", "href", sortString(isReversed, "name")).addChild("span", new String[] { "title", "style" }, new String[] { l10n("nameClickToMessage"), "border-bottom: 1px dotted; cursor: help;" }, l10n("nameTitle"));
				if(hasTrustColumn())
					peerTableHeaderRow.addChild("th").addChild("a", "href", sortString(isReversed, "trust")).addChild("span", new String[] { "title", "style" }, new String[] { l10n("trustMessage"), "border-bottom: 1px dotted; cursor: help;" }, l10n("trustTitle"));
				if(hasVisibilityColumn())
					peerTableHeaderRow.addChild("th").addChild("a", "href", sortString(isReversed, "trust")).addChild("span", new String[] { "title", "style" }, new String[] { l10n("visibilityMessage"+(advancedMode?"Advanced":"Simple")), "border-bottom: 1px dotted; cursor: help;" }, l10n("visibilityTitle"));
				peerTableHeaderRow.addChild("th").addChild("a", "href", sortString(isReversed, "address")).addChild("span", new String[] { "title", "style" }, new String[] { l10n("ipAddress"), "border-bottom: 1px dotted; cursor: help;" }, l10n("ipAddressTitle"));
				peerTableHeaderRow.addChild("th").addChild("a", "href", sortString(isReversed, "version")).addChild("#", l10n("versionTitle"));
				if (advancedMode) {
					peerTableHeaderRow.addChild("th").addChild("a", "href", sortString(isReversed, "location")).addChild("#", l10n("locationTitle"));
					peerTableHeaderRow.addChild("th").addChild("a", "href", sortString(isReversed, "backoffRT")).addChild("span", new String[] { "title", "style" }, new String[] { "Other node busy (realtime)? Display: Percentage of time the node is overloaded, Current wait time remaining (0=not overloaded)/total/last overload reason", "border-bottom: 1px dotted; cursor: help;" }, "Backoff (realtime)");
					peerTableHeaderRow.addChild("th").addChild("a", "href", sortString(isReversed, "backoffBulk")).addChild("span", new String[] { "title", "style" }, new String[] { "Other node busy (bulk)? Display: Percentage of time the node is overloaded, Current wait time remaining (0=not overloaded)/total/last overload reason", "border-bottom: 1px dotted; cursor: help;" }, "Backoff (bulk)");

					peerTableHeaderRow.addChild("th").addChild("a", "href", sortString(isReversed, "overload_p")).addChild("span", new String[] { "title", "style" }, new String[] { "Probability of the node rejecting a request due to overload or causing a timeout.", "border-bottom: 1px dotted; cursor: help;" }, "Overload Probability");
				}
				peerTableHeaderRow.addChild("th").addChild("a", "href", sortString(isReversed, "idle")).addChild("span", new String[] { "title", "style" }, new String[] { l10n("idleTime"), "border-bottom: 1px dotted; cursor: help;" }, l10n("idleTimeTitle"));
				if(hasPrivateNoteColumn())
					peerTableHeaderRow.addChild("th").addChild("a", "href", sortString(isReversed, "privnote")).addChild("span", new String[] { "title", "style" }, new String[] { l10n("privateNote"), "border-bottom: 1px dotted; cursor: help;" }, l10n("privateNoteTitle"));
 
				if(advancedMode) {
					peerTableHeaderRow.addChild("th").addChild("a", "href", sortString(isReversed, "time_routable")).addChild("#", "%\u00a0Time Routable");
					peerTableHeaderRow.addChild("th").addChild("a", "href", sortString(isReversed, "selection_percentage")).addChild("#", "%\u00a0Selection");
					peerTableHeaderRow.addChild("th").addChild("a", "href", sortString(isReversed, "total_traffic")).addChild("#", "Total\u00a0Traffic\u00a0(in/out/resent)");
					peerTableHeaderRow.addChild("th").addChild("a", "href", sortString(isReversed, "total_traffic_since_startup")).addChild("#", "Total\u00a0Traffic\u00a0(in/out) since startup");
					peerTableHeaderRow.addChild("th", "Congestion\u00a0Control");
					peerTableHeaderRow.addChild("th").addChild("a", "href", sortString(isReversed, "time_delta")).addChild("#", "Time\u00a0Delta");
					peerTableHeaderRow.addChild("th").addChild("a", "href", sortString(isReversed, "uptime")).addChild("#", "Reported\u00a0Uptime");
					peerTableHeaderRow.addChild("th", "Transmit\u00a0Queue");
					peerTableHeaderRow.addChild("th", "Peer\u00a0Capacity\u00a0Bulk");
					peerTableHeaderRow.addChild("th", "Peer\u00a0Capacity\u00a0Realtime");
				}
				
				SimpleColumn[] endCols = endColumnHeaders(advancedMode);
				if(endCols != null) {
					for(SimpleColumn col: endCols) {
						HTMLNode header = peerTableHeaderRow.addChild("th");
						String sortString = col.getSortString();
						if(sortString != null)
							header = header.addChild("a", "href", sortString(isReversed, sortString));
						header.addChild("span", new String[] { "title", "style" }, new String[] { NodeL10n.getBase().getString(col.getExplanationKey()), "border-bottom: 1px dotted; cursor: help;" }, NodeL10n.getBase().getString(col.getTitleKey()));
					}
				}

				double totalSelectionRate = 0.0;
				//calculate the total selection rate using all peers, not just the peers for the current mode,
				PeerNodeStatus[] allPeerNodeStatuses = node.getPeers().getPeerNodeStatuses(true);
				for(PeerNodeStatus status : allPeerNodeStatuses) {
					totalSelectionRate += status.getSelectionRate();
				}
				for (PeerNodeStatus peerNodeStatus: peerNodeStatuses) {
					drawRow(peerTable, peerNodeStatus, advancedMode, fProxyJavascriptEnabled, now, path, enablePeerActions, endCols, drawMessageTypes, totalSelectionRate, fix1);
				}

				if(peerForm != null) {
					drawPeerActionSelectBox(peerForm, advancedMode);
				}
			}
			// END PEER TABLE

			// FOAF locations table.
			if(advancedMode) {
				//requires a location-to-list/count in-memory transform
				List<Double> locations=new ArrayList<Double>();
				List<List<PeerNodeStatus>> peerGroups=new ArrayList<List<PeerNodeStatus>>();
				{
					for (PeerNodeStatus peerNodeStatus : peerNodeStatuses) {
						double[] peersLoc = peerNodeStatus.getPeersLocation();
						if (peersLoc!=null) {
							for (double location : peersLoc) {
								int i;
								int max=locations.size();
								// FIXME Fix O(n^2): Use Arrays.binarySearch or use a TreeMap.
								for (i=0; i<max && locations.get(i)<location; i++);
								//i now points to the proper location (equal, insertion point, or end-of-list)
								//maybe better called "reverseGroup"?
								List<PeerNodeStatus> peerGroup;
								if (i<max && locations.get(i) ==location) {
									peerGroup=peerGroups.get(i);
								} else {
									peerGroup=new ArrayList<PeerNodeStatus>();
									locations.add(i, location);
									peerGroups.add(i, peerGroup);
								}
								peerGroup.add(peerNodeStatus);
							}
						}
					}
				}
				//transform complete.... now we have peers listed by foaf's ordered by ascending location
				int trivialCount=0;
				int nonTrivialCount=0;
				int transitiveCount=0;
				for (List<PeerNodeStatus> list : peerGroups) {
					if (list.size()==1)
						trivialCount++;
					else
						nonTrivialCount++;
				}
				peerTableInfoboxContent.addChild("b", l10n("secondDegreeConnectionsCountTitle", "count", Integer.toString(locations.size())));
				peerTableInfoboxContent.addChild("br");
				if (!showTrivialFoafConnections) {
					peerTableInfoboxContent.addChild("i", l10n("secondDegreeTrivialHiddenCount", "count", Integer.toString(trivialCount)));
					//@todo: add "show these" link
				} else {
					peerTableInfoboxContent.addChild("i", l10n("secondDegreeNonTrivialCount", "count", Integer.toString(nonTrivialCount)));
					//@todo: add "hide these" link
				}
				HTMLNode foafTable = peerTableInfoboxContent.addChild("table", "class", "darknet_connections"); //@todo: change css class?
				HTMLNode foafRow = foafTable.addChild("tr");
				{
					foafRow.addChild("th", l10n("locationTitle"));
					foafRow.addChild("th", l10n("countTitle"));
					foafRow.addChild("th", l10n("foafReachableThroughTitle"));
				}
				int max=locations.size();
				for (int i=0; i<max; i++) {
					double location=locations.get(i);
					List<PeerNodeStatus> peersWithFriend=peerGroups.get(i);
					boolean isTransitivePeer=false;
					{
						for (PeerNodeStatus peerNodeStatus : peerNodeStatuses) {
							if (location==peerNodeStatus.getLocation()) {
								isTransitivePeer=true;
								transitiveCount++;
								break;
							}
						}
					}
					if (peersWithFriend.size()==1 && !showTrivialFoafConnections && !isTransitivePeer)
						continue;
					foafRow=foafTable.addChild("tr");
					{
						if (isTransitivePeer) {
							foafRow.addChild("td").addChild("b", String.valueOf(location));
						} else {
							foafRow.addChild("td", String.valueOf(location));
						}
						foafRow.addChild("td", String.valueOf(peersWithFriend.size()));
						HTMLNode locationCell=foafRow.addChild("td", "class", "peer-location");
						for (PeerNodeStatus peerNodeStatus : peersWithFriend) {
							String address=((peerNodeStatus.getPeerAddress() != null) ? peerNodeStatus.getPeerAddressAndPort() : (l10n("unknownAddress")));
							locationCell.addChild("i", address);
							locationCell.addChild("br");
						}
					}
				}
				if (transitiveCount>0) {
					peerTableInfoboxContent.addChild("i", l10n("secondDegreeAlsoOurs", "count", Integer.toString(transitiveCount)));
				}
			}
			// END FOAF TABLE

		} else {
			if(!isOpennet()) {
				try {
					throw new RedirectException("/addfriend/");
				} catch (URISyntaxException e) {
					Logger.error(this, "Impossible: "+e+" for /addfriend/", e);
				}
			}
		}
		
		// our reference
		if(shouldDrawNoderefBox(advancedMode)) {
			drawAddPeerBox(contentNode, ctx);
			drawNoderefBox(contentNode, getNoderef());
		}
		
		this.writeHTMLReply(ctx, 200, "OK", pageNode.generate());
	}

	protected abstract boolean acceptRefPosts();
	
	/** Where to redirect to if there is an error */
	protected abstract String defaultRedirectLocation();

	public void handleMethodPOST(URI uri, final HTTPRequest request, ToadletContext ctx)
			throws ToadletContextClosedException, IOException, RedirectException, ConfigException {
		boolean logMINOR = Logger.shouldLog(LogLevel.MINOR, this);

		if(!acceptRefPosts()) {
		    sendUnauthorizedPage(ctx);
			return;
		}
		
        if(!ctx.checkFullAccess(this))
            return;
        
		if (request.isPartSet("add")) {
			// add a new node
			String urltext = request.getPartAsStringFailsafe("url", 200);
			urltext = urltext.trim();
			String reftext = request.getPartAsStringFailsafe("ref", Integer.MAX_VALUE);
			reftext = reftext.trim();
			if (reftext.length() < 200) {
				reftext = request.getPartAsStringFailsafe("reffile", Integer.MAX_VALUE);
				reftext = reftext.trim();
			}
			String privateComment = null;
			if(!isOpennet())
				privateComment = request.getPartAsStringFailsafe("peerPrivateNote", 250).trim();

			if (Boolean.parseBoolean(request.getPartAsStringFailsafe("peers-offers-files", 5))) {
				File[] files = core.getNode().runDir().file("peers-offers").listFiles();
				if (files != null && files.length > 0) {
					StringBuilder peersOffersFilesContent = new StringBuilder();
					for (final File file : files) {
						if (file.isFile()) {
							String filename = file.getName();
							if (filename.endsWith(".fref"))
								peersOffersFilesContent.append(FileUtil.readUTF(file));
						}
					}
					reftext = peersOffersFilesContent.toString();
				}

				node.getConfig().get("node").set("peersOffersDismissed", true);
			}
			
			String trustS = request.getPartAsStringFailsafe("trust", 10);
			FRIEND_TRUST trust = null;
			if(trustS != null && !trustS.isEmpty())
				trust = FRIEND_TRUST.valueOf(trustS);
			
			String visibilityS = request.getPartAsStringFailsafe("visibility", 10);
			FRIEND_VISIBILITY visibility = null;
			if(visibilityS != null && !visibilityS.isEmpty())
				visibility = FRIEND_VISIBILITY.valueOf(visibilityS);
			
			if(trust == null && !isOpennet()) {
				// FIXME: Layering violation. Ideally DarknetPeerNode would do this check.
				this.sendErrorPage(ctx, 200, l10n("noTrustLevelAddingFriendTitle"), l10n("noTrustLevelAddingFriend"), !isOpennet());
				return;
			}
			
			if(visibility == null && !isOpennet()) {
				// FIXME: Layering violation. Ideally DarknetPeerNode would do this check.
				this.sendErrorPage(ctx, 200, l10n("noVisibilityLevelAddingFriendTitle"), l10n("noVisibilityLevelAddingFriend"), !isOpennet());
				return;
			}
			
			StringBuilder ref = null;
			if (!urltext.isEmpty()) {
				// fetch reference from a URL
				BufferedReader in = null;
				try {
					try {
						FreenetURI refUri = new FreenetURI(urltext);
					  ref = AddPeer.getReferenceFromFreenetURI(refUri, client);
					} catch (MalformedURLException | FetchException e) {
						Logger.warning(this, "Url cannot be used as Freenet URI, trying to fetch as URL: " + urltext);
						URL url = new URL(urltext);
					  ref = AddPeer.getReferenceFromURL(url);
					}
				} catch (IOException e) {
					this.sendErrorPage(ctx, 200, l10n("failedToAddNodeTitle"), NodeL10n.getBase().getString("DarknetConnectionsToadlet.cantFetchNoderefURL", new String[] { "url" }, new String[] { urltext }), !isOpennet());
					return;
				} finally {
					Closer.close(in);
				}
			} else if (!reftext.isEmpty()) {
				// read from post data or file upload
				// this slightly scary looking regexp chops any extra characters off the beginning or ends of lines and removes extra line breaks
				ref = new StringBuilder(reftext.replaceAll(".*?((?:[\\w,\\.]+\\=[^\r\n]+?)|(?:End))[ \\t]*(?:\\r?\\n)+", "$1\n"));
			} else {
				this.sendErrorPage(ctx, 200, l10n("failedToAddNodeTitle"), l10n("noRefOrURL"), !isOpennet());
				request.freeParts();
				return;
			}
			ref = new StringBuilder(ref.toString().trim());

			request.freeParts();

			//Split the references string, because the peers are added individually
			// FIXME split by lines at this point rather than in addNewNode would be more efficient
			int idx;
			while((idx = ref.indexOf("\r\n")) > -1) {
				ref.deleteCharAt(idx);
			}
			while((idx = ref.indexOf("\r")) > -1) {
				// Mac's just use \r
				ref.setCharAt(idx, '\n');
			}
			String[] nodesToAdd=ref.toString().split("\nEnd\n");
			for(int i=0;i<nodesToAdd.length;i++) {
				String[] split = nodesToAdd[i].split("\n");
				StringBuffer sb = new StringBuffer(nodesToAdd[i].length());
				boolean first = true;
				for(String s : split) {
					if(s.equals("End")) break;
					if(s.indexOf('=') > -1) {
						if(!first)
							sb.append('\n');
					} else {
						// Try appending it - don't add a newline.
						// This will make broken refs work sometimes.
					}
					sb.append(s);
					first = false;
				}
				nodesToAdd[i] = sb.toString();
				// Don't need to add a newline at the end, we will do that later.
			}
			//The peer's additions results
			Map<PeerAdditionReturnCodes,Integer> results=new HashMap<PeerAdditionReturnCodes, Integer>();
			for(int i=0;i<nodesToAdd.length;i++){
				//We need to trim then concat 'End' to the node's reference, this way we have a normal reference(the split() removes the 'End'-s!)
				PeerAdditionReturnCodes result=addNewNode(nodesToAdd[i].trim().concat("\nEnd"), privateComment, trust, visibility);
				//Store the result
				Integer prev = results.get(result);
				if(prev == null) prev = 0;
				results.put(result, prev+1);
			}
			
			PageNode page = ctx.getPageMaker().getPageNode(l10n("reportOfNodeAddition"), ctx);
			HTMLNode pageNode = page.outer;
			HTMLNode contentNode = page.content;
			
			//We create a table to show the results
			HTMLNode detailedStatusBox=new HTMLNode("table");
			//Header of the table
			detailedStatusBox.addChild(new HTMLNode("tr")).addChildren(new HTMLNode[]{new HTMLNode("th",l10n("resultName")),new HTMLNode("th",l10n("numOfResults"))});
			HTMLNode statusBoxTable=detailedStatusBox.addChild(new HTMLNode("tbody"));
			//Iterate through the return codes
			for(PeerAdditionReturnCodes returnCode:PeerAdditionReturnCodes.values()){
				if(results.containsKey(returnCode)){
					//Add a <tr> and 2 <td> with the name of the code and the number of occasions it happened. If the code is OK, we use green, red elsewhere.
					statusBoxTable.addChild(new HTMLNode("tr","style","color:"+(returnCode==PeerAdditionReturnCodes.OK?"green":"red"))).addChildren(new HTMLNode[]{new HTMLNode("td",l10n("peerAdditionCode."+returnCode.toString())),new HTMLNode("td",results.get(returnCode).toString())});
				}
			}

			HTMLNode infoboxContent = ctx.getPageMaker().getInfobox("infobox",l10n("reportOfNodeAddition"), contentNode, "node-added", true);
			infoboxContent.addChild(detailedStatusBox);
			if(!isOpennet())
				infoboxContent.addChild("p").addChild("a", "href", "/addfriend/", l10n("addAnotherFriend"));
			infoboxContent.addChild("p").addChild("a", "href", path(), l10n("goFriendConnectionStatus"));
			addHomepageLink(infoboxContent.addChild("p"));
			
			writeHTMLReply(ctx, 500, l10n("reportOfNodeAddition"), pageNode.generate());
		} else handleAltPost(uri, request, ctx, logMINOR);
		
		
	}
	
	/** Adds a new node. If any error arises, it returns the appropriate return code.
	 * @param nodeReference - The reference to the new node
	 * @param privateComment - The private comment when adding a Darknet node
	 * @param trust 
	 * @param request To pull any extra fields from
	 * @return The result of the addition*/
	private PeerAdditionReturnCodes addNewNode(String nodeReference,String privateComment, FRIEND_TRUST trust, FRIEND_VISIBILITY visibility){
		SimpleFieldSet fs;
		
		try {
			fs = parseNoderefLiberally(nodeReference);
			if(!fs.getEndMarker().endsWith("End")) {
				Logger.error(this, "Trying to add noderef with end marker \""+fs.getEndMarker()+"\"");
				return PeerAdditionReturnCodes.WRONG_ENCODING;
			}
			fs.setEndMarker("End"); // It's always End ; the regex above doesn't always grok this
		} catch (IOException e) {
            Logger.error(this, "IOException adding reference :" + e.getMessage(), e);
			return PeerAdditionReturnCodes.CANT_PARSE;
		} catch (Throwable t) {
		    Logger.error(this, "Internal error adding reference :" + t.getMessage(), t);
			return PeerAdditionReturnCodes.INTERNAL_ERROR;
		}
		PeerNode pn;
		try {
			if(isOpennet()) {
				pn = node.createNewOpennetNode(fs);
			} else {
				pn = node.createNewDarknetNode(fs, trust, visibility);
				((DarknetPeerNode)pn).setPrivateDarknetCommentNote(privateComment);
			}
		} catch (FSParseException e1) {
			return PeerAdditionReturnCodes.CANT_PARSE;
		} catch (PeerParseException e1) {
			return PeerAdditionReturnCodes.CANT_PARSE;
		} catch (ReferenceSignatureVerificationException e1){
			return PeerAdditionReturnCodes.INVALID_SIGNATURE;
		} catch (Throwable t) {
            Logger.error(this, "Internal error adding reference :" + t.getMessage(), t);
			return PeerAdditionReturnCodes.INTERNAL_ERROR;
		}
		if(Arrays.equals(pn.peerECDSAPubKeyHash, node.getDarknetPubKeyHash())) {
			return PeerAdditionReturnCodes.TRY_TO_ADD_SELF;
		}
		if(!this.node.addPeerConnection(pn)) {
			return PeerAdditionReturnCodes.ALREADY_IN_REFERENCE;
		}
		return PeerAdditionReturnCodes.OK;
	}

	private static SimpleFieldSet parseNoderefLiberally(String nodeReference) throws IOException {
		nodeReference = Fields.trimLines(nodeReference);
		SimpleFieldSet fs = new SimpleFieldSet(nodeReference, false, true, true);
		if (fs.directKeys().contains("lastGoodVersion")) {
			return fs;
		} else {
			Logger.warning(null, "Cannot parse noderef: does not contain lastGoodVersion, trying to replace all spaces with newlines and parsing again.");
			return new SimpleFieldSet(nodeReference.replace(" ", "\n"), false, true, true);
		}
	}

	/** Adding a darknet node or an opennet node? */
	protected abstract boolean isOpennet();

	/**
	 * Rest of handlePost() method - supplied by subclass.
	 * @throws IOException 
	 * @throws ToadletContextClosedException 
	 * @throws RedirectException 
	 */
	protected void handleAltPost(URI uri, HTTPRequest request, ToadletContext ctx, boolean logMINOR) throws ToadletContextClosedException, IOException, RedirectException {
		// Do nothing - we only support adding nodes
		handleMethodGET(uri, new HTTPRequestImpl(uri, "GET"), ctx);
	}

	/**
	 * What should the heading (before "(more detailed)") be on the peers table?
	 */
	protected abstract String getPeerListTitle();

	/** Should there be a checkbox for each peer, and drawPeerActionSelectBox() be called directly
	 * after drawing the peers list? */
	protected abstract boolean showPeerActionsBox();

	/** If showPeerActionsBox() is true, this will be called directly after drawing the peers table.
	 * A form has been added, and checkboxes added for each peer. This function should draw the rest
	 * of the form - any additional controls and one or more submit buttons.
	 */
	protected abstract void drawPeerActionSelectBox(HTMLNode peerForm, boolean advancedModeEnabled);
	
	protected abstract boolean shouldDrawNoderefBox(boolean advancedModeEnabled);

	final HTMLNode REF_LINK;
	final HTMLNode REFTEXT_LINK;

	/**
	 * @param contentNode Node to add noderef box to.
	 * @param fs Noderef to render as text if requested.
	 */
	void drawNoderefBox(HTMLNode contentNode, SimpleFieldSet fs) {
		HTMLNode referenceInfobox = contentNode.addChild("div", "class", "infobox infobox-normal");
		HTMLNode headerReferenceInfobox = referenceInfobox.addChild("div", "class", "infobox-header");
		// FIXME better way to deal with this sort of thing???
		NodeL10n.getBase().addL10nSubstitution(headerReferenceInfobox, "DarknetConnectionsToadlet.myReferenceHeader",
				new String[] { "linkref", "linktext" },
				new HTMLNode[] { REF_LINK, REFTEXT_LINK });
		HTMLNode referenceInfoboxContent = referenceInfobox.addChild("div", "class", "infobox-content");
		
		if(!isOpennet()) {
			HTMLNode myName = referenceInfoboxContent.addChild("p");
			myName.addChild("span",
					NodeL10n.getBase().getString("DarknetConnectionsToadlet.myName", "name", fs.get("myName")));
			myName.addChild("span", " [");
			myName.addChild("span").addChild("a", "href", "/config/node#name",
					NodeL10n.getBase().getString("DarknetConnectionsToadlet.changeMyName"));
			myName.addChild("span", "]");
		}

		HTMLNode warningSentence = referenceInfoboxContent.addChild("p");
		NodeL10n.getBase().addL10nSubstitution(warningSentence, "DarknetConnectionsToadlet.referenceCopyWarning",
				new String[] { "bold" },
				new HTMLNode[] { HTMLNode.STRONG });
		referenceInfoboxContent.addChild("pre", "id", "reference", fs.toOrderedStringWithBase64() + '\n');

		if(!isOpennet()) {
			HTMLNode myIps = referenceInfoboxContent.addChild("p");
			myIps.addChild("span", NodeL10n.getBase().getString("DarknetConnectionsToadlet.myIps", "ips", fs.get("physical.udp")));
		}

	}

	protected abstract String getPageTitle(String titleCountString);

	/** Draw the add a peer box. This comes immediately after the main peers table and before the noderef box.
	 * Implementors may skip it by not doing anything in this method. */
	protected void drawAddPeerBox(HTMLNode contentNode, ToadletContext ctx) {
		drawAddPeerBox(contentNode, ctx, isOpennet(), path());
	}
	
	protected static void drawAddPeerBox(HTMLNode contentNode, ToadletContext ctx, boolean isOpennet, String formTarget) {
		// BEGIN PEER ADDITION BOX
		HTMLNode peerAdditionInfobox = contentNode.addChild("div", "class", "infobox infobox-normal");
		peerAdditionInfobox.addChild("div", "class", "infobox-header", l10n(isOpennet ? "addOpennetPeerTitle" : "addPeerTitle"));
		HTMLNode peerAdditionContent = peerAdditionInfobox.addChild("div", "class", "infobox-content");
		HTMLNode peerAdditionForm = ctx.addFormChild(peerAdditionContent, formTarget, "addPeerForm");
		peerAdditionForm.addChild("#", l10n("pasteReference"));
		peerAdditionForm.addChild("br");
		peerAdditionForm.addChild("textarea", new String[] { "id", "name", "rows", "cols" }, new String[] { "reftext", "ref", "8", "74" });
		peerAdditionForm.addChild("br");
		peerAdditionForm.addChild("#", (l10n("urlReference") + ' '));
		peerAdditionForm.addChild("input", new String[] { "id", "type", "name" }, new String[] { "refurl", "text", "url" });
		peerAdditionForm.addChild("br");
		peerAdditionForm.addChild("#", (l10n("fileReference") + ' '));
		peerAdditionForm.addChild("input", new String[] { "id", "type", "name" }, new String[] { "reffile", "file", "reffile" });
		peerAdditionForm.addChild("br");
		if(!isOpennet) {
			peerAdditionForm.addChild(new PeerTrustInputForAddPeerBoxNode());
			peerAdditionForm.addChild(new PeerVisibilityInputForAddPeerBoxNode());
		}
		
		if(!isOpennet) {
			peerAdditionForm.addChild("#", (l10n("enterDescription") + ' '));
			peerAdditionForm.addChild("input", new String[] { "id", "type", "name", "size", "maxlength", "value" }, new String[] { "peerPrivateNote", "text", "peerPrivateNote", "16", "250", "" });
			peerAdditionForm.addChild("br");
		}
		peerAdditionForm.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "add", l10n("add") });
	}

	protected Comparator<PeerNodeStatus> comparator(String sortBy, boolean reversed) {
		return new ComparatorByStatus(sortBy, reversed);
	}

	abstract protected PeerNodeStatus[] getPeerNodeStatuses(boolean noHeavy);

	abstract protected SimpleFieldSet getNoderef();

	private void drawRow(HTMLNode peerTable, PeerNodeStatus peerNodeStatus, boolean advancedModeEnabled, boolean fProxyJavascriptEnabled, long now, String path, boolean enablePeerActions, SimpleColumn[] endCols, boolean drawMessageTypes, double totalSelectionRate, DecimalFormat fix1) {
		double selectionRate = peerNodeStatus.getSelectionRate();
		int peerSelectionPercentage = 0;
		if(totalSelectionRate > 0)
			peerSelectionPercentage = (int) (selectionRate * 100 / totalSelectionRate);
		HTMLNode peerRow = peerTable.addChild("tr", "class", "darknet_connections_"+(peerSelectionPercentage > PeerNode.SELECTION_PERCENTAGE_WARNING ? "warning" : "normal"));
		
		if(enablePeerActions) {
			// check box column
			peerRow.addChild("td", "class", "peer-marker").addChild("input", new String[] { "type", "name" }, new String[] { "checkbox", "node_" + peerNodeStatus.hashCode() });
		}

		// status column
		String statusString = peerNodeStatus.getStatusName();
		if (!advancedModeEnabled && (peerNodeStatus.getStatusValue() == PeerManager.PEER_NODE_STATUS_ROUTING_BACKED_OFF)) {
			statusString = "BUSY";
		}
		/*
		 * Some status names have spaces, but a space separates key and value if not immediately followed by '='.
		 * Changing the names is not an option because they are exposed over FCP and TMCI.
		 */
		final String key = "ConnectionsToadlet.nodeStatus." + statusString.replace(' ', '_');
		peerRow.addChild("td", "class", "peer-status").addChild("span", "class", peerNodeStatus.getStatusCSSName(), NodeL10n.getBase().getString(key) + (peerNodeStatus.isFetchingARK() ? "*" : ""));

		drawNameColumn(peerRow, peerNodeStatus, advancedModeEnabled);
		
		drawTrustColumn(peerRow, peerNodeStatus);
		
		drawVisibilityColumn(peerRow, peerNodeStatus, advancedModeEnabled);
		
		// address column
		String pingTime = "";
		if (peerNodeStatus.isConnected()) {
			pingTime = " (" + (int) peerNodeStatus.getAveragePingTime() + "ms / " +
			(int) peerNodeStatus.getAveragePingTimeCorrected()+"ms)";
		}
		HTMLNode addressRow = peerRow.addChild("td", "class", "peer-address");
		// Ip to country + Flags
		IPConverter ipc = IPConverter.getInstance(NodeFile.IPv4ToCountry.getFile(node));
		byte[] addr = peerNodeStatus.getPeerAddressBytes();

		Country country = ipc.locateIP(addr);
		if(country != null) {
			country.renderFlagIcon(addressRow);
		}

		addressRow.addChild("#", ((peerNodeStatus.getPeerAddress() != null) ? peerNodeStatus.getPeerAddressAndPort() : (l10n("unknownAddress"))) + pingTime);

		// version column
		if (peerNodeStatus.getStatusValue() != PeerManager.PEER_NODE_STATUS_NEVER_CONNECTED && (peerNodeStatus.isPublicInvalidVersion() || peerNodeStatus.isPublicReverseInvalidVersion())) {  // Don't draw attention to a version problem if NEVER CONNECTED
			peerRow.addChild("td", "class", "peer-version").addChild("span", "class", "peer_version_problem", Integer.toString(peerNodeStatus.getSimpleVersion()));
		} else {
			peerRow.addChild("td", "class", "peer-version").addChild("#", Integer.toString(peerNodeStatus.getSimpleVersion()));
		}

		// location column
		if (advancedModeEnabled) {
			HTMLNode locationNode = peerRow.addChild("td", "class", "peer-location");
			locationNode.addChild("b", String.valueOf(peerNodeStatus.getLocation()));
			locationNode.addChild("br");
			double[] peersLoc = peerNodeStatus.getPeersLocation();
			if(peersLoc != null) {
				locationNode.addChild("i", "+"+(peersLoc.length)+" friends");
			}
		}

		if (advancedModeEnabled) {
			// backoff column
			HTMLNode backoffCell = peerRow.addChild("td", "class", "peer-backoff");
			backoffCell.addChild("#", fix1.format(peerNodeStatus.getBackedOffPercent(true)));
			int backoff = (int) (Math.max(peerNodeStatus.getRoutingBackedOffUntil(true) - now, 0));
			// Don't list the backoff as zero before it's actually zero
			if ((backoff > 0) && (backoff < 1000)) {
				backoff = 1000;
			}
			backoffCell.addChild("#", ' ' + String.valueOf(backoff / 1000) + '/' + String.valueOf(peerNodeStatus.getRoutingBackoffLength(true) / 1000));
			backoffCell.addChild("#", (peerNodeStatus.getLastBackoffReason(true) == null) ? "" : ('/' + (peerNodeStatus.getLastBackoffReason(true))));

			// backoff column
			backoffCell = peerRow.addChild("td", "class", "peer-backoff");
			backoffCell.addChild("#", fix1.format(peerNodeStatus.getBackedOffPercent(false)));
			backoff = (int) (Math.max(peerNodeStatus.getRoutingBackedOffUntil(false) - now, 0));
			// Don't list the backoff as zero before it's actually zero
			if ((backoff > 0) && (backoff < 1000)) {
				backoff = 1000;
			}
			backoffCell.addChild("#", ' ' + String.valueOf(backoff / 1000) + '/' + String.valueOf(peerNodeStatus.getRoutingBackoffLength(false) / 1000));
			backoffCell.addChild("#", (peerNodeStatus.getLastBackoffReason(false) == null) ? "" : ('/' + (peerNodeStatus.getLastBackoffReason(false))));

			// overload probability column
			HTMLNode pRejectCell = peerRow.addChild("td", "class", "peer-backoff"); // FIXME
			pRejectCell.addChild("#", fix1.format(peerNodeStatus.getPReject()));
		}

		// idle column
		long idle = peerNodeStatus.getTimeLastRoutable();
		if (peerNodeStatus.isRoutable()) {
			idle = peerNodeStatus.getTimeLastConnectionCompleted();
		} else if (peerNodeStatus.getStatusValue() == PeerManager.PEER_NODE_STATUS_NEVER_CONNECTED) {
			idle = peerNodeStatus.getPeerAddedTime();
		}
		if(!peerNodeStatus.isConnected() && (now - idle) > (2 * 7 * 24 * 60 * 60 * (long) 1000)) { // 2 weeks
			peerRow.addChild("td", "class", "peer-idle").addChild("span", "class", "peer_idle_old", idleToString(now, idle));
		} else {
			peerRow.addChild("td", "class", "peer-idle", idleToString(now, idle));
		}

		if(hasPrivateNoteColumn())
			drawPrivateNoteColumn(peerRow, peerNodeStatus, fProxyJavascriptEnabled);

		if(advancedModeEnabled) {
			// percent of time connected column
			peerRow.addChild("td", "class", "peer-idle" /* FIXME */).addChild("#", fix1.format(peerNodeStatus.getPercentTimeRoutableConnection()));
			// selection stats
			peerRow.addChild("td", "class", "peer-idle" /* FIXME */).addChild("#", (totalSelectionRate > 0 ? (peerSelectionPercentage+"%") : "N/A"));
			// total traffic column
			long sent = peerNodeStatus.getTotalOutputBytes();
			long resent = peerNodeStatus.getResendBytesSent();
			long received = peerNodeStatus.getTotalInputBytes();
			peerRow.addChild("td", "class", "peer-idle" /* FIXME */).addChild("#", SizeUtil.formatSize(received)+" / "+SizeUtil.formatSize(sent)+"/"+SizeUtil.formatSize(resent)+" ("+fix1.format(((double)resent) / ((double)sent))+")");
			// total traffic column startup
			peerRow.addChild("td", "class", "peer-idle" /* FIXME */).addChild("#", SizeUtil.formatSize(peerNodeStatus.getTotalInputSinceStartup())+" / "+SizeUtil.formatSize(peerNodeStatus.getTotalOutputSinceStartup()));
			// congestion control
			PacketThrottle t = peerNodeStatus.getThrottle();
			String val;
			if(t == null)
				val = "none";
			else
				val = (int)t.getBandwidth()+"B/sec delay "+
					t.getDelay()+"ms (RTT "+t.getRoundTripTime()+"ms window "+t.getWindowSize()+')';
			peerRow.addChild("td", "class", "peer-idle" /* FIXME */).addChild("#", val);
			// time delta
			peerRow.addChild("td", "class", "peer-idle" /* FIXME */).addChild("#", TimeUtil.formatTime(peerNodeStatus.getClockDelta()));
			peerRow.addChild("td", "class", "peer-idle" /* FIXME */).addChild("#", peerNodeStatus.getReportedUptimePercentage()+"%");
			peerRow.addChild("td", "class", "peer-idle" /* FIXME */).addChild("#", SizeUtil.formatSize(peerNodeStatus.getMessageQueueLengthBytes())+":"+TimeUtil.formatTime(peerNodeStatus.getMessageQueueLengthTime()));
			IncomingLoadSummaryStats loadStatsBulk = peerNodeStatus.incomingLoadStatsBulk;
			if(loadStatsBulk == null)
				peerRow.addChild("td", "class", "peer-idle" /* FIXME */);
			else
				peerRow.addChild("td", "class", "peer-idle" /* FIXME */).addChild("#", loadStatsBulk.runningRequestsTotal+"reqs:out:"+SizeUtil.formatSize(loadStatsBulk.usedCapacityOutputBytes)+"/"+SizeUtil.formatSize(loadStatsBulk.othersUsedCapacityOutputBytes)+"/"+SizeUtil.formatSize(loadStatsBulk.peerCapacityOutputBytes)+"/"+SizeUtil.formatSize(loadStatsBulk.totalCapacityOutputBytes)+":in:"+SizeUtil.formatSize(loadStatsBulk.usedCapacityInputBytes)+"/"+SizeUtil.formatSize(loadStatsBulk.othersUsedCapacityInputBytes)+"/"+SizeUtil.formatSize(loadStatsBulk.peerCapacityInputBytes)+"/"+SizeUtil.formatSize(loadStatsBulk.totalCapacityInputBytes));
			IncomingLoadSummaryStats loadStatsRT = peerNodeStatus.incomingLoadStatsRealTime;
			if(loadStatsRT == null)
				peerRow.addChild("td", "class", "peer-idle" /* FIXME */);
			else
				peerRow.addChild("td", "class", "peer-idle" /* FIXME */).addChild("#", loadStatsRT.runningRequestsTotal+"reqs:out:"+SizeUtil.formatSize(loadStatsRT.usedCapacityOutputBytes)+"/"+SizeUtil.formatSize(loadStatsRT.othersUsedCapacityOutputBytes)+"/"+SizeUtil.formatSize(loadStatsRT.peerCapacityOutputBytes)+"/"+SizeUtil.formatSize(loadStatsRT.totalCapacityOutputBytes)+":in:"+SizeUtil.formatSize(loadStatsRT.usedCapacityInputBytes)+"/"+SizeUtil.formatSize(loadStatsRT.othersUsedCapacityInputBytes)+"/"+SizeUtil.formatSize(loadStatsRT.peerCapacityInputBytes)+"/"+SizeUtil.formatSize(loadStatsRT.totalCapacityInputBytes));
		}
		
		if(endCols != null) {
			for(SimpleColumn col: endCols) {
				col.drawColumn(peerRow, peerNodeStatus);
			}
		}
		
		if (drawMessageTypes) {
			drawMessageTypes(peerTable, peerNodeStatus);
		}
	}

	protected boolean hasTrustColumn() {
		return false;
	}

	protected void drawTrustColumn(HTMLNode peerRow, PeerNodeStatus peerNodeStatus) {
		// Do nothing
	}

	protected boolean hasVisibilityColumn() {
		return false;
	}

	protected void drawVisibilityColumn(HTMLNode peerRow, PeerNodeStatus peerNodeStatus, boolean advancedModeEnabled) {
		// Do nothing
	}

	/** Is there a name column? */
	abstract protected boolean hasNameColumn();
	
	/**
	 * Draw the name column, if there is one. This will be directly after the status column.
	 */
	abstract protected void drawNameColumn(HTMLNode peerRow, PeerNodeStatus peerNodeStatus, boolean advanced);

	/**
	 * Is there a private note column?
	 */
	abstract protected boolean hasPrivateNoteColumn();

	/**
	 * Draw the private note column.
	 */
	abstract protected void drawPrivateNoteColumn(HTMLNode peerRow, PeerNodeStatus peerNodeStatus, boolean fProxyJavascriptEnabled);
	
	private void drawMessageTypes(HTMLNode peerTable, PeerNodeStatus peerNodeStatus) {
		HTMLNode messageCountRow = peerTable.addChild("tr", "class", "message-status");
		messageCountRow.addChild("td", "colspan", "2");
		HTMLNode messageCountCell = messageCountRow.addChild("td", "colspan", "9");  // = total table row width - 2 from above colspan
		HTMLNode messageCountTable = messageCountCell.addChild("table", "class", "message-count");
		HTMLNode countHeaderRow = messageCountTable.addChild("tr");
		countHeaderRow.addChild("th", "Message");
		countHeaderRow.addChild("th", "Incoming");
		countHeaderRow.addChild("th", "Outgoing");
		List<String> messageNames = new ArrayList<String>();
		Map<String, Long[]> messageCounts = new HashMap<String, Long[]>();
		for (Map.Entry<String,Long> entry : peerNodeStatus.getLocalMessagesReceived().entrySet() ) {
			String messageName = entry.getKey();
			Long messageCount = entry.getValue();
			messageNames.add(messageName);
			messageCounts.put(messageName, new Long[] { messageCount, 0L});
		}
		for (Map.Entry<String,Long> entry : peerNodeStatus.getLocalMessagesSent().entrySet() ) {
			String messageName =  entry.getKey();
			Long messageCount = entry.getValue();
			if (!messageNames.contains(messageName)) {
				messageNames.add(messageName);
			}
			Long[] existingCounts = messageCounts.get(messageName);
			if (existingCounts == null) {
				messageCounts.put(messageName, new Long[] {0L, messageCount });
			} else {
				existingCounts[1] = messageCount;
			}
		}
		Collections.sort(messageNames, new Comparator<String>() {
			@Override
			public int compare(String first, String second) {
				return first.compareToIgnoreCase(second);
			}
		});
		for (String messageName: messageNames) {
			Long[] messageCount = messageCounts.get(messageName);
			HTMLNode messageRow = messageCountTable.addChild("tr");
			messageRow.addChild("td", messageName);
			messageRow.addChild("td", "class", "right-align", String.valueOf(messageCount[0]));
			messageRow.addChild("td", "class", "right-align", String.valueOf(messageCount[1]));
		}
	}

	private String idleToString(long now, long idle) {
		if (idle <= 0) {
			return " ";
		}
		long idleMilliseconds = now - idle;
		return TimeUtil.formatTime(idleMilliseconds);
	}
	
	private static String l10n(String string) {
		return NodeL10n.getBase().getString("DarknetConnectionsToadlet."+string);
	}
	
    private static String l10n(String string, String pattern, String value) {
        return NodeL10n.getBase().getString("DarknetConnectionsToadlet."+string, pattern, value);
    }
    
	private String sortString(boolean isReversed, String type) {
		return (isReversed ? ("?sortBy="+type) : ("?sortBy="+type+"&reversed"));
	}
	
	/**
	 * Send a simple error page.
	 */
	protected void sendErrorPage(ToadletContext ctx, int code, String desc, String message, boolean returnToAddFriends) throws ToadletContextClosedException, IOException {
		PageNode page = ctx.getPageMaker().getPageNode(desc, ctx);
		HTMLNode pageNode = page.outer;
		HTMLNode contentNode = page.content;
		
		HTMLNode infoboxContent = ctx.getPageMaker().getInfobox("infobox-error", desc, contentNode, null, true);
		infoboxContent.addChild("#", message);
		if(returnToAddFriends) {
			infoboxContent.addChild("br");
			infoboxContent.addChild("a", "href", DarknetAddRefToadlet.PATH, l10n("returnToAddAFriendPage"));
			infoboxContent.addChild("br");
		} else {
			infoboxContent.addChild("br");
			infoboxContent.addChild("a", "href", ".", l10n("returnToPrevPage"));
			infoboxContent.addChild("br");
		}
		addHomepageLink(infoboxContent);
		
		writeHTMLReply(ctx, code, desc, pageNode.generate());
	}

}
