/* 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.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.Date;
import java.util.WeakHashMap;

import freenet.crypt.ChecksumChecker;
import freenet.keys.FreenetURI;
import freenet.node.RequestClient;
import freenet.node.SendableRequest;
import freenet.node.useralerts.SimpleUserAlert;
import freenet.node.useralerts.UserAlert;
import freenet.support.CurrentTimeUTC;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.io.ResumeFailedException;

/** A high level request or insert. This may create any number of low-level requests of inserts,
 * for example a request may follow redirects, download splitfiles and unpack containers, while an
 * insert (for a file or a freesite) may also have to insert many blocks. A high-level request is
 * created by a client, has a FetchContext or InsertContext for configuration. Compare to 
 * @see SendableRequest for a low-level request (which may still be multiple actual requests or 
 * inserts).
 * WARNING: Changing non-transient members on classes that are Serializable can result in 
 * restarting downloads or losing uploads.
 */
public abstract class ClientRequester implements Serializable, ClientRequestSchedulerGroup {

    private static final long serialVersionUID = 1L;
    private static volatile boolean logMINOR;
	
	static {
		Logger.registerClass(ClientRequester.class);
	}

	public abstract void onTransition(ClientGetState oldState, ClientGetState newState, ClientContext context);
	
	// FIXME move the priority classes from RequestStarter here
	/** Priority class of the request or insert. */
	protected short priorityClass;
	/** Whether this is a real-time request */
	protected final boolean realTimeFlag;
	/** Has the request or insert been cancelled? */
	protected boolean cancelled;
	/** The RequestClient, used to determine whether this request is 
	 * persistent, and also we round-robin between different RequestClient's
	 * in scheduling within a given priority class and retry count. */
	protected transient RequestClient client;

	/** What is our priority class? */
	public short getPriorityClass() {
		return priorityClass;
	}

	/**
	 * zero arg c'tor for db4o on jamvm / for serialization.
	 */
	protected ClientRequester() {
		realTimeFlag = false;
		creationTime = 0;
		hashCode = 0;
	}

	protected ClientRequester(short priorityClass, ClientBaseCallback cb) {
		this.priorityClass = priorityClass;
		this.client = cb.getRequestClient();
		this.realTimeFlag = client.realTimeFlag();
		if(client == null)
			throw new NullPointerException();
		hashCode = super.hashCode(); // the old object id will do fine, as long as we ensure it doesn't change!
		synchronized(allRequesters) {
			if(!persistent())
				allRequesters.put(this, dumbValue);
		}
		creationTime = System.currentTimeMillis();
	}

	/** Cancel the request. Inner method, subclasses should actually tell 
	 * the ClientGetState or whatever to cancel itself: this does not do 
	 * anything apart from set a flag!
	 * @return Whether we were already cancelled.
	 */
	protected synchronized boolean cancel() {
		boolean ret = cancelled;
		cancelled = true;
		return ret;
	}

	/** Cancel the request. Subclasses must implement to actually tell the
	 * ClientGetState's or ClientPutState's to cancel.
	 * @param context The ClientContext object including essential but 
	 * non-persistent objects such as the schedulers.
	 */
	public abstract void cancel(ClientContext context);

	/** Is the request or insert cancelled? */
	public boolean isCancelled() {
		return cancelled;
	}

	/** Get the URI for the request or insert. For a request this is set at
	 * creation, but for an insert, it is set when we know what the final 
	 * URI will be. */
	public abstract FreenetURI getURI();

	/** Is the request or insert completed (succeeded, failed, or 
	 * cancelled, which is a kind of failure)? */
	public abstract boolean isFinished();
	
	private final int hashCode;
	
	/**
	 * We need a hash code that persists across restarts.
	 */
	@Override
	public int hashCode() {
		return hashCode;
	}

