/* This code is part of Freenet. It is distributed under the GNU General
 * Public License, version 2 (or at your option any later version). See
 * http://www.gnu.org/ for further details of the GPL. */
package freenet.clients.http;

import java.io.IOException;
import java.net.URI;

import freenet.client.HighLevelSimpleClient;
import freenet.config.Config;
import freenet.config.ConfigCallback;
import freenet.config.EnumerableOptionCallback;
import freenet.config.InvalidConfigValueException;
import freenet.config.NodeNeedRestartException;
import freenet.config.Option;
import freenet.config.SubConfig;
import freenet.config.WrapperConfig;
import freenet.l10n.NodeL10n;
import freenet.node.Node;
import freenet.node.NodeClientCore;
import freenet.node.ProgramDirectory;
import freenet.node.useralerts.AbstractUserAlert;
import freenet.node.useralerts.UserAlert;
import freenet.pluginmanager.FredPluginConfigurable;
import freenet.support.HTMLNode;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.MultiValueTable;
import freenet.support.URLEncoder;
import freenet.support.api.BooleanCallback;
import freenet.support.api.HTTPRequest;

/**
 * Node Configuration Toadlet. Accessible from <code>http://.../config/</code>.
 */
// FIXME: add logging, comments
public class ConfigToadlet extends Toadlet implements LinkEnabledCallback {
	// If a setting has to be more than a meg, something is seriously wrong!
	private static final int MAX_PARAM_VALUE_SIZE = 1024 * 1024;
	private String directoryBrowserPath;
	private final SubConfig subConfig;
	private final Config config;
	private final NodeClientCore core;
	private final Node node;
	/** plugin is always null except when this ConfigToadlet serves a plugin */
	private final FredPluginConfigurable plugin;
	private boolean needRestart = false;
	private NeedRestartUserAlert needRestartUserAlert;

	/**
	 * Prompt for node restart
	 */
	private class NeedRestartUserAlert extends AbstractUserAlert {
		private final String formPassword;
		
		public NeedRestartUserAlert(String formPassword) {
			this.formPassword = formPassword;
		}
		
		@Override
		public String getTitle() {
			return l10n("needRestartTitle");
		}

		@Override
		public String getText() {
			return getHTMLText().toString();
		}

		@Override
		public String getShortText() {
			return l10n("needRestartShort");
		}

		@Override
		public HTMLNode getHTMLText() {
			HTMLNode alertNode = new HTMLNode("div");
			alertNode.addChild("#", l10n("needRestart"));

			if (node.isUsingWrapper()) {
				alertNode.addChild("br");
				HTMLNode restartForm = alertNode.addChild(
						"form",
						new String[] { "action", "method", "enctype", "id",
								"accept-charset" },
						new String[] { "/", "post", "multipart/form-data",
								"restartForm", "utf-8" }).addChild("div");
				restartForm.addChild("input", new String[] { "type", "name",
						"value" }, new String[] { "hidden", "formPassword",
						formPassword });
				restartForm.addChild("div");
				restartForm.addChild("input",//
						new String[] { "type", "name" },//
						new String[] { "hidden", "restart" });
				restartForm.addChild("input", //
						new String[] { "type", "name", "value" },//
						new String[] { "submit", "restart2",
								l10n("restartNode") });
			}

			return alertNode;
		}

		@Override
		public short getPriorityClass() {
			return UserAlert.WARNING;
		}

		@Override
		public boolean isValid() {
			return needRestart;
		}

		@Override
		public boolean userCanDismiss() {
			return false;
		}
	}

	/**
	 * Describes which UI element should be used to present an option.
	 */
	private enum OptionType {
		/**
		 * A writable option with an enumerable list of possible values.
		 */
		DROP_DOWN("dropdown"),
		/**
		 * A writable option which can be either true or false.
		 */
		BOOLEAN("boolean"),
		/**
		 * A writable option which is a path to a directory.
		 */
		DIRECTORY("directory"),
		/**
		 * A writable option set with a string of text.
		 */
		TEXT("text"),
		/**
		 * A read-only option presented in a text field.
		 */
		TEXT_READ_ONLY("text readonly");

		/**
		 * A CSS class descriptor for this option type.
		 */
		public final String cssClass;

		private OptionType(String cssClass) {
			this.cssClass = cssClass;
		}
	}

