package freenet.clients.http;

import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import freenet.client.filter.PushingTagReplacerCallback;
import freenet.l10n.NodeL10n;
import freenet.node.DarknetPeerNode;
import freenet.node.Node;
import freenet.node.SecurityLevels;
import freenet.pluginmanager.FredPluginL10n;
import freenet.support.HTMLNode;
import freenet.support.Logger;
import freenet.support.api.HTTPRequest;

/** Simple class to output standard heads and tail for web interface pages. 
*/
public final class PageMaker {
	
	public enum THEME {
		BOXED("boxed", "Boxed (Top menu)", "", false, false),
		BOXED_CLASSIC("boxed-classic", "Boxed (Classic menu)", "", false, false),
		BOXED_DROPDOWN("boxed-dropdown", "Boxed (Dropdown menu)", "", false, false),
		BOXED_DYNAMIC("boxed-classic", "Boxed (Dynamic menu)", "", false, false),
		BOXED_STATIC("boxed-static", "Boxed (Static menu)", "", false, false),
		CLEAN("clean", "Clean", "Mr. Proper", false, false),
		CLEAN_CLASSIC("clean-classic", "Clean (Classic menu)", "Clean theme with a classic menu.", false, false),
		CLEAN_DROPDOWN("clean-dropdown", "Clean (Dropdown menu)", "Clean theme with a dropdown menu.", false, false),
		CLEAN_STATIC("clean-static", "Clean (Static menu)", "Clean theme with a static menu.", false, false),
		CLEAN_TOP("clean-top", "Clean (Top menu)", "Clean theme with a static top menu.", false, false),
		GRAYANDBLUE("grayandblue", "Gray And Blue (Classic menu)", "", false, false),
		GRAYANDBLUE_DYNAMIC("grayandblue-dynamic", "Gray And Blue (Dynamic menu)", "", false, false),
		GRAYANDBLUE_DROPDOWN("grayandblue-dropdown", "Gray And Blue (Dropdown menu)", "", false, false),
		GRAYANDBLUE_STATIC("grayandblue-static", "Gray And Blue (Static menu)", "", false, false),
		GRAYANDBLUE_TOP("grayandblue-top", "Gray And Blue (Top menu)", "", false, false),
		SKY("sky", "Sky (Top menu)", "", false, false),
		SKY_CLASSIC("sky-classic", "Sky (Classic menu)", "", false, false),
		SKY_DROPDOWN("sky-dropdown", "Sky (Dropdown menu)", "", false, false),
		SKY_DYNAMIC("sky-dynamic", "Sky (Dynamic menu)", "", false, false),
		SKY_STATIC("sky-static", "Sky (Static menu)", "", false, false),
		MINIMALBLUE("minimalblue", "Minimal Blue", "A minimalistic theme in blue", false, false),
		MINIMALISTIC("minimalist", "Minimalistic", "A very minimalistic theme based on Google's designs", true, true),
		RABBIT_HOLE("rabbit-hole", "Into the Rabbit Hole", "Simple and clean theme", false, false);

		
		public static final String[] possibleValues = {
			BOXED.code,
			BOXED_CLASSIC.code,
			BOXED_DROPDOWN.code,
			BOXED_DYNAMIC.code,
			BOXED_STATIC.code,
			CLEAN.code,
			CLEAN_CLASSIC.code,
			CLEAN_DROPDOWN.code,
			CLEAN_STATIC.code,
			CLEAN_TOP.code,
			GRAYANDBLUE.code,
			GRAYANDBLUE_DYNAMIC.code,
			GRAYANDBLUE_DROPDOWN.code,
			GRAYANDBLUE_STATIC.code,
			GRAYANDBLUE_TOP.code,
			SKY.code,
			SKY_CLASSIC.code,
			SKY_DROPDOWN.code,
			SKY_DYNAMIC.code,
			SKY_STATIC.code,
			MINIMALBLUE.code,
			MINIMALISTIC.code,
			RABBIT_HOLE.code
		};
		
		public final String code;  // the internal name
		public final String name;  // the name in "human form"
		public final String description; // description
		/**
		 * If true, the activelinks will appear on the welcome page, whether
		 * the user has enabled them or not.
		 */
		public final boolean forceActivelinks;
		/**
		 * If true, the "Fetch a key" infobox will appear above the bookmarks
		 * infobox on the welcome page.
		 */
		public final boolean fetchKeyBoxAboveBookmarks;
		
		private THEME(String code, String name, String description) {
			this(code, name, description, false, false);
		}

