package freenet.node.updater;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Properties;

import freenet.client.FetchContext;
import freenet.client.FetchException;
import freenet.client.FetchException.FetchExceptionMode;
import freenet.client.FetchResult;
import freenet.client.async.ClientContext;
import freenet.client.async.ClientGetCallback;
import freenet.client.async.ClientGetter;
import freenet.client.events.ClientEvent;
import freenet.client.events.ClientEventListener;
import freenet.client.events.SplitfileProgressEvent;
import freenet.clients.fcp.FCPMessage;
import freenet.clients.fcp.ClientPut.COMPRESS_STATE;
import freenet.clients.http.QueueToadlet;
import freenet.keys.FreenetURI;
import freenet.l10n.NodeL10n;
import freenet.node.RequestClient;
import freenet.node.RequestStarter;
import freenet.node.Version;
import freenet.node.updater.MainJarDependenciesChecker.AtomicDeployer;
import freenet.node.updater.MainJarDependenciesChecker.Deployer;
import freenet.node.updater.MainJarDependenciesChecker.JarFetcher;
import freenet.node.updater.MainJarDependenciesChecker.JarFetcherCallback;
import freenet.node.updater.MainJarDependenciesChecker.MainJarDependencies;
import freenet.node.updater.UpdateOverMandatoryManager.UOMDependencyFetcher;
import freenet.node.updater.UpdateOverMandatoryManager.UOMDependencyFetcherCallback;
import freenet.node.useralerts.UserAlert;
import freenet.support.HTMLNode;
import freenet.support.Logger;
import freenet.support.io.Closer;
import freenet.support.io.FileBucket;
import freenet.support.io.FileUtil;
import freenet.support.io.InsufficientDiskSpaceException;

public class MainJarUpdater extends NodeUpdater implements Deployer {
	
	static private volatile boolean logMINOR;
	static {
		Logger.registerClass(MainJarUpdater.class);
	}
	
	private final FetchContext dependencyCtx;
	private final ClientContext clientContext;

	MainJarUpdater(NodeUpdateManager manager, FreenetURI URI, int current, int min, int max, String blobFilenamePrefix) {
		super(manager, URI, current, min, max, blobFilenamePrefix);
		dependencyCtx = core.makeClient((short) 0, true, false).getFetchContext();
		dependencyCtx.allowSplitfiles = true;
		dependencyCtx.dontEnterImplicitArchives = false;
		dependencyCtx.maxNonSplitfileRetries = -1;
		dependencyCtx.maxSplitfileBlockRetries = -1;
		clientContext = core.clientContext;
		dependencies = new MainJarDependenciesChecker(this, manager.node.executor);
	}

	private final MainJarDependenciesChecker dependencies;
	
	@Override
	public String jarName() {
		return "freenet.jar";
	}
	
	public void start() {
		maybeProcessOldBlob();
		super.start();
	}
	
	@Override
	protected void maybeParseManifest(FetchResult result, int build) {
		// Do nothing.
	}

	@Override
	protected void processSuccess(int fetched, FetchResult result, File blob) {
		manager.onDownloadedNewJar(result.asBucket(), fetched, blob);
		// NodeUpdateManager expects us to dependencies *AFTER* we tell it about the new jar.
		parseDependencies(result, fetched);
	}

	@Override
	protected void onStartFetching() {
		manager.onStartFetching();
	}
	
	// Dependency handling.
	
	private HashSet<DependencyJarFetcher> fetchers = new HashSet<DependencyJarFetcher>();
	private HashSet<DependencyJarFetcher> essentialFetchers = new HashSet<DependencyJarFetcher>();

	protected void parseDependencies(Properties props, int build) {
		synchronized(fetchers) {
			fetchers.clear();
		}
		MainJarDependencies deps = dependencies.handle(props, build);
		if(deps != null) {
			manager.onDependenciesReady(deps);
		}
	}

	@Override
	public void deploy(MainJarDependencies deps) {
		manager.onDependenciesReady(deps);
	}
	
	/** Glue code. */
	private class DependencyJarFetcher implements JarFetcher, ClientGetCallback, RequestClient, ClientEventListener {

		private final File filename;
		private final ClientGetter getter;
		private SplitfileProgressEvent lastProgress;
		private final JarFetcherCallback cb;
		private boolean fetched;
		private final byte[] expectedHash;
		private final long expectedLength;
		private final boolean essential;
		private final File tempFile;
		private UOMDependencyFetcher uomFetcher;
		private final boolean executable;
		
