package freenet.client;

import java.util.Arrays;
import java.util.Random;

import freenet.support.TestProperty;

import junit.framework.Assert;
import junit.framework.TestCase;

/** Test the new (post db4o) high level FEC API */
public class OnionFECCodecTest extends TestCase {
    
    private static final int BLOCK_SIZE = 4096;
    private static final int MAX_SEGMENT_SIZE = 255;
    
    private final OnionFECCodec codec = new OnionFECCodec();
    private byte[][] originalDataBlocks;
    private byte[][] dataBlocks;
    private byte[][] originalCheckBlocks;
    private byte[][] checkBlocks;
    private boolean[] checkBlocksPresent;
    private boolean[] dataBlocksPresent;
    
    public void testDecodeRandomSubset() {
        Random r = new Random(19412106);
        int iterations = TestProperty.EXTENSIVE ? 100 : 10;
        for(int i=0;i<iterations;i++)
            inner(128, 128, r);
        for(int i=0;i<iterations;i++)
            inner(127, 129, r);
        for(int i=0;i<iterations;i++)
            inner(129, 127, r);
    }
    
    public void testEncodeThrowsOnNotPaddedLastBlock() {
        Random r = new Random(21502106);
        int data = 128;
        int check = 128;
        originalDataBlocks = createOriginalDataBlocks(r, data);
        originalDataBlocks[data-1] = new byte[BLOCK_SIZE/2];
        checkBlocks = setupCheckBlocks(check);
        dataBlocks = copy(originalDataBlocks);
        
        // Encode the check blocks.
        checkBlocksPresent = new boolean[checkBlocks.length];
        try {
            codec.encode(dataBlocks, checkBlocks, checkBlocksPresent, BLOCK_SIZE);
            assertTrue(false); // Should throw here!
        } catch (IllegalArgumentException e) {
            // Expected.
        }
    }
    
    public void testDecodeThrowsOnNotPaddedLastBlock() {
        Random r = new Random(21482106);
        setup(128, 128, r);
        // Now delete a random selection of blocks
        deleteRandomBlocks(r);
        dataBlocks[127] = new byte[BLOCK_SIZE/2];
        try {
            codec.decode(dataBlocks, checkBlocks, dataBlocksPresent, checkBlocksPresent, BLOCK_SIZE);
            assertTrue(false); // Should throw
        } catch (IllegalArgumentException e) {
            // Ok.
        }
    }
    
    public void testDecodeAlreadyDecoded() {
        Random r = new Random(21482106);
        setup(128, 128, r);
        // Now delete a random selection of blocks
        deleteAllCheckBlocks();
        decode(); // Should be a no-op.
    }
    
    public void testDecodeNoneDecoded() {
        Random r = new Random(21482106);
        setup(128, 128, r);
        // Now delete a random selection of blocks
        deleteAllDataBlocks();
        decode();
    }
    
    public void testManyCheckFewData() {
        Random r = new Random(21582106);
        inner(2, 253, r);
        inner(5, 250, r);
        inner(50, 200, r);
        inner(2, 3, r); // Common case, include it here.
    }
    
    public void testManyDataFewCheck() {
        Random r = new Random(21592106);
        inner(200, 55, r);
        inner(253, 2, r);
    }
    
    public void testRandomDataCheckCounts() {
        Random r = new Random(21602106);
        int iterations = TestProperty.EXTENSIVE ? 100 : 10;
        for(int i=0;i<iterations;i++) {
            int data = r.nextInt(252)+2;
            int maxCheck = 255 - data;
            int check = r.nextInt(maxCheck)+1;
            inner(data, check, r);
        }
    }
    
    protected void inner(int data, int check, Random r) {
        setup(data, check, r);
        // Now delete a random selection of blocks
        deleteRandomBlocks(r);
        decode();
    }
    
