/*
 * 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.Objects;
import java.util.UUID;

import org.jgroups.JChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.invoke.MethodHandles;

/**
 * Various utility functions
 */
public final class ActiveMQRaUtils {
   private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

   private ActiveMQRaUtils() {
   }

   /**
    * Compare two strings.
    *
    * @param me  First value
    * @param you Second value
    * @return {@code true} if object equals else false
    */
   public static boolean compare(final String me, final String you) {
      return Objects.equals(me, you);
   }

   /**
    * Compare two integers.
    *
    * @param me  First value
    * @param you Second value
    * @return {@code true} if object equals else false
    */
   public static boolean compare(final Integer me, final Integer you) {
      return Objects.equals(me, you);
   }

   /**
    * Compare two longs.
    *
    * @param me  First value
    * @param you Second value
    * @return {@code true} if object equals else false
    */
   public static boolean compare(final Long me, final Long you) {
      return Objects.equals(me, you);
   }

   /**
    * Compare two doubles.
    *
    * @param me  First value
    * @param you Second value
    * @return {@code true} if object equals else false
    */
   public static boolean compare(final Double me, final Double you) {
      return Objects.equals(me, you);
   }

   /**
    * Compare two booleans.
    *
    * @param me  First value
    * @param you Second value
    * @return {@code true} if object equals else false
    */
   public static boolean compare(final Boolean me, final Boolean you) {
      return Objects.equals(me, 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
    *
    * @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) {
         Map<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((PrivilegedAction<JChannel>) () -> {
         try {
            ClassLoader loader = Thread.currentThread().getContextClassLoader();
            Class<?> aClass = loader.loadClass(locatorClass);
            Object o = aClass.getDeclaredConstructor().newInstance();
            Method m = aClass.getMethod("locateChannel", new Class[]{String.class});
            return (JChannel) m.invoke(o, name);
         } catch (Throwable e) {
            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<>() {
         @Override
         public Object run() {
            ClassLoader loader = getClass().getClassLoader();
            try {
               Class<?> clazz = loader.loadClass(className);
               return clazz.getDeclaredConstructor().newInstance();
            } catch (Throwable t) {
               try {
                  loader = Thread.currentThread().getContextClassLoader();
                  if (loader != null)
                     return loader.loadClass(className).getDeclaredConstructor().newInstance();
               } catch (RuntimeException e) {
                  throw e;
               } catch (Exception e) {
               }

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

}
