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

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.net.MalformedURLException;
import java.util.List;

import freenet.client.ArchiveContext;
import freenet.client.ClientMetadata;
import freenet.client.FetchContext;
import freenet.client.FetchException;
import freenet.client.FetchException.FetchExceptionMode;
import freenet.client.FetchResult;
import freenet.client.InsertContext.CompatibilityMode;
import freenet.crypt.HashResult;
import freenet.keys.FreenetURI;
import freenet.keys.USK;
import freenet.node.PrioRunnable;
import freenet.node.RequestClient;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.api.Bucket;
import freenet.support.compress.Compressor;
import freenet.support.compress.DecompressorThreadManager;
import freenet.support.io.Closer;
import freenet.support.io.InsufficientDiskSpaceException;
import freenet.support.Logger.LogLevel;
import freenet.support.io.NativeThread;

/**
 * Poll a USK, and when a new slot is found, fetch it. 
 */
public class USKRetriever extends BaseClientGetter implements USKCallback {

	/** Context for fetching data */
	final FetchContext ctx;
	final USKRetrieverCallback cb;
	final USK origUSK;
	// In wierd 
	/** The USKCallback that is actually subscribed. This is used when we may
	 * be going through a USKSparseProxyCallback. */
	private USKCallback proxy;
	/** Alternatively, we may be driving a USKFetcher directly. This happens when
	 * the client subscribes with a custom FetchContext. */
	private USKFetcher fetcher;

        private static volatile boolean logMINOR;
	static {
		Logger.registerLogThresholdCallback(new LogThresholdCallback(){
			@Override
			public void shouldUpdate(){
				logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
			}
		});
	}

	public USKRetriever(FetchContext fctx, short prio,  
			final RequestClient client, USKRetrieverCallback cb, USK origUSK) {
		super(prio, new ClientBaseCallback() {
            @Override
            public void onResume(ClientContext context) {
                throw new IllegalStateException();
            }
            @Override
            public RequestClient getRequestClient() {
                return client;
            }
		});
		if(client.persistent()) throw new UnsupportedOperationException("USKRetriever cannot be persistent");
		this.ctx = fctx;
		this.cb = cb;
		this.origUSK = origUSK;
		this.proxy = this;
	}

	@Override
	public void onFoundEdition(long l, USK key, ClientContext context, boolean metadata, short codec, byte[] data, boolean newKnownGood, boolean newSlotToo) {
		if(l < 0) {
			Logger.error(this, "Found negative edition: "+l+" for "+key+" !!!");
			return;
		}
		if(l < origUSK.suggestedEdition) {
			Logger.warning(this, "Found edition prior to that specified by the client: "+l+" < "+origUSK.suggestedEdition);
			return;
		}
		if(logMINOR) Logger.minor(this, "Found edition "+l+" for "+this+" - fetching...");
		// Create a SingleFileFetcher for the key (as an SSK).
		// Put the edition number into its context object.
		// Put ourself as callback.
		// Fetch it. If it fails, ignore it, if it succeeds, return the data with the edition # to the client.
		FreenetURI uri = key.getSSK(l).getURI();
		try {
			SingleFileFetcher getter =
				(SingleFileFetcher) SingleFileFetcher.create(this, this, uri, ctx, new ArchiveContext(ctx.maxTempLength, ctx.maxArchiveLevels), 
						ctx.maxNonSplitfileRetries, 0, true, l, true, false, context, realTimeFlag, false);
			getter.schedule(context);
		} catch (MalformedURLException e) {
			Logger.error(this, "Impossible: "+e, e);
		} catch (FetchException e) {
			Logger.error(this, "Could not start fetcher for "+uri+" : "+e, e);
		}
	}

	@Override
	public void onSuccess(StreamGenerator streamGenerator, ClientMetadata clientMetadata, List<? extends Compressor> decompressors, final ClientGetState state, ClientContext context) {
		if(logMINOR)
			Logger.minor(this, "Success on "+this+" from "+state+" : length "+streamGenerator.size()+"mime type "+clientMetadata.getMIMEType());
		DecompressorThreadManager decompressorManager = null;
		OutputStream output = null;
		Bucket finalResult = null;
		long maxLen = Math.max(ctx.maxTempLength, ctx.maxOutputLength);
		try {
			finalResult = context.getBucketFactory(persistent()).makeBucket(maxLen);
		} catch (InsufficientDiskSpaceException e) {
            onFailure(new FetchException(FetchExceptionMode.NOT_ENOUGH_DISK_SPACE), state, context);
            return;
		} catch (IOException e) {
			Logger.error(this, "Caught "+e, e);
			onFailure(new FetchException(FetchExceptionMode.BUCKET_ERROR, e), state, context);
			return;
		} catch(Throwable t) {
			Logger.error(this, "Caught "+t, t);
			onFailure(new FetchException(FetchExceptionMode.INTERNAL_ERROR, t), state, context);
			return;
		}

		PipedInputStream pipeIn = null;
		PipedOutputStream pipeOut = null;
		try {
			output = finalResult.getOutputStream();
			// Decompress
			if(decompressors != null) {
				if(logMINOR) Logger.minor(this, "Decompressing...");
				pipeIn = new PipedInputStream();
				pipeOut = new PipedOutputStream(pipeIn);
				decompressorManager = new DecompressorThreadManager(pipeIn, decompressors, maxLen);
				pipeIn = decompressorManager.execute();
				ClientGetWorkerThread worker = new ClientGetWorkerThread(new BufferedInputStream(pipeIn), output, null, null, null, false, null, null, null, context.linkFilterExceptionProvider);
				worker.start();
				streamGenerator.writeTo(pipeOut, context);
				worker.waitFinished();
				// If this throws, we want the whole request to fail.
				pipeOut.close(); pipeOut = null;
			} else {
					streamGenerator.writeTo(output, context);
					// If this throws, we want the whole request to fail.
					output.close(); output = null;
			}
		} catch(IOException e) {
			Logger.error(this, "Caught "+e, e);
			onFailure(new FetchException(FetchExceptionMode.INTERNAL_ERROR, e), state, context);
		} catch (Throwable t) {
			Logger.error(this, "Caught "+t, t);
			onFailure(new FetchException(FetchExceptionMode.INTERNAL_ERROR, t), state, context);
			return;
		} finally {
			Closer.close(output);
			Closer.close(pipeOut);
		}

		final FetchResult result = new FetchResult(clientMetadata, finalResult);
		context.uskManager.updateKnownGood(origUSK, state.getToken(), context);
		context.mainExecutor.execute(new PrioRunnable() {

			@Override
			public void run() {
				cb.onFound(origUSK, state.getToken(), result);
			}

			@Override
			public int getPriority() {
				return NativeThread.NORM_PRIORITY;
			}
			
		});
		
	}