		private THEME(String code, String name, String description, boolean forceActivelinks, boolean fetchKeyBoxAboveBookmarks) {
			this.code = code;
			this.name = name;
			this.description = description;
			this.forceActivelinks = forceActivelinks;
			this.fetchKeyBoxAboveBookmarks = fetchKeyBoxAboveBookmarks;
		}

		public static THEME themeFromName(String cssName) {
			for(THEME t : THEME.values()) {
				if(t.code.equalsIgnoreCase(cssName) ||
				   t.name.equalsIgnoreCase(cssName))
				{
					return t;
				}
			}
			return getDefault();
		}

		public static THEME getDefault() {
			return THEME.SKY_STATIC;
		}
	}	
	
	public static final int MODE_SIMPLE = 1;
	public static final int MODE_ADVANCED = 2;

	/** Parameter for simple/advanced mode switch. */
	private static final String MODE_SWITCH_PARAMETER = "fproxyAdvancedMode";

	private THEME theme;
	private String override;
	private final Node node;
	
	private List<SubMenu> menuList = new ArrayList<SubMenu>();
	private Map<String, SubMenu> subMenus = new HashMap<String, SubMenu>();
	
	private static class SubMenu {
		
		/** Name of the submenu */
		private final String navigationLinkText;
		/** Link if the user clicks on the submenu itself */
		private final String defaultNavigationLink;
		/** Tooltip */
		private final String defaultNavigationLinkTitle;
		
		private final FredPluginL10n plugin;
		
		private final List<String> navigationLinkTexts = new ArrayList<String>();
		private final List<String> navigationLinkTextsNonFull = new ArrayList<String>();
		private final Map<String, String> navigationLinkTitles = new HashMap<String, String>();
		private final Map<String, String> navigationLinks = new HashMap<String, String>();
		private final Map<String, LinkEnabledCallback>  navigationLinkCallbacks = new HashMap<String, LinkEnabledCallback>();
		private final Map<String, FredPluginL10n> navigationLinkL10n = new HashMap<String, FredPluginL10n>();
		
		public SubMenu(String link, String name, String title, FredPluginL10n plugin) {
			this.navigationLinkText = name;
			this.defaultNavigationLink = link;
			this.defaultNavigationLinkTitle = title;
			this.plugin = plugin;
		}

		public void addNavigationLink(String path, String name, String title, boolean fullOnly, LinkEnabledCallback cb, FredPluginL10n l10n) {
			navigationLinkTexts.add(name);
			if(!fullOnly)
				navigationLinkTextsNonFull.add(name);
			navigationLinkTitles.put(name, title);
			navigationLinks.put(name, path);
			if(cb != null)
				navigationLinkCallbacks.put(name, cb);
			if (l10n != null)
				navigationLinkL10n.put(name, l10n);
		}

		/** Remove a link from this sub-menu. */
		public void removeNavigationLink(String name) {
			navigationLinkTexts.remove(name);
			navigationLinkTextsNonFull.remove(name);
			navigationLinkTitles.remove(name);
			navigationLinks.remove(name);
			navigationLinkL10n.remove(name); //Should this be here? If so, why not remove from navigationLinkCallbacks too
		}

	}
	
	protected PageMaker(THEME t, Node n) {
		setTheme(t);
		this.node = n;
	}
	
	void setOverride(String pointTo) {
		this.override = pointTo;
	}
	
	public void setTheme(THEME theme2) {
		if (theme2 == null) {
			this.theme = THEME.getDefault();
		} else {
			URL themeurl = getClass().getResource("staticfiles/themes/" + theme2.code + "/theme.css");
			if (themeurl == null)
				this.theme = THEME.getDefault();
			else
				this.theme = theme2;
		}
	}

	public synchronized void addNavigationCategory(String link, String name, String title, FredPluginL10n plugin) {
		SubMenu menu = new SubMenu(link, name, title, plugin);
		subMenus.put(name, menu);
		menuList.add(menu);
	}
	
	/**
	 * Add a navigation category to the menu at a given offset.
	 * @param menuOffset The position of the link in FProxy's menu. 0 = left.
	 */
	public synchronized void addNavigationCategory(String link, String name, String title, FredPluginL10n plugin, int menuOffset) {
		SubMenu menu = new SubMenu(link, name, title, plugin);
		subMenus.put(name, menu);
		menuList.add(menuOffset, menu);
	}
	

	public synchronized void removeNavigationCategory(String name) {
		SubMenu menu = subMenus.remove(name);
		if (menu == null) {
			Logger.error(this, "can't remove navigation category, name="+name);
			return;
		}	
		menuList.remove(menu);
	}
	
