package freenet.client.async;

import java.io.DataInputStream;
import java.io.IOException;
import java.util.ArrayList;

import freenet.client.FailureCodeTracker;
import freenet.client.InsertContext;
import freenet.client.InsertException;
import freenet.client.InsertException.InsertExceptionMode;
import freenet.keys.CHKBlock;
import freenet.keys.ClientKey;
import freenet.keys.Key;
import freenet.keys.KeyBlock;
import freenet.keys.SSKBlock;
import freenet.node.LowLevelPutException;
import freenet.node.RequestClient;
import freenet.node.SendableRequestItem;
import freenet.node.SimpleSendableInsert;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.api.Bucket;

public class BinaryBlobInserter implements ClientPutState {

	final ClientPutter parent;
	final RequestClient clientContext;
	final MySendableInsert[] inserters;
	final FailureCodeTracker errors;
	final int maxRetries;
	final int consecutiveRNFsCountAsSuccess;
	private boolean logMINOR;
	private int completedBlocks;
	private int succeededBlocks;
	private boolean fatal;
	final InsertContext ctx;
	final boolean realTimeFlag;

	BinaryBlobInserter(Bucket blob, ClientPutter parent, RequestClient clientContext, boolean tolerant, short prioClass, InsertContext ctx, ClientContext context)
	throws IOException, BinaryBlobFormatException {
		logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
		this.ctx = ctx;
		this.maxRetries = ctx.maxInsertRetries;
		this.consecutiveRNFsCountAsSuccess = ctx.consecutiveRNFsCountAsSuccess;
		this.parent = parent;
		this.clientContext = clientContext;
		this.errors = new FailureCodeTracker(true);
		this.realTimeFlag = clientContext.realTimeFlag();
		DataInputStream dis = new DataInputStream(blob.getInputStream());

		BlockSet blocks = new SimpleBlockSet();

		try {
			BinaryBlob.readBinaryBlob(dis, blocks, tolerant);
		} finally {
			dis.close();
		}

		ArrayList<MySendableInsert> myInserters = new ArrayList<MySendableInsert>();
		int x=0;
		for(Key key: blocks.keys()) {
			KeyBlock block = blocks.get(key);
			MySendableInsert inserter =
				new MySendableInsert(x++, block, prioClass, getScheduler(block, context), clientContext);
			myInserters.add(inserter);
		}

		inserters = myInserters.toArray(new MySendableInsert[myInserters.size()]);
		parent.addMustSucceedBlocks(inserters.length);
		parent.notifyClients(context);
	}

	private ClientRequestScheduler getScheduler(KeyBlock block, ClientContext context) {
		if(block instanceof CHKBlock)
			return context.getChkInsertScheduler(realTimeFlag);
		else if(block instanceof SSKBlock)
			return context.getSskInsertScheduler(realTimeFlag);
		else throw new IllegalArgumentException("Unknown block type "+block.getClass()+" : "+block);
	}

	@Override
	public void cancel(ClientContext context) {
		for(MySendableInsert inserter: inserters) {
			if(inserter != null)
				inserter.cancel(context);
		}
		parent.onFailure(new InsertException(InsertExceptionMode.CANCELLED), this, context);
	}

	@Override
	public BaseClientPutter getParent() {
		return parent;
	}

	@Override
	public Object getToken() {
		return clientContext;
	}

	@Override
	public void schedule(ClientContext context) throws InsertException {
		for(MySendableInsert inserter: inserters) {
			inserter.schedule();
		}
	}

	class MySendableInsert extends SimpleSendableInsert {

        private static final long serialVersionUID = 1L;
        final int blockNum;
		private int consecutiveRNFs;
		private int retries;

		public MySendableInsert(int i, KeyBlock block, short prioClass, ClientRequestScheduler scheduler, RequestClient client) {
			super(block, prioClass, client, scheduler);
			this.blockNum = i;
		}

