/* 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.io.PrintWriter;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.EnumMap;

import freenet.client.HighLevelSimpleClient;
import freenet.clients.http.wizardsteps.BANDWIDTH;
import freenet.clients.http.wizardsteps.BANDWIDTH_MONTHLY;
import freenet.clients.http.wizardsteps.BANDWIDTH_RATE;
import freenet.clients.http.wizardsteps.BROWSER_WARNING;
import freenet.clients.http.wizardsteps.DATASTORE_SIZE;
import freenet.clients.http.wizardsteps.MISC;
import freenet.clients.http.wizardsteps.NAME_SELECTION;
import freenet.clients.http.wizardsteps.OPENNET;
import freenet.clients.http.wizardsteps.PageHelper;
import freenet.clients.http.wizardsteps.PersistFields;
import freenet.clients.http.wizardsteps.SECURITY_NETWORK;
import freenet.clients.http.wizardsteps.SECURITY_PHYSICAL;
import freenet.clients.http.wizardsteps.Step;
import freenet.clients.http.wizardsteps.WELCOME;
import freenet.config.Config;
import freenet.config.SubConfig;
import freenet.l10n.NodeL10n;
import freenet.node.Node;
import freenet.node.NodeClientCore;
import freenet.node.SecurityLevels;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.api.BooleanCallback;
import freenet.support.api.HTTPRequest;

/**
 * A first time wizard aimed to ease the configuration of the node.
 */
public class FirstTimeWizardToadlet extends Toadlet {
	private final NodeClientCore core;
	private final EnumMap<WIZARD_STEP, Step> steps;
	private final MISC stepMISC;
	private final SECURITY_NETWORK stepSECURITY_NETWORK;
	private final SECURITY_PHYSICAL stepSECURITY_PHYSICAL;

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

	public enum WIZARD_STEP {
		WELCOME,
		BROWSER_WARNING,
		MISC,
		OPENNET,
		SECURITY_NETWORK,
		SECURITY_PHYSICAL,
		NAME_SELECTION,
		DATASTORE_SIZE,
		BANDWIDTH,
		BANDWIDTH_MONTHLY,
		BANDWIDTH_RATE,
		COMPLETE //Redirects to front page
	}

	public enum WIZARD_PRESET {
		LOW,
		HIGH
	}

	FirstTimeWizardToadlet(HighLevelSimpleClient client, Node node, NodeClientCore core) {
		//Generic Toadlet-related initialization.
		super(client);
		this.core = core;
		Config config = node.getConfig();

		addWizardConfiguration(config);

		//Add step handlers that aren't set by presets
		steps = new EnumMap<WIZARD_STEP, Step>(WIZARD_STEP.class);
		steps.put(WIZARD_STEP.WELCOME, new WELCOME(config));
		steps.put(WIZARD_STEP.BROWSER_WARNING, new BROWSER_WARNING());
		steps.put(WIZARD_STEP.NAME_SELECTION, new NAME_SELECTION(config));
		steps.put(WIZARD_STEP.DATASTORE_SIZE, new DATASTORE_SIZE(core, config));
		steps.put(WIZARD_STEP.OPENNET, new OPENNET());
		steps.put(WIZARD_STEP.BANDWIDTH, new BANDWIDTH());
		steps.put(WIZARD_STEP.BANDWIDTH_MONTHLY, new BANDWIDTH_MONTHLY(core, config));
		steps.put(WIZARD_STEP.BANDWIDTH_RATE, new BANDWIDTH_RATE(core, config));

		//Add step handlers that are set by presets
		stepMISC = new MISC(core, config);
		steps.put(WIZARD_STEP.MISC, stepMISC);

		stepSECURITY_NETWORK = new SECURITY_NETWORK(core);
		steps.put(WIZARD_STEP.SECURITY_NETWORK, stepSECURITY_NETWORK);

		stepSECURITY_PHYSICAL = new SECURITY_PHYSICAL(core);
		steps.put(WIZARD_STEP.SECURITY_PHYSICAL, stepSECURITY_PHYSICAL);
	}