	/** Total number of blocks this request has tried to fetch/put. */
	protected int totalBlocks;
	/** Number of blocks we have successfully completed a fetch/put for. */
	protected int successfulBlocks;
    /**
     * ATTENTION: This may be null for very old databases.
     * @see #getLatestSuccess() Explanation of the content and especially the default value. */
    protected Date latestSuccess = CurrentTimeUTC.get();
	/** Number of blocks which have failed. */
	protected int failedBlocks;
	/** Number of blocks which have failed fatally. */
	protected int fatallyFailedBlocks;
    /** @see #getLatestFailure() */
    protected Date latestFailure = null;
	/** Minimum number of blocks required to succeed for success. */
	protected int minSuccessBlocks;
	/** Has totalBlocks stopped growing? */
	protected boolean blockSetFinalized;
	/** Has at least one block been scheduled to be sent to the network? 
	 * Requests can be satisfied entirely from the datastore sometimes. */
	protected boolean sentToNetwork;
	
    public int getTotalBlocks() {
        return totalBlocks;
    }

    /**
     * UTC Date of latest increase of {@link #successfulBlocks}.<br>
     * Initialized to current time for usability purposes: This allows the user to sort downloads by
     * last success in the user interface to determine which ones are stalling - those will be the
     * ones with the oldest last success date. If we initialized it to "null" only, that would not
     * be possible: The user couldn't distinguish very old stalling downloads from downloads which
     * merely had no success yet because they were added a short time ago.<br> */
    public Date getLatestSuccess() {
        // clone() because Date is mutable.
        // Null-check for backwards compatibility: Old serialized versions of objects of this
        // class might not have this field yet.
        return latestSuccess != null ? (Date)latestSuccess.clone() : new Date(0);
    }

    /**
     * UTC Date of latest increase of {@link #failedBlocks} or {@link #fatallyFailedBlocks}.<br>
     * Null if there was no failure yet. */
    public Date getLatestFailure() {
        // clone() because Date is mutable.
        // Null-check for backwards compatibility: Old serialized versions of objects of this
        // class might not have this field yet.
        return latestFailure != null ? (Date)latestFailure.clone() : null;
    }

	protected synchronized void resetBlocks() {
		totalBlocks = 0;
		successfulBlocks = 0;
        // See ClientRequester.getLatestSuccess() for why this defaults to current time.
        latestSuccess = CurrentTimeUTC.get();
		failedBlocks = 0;
		fatallyFailedBlocks = 0;
        latestFailure = null;
		minSuccessBlocks = 0;
		blockSetFinalized = false;
		sentToNetwork = false;
	}
	
	/** The set of blocks has been finalised, total will not change any
	 * more. Notify clients.
	 * @param context The ClientContext object including essential but 
	 * non-persistent objects such as the schedulers.
	 */
	public void blockSetFinalized(ClientContext context) {
		synchronized(this) {
			if(blockSetFinalized) return;
			blockSetFinalized = true;
		}
		if(logMINOR)
			Logger.minor(this, "Finalized set of blocks for "+this, new Exception("debug"));
		notifyClients(context);
	}

	/** Add a block to our estimate of the total. Don't notify clients. */
	public void addBlock() {
		boolean wasFinalized;
		synchronized (this) {
			totalBlocks++;
			wasFinalized = blockSetFinalized;
		}

		if (wasFinalized) {
			if (LogLevel.MINOR.matchesThreshold(Logger.globalGetThresholdNew()))
				Logger.error(this, "addBlock() but set finalized! on " + this, new Exception("error"));
			else
				Logger.error(this, "addBlock() but set finalized! on " + this);
		}
		
		if(logMINOR) Logger.minor(this, "addBlock(): total="+totalBlocks+" successful="+successfulBlocks+" failed="+failedBlocks+" required="+minSuccessBlocks);
	}

	/** Add several blocks to our estimate of the total. Don't notify clients. */
	public void addBlocks(int num) {
		boolean wasFinalized;
		synchronized (this) {
			totalBlocks += num;
			wasFinalized = blockSetFinalized;
		}

		if (wasFinalized) {
			if (LogLevel.MINOR.matchesThreshold(Logger.globalGetThresholdNew()))
				Logger.error(this, "addBlocks() but set finalized! on "+this, new Exception("error"));
			else
				Logger.error(this, "addBlocks() but set finalized! on "+this);
		}
		
		if(logMINOR) Logger.minor(this, "addBlocks("+num+"): total="+totalBlocks+" successful="+successfulBlocks+" failed="+failedBlocks+" required="+minSuccessBlocks); 
	}