	public synchronized void addNavigationLink(String menutext, String path, String name, String title, boolean fullOnly, LinkEnabledCallback cb, FredPluginL10n l10n) {
		SubMenu menu = subMenus.get(menutext);
		if(menu == null)
			throw new NullPointerException("there is no menu named "+menutext);
		menu.addNavigationLink(path, name, title, fullOnly, cb, l10n);
	}

	/** Remove a navigation link from a sub-menu. Applies globally, do not use this to customise 
	 * menus when sending one page! */
	public synchronized void removeNavigationLink(String menutext, String name) {
		SubMenu menu = subMenus.get(menutext);
		// The menu may have already been removed.
		if(menu != null)
		    menu.removeNavigationLink(name);
	}
	
	public HTMLNode createBackLink(ToadletContext toadletContext, String name) {
		String referer = toadletContext.getHeaders().get("referer");
		if (referer != null) {
			return new HTMLNode("a", new String[] { "href", "title" }, new String[] { referer, name }, name);
		}
		return new HTMLNode("a", new String[] { "href", "title" }, new String[] { "javascript:back()", name }, name);
	}

	/**
	 * Generates an FProxy template page suitable for adding content to.
	 *
	 * @param title
	 *            Title of the page.
	 * @param ctx
	 *            ToadletContext to use to render the page.
	 * @return A template PageNode.
	 */
	public PageNode getPageNode(String title, ToadletContext ctx) {
		return getPageNode(title, true, ctx);
	}

	/**
	 * Generates an FProxy template page with optional navigation bar suitable
	 * for adding content to.
	 *
	 * @param title
	 *            Title of the page.
	 * @param renderNavigationLinks
	 *            Whether to render navigation links.
	 * @param ctx
	 *            ToadletContext to use to render the page.
	 * @return A template PageNode.
	 * @deprecated Use
	 *             {@link #getPageNode(String, ToadletContext, RenderParameters)}
	 *             instead
	 */
	@Deprecated
	public PageNode getPageNode(String title, boolean renderNavigationLinks, ToadletContext ctx) {
		return getPageNode(title, renderNavigationLinks, true, ctx);
	}

	/**
	 * Generates an FProxy template page with optional navigation bar and status
	 * information suitable for adding content to.
	 *
	 * @param title
	 *            Title of the page.
	 * @param renderNavigationLinks
	 *            Whether to render navigation links.
	 * @param renderStatus
	 *            Whether to render the status display.
	 * @param ctx
	 *            ToadletContext to use to render the page.
	 * @return A template PageNode.
	 * @deprecated Use
	 *             {@link #getPageNode(String, ToadletContext, RenderParameters)}
	 *             instead
	 */
	@Deprecated
	public PageNode getPageNode(String title, boolean renderNavigationLinks, boolean renderStatus, ToadletContext ctx) {
		return getPageNode(title, ctx, new RenderParameters().renderNavigationLinks(renderNavigationLinks).renderStatus(renderStatus).renderModeSwitch(true));
	}