	public ConfigToadlet(String directoryBrowserPath,
			HighLevelSimpleClient client, Config conf, SubConfig subConfig,
			Node node, NodeClientCore core) {
		this(directoryBrowserPath, client, conf, subConfig, node, core, null);
	}

	public ConfigToadlet(HighLevelSimpleClient client, Config conf,
			SubConfig subConfig, Node node, NodeClientCore core) {
		this(client, conf, subConfig, node, core, null);
	}

	public ConfigToadlet(String directoryBrowserPath,
			HighLevelSimpleClient client, Config conf, SubConfig subConfig,
			Node node, NodeClientCore core, FredPluginConfigurable plugin) {
		this(client, conf, subConfig, node, core, plugin);
		this.directoryBrowserPath = directoryBrowserPath;
	}

	public ConfigToadlet(HighLevelSimpleClient client, Config conf,
			SubConfig subConfig, Node node, NodeClientCore core,
			FredPluginConfigurable plugin) {
		super(client);
		config = conf;
		this.core = core;
		this.node = node;
		this.subConfig = subConfig;
		this.plugin = plugin;
		this.directoryBrowserPath = "/unset-browser-path/";
	}

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

		// User requested reset to defaults, so present confirmation page.
		if (request.isPartSet("confirm-reset-to-defaults")) {
			PageNode page = ctx.getPageMaker().getPageNode(
					l10n("confirmResetTitle"), ctx);
			HTMLNode pageNode = page.outer;
			HTMLNode contentNode = page.content;

			HTMLNode content = ctx.getPageMaker().getInfobox("infobox-warning",
					l10n("confirmResetTitle"), contentNode, "reset-confirm",
					true);
			content.addChild("#", l10n("confirmReset"));

			HTMLNode formNode = ctx.addFormChild(content, path(), "yes-button");
			String subconfig = request.getPartAsStringFailsafe("subconfig",
					MAX_PARAM_VALUE_SIZE);
			formNode.addChild("input",
					new String[] { "type", "name", "value" }, new String[] {
							"hidden", "subconfig", subconfig });

			// Persist visible fields so that they are reset to default or
			// unsaved changes are persisted.
			for (String part : request.getParts()) {
				if (part.startsWith(subconfig)) {
					formNode.addChild(
							"input",
							new String[] { "type", "name", "value" },
							new String[] {
									"hidden",
									part,
									request.getPartAsStringFailsafe(part,
											MAX_PARAM_VALUE_SIZE) });
				}
			}

			formNode.addChild("input",
					new String[] { "type", "name", "value" }, new String[] {
							"submit", "reset-to-defaults",
							NodeL10n.getBase().getString("Toadlet.yes") });

			formNode.addChild("input",
					new String[] { "type", "name", "value" }, new String[] {
							"submit", "decline-default-reset",
							NodeL10n.getBase().getString("Toadlet.no") });
			writeHTMLReply(ctx, 200, "OK", pageNode.generate());
			return;
		}

		// Returning from directory selector with a selection or declining
		// resetting settings to defaults.
		// Re-render config page with any changes made in the selector and/or
		// persisting values changed but
		// not applied.
		if (request.isPartSet(LocalFileBrowserToadlet.selectDir)
				|| request.isPartSet("decline-default-reset")) {
			handleMethodGET(uri, request, ctx);
			return;
		}

		// Entering directory selector from config page.
		// This would be two loops if it checked for a redirect
		// (key.startsWith("select-directory.")) before
		// constructing params string. It always constructs it, then redirects
		// if it turns out to be needed.
		boolean directorySelector = false;
		StringBuilder paramsBuilder = new StringBuilder();
		paramsBuilder.append('?');
		String value;
		for (String key : request.getParts()) {
			// Prepare parts for page selection redirect:
			// Extract option and put into "select-for"; preserve others.
			value = request.getPartAsStringFailsafe(key, MAX_PARAM_VALUE_SIZE);
			if (key.startsWith("select-directory.")) {
				paramsBuilder
					.append("select-for=")
					.append(URLEncoder.encode(key.substring("select-directory.".length()), true))
					.append('&');
				directorySelector = true;
			} else {
				paramsBuilder.append(URLEncoder.encode(key, true)).append('=')
					.append(URLEncoder.encode(value, true)).append('&');
			}
		}
		String params = paramsBuilder.toString();
		if (directorySelector) {
			MultiValueTable<String, String> headers = new MultiValueTable<String, String>(
					1);
			// params ends in &. Download directory browser starts in default
			// download directory.
			headers.put("Location", directoryBrowserPath + params + "path="
					+ core.getDownloadsDir().getAbsolutePath());
			ctx.sendReplyHeaders(302, "Found", headers, null, 0);
			return;
		}

