package freenet.clients.http.wizardsteps;

import freenet.clients.http.FirstTimeWizardToadlet;
import freenet.config.Config;
import freenet.config.ConfigException;
import freenet.config.Option;
import freenet.l10n.NodeL10n;
import freenet.node.Node;
import freenet.node.NodeClientCore;
import freenet.node.NodeStarter;
import freenet.support.Fields;
import freenet.support.HTMLNode;
import freenet.support.Logger;
import freenet.support.SizeUtil;
import freenet.support.api.HTTPRequest;
import freenet.support.io.FileUtil;

/**
 * Allows the user to select datastore size, considering available storage space when offering options.
 */
public class DATASTORE_SIZE implements Step {

	private final NodeClientCore core;
	private final Config config;

	public DATASTORE_SIZE(NodeClientCore core, Config config) {
		this.config = config;
		this.core = core;
	}

	@Override
	public void getStep(HTTPRequest request, PageHelper helper) {
		HTMLNode contentNode = helper.getPageContent(WizardL10n.l10n("step4Title"));
		HTMLNode bandwidthInfoboxContent = helper.getInfobox("infobox-header", WizardL10n.l10n("datastoreSize"),
		        contentNode, null, false);

		bandwidthInfoboxContent.addChild("#", WizardL10n.l10n("datastoreSizeLong"));
		HTMLNode bandwidthForm = helper.addFormChild(bandwidthInfoboxContent, ".", "dsForm");
		HTMLNode result = bandwidthForm.addChild("select", "name", "ds");

		long maxSize = maxDatastoreSize();

		long autodetectedSize = canAutoconfigureDatastoreSize();
		if(maxSize < autodetectedSize) autodetectedSize = maxSize;

		@SuppressWarnings("unchecked")
		Option<Long> sizeOption = (Option<Long>) config.get("node").getOption("storeSize");
		if(!sizeOption.isDefault()) {
			long current = sizeOption.getValue();
			result.addChild("option",
			        new String[] { "value", "selected" },
			        new String[] { SizeUtil.formatSize(current), "on" }, WizardL10n.l10n("currentPrefix")+" "+SizeUtil.formatSize(current));
		} else if(autodetectedSize != -1) {
			result.addChild("option",
			        new String[] { "value", "selected" },
			        new String[] { SizeUtil.formatSize(autodetectedSize), "on" }, SizeUtil.formatSize(autodetectedSize));
		}
		if(autodetectedSize != 512*1024*1024) {
			result.addChild("option", "value", "512M", "512 MiB");
		}
		// We always allow at least 1GB
		result.addChild("option", "value", "1G", "1 GiB");
		if(maxSize >= 2l*1024*1024*1024) {
			if(autodetectedSize != -1 || !sizeOption.isDefault()) {
				result.addChild("option", "value", "2G", "2 GiB");
			} else {
				result.addChild("option",
				        new String[] { "value", "selected" },
				        new String[] { "2G", "on" }, "2GiB");
			}
		}
		if(maxSize >= 3l*1024*1024*1024) result.addChild("option", "value", "3G", "3 GiB");
		if(maxSize >= 5l*1024*1024*1024) result.addChild("option", "value", "5G", "5 GiB");
		if(maxSize >= 10l*1024*1024*1024) result.addChild("option", "value", "10G", "10 GiB");
		if(maxSize >= 20l*1024*1024*1024) result.addChild("option", "value", "20G", "20 GiB");
		if(maxSize >= 50l*1024*1024*1024) result.addChild("option", "value", "50G", "50 GiB");
		if(maxSize >= 200l*1024*1024*1024) result.addChild("option", "value", "200G", "200GiB");
		if(maxSize >= 500l*1024*1024*1024) result.addChild("option", "value", "500G", "500GiB");

		//Put buttons below dropdown.
		HTMLNode below = bandwidthForm.addChild("div");
		below.addChild("input",
		        new String[] { "type", "name", "value" },
		        new String[] { "submit", "back", NodeL10n.getBase().getString("Toadlet.back")});
		below.addChild("input",
		        new String[] { "type", "name", "value" },
		        new String[] { "submit", "next", NodeL10n.getBase().getString("Toadlet.next")});
	}

	@Override
	public String postStep(HTTPRequest request) {
		// drop down options may be 6 chars or less, but formatted ones e.g. old value if re-running can be more
		_setDatastoreSize(request.getPartAsStringFailsafe("ds", 20));
		return FirstTimeWizardToadlet.WIZARD_STEP.BANDWIDTH.name();
	}