		DependencyJarFetcher(File filename, FreenetURI chk, long expectedLength, byte[] expectedHash, JarFetcherCallback cb, boolean essential, boolean executable) throws FetchException {
			FetchContext myCtx = new FetchContext(dependencyCtx, FetchContext.IDENTICAL_MASK);
			File parent = filename.getParentFile();
			if(parent == null) parent = new File(".");
			try {
				tempFile = File.createTempFile(filename.getName(), NodeUpdateManager.TEMP_FILE_SUFFIX, parent);
			} catch (InsufficientDiskSpaceException e) {
			    throw new FetchException(FetchExceptionMode.NOT_ENOUGH_DISK_SPACE);
			} catch (IOException e) {
				throw new FetchException(FetchExceptionMode.BUCKET_ERROR, "Cannot create temp file for "+filename+" in "+parent+" - disk full? permissions problem?");
			}
			getter = new ClientGetter(this,  
					chk, myCtx, RequestStarter.IMMEDIATE_SPLITFILE_PRIORITY_CLASS,
					new FileBucket(tempFile, false, false, false, false), null, null);
			myCtx.eventProducer.addEventListener(this);
			this.cb = cb;
			this.filename = filename;
			this.expectedHash = expectedHash;
			this.expectedLength = expectedLength;
			this.essential = essential;
			this.executable = executable;
		}
		
		@Override
		public void cancel() {
			final UOMDependencyFetcher f;
			synchronized(this) {
				fetched = true;
				f = uomFetcher;
			}
			MainJarUpdater.this.node.executor.execute(new Runnable() {

				@Override
				public void run() {
					getter.cancel(clientContext);
					if(f != null) f.cancel();
				}
				
			});
		}
		
		@Override
		public boolean persistent() {
			return false;
		}
		
		@Override
		public boolean realTimeFlag() {
			return false;
		}
		
		@Override
		public void onSuccess(FetchResult result, ClientGetter state) {
			synchronized(this) {
				if(fetched) {
					tempFile.delete();
					return;
				}
				fetched = true;
			}
            if(!MainJarDependenciesChecker.validFile(tempFile, expectedHash, expectedLength, executable)) {
                Logger.error(this, "Unable to download dependency "+filename+" : not the expected size or hash!");
                System.err.println("Download of "+filename+" for update failed because temp file appears to be corrupted!");
                if(cb != null)
                    cb.onFailure(new FetchException(FetchExceptionMode.BUCKET_ERROR, "Downloaded jar from Freenet but failed consistency check: "+tempFile+" length "+tempFile.length()+" "));
                tempFile.delete();
                return;
            }
			if(!FileUtil.renameTo(tempFile, filename)) {
				Logger.error(this, "Unable to rename temp file "+tempFile+" to "+filename);
				System.err.println("Download of "+filename+" for update failed because cannot rename from "+tempFile);
				if(cb != null)
				    cb.onFailure(new FetchException(FetchExceptionMode.BUCKET_ERROR, "Unable to rename temp file "+tempFile+" to "+filename));
                tempFile.delete();
				return;
			}
			if(cb != null) cb.onSuccess();
		}
		
		@Override
		public void onFailure(FetchException e, ClientGetter state) {
			tempFile.delete();
			synchronized(this) {
				if(fetched) return;
			}
			if(cb != null) cb.onFailure(e);
		}
		
		@Override
		public synchronized void receive(ClientEvent ce, ClientContext context) {
			if(ce instanceof SplitfileProgressEvent)
				lastProgress = (SplitfileProgressEvent) ce;
		}
		
		private void start() throws FetchException {
			getter.start(clientContext);
		}

		public synchronized HTMLNode renderRow() {
			HTMLNode row = new HTMLNode("tr");
			row.addChild("td").addChild("p", filename.toString());
			
			if(uomFetcher != null)
				row.addChild("td").addChild("#", l10n("fetchingFromUOM"));
			else if(lastProgress == null)
				row.addChild(QueueToadlet.createProgressCell(false, true, COMPRESS_STATE.WORKING, 0, 0, 0, 0, 0, false, false));
			else
				row.addChild(QueueToadlet.createProgressCell(false, 
						true, COMPRESS_STATE.WORKING, lastProgress.succeedBlocks, lastProgress.failedBlocks, lastProgress.fatallyFailedBlocks, lastProgress.minSuccessfulBlocks, lastProgress.totalBlocks, lastProgress.finalizedTotal, false));
			return row;
		}

		public void fetchFromUOM() {
			synchronized(this) {
				if(fetched) return;
				if(!essential) return;
			}
			UOMDependencyFetcher f = manager.uom.fetchDependency(expectedHash, expectedLength, filename, executable,
					new UOMDependencyFetcherCallback() {

						@Override
						public void onSuccess() {
							synchronized(DependencyJarFetcher.this) {
								if(fetched) return;
								fetched = true;
							}
							if(cb != null) cb.onSuccess();
						}
						
			});
			synchronized(this) {
				if(uomFetcher != null) {
					Logger.error(this, "Started UOMFetcher twice for "+filename, new Exception("error"));
					return;
				}
				uomFetcher = f;
			}
		}

