/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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 flex.messaging.util;

import java.util.Random;
import java.util.UUID;

public class UUIDUtils {
    private static Random _weakRand = new Random();

    /**
     * The spec indicates that our time value should be based on 100 nano
     * second increments but our time granularity is in milliseconds.
     * The spec also says we can approximate the time by doing an increment
     * when we dole out new ids in the same millisecond.  We can fit 10,000
     * 100 nanos into a single millisecond.
     */
    private static final int MAX_IDS_PER_MILLI = 10000;

    /**
     * Any given time-of-day value can only be used once; remember the last used
     * value so we don't reuse them.
     * <p>NOTE: this algorithm assumes the clock will not be turned back.
     */
    private static long lastUsedTOD = 0;
    /**
     * Counter to use when we need more than one id in the same millisecond.
     */
    private static int numIdsThisMilli = 0;

    /**
     * Hex digits, used for padding UUID strings with random characters.
     */
    private static final String alphaNum = "0123456789ABCDEF";

    /**
     * 4 bits per hex character.
     */
    private static final int BITS_PER_DIGIT = 4;

    private static final int BITS_PER_INT = 32;
    private static final int BITS_PER_LONG = 64;
    private static final int DIGITS_PER_INT = BITS_PER_INT / BITS_PER_DIGIT;
    private static final int DIGITS_PER_LONG = BITS_PER_LONG / BITS_PER_DIGIT;

    /**
     * @private
     */
    private static char[] UPPER_DIGITS = new char[]{
            '0', '1', '2', '3', '4', '5', '6', '7',
            '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
    };

    /**
     * Private constructor to prevent instances from being created.
     */
    private UUIDUtils() {
    }

    /**
     * Use the createUUID function when you need a unique string that you will
     * use as a persistent identifier in a distributed environment. To a very
     * high degree of certainty, this function returns a unique value; no other
     * invocation on the same or any other system should return the same value.
     *
     * @return a Universally Unique Identifier (UUID)
     * Proper Format: `XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'
     * where `X' stands for a hexadecimal digit (0-9 or A-F).
     */
    public static String createUUID() {
        return createUUID(false);
    }

    public static String createUUID(boolean secure) throws Error {
        Random rand = _weakRand;
        if (secure)
            throw new Error("Secure UUIDs not implemented");

        StringBuffer s = new StringBuffer(36);

        appendHexString(uniqueTOD(), false, 11, s);

        //  Just use random padding characters, but ensure that the high bit
        //  is set to eliminate chances of collision with an IEEE 802 address.
        s.append(alphaNum.charAt(rand.nextInt(16) | 8));

        //  Add random padding characters.
        appendRandomHexChars(32 - s.length(), rand, s);

        //insert dashes in proper position. so the format matches CF
        s.insert(8, "-");
        s.insert(13, "-");
        s.insert(18, "-");
        s.insert(23, "-");

        return s.toString();
    }

    /**
     * Converts a 128-bit UID encoded as a byte[] to a String representation.
     * The format matches that generated by createUID. If a suitable byte[]
     * is not provided, null is returned.
     *
     * @param ba byte[] 16 bytes in length representing a 128-bit UID.
     * @return String representation of the UID, or null if an invalid
     * byte[] is provided.
     */
    public static String fromByteArray(byte[] ba) {
        if (ba == null || ba.length != 16)
            return null;

        StringBuffer result = new StringBuffer(36);
        for (int i = 0; i < 16; i++) {
            if (i == 4 || i == 6 || i == 8 || i == 10)
                result.append('-');

            result.append(UPPER_DIGITS[(ba[i] & 0xF0) >>> 4]);
            result.append(UPPER_DIGITS[(ba[i] & 0x0F)]);
        }
        return result.toString();
    }


