import java.util.Random;

public class Benchmark {
    public static void main(String[] args) throws Exception {
        int seconds = 10;
        if (args.length > 0)
            seconds = Integer.parseInt(args[0]);

        byte[] testInput1k = generateTestInput(1000);
        byte[] testInput100k = generateTestInput(100000);

        TestRun[] tests = new TestRun[] {
            new OriginalTest(testInput1k),
            new Array2DTest(testInput1k),
            new Array1DTest(testInput1k),

            new OriginalTest(testInput100k),
            new Array2DTest(testInput100k),
            new Array1DTest(testInput100k),
        };

        for (TestRun test : tests)
            benchmark(seconds, test);
    }

    interface TestRun {
        public void run(int iters) throws Exception;
    }

    private static char[] hexdigit = new char[] {
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'B', 'c', 'D', 'e', 'F',
    };

    private static byte[] generateTestInput(int length) {
        Random rand = new Random(length); // predictable seed

        byte[] result = new byte[length];
        for (int i = 2; i < length; i += 2) {
            int randByte = rand.nextInt(256);
            result[i] = (byte) hexdigit[randByte>>4];
            result[i+1] = (byte) hexdigit[randByte&15];
        }

        return result;
    }

    private static void benchmark(int seconds, TestRun toRun) {
        try {
            final long target = seconds * 1000000000L;
            
            int iters = 1;
            long elapsed;
            
            do {
                iters *= 2;
                
                final long start = System.nanoTime();
                toRun.run(iters);
                final long end = System.nanoTime();
                elapsed = end - start;
            } while (elapsed < target);
            
            final int itersForTarget = (int) ((double) iters * target / elapsed);

            final long start = System.nanoTime();
            toRun.run(itersForTarget);
            final long end = System.nanoTime();
            elapsed = end - start;
            
            double nsPerIter = (double) elapsed / itersForTarget;
            
            System.err.println(String.format("%-40s   %9d iterations in %8d us  %7.2f us/iteration", toRun.toString(), itersForTarget, elapsed / 1000L, nsPerIter / 1000.0));
        } catch (Throwable t) {
            t.printStackTrace(System.err);
        }
    }

    private static final class OriginalTest implements TestRun {
        private final byte[] input;

        OriginalTest(final byte[] input) {
            this.input = input;
        }

        public void run(int iters) throws Exception {
            for (int i = 0; i < iters; ++i) {
                toBytesHexEscaped(input);
            }
        }

        private static byte[] toBytesHexEscaped(final byte[] s) {
            final byte[] output = new byte[(s.length - 2) / 2];
            for (int i=0; i<output.length; i++) {
                byte b1 = gethex(s[2 + i*2]);
                byte b2 = gethex(s[2 + i*2 + 1]);
                output[i] = (byte)((b1 << 4) | b2);
            }
            return output;
        }
        
        private static byte gethex(byte b) {
            // 0-9 == 48-57
            if (b <= 57)
                return (byte)(b - 48);
            
            // a-f == 97-102
            if (b >= 97)
                return (byte)(b - 97 + 10);
            
            // A-F == 65-70
            return (byte)(b - 65 + 10);
        }

        public String toString() {
            return "original code, " + input.length;
        }
    }

    private static final class Array2DTest implements TestRun {
        private final byte[] input;
        
        private static final byte[][] HEX_LOOKUP = new byte[103][103];
        
        static {
            // build the hex lookup table
            for (int i = 0; i < 256; ++i) {
                int lo = i & 0x0f;
                int lo2 = lo;
                int hi = (i & 0xf0) >> 4;
                int hi2 = hi;
                // 0-9 == 48-57
                // a-f == 97-102
                // A-F == 65-70
                if (lo < 10) {
                    lo += 48;
                    lo2 = lo;
                } else {
                    lo += 87;
                    lo2 += 55;
                }
                if (hi < 10) {
                    hi += 48;
                    hi2 = hi;
                } else {
                    hi += 87;
                    hi2 += 55;
                }
                HEX_LOOKUP[lo] [hi] = (byte)i;
                HEX_LOOKUP[lo2][hi] = (byte)i;
                HEX_LOOKUP[lo] [hi2] = (byte)i;
                HEX_LOOKUP[lo2][hi2] = (byte)i;
            }
        }
        
        Array2DTest(final byte[] input) {
            this.input = input;
        }

        public void run(int iters) throws Exception {
            for (int i = 0; i < iters; ++i) {
                toBytesHexEscaped(input);
            }
        }

        private static byte[] toBytesHexEscaped(final byte[] s) {
            final byte[] output = new byte[(s.length - 2) / 2];
            for (int i=0; i<output.length; i++)
                output[i] = HEX_LOOKUP[s[2 + i*2+1]][s[2 + i*2]];
            return output;
        }

        public String toString() {
            return "2D array lookup, " + input.length;
        }
    }

    private static final class Array1DTest implements TestRun {
        private final byte[] input;
        
        private static final int[] HEX_LOOKUP = new int[256];
        
        static {
            // build the hex lookup table
            for (int i = 0; i < 16; ++i) {
                if (i <= 9)
                    HEX_LOOKUP['0' + i] = i;
                else {
                    HEX_LOOKUP['a' + i] = i;
                    HEX_LOOKUP['A' + i] = i;
                }
            }
        }
        
        Array1DTest(final byte[] input) {
            this.input = input;
        }

        public void run(int iters) throws Exception {
            for (int i = 0; i < iters; ++i) {
                toBytesHexEscaped(input);
            }
        }

        private static byte[] toBytesHexEscaped(final byte[] s) {
            final byte[] output = new byte[(s.length - 2) / 2];
            for (int i=0; i<output.length; i++)
                output[i] = (byte) ((HEX_LOOKUP[s[2 + i*2]] << 4) | HEX_LOOKUP[s[2 + i*2+1]]);
            return output;
        }

        public String toString() {
            return "1D array lookup, " + input.length;
        }
    }
}