	/** We completed a block. Count it and notify clients unless dontNotify. */
	public void completedBlock(boolean dontNotify, ClientContext context) {
		if(logMINOR)
			Logger.minor(this, "Completed block ("+dontNotify+ "): total="+totalBlocks+" success="+successfulBlocks+" failed="+failedBlocks+" fatally="+fatallyFailedBlocks+" finalised="+blockSetFinalized+" required="+minSuccessBlocks+" on "+this);
		synchronized(this) {
			if(cancelled) return;
			successfulBlocks++;
            latestSuccess = CurrentTimeUTC.get();
		}
		if(dontNotify) return;
		notifyClients(context);
	}
	
	transient static final UserAlert brokenClientAlert = new SimpleUserAlert(true, "Some broken downloads/uploads were cancelled. Please restart them.", "Some downloads/uploads were broken due to a bug (some time before 1287) causing unrecoverable database corruption. They have been cancelled. Please restart them from the Downloads or Uploads page.", "Some downloads/uploads were broken due to a pre-1287 bug, please restart them.", UserAlert.ERROR);

    /** A block failed. Count it and notify our clients. */
    public void failedBlock(boolean dontNotify, ClientContext context) {
        synchronized(this) {
            failedBlocks++;
            latestFailure = CurrentTimeUTC.get();
        }
        if(!dontNotify)
            notifyClients(context);
    }

	/** A block failed. Count it and notify our clients. */
	public void failedBlock(ClientContext context) {
	    failedBlock(false, context);
	}

	/** A block failed fatally. Count it and notify our clients. */
	public void fatallyFailedBlock(ClientContext context) {
		synchronized(this) {
			fatallyFailedBlocks++;
            latestFailure = CurrentTimeUTC.get();
		}
		notifyClients(context);
	}

	/** Add one or more blocks to the number of requires blocks, and don't notify the clients. */
	public synchronized void addMustSucceedBlocks(int blocks) {
		totalBlocks += blocks;
		minSuccessBlocks += blocks;
		if(logMINOR) Logger.minor(this, "addMustSucceedBlocks("+blocks+"): total="+totalBlocks+" successful="+successfulBlocks+" failed="+failedBlocks+" required="+minSuccessBlocks); 
	}

	/** Insertors should override this. The method is duplicated rather than calling addMustSucceedBlocks to avoid confusing consequences when addMustSucceedBlocks does other things. */
	public synchronized void addRedundantBlocksInsert(int blocks) {
		totalBlocks += blocks;
		minSuccessBlocks += blocks;
		if(logMINOR) Logger.minor(this, "addMustSucceedBlocks("+blocks+"): total="+totalBlocks+" successful="+successfulBlocks+" failed="+failedBlocks+" required="+minSuccessBlocks); 
	}
	
	/** Notify clients by calling innerNotifyClients off-thread. */
	public final void notifyClients(ClientContext context) {
	    context.getJobRunner(persistent()).queueNormalOrDrop(new PersistentJob() {

            @Override
            public boolean run(ClientContext context) {
                innerNotifyClients(context);
                return false;
            }
	        
	    });
	}
	
	/** Notify clients, usually via a SplitfileProgressEvent, of the current progress. Called 
	 * off-thread. Please do not change SimpleEventProducer to always produce events off-thread, it
	 * is better to deal with that here, because events could be re-ordered, which matters for some
	 * events notably SimpleProgressEvent. */
	protected abstract void innerNotifyClients(ClientContext context);
	
	/** Called when we first send a request to the network. Ensures that it really is the first time and
	 * passes on to innerToNetwork().
	 */
	public void toNetwork(ClientContext context) {
		synchronized(this) {
			if(sentToNetwork) return;
			sentToNetwork = true;
		}
		innerToNetwork(context);
	}