    /**
     * A utility method to check whether a String value represents a
     * correctly formatted UID value. UID values are expected to be
     * in the format generated by createUID(), implying that only
     * capitalized A-F characters in addition to 0-9 digits are
     * supported.
     *
     * @param uid The value to test whether it is formatted as a UID.
     * @return Returns true if the value is formatted as a UID.
     */
    public static boolean isUID(String uid) {
        if (uid == null || uid.length() != 36)
            return false;

        char[] chars = uid.toCharArray();
        for (int i = 0; i < 36; i++) {
            char c = chars[i];

            // Check for correctly placed hyphens
            if (i == 8 || i == 13 || i == 18 || i == 23) {
                if (c != '-')
                    return false;
            }
            // We allow capital alpha-numeric hex digits only
            else if (c < 48 || c > 70 || (c > 57 && c < 65)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Converts a UID formatted String to a byte[]. The UID must be in the
     * format generated by createUID, otherwise null is returned.
     *
     * @param uid String representing a 128-bit UID.
     * @return byte[] 16 bytes in length representing the 128-bits of the
     * UID or null if the uid could not be converted.
     */
    public static byte[] toByteArray(String uid) {
        if (!isUID(uid))
            return null;

        byte[] result = new byte[16];
        char[] chars = uid.toCharArray();
        int r = 0;

        for (int i = 0; i < chars.length; i++) {
            if (chars[i] == '-')
                continue;
            int h1 = Character.digit(chars[i], 16);
            i++;
            int h2 = Character.digit(chars[i], 16);
            result[r++] = (byte) (((h1 << 4) | h2) & 0xFF);
        }
        return result;
    }

    private static void appendRandomHexChars(int n, Random rand, StringBuffer result) {
        int digitsPerInt = DIGITS_PER_INT;
        while (n > 0) {
            int digitsToUse = Math.min(n, digitsPerInt);
            n -= digitsToUse;
            appendHexString(rand.nextInt(), true, digitsToUse, result);
        }
    }

    private static void appendHexString
            (long value, boolean prependZeroes, int nLeastSignificantDigits,
             StringBuffer result) {
        int bitsPerDigit = BITS_PER_DIGIT;

        long mask = (1L << bitsPerDigit) - 1;

        if (nLeastSignificantDigits < DIGITS_PER_LONG) {
            // Clear the bits that we don't care about.
            value &= (1L << (bitsPerDigit * nLeastSignificantDigits)) - 1;
        }

        // Reorder the sequence so that the first set of bits will become the
        // last set of bits.
        int i = 0;
        long reorderedValue = 0;
        if (value == 0) {
            // One zero is dumped.
            i++;
        } else {
            do {
                reorderedValue = (reorderedValue << bitsPerDigit) | (value & mask);
                value >>>= bitsPerDigit;
                i++;
            } while (value != 0);
        }

        if (prependZeroes) {
            for (int j = nLeastSignificantDigits - i; j > 0; j--) {
                result.append('0');
            }
        }


        // Dump the reordered sequence, with the most significant character
        // first.
        for (; i > 0; i--) {
            result.append(alphaNum.charAt((int) (reorderedValue & mask)));
            reorderedValue >>>= bitsPerDigit;
        }
    }

    private static String createInsecureUUID() {
        StringBuffer s = new StringBuffer(36);

        appendHexString(uniqueTOD(), false, 11, s);

        //  Just use random padding characters, but ensure that the high bit
        //  is set to eliminate chances of collision with an IEEE 802 address.
        s.append(alphaNum.charAt(_weakRand.nextInt(16) | 8));

        //  Add random padding characters.
        appendRandomHexChars(32 - s.length(), _weakRand, s);

        //insert dashes in proper position. so the format matches CF
        s.insert(8, "-");
        s.insert(13, "-");
        s.insert(18, "-");
        s.insert(23, "-");

        return s.toString();
    }

    /**
     * @return a time value, unique for calls to this method loaded by the same classloader.
     */
    private static synchronized long uniqueTOD() {
        long currentTOD = System.currentTimeMillis();

        // Clock was set back... do not hang in this case waiting to catch up.
        // Instead, rely on the random number part to differentiate the ids.
        if (currentTOD < lastUsedTOD)
            lastUsedTOD = currentTOD;

        if (currentTOD == lastUsedTOD) {
            numIdsThisMilli++;
            /*
             * Fall back to the old technique of sleeping if we allocate
             * too many ids in one time interval.
             */
            if (numIdsThisMilli >= MAX_IDS_PER_MILLI) {
                while (currentTOD == lastUsedTOD) {
                    try {
                        Thread.sleep(1);
                    } catch (Exception interrupt) { /* swallow, wake up */ }
                    currentTOD = System.currentTimeMillis();
                }
                lastUsedTOD = currentTOD;
                numIdsThisMilli = 0;
            }
        } else {
            //  We have a new TOD, reset the counter
            lastUsedTOD = currentTOD;
            numIdsThisMilli = 0;
        }

        return lastUsedTOD * MAX_IDS_PER_MILLI + (long) numIdsThisMilli;
    }
}
