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

import java.io.IOException;
import java.util.Arrays;

import freenet.io.comm.MessageCore;
import freenet.io.comm.RetrievalException;
import freenet.support.BitArray;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.api.RandomAccessBuffer;

/**
 * Equivalent of PartiallyReceivedBlock, for large(ish) file transfers.
 * As presently implemented, we keep a bitmap in RAM of blocks received, so it should be adequate
 * for fairly large files (128kB for a 1GB file e.g.). We can compress this structure later on if
 * need be.
 * @author toad
 */
public class PartiallyReceivedBulk {
	
	/** The size of the data being received. Does *not* have to be a multiple of blockSize. */
	final long size;
	/** The size of the blocks sent as packets. */
	final int blockSize;
	private final RandomAccessBuffer raf;
	/** Which blocks have been received and written? */
	private final BitArray blocksReceived;
	final int blocks;
	private BulkTransmitter[] transmitters;
	final MessageCore usm;
	/** The one and only BulkReceiver */
	BulkReceiver recv;
	private int blocksReceivedCount;
	// Abort status
	boolean _aborted;
	int _abortReason;
	String _abortDescription;

        private static volatile boolean logMINOR;
	static {
		Logger.registerLogThresholdCallback(new LogThresholdCallback(){
			@Override
			public void shouldUpdate(){
				logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
			}
		});
	}
	
	/**
	 * Construct a PartiallyReceivedBulk.
	 * @param size Size of the file, does not have to be a multiple of blockSize.
	 * @param blockSize Block size.
	 * @param raf Where to store the data.
	 * @param initialState If true, assume all blocks have been received. If false, assume no blocks have
	 * been received.
	 */
	public PartiallyReceivedBulk(MessageCore usm, long size, int blockSize, RandomAccessBuffer raf, boolean initialState) {
		this.size = size;
		this.blockSize = blockSize;
		this.raf = raf;
		this.usm = usm;
		long blocks = (size + blockSize - 1) / blockSize;
		if(blocks > Integer.MAX_VALUE)
			throw new IllegalArgumentException("Too big");
		this.blocks = (int)blocks;
		blocksReceived = new BitArray(this.blocks);
		if(initialState) {
			blocksReceived.setAllOnes();
			blocksReceivedCount = this.blocks;
		}
		assert(raf.size() >= size);
	}

	/**
	 * Clone the blocksReceived BitArray. Used by BulkTransmitter to find what blocks are available on 
	 * creation. BulkTransmitter will have already taken the lock and will keep it over the add() also.
	 * @return A copy of blocksReceived.
	 */
	synchronized BitArray cloneBlocksReceived() {
		return new BitArray(blocksReceived);
	}
	
	/**
	 * Add a BulkTransmitter to the list of BulkTransmitters. When a block comes in, we will tell each
	 * BulkTransmitter about it.
	 * @param bt The BulkTransmitter to register.
	 */
	synchronized void add(BulkTransmitter bt) {
		if(transmitters == null)
			transmitters = new BulkTransmitter[] { bt };
		else {
			transmitters = Arrays.copyOf(transmitters, transmitters.length+1);
			transmitters[transmitters.length-1] = bt;
		}
	}
	
	/**
	 * Called when a block has been received. Will copy the data from the provided buffer and store it.
	 * @param blockNum The block number.
	 * @param data The byte array from which to read the data.
	 * @param offset The start of the data in the buffer.
	 */
	void received(int blockNum, byte[] data, int offset, int length) {
		if(blockNum > blocks) {
			Logger.error(this, "Received block "+blockNum+" of "+blocks+" !");
			return;
		}
		if(logMINOR)
			Logger.minor(this, "Received block "+blockNum);
		BulkTransmitter[] notifyBTs;
		long fileOffset = (long)blockNum * (long)blockSize;
		int bs = (int) Math.min(blockSize, size - fileOffset);
		if(length < bs) {
			String err = "Data too short! Should be "+bs+" actually "+length;
			Logger.error(this, err+" for "+this);
			abort(RetrievalException.PREMATURE_EOF, err);
			return;
		}
		synchronized(this) {
			if(blocksReceived.bitAt(blockNum)) return; // ignore
			blocksReceived.setBit(blockNum, true); // assume the rest of the function succeeds
			blocksReceivedCount++;
			notifyBTs = transmitters;
		}
		try {
			raf.pwrite(fileOffset, data, offset, bs);
		} catch (Throwable t) {
			Logger.error(this, "Failed to store received block "+blockNum+" on "+this+" : "+t, t);
			abort(RetrievalException.IO_ERROR, t.toString());
		}
		if(notifyBTs == null) return;
		for(BulkTransmitter notifyBT: notifyBTs) {
			// Not a generic callback, so no catch{} guard
			notifyBT.blockReceived(blockNum);
		}
	}

	public void abort(int errCode, String why) {
		if(logMINOR)
			Logger.normal(this, "Aborting "+this+": "+errCode+" : "+why+" first missing is "+blocksReceived.firstZero(0), new Exception("debug"));
		BulkTransmitter[] notifyBTs;
		BulkReceiver notifyBR;
		synchronized(this) {
			_aborted = true;
			_abortReason = errCode;
			_abortDescription = why;
			notifyBTs = transmitters;
			notifyBR = recv;
		}
		if(notifyBTs != null) {
			for(BulkTransmitter notifyBT: notifyBTs) {
				notifyBT.onAborted();
			}
		}
		if(notifyBR != null)
			notifyBR.onAborted();
		raf.close();
	}

	public synchronized boolean isAborted() {
		return _aborted;
	}

	public boolean hasWholeFile() {
		return blocksReceivedCount >= blocks;
	}

	public byte[] getBlockData(int blockNum) {
		long fileOffset = (long)blockNum * (long)blockSize;
		int bs = (int) Math.min(blockSize, size - fileOffset);
		byte[] data = new byte[bs];
		try {
			raf.pread(fileOffset, data, 0, bs);
		} catch (IOException e) {
			Logger.error(this, "Failed to read stored block "+blockNum+" on "+this+" : "+e, e);
			abort(RetrievalException.IO_ERROR, e.toString());
			return null;
		}
		return data;
	}

	public synchronized void remove(BulkTransmitter remove) {
		boolean found = false;
		for(BulkTransmitter t: transmitters) {
			if(t == remove) found = true;
		}
		if(!found) return;
		BulkTransmitter[] newTrans = new BulkTransmitter[transmitters.length-1];
		int j = 0;
		for(BulkTransmitter t: transmitters) {
			if(t == remove) continue;
			newTrans[j++] = t;
		}
		transmitters = newTrans;
	}
	
	public int getAbortReason() {
		return _abortReason;
	}
	
	public String getAbortDescription() {
		return _abortDescription;
	}
}
