package freenet.pluginmanager;

import static java.util.Collections.unmodifiableCollection;

import java.net.MalformedURLException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import freenet.keys.FreenetURI;
import freenet.node.updater.NodeUpdater;
import freenet.node.updater.PluginJarUpdater;

/**
 * Container for Freenet’s official plugins.
 *
 * FIXME: Connectivity essential plugins shouldn't have their minimum version increased!
 * @see https://bugs.freenetproject.org/view.php?id=6600
 *
 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
 */
public class OfficialPlugins {

	private final Map<String, OfficialPluginDescription> officialPlugins = new HashMap<String, OfficialPluginDescription>();

	public OfficialPlugins() {
		try {
			addPlugin("Freemail")
					.inGroup("communication")
					.minimumVersion(15)
					.usesXml()
					.loadedFrom("CHK@6dfMgGf7YEfJhF0W~K0HUv0fnbuRwYH6iMqrLIbTI7k,huYBf8oBevwW6lRQnz-0jDP1dl5ej7FKeyVZ3CnH0Ec,AAMC--8/Freemail.jar")
					.deprecated();
			addPlugin("Freemail_wot")
					.inGroup("communication")
					.minimumVersion(27)
					.loadedFrom("CHK@fR~OePzegkidZcKQN3aX-2EmS8RPo6Qj3hNTKf~BmJA,Mb7laJ5Mz0vvnEIhYr6yZbAytgdhQeXxJDA2LL0ziZU,AAMC--8/Freemail-v0.2.7.2.jar");
			addPlugin("HelloWorld")
					.inGroup("example")
					.loadedFrom("CHK@ZdTXnWV-ikkt25-y8jmhlHjCY-nikDMQwcYlWHww5eg,Usq3uRHpHuIRmMRRlNQE7BNveO1NwNI7oNKdb7cowFM,AAIC--8/HelloWorld.jar")
					.advanced();
			addPlugin("HelloFCP")
					.inGroup("example")
					.loadedFrom("CHK@0gtXJpw1QUJCmFOhoPRNqhsNbMtVw1CGVe46FUv7-e0,X8QqhtPkHoaFCUd89bgNaKxX1AV0WNBVf3sRgSF51-g,AAIC--8/HelloFCP.jar")
					.advanced();
			addPlugin("JSTUN")
					.inGroup("connectivity")
					.essential()
					.minimumVersion(2)
					.loadedFrom("CHK@Zgib8xrGxcEuix7AVB4eajton1FpNHbIJeQZgEbHMNU,BQekU261VLSDUBQPOHSMKUF5qxY1v0zjXa33RyoEbYk,AAMC--8/JSTUN.jar");
			addPlugin("KeyUtils")
					.inGroup("technical")
					.minimumVersion(5026)
					.loadedFrom("CHK@VGVmYp8qGseh2S7irYSDS7f6Goy0Z7rZcOc-iiQVPXg,Q4VeVf0fhdfejLqXrSYY5TK76f37D34Ly3Npv5X1GMA,AAMC--8/KeyUtils-v5026.jar")
					.advanced();
			addPlugin("MDNSDiscovery")
					.inGroup("connectivity")
					.minimumVersion(2)
					.loadedFrom("CHK@wPyhY61bsDM3OW6arFlxYX8~mBKjo~XtOTIAbT0dk88,Vr3MTAzkW5J28SJs2dTxkj6D4GVNm3u8GFsxJgzTL1M,AAIC--8/MDNSDiscovery.jar");
			addPlugin("SNMP")
					.inGroup("connectivity")
					.loadedFrom("CHK@EykJIv83UE291zONVzfXqyJYX5t66uCQJHkzQrB61MI,-npuolPZj1fcAWane2~qzRNEjKDERx52aQ5bC6NBQgw,AAIC--8/SNMP.jar")
					.advanced();
			addPlugin("TestGallery")
					.inGroup("example")
					.minimumVersion(1)
					.loadedFrom("CHK@LfJVh1EkCr4ry0yDW74vwxkX-3nkr~ztW2z0SUZHfC0,-mz7l39dC6n0RTUiSokjC~pUDO7PWZ89miYesKH0-WA,AAIC--8/TestGallery.jar")
					.experimental();
			addPlugin("ThawIndexBrowser")
					.inGroup("file-transfer")
					.minimumVersion(6)
					.usesXml()
					.loadedFrom("CHK@9bjNQtl7ndPKh~gi4woH0Xvb7uRunJ81deIlXwGE6qg,clwp0Bhx2LZxt2XCWeARqv24tBNmjlhXDZtwAJpzlIc,AAMC--8/ThawIndexBrowser-v6.jar");
			addPlugin("UPnP")
					.inGroup("connectivity")
					.essential()
                    .recommendedVersion(10007)
					.minimumVersion(10003)
					.loadedFrom(
                        "CHK@ZiX8yeMHTUtNfJAgxpwH~jLRnnbb41BKEkAxOD~33tY,aBTvD3IoPKPLjnHOCNQ4-iRwqVED5kHgkmD4UhGdITk,AAMC--8/UPnP-10007.jar");
			addPlugin("XMLLibrarian")
					.inGroup("index")
					.minimumVersion(26)
					.usesXml()
					.loadedFrom("CHK@TvjyCaG1dx0xIBSJkXSKA1ZT4I~NkRKeQqwC0a0bhFM,JiQe4CRjF1RwhQRFFQzP-ih9t2i0peV0tBCfJAeFCdk,AAIC--8/XMLLibrarian.jar")
					.unsupported();
			addPlugin("XMLSpider")
					.inGroup("index")
					.minimumVersion(48)
					.usesXml()
					.loadedFrom("CHK@ne-aaLuzVZLcHj0YmrclaCXJqxsSb7q-J0eYEiL9V9o,v0EdgDGBhTE9k6GsB44UrQ4ADUq5LCUVknLaE4iSEBk,AAMC--8/XMLSpider.jar")
					.unsupported();
			addPlugin("Freereader")
					.inGroup("index")
					.minimumVersion(6)
					.usesXml()
					.loadedFrom("CHK@SjXgPC5IEZa2g7a6gIKmNuxEKN4~eWrPhIwsznmGV-8,QUxm9R3sp3mNwhEHhL8mlx9zbOhIqIyR93tu7jD~0EU,AAMC--8/Freereader-6.jar");
			addPlugin("Library")
					.inGroup("index")
					.recommendedVersion(37)
					.minimumVersion(36)
					.usesXml()
					.loadedFrom("CHK@qh6MyHn0umAm5luL3Ak1dJxj39vGJeiLp6KdtPTng08,PRwIA7m8bhMsbhMR8M6wWmd4iH~op3ImjsdCW1XBGmQ,AAMC--8/Library-v37.jar");
			addPlugin("Spider")
					.inGroup("index")
					.minimumVersion(52)
					.loadedFrom("CHK@94gCPJEkEXq6Zti4wxDrqr9e~geQS4B3kdIwl4TXzV8,NUlmfjeqja28Lim6m3kTuxGHRSNtQHsbRoIAilxdkJY,AAMC--8/Spider-v52.jar")
					.advanced();
			addPlugin("WebOfTrust")
					.inGroup("communication")
					.minimumVersion(18)
					.usesXml()
					.loadedFrom("CHK@qprIfrwqW0zg4izvRqStw0GUR2siJBe6LuDuy-t6D7A,eVd5fUG5nrNnIl7vwlRGzyhTe~TTZwuh6P1oqZH8g0A,AAMC--8/WebOfTrust-build0018.jar");
			addPlugin("WebOfTrustTesting")
					.inGroup("communication")
					.advanced()
					.experimental()
					.usesXml()
					.alwaysFetchLatestVersion()
					.minimumVersion(17) // When changing this also update edition of USK below!
					.loadedFrom("USK@QeTBVWTwBldfI-lrF~xf0nqFVDdQoSUghT~PvhyJ1NE,OjEywGD063La2H-IihD7iYtZm3rC0BP6UTvvwyF5Zh4,AQACAAE/WebOfTrustTesting.jar/17");
			addPlugin("FlogHelper")
					.inGroup("communication")
					.minimumVersion(35)
					.usesXml()
					.loadedFrom("CHK@3Ht6otYHogKxa1Z8SC0hz46kV9q1qaSBsjbJxuwPosU,txky3jF33oWU9n6YiGMphiHA1V9q3i4ZN0VPtgElSaI,AAMC--8/FlogHelper-v35.jar");
			addPlugin("Sharesite")
					.inGroup("communication")
					.recommendedVersion(4)
					.minimumVersion(2)
					.loadedFrom("CHK@MktEdCJqSCG~5LaI9f~3xoQjejZCEVraQlfzKRs-H3I,yI2mQDrN1omHqD6ZibXDTRqnCK~2ut4Q11p18J1~cGE,AAMC--8/Sharesite-0.4.4.jar");
		} catch (MalformedURLException mue1) {
			throw new RuntimeException("Could not create FreenetURI.", mue1);
		}
	}

