package freenet.client.async;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;

import freenet.client.FECCodec;
import freenet.client.FetchException;
import freenet.client.FetchException.FetchExceptionMode;
import freenet.client.async.PersistentJobRunner.CheckpointLock;
import freenet.keys.CHKBlock;
import freenet.keys.CHKEncodeException;
import freenet.keys.ClientCHK;
import freenet.keys.ClientCHKBlock;
import freenet.support.Logger;
import freenet.support.MemoryLimitedChunk;
import freenet.support.MemoryLimitedJob;
import freenet.support.MemoryLimitedJobRunner;
import freenet.support.io.StorageFormatException;

/** Cross-segments are "in parallel" with the main segments, an interlaced Reed-Solomon scheme 
 * similar to that used on CD's, allowing us to fill in blocks from other segments. There are 3
 * "cross-check blocks" in each segment, and therefore 3 in each cross-segment; the rest are data
 * blocks.
 */
public class SplitFileFetcherCrossSegmentStorage {
    
    private static volatile boolean logMINOR;

    static {
        Logger.registerClass(SplitFileFetcherCrossSegmentStorage.class);
    }

    public final int crossSegmentNumber;
    public final SplitFileFetcherStorage parent;
    
    /** Segment for each block */
    private final SplitFileFetcherSegmentStorage[] segments;
    /** Block number within the segment for each block */
    private final int[] blockNumbers;
    /** Whether each block in the cross-segment has been found. Kept up to date when blocks are
     * found in the other segments. However, as in a normal segment, these may not be 100% 
     * accurate! */
    private final boolean[] blocksFound;
    /** Number of data blocks chosen from the various segments. */
    final int dataBlockCount;
    /** Number of check blocks chosen from the various segments. Typically 3. */
    final int crossCheckBlockCount;
    final int totalBlocks;
    private int totalFound;
    /** If true, we are currrently decoding */
    private boolean tryDecode;
    /** True if the request has been terminated for some reason. */
    private boolean cancelled;
    /** If true, the segment has completed. Once a segment decode starts, finished must not be set
     * until it exits. */
    private boolean succeeded;
    private final FECCodec codec;
    /** Used in assigning blocks */
    private int counter;

    SplitFileFetcherCrossSegmentStorage(int segNo, int blocksPerSegment, int crossCheckBlocks, 
            SplitFileFetcherStorage parent, FECCodec codec) {
        this.crossSegmentNumber = segNo;
        this.parent = parent;
        this.dataBlockCount = blocksPerSegment;
        this.crossCheckBlockCount = crossCheckBlocks;
        totalBlocks = dataBlockCount + crossCheckBlocks;
        int totalBlocks = dataBlockCount + crossCheckBlocks;
        this.codec = codec;
        segments = new SplitFileFetcherSegmentStorage[totalBlocks];
        blockNumbers = new int[totalBlocks];
        blocksFound = new boolean[totalBlocks];
    }
    
    /** Called when a segment fetches a block that it believes to be relevant to us */
    public void onFetchedRelevantBlock(SplitFileFetcherSegmentStorage segment, int blockNo) {
        short priorityClass = parent.getPriorityClass();
        synchronized(this) {
            boolean found = false;
            for(int i=0;i<segments.length;i++) {
                if(segments[i] == segment && blockNumbers[i] == blockNo) {
                    found = true;
                    if(blocksFound[i]) {
                        // Already handled, don't loop.
                        return;
                    }
                    blocksFound[i] = true;
                    totalFound++;
                }
            }
            if(tryDecode || succeeded || cancelled) return;
            if(!found) {
                Logger.warning(this, "Block "+blockNo+" on "+segment+" not wanted by "+this);
                return;
            }
            if(totalFound < dataBlockCount) {
                if(logMINOR) Logger.minor(this, "Not decoding "+this+" : found "+totalFound+" blocks of "+dataBlockCount+" (total "+segments.length+")");
                return;
            }
            tryDecodeOrEncode(priorityClass);
        }
    }
    
