package freenet.client.async;

import java.io.IOException;
import java.io.Serializable;

import freenet.client.ArchiveManager.ARCHIVE_TYPE;
import freenet.client.ClientMetadata;
import freenet.client.InsertContext;
import freenet.client.InsertException;
import freenet.client.InsertException.InsertExceptionMode;
import freenet.client.Metadata;
import freenet.client.async.SplitFileInserterSegmentStorage.MissingKeyException;
import freenet.crypt.CRCChecksumChecker;
import freenet.crypt.ChecksumFailedException;
import freenet.crypt.HashResult;
import freenet.node.SendableInsert;
import freenet.support.Logger;
import freenet.support.api.LockableRandomAccessBuffer;
import freenet.support.compress.Compressor.COMPRESSOR_TYPE;
import freenet.support.io.ResumeFailedException;
import freenet.support.io.StorageFormatException;

/** Top level class for a splitfile insert. Note that Storage is not persistent, it will be 
 * recreated here on resume, like in splitfile fetches. The actual status is stored in a RAF.
 * @author toad
 */
public class SplitFileInserter implements ClientPutState, Serializable, SplitFileInserterStorageCallback {
    
    private static volatile boolean logMINOR;
    private static volatile boolean logDEBUG;
    static {
        Logger.registerClass(SplitFileInserter.class);
    }

    private static final long serialVersionUID = 1L;
    /** Is the insert persistent? */
    final boolean persistent;
    /** Parent ClientPutter etc */
    final BaseClientPutter parent;
    /** Callback to send Metadata, completion status etc to */
    private final PutCompletionCallback cb;
    /** The file to be inserted */
    private final LockableRandomAccessBuffer originalData;
    /** Whether to free the data when the insert completes/fails. E.g. this is true if the data is
     * the result of compression. */
    private final boolean freeData;
    /** The RAF that stores check blocks and status info, used and created by storage. */
    private final LockableRandomAccessBuffer raf;
    /** Stores the state of the insert and does most of the work. 
     * Created in onResume() or in the constructor, so must be volatile. */
    private transient volatile SplitFileInserterStorage storage;
    /** Actually does the insert.
     * Created in onResume() or in the constructor, so must be volatile. */
    private transient volatile SplitFileInserterSender sender;
    /** Used any time a callback from storage needs us to do something higher level */
    private transient ClientContext context;
    /** Is the insert real-time? */
    final boolean realTime;
    /** Token to be kept with the insert */
    private final Object token;
    /** Insert settings */
    final InsertContext ctx;
    private transient boolean resumed;
    
    SplitFileInserter(boolean persistent, BaseClientPutter parent, PutCompletionCallback cb,
            LockableRandomAccessBuffer originalData, boolean freeData, InsertContext ctx, 
            ClientContext context, long decompressedLength, COMPRESSOR_TYPE compressionCodec, 
            ClientMetadata meta, boolean isMetadata, ARCHIVE_TYPE archiveType, 
            byte splitfileCryptoAlgorithm, byte[] splitfileCryptoKey, byte[] hashThisLayerOnly, 
            HashResult[] hashes, boolean topDontCompress, int topRequiredBlocks, 
            int topTotalBlocks, long origDataSize, long origCompressedDataSize, boolean realTime, 
            Object token) throws InsertException {
        this.persistent = persistent;
        this.parent = parent;
        this.cb = cb;
        this.originalData = originalData;
        this.context = context;
        this.freeData = freeData;
        try {
            storage = new SplitFileInserterStorage(originalData, decompressedLength, this, 
                    compressionCodec, meta, isMetadata, archiveType, 
                    context.getRandomAccessBufferFactory(persistent), persistent, 
                    ctx, splitfileCryptoAlgorithm, splitfileCryptoKey, hashThisLayerOnly, hashes,
                    context.tempBucketFactory /* only used for temporaries within constructor */,
                    new CRCChecksumChecker(), context.fastWeakRandom, context.memoryLimitedJobRunner,
                    context.getJobRunner(persistent), context.ticker, 
                    context.getChkInsertScheduler(realTime).fetchingKeys(), topDontCompress, 
                    topRequiredBlocks, topTotalBlocks, origDataSize, origCompressedDataSize);
            int mustSucceed = storage.topRequiredBlocks - topRequiredBlocks;
            parent.addMustSucceedBlocks(mustSucceed);
            parent.addRedundantBlocksInsert(storage.topTotalBlocks - topTotalBlocks - mustSucceed);
            parent.notifyClients(context);
        } catch (IOException e) {
            throw new InsertException(InsertExceptionMode.BUCKET_ERROR, e, null);
        }
        this.raf = storage.getRAF();
        this.sender = new SplitFileInserterSender(this, storage);
        this.realTime = realTime;
        this.token = token;
        this.ctx = ctx;
    }

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

    @Override
    public void cancel(ClientContext context) {
        storage.fail(new InsertException(InsertExceptionMode.CANCELLED));
    }

