/*
 * Copyright 2015-2016 Groupon, Inc
 * Copyright 2015-2016 The Billing Project, LLC
 *
 * The Billing Project licenses this file to you under the Apache License, version 2.0
 * (the "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at:
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package org.killbill.billing.util;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Random;
import java.util.UUID;

/**
 * UUIDs helper.
 *
 * @author kares
 */
public abstract class UUIDs {

    public static UUID randomUUID() { return rndUUIDv4(); }

    public static void setRandom(final Random random) {
        threadRandom.set(random);
    }

    public static Random getRandom() {
        return threadRandom.get();
    }

    private static UUID rndUUIDv4() {
        // ~ return UUID.randomUUID() :
        final Random random = threadRandom.get();

        final byte[] uuid = new byte[16];
        random.nextBytes(uuid);
        uuid[6]  &= 0x0f;  /* clear version        */
        uuid[6]  |= 0x40;  /* set to version 4     */
        uuid[8]  &= 0x3f;  /* clear variant        */
        uuid[8]  |= 0x80;  /* set to IETF variant  */

        long msb = 0;
        msb = (msb << 8) | (uuid[0] & 0xff);
        msb = (msb << 8) | (uuid[1] & 0xff);
        msb = (msb << 8) | (uuid[2] & 0xff);
        msb = (msb << 8) | (uuid[3] & 0xff);
        msb = (msb << 8) | (uuid[4] & 0xff);
        msb = (msb << 8) | (uuid[5] & 0xff);
        msb = (msb << 8) | (uuid[6] & 0xff);
        msb = (msb << 8) | (uuid[7] & 0xff);

        long lsb = 0;
        lsb = (lsb << 8) | (uuid[8] & 0xff);
        lsb = (lsb << 8) | (uuid[9] & 0xff);
        lsb = (lsb << 8) | (uuid[10] & 0xff);
        lsb = (lsb << 8) | (uuid[11] & 0xff);
        lsb = (lsb << 8) | (uuid[12] & 0xff);
        lsb = (lsb << 8) | (uuid[13] & 0xff);
        lsb = (lsb << 8) | (uuid[14] & 0xff);
        lsb = (lsb << 8) | (uuid[15] & 0xff);

        return new UUID(msb, lsb);
    }

    private static final ThreadLocal<Random> threadRandom =
        new ThreadLocal<Random>() {
            protected Random initialValue() {
                return new LightSecureRandom(); // new SecureRandom();
            }
    };

    /**
     * An implementation of SecureRandom inspired by bouncy-castle's own for
     * light-weight APIs (JDK 1.0, and J2ME).
     *
     * Random generation is based on the traditional SHA1 with
     * counter. Calling setSeed will always increase the entropy of the hash.
     */
    // NOTE: assumes non-concurrent use (generator calls should be synchronized)
    private static class LightSecureRandom extends Random {

        private static abstract class SeederHolder {
            static final SecureRandom seeder;
            /* some related info from the JDK itself :
            # By default, an attempt is made to use the entropy gathering device
            # specified by the "securerandom.source" Security property.  If an
            # exception occurs while accessing the specified URL:
            #
            #     SHA1PRNG:
            #         the traditional system/thread activity algorithm will be used.
            #
            #     NativePRNG:
            #         a default value of /dev/random will be used.  If neither
                      are available, the implementation will be disabled.
            #         "file" is the only currently supported protocol type.
            #
            # The entropy gathering device can also be specified with the System
              property "java.security.egd". For example:
            #
            #   % java -Djava.security.egd=file:/dev/random MainClass
            #
            # Specifying this System property will override the
            # "securerandom.source" Security property.
            #
            # In addition, if "file:/dev/random" or "file:/dev/urandom" is
            # specified, the "NativePRNG" implementation will be preferred over
            # SHA1PRNG in the Sun provider.
            #
            securerandom.source=file:/dev/random
            */
            static {
                SecureRandom random;
                // if the securerandom.seed preference is set to file:/dev/urandom
                // (default) for Linux, new SecureRandom() returns a NativePRNG,
                // which generateSeed()s from /dev/xxx (thus might block terribly)
                // ... instead we explicitly use a SHA1PRNG seeder which will only
                // seeds itself on initialization (possibly from /dev/urandom)
                try {
                    random = SecureRandom.getInstance("SHA1PRNG");
                }
                catch (NoSuchAlgorithmException e) {
                    random = new SecureRandom(); // should never happen
                }
                seeder = random;
            }
        }

