class MD5 {

    private static final int INIT_A = 0x67452301
    private static final int INIT_B = (int)0xEFCDAB89L
    private static final int INIT_C = (int)0x98BADCFEL
    private static final int INIT_D = 0x10325476

    private static final int[] SHIFT_AMTS = [
            7, 12, 17, 22,
            5,  9, 14, 20,
            4, 11, 16, 23,
            6, 10, 15, 21
    ]

    private static final int[] TABLE_T = new int[64]
    static
    {
        for (int i in 0..63)
            TABLE_T[i] = (int)(long)((1L << 32) * Math.abs(Math.sin(i + 1)))
    }

    static byte[] computeMD5(byte[] message)
    {
        int messageLenBytes = message.length
        int numBlocks = ((messageLenBytes + 8) >>> 6) + 1
        int totalLen = numBlocks << 6
        byte[] paddingBytes = new byte[totalLen - messageLenBytes]
        paddingBytes[0] = (byte)0x80

        long messageLenBits = (long)messageLenBytes << 3
        for (int i in 0..7)
        {
            paddingBytes[paddingBytes.length - 8 + i] = (byte)messageLenBits
            messageLenBits >>>= 8
        }

        int a = INIT_A
        int b = INIT_B
        int c = INIT_C
        int d = INIT_D
        int[] buffer = new int[16]
        for (int i in 0..(numBlocks - 1))
        {
            int index = i << 6
            for (int j in 0..63) {
                buffer[j >>> 2] = ((int) ((index < messageLenBytes) ? message[index]: paddingBytes[index - messageLenBytes]) << 24) | (buffer[j >>> 2] >>> 8)
                index++
            }
            int originalA = a
            int originalB = b
            int originalC = c
            int originalD = d
            for (int j in 0..63)
            {
                int div16 = j >>> 4
                int f = 0
                int bufferIndex = j
                switch (div16)
                {
                    case 0:
                        f = (b & c) | (~b & d)
                        break

                    case 1:
                        f = (b & d) | (c & ~d)
                        bufferIndex = (bufferIndex * 5 + 1) & 0x0F
                        break

                    case 2:
                        f = b ^ c ^ d
                        bufferIndex = (bufferIndex * 3 + 5) & 0x0F
                        break

                    case 3:
                        f = c ^ (b | ~d)
                        bufferIndex = (bufferIndex * 7) & 0x0F
                        break
                }
                int temp = b + Integer.rotateLeft(a + f + buffer[bufferIndex] + TABLE_T[j], SHIFT_AMTS[(div16 << 2) | (j & 3)])
                a = d
                d = c
                c = b
                b = temp
            }

            a += originalA
            b += originalB
            c += originalC
            d += originalD
        }

        byte[] md5 = new byte[16]
        int count = 0
        for (int i in 0..3)
        {
            int n = (i == 0) ? a: ((i == 1) ? b: ((i == 2) ? c: d))
            for (int j in 0..3)
            {
                md5[count++] = (byte)n
                n >>>= 8
            }
        }
        return md5
    }

    static String toHexString(byte[] b)
    {
        StringBuilder sb = new StringBuilder()
        for (int i in 0..(b.length - 1))
        {
            sb.append(String.format("%02X", b[i] & 0xFF))
        }
        return sb.toString()
    }

    static void main(String[] args)
    {
        String[] testStrings = ["", "a", "abc", "message digest", "abcdefghijklmnopqrstuvwxyz",
                                "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
                                "12345678901234567890123456789012345678901234567890123456789012345678901234567890" ]
        for (String s: testStrings)
            System.out.println("0x" + toHexString(computeMD5(s.getBytes())) + " <== \"" + s + "\"")
    }

}