	private OfficialPluginBuilder addPlugin(String name) {
		return new OfficialPluginBuilder(name);
	}

	public OfficialPluginDescription get(String name) {
		return officialPlugins.get(name);
	}

	public Collection<OfficialPluginDescription> getAll() {
		return unmodifiableCollection(officialPlugins.values());
	}

	private class OfficialPluginBuilder {

		private final String name;
		private String group;
		private boolean essential;
		private long minimumVersion = -1;
		private long recommendedVersion = -1;
		/** @see OfficialPluginDescription#alwaysFetchLatestVersion */
		private boolean alwaysFetchLatestVersion;
		private boolean usesXml;
		/** @see OfficialPluginDescription#uri */
		private FreenetURI uri;
		private boolean deprecated;
		private boolean experimental;
		private boolean advanced;
    private boolean unsupported;

		private OfficialPluginBuilder(String name) {
			this.name = name;
			addCurrentPluginDescription();
		}

		public OfficialPluginBuilder inGroup(String group) {
			this.group = group;
			addCurrentPluginDescription();
			return this;
		}

		public OfficialPluginBuilder essential() {
			essential = true;
			addCurrentPluginDescription();
			return this;
		}

		public OfficialPluginBuilder minimumVersion(int minimumVersion) {
			this.minimumVersion = minimumVersion;
			addCurrentPluginDescription();
			return this;
		}

