package org.bouncycastle.util.encoders.test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Random;

import junit.framework.TestCase;
import org.bouncycastle.util.encoders.Encoder;

public abstract class AbstractCoderTest extends TestCase
{

    private static final int[] SIZES_TO_CHECK = {64, 128, 1024, 1025, 1026, 2048,
        2049, 2050, 4096, 4097, 4098, 8192, 8193, 8194};
    
    protected Encoder enc;
    private Random r;

    AbstractCoderTest(
        String    name)
    {
        super(name);
    }
    
    protected void setUp()
    {
        r = new Random();
    }

    private void checkArrayOfSize(int size) 
        throws IOException
    {
        byte[] original = new byte[size];
        r.nextBytes(original);
        
        ByteArrayOutputStream    bOut = new ByteArrayOutputStream();
        enc.encode(original, 0, original.length, bOut);
        
        byte[] encoded = bOut.toByteArray();

        assertTrue(encoded.length > original.length);
        assertTrue(encoded.length <= (original.length * 2));
        checkEncoding(encoded);
        checkSimpleDecode(original, encoded);
        checkStringDecode(original, encoded);
        checkOutputStreamDecode(original, encoded);
        
        int    offset = r.nextInt(20);
        byte[] offsetEncoded = new byte[offset + encoded.length];
        System.arraycopy(encoded, 0, offsetEncoded, offset, encoded.length);
        checkOffsetDecode(original, offsetEncoded, offset, encoded.length);
        
        offset = r.nextInt(20);
        byte[] offsetOriginal = new byte[offset + original.length];
        System.arraycopy(original, 0, offsetOriginal, offset, original.length);
        checkOffsetEncode(original, offsetOriginal, offset, original.length);
        
        byte[] encodedWithSpace = addWhitespace(encoded);
        checkSimpleDecode(original, encodedWithSpace);
        checkStringDecode(original, encodedWithSpace);
        checkOutputStreamDecode(original, encodedWithSpace);
    }

    public void testEncode()
        throws IOException
    {
        for (int i = 0; i < SIZES_TO_CHECK.length; i++)
        {
            checkArrayOfSize(SIZES_TO_CHECK[i]);
        }
    }

    private void checkEncoding(byte[] encoded)
    {
        String encString = convertBytesToString(encoded);
        for (int i = 0; i < encString.length(); i++)
        {
            char c = encString.charAt(i);
            if (c == paddingChar())
            {
                // should only be padding at end of string
                if (this instanceof Base32Test)
                {
                    assertTrue(i > encString.length() - 7);
                }
                else
                {
                    assertTrue(i > encString.length() - 3);
                }
                continue;
            }
            else if (isEncodedChar(c))
            {
                continue;
            }
            fail("Unexpected encoded character " + c);
        }
    }

    private void checkOutputStreamDecode(byte[] original, byte[] encoded)
    {
        String encString = convertBytesToString(encoded);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try
        {
            assertEquals(original.length, enc.decode(encString, out));
            assertTrue(Arrays.equals(original, out.toByteArray()));
        }
        catch (IOException e)
        {
            fail("This shouldn't happen");
        }
    }

    private void checkSimpleDecode(byte[] original, byte[] encoded) 
        throws IOException
    {
        ByteArrayOutputStream    bOut = new ByteArrayOutputStream();
        enc.decode(encoded, 0, encoded.length, bOut);

        assertTrue(Arrays.equals(original, bOut.toByteArray()));
    }
    
    private void checkOffsetEncode(byte[] original, byte[] offsetOriginal, int off, int length) 
        throws IOException
    {
        ByteArrayOutputStream    bOut = new ByteArrayOutputStream();
        
        enc.encode(offsetOriginal, off, length, bOut);
        
        byte[] encoded = bOut.toByteArray();

        assertEquals(enc.getEncodedLength(length), encoded.length);

        bOut.reset();
        
        enc.decode(encoded, 0, encoded.length, bOut);

        assertTrue(Arrays.equals(original, bOut.toByteArray()));
    }
    
    private void checkOffsetDecode(byte[] original, byte[] encoded, int off, int length) 
        throws IOException
    {
        ByteArrayOutputStream    bOut = new ByteArrayOutputStream();
        enc.decode(encoded, off, length, bOut);

        assertTrue(Arrays.equals(original, bOut.toByteArray()));
    }
    
    private void checkStringDecode(byte[] original, byte[] encoded) 
        throws IOException
    {
        String encString = convertBytesToString(encoded);
        ByteArrayOutputStream    bOut = new ByteArrayOutputStream();
        enc.decode(encString, bOut);
        assertTrue(Arrays.equals(original, bOut.toByteArray()));
    }

    private byte[] addWhitespace(byte[] encoded)
    {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        addSpace(out);
        for (int i = 0; i < encoded.length - 5; i++)
        {
            out.write(encoded, i, 1);
            if (r.nextInt(100) < 5)
            {
                addSpace(out);
            }
        }
        for (int i = encoded.length - 5; i < encoded.length; i++)
        {
            out.write(encoded, i, 1);
        }
        addSpace(out);
        return out.toByteArray();
    }

    private void addSpace(ByteArrayOutputStream out)
    {
        do
        {
            switch (r.nextInt(3))
            {
                case 0 :
                    out.write((int) '\n');
                    break;
                case 1 :
                    out.write((int) '\r');
                    break;
                case 2 :
                    out.write((int) '\t');
                    break;
                case 3 :
                    out.write((int) ' ');
                    break;
            }
        } while (r.nextBoolean());
    }

    private String convertBytesToString(byte[] encoded)
    {
        StringBuilder    b = new StringBuilder();

        for (int i = 0; i != encoded.length; i++)
        {
            b.append((char)(encoded[i] & 0xff));
        }
        
        return b.toString();
    }

    abstract protected char paddingChar();

    abstract protected boolean isEncodedChar(char c);

}
