/* 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.fcp;

import java.io.File;
import java.io.FileNotFoundException;
import java.net.MalformedURLException;
import java.util.Date;
import java.util.HashMap;

import freenet.client.DefaultMIMETypes;
import freenet.client.FetchException;
import freenet.client.FetchResult;
import freenet.client.InsertContext;
import freenet.client.InsertException;
import freenet.client.InsertException.InsertExceptionMode;
import freenet.client.async.BaseClientPutter;
import freenet.client.async.ClientContext;
import freenet.client.async.ClientGetter;
import freenet.client.async.ClientRequester;
import freenet.client.async.ContainerInserter;
import freenet.client.async.DefaultManifestPutter;
import freenet.client.async.ManifestPutter;
import freenet.client.async.TooManyFilesInsertException;
import freenet.clients.fcp.RequestIdentifier.RequestType;
import freenet.keys.FreenetURI;
import freenet.node.NodeClientCore;
import freenet.support.CurrentTimeUTC;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.api.ManifestElement;
import freenet.support.io.FileBucket;
import freenet.support.io.ResumeFailedException;

public class ClientPutDir extends ClientPutBase {

    private static final long serialVersionUID = 1L;
    private HashMap<String, Object> manifestElements;
	private ManifestPutter putter;
	private final String defaultName;
	private final long totalSize;
	private final int numberOfFiles;
	private final boolean wasDiskPut;
	
	private static volatile boolean logMINOR;
	private final byte[] overrideSplitfileCryptoKey;
	
	static {
		Logger.registerLogThresholdCallback(new LogThresholdCallback() {
			
			@Override
			public void shouldUpdate() {
				logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
			}
		});
	}

	public ClientPutDir(FCPConnectionHandler handler, ClientPutDirMessage message, 
			HashMap<String, Object> manifestElements, boolean wasDiskPut, FCPServer server) throws IdentifierCollisionException, MalformedURLException, TooManyFilesInsertException {
		super(checkEmptySSK(message.uri, message.targetFilename != null ? message.targetFilename : "site", server.core.clientContext), message.identifier, message.verbosity, null,
				handler, message.priorityClass, message.persistence, message.clientToken,
				message.global, message.getCHKOnly, message.dontCompress, message.localRequestOnly, message.maxRetries, message.earlyEncode, message.canWriteClientCache, message.forkOnCacheable, message.compressorDescriptor, message.extraInsertsSingleBlock, message.extraInsertsSplitfileHeaderBlock, message.realTimeFlag, message.compatibilityMode, message.ignoreUSKDatehints, server);
		logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
		this.wasDiskPut = wasDiskPut;
		this.overrideSplitfileCryptoKey = message.overrideSplitfileCryptoKey;
		
		// objectOnNew is called once, objectOnUpdate is never called, yet manifestElements get blanked anyway!
		
		this.manifestElements = new HashMap<String,Object>();
		this.manifestElements.putAll(manifestElements);
		
//		this.manifestElements = manifestElements;
		
//		this.manifestElements = new HashMap<String, Object>();
//		this.manifestElements.putAll(manifestElements);
		this.defaultName = message.defaultName;
		makePutter(server.core.clientContext);
		if(putter != null) {
			numberOfFiles = putter.countFiles();
			totalSize = putter.totalSize();
		} else {
			numberOfFiles = -1;
			totalSize = -1;
		}
		if(logMINOR) Logger.minor(this, "Putting dir "+identifier+" : "+priorityClass);
	}

	/**
	 * Fproxy
	*	Puts a disk dir
	 * @throws TooManyFilesInsertException 
	 * @throws InsertException 
	*/
	public ClientPutDir(PersistentRequestClient client, FreenetURI uri, String identifier, int verbosity, short priorityClass, Persistence persistence, String clientToken, boolean getCHKOnly, boolean dontCompress, int maxRetries, File dir, String defaultName, boolean allowUnreadableFiles, boolean includeHiddenFiles, boolean global, boolean earlyEncode, boolean canWriteClientCache, boolean forkOnCacheable, int extraInsertsSingleBlock, int extraInsertsSplitfileHeaderBlock, boolean realTimeFlag, byte[] overrideSplitfileCryptoKey, NodeClientCore core) throws FileNotFoundException, IdentifierCollisionException, MalformedURLException, TooManyFilesInsertException {
		super(checkEmptySSK(uri, "site", core.clientContext), identifier, verbosity , null, null, client, priorityClass, persistence, clientToken, global, getCHKOnly, dontCompress, maxRetries, earlyEncode, canWriteClientCache, forkOnCacheable, false, extraInsertsSingleBlock, extraInsertsSplitfileHeaderBlock, realTimeFlag, null, InsertContext.CompatibilityMode.COMPAT_DEFAULT, false/*XXX ignoreUSKDatehints*/, core);
		wasDiskPut = true;
		this.overrideSplitfileCryptoKey = overrideSplitfileCryptoKey;
		logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
		this.manifestElements = makeDiskDirManifest(dir, "", allowUnreadableFiles, includeHiddenFiles);
		this.defaultName = defaultName;
		makePutter(core.clientContext);
		if(putter != null) {
			numberOfFiles = putter.countFiles();
			totalSize = putter.totalSize();
		} else {
			numberOfFiles = -1;
			totalSize = -1;
		}
		if(logMINOR) Logger.minor(this, "Putting dir "+identifier+" : "+priorityClass);
	}

	public ClientPutDir(PersistentRequestClient client, FreenetURI uri, String identifier, int verbosity, short priorityClass, Persistence persistence, String clientToken, boolean getCHKOnly, boolean dontCompress, int maxRetries, HashMap<String, Object> elements, String defaultName, boolean global, boolean earlyEncode, boolean canWriteClientCache, boolean forkOnCacheable, int extraInsertsSingleBlock, int extraInsertsSplitfileHeaderBlock, boolean realTimeFlag, byte[] overrideSplitfileCryptoKey, NodeClientCore core) throws IdentifierCollisionException, MalformedURLException, TooManyFilesInsertException {
		super(checkEmptySSK(uri, "site", core.clientContext), identifier, verbosity , null, null, client, priorityClass, persistence, clientToken, global, getCHKOnly, dontCompress, maxRetries, earlyEncode, canWriteClientCache, forkOnCacheable, false, extraInsertsSingleBlock, extraInsertsSplitfileHeaderBlock, realTimeFlag, null, InsertContext.CompatibilityMode.COMPAT_DEFAULT, false/*XXX ignoreUSKDatehints*/, core);
		wasDiskPut = false;
		this.overrideSplitfileCryptoKey = overrideSplitfileCryptoKey;
		logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
		this.manifestElements = elements;
		this.defaultName = defaultName;
		makePutter(core.clientContext);
		if(putter != null) {
			numberOfFiles = putter.countFiles();
			totalSize = putter.totalSize();
		} else {
			numberOfFiles = -1;
			totalSize = -1;
		}
		if(logMINOR) Logger.minor(this, "Putting data from custom buckets "+identifier+" : "+priorityClass);
	}
	
	protected ClientPutDir() {
	    // For serialization.
	    defaultName = null;
	    totalSize = 0;
	    numberOfFiles = 0;
	    wasDiskPut = false;
	    overrideSplitfileCryptoKey = null;
	}

	@Override
	void register(boolean noTags) throws IdentifierCollisionException {
		if(persistence != Persistence.CONNECTION)
			client.register(this);
		if(persistence != Persistence.CONNECTION && !noTags) {
			FCPMessage msg = persistentTagMessage();
			client.queueClientRequestMessage(msg, 0);
		}
	}
	
	private HashMap<String, Object> makeDiskDirManifest(File dir, String prefix, boolean allowUnreadableFiles, boolean includeHiddenFiles) throws FileNotFoundException {

		HashMap<String, Object> map = new HashMap<String, Object>();
		File[] files = dir.listFiles();
		
		if(files == null)
			throw new IllegalArgumentException("No such directory");

		for (File f : files) {
			
    		if(f.isHidden() && !includeHiddenFiles) continue;

			if (f.exists() && f.canRead()) {
				if(f.isFile()) {
					FileBucket bucket = new FileBucket(f, true, false, false, false);
					if(logMINOR)
						Logger.minor(this, "Add file : " + f.getAbsolutePath());
					
					map.put(f.getName(), new ManifestElement(f.getName(), prefix + f.getName(), bucket, DefaultMIMETypes.guessMIMEType(f.getName(), true), f.length()));
				} else if(f.isDirectory()) {
					if(logMINOR)
						Logger.minor(this, "Add dir : " + f.getAbsolutePath());
					
					map.put(f.getName(), makeDiskDirManifest(f, prefix + f.getName() + "/", allowUnreadableFiles, includeHiddenFiles));
				} else {
					if(!allowUnreadableFiles)
						throw new FileNotFoundException("Not a file and not a directory : " + f);
				}
			} else if (!allowUnreadableFiles)
				throw new FileNotFoundException("The file does not exist or is unreadable : " + f);
			
		}

		return map;
	}
	
	private void makePutter(ClientContext context) throws TooManyFilesInsertException {
	    putter = new DefaultManifestPutter(this,
	            manifestElements, priorityClass, uri, defaultName, ctx,
	            persistence == Persistence.FOREVER, overrideSplitfileCryptoKey, context);
	}

	@Override
	public void start(ClientContext context) {
		if(finished) return;
		if(started) return;
		try {
			if(putter != null)
				putter.start(context);

			started = true;
			if(client != null) {
				RequestStatusCache cache = client.getRequestStatusCache();
				if(cache != null) {
					cache.updateStarted(identifier, true);
				}
			}
			if(logMINOR) Logger.minor(this, "Started "+putter+" for "+this+" persistence="+persistence);
			if(persistence != Persistence.CONNECTION && !finished) {
				FCPMessage msg = persistentTagMessage();
				client.queueClientRequestMessage(msg, 0);
			}
		} catch (InsertException e) {
			started = true;
			onFailure(e, null);
		}
	}
	
	@Override
	public void onLostConnection(ClientContext context) {
		if(persistence == Persistence.CONNECTION)
			cancel(context);
		// otherwise ignore
	}
	
	@Override
	protected void freeData() {
		if(logMINOR) Logger.minor(this, "freeData() on "+this+" persistence type = "+persistence);
		synchronized(this) {
			if(manifestElements == null) {
				if(logMINOR)
					Logger.minor(this, "manifestElements = "+manifestElements, new Exception("error"));
				return;
			}
		}
		if(logMINOR) Logger.minor(this, "freeData() more on "+this+" persistence type = "+persistence);
		// We have to commit everything, so activating everything here doesn't cost us much memory...?
		freeData(manifestElements);
		manifestElements = null;
	}
	
	@SuppressWarnings("unchecked")
	private void freeData(HashMap<String, Object> manifestElements) {
		if(logMINOR) Logger.minor(this, "freeData() inner on "+this+" persistence type = "+persistence+" size = "+manifestElements.size());
		for(Object o: manifestElements.values()) {
			if(o instanceof HashMap) {
				freeData((HashMap<String, Object>) o);
			} else {
				ManifestElement e = (ManifestElement) o;
				if(logMINOR) Logger.minor(this, "Freeing "+e);
				e.freeData();
			}
		}
	}

	@Override
	protected ClientRequester getClientRequest() {
		return putter;
	}

	@Override
	protected FCPMessage persistentTagMessage() {
		// FIXME: remove debug code
		if (lowLevelClient == null)
			Logger.error(this, "lowLevelClient == null", new Exception("error"));
		if (putter == null)
			Logger.error(this, "putter == null", new Exception("error"));
		// FIXME end
		return new PersistentPutDir(identifier, publicURI, uri, verbosity, priorityClass,
				persistence, global, defaultName, manifestElements, clientToken, started, ctx.maxInsertRetries, ctx.dontCompress, ctx.compressorDescriptor, wasDiskPut, isRealTime(), putter != null ? putter.getSplitfileCryptoKey() : null, this.ctx.getCompatibilityMode());
	}
	
	private boolean isRealTime() {
		// FIXME: remove debug code
		if (lowLevelClient == null) {
			// This can happen but only due to data corruption - old databases on which various bugs have resulted in it getting deleted, and also possibly failed deletions.
			Logger.error(this, "lowLevelClient == null", new Exception("error"));
			return false;
		}
		return lowLevelClient.realTimeFlag();
	}

	@Override
	protected String getTypeName() {
		return "PUTDIR";
	}

	@Override
	public boolean hasSucceeded() {
		return succeeded;
	}

	public FreenetURI getFinalURI() {
		return generatedURI;
	}

	public int getNumberOfFiles() {
		return numberOfFiles;
	}

	public long getTotalDataSize() {
		return totalSize;
	}

	@Override
	public boolean canRestart() {
		if(!finished) {
			Logger.minor(this, "Cannot restart because not finished for "+identifier);
			return false;
		}
		if(succeeded) {
			Logger.minor(this, "Cannot restart because succeeded for "+identifier);
			return false;
		}
		return true;
	}

	@Override
	public boolean restart(ClientContext context, final boolean disableFilterData) {
		if(!canRestart()) return false;
		setVarsRestart();
		if(client != null) {
			RequestStatusCache cache = client.getRequestStatusCache();
			if(cache != null) {
				cache.updateStarted(identifier, false);
			}
		}
		try {
			makePutter(context);
		} catch (TooManyFilesInsertException e) {
			this.onFailure(new InsertException(e), null);
		}
		start(context);
		return true;
	}

	public void onFailure(FetchException e, ClientGetter state) {}

	public void onSuccess(FetchResult result, ClientGetter state) {}
	
	@Override
	public void onSuccess(BaseClientPutter state) {
		super.onSuccess(state);
	}
	
	@Override
	public void onFailure(InsertException e, BaseClientPutter state) {
		super.onFailure(e, state);
	}

	@Override
	public void requestWasRemoved(ClientContext context) {
		if(persistence == Persistence.FOREVER) {
			putter = null;
		}
		super.requestWasRemoved(context);
	}

	@Override
	protected void onStartCompressing() {
		// Ignore
	}

	@Override
	protected void onStopCompressing() {
		// Ignore
	}
	
	@Override
	RequestStatus getStatus() {
		FreenetURI finalURI = getFinalURI();
		InsertExceptionMode failureCode = null;
		String failureReasonShort = null;
		String failureReasonLong = null;
		if(putFailedMessage != null) {
			failureCode = putFailedMessage.code;
			failureReasonShort = putFailedMessage.getShortFailedMessage();
			failureReasonShort = putFailedMessage.getLongFailedMessage();
		}
		
		int total=0, min=0, fetched=0, fatal=0, failed=0;
		// See ClientRequester.getLatestSuccess() for why this defaults to current time.
		Date latestSuccess = CurrentTimeUTC.get();
		Date latestFailure = null;
		boolean totalFinalized = false;
		
		if(progressMessage != null) {
			if(progressMessage instanceof SimpleProgressMessage) {
				SimpleProgressMessage msg = (SimpleProgressMessage)progressMessage;
				total = (int) msg.getTotalBlocks();
				min = (int) msg.getMinBlocks();
				fetched = (int) msg.getFetchedBlocks();
				latestSuccess = msg.getLatestSuccess();
				fatal = (int) msg.getFatalyFailedBlocks();
				failed = (int) msg.getFailedBlocks();
				latestFailure = msg.getLatestFailure();
				totalFinalized = msg.isTotalFinalized();
			}
		}
		
		return new UploadDirRequestStatus(
		    identifier, persistence, started, finished, succeeded, total, min, fetched,
		    latestSuccess, fatal, failed, latestFailure, totalFinalized, priorityClass, finalURI,
		    uri, failureCode, failureReasonShort, failureReasonLong, totalSize, numberOfFiles);
	}
	
	@Override
	public void innerResume(ClientContext context) throws ResumeFailedException {
	    ContainerInserter.resumeMetadata(manifestElements, context);
	}

    @Override
    RequestType getType() {
        return RequestType.PUTDIR;
    }

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

}
