/*
 * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.jersey.internal.util;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import jakarta.ws.rs.RuntimeType;

import org.glassfish.jersey.CommonProperties;
import org.glassfish.jersey.internal.LocalizationMessages;

/**
 * Helper class containing convenience methods for reading
 * {@code org.glassfish.jersey.server.ResourceConfig} and {@link jakarta.ws.rs.core.Configuration} properties.
 *
 * @author Martin Matula
 */
public final class PropertiesHelper {

    private static final Logger LOGGER = Logger.getLogger(PropertiesHelper.class.getName());
    private static final boolean METAINF_SERVICES_LOOKUP_DISABLE_DEFAULT = false;
    private static final boolean JAXRS_SERVICE_LOADING_ENABLE_DEFAULT = true;
    private static final String RUNTIME_SERVER_LOWER = RuntimeType.SERVER.name().toLowerCase(Locale.ROOT);
    private static final String RUNTIME_CLIENT_LOWER = RuntimeType.CLIENT.name().toLowerCase(Locale.ROOT);

    /**
     * Get system properties.
     *
     * This method delegates to {@link System#getProperties()} while running it in a privileged
     * code block.
     *
     * @return privileged action to obtain system properties.
     */
    public static PrivilegedAction<Properties> getSystemProperties() {
        return new PrivilegedAction<Properties>() {
            @Override
            public Properties run() {
                return System.getProperties();
            }
        };
    }

    /**
     * Get system property.
     *
     * This method delegates to {@link System#getProperty(String)} while running it in a privileged
     * code block.
     *
     * @param name system property name.
     * @return privileged action to obtain system property value that will return {@code null}
     *         if there's no such system property.
     */
    public static PrivilegedAction<String> getSystemProperty(final String name) {
        return new PrivilegedAction<String>() {
            @Override
            public String run() {
                return System.getProperty(name);
            }
        };
    }

    /**
     * Get system property.
     *
     * This method delegates to {@link System#getProperty(String)} while running it in a privileged
     * code block.
     *
     * @param name system property name.
     * @param def  default property value.
     * @return privileged action to obtain system property value that will return the default value
     *         if there's no such system property.
     */
    public static PrivilegedAction<String> getSystemProperty(final String name, final String def) {
        return new PrivilegedAction<String>() {
            @Override
            public String run() {
                return System.getProperty(name, def);
            }
        };
    }

    /**
     * Return value of a specified property. If the property is not set or the real value type is not compatible with
     * defaultValue type, the specified defaultValue is returned. Calling this method is equivalent to calling
     * {@code PropertyHelper.getValue(properties, key, defaultValue, (Class<T>) defaultValue.getClass())}
     *
     * @param properties   Map of properties to get the property value from.
     * @param key          Name of the property.
     * @param defaultValue Default value to be returned if the specified property is not set or cannot be read.
     * @param <T>          Type of the property value.
     * @param legacyMap    Legacy fallback map, where key is the actual property name, value is the old property name
     * @return Value of the property or defaultValue.
     */
    public static <T> T getValue(Map<String, ?> properties, String key, T defaultValue, Map<String, String> legacyMap) {
        return getValue(properties, null, key, defaultValue, legacyMap);
    }


    /**
     * Return value of a specified property. If the property is not set or the real value type is not compatible with
     * defaultValue type, the specified defaultValue is returned. Calling this method is equivalent to calling
     * {@code PropertyHelper.getValue(properties, runtimeType, key, defaultValue, (Class<T>) defaultValue.getClass())}
     *
     * @param properties   Map of properties to get the property value from.
     * @param runtimeType  Runtime type which is used to check whether there is a property with the same
     *                     {@code key} but post-fixed by runtime type (<tt>.server</tt>
     *                     or {@code .client}) which would override the {@code key} property.
     * @param key          Name of the property.
     * @param defaultValue Default value to be returned if the specified property is not set or cannot be read.
     * @param <T>          Type of the property value.
     * @param legacyMap    Legacy fallback map, where key is the actual property name, value is the old property name
     * @return Value of the property or defaultValue.
     */
    @SuppressWarnings("unchecked")
    public static <T> T getValue(Map<String, ?> properties,
                                 RuntimeType runtimeType,
                                 String key,
                                 T defaultValue,
                                 Map<String, String> legacyMap) {
        return getValue(properties, runtimeType, key, defaultValue, (Class<T>) defaultValue.getClass(), legacyMap);
    }

    /**
     * Returns value of a specified property. If the property is not set or the real value type is not compatible with
     * the specified value type, returns defaultValue.
     *
     * @param properties   Map of properties to get the property value from.
     * @param key          Name of the property.
     * @param defaultValue Default value of the property.
     * @param type         Type to retrieve the value as.
     * @param <T>          Type of the property value.
     * @param legacyMap    Legacy fallback map, where key is the actual property name, value is the old property name
     * @return Value of the property or null.
     */
    public static <T> T getValue(Map<String, ?> properties,
                                 String key,
                                 T defaultValue,
                                 Class<T> type,
                                 Map<String, String> legacyMap) {
        return getValue(properties, null, key, defaultValue, type, legacyMap);
    }


