package freenet.clients.http;

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

import java.io.File;
import java.io.IOException;
import java.net.SocketException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import freenet.client.HighLevelSimpleClient;
import freenet.l10n.NodeL10n;
import freenet.node.Node;
import freenet.pluginmanager.AccessDeniedPluginHTTPException;
import freenet.pluginmanager.DownloadPluginHTTPException;
import freenet.pluginmanager.NotFoundPluginHTTPException;
import freenet.pluginmanager.OfficialPlugins.OfficialPluginDescription;
import freenet.pluginmanager.PluginHTTPException;
import freenet.pluginmanager.PluginInfoWrapper;
import freenet.pluginmanager.PluginManager;
import freenet.pluginmanager.RedirectPluginHTTPException;
import freenet.pluginmanager.PluginManager.PluginProgress;
import freenet.support.HTMLNode;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.MultiValueTable;
import freenet.support.TimeUtil;
import freenet.support.Logger.LogLevel;
import freenet.support.api.HTTPRequest;

public class PproxyToadlet extends Toadlet {
	private static final int MAX_PLUGIN_NAME_LENGTH = 1024;
	/** Maximum time to wait for a threaded plugin to exit */
	private static final long MAX_THREADED_UNLOAD_WAIT_TIME = SECONDS.toMillis(60);
	private final Node node;

        private static volatile boolean logMINOR;
	static {
		Logger.registerLogThresholdCallback(new LogThresholdCallback(){
			@Override
			public void shouldUpdate(){
				logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
			}
		});
	}

	public PproxyToadlet(HighLevelSimpleClient client, Node node) {
		super(client);
		this.node = node;
	}
	
	public boolean allowPOSTWithoutPassword() {
		return true;
	}