    protected void setup(int data, int check, Random r) {
        originalDataBlocks = createOriginalDataBlocks(r, data);
        checkBlocks = setupCheckBlocks(check);
        dataBlocks = copy(originalDataBlocks);
        
        // Encode the check blocks.
        checkBlocksPresent = new boolean[checkBlocks.length];
        codec.encode(dataBlocks, checkBlocks, checkBlocksPresent, BLOCK_SIZE);
        assertEquals(originalDataBlocks, dataBlocks);
        originalCheckBlocks = copy(checkBlocks);
        
        // Initially everything is present...
        dataBlocksPresent = new boolean[dataBlocks.length];
        for(int i=0;i<dataBlocksPresent.length;i++) dataBlocksPresent[i] = true;
        for(int i=0;i<checkBlocksPresent.length;i++) checkBlocksPresent[i] = true;
    }
    
    protected void decode() {
        boolean[] oldDataBlocksPresent = dataBlocksPresent.clone();
        boolean[] oldCheckBlocksPresent = checkBlocksPresent.clone();
        codec.decode(dataBlocks, checkBlocks, dataBlocksPresent, checkBlocksPresent, BLOCK_SIZE);
        assertEquals(originalDataBlocks, dataBlocks);
        assertTrue(Arrays.equals(oldDataBlocksPresent, dataBlocksPresent));
        assertTrue(Arrays.equals(oldCheckBlocksPresent, checkBlocksPresent));
        for(int i=0;i<dataBlocksPresent.length;i++) dataBlocksPresent[i] = true;
        codec.encode(dataBlocks, checkBlocks, checkBlocksPresent, BLOCK_SIZE);
        assertEquals(originalCheckBlocks, checkBlocks);
        assertTrue(Arrays.equals(oldCheckBlocksPresent, checkBlocksPresent));
    }
    
    private void deleteRandomBlocks(Random r) {
        int dropped = 0;
        int data = dataBlocks.length;
        int check = checkBlocks.length;
        while(dropped < check) {
            int blockNo = r.nextInt(data+check);
            if(blockNo < data) {
                if(!dataBlocksPresent[blockNo]) continue;
                clear(dataBlocks, blockNo);
                dataBlocksPresent[blockNo] = false;
            } else {
                blockNo -= data;
                if(!checkBlocksPresent[blockNo]) continue;
                clear(checkBlocks, blockNo);
                checkBlocksPresent[blockNo] = false;
            }
            dropped++;
        }
    }

    private void clear(byte[][] dataBlocks, int blockNo) {
        Arrays.fill(dataBlocks[blockNo], (byte)0);
   }

    protected byte[][] createOriginalDataBlocks(Random r, int count) {
        byte[][] blocks = new byte[count][];
        for(int i=0;i<count;i++) {
            blocks[i] = new byte[BLOCK_SIZE];
            r.nextBytes(blocks[i]);
        }
        return blocks;
    }
    
    protected byte[][] setupCheckBlocks(int count) {
        byte[][] blocks = new byte[count][];
        for(int i=0;i<count;i++) {
            blocks[i] = new byte[BLOCK_SIZE];
        }
        return blocks;
    }
    
    protected byte[][] copy(byte[][] blocks) {
        byte[][] ret = new byte[blocks.length][]; // FIXME would blocks.clone() shallow or deep copy?
        for(int i=0;i<ret.length;i++) {
            ret[i] = blocks[i].clone();
        }
        return ret;
    }
    
    private void assertEquals(byte[][] blocks1, byte[][] blocks2) {
        assertEquals(blocks1.length, blocks2.length);
        for(int i=0;i<blocks1.length;i++) {
            assertTrue(Arrays.equals(blocks1[i], blocks2[i]));
        }
    }
    
    private void deleteAllDataBlocks() {
        for(int i=0;i<dataBlocks.length;i++) {
            clear(dataBlocks, i);
            dataBlocksPresent[i] = false;
        }
    }

    private void deleteAllCheckBlocks() {
        for(int i=0;i<checkBlocks.length;i++) {
            clear(checkBlocks, i);
            checkBlocksPresent[i] = false;
        }
    }

}