	public static final String TOADLET_URL = "/wizard/";

	private void addWizardConfiguration(Config configuration) {
		SubConfig wizardConfiguration = new SubConfig("firstTimeWizard", configuration);
		wizardConfiguration.register("loadUPnPPlugin", true, 0, true, false, "FirstTimeWizardToadlet.loadUPnPPlugin", "FirstTimeWizardToadlet.loadUPnPPluginLong", createLoadUPnPPluginCallback());
		wizardConfiguration.register("enableAutoUpdater", true, 1, true, false, "FirstTimeWizardToadlet.enableAutoUpdater", "FirstTimeWizardToadlet.enableAutoUpdaterLong", createEnableAutoUpdaterCallback());
		loadUPnPPlugin = wizardConfiguration.getBoolean("loadUPnPPlugin");
		enableAutoUpdater = wizardConfiguration.getBoolean("enableAutoUpdater");
		wizardConfiguration.finishedInitialization();
	}

	private boolean loadUPnPPlugin;

	private BooleanCallback createLoadUPnPPluginCallback() {
		return new BooleanCallback() {
			@Override
			public Boolean get() {
				return loadUPnPPlugin;
			}

			@Override
			public void set(Boolean value) {
				loadUPnPPlugin = value;
			}
		};
	}

	private boolean enableAutoUpdater;

	private BooleanCallback createEnableAutoUpdaterCallback() {
		return new BooleanCallback() {
			@Override
			public Boolean get() {
				return enableAutoUpdater;
			}

			@Override
			public void set(Boolean value) {
				enableAutoUpdater = value;
			}
		};
	}

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

		//Read the current step from the URL parameter, defaulting to the welcome page if unset or invalid..
		WIZARD_STEP currentStep;
		try {
			currentStep = WIZARD_STEP.valueOf(request.getParam("step", WIZARD_STEP.WELCOME.toString()));
		} catch (IllegalArgumentException e) {
			currentStep = WIZARD_STEP.WELCOME;
		}

		PersistFields persistFields = new PersistFields(request);

		//Skip the browser warning page if using Chrome in incognito mode
		if (currentStep == WIZARD_STEP.BROWSER_WARNING &&
				request.isChrome() && request.isIncognito()) {
			super.writeTemporaryRedirect(ctx, "Skipping unneeded warning",
			        persistFields.appendTo(TOADLET_URL+"?step=MISC"));
			return;
		} else if (currentStep == WIZARD_STEP.MISC && persistFields.isUsingPreset()) {
			/*If using a preset, skip the miscellaneous page as both high and low security set those settings.
			 * This overrides the persistence fields.*/
			StringBuilder redirectBase = new StringBuilder(TOADLET_URL+"?step=");
			if (persistFields.preset == WIZARD_PRESET.HIGH) {
				redirectBase.append("SECURITY_NETWORK&preset=HIGH&confirm=true&opennet=false&security-levels.networkThreatLevel=HIGH");
			} else /*if (persistFields.preset == WIZARD_PRESET.LOW)*/ {
				redirectBase.append("DATASTORE_SIZE&preset=LOW&opennet=true");
			}
			//addPersistFields() is not used here because the fields are overridden.
			super.writeTemporaryRedirect(ctx, "Skipping to next necessary step", redirectBase.toString());
			return;
		} else if (currentStep == WIZARD_STEP.SECURITY_NETWORK && !request.isParameterSet("opennet")) {
			//If opennet isn't defined when attempting to set network security level, ask again.
			super.writeTemporaryRedirect(ctx, "Need opennet choice",
			        persistFields.appendTo(TOADLET_URL+"?step=OPENNET"));
			return;
		} else if (currentStep == WIZARD_STEP.NAME_SELECTION && core.getNode().isOpennetEnabled()) {
			//Skip node name selection if not in darknet mode.
			super.writeTemporaryRedirect(ctx, "Skip name selection",
			        persistFields.appendTo(stepURL(WIZARD_STEP.DATASTORE_SIZE.name())));
			return;
		} else if (currentStep == WIZARD_STEP.COMPLETE) {
			super.writeTemporaryRedirect(ctx, "Wizard complete", WelcomeToadlet.PATH);
			return;
		}