	@Override
	public void onFailure(FetchException e, ClientGetState state, ClientContext context) {
		switch(e.mode) {
		case NOT_ENOUGH_PATH_COMPONENTS:
		case PERMANENT_REDIRECT:
			context.uskManager.updateKnownGood(origUSK, state.getToken(), context);
			return;
		}
		Logger.warning(this, "Found edition "+state.getToken()+" but failed to fetch edition: "+e, e);
	}

	@Override
	public void onBlockSetFinished(ClientGetState state, ClientContext context) {
		// Ignore
	}

	/**
	 * Get the original USK URI which was passed when creating the retriever - not the latest known URI!
	 */
	public USK getOriginalUSK() {
		return origUSK;
	}

	/**
	 * Get the original USK URI which was passed when creating the retriever - not the latest known URI!
	 */
	@Override
	public FreenetURI getURI() {
		// FIXME: Toad: Why did getURI() return null? Does it break anything that I made it return the URI?
		return origUSK.getURI();
	}

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

	@Override
	protected void innerNotifyClients(ClientContext context) {
		// Ignore for now
	}

	@Override
	public void onTransition(ClientGetState oldState, ClientGetState newState, ClientContext context) {
		// Ignore
	}

	@Override
	public void onExpectedMIME(ClientMetadata meta, ClientContext context) {
		// Ignore
	}

	@Override
	public void onExpectedSize(long size, ClientContext context) {
		// Ignore
	}

	@Override
	public void onFinalizedMetadata() {
		// Ignore
	}

	@Override
	public short getPollingPriorityNormal() {
		return cb.getPollingPriorityNormal();
	}

	@Override
	public short getPollingPriorityProgress() {
		return cb.getPollingPriorityProgress();
	}

	@Override
	public void cancel(ClientContext context) {
		super.cancel();
	}

	@Override
	protected void innerToNetwork(ClientContext context) {
		// Ignore
	}

	@Override
	public void onExpectedTopSize(long size, long compressed, int blocksReq, int blocksTotal, ClientContext context) {
		// Ignore
	}

	@Override
	public void onSplitfileCompatibilityMode(CompatibilityMode min, CompatibilityMode max, byte[] splitfileKey, boolean compressed, boolean bottomLayer, boolean definitiveAnyway, ClientContext context) {
		// Ignore
	}
	
	@Override
	public void onHashes(HashResult[] hashes, ClientContext context) {
		// Ignore
	}
	
	/** Called when we subscribe() in USKManager, if we don't directly subscribe
	 * the USKRetriever. Usually this happens when we put a proxy between them,
	 * e.g. USKProxyCompletionCallback, which hides updates for efficiency. 
	 * @param cb The callback that is actually USKManager.subscribe()'ed.
	 */
	synchronized void setProxy(USKCallback cb) {
		proxy = cb;
	}
	
	synchronized USKCallback getProxy() {
		return proxy;
	}
	
	synchronized void setFetcher(USKFetcher f) {
		fetcher = f;
	}
	
	synchronized USKFetcher getFetcher() {
		return fetcher;
	}

	public void unsubscribe(USKManager manager) {
		USKFetcher f;
		USKCallback p;
		synchronized(this) {
			f = fetcher;
			p = proxy;
		}
		if(f != null)
			f.cancel(manager.getContext());
		if(p != null)
			manager.unsubscribe(origUSK, p);
	}
	
	/** Only works if setFetcher() has been called, i.e. if this was created
	 * through USKManager.subscribeContentCustom().
	 * FIXME this is a special case hack, 
	 * For a generic solution see https://bugs.freenetproject.org/view.php?id=4984
	 * @param time The new cooldown time. At least 30 minutes or we throw.
	 * @param tries The new number of tries after each cooldown. Greater than 0
	 * and less than 3 or we throw.
	 */
	public void changeUSKPollParameters(long time, int tries, ClientContext context) {
		USKFetcher f;
		synchronized(this) {
			f = fetcher;
		}
		if(f == null) throw new IllegalStateException();
		f.changeUSKPollParameters(time, tries, context);
	}

    @Override
    public void innerOnResume(ClientContext context) {
        Logger.error(this, "Cannot be persistent");
        // Do nothing. Cannot be persistent.
    }

    @Override
    protected ClientBaseCallback getCallback() {
        // Not persistent.
        return null;
    }
}