		@Override
		public void onSuccess(SendableRequestItem keyNum, ClientKey key, ClientContext context) {
			synchronized(this) {
				if(inserters[blockNum] == null) return;
				inserters[blockNum] = null;
				completedBlocks++;
				succeededBlocks++;
			}
			parent.completedBlock(false, context);
			maybeFinish(context);
		}

		// FIXME duplicated code from SingleBlockInserter
		// FIXME combine it somehow
		@Override
		public void onFailure(LowLevelPutException e, SendableRequestItem keyNum, ClientContext context) {
			synchronized(BinaryBlobInserter.this) {
				if(inserters[blockNum] == null) return;
			}
			if(parent.isCancelled()) {
				fail(new InsertException(InsertExceptionMode.CANCELLED), true, context);
				return;
			}
			logMINOR = Logger.shouldLog(LogLevel.MINOR, BinaryBlobInserter.this);
			switch(e.code) {
			case LowLevelPutException.COLLISION:
				fail(new InsertException(InsertExceptionMode.COLLISION), false, context);
				break;
			case LowLevelPutException.INTERNAL_ERROR:
				errors.inc(InsertExceptionMode.INTERNAL_ERROR);
				break;
			case LowLevelPutException.REJECTED_OVERLOAD:
				errors.inc(InsertExceptionMode.REJECTED_OVERLOAD);
				break;
			case LowLevelPutException.ROUTE_NOT_FOUND:
				errors.inc(InsertExceptionMode.ROUTE_NOT_FOUND);
				break;
			case LowLevelPutException.ROUTE_REALLY_NOT_FOUND:
				errors.inc(InsertExceptionMode.ROUTE_REALLY_NOT_FOUND);
				break;
			default:
				Logger.error(this, "Unknown LowLevelPutException code: "+e.code);
				errors.inc(InsertExceptionMode.INTERNAL_ERROR);
			}
			if(e.code == LowLevelPutException.ROUTE_NOT_FOUND) {
				consecutiveRNFs++;
				if(logMINOR) Logger.minor(this, "Consecutive RNFs: "+consecutiveRNFs+" / "+consecutiveRNFsCountAsSuccess);
				if(consecutiveRNFs == consecutiveRNFsCountAsSuccess) {
					if(logMINOR) Logger.minor(this, "Consecutive RNFs: "+consecutiveRNFs+" - counting as success");
					onSuccess(keyNum, null, context);
					return;
				}
			} else
				consecutiveRNFs = 0;
			if(logMINOR) Logger.minor(this, "Failed: "+e);
			retries++;
			if((retries > maxRetries) && (maxRetries != -1)) {
				fail(InsertException.construct(errors), false, context);
				return;
			}
			this.clearWakeupTime(context);
			// Retry *this block*
			this.schedule();
		}

		private void fail(InsertException e, boolean fatal, ClientContext context) {
			synchronized(BinaryBlobInserter.this) {
				if(inserters[blockNum] == null) return;
				inserters[blockNum] = null;
				completedBlocks++;
				if(fatal) BinaryBlobInserter.this.fatal = true;
			}
			if(fatal)
				parent.fatallyFailedBlock(context);
			else
				parent.failedBlock(context);
			maybeFinish(context);
		}

	}

	public void maybeFinish(ClientContext context) {
		boolean success;
		boolean wasFatal;
		synchronized(this) {
			if(completedBlocks != inserters.length)
				return;
			success = completedBlocks == succeededBlocks;
			wasFatal = fatal;
		}
		if(success) {
			parent.onSuccess(this, context);
		} else if(wasFatal)
			parent.onFailure(new InsertException(InsertExceptionMode.FATAL_ERRORS_IN_BLOCKS, errors, null), this, context);
		else
			parent.onFailure(new InsertException(InsertExceptionMode.TOO_MANY_RETRIES_IN_BLOCKS, errors, null), this, context);
	}

    @Override
    public void onResume(ClientContext context) throws InsertException {
        // TODO binary blob inserter isn't persistent yet, right?
        throw new InsertException(InsertExceptionMode.INTERNAL_ERROR, "Persistence not supported yet", null);
    }

    @Override
    public void onShutdown(ClientContext context) {
        // Ignore.
    }

}