		Step getStep = steps.get(currentStep);
		PageHelper helper = new PageHelper(ctx, persistFields, currentStep);
		getStep.getStep(request, helper);
		writeHTMLReply(ctx, 200, "OK", helper.getPageOuter().generate());
	}

	/**
	 * @return whether wizard steps should log minor events.
	 */
	public static boolean shouldLogMinor() {
		return logMINOR;
	}

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

		WIZARD_STEP currentStep;
		try {
			//Attempt to parse the current step, defaulting to WELCOME if unspecified or invalid.
			String currentValue = request.getPartAsStringFailsafe("step", 20);
			currentStep = currentValue.isEmpty() ? WIZARD_STEP.WELCOME : WIZARD_STEP.valueOf(currentValue);
		} catch (IllegalArgumentException e) {
			//Failed to parse enum value, default to welcome.
			//TODO: Should this be an error page instead?
			currentStep = WIZARD_STEP.WELCOME;
		}

		PersistFields persistFields = new PersistFields(request);
		String redirectTarget;

		if (currentStep.equals(WIZARD_STEP.WELCOME) &&
		        (request.isPartSet("presetLow") || request.isPartSet("presetHigh") || request.isPartSet("presetNone"))) {

			/*Apply presets and UPnP is enabled first to allow it time to load (and thus enable
			  autodetection) before hitting the bandwidth page. This also effectively sets the preset field.*/
			StringBuilder redirectTo = new StringBuilder(TOADLET_URL+"?step=BROWSER_WARNING&incognito=");
			redirectTo.append(request.getPartAsStringFailsafe("incognito", 5));

			//Translate button name to preset value on the query string.
			if (request.isPartSet("presetLow")) {
				//Low security preset
				stepMISC.setUPnP(loadUPnPPlugin);
				stepMISC.setAutoUpdate(enableAutoUpdater);
				redirectTo.append("&preset=LOW&opennet=true");
				stepSECURITY_NETWORK.setThreatLevel(SecurityLevels.NETWORK_THREAT_LEVEL.LOW);
				stepSECURITY_PHYSICAL.setThreatLevel(SecurityLevels.PHYSICAL_THREAT_LEVEL.NORMAL,
				        stepSECURITY_PHYSICAL.getCurrentLevel());
			} else if (request.isPartSet("presetHigh")) {
				//High security preset
				stepMISC.setUPnP(loadUPnPPlugin);
				stepMISC.setAutoUpdate(enableAutoUpdater);
				redirectTo.append("&preset=HIGH&opennet=false");
			}

			super.writeTemporaryRedirect(ctx, "Wizard set preset", redirectTo.toString());
			return;
		} else if (request.isPartSet("back")) {
			//User chose back, return to previous page, or cancel if single step.
			if (request.isPartSet("singlestep")) {
				redirectTarget = WIZARD_STEP.COMPLETE.name();
			} else {
				redirectTarget = getPreviousStep(currentStep, persistFields.preset).name();
			}
		} else {
			try {
				redirectTarget = steps.get(currentStep).postStep(request);
				//Go to complete rather than go to next if single step.
				if (request.isPartSet("singlestep") && !redirectTarget.startsWith(currentStep.name())) {
					redirectTarget = WIZARD_STEP.COMPLETE.name();
				}

				//Opennet step can change the persisted value for opennet.
				if (currentStep == WIZARD_STEP.OPENNET) {
					try {
						HTTPRequest newRequest = new HTTPRequestImpl(new URI(
						        stepURL(redirectTarget)), "GET");
						//Only continue if a value for opennet has been selected.
						if (newRequest.isPartSet("opennet")) {
							redirectTarget = WIZARD_STEP.SECURITY_NETWORK.name();
							persistFields = new PersistFields(persistFields.preset, newRequest);
						}
					} catch (URISyntaxException e) {
						Logger.error(this, "Unexpected invalid query string from OPENNET step! "+e, e);
						redirectTarget = WIZARD_STEP.WELCOME.name();
					}
				}
			} catch (IOException e) {
				String title;
				if (e.getMessage().equals("cantWriteNewMasterKeysFile")) {
					//Recognized as being unable to write to the master keys file.
					title = NodeL10n.getBase().getString("SecurityLevels.cantWriteNewMasterKeysFileTitle");
				} else {
					//Some other error.
					title = NodeL10n.getBase().getString("Toadlet.internalErrorPleaseReport");
				}

				//Very loud error message, with descriptive title and header if possible.
				StringBuilder msg = new StringBuilder("<html><head><title>").append(title).
				        append("</title></head><body><h1>").append(title).append("</h1><pre>");

				//Print stack trace.
				StringWriter sw = new StringWriter();
				PrintWriter pw = new PrintWriter(sw);
				e.printStackTrace(pw);
				pw.flush();
				msg.append(sw.toString()).append("</pre>");

				//Include internal exception if one exists.
				Throwable internal = e.getCause();
				if (internal != null) {
					msg.append("<h1>").
					        append(NodeL10n.getBase().getString("Toadlet.internalErrorPleaseReport")).
					        append("</h1>").append("<pre>");

					sw = new StringWriter();
					pw = new PrintWriter(sw);
					internal.printStackTrace(pw);
					pw.flush();
					msg.append(sw.toString()).append("</pre>");
				}
				msg.append("</body></html>");
				writeHTMLReply(ctx, 500, "Internal Error", msg.toString());
				return;
			}
		}
		super.writeTemporaryRedirect(ctx, "Wizard redirect", stepURL(persistFields.appendTo(redirectTarget)));
	}

	private String stepURL(String step) {
		return TOADLET_URL+"?step="+step;
	}

	//FIXME: There really has to be a better way to find the previous step, but with an enum there's no decrement.
	//FIXME: Would a set work better than an enum?
	public static WIZARD_STEP getPreviousStep(WIZARD_STEP currentStep, WIZARD_PRESET preset) {

		//Might be obvious, but still: No breaks needed in cases because their only contents are returns.

		//First pages for the presets
		if (preset == WIZARD_PRESET.HIGH) {
			switch (currentStep) {
				case SECURITY_NETWORK:
				case SECURITY_PHYSICAL:
					//Go back to the beginning from the warning or the physical security page.
					return WIZARD_STEP.WELCOME;
				default:
					//do nothing
			}
		} else  if (preset == WIZARD_PRESET.LOW) {
			switch (currentStep) {
				case DATASTORE_SIZE:
					//Go back to the beginning from the datastore page.
					return WIZARD_STEP.WELCOME;
				default:
					//do nothing
			}
		}

		//Otherwise normal order.
		switch (currentStep) {
			case MISC:
			case BROWSER_WARNING:
				return WIZARD_STEP.WELCOME;
			case OPENNET:
				return WIZARD_STEP.MISC;
			case SECURITY_NETWORK:
				return WIZARD_STEP.OPENNET;
			case SECURITY_PHYSICAL:
				return WIZARD_STEP.SECURITY_NETWORK;
			case NAME_SELECTION:
				return WIZARD_STEP.SECURITY_PHYSICAL;
			case DATASTORE_SIZE:
				return WIZARD_STEP.NAME_SELECTION;
			case BANDWIDTH:
				return WIZARD_STEP.DATASTORE_SIZE;
			case BANDWIDTH_MONTHLY:
			case BANDWIDTH_RATE:
				return WIZARD_STEP.BANDWIDTH;
			default:
				//do nothing
		}

		//Should be matched by this point, unknown step.
		return WIZARD_STEP.WELCOME;
	}

	@Override
	public String path() {
		return TOADLET_URL;
	}
}