    private synchronized void tryDecodeOrEncode(final short prio) {
        if(succeeded) return;
        if(tryDecode) return;
        if(cancelled) return;
        long limit = totalBlocks * CHKBlock.DATA_LENGTH + 
            Math.max(parent.fecCodec.maxMemoryOverheadDecode(dataBlockCount, crossCheckBlockCount),
                    parent.fecCodec.maxMemoryOverheadEncode(dataBlockCount, crossCheckBlockCount));
        parent.memoryLimitedJobRunner.queueJob(new MemoryLimitedJob(limit) {
            
            @Override
            public int getPriority() {
                return prio;
            }
            
            @Override
            public boolean start(MemoryLimitedChunk chunk) {
                boolean shutdown = false;
                CheckpointLock lock = null;
                try {
                    lock = parent.jobRunner.lock();
                    innerDecode(chunk);
                } catch (IOException e) {
                    Logger.error(this, "Failed to decode "+this+" because of disk error: "+e, e);
                    parent.failOnDiskError(e);
                } catch (PersistenceDisabledException e) {
                    shutdown = true;
                } finally {
                    chunk.release();
                    try {
                        if(!shutdown) {
                            // We do want to call the callback even if we threw something, because we 
                            // may be waiting to cancel. However we DON'T call it if we are shutting down.
                            synchronized(SplitFileFetcherCrossSegmentStorage.this) {
                                tryDecode = false;
                            }
                            parent.finishedEncoding(SplitFileFetcherCrossSegmentStorage.this);
                        }
                    } finally {
                        // Callback is part of the persistent job, unlock *after* calling it.
                        if(lock != null) lock.unlock(false, MemoryLimitedJobRunner.THREAD_PRIORITY);
                    }
                }
                return true;
            }
            
        });
        tryDecode = true;
    }
    
    /** Attempt FEC decoding. Check blocks before decoding in case there is disk corruption. Check
     * the new decoded blocks afterwards to ensure reproducible behaviour. */
    private void innerDecode(MemoryLimitedChunk chunk) throws IOException {
        if(logMINOR) Logger.minor(this, "Trying to decode "+this+" for "+parent);
        boolean killed = false;
        synchronized(this) {
            if(succeeded) return;
            if(cancelled) {
                killed = true;
            }
        }
        if(killed) {
            return;
        }
        
        // readAllBlocks does most of the housekeeping for us, see below...
        byte[][] dataBlocks = readBlocks(false);
        byte[][] checkBlocks = readBlocks(true);
        if(dataBlocks == null || checkBlocks == null) return; // Failed with disk error.
        
        // Original status.
        boolean[] dataBlocksFound = wasNonNullFill(dataBlocks);
        boolean[] checkBlocksFound = wasNonNullFill(checkBlocks);

        int realTotalDataBlocks = count(dataBlocksFound);
        int realTotalCrossCheckBlocks = count(checkBlocksFound);
        int realTotalFound = realTotalDataBlocks + realTotalCrossCheckBlocks;
        
        if(realTotalFound < dataBlockCount) {
            // Not finished yet.
            return;
        }
        
        boolean decoded = false;
        boolean encoded = false;
        
        if(realTotalDataBlocks < dataBlockCount) {
            // Decode.
            codec.decode(dataBlocks, checkBlocks, dataBlocksFound, checkBlocksFound, 
                    CHKBlock.DATA_LENGTH);
            for(int i=0;i<dataBlockCount;i++) {
                if(!dataBlocksFound[i]) {
                    checkDecodedBlock(i, dataBlocks[i]);
                    dataBlocksFound[i] = true;
                }
            }
        }
        
        if(realTotalCrossCheckBlocks < crossCheckBlockCount) {
            // Decode.
            codec.encode(dataBlocks, checkBlocks, checkBlocksFound, CHKBlock.DATA_LENGTH);
            for(int i=0;i<crossCheckBlockCount;i++) {
                if(!checkBlocksFound[i]) {
                    checkDecodedBlock(i+dataBlockCount, checkBlocks[i]);
                }
            }
        }
        
        synchronized(this) {
            succeeded = true;
        }
        
        if(logMINOR) Logger.minor(this, "Completed a cross-segment: decoded="+decoded+" encoded="+encoded);
    }