	private void _setDatastoreSize(String selectedStoreSize) {
		try {
			long size = Fields.parseLong(selectedStoreSize);
			// client cache: 10% up to 200MB
			long clientCacheSize = Math.min(size / 10, 200*1024*1024);
			// recent requests cache / slashdot cache / ULPR cache
			int upstreamLimit = config.get("node").getInt("outputBandwidthLimit");
			int downstreamLimit = config.get("node").getInt("inputBandwidthLimit");
			// is used for remote stuff, so go by the minimum of the two
			int limit;
			if(downstreamLimit <= 0) limit = upstreamLimit;
			else limit = Math.min(downstreamLimit, upstreamLimit);
			// 35KB/sec limit has been seen to have 0.5 store writes per second.
			// So saying we want to have space to cache everything is only doubling that ...
			// OTOH most stuff is at low enough HTL to go to the datastore and thus not to
			// the slashdot cache, so we could probably cut this significantly...
			long lifetime = config.get("node").getLong("slashdotCacheLifetime");
			long maxSlashdotCacheSize = (lifetime / 1000) * limit;
			long slashdotCacheSize = Math.min(size / 10, maxSlashdotCacheSize);

			long storeSize = size - (clientCacheSize + slashdotCacheSize);

			System.out.println("Setting datastore size to "+Fields.longToString(storeSize, true));
			config.get("node").set("storeSize", Fields.longToString(storeSize, true));
			if(config.get("node").getString("storeType").equals("ram"))
				config.get("node").set("storeType", "salt-hash");
			System.out.println("Setting client cache size to "+Fields.longToString(clientCacheSize, true));
			config.get("node").set("clientCacheSize", Fields.longToString(clientCacheSize, true));
			if(config.get("node").getString("clientCacheType").equals("ram"))
				config.get("node").set("clientCacheType", "salt-hash");
			System.out.println("Setting slashdot/ULPR/recent requests cache size to "+Fields.longToString(slashdotCacheSize, true));
			config.get("node").set("slashdotCacheSize", Fields.longToString(slashdotCacheSize, true));


			Logger.normal(this, "The storeSize has been set to " + selectedStoreSize);
		} catch(ConfigException e) {
			Logger.error(this, "Should not happen, please report!" + e, e);
		}
	}

	private long maxDatastoreSize() {
		long maxMemory = NodeStarter.getMemoryLimitBytes();
		if(maxMemory == Long.MAX_VALUE) return 1024*1024*1024; // Treat as don't know.
		if(maxMemory < 128*1024*1024) return 1024*1024*1024; // 1GB default if don't know or very small memory.
		// Don't use the first 100MB for slot filters.
		long available = maxMemory - 100*1024*1024;
		// Don't use more than 50% of available memory for slot filters.
		available = available / 2;
		// Slot filters are 4 bytes per slot.
		long slots = available / 4;
		// There are 3 types of keys. We want the number of { SSK, CHK, pubkey } i.e. the number of slots in each store.
		slots /= 3;
		// We return the total size, so we don't need to worry about cache vs store or even client cache.
		// One key of all 3 types combined uses Node.sizePerKey bytes on disk. So we get a size.
		return slots * Node.sizePerKey;
	}

    private long canAutoconfigureDatastoreSize() {
        if (!config.get("node").getOption("storeSize").isDefault())
            return -1;

        long freeSpace = core.node.getStoreDir().getUsableSpace();

        if (freeSpace <= 0) {
            return -1;
        } else {
            long shortSize;
            long oneGiB = 1024 * 1024 * 1024L;
            // Maximum for Freenet: 256GB. That's a 128MiB bloom filter.
            long bloomFilter128MiBMax = 256 * oneGiB;
            // Maximum to suggest to keep Disk I/O managable. This
            // value might need revisiting when hardware or
            // filesystems change.
            long diskIoMax = 20 * oneGiB;

            // Choose a suggested store size based on available free space.
            if (freeSpace > 50 * oneGiB) {
                // > 50 GiB: Use 10% free space; minimum 10 GiB. Limited by
                // bloom filters and disk I/O.
                shortSize = Math.max(10 * oneGiB,
                                     Math.min(freeSpace / 10,
                                              Math.min(diskIoMax,
                                                       bloomFilter128MiBMax)));
            } else if (freeSpace > 5 * oneGiB) {
                // > 5 GiB: Use 20% free space, minimum 2 GiB.
                shortSize = Math.max(freeSpace / 5, 2 * oneGiB);
            } else if (freeSpace > 2 * oneGiB) {
                // > 2 GiB: 512 MiB.
                shortSize = 512 * (1024 * 1024);
            } else {
                // <= 2 GiB: 256 MiB.
                shortSize = 256 * (1024 * 1024);
            }

            return shortSize;
        }
    }
}
