/*
 * 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 org.apache.activemq.artemis.utils;

import java.lang.reflect.Method;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.logs.ActiveMQUtilLogger;

public final class UUIDGenerator
{
   private static final UUIDGenerator sSingleton = new UUIDGenerator();

   // Windows has some fake adapters that will return the same HARDWARE ADDRESS on any computer. We need to ignore those
   private static final byte[][] BLACK_LIST = new byte[][]{{2, 0, 84, 85, 78, 1}};

   /**
    * Random-generator, used by various UUID-generation methods:
    */
   private Random mRnd = null;

   private final Object mTimerLock = new Object();

   private UUIDTimer mTimer = null;

   private byte[] address;

   /**
    * Constructor is private to enforce singleton access.
    */
   private UUIDGenerator()
   {
   }

   /**
    * Method used for accessing the singleton generator instance.
    *
    * @return Instance of UUID Generator
    */
   public static UUIDGenerator getInstance()
   {
      return UUIDGenerator.sSingleton;
   }

   /*
    * ///////////////////////////////////////////////////// // Configuration
    * /////////////////////////////////////////////////////
    */

   /**
    * Method for getting the shared random number generator used for generating
    * the UUIDs. This way the initialization cost is only taken once; access
    * need not be synchronized (or in cases where it has to, SecureRandom takes
    * care of it); it might even be good for getting really 'random' stuff to
    * get shared access..
    *
    * @return A Random number generator.
    */
   public Random getRandomNumberGenerator()
   {
      /*
       * Could be synchronized, but since side effects are trivial (ie.
       * possibility of generating more than one SecureRandom, of which all but
       * one are dumped) let's not add synchronization overhead:
       */
      if (mRnd == null)
      {
         mRnd = new SecureRandom();
      }
      return mRnd;
   }

   public UUID generateTimeBasedUUID(final byte[] byteAddr)
   {
      byte[] contents = new byte[16];
      int pos = 10;
      for (int i = 0; i < 6; ++i)
      {
         contents[pos + i] = byteAddr[i];
      }

      synchronized (mTimerLock)
      {
         if (mTimer == null)
         {
            mTimer = new UUIDTimer(getRandomNumberGenerator());
         }

         mTimer.getTimestamp(contents);
      }

      return new UUID(UUID.TYPE_TIME_BASED, contents);
   }

   public byte[] generateDummyAddress()
   {
      Random rnd = getRandomNumberGenerator();
      byte[] dummy = new byte[6];
      rnd.nextBytes(dummy);
      /* Need to set the broadcast bit to indicate it's not a real
       * address.
       */
      dummy[0] |= (byte) 0x01;

      if (ActiveMQUtilLogger.LOGGER.isDebugEnabled())
      {
         ActiveMQUtilLogger.LOGGER.debug("using dummy address " + UUIDGenerator.asString(dummy));
      }
      return dummy;
   }

   /**
    * If running java 6 or above, returns {@link NetworkInterface#getHardwareAddress()}, else return {@code null}.
    * The first hardware address is returned when iterating all the NetworkInterfaces
    *
    * @return A byte array containing teh hardware address.
    */
   public static byte[] getHardwareAddress()
   {
      Method getHardwareAddressMethod;
      Method isUpMethod;
      Method isLoopbackMethod;
      Method isVirtualMethod;
      try
      {
         getHardwareAddressMethod = NetworkInterface.class.getMethod("getHardwareAddress");
         isUpMethod = NetworkInterface.class.getMethod("isUp");
         isLoopbackMethod = NetworkInterface.class.getMethod("isLoopback");
         isVirtualMethod = NetworkInterface.class.getMethod("isVirtual");
         // check if we have enough security permissions to create and shutdown an executor
         ExecutorService executor = Executors.newFixedThreadPool(0);
         executor.shutdownNow();
      }
      catch (Throwable t)
      {
         // not on Java 6 or not enough security permission
         return null;
      }

      try
      {
         List<NetworkInterface> ifaces = getAllNetworkInterfaces();

         if (ifaces.size() == 0)
         {
            return null;
         }

         byte[] address = findFirstMatchingHardwareAddress(ifaces,
                                                           getHardwareAddressMethod,
                                                           isUpMethod,
                                                           isLoopbackMethod,
                                                           isVirtualMethod);
         if (address != null)
         {
            if (ActiveMQUtilLogger.LOGGER.isDebugEnabled())
            {
               ActiveMQUtilLogger.LOGGER.debug("using hardware address " + UUIDGenerator.asString(address));
            }
            return address;
         }
         return null;
      }
      catch (Exception e)
      {
         return null;
      }
   }

   public SimpleString generateSimpleStringUUID()
   {
      return new SimpleString(generateStringUUID());
   }

   public UUID generateUUID()
   {
      byte[] address = getAddressBytes();

      UUID uid = generateTimeBasedUUID(address);

      return uid;
   }

   public String generateStringUUID()
   {
      byte[] address = getAddressBytes();

      if (address == null)
      {
         return java.util.UUID.randomUUID().toString();
      }
      else
      {
         return generateTimeBasedUUID(address).toString();
      }
   }

   public static byte[] getZeroPaddedSixBytes(final byte[] bytes)
   {
      if (bytes == null)
      {
         return null;
      }
      if (bytes.length > 0 && bytes.length <= 6)
      {
         if (bytes.length == 6)
         {
            return bytes;
         }
         else
         {
            // pad with zeroes to have a 6-byte array
            byte[] paddedAddress = new byte[6];
            System.arraycopy(bytes, 0, paddedAddress, 0, bytes.length);
            for (int i = bytes.length; i < 6; i++)
            {
               paddedAddress[i] = 0;
            }
            return paddedAddress;
         }
      }
      return null;
   }

   // Private -------------------------------------------------------

   private static boolean isBlackList(final byte[] address)
   {
      for (byte[] blackList : UUIDGenerator.BLACK_LIST)
      {
         if (Arrays.equals(address, blackList))
         {
            return true;
         }
      }
      return false;
   }

   private byte[] getAddressBytes()
   {
      if (address == null)
      {
         address = UUIDGenerator.getHardwareAddress();
         if (address == null)
         {
            address = generateDummyAddress();
         }
      }

      return address;
   }

   private static String asString(final byte[] bytes)
   {
      if (bytes == null)
      {
         return null;
      }

      StringBuilder s = new StringBuilder();
      for (int i = 0; i < bytes.length - 1; i++)
      {
         s.append(Integer.toHexString(bytes[i]));
         s.append(":");
      }
      s.append(bytes[bytes.length - 1]);
      return s.toString();
   }

   private static List<NetworkInterface> getAllNetworkInterfaces()
   {
      Enumeration<NetworkInterface> networkInterfaces;
      try
      {
         networkInterfaces = NetworkInterface.getNetworkInterfaces();

         List<NetworkInterface> ifaces = new ArrayList<NetworkInterface>();
         while (networkInterfaces.hasMoreElements())
         {
            ifaces.add(networkInterfaces.nextElement());
         }
         return ifaces;
      }
      catch (SocketException e)
      {
         return Collections.emptyList();
      }
   }

   private static byte[] findFirstMatchingHardwareAddress(List<NetworkInterface> ifaces,
                                                          final Method getHardwareAddressMethod,
                                                          final Method isUpMethod,
                                                          final Method isLoopbackMethod,
                                                          final Method isVirtualMethod)
   {
      ExecutorService executor = Executors.newFixedThreadPool(ifaces.size());
      Collection<Callable<byte[]>> tasks = new ArrayList<Callable<byte[]>>(ifaces.size());

      for (final NetworkInterface networkInterface : ifaces)
      {
         tasks.add(new Callable<byte[]>()
         {
            public byte[] call() throws Exception
            {
               boolean up = (Boolean) isUpMethod.invoke(networkInterface);
               boolean loopback = (Boolean) isLoopbackMethod.invoke(networkInterface);
               boolean virtual = (Boolean) isVirtualMethod.invoke(networkInterface);

               if (loopback || virtual || !up)
               {
                  throw new Exception("not suitable interface");
               }

               Object res = getHardwareAddressMethod.invoke(networkInterface);
               if (res != null && res instanceof byte[])
               {

                  byte[] address = (byte[]) res;
                  byte[] paddedAddress = UUIDGenerator.getZeroPaddedSixBytes(address);

                  if (UUIDGenerator.isBlackList(address))
                  {
                     throw new Exception("black listed address");
                  }

                  if (paddedAddress != null)
                  {
                     return paddedAddress;
                  }
               }

               throw new Exception("invalid network interface");
            }
         });
      }
      try
      {
         // we wait 5 seconds to get the first matching hardware address. After that, we give up and return null
         byte[] address = executor.invokeAny(tasks, 5, TimeUnit.SECONDS);
         return address;
      }
      catch (Exception e)
      {
         return null;
      }
      finally
      {
         executor.shutdownNow();
      }
   }
}