		StringBuilder errbuf = new StringBuilder();
		boolean logMINOR = Logger.shouldLog(LogLevel.MINOR, this);

		String prefix = request.getPartAsStringFailsafe("subconfig",
				MAX_PARAM_VALUE_SIZE);
		if (logMINOR) {
			Logger.minor(this, "Current config prefix is " + prefix);
		}
		boolean resetToDefault = request.isPartSet("reset-to-defaults");
		if (resetToDefault && logMINOR) {
			Logger.minor(this, "Resetting to defaults");
		}

		for (Option<?> o : config.get(prefix).getOptions()) {
			String configName = o.getName();
			if (logMINOR) {
				Logger.minor(this, "Checking option " + prefix + '.'
						+ configName);
			}

			// This ignores unrecognized parameters.
			if (request.isPartSet(prefix + '.' + configName)) {
				// Current subconfig is to be reset to default.
				if (resetToDefault) {
					// Disallow resetting fproxy port number to default as it
					// might break the link to start fproxy on the system tray,
					// shortcuts etc.
					if (prefix.equals("fproxy") && configName.equals("port"))
						continue;
					value = o.getDefault();
				} else {
					value = request.getPartAsStringFailsafe(prefix + '.'
							+ configName, MAX_PARAM_VALUE_SIZE);
				}

				if (!(o.getValueDisplayString().equals(value))) {

					if (logMINOR) {
						Logger.minor(this, "Changing " + prefix + '.'
								+ configName + " to " + value);
					}

					try {
						o.setValue(value);
					} catch (InvalidConfigValueException e) {
						errbuf.append(o.getName()).append(' ')
								.append(e.getMessage()).append('\n');
					} catch (NodeNeedRestartException e) {
						needRestart = true;
					} catch (Exception e) {
						errbuf.append(o.getName()).append(' ').append(e)
								.append('\n');
						Logger.error(this, "Caught " + e, e);
					}
				} else if (logMINOR) {
					Logger.minor(this, prefix + '.' + configName
							+ " not changed");
				}
			}
		}

		// Wrapper params
		String wrapperConfigName = "wrapper.java.maxmemory";
		if (request.isPartSet(wrapperConfigName)) {
			value = request.getPartAsStringFailsafe(wrapperConfigName,
					MAX_PARAM_VALUE_SIZE);
			if (!WrapperConfig.getWrapperProperty(wrapperConfigName).equals(
					value)) {
				if (logMINOR) {
					Logger.minor(this, "Setting " + wrapperConfigName + " to "
							+ value);
				}
				WrapperConfig.setWrapperProperty(wrapperConfigName, value);
			}
		}

		config.store();

		PageNode page = ctx.getPageMaker().getPageNode(l10n("appliedTitle"),
				ctx);
		HTMLNode pageNode = page.outer;
		HTMLNode contentNode = page.content;

		if (errbuf.length() == 0) {
			HTMLNode content = ctx.getPageMaker().getInfobox("infobox-success",
					l10n("appliedTitle"), contentNode, "configuration-applied",
					true);
			content.addChild("#", l10n("appliedSuccess"));

			if (needRestart) {
				content.addChild("br");
				content.addChild("#", l10n("needRestart"));

				if (node.isUsingWrapper()) {
					content.addChild("br");
					HTMLNode restartForm = ctx.addFormChild(content, "/",
							"restartForm");
					restartForm.addChild("input",//
							new String[] { "type", "name" },//
							new String[] { "hidden", "restart" });
					restartForm.addChild("input", //
							new String[] { "type", "name", "value" },//
							new String[] { "submit", "restart2",//
									l10n("restartNode") });
				}

				if (needRestartUserAlert == null) {
					needRestartUserAlert = new NeedRestartUserAlert(ctx.getFormPassword());
					ctx.getAlertManager().register(needRestartUserAlert);
				}
			}
		} else {
			HTMLNode content = ctx
					.getPageMaker()
					.getInfobox("infobox-error", l10n("appliedFailureTitle"),
							contentNode, "configuration-error", true)
					.addChild("div", "class", "infobox-content");
			content.addChild("#", l10n("appliedFailureExceptions"));
			content.addChild("br");
			content.addChild("#", errbuf.toString());
		}