        private final DigestRandomGenerator generator;

        LightSecureRandom() {
            this( sha1Generator() );
            setSeed( SeederHolder.seeder.generateSeed( generator.getDigestLength() ) );
        }

        LightSecureRandom(final byte[] inSeed) {
            this( sha1Generator() );
            setSeed(inSeed);
        }

        private LightSecureRandom(DigestRandomGenerator generator) {
            super(0);
            this.generator = generator;
        }

        private static DigestRandomGenerator sha1Generator() {
            try {
                return new DigestRandomGenerator(MessageDigest.getInstance("SHA-1"));
            }
            catch (NoSuchAlgorithmException ex) {
                throw new Error("unexpeced missing SHA-1 digest", ex);
            }
        }

        @Override
        public void setSeed(final long seed) {
            if ( seed != 0 ) { // to avoid problems with Random calling setSeed in construction
                generator.addSeedMaterial(seed);
            }
        }

        public void setSeed(final byte[] seed) {
            generator.addSeedMaterial(seed);
        }

        @Override
        public void nextBytes(byte[] bytes) {
            generator.nextBytes(bytes);
        }

        @Override
        public int nextInt() {
            final byte[] intBytes = new byte[4];

            nextBytes(intBytes);

            int result = 0;

            for ( int i = 0; i < 4; i++ ) {
                result = (result << 8) + (intBytes[i] & 0xff);
            }

            return result;
        }

        @Override
        protected final int next(int numBits) {
            int size = (numBits + 7) / 8;
            byte[] bytes = new byte[size];

            nextBytes(bytes);

            int result = 0;

            for (int i = 0; i < size; i++)
            {
                result = (result << 8) + (bytes[i] & 0xff);
            }

            return result & ((1 << numBits) - 1);
        }

    }

    private static class DigestRandomGenerator {

        private static final long CYCLE_COUNT = 10;

        private long stateCounter;
        private long seedCounter;
        private final MessageDigest digest;
        private byte[] state;
        private byte[] seed;

        private DigestRandomGenerator(MessageDigest digest) {
            this.digest = digest;

            this.seed = new byte[digest.getDigestLength()];
            this.seedCounter = 1;

            this.state = new byte[digest.getDigestLength()];
            this.stateCounter = 1;
        }

        int getDigestLength() { return digest.getDigestLength(); }

        // NOTE: requires external synchronization
        final void addSeedMaterial(byte[] inSeed) {
            //synchronized (this) {
            digestUpdate(inSeed);
            digestUpdate(seed);
            seed = digest.digest(); // digestDoFinal(seed);
            //}
        }

        // NOTE: requires external synchronization
        final void addSeedMaterial(long rSeed) {
            //synchronized (this) {
            digestAddCounter(rSeed);
            digestUpdate(seed);
            seed = digest.digest(); // digestDoFinal(seed);
            //}
        }

        void nextBytes(byte[] bytes) {
            nextBytes(bytes, 0, bytes.length);
        }

        // NOTE: requires external synchronization
        final void nextBytes(byte[] bytes, int start, int len) {
            //synchronized (this) {
            int stateOff = 0;

            generateState();

            int end = start + len;
            for (int i = start; i != end; i++) {
                if (stateOff == state.length) {
                    generateState();
                    stateOff = 0;
                }
                bytes[i] = state[stateOff++];
            }
            //}
        }

        private void cycleSeed() {
            digestUpdate(seed);
            digestAddCounter(seedCounter++);

            seed = digest.digest(); // digestDoFinal(seed);
        }

        private void generateState() {
            digestAddCounter(stateCounter++);
            digestUpdate(state);
            digestUpdate(seed);

            state = digest.digest(); // digestDoFinal(state);

            if ((stateCounter % CYCLE_COUNT) == 0) {
                cycleSeed();
            }
        }

        private void digestAddCounter(long seed) {
            for (int i = 0; i != 8; i++) {
                digest.update((byte) seed);
                seed >>>= 8;
            }
        }

        private void digestUpdate(byte[] inSeed) {
            digest.update(inSeed, 0, inSeed.length);
        }

        //private void digestDoFinal(byte[] result) {
        //    digest.doFinal(result, 0);
        //}

    }

}
