/*
 * 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.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;
      System.arraycopy(byteAddr, 0, contents, pos, 6);

      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 the hardware address.
    */
   public static byte[] getHardwareAddress() {
      try {
         // check if we have enough security permissions to create and shutdown an executor
         ExecutorService executor = Executors.newFixedThreadPool(1, ActiveMQThreadFactory.defaultThreadFactory());
         executor.shutdownNow();
      }
      catch (Throwable t) {
         // not enough security permission
         return null;
      }

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

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

         byte[] address = findFirstMatchingHardwareAddress(ifaces);
         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<>();
         while (networkInterfaces.hasMoreElements()) {
            ifaces.add(networkInterfaces.nextElement());
         }
         return ifaces;
      }
      catch (SocketException e) {
         return Collections.emptyList();
      }
   }

   private static byte[] findFirstMatchingHardwareAddress(List<NetworkInterface> ifaces) {
      ExecutorService executor = Executors.newFixedThreadPool(ifaces.size(), ActiveMQThreadFactory.defaultThreadFactory());
      Collection<Callable<byte[]>> tasks = new ArrayList<>(ifaces.size());

      for (final NetworkInterface networkInterface : ifaces) {
         tasks.add(new Callable<byte[]>() {
            @Override
            public byte[] call() throws Exception {
               boolean up = networkInterface.isUp();
               boolean loopback = networkInterface.isLoopback();
               boolean virtual = networkInterface.isVirtual();

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

               byte[] address = networkInterface.getHardwareAddress();
               if (address != null) {

                  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();
      }
   }
}