    /**
     * Returns value of a specified property. If the property is not set or the real value type is not compatible with
     * the specified value type, returns defaultValue.
     *
     * @param properties   Map of properties to get the property value from.
     * @param runtimeType  Runtime type which is used to check whether there is a property with the same
     *                     {@code key} but post-fixed by runtime type (<tt>.server</tt>
     *                     or {@code .client}) which would override the {@code key} property.
     * @param key          Name of the property.
     * @param defaultValue Default value of the property.
     * @param type         Type to retrieve the value as.
     * @param <T>          Type of the property value.
     * @param legacyMap    Legacy fallback map, where key is the actual property name, value is the old property name
     * @return Value of the property or null.
     */
    public static <T> T getValue(Map<String, ?> properties, RuntimeType runtimeType, String key,
                                 T defaultValue, Class<T> type, Map<String, String> legacyMap) {
        T value = getValue(properties, runtimeType, key, type, legacyMap);
        if (value == null) {
            value = defaultValue;
        }
        return value;
    }

    /**
     * Returns value of a specified property. If the property is not set or the real value type is not compatible with
     * the specified value type, returns null.
     *
     * @param properties Map of properties to get the property value from.
     * @param key        Name of the property.
     * @param type       Type to retrieve the value as.
     * @param <T>        Type of the property value.
     * @param legacyMap  Legacy fallback map, where key is the actual property name, value is the old property name
     * @return Value of the property or null.
     */
    public static <T> T getValue(Map<String, ?> properties, String key, Class<T> type, Map<String, String> legacyMap) {
        return getValue(properties, null, key, type, legacyMap);
    }


    /**
     * Returns value of a specified property. If the property is not set or the real value type is not compatible with
     * the specified value type, returns null.
     *
     * @param properties  Map of properties to get the property value from.
     * @param runtimeType Runtime type which is used to check whether there is a property with the same
     *                    {@code key} but post-fixed by runtime type (<tt>.server</tt>
     *                    or {@code .client}) which would override the {@code key} property.
     * @param key         Name of the property.
     * @param type        Type to retrieve the value as.
     * @param <T>         Type of the property value.
     * @return Value of the property or null.
     */
    public static <T> T getValue(Map<String, ?> properties, RuntimeType runtimeType, String key, Class<T> type,
                                 Map<String, String> legacyMap) {
        Object value = null;
        if (runtimeType != null) {
            String runtimeAwareKey = getPropertyNameForRuntime(key, runtimeType);
            if (key.equals(runtimeAwareKey)) {
                // legacy behaviour
                runtimeAwareKey = key + "." + toLowerCase(runtimeType);
            }
            value = properties.get(runtimeAwareKey);
        }
        if (value == null) {
            value = properties.get(key);
        }
        if (value == null) {
            value = getLegacyFallbackValue(properties, legacyMap, key);
        }
        if (value == null) {
            return null;
        }

        return convertValue(value, type);
    }

    /**
     * Returns specific property value for given {@link RuntimeType}.
     *
     * Some properties have complementary client and server versions along with a common version (effective for both environments,
     * if the specific one is not set). This methods returns a specific name for the environment (determined by convention),
     * if runtime is not null, the property is a Jersey property name (starts with {@code jersey.config}) and does not contain
     * a runtime specific part already. If those conditions are not met, original property name is returned.
     *
     * @param key property name
     * @param runtimeType runtime type
     * @return runtime-specific property name, where possible, original key in other cases.
     * original key
     */
    public static String getPropertyNameForRuntime(String key, RuntimeType runtimeType) {
        if (runtimeType != null && key.startsWith("jersey.config")) {
            RuntimeType[] types = RuntimeType.values();
            for (RuntimeType type : types) {
                if (key.startsWith("jersey.config." + toLowerCase(type))) {
                    return key;
                }
            }
            return key.replace("jersey.config", "jersey.config." + toLowerCase(runtimeType));
        }
        return key;
    }

    private static Object getLegacyFallbackValue(Map<String, ?> properties, Map<String, String> legacyFallbackMap, String key) {
        if (legacyFallbackMap == null || !legacyFallbackMap.containsKey(key)) {
            return null;
        }
        String fallbackKey = legacyFallbackMap.get(key);
        Object value = properties.get(fallbackKey);
        if (value != null && LOGGER.isLoggable(Level.CONFIG)) {
            LOGGER.config(LocalizationMessages.PROPERTIES_HELPER_DEPRECATED_PROPERTY_NAME(fallbackKey, key));
        }
        return value;
    }