		public OfficialPluginBuilder recommendedVersion(int recommendedVersion) {
			this.recommendedVersion = recommendedVersion;
			addCurrentPluginDescription();
			return this;
		}

		/** @see OfficialPluginDescription#alwaysFetchLatestVersion */
		public OfficialPluginBuilder alwaysFetchLatestVersion() {
			this.alwaysFetchLatestVersion = true;
			addCurrentPluginDescription();
			return this;
		}

		public OfficialPluginBuilder usesXml() {
			usesXml = true;
			addCurrentPluginDescription();
			return this;
		}

		/**
		 * ATTENTION: Please read {@link OfficialPluginDescription#uri} before deciding whether
		 * to use USK or CHK! */
		public OfficialPluginBuilder loadedFrom(String uri) throws MalformedURLException {
			this.uri = new FreenetURI(uri);
			addCurrentPluginDescription();
			return this;
		}

		public OfficialPluginBuilder deprecated() {
			deprecated = true;
			addCurrentPluginDescription();
			return this;
		}

		public OfficialPluginBuilder experimental() {
			experimental = true;
			addCurrentPluginDescription();
			return this;
		}

		public OfficialPluginBuilder advanced() {
			advanced = true;
			addCurrentPluginDescription();
			return this;
		}

    public OfficialPluginBuilder unsupported() {
      unsupported = true;
      addCurrentPluginDescription();
      return this;
    }

		private void addCurrentPluginDescription() {
            if(recommendedVersion == 0 && minimumVersion > 0)
                recommendedVersion = minimumVersion;
            if(minimumVersion == 0 && recommendedVersion > 0)
                minimumVersion = recommendedVersion;
			officialPlugins.put(name, createOfficialPluginDescription());
		}

		private OfficialPluginDescription createOfficialPluginDescription() {
			return new OfficialPluginDescription(name, group, essential, minimumVersion,
				recommendedVersion, alwaysFetchLatestVersion, usesXml, uri, deprecated,
				experimental, advanced, unsupported);
		}

	}

	public static class OfficialPluginDescription {

		/** The name of the plugin */
		public final String name;

		/**
		 * The group of the plugin. The group is a technical name that needs to
		 * be translated before it is shown to the user.
		 */
		public final String group;

		/**
		 * If true, we will download it, blocking, over HTTP, during startup (unless
		 * explicitly forbidden to use HTTP). If not, we will download it on a
		 * separate thread after startup. Both are assuming we don't have it in a
		 * file.
		 */
		public final boolean essential;
		/**
		 * Minimum getRealVersion(). If the plugin is older than this, we will fail
		 * the load.
		 */
		public final long minimumVersion;
		/**
		 * Recommended getRealVersion(). If the plugin is older than this, we will
		 * download the new version in the background, and either use it on restart,
		 * or offer the user the option to reload it. This is in fact identical to
		 * what happens on a USK-based update...
		 */
		public final long recommendedVersion;
		
