/*
 * 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.logging.log4j.util;

import java.nio.charset.Charset;
import java.time.Duration;
import java.util.ResourceBundle;

public interface PropertyEnvironment {

    /**
     * Allows a PropertySource to be added after PropertiesUtil has been created.
     * @param propertySource the PropertySource to add.
     */
    void addPropertySource(PropertySource propertySource);

    /**
     * Allows a PropertySource that was added to be removed.
     * @param propertySource the PropertySource to remove.
     */
    void removePropertySource(PropertySource propertySource);

    /**
     * Returns {@code true} if the specified property is defined, regardless of its value (it may not have a value).
     *
     * @param name the name of the property to verify
     * @return {@code true} if the specified property is defined, regardless of its value
     */
    boolean hasProperty(String name);

    /**
     * Returns {@code true} if the specified property is defined, regardless of its value (it may not have a value).
     *
     * @param key the key of the property to verify
     * @return {@code true} if the specified property is defined, regardless of its value
     */
    default boolean hasProperty(final PropertyKey key) {
        return hasProperty(key.getKey());
    }

    /**
     * Gets the named property as a boolean value. If the property matches the string {@code "true"} (case-insensitive),
     * then it is returned as the boolean value {@code true}. Any other non-{@code null} text in the property is
     * considered {@code false}.
     *
     * @param name the name of the property to look up
     * @return the boolean value of the property or {@code false} if undefined.
     */
    default boolean getBooleanProperty(String name) {
        return getBooleanProperty(name, false);
    }

    /**
     * Gets the named property as a boolean value.
     *
     * @param name         the name of the property to look up
     * @param defaultValue the default value to use if the property is undefined
     * @return the boolean value of the property or {@code defaultValue} if undefined.
     */
    default boolean getBooleanProperty(String name, boolean defaultValue) {
        final String prop = getStringProperty(name);
        return prop == null ? defaultValue : "true".equalsIgnoreCase(prop);
    }

    /**
     * Gets the named property as a boolean value.
     *
     * @param key         the key of the property to look up
     * @return the boolean value of the property or {@code defaultValue} if undefined.
     */
    default boolean getBooleanProperty(PropertyKey key) {
        if (key == null) {
            return false;
        }
        return getBooleanProperty(key.getKey());
    }

    /**
     * Gets the named property as a boolean value.
     *
     * @param key         the key of the property to look up
     * @param defaultValue the default value to use if the property is undefined
     * @return the boolean value of the property or {@code defaultValue} if undefined.
     */
    default boolean getBooleanProperty(PropertyKey key, boolean defaultValue) {
        if (key == null) {
            return defaultValue;
        }
        return getBooleanProperty(key.getKey(), defaultValue);
    }

    /**
     * Gets the named property as a boolean value.
     *
     * @param name                  the name of the property to look up
     * @param defaultValueIfAbsent  the default value to use if the property is undefined
     * @param defaultValueIfPresent the default value to use if the property is defined but not assigned
     * @return the boolean value of the property or {@code defaultValue} if undefined.
     */
    default boolean getBooleanProperty(String name, boolean defaultValueIfAbsent, boolean defaultValueIfPresent) {
        final String prop = getStringProperty(name);
        return prop == null
                ? defaultValueIfAbsent
                : prop.isEmpty() ? defaultValueIfPresent : "true".equalsIgnoreCase(prop);
    }

    /**
     * Gets the named property as a boolean value.
     *
     * @param key                  the key of the property to look up
     * @param defaultValueIfAbsent  the default value to use if the property is undefined
     * @param defaultValueIfPresent the default value to use if the property is defined but not assigned
     * @return the boolean value of the property or {@code defaultValue} if undefined.
     */
    default boolean getBooleanProperty(PropertyKey key, boolean defaultValueIfAbsent, boolean defaultValueIfPresent) {
        if (key == null) {
            return defaultValueIfAbsent;
        }
        return getBooleanProperty(key.getKey(), defaultValueIfAbsent, defaultValueIfPresent);
    }

    /**
     * Retrieves a property that may be prefixed by more than one string.
     * @param prefixes The array of prefixes.
     * @param key The key to locate.
     * @param supplier The method to call to derive the default value. If the value is null, null will be returned
     * if no property is found.
     * @return The value or null if it is not found.
     * @since 2.13.0
     */
    default Boolean getBooleanProperty(String[] prefixes, String key, Supplier<Boolean> supplier) {
        for (final String prefix : prefixes) {
            if (hasProperty(prefix + key)) {
                return getBooleanProperty(prefix + key);
            }
        }
        return supplier != null ? supplier.get() : null;
    }

    /**
     * Gets the named property as a Charset value.
     *
     * @param name the name of the property to look up
     * @return the Charset value of the property or {@link Charset#defaultCharset()} if undefined.
     */
    default Charset getCharsetProperty(String name) {
        return getCharsetProperty(name, Charset.defaultCharset());
    }

    /**
     * Gets the named property as a Charset value.
     *
     * @param key the key of the property to look up
     * @return the Charset value of the property or {@link Charset#defaultCharset()} if undefined.
     */
    default Charset getCharsetProperty(PropertyKey key) {
        if (key == null) {
            return null;
        }
        return getCharsetProperty(key.getKey());
    }

    /**
     * Gets the named property as a Charset value.
     *
     * @param key the key of the property to look up
     * @param defaultValue the default value to use if the property is undefined
     * @return the Charset value of the property or {@link Charset#defaultCharset()} if undefined.
     */
    default Charset getCharsetProperty(PropertyKey key, Charset defaultValue) {
        if (key == null) {
            return defaultValue;
        }
        return getCharsetProperty(key.getKey(), defaultValue);
    }

    /**
     * Gets the named property as a Charset value. If we cannot find the named Charset, see if it is mapped in
     * file {@code Log4j-charsets.properties} on the class path.
     *
     * @param name         the name of the property to look up
     * @param defaultValue the default value to use if the property is undefined
     * @return the Charset value of the property or {@code defaultValue} if undefined.
     */
    default Charset getCharsetProperty(String name, Charset defaultValue) {
        final String charsetName = getStringProperty(name);
        if (charsetName == null) {
            return defaultValue;
        }
        if (Charset.isSupported(charsetName)) {
            return Charset.forName(charsetName);
        }
        final ResourceBundle bundle = PropertiesUtil.getCharsetsResourceBundle();
        if (bundle.containsKey(name)) {
            final String mapped = bundle.getString(name);
            if (Charset.isSupported(mapped)) {
                return Charset.forName(mapped);
            }
        }
        LowLevelLogUtil.log("Unable to get Charset '" + charsetName + "' for property '" + name + "', using default "
                + defaultValue + " and continuing.");
        return defaultValue;
    }

    /**
     * Gets the named property as an integer.
     *
     * @param name         the name of the property to look up
     * @param defaultValue the default value to use if the property is undefined
     * @return the parsed integer value of the property or {@code defaultValue} if it was undefined or could not be
     * parsed.
     */
    default int getIntegerProperty(String name, int defaultValue) {
        final String prop = getStringProperty(name);
        if (prop != null) {
            try {
                return Integer.parseInt(prop);
            } catch (final Exception ignored) {
                return defaultValue;
            }
        }
        return defaultValue;
    }
    /**
     * Gets the named property as an integer.
     *
     * @param key          the key of the property to look up
     * @param defaultValue the default value to use if the property is undefined
     * @return the parsed integer value of the property or {@code defaultValue} if it was undefined or could not be
     * parsed.
     */
    default int getIntegerProperty(PropertyKey key, int defaultValue) {
        if (key == null) {
            return defaultValue;
        }
        return getIntegerProperty(key.getKey(), defaultValue);
    }

    /**
     * Retrieves a property that may be prefixed by more than one string.
     * @param prefixes The array of prefixes.
     * @param key The key to locate.
     * @param supplier The method to call to derive the default value. If the value is null, null will be returned
     * if no property is found.
     * @return The value or null if it is not found.
     * @since 2.13.0
     */
    default Integer getIntegerProperty(String[] prefixes, String key, Supplier<Integer> supplier) {
        for (final String prefix : prefixes) {
            if (hasProperty(prefix + key)) {
                return getIntegerProperty(prefix + key, 0);
            }
        }
        return supplier != null ? supplier.get() : null;
    }

    /**
     * Gets the named property as a long.
     *
     * @param key         the key of the property to look up
     * @param defaultValue the default value to use if the property is undefined
     * @return the parsed long value of the property or {@code defaultValue} if it was undefined or could not be parsed.
     */
    default long getLongProperty(PropertyKey key, long defaultValue) {
        if (key == null) {
            return defaultValue;
        }
        return getLongProperty(key.getKey(), defaultValue);
    }

    /**
     * Gets the named property as a long.
     *
     * @param name         the name of the property to look up
     * @param defaultValue the default value to use if the property is undefined
     * @return the parsed long value of the property or {@code defaultValue} if it was undefined or could not be parsed.
     */
    default long getLongProperty(String name, long defaultValue) {
        final String prop = getStringProperty(name);
        if (prop != null) {
            try {
                return Long.parseLong(prop);
            } catch (final Exception ignored) {
                return defaultValue;
            }
        }
        return defaultValue;
    }

    /**
     * Retrieves a property that may be prefixed by more than one string.
     * @param prefixes The array of prefixes.
     * @param key The key to locate.
     * @param supplier The method to call to derive the default value. If the value is null, null will be returned
     * if no property is found.
     * @return The value or null if it is not found.
     * @since 2.13.0
     */
    default Long getLongProperty(String[] prefixes, String key, Supplier<Long> supplier) {
        for (final String prefix : prefixes) {
            if (hasProperty(prefix + key)) {
                return getLongProperty(prefix + key, 0);
            }
        }
        return supplier != null ? supplier.get() : null;
    }

    /**
     * Retrieves a Duration where the String is of the format nnn[unit] where nnn represents an integer value
     * and unit represents a time unit.
     * @param key The key for the property.
     * @param defaultValue The default value.
     * @return The value of the String as a Duration or the default value, which may be null.
     * @since 2.13.0
     */
    default Duration getDurationProperty(PropertyKey key, Duration defaultValue) {
        if (key == null) {
            return defaultValue;
        }
        return getDurationProperty(key.getKey(), defaultValue);
    }

    /**
     * Retrieves a Duration where the String is of the format nnn[unit] where nnn represents an integer value
     * and unit represents a time unit.
     * @param name The property name.
     * @param defaultValue The default value.
     * @return The value of the String as a Duration or the default value, which may be null.
     * @since 2.13.0
     */
    default Duration getDurationProperty(String name, Duration defaultValue) {
        final String prop = getStringProperty(name);
        if (prop != null) {
            return TimeUnit.getDuration(prop);
        }
        return defaultValue;
    }

    /**
     * Retrieves a property that may be prefixed by more than one string.
     * @param prefixes The array of prefixes.
     * @param key The key to locate.
     * @param supplier The method to call to derive the default value. If the value is null, null will be returned
     * if no property is found.
     * @return The value or null if it is not found.
     * @since 2.13.0
     */
    default Duration getDurationProperty(String[] prefixes, String key, Supplier<Duration> supplier) {
        for (final String prefix : prefixes) {
            if (hasProperty(prefix + key)) {
                return getDurationProperty(prefix + key, null);
            }
        }
        return supplier != null ? supplier.get() : null;
    }

    /**
     * Retrieves a property that may be prefixed by more than one string.
     * @param prefixes The array of prefixes.
     * @param key The key to locate.
     * @param supplier The method to call to derive the default value. If the value is null, null will be returned
     * if no property is found.
     * @return The value or null if it is not found.
     * @since 2.13.0
     */
    default String getStringProperty(String[] prefixes, String key, Supplier<String> supplier) {
        for (final String prefix : prefixes) {
            final String result = getStringProperty(prefix + key);
            if (result != null) {
                return result;
            }
        }
        return supplier != null ? supplier.get() : null;
    }

    /**
     * Gets the named property as a String.
     *
     * @param key the key of the property to look up
     * @return the String value of the property or {@code null} if undefined.
     */
    default String getStringProperty(PropertyKey key) {
        if (key == null) {
            return null;
        }
        return getStringProperty(key.getKey());
    }

    /**
     * Gets the named property as a String.
     *
     * @param name the name of the property to look up
     * @return the String value of the property or {@code null} if undefined.
     */
    String getStringProperty(String name);

    /**
     * Gets the named property as a String.
     *
     * @param key         the key of the property to look up
     * @param defaultValue the default value to use if the property is undefined
     * @return the String value of the property or {@code defaultValue} if undefined.
     */
    default String getStringProperty(PropertyKey key, String defaultValue) {
        if (key == null) {
            return defaultValue;
        }
        return getStringProperty(key.getKey(), defaultValue);
    }

    /**
     * Gets the named property as a String.
     *
     * @param name         the name of the property to look up
     * @param defaultValue the default value to use if the property is undefined
     * @return the String value of the property or {@code defaultValue} if undefined.
     */
    default String getStringProperty(String name, String defaultValue) {
        final String prop = getStringProperty(name);
        return (prop == null) ? defaultValue : prop;
    }

    /**
     * Returns true if system properties tell us we are running on Windows.
     *
     * @return true if system properties tell us we are running on Windows.
     */
    default boolean isOsWindows() {
        return getStringProperty("os.name", "").startsWith("Windows");
    }
}