    /**
     * Convert {@code Object} value to a value of the specified class type.
     *
     * @param value {@code Object} value to convert.
     * @param type conversion type.
     * @param <T> converted value type.
     * @return value converted to the specified class type.
     */
    public static <T> T convertValue(Object value, Class<T> type) {

        if (((type.equals(Integer.class)) || (type.equals(int.class))) && Number.class.isInstance(value)) {
            final Integer number2Int = ((Number) value).intValue();
            return  (T) number2Int;
        } else if (((type.equals(Long.class)) || (type.equals(long.class))) && Number.class.isInstance(value)) {
            final Long number2Long =  ((Number) value).longValue();
            return  (T) number2Long;
        }

        if (!type.isInstance(value)) {
            // TODO: Move string value readers from server to common and utilize them here
            final Constructor constructor = AccessController.doPrivileged(ReflectionHelper.getStringConstructorPA(type));
            if (constructor != null) {
                try {
                    return type.cast(constructor.newInstance(value));
                } catch (Exception e) {
                    // calling the constructor wasn't successful - ignore and try valueOf()
                }
            }

            final Method valueOf = AccessController.doPrivileged(ReflectionHelper.getValueOfStringMethodPA(type));
            if (valueOf != null) {
                try {
                    return type.cast(valueOf.invoke(null, value));
                } catch (Exception e) {
                    // calling valueOf wasn't successful
                }
            }

            // at this point we don't know what to return -> return null
            if (LOGGER.isLoggable(Level.WARNING)) {
                LOGGER.warning(LocalizationMessages.PROPERTIES_HELPER_GET_VALUE_NO_TRANSFORM(String.valueOf(value),
                        value.getClass().getName(), type.getName()));
            }

            return null;
        }

        return type.cast(value);
    }

    /**
     * Determine whether {@link CommonProperties#METAINF_SERVICES_LOOKUP_DISABLE} does not globally
     * disable META-INF/services lookup on client/server.
     *
     * @param properties  map containing application properties. May be {@code null}
     * @param runtimeType runtime (client or server) where the service finder binder is used.
     * @return {@code true} if the {@link CommonProperties#METAINF_SERVICES_LOOKUP_DISABLE} is not se to true
     */
    public static boolean isMetaInfServicesEnabled(Map<String, Object> properties, RuntimeType runtimeType) {
        boolean disableMetaInfServicesLookup = METAINF_SERVICES_LOOKUP_DISABLE_DEFAULT;
        if (properties != null) {
            disableMetaInfServicesLookup = CommonProperties.getValue(properties, runtimeType,
                    CommonProperties.METAINF_SERVICES_LOOKUP_DISABLE, METAINF_SERVICES_LOOKUP_DISABLE_DEFAULT, Boolean.class);
        }
        return !disableMetaInfServicesLookup;
    }

    /**
     * Check whether loading of JAX-RS services is allowed or not. Services shall implement Feature or DynamicFeature
     * interface and be listed as SPI in an application
     *
     * @param properties list of properties to be checked
     * @return false if loading of JAX-RS services is not enabled
     *
     * @since 2.35
     */
    public static boolean isJaxRsServiceLoadingEnabled(Map<String, Object> properties) {
        boolean enableServicesLoading = JAXRS_SERVICE_LOADING_ENABLE_DEFAULT;
        if (properties != null) {
            enableServicesLoading = CommonProperties.getValue(properties,
                    CommonProperties.JAXRS_SERVICE_LOADING_ENABLE, JAXRS_SERVICE_LOADING_ENABLE_DEFAULT);
        }
        return enableServicesLoading;
    }

    /**
     * Get the value of the property with a given name converted to {@code boolean}. Returns {@code false} if the value is
     * not convertible.
     *
     * @param properties key-value map of properties.
     * @param name       property name.
     * @return {@code boolean} property value or {@code false} if the property is not convertible.
     */
    public static boolean isProperty(final Map<String, Object> properties, final String name) {
        return properties.containsKey(name) && isProperty(properties.get(name));
    }

    /**
     * Get the value of the property converted to {@code boolean}. Returns {@code false} if the value is not convertible.
     *
     * @param value property value.
     * @return {@code boolean} property value or {@code false} if the property is not convertible.
     */
    public static boolean isProperty(final Object value) {
        if (value instanceof Boolean) {
            return Boolean.class.cast(value);
        } else {
            return value != null && Boolean.parseBoolean(value.toString());
        }
    }

    /**
     * Faster replacement of {@code RuntimeType#name().toLowerCase(Locale.ROOT)}
     * @param runtimeType The runtime type to lower case
     * @return the lower-cased variant of the {@link RuntimeType}.
     */
    private static String toLowerCase(RuntimeType runtimeType) {
        switch (runtimeType) {
            case CLIENT:
                return RUNTIME_CLIENT_LOWER;
            default:
                return RUNTIME_SERVER_LOWER;
        }
    }

    /**
     * Prevent instantiation.
     */
    private PropertiesHelper() {
    }
}