	/**
	 * Generates an FProxy template page suitable for adding content to.
	 *
	 * @param title
	 *            Title of the page.
	 * @param ctx
	 *            ToadletContext to use to render the page. Can be null, e.g. if the HTML is not 
	 *            being generated as part of a toadlet request, for example if it's using the old
	 *            FredPluginHTTP interface.
	 * @param renderParameters
	 *            Parameters for inclusion or omission of certain page elements
	 * @return A template PageNode.
	 */
	public PageNode getPageNode(String title, ToadletContext ctx, RenderParameters renderParameters) {
		boolean fullAccess = ctx == null ? false : ctx.isAllowedFullAccess();
		HTMLNode pageNode = new HTMLNode.HTMLDoctype("html", "-//W3C//DTD XHTML 1.1//EN");
		HTMLNode htmlNode = pageNode.addChild("html", "xml:lang", NodeL10n.getBase().getSelectedLanguage().isoCode);
		HTMLNode headNode = htmlNode.addChild("head");
		headNode.addChild("meta", new String[] { "http-equiv", "content" }, new String[] { "Content-Type", "text/html; charset=utf-8" });
		headNode.addChild("title", title + " - Freenet");
		//To make something only rendered when javascript is on, then add the jsonly class to it
		headNode.addChild("noscript").addChild("style"," .jsonly {display:none;}");
		if(override != null)
			headNode.addChild(getOverrideContent());
		else 
			headNode.addChild("link", new String[] { "rel", "href", "type", "title" }, new String[] { "stylesheet", "/static/themes/" + theme.code + "/theme.css", "text/css", theme.code });
		
		boolean sendAllThemes =  ctx != null && ctx.getContainer().sendAllThemes();
		
		if(sendAllThemes) {
			for (THEME t: THEME.values()) {
				String themeName = t.code;
				headNode.addChild("link", new String[] { "rel", "href", "type", "media", "title" }, new String[] { "alternate stylesheet", "/static/themes/" + themeName + "/theme.css", "text/css", "screen", themeName });
			}
		}
		
		boolean webPushingEnabled = 
			ctx != null && ctx.getContainer().isFProxyJavascriptEnabled() && ctx.getContainer().isFProxyWebPushingEnabled();
		
		// Add the generated javascript, if it and pushing is enabled
		if (webPushingEnabled) headNode.addChild("script", new String[] { "type", "language", "src" }, new String[] {
				"text/javascript", "javascript", "/static/freenetjs/freenetjs.nocache.js" });
		
		Toadlet t;
		if (ctx != null) {
			t = ctx.activeToadlet();
			t = t.showAsToadlet(ctx);
		} else
			t = null;
		String activePath = "";
		if(t != null) activePath = t.path();
		HTMLNode bodyNode = htmlNode.addChild("body",
		        new String[] { "class", "id" },
		        new String[] { "fproxy-page", filterCSSIdentifier("page-"+activePath) });
		//Add a hidden input that has the request's id
		if(webPushingEnabled)
			bodyNode.addChild("input",new String[]{"type","name","value","id"},new String[]{"hidden","requestId",ctx.getUniqueId(),"requestId"});
		
		// Add the client-side localization only when pushing is enabled
		if (webPushingEnabled) {
			bodyNode.addChild("script", new String[] { "type", "language" }, new String[] { "text/javascript", "javascript" }).addChild("%", PushingTagReplacerCallback.getClientSideLocalizationScript());
		}
		
		HTMLNode pageDiv = bodyNode.addChild("div", "id", "page");
		HTMLNode topBarDiv = pageDiv.addChild("div", "id", "topbar");

		if (renderParameters.isRenderStatus() && fullAccess) {
			final HTMLNode statusBarDiv = pageDiv.addChild("div", "id", "statusbar-container").addChild("div", "id", "statusbar");

			 if (node != null && node.clientCore != null) {
				 final HTMLNode alerts = ctx.getAlertManager().createSummary(true);
				 if (alerts != null) {
					 statusBarDiv.addChild(alerts).addAttribute("id", "statusbar-alerts");
					 statusBarDiv.addChild("div", "class", "separator", "\u00a0");
				 }
			 }


			statusBarDiv.addChild("div", "id", "statusbar-language").addChild("a", "href", "/config/node#l10n", NodeL10n.getBase().getSelectedLanguage().fullName);

			if (node.clientCore != null && ctx != null && renderParameters.isRenderModeSwitch()) {
				boolean isAdvancedMode = ctx.isAdvancedModeEnabled();
				String uri = ctx.getUri().getQuery();
				Map<String, List<String>> parameters = HTTPRequestImpl.parseUriParameters(uri, true);
				List<String> newModeSwitchValues = new ArrayList<String>();
				newModeSwitchValues.add(String.valueOf(isAdvancedMode ? MODE_SIMPLE : MODE_ADVANCED));
				/* overwrite any previously existing parameter value. */
				parameters.put(MODE_SWITCH_PARAMETER, newModeSwitchValues);

				statusBarDiv.addChild("div", "class", "separator", "\u00a0");
				final HTMLNode switchMode = statusBarDiv.addChild("div", "id", "statusbar-switchmode");
				switchMode.addAttribute("class", isAdvancedMode ? "simple" : "advanced");
				switchMode.addChild("a", "href", "?" + HTTPRequestImpl.createQueryString(parameters, false), isAdvancedMode ? NodeL10n.getBase().getString("StatusBar.switchToSimpleMode") : NodeL10n.getBase().getString("StatusBar.switchToAdvancedMode"));
			}

			if (node != null && node.clientCore != null) {
				statusBarDiv.addChild("div", "class", "separator", "\u00a0");
				final HTMLNode secLevels = statusBarDiv.addChild("div", "id", "statusbar-seclevels", NodeL10n.getBase().getString("SecurityLevels.statusBarPrefix"));

				final HTMLNode network = secLevels.addChild("a", "href", "/seclevels/", SecurityLevels.localisedName(node.securityLevels.getNetworkThreatLevel()) + "\u00a0");
				network.addAttribute("title", NodeL10n.getBase().getString("SecurityLevels.networkThreatLevelShort"));
				network.addAttribute("class", node.securityLevels.getNetworkThreatLevel().toString().toLowerCase());

				final HTMLNode physical = secLevels.addChild("a", "href", "/seclevels/", SecurityLevels.localisedName(node.securityLevels.getPhysicalThreatLevel()));
				physical.addAttribute("title", NodeL10n.getBase().getString("SecurityLevels.physicalThreatLevelShort"));
				physical.addAttribute("class", node.securityLevels.getPhysicalThreatLevel().toString().toLowerCase());

				statusBarDiv.addChild("div", "class", "separator", "\u00a0");

				final int connectedPeers = node.peers.countConnectedPeers();
				int darknetTotal = 0;
				for(DarknetPeerNode n : node.peers.getDarknetPeers()) {
					if(n == null) continue;
					if(n.isDisabled()) continue;
					darknetTotal++;
				}
				final int connectedDarknetPeers = node.peers.countConnectedDarknetPeers();
				final int totalPeers = (node.getOpennet() == null) ? (darknetTotal > 0 ? darknetTotal : Integer.MAX_VALUE) : node.getOpennet().getNumberOfConnectedPeersToAimIncludingDarknet();
				final double connectedRatio = ((double)connectedPeers) / (double)totalPeers;
				final String additionalClass;

				// If we use Opennet, we color the bar by the ratio of connected nodes
				if(connectedPeers > connectedDarknetPeers) {
					if (connectedRatio < 0.3D || connectedPeers < 3) {
						additionalClass = "very-few-peers";
					} else if (connectedRatio < 0.5D) {
						additionalClass = "few-peers";
					} else if (connectedRatio < 0.75D) {
						additionalClass = "avg-peers";
					} else {
						additionalClass = "full-peers";
					}
				} else {
					// If we are darknet only, we color by absolute connected peers
					if (connectedDarknetPeers < 3) {
						additionalClass = "very-few-peers";
					} else if (connectedDarknetPeers < 5) {
						additionalClass = "few-peers";
					} else if (connectedDarknetPeers < 10) {
						additionalClass = "avg-peers";
					} else {
						additionalClass = "full-peers";
					}
				}

				HTMLNode progressBar = statusBarDiv.addChild("div", "class", "progressbar");
				progressBar.addChild("div", new String[] { "class", "style" }, new String[] { "progressbar-done progressbar-peers " + additionalClass, "width: " +
						Math.min(100,Math.floor(100*connectedRatio)) + "%;" });

				progressBar.addChild("div", new String[] { "class", "title" }, new String[] { "progress_fraction_finalized", NodeL10n.getBase().getString("StatusBar.connectedPeers", new String[]{"X", "Y"},
						new String[]{Integer.toString(node.peers.countConnectedDarknetPeers()), Integer.toString(node.peers.countConnectedOpennetPeers())}) },
						Integer.toString(connectedPeers) + ((totalPeers != Integer.MAX_VALUE) ? " / " + Integer.toString(totalPeers) : ""));
			}
		}

		topBarDiv.addChild("h1", title);
		if (renderParameters.isRenderNavigationLinks()) {
			SubMenu selected = null;
			// Render the full menu.
			HTMLNode navbarDiv = pageDiv.addChild("div", "id", "navbar");
			HTMLNode navbarUl = navbarDiv.addChild("ul", "id", "navlist");
			synchronized (this) {
				for (SubMenu menu : menuList) {
					HTMLNode subnavlist = new HTMLNode("ul");
					boolean isSelected = false;
					boolean nonEmpty = false;
					for (String navigationLink :  fullAccess ? menu.navigationLinkTexts : menu.navigationLinkTextsNonFull) {
						LinkEnabledCallback cb = menu.navigationLinkCallbacks.get(navigationLink);
						if(cb != null && !cb.isEnabled(ctx)) continue;
						nonEmpty = true;
						String navigationTitle = menu.navigationLinkTitles.get(navigationLink);
						String navigationPath = menu.navigationLinks.get(navigationLink);
						HTMLNode sublistItem;
						if(activePath.equals(navigationPath)) {
							sublistItem = subnavlist.addChild("li", "class", "submenuitem-selected");
							isSelected = true;
						} else {
							sublistItem = subnavlist.addChild("li", "class", "submenuitem-not-selected");
						}
						
						FredPluginL10n l10n = menu.navigationLinkL10n.get(navigationLink);
						if(l10n == null) l10n = menu.plugin;
						if(l10n != null) {
							// From a plugin. Include the plugin name in the id.
							sublistItem.addAttribute("id", getPluginL10nCSSIdentifier(l10n, navigationTitle));

							if(navigationTitle != null) {
								String newNavigationTitle = l10n.getString(navigationTitle);
								if(newNavigationTitle == null) {
									Logger.error(this, "Plugin '"+l10n+"' did return null in getString(key)!");
								} else {
									navigationTitle = newNavigationTitle;
								}
							}
							if(navigationLink != null) {
								String newNavigationLink = l10n.getString(navigationLink);
								if(newNavigationLink == null) {
									Logger.error(this, "Plugin '"+l10n+"' did return null in getString(key)!");
								} else {
									navigationLink = newNavigationLink;
								}
							}
						} else {
							// Not from a plugin. Add the localization key as id.
							sublistItem.addAttribute("id", filterCSSIdentifier(navigationTitle));

							if(navigationTitle != null) navigationTitle = NodeL10n.getBase().getString(navigationTitle);
							if(navigationLink != null) navigationLink = NodeL10n.getBase().getString(navigationLink);
						}
						if(navigationTitle != null)
							sublistItem.addChild("a", new String[] { "href", "title" }, new String[] { navigationPath, navigationTitle }, navigationLink);
						else
							sublistItem.addChild("a", "href", navigationPath, navigationLink);
					}
					if(nonEmpty) {
						HTMLNode listItem;
						if(isSelected) {
							selected = menu;
							subnavlist.addAttribute("class", "subnavlist-selected");
							listItem = new HTMLNode("li", "class", "navlist-selected");
						} else {
							subnavlist.addAttribute("class", "subnavlist");
							listItem = new HTMLNode("li", "class", "navlist-not-selected");
						}
						String menuItemTitle = menu.defaultNavigationLinkTitle;
						String text = menu.navigationLinkText;
						if(menu.plugin == null) {
							// Not from a plugin. Add the localization key as id.
							listItem.addAttribute("id", filterCSSIdentifier(menuItemTitle));

							menuItemTitle = NodeL10n.getBase().getString(menuItemTitle);
							text = NodeL10n.getBase().getString(text);
						} else {
							/*
							 * From a plugin. Include the plugin name in the id.
							 *
							 * Note that a plugin could misbehave and fail to register its
							 * menu with proper localization keys.
							 */
							listItem.addAttribute("id", getPluginL10nCSSIdentifier(menu.plugin, text));

							String newTitle = menu.plugin.getString(menuItemTitle);
							if(newTitle == null) {
								Logger.error(this, "Plugin '"+menu.plugin+"' did return null in getString(key)!");
							} else {
								menuItemTitle = newTitle;
							}
							String newText = menu.plugin.getString(text);
							if(newText == null) {
								Logger.error(this, "Plugin '"+menu.plugin+"' did return null in getString(key)!");
							} else {
								text = newText;
							}
						}
						
						listItem.addChild("a", new String[] { "href", "title" }, new String[] { menu.defaultNavigationLink, menuItemTitle }, text);
						listItem.addChild(subnavlist);
						navbarUl.addChild(listItem);
					}
				}
			}
			// Some themes want the selected submenu separately.
			if(selected != null) {
				HTMLNode div = new HTMLNode("div", "id", "selected-subnavbar");
				HTMLNode subnavlist = div.addChild("ul", "id", "selected-subnavbar-list");
				boolean nonEmpty = false;
				for (String navigationLink :  fullAccess ? selected.navigationLinkTexts : selected.navigationLinkTextsNonFull) {
					LinkEnabledCallback cb = selected.navigationLinkCallbacks.get(navigationLink);
					if(cb != null && !cb.isEnabled(ctx)) continue;
					nonEmpty = true;
					String navigationTitle = selected.navigationLinkTitles.get(navigationLink);
					String navigationPath = selected.navigationLinks.get(navigationLink);
					HTMLNode sublistItem;
					if(activePath.equals(navigationPath)) {
						sublistItem = subnavlist.addChild("li", "class", "submenuitem-selected");
					} else {
						sublistItem = subnavlist.addChild("li", "class", "submenuitem-not-selected");
					}
					
					FredPluginL10n l10n = selected.navigationLinkL10n.get(navigationLink);
					if (l10n == null) l10n = selected.plugin;
					if(l10n != null) {
						if(navigationTitle != null) navigationTitle = l10n.getString(navigationTitle);
						if(navigationLink != null) navigationLink = l10n.getString(navigationLink);
					} else {
						if(navigationTitle != null) navigationTitle = NodeL10n.getBase().getString(navigationTitle);
						if(navigationLink != null) navigationLink = NodeL10n.getBase().getString(navigationLink);
					}
					if(navigationTitle != null)
						sublistItem.addChild("a", new String[] { "href", "title" }, new String[] { navigationPath, navigationTitle }, navigationLink);
					else
						sublistItem.addChild("a", "href", navigationPath, navigationLink);
				}
				if(nonEmpty)
					pageDiv.addChild(div);
			}
		}
		HTMLNode contentDiv = pageDiv.addChild("div", "id", "content");
		return new PageNode(pageNode, headNode, contentDiv);
	}