		/**
		 * If true, if during startup we already have a copy of the plugin JAR on disk, the
		 * {@link PluginManager} will ignore it and redownload the JAR instead so the user gets a
		 * recent version if there is one.<br><br>
		 * 
		 * This is for being used together with plugins which are fetched from a USK {@link #uri},
		 * and which are not included in the official main Freenet update USK which
		 * {@link PluginJarUpdater} watches.<br>
		 * For plugins which are in the main Freenet update USK, setting this to true is usually
		 * not necessary: The {@link PluginJarUpdater} will update the plugin if there is a new
		 * version.<br><br>
		 * 
		 * In other words: Plugins which are NOT in the official USK but have their own USK will
		 * not have the {@link PluginJarUpdater} monitor their USK, it only monitors the main
		 * USK. Thus, the only chance to update them is during startup by ignoring the JAR and
		 * causing a re-download of it. */
		public final boolean alwaysFetchLatestVersion;
		
		/** Does it use XML? If so, if the JVM is vulnerable, then don't load it */
		public final boolean usesXML;
		/**
		 * FreenetURI to get the latest version from.<br>
		 * Typically a CHK, not USK, since updates are deployed using the main Freenet USK of
		 * {@link NodeUpdater}'s subclass {@link PluginJarUpdater}.<br><br>
		 * 
		 * To allow people to insert plugin updates without giving them write access to the main
		 * USK, this *can* be an USK, but updating when a new version is inserted to the USK will
		 * only happen at certain points in time:<br>
		 * - if the plugin is manually unloaded and loaded again.<br>
		 * - at restart of Freenet if {@link #alwaysFetchLatestVersion} is true. If it is false, the
		 *   cached local JAR file on disk will prevent updating!<br>
		 * So to make updating work using USK, set {@link #alwaysFetchLatestVersion} so we check
		 * for updates when the node is restarted.<br><br>
		 * 
		 * NOTICE the conclusion of the above: It is NOT RECOMMENDED to use USKs here: Updates will
		 * only be delivered at restarts of the node, while the main Freenet USK supports live
		 * updates; and also there is no revocation mechanism for the USKs. Instead of using USKs
		 * here, a CHK should be preferred, and new plugin versions then should be inserted at the
		 * main Freenet update USK of the the {@link NodeUpdater}. A typical usecase for
		 * nevertheless using an USK here is to allow individual plugin developers to push testing
		 * versions of their plugin on their own without giving them write-access to the main
		 * Freenet update USK.*/
		public final FreenetURI uri;
		/** If true, the plugin is obsolete. */
		public final boolean deprecated;
		/** If true, the plugin is experimental. */
		public final boolean experimental;
		/**
		 * If true, the plugin is geeky - it should not be shown except in advanced
		 * mode even though it's not deprecated nor is it experimental.
		 */
		public final boolean advanced;
    /**
     * If true, the plugin used to be official, but is no longer supported.
     * These are not shown even in advanced mode.
     */
    public final boolean unsupported;

		OfficialPluginDescription(String name, String group, boolean essential, long minVer,
				long recVer, boolean alwaysFetchLatestVersion, boolean usesXML, FreenetURI uri,
				boolean deprecated, boolean experimental, boolean advanced, boolean unsupported) {
			
			this.name = name;
			this.group = group;
			this.essential = essential;
			this.minimumVersion = minVer;
			this.recommendedVersion = recVer;
			this.alwaysFetchLatestVersion = alwaysFetchLatestVersion;
			this.usesXML = usesXML;
			this.deprecated = deprecated;
			this.experimental = experimental;
			this.advanced = advanced;
      this.unsupported = unsupported;

			if (alwaysFetchLatestVersion && uri != null) {
				assert(uri.isUSK()) : "Non-USK URIs do not support updates!";
				
				// Force fetching the latest edition by setting a negative USK edition.
				long edition = uri.getSuggestedEdition();
				if (edition >= 0) {
					edition = Math.min(-1, -edition);
				}
				uri = uri.setSuggestedEdition(edition);
			}
			
			this.uri = uri;
		}

		public String getLocalisedPluginName() {
			return PluginManager.getOfficialPluginLocalisedName(name);
		}

		public String getLocalisedPluginDescription() {
			return PluginManager.l10n("pluginDesc." + name);
		}

	}

}