	/** Notify clients that a request has gone to the network, for the first time, i.e. we have finished
	 * checking the datastore for at least one part of the request. */
	protected abstract void innerToNetwork(ClientContext context);

	protected void clearCountersOnRestart() {
		this.blockSetFinalized = false;
		this.cancelled = false;
		this.failedBlocks = 0;
		this.fatallyFailedBlocks = 0;
        this.latestFailure = null;
		this.minSuccessBlocks = 0;
		this.sentToNetwork = false;
		this.successfulBlocks = 0;
        // See ClientRequester.getLatestSuccess() for why this defaults to current time.
        this.latestSuccess = CurrentTimeUTC.get();
		this.totalBlocks = 0;
	}

	/** Get client context object */
	public RequestClient getClient() {
		return client;
	}

	/** Change the priority class of the request (request includes inserts here).
	 * @param newPriorityClass The new priority class for the request or insert.
	 * @param ctx The ClientContext, contains essential transient objects such as the schedulers.
	 */
	public void setPriorityClass(short newPriorityClass, ClientContext ctx) {
		short oldPrio;
		synchronized(this) {
			oldPrio = priorityClass;
			this.priorityClass = newPriorityClass;
		}
		if(logMINOR) Logger.minor(this, "Changing priority class of "+this+" from "+oldPrio+" to "+newPriorityClass);
		ctx.getChkFetchScheduler(realTimeFlag).reregisterAll(this, oldPrio);
		ctx.getChkInsertScheduler(realTimeFlag).reregisterAll(this, oldPrio);
		ctx.getSskFetchScheduler(realTimeFlag).reregisterAll(this, oldPrio);
		ctx.getSskInsertScheduler(realTimeFlag).reregisterAll(this, oldPrio);
	}

	public boolean realTimeFlag() {
		return realTimeFlag;
	}

	/** Is this request persistent? */
	public boolean persistent() {
		return client.persistent();
	}

	private static WeakHashMap<ClientRequester,Object> allRequesters = new WeakHashMap<ClientRequester,Object>();
	private static Object dumbValue = new Object();
	public final long creationTime;

	public static ClientRequester[] getAll() {
		synchronized(allRequesters) {
			return allRequesters.keySet().toArray(new ClientRequester[0]);
		}
	}

    /** @return A byte[] representing the original client, to be written to the file storing a 
     * persistent download. E.g. for FCP, this will include the Identifier, whether it is on the 
     * global queue and the client name. 
     * @param checker Used to checksum and isolate large components where we can recover if they 
     * fail.
     * @throws IOException */
    public byte[] getClientDetail(ChecksumChecker checker) throws IOException {
        return new byte[0];
    }
    
    protected static byte[] getClientDetail(PersistentClientCallback callback, ChecksumChecker checker) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(baos);
        callback.getClientDetail(dos, checker);
        return baos.toByteArray();
    }
    
    private transient boolean resumed = false;
    
    /** Called for a persistent request after startup. Should call notifyClients() at the end,
     * after the callback has been registered etc. 
     * @throws ResumeFailedException */
    public final void onResume(ClientContext context) throws ResumeFailedException {
        synchronized(this) {
            if(resumed) return;
            resumed = true;
        }
        innerOnResume(context);
    }

    /** Called by onResume() once and only once after restarting. Must be overridden, and must call
     * super.innerOnResume(). 
     * @throws ResumeFailedException */
    protected void innerOnResume(ClientContext context) throws ResumeFailedException {
        ClientBaseCallback cb = getCallback();
        client = cb.getRequestClient();
        assert(client.persistent());
        if(sentToNetwork)
            innerToNetwork(context);
    }

    protected abstract ClientBaseCallback getCallback();

    /** Called just before the final write when shutting down the node. */
    public void onShutdown(ClientContext context) {
        // Do nothing.
    }

    public boolean isCurrentState(ClientGetState state) {
        return false;
    }

    /**
     * Get the group the request belongs to. For single requests (the default) this is the request
     * itself; for those in a group, such as a site insert, it is a common value between them.
     */
    public ClientRequestSchedulerGroup getSchedulerGroup() {
      return this;
    }

}