	/**
	 * Create a CSS identifier incorporating both a class name and a localization key.
	 * @param plugin plugin localization instance (used for class name)
	 * @param key localization key
	 * @return valid CSS identifier.
	 */
	// TODO: Less-stupid name.
	public static String getPluginL10nCSSIdentifier(FredPluginL10n plugin, String key) {
		return filterCSSIdentifier(plugin.getClass().getName()+'-'+key);
	}

	/**
	 * Filters a given string so that it will be a valid CSS identifier. It replaces all characters that are not
	 * a dash, underscore, or alphanumeric with an underscore. If the first character is a dash and the second
	 * character is not a letter or underscore, replaces the second character with an underscore. This filter is
	 * overly strict as it does not allow non-ASCII characters or escapes. If the given string is below two
	 * characters in length, it appends underscores until it is not.
	 * @param input string to filter
	 * @return a filtered string guaranteed to be a syntactically valid CSS identifier.
	 * @link http://www.w3.org/TR/CSS21/syndata.html#tokenization
	 * @link http://www.w3.org/TR/CSS21/grammar.html#scanner
	 * @link http://stackoverflow.com/questions/448981/
	 */
	public static String filterCSSIdentifier(String input) {
		while (input.length() < 2) input = input.concat("_");

		return input.replaceFirst("^-[^_a-zA-Z]", "-_").replaceAll("[^-_a-zA-Z0-9]", "_");
	}

