/*
 * 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.ra;

import javax.naming.Context;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.jgroups.JChannel;

/**
 * Various utility functions
 */
public final class ActiveMQRaUtils {

   /**
    * Private constructor
    */
   private ActiveMQRaUtils() {
   }

   /**
    * Compare two strings.
    *
    * @param me  First value
    * @param you Second value
    * @return True if object equals else false.
    */
   @SuppressWarnings("StringEquality")
   public static boolean compare(final String me, final String you) {
      // If both null or intern equals
      if (me == you) {
         return true;
      }

      // if me null and you are not
      if (me == null) {
         return false;
      }

      // me will not be null, test for equality
      return me.equals(you);
   }

   /**
    * Compare two integers.
    *
    * @param me  First value
    * @param you Second value
    * @return True if object equals else false.
    */
   public static boolean compare(final Integer me, final Integer you) {
      // If both null or intern equals
      if (me == you) {
         return true;
      }

      // if me null and you are not
      if (me == null) {
         return false;
      }

      // me will not be null, test for equality
      return me.equals(you);
   }

   /**
    * Compare two longs.
    *
    * @param me  First value
    * @param you Second value
    * @return True if object equals else false.
    */
   public static boolean compare(final Long me, final Long you) {
      // If both null or intern equals
      if (me == you) {
         return true;
      }

      // if me null and you are not
      if (me == null) {
         return false;
      }

      // me will not be null, test for equality
      return me.equals(you);
   }

   /**
    * Compare two doubles.
    *
    * @param me  First value
    * @param you Second value
    * @return True if object equals else false.
    */
   public static boolean compare(final Double me, final Double you) {
      // If both null or intern equals
      if (me == you) {
         return true;
      }

      // if me null and you are not
      if (me == null) {
         return false;
      }

      // me will not be null, test for equality
      return me.equals(you);
   }

   /**
    * Compare two booleans.
    *
    * @param me  First value
    * @param you Second value
    * @return True if object equals else false.
    */
   public static boolean compare(final Boolean me, final Boolean you) {
      // If both null or intern equals
      if (me == you) {
         return true;
      }

      // if me null and you are not
      if (me == null) {
         return false;
      }

      // me will not be null, test for equality
      return me.equals(you);
   }

   /**
    * Lookup an object in the default initial context
    *
    * @param context The context to use
    * @param name    the name to lookup
    * @param clazz   the expected type
    * @return the object
    * @throws Exception for any error
    */
   public static Object lookup(final Context context, final String name, final Class<?> clazz) throws Exception {
      return context.lookup(name);
   }

   /**
    * Used on parsing JNDI Configuration
    *
    * @param config
    * @return hash-table with configuration option pairs
    */
   public static Hashtable<String, String> parseHashtableConfig(final String config) {
      Hashtable<String, String> hashtable = new Hashtable<>();

      String[] topElements = config.split(";");

      for (String element : topElements) {
         String[] expression = element.split("=");

         if (expression.length != 2) {
            throw new IllegalArgumentException("Invalid expression " + element + " at " + config);
         }

         hashtable.put(expression[0].trim(), expression[1].trim());
      }

      return hashtable;
   }

   public static List<Map<String, Object>> parseConfig(final String config) {
      List<Map<String, Object>> result = new ArrayList<>();

      /**
       * Some configuration values can contain commas (e.g. enabledProtocols, enabledCipherSuites, etc.).
       * To support config values with commas, the commas in the values must be escaped (e.g. "\\,") so that
       * the commas used to separate configs for different connectors can still function as designed.
       */
      String commaPlaceHolder = UUID.randomUUID().toString();
      String replaced = config.replace("\\,", commaPlaceHolder);

      String[] topElements = replaced.split(",");

      for (String topElement : topElements) {
         HashMap<String, Object> map = new HashMap<>();
         result.add(map);

         String[] elements = topElement.split(";");

         for (String element : elements) {
            String[] expression = element.split("=");

            if (expression.length != 2) {
               throw new IllegalArgumentException("Invalid expression " + element + " at " + config);
            }

            // put the commas back
            map.put(expression[0].trim(), expression[1].trim().replace(commaPlaceHolder, ","));
         }
      }

      return result;
   }

   public static List<String> parseConnectorConnectorConfig(String config) {
      List<String> res = new ArrayList<>();

      String[] elements = config.split(",");

      for (String element : elements) {
         res.add(element.trim());
      }

      return res;
   }

   /**
    * Within AS7 the RA is loaded by JCA. properties can only be passed in String form. However if
    * RA is configured using jgroups stack, we need to pass a Channel object. As is impossible with
    * JCA, we use this method to allow a JChannel object to be located.
    */
   public static JChannel locateJGroupsChannel(final String locatorClass, final String name) {
      return AccessController.doPrivileged(new PrivilegedAction<JChannel>() {
         @Override
         public JChannel run() {
            try {
               ClassLoader loader = Thread.currentThread().getContextClassLoader();
               Class<?> aClass = loader.loadClass(locatorClass);
               Object o = aClass.newInstance();
               Method m = aClass.getMethod("locateChannel", new Class[]{String.class});
               return (JChannel) m.invoke(o, name);
            } catch (Throwable e) {
               ActiveMQRALogger.LOGGER.debug(e.getMessage(), e);
               return null;
            }
         }
      });
   }

   /**
    * This seems duplicate code all over the place, but for security reasons we can't let something like this to be open in a
    * utility class, as it would be a door to load anything you like in a safe VM.
    * For that reason any class trying to do a privileged block should do with the AccessController directly.
    */
   private static Object safeInitNewInstance(final String className) {
      return AccessController.doPrivileged(new PrivilegedAction<Object>() {
         @Override
         public Object run() {
            ClassLoader loader = getClass().getClassLoader();
            try {
               Class<?> clazz = loader.loadClass(className);
               return clazz.newInstance();
            } catch (Throwable t) {
               try {
                  loader = Thread.currentThread().getContextClassLoader();
                  if (loader != null)
                     return loader.loadClass(className).newInstance();
               } catch (RuntimeException e) {
                  throw e;
               } catch (Exception e) {
               }

               throw new IllegalArgumentException("Could not find class " + className);
            }
         }
      });
   }

}