    @Override
    public void schedule(ClientContext context) throws InsertException {
        cb.onBlockSetFinished(this, context);
        storage.start();
        if(!ctx.getCHKOnly) {
            sender.clearWakeupTime(context);
            sender.schedule(context);
        }
    }

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

    @Override
    public void onResume(ClientContext context) throws InsertException, ResumeFailedException {
        assert(persistent);
        synchronized(this) {
            if(resumed) return;
            resumed = true;
        }
        this.context = context;
        try {
            raf.onResume(context);
            originalData.onResume(context);
            this.storage = new SplitFileInserterStorage(raf, originalData, this, context.fastWeakRandom, 
                    context.memoryLimitedJobRunner, context.getJobRunner(true), context.ticker,
                    context.getChkInsertScheduler(realTime).fetchingKeys(), context.persistentFG, 
                    context.persistentFileTracker, context.getPersistentMasterSecret());
            storage.onResume(context);
            this.sender = new SplitFileInserterSender(this, storage);
            schedule(context);
        } catch (IOException e) {
            Logger.error(this, "Resume failed: "+e, e);
            raf.close();
            raf.free();
            originalData.close();
            if(freeData)
                originalData.free();
            throw new InsertException(InsertExceptionMode.BUCKET_ERROR, e, null);
        } catch (StorageFormatException e) {
            Logger.error(this, "Resume failed: "+e, e);
            raf.close();
            raf.free();
            originalData.close();
            if(freeData)
                originalData.free();
            throw new InsertException(InsertExceptionMode.BUCKET_ERROR, e, null);
        } catch (ChecksumFailedException e) {
            Logger.error(this, "Resume failed: "+e, e);
            raf.close();
            raf.free();
            originalData.close();
            if(freeData)
                originalData.free();
            throw new InsertException(InsertExceptionMode.BUCKET_ERROR, e, null);
        }
    }

    @Override
    public void onFinishedEncode() {
        // Ignore.
    }

    @Override
    public void encodingProgress() {
        // We've encoded a segment. Start inserting the blocks we have immediately.
        if (ctx.getCHKOnly) {
            // We are not inserting any blocks. Wait for onHasKeys().
            return;
        }
        try {
            // Reschedule to insert the new check blocks.
            schedule(context);
        } catch (InsertException e) {
            storage.fail(e);
        }
    }

    @Override
    public void onHasKeys() {
        if(ctx.earlyEncode || ctx.getCHKOnly) {
            context.getJobRunner(persistent).queueNormalOrDrop(new PersistentJob() {
                
                @Override
                public boolean run(ClientContext context) {
                    try {
                        Metadata metadata = storage.encodeMetadata();
                        reportMetadata(metadata);
                        if(ctx.getCHKOnly)
                            onSucceeded(metadata);
                    } catch (IOException e) {
                        storage.fail(new InsertException(InsertExceptionMode.BUCKET_ERROR, e, null));
                    } catch (MissingKeyException e) {
                        storage.fail(new InsertException(InsertExceptionMode.BUCKET_ERROR, "Lost one or more keys", e, null));
                    }
                    return false;
                }
                
            });
        }
    }

    @Override
    public void onSucceeded(final Metadata metadata) {
        context.getJobRunner(persistent).queueNormalOrDrop(new PersistentJob() {

            @Override
            public boolean run(ClientContext context) {
                if(logMINOR) Logger.minor(this, "Succeeding on "+SplitFileInserter.this);
                unregisterSender();
                if(!(ctx.earlyEncode || ctx.getCHKOnly)) {
                    reportMetadata(metadata);
                }
                cb.onSuccess(SplitFileInserter.this, context);
                raf.close();
                raf.free();
                originalData.close();
                if(freeData)
                    originalData.free();
                return true;
            }
            
        });
    }

    protected void unregisterSender() {
        sender.unregister(context, parent.getPriorityClass());
    }

    protected void reportMetadata(Metadata metadata) {
        // Splitfile insert is always reportMetadataOnly, i.e. it always passes the metadata back 
        // to the parent SingleFileInserter, which will write it to a Bucket and probably insert it.
        cb.onMetadata(metadata, this, context);
    }

    @Override
    public void onFailed(final InsertException e) {
        context.getJobRunner(persistent).queueNormalOrDrop(new PersistentJob() {

            @Override
            public boolean run(ClientContext context) {
                unregisterSender();
                raf.close();
                raf.free();
                originalData.close();
                if(freeData)
                    originalData.free();
                cb.onFailure(e, SplitFileInserter.this, context);
                return true;
            }
        });
    }

    public long getLength() {
        return storage.dataLength;
    }

    @Override
    public void onInsertedBlock() {
        parent.completedBlock(false, context);
    }
    
    @Override
    public void onShutdown(ClientContext context) {
        storage.onShutdown(context);
    }

    @Override
    public void clearCooldown() {
        sender.clearWakeupTime(context);
    }

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

}