	public THEME getTheme() {
		return this.theme;
	}

	public InfoboxNode getInfobox(String header) {
		return getInfobox(header, null, false);
	}

	public InfoboxNode getInfobox(HTMLNode header) {
		return getInfobox(header, null, false);
	}

	public InfoboxNode getInfobox(String category, String header) {
		return getInfobox(category, header, null, false);
	}

	public HTMLNode getInfobox(String category, String header, HTMLNode parent) {
		return getInfobox(category, header, parent, null, false);
	}

	public InfoboxNode getInfobox(String category, HTMLNode header) {
		return getInfobox(category, header, null, false);
	}

	public InfoboxNode getInfobox(String header, String title, boolean isUnique) {
		if (header == null) throw new NullPointerException();
		return getInfobox(new HTMLNode("#", header), title, isUnique);
	}
	
	public InfoboxNode getInfobox(HTMLNode header, String title, boolean isUnique) {
		if (header == null) throw new NullPointerException();
		return getInfobox(null, header, title, isUnique);
	}

	public InfoboxNode getInfobox(String category, String header, String title, boolean isUnique) {
		if (header == null) throw new NullPointerException();
		return getInfobox(category, new HTMLNode("#", header), title, isUnique);
	}

	/** Create an infobox, attach it to the given parent, and return the content node. */
	public HTMLNode getInfobox(String category, String header, HTMLNode parent, String title, boolean isUnique) {
		InfoboxNode node = getInfobox(category, header, title, isUnique);
		parent.addChild(node.outer);
		return node.content;
	}