    private void checkDecodedBlock(int i, byte[] data) {
        ClientCHK key = getKey(i);
        if(key == null) {
            Logger.error(this, "Key not found");
            failOffThread(new FetchException(FetchExceptionMode.INTERNAL_ERROR, "Key not found"));
            return;
        }
        ClientCHKBlock block = encodeBlock(key, data);
        String decoded = i >= dataBlockCount ? "Encoded" : "Decoded";
        if(block == null || !key.getNodeCHK().equals(block.getKey())) {
            Logger.error(this, decoded+" cross-segment block "+i+" failed!");
            failOffThread(new FetchException(FetchExceptionMode.SPLITFILE_DECODE_ERROR, decoded+" cross-segment block does not match expected key"));
            return;
        } else {
            reportBlockToSegmentOffThread(i, key, block, data);
        }
    }

    private ClientCHKBlock encodeBlock(ClientCHK key, byte[] data) {
        try {
            return ClientCHKBlock.encodeSplitfileBlock(data, key.getCryptoKey(), key.getCryptoAlgorithm());
        } catch (CHKEncodeException e) {
            return null;
        }
    }

    private void reportBlockToSegmentOffThread(final int blockNo, final ClientCHK key, 
            final ClientCHKBlock block, final byte[] data) {
        parent.jobRunner.queueNormalOrDrop(new PersistentJob() {

            @Override
            public boolean run(ClientContext context) {
                try {
                    // FIXME CPU USAGE Add another API to the segment to avoid re-decoding.
                    SplitFileSegmentKeys keys = segments[blockNo].getSegmentKeys();
                    if(keys == null) return false;
                    boolean success = segments[blockNo].innerOnGotKey(key.getNodeCHK(), block, keys, 
                            blockNumbers[blockNo], data);
                    if(success) {
                        if(logMINOR)
                            Logger.minor(this, "Successfully decoded cross-segment block");
                    } else {
                        // Not really a big deal, but potentially interesting...
                        Logger.warning(this, "Decoded cross-segment block but not wanted by segment");
                    }
                } catch (IOException e) {
                    parent.failOnDiskError(e);
                    return true;
                }
                return false;
            }
        });
    }

    private void failOffThread(final FetchException e) {
        parent.jobRunner.queueNormalOrDrop(new PersistentJob() {

            @Override
            public boolean run(ClientContext context) {
                parent.fail(e);
                return true;
            }
            
        });
    }

    private void failDiskOffThread(final IOException e) {
        parent.jobRunner.queueNormalOrDrop(new PersistentJob() {

            @Override
            public boolean run(ClientContext context) {
                parent.failOnDiskError(e);
                return true;
            }
            
        });
    }

    private ClientCHK getKey(int i) {
        return segments[i].getKey(blockNumbers[i]);
    }

    private static boolean[] wasNonNullFill(byte[][] blocks) {
        boolean[] nonNulls = new boolean[blocks.length];
        for(int i=0;i<blocks.length;i++) {
            if(blocks[i] == null) {
                blocks[i] = new byte[CHKBlock.DATA_LENGTH];
            } else {
                nonNulls[i] = true;
            }
        }
        return nonNulls;
    }

    /** Read all blocks from all segments, checking the contents of the block against each key.
     * @param If false, read data blocks, if true, read check blocks. (The FEC code takes separate
     * arrays).
     * @return An array of blocks, in the correct order. Each element is either a valid block or
     * null if the block is invalid or hasn't been fetched yet. Will tell the ordinary segment if 
     * the block is bogus. Will also update our blocksFound. */
    