		HTMLNode content = ctx.getPageMaker().getInfobox("infobox-normal",
				l10n("possibilitiesTitle"), contentNode,
				"configuration-possibilities", false);
		content.addChild("a", new String[] { "href", "title" }, new String[] {
				path(), l10n("shortTitle") }, l10n("returnToNodeConfig"));
		content.addChild("br");
		addHomepageLink(content);

		writeHTMLReply(ctx, 200, "OK", pageNode.generate());

	}

	private static String l10n(String string) {
		return NodeL10n.getBase().getString("ConfigToadlet." + string);
	}

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

        if(!ctx.checkFullAccess(this))
            return;
        
		boolean advancedModeEnabled = ctx.isAdvancedModeEnabled();

		PageNode page = ctx.getPageMaker().getPageNode(
				NodeL10n.getBase().getString("ConfigToadlet.fullTitle"), ctx);
		HTMLNode pageNode = page.outer;
		HTMLNode contentNode = page.content;

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

		HTMLNode infobox = contentNode.addChild("div", "class",
				"infobox infobox-normal");
		infobox.addChild("div", "class", "infobox-header", l10n("title"));
		HTMLNode configNode = infobox.addChild("div", "class",
				"infobox-content");
		HTMLNode formNode = ctx.addFormChild(configNode, path(), "configForm");

		// Invisible apply button at the top so that an enter keypress will
		// apply settings instead of
		// going to a directory browser if present.
		formNode.addChild("input", new String[] { "type", "value", "class" },
				new String[] { "submit", l10n("apply"), "invisible" });

		/*
		 * Special case: present an option for the wrapper's maximum memory
		 * under Core configuration, provided the maximum memory property is
		 * defined. (the wrapper is being used)
		 */
		if (subConfig.getPrefix().equals("node")
				&& WrapperConfig.canChangeProperties()) {
			String configName = "wrapper.java.maxmemory";
			String curValue = WrapperConfig.getWrapperProperty(configName);
			// If persisted from directory browser, override. This is a POST
			// HTTPRequest.
			if (req.isPartSet(configName)) {
				curValue = req.getPartAsStringFailsafe(configName,
						MAX_PARAM_VALUE_SIZE);
			}
			if (curValue != null) {
				formNode.addChild("div", "class", "configprefix",
						l10n("wrapper"));
				HTMLNode list = formNode.addChild("ul", "class", "config");
				HTMLNode item = list.addChild("li", "class",
						OptionType.TEXT.cssClass);
				// FIXME how to get the real default???
				String defaultValue = "256";
				item.addChild(
						"span",
						new String[] { "class", "title", "style" },
						new String[] {
								"configshortdesc",
								NodeL10n.getBase().getString(
										"ConfigToadlet.defaultIs",
										new String[] { "default" },
										new String[] { defaultValue }),
								"cursor: help;" }).addChild(
						NodeL10n.getBase().getHTMLNode(
								"WrapperConfig." + configName + ".short"));
				item.addChild("span", "class", "config")
						.addChild(
								"input",
								new String[] { "type", "class", "name", "value" },
								new String[] { "text", "config", configName,
										curValue });
				item.addChild("span", "class", "configlongdesc").addChild(
						NodeL10n.getBase().getHTMLNode(
								"WrapperConfig." + configName + ".long"));
			}
		}

		short displayedConfigElements = 0;
		HTMLNode configGroupUlNode = new HTMLNode("ul", "class", "config");

		String overriddenOption = null;
		String overriddenValue = null;

		// A value changed by the directory selector takes precedence.
		if (req.isPartSet("select-for")
				&& req.isPartSet(LocalFileBrowserToadlet.selectDir)) {
			overriddenOption = req.getPartAsStringFailsafe("select-for",
					MAX_PARAM_VALUE_SIZE);
			overriddenValue = req.getPartAsStringFailsafe("filename",
					MAX_PARAM_VALUE_SIZE);
		}

		/*
		 * Present all other options for this subconfig.
		 */
		for (Option<?> o : subConfig.getOptions()) {
			if (!((!advancedModeEnabled) && o.isExpert())) {
				displayedConfigElements++;
				String configName = o.getName();
				String fullName = subConfig.getPrefix() + '.' + configName;
				String value = o.getValueDisplayString();

				if (value == null) {
					Logger.error(this, fullName
							+ "has returned null from config!);");
					continue;
				}

				ConfigCallback<?> callback = o.getCallback();

				final OptionType optionType;
				if (callback instanceof EnumerableOptionCallback) {
					optionType = OptionType.DROP_DOWN;
				} else if (callback instanceof BooleanCallback) {
					optionType = OptionType.BOOLEAN;
				} else if (callback instanceof ProgramDirectory.DirectoryCallback
						&& !callback.isReadOnly()) {
					optionType = OptionType.DIRECTORY;
				} else if (!callback.isReadOnly()) {
					optionType = OptionType.TEXT;
				} else /* if (callback.isReadOnly()) */{
					optionType = OptionType.TEXT_READ_ONLY;
				}

				// If ConfigToadlet is serving a plugin, ask the plugin to
				// translate the
				// config descriptions, otherwise use the node's BaseL10n
				// instance like
				// normal.
				HTMLNode shortDesc = o.getShortDescNode(plugin);
				HTMLNode longDesc = o.getLongDescNode(plugin);

				HTMLNode configItemNode = configGroupUlNode.addChild("li");
				String defaultValue;
				if (callback instanceof BooleanCallback) {
					// Only case where values are localised.
					defaultValue = l10n(o.getDefault());
				} else {
					defaultValue = o.getDefault();
				}

				configItemNode.addAttribute("class", optionType.cssClass);
				configItemNode
						.addChild("a", new String[] { "name", "id" },
								new String[] { configName, configName })
						.addChild(
								"span",
								new String[] { "class", "title", "style" },
								new String[] {
										"configshortdesc",
										NodeL10n.getBase().getString(
												"ConfigToadlet.defaultIs",
												new String[] { "default" },
												new String[] { defaultValue })
												+ (advancedModeEnabled ? " ["
														+ fullName + ']' : ""),
										"cursor: help;" }).addChild(shortDesc);
				HTMLNode configItemValueNode = configItemNode.addChild("span",
						"class", "config");

				// Values persisted through browser or backing down from
				// resetting to defaults
				// override the currently applied ones.
				if (req.isPartSet(fullName)) {
					value = req.getPartAsStringFailsafe(fullName,
							MAX_PARAM_VALUE_SIZE);
				}
				if (overriddenOption != null
						&& overriddenOption.equals(fullName))
					value = overriddenValue;
				switch (optionType) {
				case DROP_DOWN:
					configItemValueNode.addChild(addComboBox(value,
							(EnumerableOptionCallback) callback, fullName,
							callback.isReadOnly()));
					break;
				case BOOLEAN:
					configItemValueNode.addChild(addBooleanComboBox(
							Boolean.valueOf(value), fullName,
							callback.isReadOnly()));
					break;
				case DIRECTORY:
					configItemValueNode.addChild(addTextBox(value, fullName, o,
							false));
					configItemValueNode.addChild(
							"input",
							new String[] { "type", "name", "value" },
							new String[] {
									"submit",
									"select-directory." + fullName,
									NodeL10n.getBase().getString(
											"QueueToadlet.browseToChange") });
					break;
				case TEXT_READ_ONLY:
					configItemValueNode.addChild(addTextBox(value, fullName, o,
							true));
					break;
				case TEXT:
					configItemValueNode.addChild(addTextBox(value, fullName, o,
							false));
					break;
				}

				configItemNode.addChild("span", "class", "configlongdesc")
						.addChild(longDesc);
			}
		}

		if (displayedConfigElements > 0) {
			formNode.addChild(
					"div",
					"class",
					"configprefix",
					(plugin == null) ? l10n(subConfig.getPrefix()) : plugin
							.getString(subConfig.getPrefix()));
			formNode.addChild("a", "id", subConfig.getPrefix());
			formNode.addChild(configGroupUlNode);
		}

		formNode.addChild("input", new String[] { "type", "value" },
				new String[] { "submit", l10n("apply") });
		formNode.addChild("input", new String[] { "type", "value" },
				new String[] { "reset", l10n("undo") });
		formNode.addChild("input", new String[] { "type", "name", "value" },
				new String[] { "hidden", "subconfig", subConfig.getPrefix() });
		// 'Node' prefix options should not be reset to defaults as it is a,
		// quoting Toad, "very bad idea".
		// Options whose defaults are not wise to apply include the location of
		// the master keys file,
		// the Darknet port number, and the datastore size.
		if (!subConfig.getPrefix().equals("node")) {
			formNode.addChild("input",
					new String[] { "type", "name", "value" }, new String[] {
							"submit", "confirm-reset-to-defaults",
							l10n("resetToDefaults") });
		}

		this.writeHTMLReply(ctx, 200, "OK", pageNode.generate());
	}

	/**
	 * Generates a text box for the given setting suitable for adding to an
	 * existing form.
	 * 
	 * @param value
	 *            The current value of the option. It is displayed in the text
	 *            box.
	 * @param fullName
	 *            The full name of the option, used to name the text field.
	 * @param o
	 *            The option, used to add the short description as an "alt"
	 *            attribute.
	 * @param disabled
	 *            Whether the text box should be disabled.
	 * @return An input of type "text" and class "config" containing the current
	 *         value of the option.
	 */
	public static HTMLNode addTextBox(String value, String fullName,
			Option<?> o, boolean disabled) {
		HTMLNode result;

		if (disabled) {
			result = new HTMLNode("input", new String[] { "type", "class",
					"disabled", "alt", "name", "value" }, //
					new String[] { "text", "config", "disabled",
							o.getShortDesc(), fullName, value });
		} else {
			result = new HTMLNode("input", new String[] { "type", "class",
					"alt", "name", "value" }, //
					new String[] { "text", "config", o.getShortDesc(),
							fullName, value });
		}

		return result;
	}

	/**
	 * Generates a drop-down combobox for the given enumerable option suitable
	 * for adding to an existing form. Its first element is the "select"
	 * element, so any Javascript attributes can be added to the output.
	 * 
	 * @param value
	 *            The currently applied value of the option.
	 * @param o
	 *            The option, used to list all values.
	 * @param fullName
	 *            The full name of the option, used to name the drop-down.
	 * @param disabled
	 *            Whether the drop-down should be disabled.
	 * @return An HTMLNode of a "select" with "option" children for each of the
	 *         possible values. If the value specified in value is one of the
	 *         options, it will be selected.
	 */
	public static HTMLNode addComboBox(String value,
			EnumerableOptionCallback o, String fullName, boolean disabled) {
		HTMLNode result;

		if (disabled) {
			result = new HTMLNode("select", //
					new String[] { "name", "disabled" }, //
					new String[] { fullName, "disabled" });
		} else {
			result = new HTMLNode("select", "name", fullName);
		}

		for (String possibleValue : o.getPossibleValues()) {
			if (possibleValue.equals(value)) {
				result.addChild("option", new String[] { "value", "selected" },
						new String[] { possibleValue, "selected" },
						possibleValue);
			} else {
				result.addChild("option", "value", possibleValue, possibleValue);
			}
		}

		return result;
	}

	/**
	 * Generates a drop-down combobox for a true/false option suitable for
	 * adding to an existing form. Its first element is the "select" element, so
	 * any Javascript attributes can be added to the output.
	 * 
	 * @param value
	 *            The current value of the option. This will be selected.
	 * @param fullName
	 *            The full name of the option, used to name the drop-down.
	 * @param disabled
	 *            Whether the drop-down should be disabled.
	 * @return An HTMLNode of a "select" with an "option" child for localized
	 *         "true" and "false", with the current value selected.
	 */
	public static HTMLNode addBooleanComboBox(boolean value, String fullName,
			boolean disabled) {
		HTMLNode result;

		if (disabled) {
			result = new HTMLNode("select", //
					new String[] { "name", "disabled" }, //
					new String[] { fullName, "disabled" });
		} else {
			result = new HTMLNode("select", "name", fullName);
		}

		if (value) {
			result.addChild("option", new String[] { "value", "selected" },
					new String[] { "true", "selected" }, l10n("true"));
			result.addChild("option", "value", "false", l10n("false"));
		} else {
			result.addChild("option", "value", "true", l10n("true"));
			result.addChild("option", new String[] { "value", "selected" },
					new String[] { "false", "selected" }, l10n("false"));
		}

		return result;
	}

	@Override
	public String path() {
		return "/config/" + subConfig.getPrefix();
	}

	@Override
	public boolean isEnabled(ToadletContext ctx) {
		Option<?>[] o = subConfig.getOptions();
		if (ctx.isAdvancedModeEnabled())
			return true;
		for (Option<?> option : o)
			if (!option.isExpert())
				return true;
		return false;
	}
}