	/**
	 * Returns an infobox with the given style and header.
	 * 
	 * @param category
	 *            The CSS styles, separated by a space (' ')
	 * @param header
	 *            The header HTML node
	 * @return The infobox
	 */
	public InfoboxNode getInfobox(String category, HTMLNode header, String title, boolean isUnique) {
		if (header == null) throw new NullPointerException();

		StringBuffer classes = new StringBuffer("infobox");
		if(category != null) {
			classes.append(" ");
			classes.append(category);
		}
		if(title != null && !isUnique) {
			classes.append(" ");
			classes.append(title);
		}

		HTMLNode infobox = new HTMLNode("div", "class", classes.toString());

		if(title != null && isUnique) {
			infobox.addAttribute("id", title);
		}

		infobox.addChild("div", "class", "infobox-header").addChild(header);
		return new InfoboxNode(infobox, infobox.addChild("div", "class", "infobox-content"));
	}
	
	private HTMLNode getOverrideContent() {
		return new HTMLNode("link", new String[] { "rel", "href", "type", "media", "title" }, new String[] { "stylesheet", override, "text/css", "screen", "custom" });
	}

	public boolean advancedMode(HTTPRequest req, ToadletContainer container) {
		return parseMode(req, container) >= MODE_ADVANCED;
	}