    private byte[][] readBlocks(boolean checkBlocks) {
        int start = checkBlocks ? dataBlockCount : 0;
        int end = checkBlocks ? totalBlocks : dataBlockCount;
        byte[][] blocks = new byte[end-start][];
        for(int i=start;i<end;i++) {
            try {
                byte[] block = segments[i].checkAndGetBlockData(blockNumbers[i]);
                blocks[i-start] = block;
                synchronized(this) {
                    if(block != null) {
                        if(!blocksFound[i]) totalFound++;
                        blocksFound[i] = true;
                    } else {
                        if(blocksFound[i]) totalFound--;
                        blocksFound[i] = false;
                    }
                }
            } catch (IOException e) {
                failDiskOffThread(e);
                return null;
            }
        }
        return blocks;
    }
    
    private static int count(boolean[] array) {
        int total = 0;
        for(boolean b : array)
            if(b) total++;
        return total;
    }

    public void addDataBlock(SplitFileFetcherSegmentStorage seg, int blockNum) {
        segments[counter] = seg;
        blockNumbers[counter] = blockNum;
        counter++;
    }
    
    public synchronized boolean isDecoding() {
        return tryDecode;
    }

    public void writeFixedMetadata(DataOutputStream dos) throws IOException {
        dos.writeInt(dataBlockCount);
        dos.writeInt(crossCheckBlockCount);
        for(int i=0;i<totalBlocks;i++) {
            dos.writeInt(segments[i].segNo);
            dos.writeInt(blockNumbers[i]);
        }
    }
    
    public SplitFileFetcherCrossSegmentStorage(SplitFileFetcherStorage parent, int segNo,
            DataInputStream dis) throws IOException, StorageFormatException {
        this.parent = parent;
        this.crossSegmentNumber = segNo;
        this.codec = parent.fecCodec;
        this.dataBlockCount = dis.readInt();
        this.crossCheckBlockCount = dis.readInt();
        this.totalBlocks = dataBlockCount + crossCheckBlockCount;
        blocksFound = new boolean[totalBlocks];
        segments = new SplitFileFetcherSegmentStorage[totalBlocks];
        blockNumbers = new int[totalBlocks];
        for(int i=0;i<totalBlocks;i++) {
            int readSeg = dis.readInt();
            if(readSeg < 0 || readSeg >= parent.segments.length)
                throw new StorageFormatException("Invalid segment number "+readSeg);
            SplitFileFetcherSegmentStorage segment = parent.segments[readSeg];
            this.segments[i] = segment;
            int blockNo = dis.readInt();
            if(blockNo < 0 || blockNo >= segment.totalBlocks())
                throw new StorageFormatException("Invalid block number "+blockNo+" for segment "+segment.segNo);
            this.blockNumbers[i] = blockNo;
            segment.resumeCallback(blockNo, this);
        }
    }
    
    /** Should be called before scheduling, unlike restart(). Doesn't lock, i.e. part of 
     * construction. But we must have read metadata on the regular segments first, which won't be 
     * true in the constructor. */
    public void checkBlocks() {
        for(int i=0;i<totalBlocks;i++) {
            if(segments[i].hasBlock(blockNumbers[i])) {
                blocksFound[i] = true;
                totalFound++;
            }
        }
    }

    /** Check for blocks and try to decode. */
    public void restart() {
        synchronized(this) {
            if(succeeded) return;
        }
        short priorityClass = parent.getPriorityClass();
        synchronized(this) {
            if(totalBlocks < dataBlockCount) return;
            tryDecodeOrEncode(priorityClass);
        }
    }
    
    public void cancel() {
        synchronized(this) {
            cancelled = true;
            if(tryDecode) return;
            succeeded = true;
        }
        parent.finishedEncoding(this);
    }
    
    int[] getSegmentNumbers() {
        int[] ret = new int[totalBlocks];
        for(int i=0;i<totalBlocks;i++)
            ret[i] = segments[i].segNo;
        return ret;
    }
    
    int[] getBlockNumbers() {
        return blockNumbers.clone();
    }

}
