package freenet.client.async;

import java.io.IOException;

import freenet.client.InsertException;
import freenet.client.async.SplitFileInserterSegmentStorage.BlockInsert;
import freenet.keys.CHKBlock;
import freenet.keys.ClientCHK;
import freenet.keys.ClientCHKBlock;
import freenet.keys.ClientKey;
import freenet.node.KeysFetchingLocally;
import freenet.node.LowLevelPutException;
import freenet.node.Node;
import freenet.node.NodeClientCore;
import freenet.node.RequestClient;
import freenet.node.RequestScheduler;
import freenet.node.SendableInsert;
import freenet.node.SendableRequestItem;
import freenet.node.SendableRequestSender;
import freenet.store.KeyCollisionException;
import freenet.support.Logger;
import freenet.support.io.ResumeFailedException;

/**
 * Interface to the low level insertion code for inserting a splitfile.
 * 
 * PERSISTENCE: Not persisted, recreated on resume by SplitFileInserter.
 * @author toad
 */
@SuppressWarnings("serial") // Not persisted.
public class SplitFileInserterSender extends SendableInsert {
    
    final SplitFileInserter parent;
    final SplitFileInserterStorage storage;

    public SplitFileInserterSender(SplitFileInserter parent, SplitFileInserterStorage storage) {
        super(parent.persistent, parent.realTime); // Persistence should be from parent so that e.g. callbacks get run on the right jobRunner.
        this.parent = parent;
        this.storage = storage;
    }

    @Override
    public void onSuccess(SendableRequestItem keyNum, ClientKey key, ClientContext context) {
        BlockInsert block = (BlockInsert) keyNum;
        block.segment.onInsertedBlock(block.blockNumber, (ClientCHK) key);
    }

    @Override
    public void onFailure(LowLevelPutException e, SendableRequestItem keyNum, ClientContext context) {
        InsertException e1 = InsertException.constructFrom(e);
        if(keyNum == null) {
            storage.fail(e1);
        } else {
            BlockInsert block = (BlockInsert) keyNum;
            block.segment.onFailure(block.blockNumber, e1);
        }
    }

    @Override
    public boolean canWriteClientCache() {
        return parent.ctx.canWriteClientCache;
    }

    @Override
    public boolean localRequestOnly() {
        return parent.ctx.localRequestOnly;
    }

    @Override
    public boolean forkOnCacheable() {
        return parent.ctx.forkOnCacheable;
    }

    @Override
    public void onEncode(SendableRequestItem token, ClientKey key, ClientContext context) {
        BlockInsert block = (BlockInsert) token;
        // Should already be set. This is a sanity check.
        try {
            if(storage.hasFinished()) return;
            block.segment.setKey(block.blockNumber, (ClientCHK) key);
        } catch (IOException e) {
            if(storage.hasFinished()) return; // Race condition possible as this is a callback
            storage.failOnDiskError(e);
        }
    }

    @Override
    public boolean isEmpty() {
        return isCancelled();
    }

    @Override
    protected void innerOnResume(ClientContext context) throws InsertException,
            ResumeFailedException {
        throw new UnsupportedOperationException(); // Not persisted.
    }

    @Override
    public short getPriorityClass() {
        return parent.parent.getPriorityClass();
    }

    @Override
    public SendableRequestItem chooseKey(KeysFetchingLocally keys, ClientContext context) {
        return storage.chooseBlock();
    }

    @Override
    public long countAllKeys(ClientContext context) {
        return storage.countAllKeys();
    }

    @Override
    public long countSendableKeys(ClientContext context) {
        return storage.countSendableKeys();
    }
    
    class MySendableRequestSender implements SendableRequestSender {
        
        @Override
        public boolean send(NodeClientCore node, final RequestScheduler sched, ClientContext context,
                final ChosenBlock request) {
            final BlockInsert token = (BlockInsert) request.token;
            try {
                ClientCHKBlock clientBlock = token.segment.encodeBlock(token.blockNumber);
                CHKBlock block = clientBlock.getBlock();
                final ClientCHK key = (ClientCHK) clientBlock.getClientKey();
                context.getJobRunner(request.isPersistent()).queueNormalOrDrop(new PersistentJob() {
                    
                    @Override
                    public boolean run(ClientContext context) {
                        onEncode(token, key, context);
                        return false;
                    }
                    
                });
                if(request.localRequestOnly) {
                    try {
                        node.node.store(block, false, request.canWriteClientCache, true, false);
                    } catch (KeyCollisionException e) {
                        throw new LowLevelPutException(LowLevelPutException.COLLISION);
                    }
                } else {
                    node.realPut(block, request.canWriteClientCache, request.forkOnCacheable, Node.PREFER_INSERT_DEFAULT, Node.IGNORE_LOW_BACKOFF_DEFAULT, request.realTimeFlag);
                }
                request.onInsertSuccess(key, context);
                return true;
            } catch (final LowLevelPutException e) {
                request.onFailure(e, context);
                return true;
            } catch (final IOException e) {
                context.getJobRunner(request.isPersistent()).queueNormalOrDrop(new PersistentJob() {
                    
                    @Override
                    public boolean run(ClientContext context) {
                        try {
                            storage.failOnDiskError(e);
                        } finally {
                            // Must terminate the request anyway.
                            request.onFailure(new LowLevelPutException(LowLevelPutException.INTERNAL_ERROR, "Disk error", e), context);
                        }
                        return true;
                    }
                });
                return true;
            } catch (Throwable t) {
                Logger.error(this, "Failed to send insert: "+t, t);
                // We still need to terminate the insert.
                request.onFailure(new LowLevelPutException(LowLevelPutException.INTERNAL_ERROR, "Failed: "+t, t), context);
                return true;
            }
        }

        @Override
        public boolean sendIsBlocking() {
            return true;
        }
        
    }
    
    final MySendableRequestSender sender = new MySendableRequestSender();
    
    @Override
    public SendableRequestSender getSender(ClientContext context) {
        return sender;
    }

    @Override
    public boolean isCancelled() {
        return storage.hasFinished();
    }

    @Override
    public RequestClient getClient() {
        return parent.parent.getClient();
    }

    @Override
    public ClientRequester getClientRequest() {
        return parent.parent;
    }

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

    public void schedule(ClientContext context) {
        if(getParentGrabArray() != null) return; // If change priority will unregister first.
        context.getChkInsertScheduler(parent.realTime).registerInsert(this, persistent);
    }
    
    @Override
    public long getWakeupTime(ClientContext context, long now) {
        return storage.getWakeupTime(context, now);
    }
}