	    @Override
	    public void onResume(ClientContext context) {
	        // Do nothing. Not persistent.
	    }

        @Override
        public RequestClient getRequestClient() {
            return this;
        }

	}
	
	@Override
	public JarFetcher fetch(FreenetURI uri, File downloadTo,
			long expectedLength, byte[] expectedHash, JarFetcherCallback cb, int build, boolean essential, boolean executable) throws FetchException {
		if(essential)
			System.out.println("Fetching "+downloadTo+" needed for new Freenet update "+build);
		else if(build != 0) // build 0 means it's a preload or a multi-file update.
			System.out.println("Preloading "+downloadTo+" needed for new Freenet update "+build);
		if(logMINOR) Logger.minor(this, "Fetching "+uri+" to "+downloadTo+" for next update");
		DependencyJarFetcher fetcher = new DependencyJarFetcher(downloadTo, uri, expectedLength, expectedHash, cb, essential, executable);
		synchronized(fetchers) {
			fetchers.add(fetcher);
			if(essential)
				essentialFetchers.add(fetcher);
		}
		fetcher.start();
		if(manager.uom.fetchingUOM()) {
			if(essential)
				fetcher.fetchFromUOM();
		}
		return fetcher;
	}
	
	public void onStartFetchingUOM() {
		DependencyJarFetcher[] f;
		synchronized(fetchers) {
			f = fetchers.toArray(new DependencyJarFetcher[fetchers.size()]);
		}
		for(DependencyJarFetcher fetcher : f)
			fetcher.fetchFromUOM();
	}
	
	public void renderProperties(HTMLNode alertNode) {
		synchronized(fetchers) {
			if(!fetchers.isEmpty()) {
				alertNode.addChild("p", l10n("fetchingDependencies")+":");
				HTMLNode table = alertNode.addChild("table");
				for(DependencyJarFetcher f : fetchers) {
					table.addChild(f.renderRow());
				}
			}
		}
	}

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

	public boolean brokenDependencies() {
		return dependencies.isBroken();
	}

	public void cleanupDependencies() {
		InputStream is = getClass().getResourceAsStream("/"+DEPENDENCIES_FILE);
		if(is == null) {
			System.err.println("Can't find dependencies file. Other nodes will not be able to use Update Over Mandatory through this one.");
			return;
		}
		Properties props = new Properties();
		try {
			props.load(is);
		} catch (IOException e) {
			System.err.println("Can't read dependencies file. Other nodes will not be able to use Update Over Mandatory through this one.");
			return;
		} finally {
			Closer.close(is);
		}
		dependencies.cleanup(props, this, Version.buildNumber());
	}

	@Override
	public void addDependency(byte[] expectedHash, File filename) {
		manager.uom.addDependency(expectedHash, filename);
	}

    @Override
    public void reannounce() {
        this.manager.broadcastUOMAnnouncesNew();
        this.manager.broadcastUOMAnnouncesOld();
    }

    @Override
    public void multiFileReplaceReadyToDeploy(final MainJarDependenciesChecker.AtomicDeployer atomicDeployer) {
        if(this.manager.isAutoUpdateAllowed()) {
            atomicDeployer.deployMultiFileUpdateOffThread();
        } else {
            final long now = System.currentTimeMillis();
            System.err.println("Not deploying multi-file update for "+atomicDeployer.name+" because auto-update is not enabled.");
            node.clientCore.alerts.register(new UserAlert() {

                private String l10n(String key) {
                    return NodeL10n.getBase().getString("MainJarUpdater.ConfirmMultiFileUpdater."+key);
                }
                
                @Override
                public boolean userCanDismiss() {
                    return true;
                }

                @Override
                public String getTitle() {
                    return l10n("title."+atomicDeployer.name);
                }

                @Override
                public String getText() {
                    return l10n("text."+atomicDeployer.name);
                }

                @Override
                public HTMLNode getHTMLText() {
                    return new HTMLNode("p", getText());
                    // FIXME separate button, then the alert could be dismissable? Only useful if it's permanently dismissable though, which means a config setting as well...
                }

                @Override
                public String getShortText() {
                    return getTitle();
                }

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

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

                @Override
                public void isValid(boolean validity) {
                    // Ignore
                }

                @Override
                public String dismissButtonText() {
                    return NodeL10n.getBase().getDefaultString("UpdatedVersionAvailableUserAlert.updateNowButton");
                }

                @Override
                public boolean shouldUnregisterOnDismiss() {
                    return true;
                }

                @Override
                public void onDismiss() {
                    atomicDeployer.deployMultiFileUpdateOffThread();
                }

                @Override
                public String anchor() {
                    return "multi-file-update-confirm-"+atomicDeployer.name;
                }

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

                @Override
                public FCPMessage getFCPMessage() {
                    return null;
                }

                @Override
                public long getUpdatedTime() {
                    return now;
                }
                
            });
        }
    }

}
