package freenet.node.updater;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import freenet.client.FetchResult;
import freenet.clients.http.PproxyToadlet;
import freenet.keys.FreenetURI;
import freenet.l10n.NodeL10n;
import freenet.node.RequestClient;
import freenet.node.Version;
import freenet.node.useralerts.AbstractUserAlert;
import freenet.node.useralerts.UserAlert;
import freenet.pluginmanager.PluginInfoWrapper;
import freenet.pluginmanager.PluginManager;
import freenet.support.HTMLNode;
import freenet.support.Logger;
import freenet.support.api.Bucket;
import freenet.support.io.BucketTools;

public class PluginJarUpdater extends NodeUpdater {

	final String pluginName;
	final PluginManager pluginManager;
	private UserAlert alert;
	private boolean deployOnNoRevocation;
	private boolean deployOnNextNoRevocation;
	private boolean readyToDeploy;
	private FetchResult result;
	
	private final Object writeJarSync = new Object();
	private int writtenVersion;

	/**
	 * @return True if the caller should restart the revocation checker.
	 */
	boolean onNoRevocation() {
		synchronized(this) {
			if(!readyToDeploy) return false;
			if(deployOnNoRevocation) {
				// Lets go ...
			} else if(deployOnNextNoRevocation) {
				deployOnNoRevocation = true;
				deployOnNextNoRevocation = false;
				System.out.println("Deploying "+pluginName+" after next revocation check");
				return true;
			} else
				return false;
		}
		// Deploy it!
		if (!pluginManager.isPluginLoaded(pluginName)) {
			Logger.error(this, "Plugin is not loaded, so not deploying: "+pluginName);
			tempBlobFile.delete();
			return false;
		}
		System.out.println("Deploying new version of "+pluginName+" : unloading old version...");
		// Write the new version of the plugin before shutting down, so if there is a deadlock in terminate, we will still get the new version after a restart.
		try {
			writeJar();
		} catch (IOException e) {
			Logger.error(this, "Cannot deploy: "+e, e);
			System.err.println("Cannot deploy new version of "+pluginName+" : "+e);
			e.printStackTrace();
			return false; // Not much we can do ...
			// FIXME post a useralert
		}
		pluginManager.killPluginByFilename(pluginName, Integer.MAX_VALUE, true);
		pluginManager.startPluginAuto(pluginName, true);
		UserAlert a;
		synchronized(this) {
			a = alert;
			alert = null;
		}
		node.getClientCore().getAlerts().unregister(a);
		return false;
	}
	
	PluginJarUpdater(NodeUpdateManager manager, FreenetURI URI, int current, int min, int max, String blobFilenamePrefix, String pluginName, PluginManager pm, boolean autoDeployOnRestart) {
		super(manager, URI, current, min, max, blobFilenamePrefix);
		this.pluginName = pluginName;
		this.pluginManager = pm;
	}

	@Override
	public String jarName() {
		return pluginName;
	}

	private int requiredNodeVersion;
	
	private static final String REQUIRED_NODE_VERSION_PREFIX = "Required-Node-Version: ";
	
	@Override
	protected void maybeParseManifest(FetchResult result, int build) {
		requiredNodeVersion = -1;
		parseManifest(result);
		if(requiredNodeVersion != -1) {
			System.err.println("Required node version for plugin "+pluginName+": "+requiredNodeVersion);
			Logger.normal(this, "Required node version for plugin "+pluginName+": "+requiredNodeVersion);
		}
	}
	
	@Override
	protected void parseManifestLine(String line) {
		if(line.startsWith(REQUIRED_NODE_VERSION_PREFIX)) {
			requiredNodeVersion = Integer.parseInt(line.substring(REQUIRED_NODE_VERSION_PREFIX.length()));
		}
	}
	
	@Override
	protected void onStartFetching() {
		System.err.println("Starting to fetch plugin "+pluginName);
	}