	public void handleMethodPOST(URI uri, final HTTPRequest request, ToadletContext ctx)
	throws ToadletContextClosedException, IOException {

		MultiValueTable<String, String> headers = new MultiValueTable<String, String>();

        if(!ctx.checkFullAccess(this))
            return;

		String path=request.getPath();

		// remove leading / and plugins/ from path
		if(path.startsWith("/")) path = path.substring(1);
		if(path.startsWith("plugins/")) path = path.substring("plugins/".length());

		if(logMINOR) Logger.minor(this, "Pproxy received POST on "+path);

		final PluginManager pm = node.getPluginManager();

		if(!path.isEmpty())
		{
			// Plugins handle their own formPassword checking.
			try
			{
				String plugin = null;
				// split path into plugin class name and 'daa' path for plugin
				int to = path.indexOf('/');
				if(to == -1)
				{
					plugin = path;
				}
				else
				{
					plugin = path.substring(0, to);
				}

				writeHTMLReply(ctx, 200, "OK", pm.handleHTTPPost(plugin, request));
			}
			catch (RedirectPluginHTTPException e) {
				writeTemporaryRedirect(ctx, e.message, e.newLocation);
			}
			catch (NotFoundPluginHTTPException e) {
				sendErrorPage(ctx, NotFoundPluginHTTPException.code, e.message, e.location);
			}
			catch (AccessDeniedPluginHTTPException e) {
				sendErrorPage(ctx, AccessDeniedPluginHTTPException.code, e.message, e.location);
			}
			catch (DownloadPluginHTTPException e) {
				// FIXME: maybe it ought to be defined like sendErrorPage : in toadlets

				MultiValueTable<String, String> head = MultiValueTable.from(
					"Content-Disposition",
					"attachment; filename=\"" + e.filename + '"'
				);
				ctx.sendReplyHeaders(DownloadPluginHTTPException.CODE, "Found", head, e.mimeType, e.data.length);
				ctx.writeData(e.data);
			}
			catch(PluginHTTPException e)
			{
				sendErrorPage(ctx, PluginHTTPException.code, e.message, e.location);
			}
			catch(Throwable t)
			{
				writeInternalError(t, ctx);
			}
		}
		else
		{
			if(!ctx.checkFormPassword(request)) return;
			
			PageMaker pageMaker = ctx.getPageMaker();
			
			if (request.isPartSet("submit-official")) {
				final String pluginName = request.getPartAsStringFailsafe("plugin-name", 40);

				node.getExecutor().execute(new Runnable() {
					@Override
					public void run() {
						pm.startPluginOfficial(pluginName, true);
					}
				});
				
				headers.put("Location", ".");
				ctx.sendReplyHeaders(302, "Found", headers, null, 0);
				return;
			}
			if (request.isPartSet("submit-other")) {
				final String pluginName = request.getPartAsStringFailsafe("plugin-url", 200);
				final boolean fileonly = "on".equalsIgnoreCase(request.getPartAsStringFailsafe("fileonly", 20));
				
				node.getExecutor().execute(new Runnable() {
					@Override
					public void run() {
						if (fileonly) 
							pm.startPluginFile(pluginName, true);
						else
							pm.startPluginURL(pluginName, true);
					}
				});

				headers.put("Location", ".");
				ctx.sendReplyHeaders(302, "Found", headers, null, 0);
				return;
			}
			if (request.isPartSet("submit-freenet")) {
				final String pluginName = request.getPartAsStringFailsafe("plugin-uri", 300);
				
				node.getExecutor().execute(new Runnable() {
					@Override
					public void run() {
						pm.startPluginFreenet(pluginName, true);
					}
				});

				headers.put("Location", ".");
				ctx.sendReplyHeaders(302, "Found", headers, null, 0);
				return;
			}
			if (request.isPartSet("cancel")){
				headers.put("Location", "/plugins/");
				ctx.sendReplyHeaders(302, "Found", headers, null, 0);
				return;
			}
			if (!request.getPartAsStringFailsafe("unloadconfirm", MAX_PLUGIN_NAME_LENGTH).isEmpty()) {
				String pluginThreadName = request.getPartAsStringFailsafe("unloadconfirm", MAX_PLUGIN_NAME_LENGTH);
				String pluginSpecification = getPluginSpecification(pm, pluginThreadName);
				pm.killPlugin(pluginThreadName, MAX_THREADED_UNLOAD_WAIT_TIME, false);
				if (request.isPartSet("purge")) {
					pm.removeCachedCopy(pluginSpecification);
				}
				PageNode page = pageMaker.getPageNode(l10n("plugins"), ctx);
				HTMLNode pageNode = page.outer;
				HTMLNode contentNode = page.content;
				HTMLNode infobox = contentNode.addChild("div", "class", "infobox infobox-success");
				infobox.addChild("div", "class", "infobox-header", l10n("pluginUnloaded"));
				HTMLNode infoboxContent = infobox.addChild("div", "class", "infobox-content");
				infoboxContent.addChild("#", l10n("pluginUnloadedWithName", "name", pluginThreadName));
				infoboxContent.addChild("br");
                                infoboxContent.addChild("#", l10n("pluginFilesWarning"));
                                infoboxContent.addChild("br");
                                infoboxContent.addChild("br");
				infoboxContent.addChild("a", "href", "/plugins/", l10n("returnToPluginPage"));
				writeHTMLReply(ctx, 200, "OK", pageNode.generate());
				return;
			}if (!request.getPartAsStringFailsafe("unload", MAX_PLUGIN_NAME_LENGTH).isEmpty()) {
				PageNode page = pageMaker.getPageNode(l10n("plugins"), ctx);
				HTMLNode pageNode = page.outer;
				HTMLNode contentNode = page.content;
				HTMLNode infobox = contentNode.addChild("div", "class", "infobox infobox-query");
				infobox.addChild("div", "class", "infobox-header", l10n("unloadPluginTitle"));
				HTMLNode infoboxContent = infobox.addChild("div", "class", "infobox-content");
				infoboxContent.addChild("#", l10n("unloadPluginWithName", "name", request.getPartAsStringFailsafe("unload", MAX_PLUGIN_NAME_LENGTH)));
				HTMLNode unloadForm = 
					ctx.addFormChild(infoboxContent, "/plugins/", "unloadPluginConfirmForm");
				unloadForm.addChild("input", new String[] { "type", "name", "value" }, new String[] { "hidden", "unloadconfirm", request.getPartAsStringFailsafe("unload", MAX_PLUGIN_NAME_LENGTH) });
				HTMLNode tempNode = unloadForm.addChild("p");
				tempNode.addChild("input", new String[] { "type", "name" }, new String[] { "checkbox", "purge" });
				tempNode.addChild("#", l10n("unloadPurge"));
				tempNode = unloadForm.addChild("p");
				tempNode.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "confirm", l10n("unload") });
				tempNode.addChild("#", " ");
				tempNode.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "cancel", NodeL10n.getBase().getString("Toadlet.cancel") });
				writeHTMLReply(ctx, 200, "OK", pageNode.generate());
				return;
			} else if (!request.getPartAsStringFailsafe("reload", MAX_PLUGIN_NAME_LENGTH).isEmpty()) {
				PageNode page = pageMaker.getPageNode(l10n("plugins"), ctx);
				HTMLNode pageNode = page.outer;
				HTMLNode contentNode = page.content;
				HTMLNode reloadContent = pageMaker.getInfobox("infobox infobox-query", l10n("reloadPluginTitle"), contentNode, "plugin-reload", true);
				reloadContent.addChild("p", l10n("reloadExplanation"));
				reloadContent.addChild("p", l10n("reloadWarning"));
				HTMLNode reloadForm = ctx.addFormChild(reloadContent, "/plugins/", "reloadPluginConfirmForm");
				String pluginIdentifier = request.getPartAsStringFailsafe("reload", MAX_PLUGIN_NAME_LENGTH);
				reloadForm.addChild("input", new String[] { "type", "name", "value" }, new String[] { "hidden", "reloadconfirm", pluginIdentifier });
				if (!pluginWasLoadedFromLocalDisk(pm, pluginIdentifier)) {
					HTMLNode tempNode = reloadForm.addChild("p");
					tempNode.addChild("input", new String[] { "type", "name" }, new String[] { "checkbox", "purge" });
					tempNode.addChild("#", l10n("reloadPurgeWarning"));
				}
				HTMLNode tempNode = reloadForm.addChild("p");
				tempNode.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "confirm", l10n("reload") });
				tempNode.addChild("#", " ");
				tempNode.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "cancel", NodeL10n.getBase().getString("Toadlet.cancel") });
				
				writeHTMLReply(ctx, 200, "OK", pageNode.generate());
				return;
			} else if (!request.getPartAsStringFailsafe("update", MAX_PLUGIN_NAME_LENGTH).isEmpty()) {
				// Deploy the plugin update
				final String pluginFilename = request.getPartAsStringFailsafe("update", MAX_PLUGIN_NAME_LENGTH);

				if (!pm.isPluginLoaded(pluginFilename)) {
					sendErrorPage(ctx, 404, l10n("pluginNotFoundUpdatingTitle"), 
							l10n("pluginNotFoundUpdating", "name", pluginFilename));
				} else {
					node.getNodeUpdater().deployPluginWhenReady(pluginFilename);

					headers.put("Location", ".");
					ctx.sendReplyHeaders(302, "Found", headers, null, 0);
				}
				return;
				
			}else if (!request.getPartAsStringFailsafe("reloadconfirm", MAX_PLUGIN_NAME_LENGTH).isEmpty()) {
				boolean purge = request.isPartSet("purge");
				String pluginThreadName = request.getPartAsStringFailsafe("reloadconfirm", MAX_PLUGIN_NAME_LENGTH);
				final String fn = getPluginSpecification(pm, pluginThreadName);

				if (fn == null) {
					sendErrorPage(ctx, 404, l10n("pluginNotFoundReloadTitle"), 
							l10n("pluginNotFoundReload"));
				} else {
					pm.killPlugin(pluginThreadName, MAX_THREADED_UNLOAD_WAIT_TIME, true);
					if (purge) {
						pm.removeCachedCopy(fn);
					}
					node.getExecutor().execute(new Runnable() {

						@Override
						public void run() {
							// FIXME
							pm.startPluginAuto(fn, true);
						}
						
					});

					headers.put("Location", ".");
					ctx.sendReplyHeaders(302, "Found", headers, null, 0);
				}
				return;
			}else {
				// Ignore
				headers.put("Location", ".");
				ctx.sendReplyHeaders(302, "Found", headers, null, 0);
			}

		}

	}

	private boolean pluginWasLoadedFromLocalDisk(PluginManager pluginManager, String pluginIdentifier) {
		for (PluginInfoWrapper pluginInfoWrapper : pluginManager.getPlugins()) {
			if (pluginInfoWrapper.getThreadName().equals(pluginIdentifier)) {
				File pluginFile = new File(pluginInfoWrapper.getFilename());
				if (pluginFile.exists() && pluginFile.isFile()) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Searches all plugins for the plugin with the given thread name and
	 * returns the plugin specification used to load the plugin.
	 * 
	 * @param pluginManager
	 *            The plugin manager
	 * @param pluginThreadName
	 *            The thread name of the plugin
	 * @return The plugin specification of the plugin, or <code>null</code> if
	 *         no plugin was found
	 */
	private String getPluginSpecification(PluginManager pluginManager, String pluginThreadName) {
		for(PluginInfoWrapper pi: pluginManager.getPlugins()) {
			if (pi.getThreadName().equals(pluginThreadName)) {
				return pi.getFilename();
			}
		}
		return null;
	}

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

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

	public void handleMethodGET(URI uri, HTTPRequest request, ToadletContext ctx)
	throws ToadletContextClosedException, IOException {

		//String basepath = "/plugins/";
		String path = request.getPath();

		// remove leading / and plugins/ from path
		if(path.startsWith("/")) path = path.substring(1);
		if(path.startsWith("plugins/")) path = path.substring("plugins/".length());

		PluginManager pm = node.getPluginManager();

		if(logMINOR)
			Logger.minor(this, "Pproxy fetching "+path);
		try {
			if (path.isEmpty()) {
		        if(!ctx.checkFullAccess(this))
		            return;

				Iterator<PluginProgress> loadingPlugins = pm.getStartingPlugins().iterator();

				PageNode page = ctx.getPageMaker().getPageNode(l10n("plugins"), ctx);
				boolean advancedModeEnabled = ctx.isAdvancedModeEnabled();
				HTMLNode pageNode = page.outer;
				if (loadingPlugins.hasNext()) {
					/* okay, add a refresh. */
					page.headNode.addChild("meta", new String[] { "http-equiv", "content" }, new String[] { "refresh", "10; url=" });
				}
				HTMLNode contentNode = page.content;

				contentNode.addChild(ctx.getAlertManager().createSummary());

				/* find which plugins have already been loaded. */
				List<OfficialPluginDescription> availablePlugins = pm.findAvailablePlugins();
				for(PluginInfoWrapper pluginInfoWrapper: pm.getPlugins()) {
					String pluginName = pluginInfoWrapper.getPluginClassName();
					String shortPluginName = pluginName.substring(pluginName.lastIndexOf('.') + 1);

					/* FIXME: Workaround the "Freemail" plugin show duplicate problem
					 * The "Freemail" plugin is show on "Aviliable Plugin" even
					 * if it is loaded. However fixing the plugin itself may break
					 * running it as standalone application. */
					if (shortPluginName.equals("FreemailPlugin")) shortPluginName = "Freemail"; // DOH!

					availablePlugins.remove(pm.isOfficialPlugin(shortPluginName));
				}
				while (loadingPlugins.hasNext()) {
					PluginProgress pluginProgress = loadingPlugins.next();
					String pluginName = pluginProgress.getName();
					availablePlugins.remove(pm.isOfficialPlugin(pluginName));
				}

				/* sort available plugins into groups. */
				SortedMap<String, List<OfficialPluginDescription>> groupedAvailablePlugins = new TreeMap<String, List<OfficialPluginDescription>>();
				for (OfficialPluginDescription pluginDescription : availablePlugins) {
					if (!advancedModeEnabled && (pluginDescription.advanced || pluginDescription.experimental || pluginDescription.deprecated)) {
						continue;
					}
					String translatedGroup = l10n("pluginGroup." + pluginDescription.group);
					if (!groupedAvailablePlugins.containsKey(translatedGroup)) {
						groupedAvailablePlugins.put(translatedGroup, new ArrayList<OfficialPluginDescription>());
					}
					groupedAvailablePlugins.get(translatedGroup).add(pluginDescription);
				}
				for (List<OfficialPluginDescription> pluginDescriptions : groupedAvailablePlugins.values()) {
					Collections.sort(pluginDescriptions, new Comparator<OfficialPluginDescription>() {
						/**
						 * {@inheritDoc}
						 */
						@Override
						public int compare(OfficialPluginDescription o1, OfficialPluginDescription o2) {
							return o1.name.compareTo(o2.name);
						}
					});
				}

				showStartingPlugins(pm, contentNode);
				showPluginList(ctx, pm, contentNode, advancedModeEnabled);
				showOfficialPluginLoader(ctx, contentNode, groupedAvailablePlugins, pm, advancedModeEnabled);
				showUnofficialPluginLoader(ctx, contentNode);
				showFreenetPluginLoader(ctx, contentNode);

				writeHTMLReply(ctx, 200, "OK", pageNode.generate());
			} else {
				// split path into plugin class name and 'data' path for plugin
				int to = path.indexOf('/');
				String plugin;
				if (to == -1) {
					plugin = path;
				} else {
					plugin = path.substring(0, to);
				}

				// Plugin may need to know where it was accessed from, so it can e.g. produce relative URLs.
				//writeReply(ctx, 200, "text/html", "OK", mkPage("plugin", pm.handleHTTPGet(plugin, data)));
				writeHTMLReply(ctx, 200, "OK", pm.handleHTTPGet(plugin, request));				
			}

			//FetchResult result = fetch(key);
			//writeReply(ctx, 200, result.getMimeType(), "OK", result.asBucket());
		} catch (RedirectPluginHTTPException e) {
			writeTemporaryRedirect(ctx, e.message, e.newLocation);
		} catch (NotFoundPluginHTTPException e) {
			sendErrorPage(ctx, NotFoundPluginHTTPException.code, e.message, e.location);
		} catch (AccessDeniedPluginHTTPException e) {
			sendErrorPage(ctx, AccessDeniedPluginHTTPException.code, e.message, e.location);
		} catch (DownloadPluginHTTPException e) {
			// FIXME: maybe it ought to be defined like sendErrorPage : in toadlets

			MultiValueTable<String, String> head = MultiValueTable.from(
				"Content-Disposition",
				"attachment; filename=\"" + e.filename + '"'
			);
			ctx.sendReplyHeaders(DownloadPluginHTTPException.CODE, "Found", head, e.mimeType, e.data.length);
			ctx.writeData(e.data);
		} catch(PluginHTTPException e) {
			sendErrorPage(ctx, PluginHTTPException.code, e.message, e.location);
		} catch (SocketException e) {
			ctx.forceDisconnect();
		} catch (Throwable t) {
			ctx.forceDisconnect();
			Logger.error(this, "Caught: "+t, t);
			writeInternalError(t, ctx);
		}
	}

	/**
	 * Shows a list of all currently loading plugins.
	 * 
	 * @param pluginManager
	 *            The plugin manager
	 * @param contentNode
	 *            The node to add content to
	 */
	private void showStartingPlugins(PluginManager pluginManager, HTMLNode contentNode) {
		Set<PluginProgress> startingPlugins = pluginManager.getStartingPlugins();
		if (!startingPlugins.isEmpty()) {
			HTMLNode startingPluginsBox = contentNode.addChild("div", "class", "infobox infobox-normal");
			startingPluginsBox.addChild("div", "class", "infobox-header", l10n("startingPluginsTitle"));
			HTMLNode startingPluginsContent = startingPluginsBox.addChild("div", "class", "infobox-content");
			HTMLNode startingPluginsTable = startingPluginsContent.addChild("table");
			HTMLNode startingPluginsHeader = startingPluginsTable.addChild("tr");
			startingPluginsHeader.addChild("th", l10n("startingPluginName"));
			startingPluginsHeader.addChild("th", l10n("startingPluginStatus"));
			startingPluginsHeader.addChild("th", l10n("startingPluginTime"));
			Iterator<PluginProgress> startingPluginsIterator = startingPlugins.iterator();
			while (startingPluginsIterator.hasNext()) {
				PluginProgress pluginProgress = startingPluginsIterator.next();
				HTMLNode startingPluginsRow = startingPluginsTable.addChild("tr");
				startingPluginsRow.addChild("td", pluginProgress.getLocalisedPluginName());
				startingPluginsRow.addChild(pluginProgress.toLocalisedHTML());
				startingPluginsRow.addChild("td", "aligh", "right", TimeUtil.formatTime(pluginProgress.getTime()));
			}
		}
	}

	private void showPluginList(ToadletContext ctx, PluginManager pm, HTMLNode contentNode, boolean advancedMode) throws ToadletContextClosedException, IOException {
		HTMLNode infobox = contentNode.addChild("div", "class", "infobox infobox-normal");
		infobox.addChild("div", "class", "infobox-header", NodeL10n.getBase().getString("PluginToadlet.pluginListTitle"));
		HTMLNode infoboxContent = infobox.addChild("div", "class", "infobox-content");
		if (pm.getPlugins().isEmpty()) {
			infoboxContent.addChild("div", l10n("noPlugins"));
		} else {
			HTMLNode pluginTable = infoboxContent.addChild("table", "class", "plugins");
			HTMLNode headerRow = pluginTable.addChild("tr");
			headerRow.addChild("th", l10n("pluginFilename"));
			if(advancedMode)
				headerRow.addChild("th", l10n("classNameTitle"));
			headerRow.addChild("th", l10n("versionTitle"));
			if(advancedMode) {
				headerRow.addChild("th", l10n("internalIDTitle"));
				headerRow.addChild("th", l10n("startedAtTitle"));
			}
			headerRow.addChild("th");
			headerRow.addChild("th");
			headerRow.addChild("th");
			Iterator<PluginInfoWrapper> it = pm.getPlugins().iterator();
			while (it.hasNext()) {
				PluginInfoWrapper pi = it.next();
				HTMLNode pluginRow = pluginTable.addChild("tr");
				pluginRow.addChild("td", pi.getLocalisedPluginName());
				if(advancedMode)
					pluginRow.addChild("td", pi.getPluginClassName());
				long ver = pi.getPluginLongVersion();
				if(ver != -1)
					pluginRow.addChild("td", pi.getPluginVersion()+" ("+ver+")");
				else
					pluginRow.addChild("td", pi.getPluginVersion());
				if(advancedMode) {
					pluginRow.addChild("td", pi.getThreadName());
					pluginRow.addChild("td", new Date(pi.getStarted()).toString());
				}
				if (pi.isStopping()) {
					pluginRow.addChild("td", l10n("pluginStopping"));
					/* add two empty cells. */
					pluginRow.addChild("td");
					pluginRow.addChild("td");
				} else {
					if (pi.isPproxyPlugin()) {
						HTMLNode visitForm = pluginRow.addChild("td").addChild("form", new String[] { "method", "action", "target", "rel" }, new String[] { "get", pi.getPluginClassName(), "_blank", "noreferrer noopener" });
						visitForm.addChild("input", new String[] { "type", "name", "value" }, new String[] { "hidden", "formPassword", ctx.getFormPassword() });
						visitForm.addChild("input", new String[] { "type", "value" }, new String[] { "submit", NodeL10n.getBase().getString("PluginToadlet.visit") });
					} else
						pluginRow.addChild("td");
					HTMLNode unloadForm = ctx.addFormChild(pluginRow.addChild("td"), ".", "unloadPluginForm");
					unloadForm.addChild("input", new String[] { "type", "name", "value" }, new String[] { "hidden", "unload", pi.getThreadName() });
					unloadForm.addChild("input", new String[] { "type", "value" }, new String[] { "submit", l10n("unload") });
					HTMLNode reloadForm = ctx.addFormChild(pluginRow.addChild("td"), ".", "reloadPluginForm");
					reloadForm.addChild("input", new String[] { "type", "name", "value" }, new String[] { "hidden", "reload", pi.getThreadName() });
					reloadForm.addChild("input", new String[] { "type", "value" }, new String[] { "submit", l10n("reload") });
				}
			}
		}
	}
	
	private void showOfficialPluginLoader(ToadletContext toadletContext, HTMLNode contentNode, Map<String, List<OfficialPluginDescription>> availablePlugins, PluginManager pm, boolean advancedModeEnabled) {
		/* box for "official" plugins. */
		HTMLNode addOfficialPluginBox = contentNode.addChild("div", "class", "infobox infobox-normal");
		addOfficialPluginBox.addChild("div", "class", "infobox-header", l10n("loadOfficialPlugin"));
		HTMLNode addOfficialPluginContent = addOfficialPluginBox.addChild("div", "class", "infobox-content");
		HTMLNode addOfficialForm = toadletContext.addFormChild(addOfficialPluginContent, ".", "addOfficialPluginForm");
		
		HTMLNode p = addOfficialForm.addChild("p");
		p.addChild("#", l10n("loadOfficialPluginText"));
		
		for (Entry<String, List<OfficialPluginDescription>> groupPlugins : availablePlugins.entrySet()) {
			List<OfficialPluginDescription> notLoadedPlugins = getNotLoadedPlugins(pm, groupPlugins.getValue());
			if (notLoadedPlugins.isEmpty()) {
				continue;
			}
			addPluginGroupForLoading(advancedModeEnabled, groupPlugins, addOfficialForm, notLoadedPlugins);
		}
		addOfficialForm.addChild("p").addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "submit-official", l10n("Load") });
	}

	private void addPluginGroupForLoading(boolean advancedModeEnabled, Entry<String, List<OfficialPluginDescription>> groupPlugins, HTMLNode addOfficialForm, List<OfficialPluginDescription> notLoadedPlugins) {
		HTMLNode pluginGroupNode = addOfficialForm.addChild("div", "class", "plugin-group");
		pluginGroupNode.addChild("div", "class", "plugin-group-title", l10n("pluginGroupTitle", "pluginGroup", groupPlugins.getKey()));
		for (OfficialPluginDescription pluginDescription : notLoadedPlugins) {
			if (pluginDescription.unsupported) {
				continue;
			}
			HTMLNode pluginNode = pluginGroupNode.addChild("div", "class", "plugin");
			HTMLNode option = pluginNode.addChild("input",
					new String[] { "type", "name", "value", "id" },
					new String[] { "radio", "plugin-name", pluginDescription.name, "radioPlugin" + pluginDescription.name });
			option.addChild("label",
					new String[] { "for" },
					new String[] { "radioPlugin" + pluginDescription.name }
			).addChild("i", pluginDescription.getLocalisedPluginName());
			if (pluginDescription.deprecated)
				option.addChild("b", " (" + l10n("loadLabelDeprecated") + ")");
			if (pluginDescription.experimental)
				option.addChild("b", " (" + l10n("loadLabelExperimental") + ")");
			if (advancedModeEnabled && pluginDescription.minimumVersion >= 0) {
				option.addChild("#", " (" + l10n("pluginVersion") + " " + pluginDescription.recommendedVersion + ")");
			}
			option.addChild("#", " - " + pluginDescription.getLocalisedPluginDescription());
		}
	}

	private List<OfficialPluginDescription> getNotLoadedPlugins(PluginManager pluginManager, List<OfficialPluginDescription> plugins) {
		List<OfficialPluginDescription> notLoadedPlugins = new ArrayList<OfficialPluginDescription>();
		for (OfficialPluginDescription plugin : plugins) {
			if (!pluginManager.isPluginLoaded(plugin.name)) {
				notLoadedPlugins.add(plugin);
			}
		}
		return notLoadedPlugins;
	}

	private void showUnofficialPluginLoader(ToadletContext toadletContext, HTMLNode contentNode) {
		/* box for unofficial plugins. */
		HTMLNode addOtherPluginBox = contentNode.addChild("div", "class", "infobox infobox-normal");
		addOtherPluginBox.addChild("div", "class", "infobox-header", l10n("loadOtherPlugin"));
		HTMLNode addOtherPluginContent = addOtherPluginBox.addChild("div", "class", "infobox-content");
		HTMLNode addOtherForm = toadletContext.addFormChild(addOtherPluginContent, ".", "addOtherPluginForm");
		addOtherForm.addChild("div", l10n("loadOtherPluginText"));
		addOtherForm.addChild("#", (l10n("loadOtherURLLabel") + ": "));
		addOtherForm.addChild("input", new String[] { "type", "name", "size" }, new String[] { "text", "plugin-url", "80" });
		addOtherForm.addChild("#", " ");
		addOtherForm.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "submit-other", l10n("Load") });
		addOtherForm.addChild("br");
		addOtherForm.addChild("input",
			new String[] { "type", "name", "checked", "id" },
			new String[] { "checkbox", "fileonly", "checked", "fileonly" });
		addOtherForm.addChild("label",
			new String[] { "for" },
			new String[] { "fileonly" },
			" " + l10n("fileonly"));
	}
	
	private void showFreenetPluginLoader(ToadletContext toadletContext, HTMLNode contentNode) {
		/* box for freenet plugins. */
		HTMLNode addFreenetPluginBox = contentNode.addChild("div", "class", "infobox infobox-normal");
		addFreenetPluginBox.addChild("div", "class", "infobox-header", l10n("loadFreenetPlugin"));
		HTMLNode addFreenetPluginContent = addFreenetPluginBox.addChild("div", "class", "infobox-content");
		HTMLNode addFreenetForm = toadletContext.addFormChild(addFreenetPluginContent, ".", "addFreenetPluginForm");
		addFreenetForm.addChild("div", l10n("loadFreenetPluginText"));
		addFreenetForm.addChild("#", (l10n("loadFreenetURLLabel") + ": "));
		addFreenetForm.addChild("input", new String[] { "type", "name", "size" }, new String[] { "text", "plugin-uri", "80" });
		addFreenetForm.addChild("#", " ");
		addFreenetForm.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "submit-freenet", l10n("Load") });
	}

	@Override
	public String path() {
		return PATH;
	}
	
	public static final String PATH = "/plugins/";

}