	/** Call this before getPageNode(), so the menus reflect the advanced mode setting. */
	public int parseMode(HTTPRequest req, ToadletContainer container) {
		int mode = container.isAdvancedModeEnabled() ? MODE_ADVANCED : MODE_SIMPLE;

		if(req.isParameterSet(MODE_SWITCH_PARAMETER)) {
			mode = req.getIntParam(MODE_SWITCH_PARAMETER, mode);
			if(mode == MODE_ADVANCED)
				container.setAdvancedMode(true);
			else
				container.setAdvancedMode(false);
		}
		
		return mode;
	}
	
	/**
	 * Bundles parameters that are used to create the page node. The default for
	 * the render parameters is to include all optional render tasks. Individual
	 * tasks may be enabled or disabled by calling the appropriate methods which
	 * returns a new {@link RenderParameters} object as {@link RenderParameters}
	 * are immutable.
	 *
	 * @see PageMaker#getPageNode(String, ToadletContext, RenderParameters)
	 * @author <a href="mailto:bombe@pterodactylus.net">David ?Bombe? Roden</a>
	 */
	public static class RenderParameters {

		/** Whether to include navigation links in the page. */
		private final boolean renderNavigationLinks;

		/** Whether to include the status bar in the page. */
		private final boolean renderStatus;

		/** Whether to include the mode switch in the page. */
		private final boolean renderModeSwitch;

		/**
		 * Creates default render parameters that include all elements.
		 */
		public RenderParameters() {
			this(true, true, true);
		}

		/**
		 * Creates render parameters.
		 *
		 * @param renderNavigationLinks
		 *            {@code true} to include navigation links in the page
		 * @param renderStatus
		 *            {@code true} to include the status bar in the page
		 * @param renderModeSwitch
		 *            {@code true} to include the mode switch in the status bar
		 */
		private RenderParameters(boolean renderNavigationLinks, boolean renderStatus, boolean renderModeSwitch) {
			this.renderNavigationLinks = renderNavigationLinks;
			this.renderStatus = renderStatus;
			this.renderModeSwitch = renderModeSwitch;
		}

		//
		// ACCESSORS
		//

		/**
		 * Returns whether the navigation links should be included in the page.
		 *
		 * @return {@code true} if the navigation links should be included in
		 *         the page, {@code false} otherwise
		 */
		public boolean isRenderNavigationLinks() {
			return renderNavigationLinks;
		}

		/**
		 * Returns a new {@link RenderParameters} object that renders the
		 * navigation links according to the given parameter.
		 *
		 * @param renderNavigationLinks
		 *            {@code true} to render the navigation links, {@code false}
		 *            otherwise
		 * @return A new {@link RenderParameters} object
		 */
		public RenderParameters renderNavigationLinks(boolean renderNavigationLinks) {
			return new RenderParameters(renderNavigationLinks, renderStatus, renderModeSwitch);
		}

		/**
		 * Returns whether the status bar should be included in the page.
		 *
		 * @return {@code true} if the status bar should be included in the
		 *         page, {@code false} otherwise
		 */
		public boolean isRenderStatus() {
			return renderStatus;
		}

		/**
		 * Returns a new {@link RenderParameters} object that renders the status
		 * bar according to the given parameter.
		 *
		 * @param renderStatus
		 *            {@code true} to render the status bar, {@code false}
		 *            otherwise
		 * @return A new {@link RenderParameters} object
		 */
		public RenderParameters renderStatus(boolean renderStatus) {
			return new RenderParameters(renderNavigationLinks, renderStatus, renderModeSwitch);
		}

		/**
		 * Returns whether the mode switch should be included in the page.
		 *
		 * @return {@code true} if the mode switch should be included in the
		 *         page, {@code false} otherwise
		 */
		public boolean isRenderModeSwitch() {
			return renderModeSwitch;
		}

		/**
		 * Returns a new {@link RenderParameters} object that renders the mode
		 * switch according to the given parameter.
		 *
		 * @param renderModeSwitch
		 *            {@code true} to render the mode switch, {@code false}
		 *            otherwise
		 * @return A new {@link RenderParameters} object
		 */
		public RenderParameters renderModeSwitch(boolean renderModeSwitch) {
			return new RenderParameters(renderNavigationLinks, renderStatus, renderModeSwitch);
		}

	}

}