	@Override
	protected void processSuccess(int build, FetchResult result, File blob) {
		Bucket oldResult = null;
		synchronized(this) {
			if(requiredNodeVersion > Version.buildNumber()) {
				System.err.println("Found version "+fetchedVersion+" of "+pluginName+" but needs node version "+requiredNodeVersion);
				// FIXME deploy it with the main jar
				tempBlobFile.delete();
				return;
			}
			if(this.result != null)
				oldResult = this.result.asBucket();
			this.result = result;
		}
		if(oldResult != null) oldResult.free();
		
		PluginInfoWrapper loaded = pluginManager.getPluginInfo(pluginName);
		
		if(loaded == null) {
			if(!node.getPluginManager().isPluginLoadedOrLoadingOrWantLoad(pluginName)) {
				System.err.println("Don't want plugin: "+pluginName);
				Logger.error(this, "Don't want plugin: "+pluginName);
				tempBlobFile.delete();
				return;
			}
		}
		
		if(loaded.getPluginLongVersion() >= fetchedVersion) {
			tempBlobFile.delete();
			return;
		}
		// Create a useralert to ask the user to deploy the new version.
		
		UserAlert toRegister = null;
		synchronized(this) {
			readyToDeploy = true;
			if(alert != null) return;
			
			toRegister = alert = new AbstractUserAlert(true, l10n("pluginUpdatedTitle", "name", pluginName), l10n("pluginUpdatedText", "name", pluginName), l10n("pluginUpdatedShortText", "name", pluginName), null, UserAlert.ERROR, true, NodeL10n.getBase().getString("UserAlert.hide"), true, this) {
				
				@Override
				public void onDismiss() {
					synchronized(PluginJarUpdater.this) {
						alert = null;
					}
				}
				
				@Override
				public HTMLNode getHTMLText() {
					HTMLNode div = new HTMLNode("div");
					// Text saying the plugin has been updated...
					synchronized(this) {
					
						if(deployOnNoRevocation || deployOnNextNoRevocation) {
							div.addChild("#", l10n("willDeployAfterRevocationCheck", "name", pluginName));
						} else {
							div.addChild("#", l10n("pluginUpdatedText", new String[] { "name", "newVersion" }, new String[] { pluginName, Long.toString(fetchedVersion) }));
							
							// Form to deploy the updated version.
							// This is not the same as reloading because we haven't written it yet.
							
							HTMLNode formNode = div.addChild("form", new String[] { "action", "method" }, new String[] { PproxyToadlet.PATH, "post" });
							formNode.addChild("input", new String[] { "type", "name", "value" }, new String[] { "hidden", "formPassword", node.getClientCore().getFormPassword() });
							formNode.addChild("input", new String[] { "type", "name", "value" }, new String[] { "hidden", "update", pluginName });
							formNode.addChild("input", new String[] { "type", "value" }, new String[] { "submit", l10n("updatePlugin") });
						}
					}
					return div;
				}
			};
		}
		if(toRegister != null)
			node.getClientCore().getAlerts().register(toRegister);
	}

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

	private String l10n(String key, String name, String value) {
		return NodeL10n.getBase().getString("PluginJarUpdater."+key, name, value);
	}

	private String l10n(String key, String[] names, String[] values) {
		return NodeL10n.getBase().getString("PluginJarUpdater."+key, names, values);
	}
	
	public void writeJarTo(FetchResult result, File fNew) throws IOException {
		int fetched;
		synchronized(this) {
			fetched = fetchedVersion;
		}
		synchronized(writeJarSync) {
			if (!fNew.delete() && fNew.exists()) {
				System.err.println("Can't delete " + fNew + "!");
			}

			FileOutputStream fos;
			fos = new FileOutputStream(fNew);

			BucketTools.copyTo(result.asBucket(), fos, -1);

			fos.flush();
			fos.close();
		}
		synchronized(this) {
			writtenVersion = fetched;
		}
		System.err.println("Written " + jarName() + " to " + fNew);
	}

	void writeJar() throws IOException {
		writeJarTo(result, pluginManager.getPluginFilename(pluginName));
		UserAlert a;
		synchronized(this) {
			a = alert;
			alert = null;
		}
		if(a != null)
			node.getClientCore().getAlerts().unregister(a);
	}

	@Override
	void kill() {
		super.kill();
		UserAlert a;
		synchronized(this) {
			a = alert;
			alert = null;
		}
		if(a != null)
			node.getClientCore().getAlerts().unregister(a);
	}

	public synchronized void arm(boolean wasRunning) {
		if(wasRunning) {
			deployOnNextNoRevocation = true;
			System.out.println("Deploying "+pluginName+" after next but one revocation check");
		} else {
			deployOnNoRevocation = true;
			System.out.println("Deploying "+pluginName+" after next revocation check");
		}
	}

	@Override
	public RequestClient getRequestClient() {
		return pluginManager.getSingleUpdaterRequestClient();
	}
	
}
