/*
 * Copyright 2017 Red Hat, Inc.
 *
 * Licensed 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 io.smallrye.config;

import static io.smallrye.config.common.utils.StringUtil.unquoted;

import java.io.File;
import java.io.ObjectStreamException;
import java.io.Serial;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Comparator;
import java.util.Currency;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.UUID;
import java.util.function.IntFunction;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.eclipse.microprofile.config.spi.Converter;

import io.smallrye.config._private.ConfigLogging;
import io.smallrye.config._private.ConfigMessages;
import io.smallrye.config.common.AbstractConverter;
import io.smallrye.config.common.AbstractDelegatingConverter;
import io.smallrye.config.common.AbstractSimpleDelegatingConverter;
import io.smallrye.config.common.utils.StringUtil;

/**
 * General converter utilities and constants.
 *
 * @author <a href="http://jmesnil.net/">Jeff Mesnil</a> (c) 2017 Red Hat inc.
 */
public final class Converters {
    private Converters() {
        throw new UnsupportedOperationException();
    }

    static final Converter<ConfigValue> CONFIG_VALUE_CONVERTER = new ConfigValueConverter();

    static final Converter<String> STRING_CONVERTER = BuiltInConverter.of(0, newEmptyValueConverter(value -> value));

    static final Converter<Boolean> BOOLEAN_CONVERTER = BuiltInConverter.of(1, newTrimmingConverter(newEmptyValueConverter(
            new Converter<Boolean>() {
                @Override
                public Boolean convert(final String value) throws IllegalArgumentException, NullPointerException {
                    if ("TRUE".equalsIgnoreCase(value)
                            || "1".equalsIgnoreCase(value)
                            || "YES".equalsIgnoreCase(value)
                            || "Y".equalsIgnoreCase(value)
                            || "ON".equalsIgnoreCase(value)
                            || "JA".equalsIgnoreCase(value)
                            || "J".equalsIgnoreCase(value)
                            || "SI".equalsIgnoreCase(value)
                            || "SIM".equalsIgnoreCase(value)
                            || "OUI".equalsIgnoreCase(value)) {
                        return Boolean.TRUE;
                    } else if ("FALSE".equalsIgnoreCase(value)
                            || "0".equalsIgnoreCase(value)
                            || "NO".equalsIgnoreCase(value)
                            || "N".equalsIgnoreCase(value)
                            || "OFF".equalsIgnoreCase(value)
                            || "NEIN".equalsIgnoreCase(value)
                            || "NÃO".equalsIgnoreCase(value)
                            || "NON".equalsIgnoreCase(value)) {
                        return Boolean.FALSE;
                    } else {
                        ConfigLogging.log.booleanConversionFalse(value);
                        return Boolean.FALSE;
                    }
                }
            })));

    static final Converter<Double> DOUBLE_CONVERTER = BuiltInConverter.of(2,
            newTrimmingConverter(newEmptyValueConverter(value -> {
                try {
                    return Double.valueOf(value);
                } catch (NumberFormatException nfe) {
                    throw ConfigMessages.msg.doubleExpected(value);
                }
            })));

    static final Converter<Float> FLOAT_CONVERTER = BuiltInConverter.of(3,
            newTrimmingConverter(newEmptyValueConverter(value -> {
                try {
                    return Float.valueOf(value);
                } catch (NumberFormatException nfe) {
                    throw ConfigMessages.msg.floatExpected(value);
                }
            })));

    static final Converter<Long> LONG_CONVERTER = BuiltInConverter.of(4,
            newTrimmingConverter(newEmptyValueConverter(value -> {
                try {
                    return Long.valueOf(value);
                } catch (NumberFormatException nfe) {
                    throw ConfigMessages.msg.longExpected(value);
                }
            })));

    static final Converter<Integer> INTEGER_CONVERTER = BuiltInConverter.of(5,
            newTrimmingConverter(newEmptyValueConverter(value -> {
                try {
                    return Integer.valueOf(value);
                } catch (NumberFormatException nfe) {
                    throw ConfigMessages.msg.integerExpected(value);
                }
            })));

    static final Converter<Class<?>> CLASS_CONVERTER = BuiltInConverter.of(6,
            newTrimmingConverter(newEmptyValueConverter(value -> {
                try {
                    return Class.forName(value, true, SecuritySupport.getContextClassLoader());
                } catch (ClassNotFoundException e) {
                    throw ConfigMessages.msg.classConverterNotFound(e, value);
                }
            })));

    static final Converter<OptionalInt> OPTIONAL_INT_CONVERTER = BuiltInConverter.of(7,
            newOptionalIntConverter(INTEGER_CONVERTER));

    static final Converter<OptionalLong> OPTIONAL_LONG_CONVERTER = BuiltInConverter.of(8,
            newOptionalLongConverter(LONG_CONVERTER));

    static final Converter<OptionalDouble> OPTIONAL_DOUBLE_CONVERTER = BuiltInConverter.of(9,
            newOptionalDoubleConverter(DOUBLE_CONVERTER));

    static final Converter<InetAddress> INET_ADDRESS_CONVERTER = BuiltInConverter.of(10,
            newTrimmingConverter(newEmptyValueConverter(value -> {
                try {
                    return InetAddress.getByName(value);
                } catch (UnknownHostException e) {
                    throw ConfigMessages.msg.unknownHost(e, value);
                }
            })));

    static final Converter<Character> CHARACTER_CONVERTER = BuiltInConverter.of(11, newEmptyValueConverter(value -> {
        if (value.length() == 1) {
            return value.charAt(0);
        }
        throw ConfigMessages.msg.failedCharacterConversion(value);
    }));

    static final Converter<Short> SHORT_CONVERTER = BuiltInConverter.of(12,
            newTrimmingConverter(newEmptyValueConverter(Short::valueOf)));

    static final Converter<Byte> BYTE_CONVERTER = BuiltInConverter.of(13,
            newTrimmingConverter(newEmptyValueConverter(Byte::valueOf)));

    static final Converter<UUID> UUID_CONVERTER = BuiltInConverter.of(14,
            newTrimmingConverter(newEmptyValueConverter((s) -> {
                try {
                    return UUID.fromString(s);
                } catch (IllegalArgumentException e) {
                    throw ConfigMessages.msg.malformedUUID(e, s);
                }
            })));

    static final Converter<Currency> CURRENCY_CONVERTER = BuiltInConverter.of(15,
            newTrimmingConverter(newEmptyValueConverter((s) -> Currency.getInstance(s))));

    static final Converter<BitSet> BITSET_CONVERTER = BuiltInConverter.of(16,
            newTrimmingConverter(newTrimmingConverter((s) -> {
                int len = s.length();
                byte[] data = new byte[len / 2];
                for (int i = 0; i < len; i += 2) {
                    data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                            + Character.digit(s.charAt(i + 1), 16));
                }
                return BitSet.valueOf(data);
            })));

    static final Converter<Pattern> PATTERN_CONVERTER = BuiltInConverter.of(17,
            newTrimmingConverter(newEmptyValueConverter(Pattern::compile)));

    static final Converter<Path> PATH_CONVERTER = BuiltInConverter.of(18,
            newEmptyValueConverter(Path::of));

    static final Converter<File> FILE_CONVERTER = BuiltInConverter.of(19, newEmptyValueConverter(File::new));

    static final Converter<URI> URI_CONVERTER = BuiltInConverter.of(20,
            newTrimmingConverter(newEmptyValueConverter(URI::create)));

    static final Converter<DateTimeFormatter> DATE_TIME_FORMATTER_CONVERTER = BuiltInConverter.of(21,
            newTrimmingConverter(newEmptyValueConverter(DateTimeFormatter::ofPattern)));

    static final Converter<CharSequence> CHAR_SEQUENCE_CONVERTER = BuiltInConverter.of(22,
            newTrimmingConverter(newEmptyValueConverter(Object::toString)));

    static final Map<Class<?>, Class<?>> PRIMITIVE_TYPES;

    static final Map<Type, Converter<?>> ALL_CONVERTERS = new HashMap<>();

    static {
        ALL_CONVERTERS.put(String.class, STRING_CONVERTER);
        ALL_CONVERTERS.put(Boolean.class, BOOLEAN_CONVERTER);
        ALL_CONVERTERS.put(Double.class, DOUBLE_CONVERTER);
        ALL_CONVERTERS.put(Float.class, FLOAT_CONVERTER);
        ALL_CONVERTERS.put(Long.class, LONG_CONVERTER);
        ALL_CONVERTERS.put(Integer.class, INTEGER_CONVERTER);
        ALL_CONVERTERS.put(Short.class, SHORT_CONVERTER);
        ALL_CONVERTERS.put(Class.class, CLASS_CONVERTER);
        ALL_CONVERTERS.put(InetAddress.class, INET_ADDRESS_CONVERTER);
        ALL_CONVERTERS.put(OptionalInt.class, OPTIONAL_INT_CONVERTER);
        ALL_CONVERTERS.put(OptionalLong.class, OPTIONAL_LONG_CONVERTER);
        ALL_CONVERTERS.put(OptionalDouble.class, OPTIONAL_DOUBLE_CONVERTER);
        ALL_CONVERTERS.put(Character.class, CHARACTER_CONVERTER);
        ALL_CONVERTERS.put(Byte.class, BYTE_CONVERTER);
        ALL_CONVERTERS.put(UUID.class, UUID_CONVERTER);
        ALL_CONVERTERS.put(Currency.class, CURRENCY_CONVERTER);
        ALL_CONVERTERS.put(BitSet.class, BITSET_CONVERTER);
        ALL_CONVERTERS.put(Pattern.class, PATTERN_CONVERTER);
        ALL_CONVERTERS.put(Path.class, PATH_CONVERTER);
        ALL_CONVERTERS.put(File.class, FILE_CONVERTER);
        ALL_CONVERTERS.put(URI.class, URI_CONVERTER);
        ALL_CONVERTERS.put(DateTimeFormatter.class, DATE_TIME_FORMATTER_CONVERTER);
        ALL_CONVERTERS.put(CharSequence.class, CHAR_SEQUENCE_CONVERTER);

        Map<Class<?>, Class<?>> primitiveTypes = new HashMap<>(9);
        primitiveTypes.put(byte.class, Byte.class);
        primitiveTypes.put(short.class, Short.class);
        primitiveTypes.put(int.class, Integer.class);
        primitiveTypes.put(long.class, Long.class);
        primitiveTypes.put(float.class, Float.class);
        primitiveTypes.put(double.class, Double.class);
        primitiveTypes.put(char.class, Character.class);
        primitiveTypes.put(boolean.class, Boolean.class);
        primitiveTypes.put(void.class, Void.class);
        PRIMITIVE_TYPES = primitiveTypes;
    }

    static Class<?> wrapPrimitiveType(Class<?> primitiveType) {
        assert primitiveType.isPrimitive();
        return PRIMITIVE_TYPES.get(primitiveType);
    }

    /**
     * Get the type of the converter specified by {@code clazz}. If the given class is not a valid
     * converter, then {@code null} is returned.
     *
     * @param clazz the converter class (must not be {@code null})
     * @return the converter target type, or {@code null} if the class does not represent a valid configuration
     * @throws IllegalStateException if the given converter class is not properly parameterized
     */
    public static Type getConverterType(Class<?> clazz) {
        if (clazz.equals(Object.class)) {
            return null;
        }

        for (Type type : clazz.getGenericInterfaces()) {
            if (type instanceof ParameterizedType pt) {
                if (pt.getRawType().equals(Converter.class)) {
                    Type[] typeArguments = pt.getActualTypeArguments();
                    if (typeArguments.length != 1) {
                        throw ConfigMessages.msg.singleTypeConverter(clazz.getName());
                    }
                    return typeArguments[0];
                }
            }
        }

        return getConverterType(clazz.getSuperclass());
    }

    /**
     * Get the implicit converter for the given type class, if any.
     *
     * @param type the type class
     * @param <T> the type
     * @return the implicit converter for the given type class, or {@code null} if none exists
     */
    @SuppressWarnings("unchecked")
    public static <T> Converter<T> getImplicitConverter(Class<? extends T> type) {
        return (Converter<T>) ALL_CONVERTERS.getOrDefault(type, Implicit.getConverter(type));
    }

    /**
     * Get a converter that converts a comma-separated string into a list of converted items.
     *
     * @param itemConverter the item converter (must not be {@code null})
     * @param collectionFactory the collection factory (must not be {@code null})
     * @param <T> the item type
     * @param <C> the collection type
     * @return the new converter (not {@code null})
     */
    public static <T, C extends Collection<T>> Converter<C> newCollectionConverter(Converter<? extends T> itemConverter,
            IntFunction<C> collectionFactory) {
        return new CollectionConverter<>(itemConverter, collectionFactory);
    }

    /**
     * Get a converter that converts a comma-separated string into an array of converted items.
     *
     * @param itemConverter the item converter (must not be {@code null})
     * @param arrayType the array type class (must not be {@code null})
     * @param <T> the item type
     * @param <A> the array type
     * @return the new converter (not {@code null})
     */
    public static <A, T> Converter<A> newArrayConverter(Converter<? extends T> itemConverter, Class<A> arrayType) {
        if (!arrayType.isArray()) {
            throw ConfigMessages.msg.notArrayType(arrayType.toString());
        }
        return new ArrayConverter<>(itemConverter, arrayType);
    }

    /**
     * Get a converter that converts content of type {@code <key1>=<value1>;<key2>=<value2>...} into
     * a {@code Map<K, V>} using the given key and value converters.
     *
     * @param keyConverter the converter used to convert the keys
     * @param valueConverter the converter used to convert the values
     * @param mapFactory the map factory (must not be {@code null})
     * @return the new converter (not {@code null})
     * @param <K> the type of the keys
     * @param <V> the type of the values
     */
    public static <K, V> Converter<Map<K, V>> newMapConverter(Converter<? extends K> keyConverter,
            Converter<? extends V> valueConverter, IntFunction<Map<K, V>> mapFactory) {
        return new MapConverter<>(keyConverter, valueConverter, mapFactory);
    }

    /**
     * Get a converter which wraps another converter's result into an {@code Optional}. If the delegate converter
     * returns {@code null}, this converter returns {@link Optional#empty()}.
     *
     * @param delegateConverter the delegate converter (must not be {@code null})
     * @param <T> the item type
     * @return the new converter (not {@code null})
     */
    public static <T> Converter<Optional<T>> newOptionalConverter(Converter<? extends T> delegateConverter) {
        return new OptionalConverter<>(delegateConverter);
    }

    /**
     * Get a converter which wraps another converter's result into an {@code OptionalInt}. If the delegate converter
     * returns {@code null}, this converter returns {@link Optional#empty()}.
     *
     * @param delegateConverter the delegate converter (must not be {@code null})
     * @return the new converter (not {@code null})
     */
    public static Converter<OptionalInt> newOptionalIntConverter(Converter<Integer> delegateConverter) {
        return new OptionalIntConverter(delegateConverter);
    }

    /**
     * Get a converter which wraps another converter's result into an {@code OptionalLong}. If the delegate converter
     * returns {@code null}, this converter returns {@link Optional#empty()}.
     *
     * @param delegateConverter the delegate converter (must not be {@code null})
     * @return the new converter (not {@code null})
     */
    public static Converter<OptionalLong> newOptionalLongConverter(Converter<Long> delegateConverter) {
        return new OptionalLongConverter(delegateConverter);
    }

    /**
     * Get a converter which wraps another converter's result into an {@code OptionalDouble}. If the delegate converter
     * returns {@code null}, this converter returns {@link Optional#empty()}.
     *
     * @param delegateConverter the delegate converter (must not be {@code null})
     * @return the new converter (not {@code null})
     */
    public static Converter<OptionalDouble> newOptionalDoubleConverter(Converter<Double> delegateConverter) {
        return new OptionalDoubleConverter(delegateConverter);
    }

    /**
     * Get a converter with wraps another converter's result into a {@code Secret}.
     *
     * @param delegateConverter the delegate converter (must not be {@code null})
     * @param <T> the secret type
     * @return the new converter (not {@code null})
     */
    public static <T> Converter<Secret<T>> newSecretConverter(Converter<T> delegateConverter) {
        return new SecretConverter<>(delegateConverter);
    }

    /**
     * Get a converter which wraps another converter and returns a special value to represent empty.
     *
     * @param delegateConverter the converter to delegate to (must not be {@code null})
     * @param emptyValue the empty value to return
     * @param <T> the value type
     * @return the converter
     */
    public static <T> Converter<T> newEmptyValueConverter(Converter<T> delegateConverter, T emptyValue) {
        if (emptyValue == null) {
            return delegateConverter;
        }
        return new EmptyValueConverter<>(delegateConverter, emptyValue);
    }

    /**
     * Get a converter which wraps another converter and handles empty values correctly. This allows the
     * delegate converter to assume that the value being converted will not be {@code null} or empty.
     *
     * @param delegateConverter the converter to delegate to (must not be {@code null})
     * @param <T> the value type
     * @return the converter
     */
    public static <T> Converter<T> newEmptyValueConverter(Converter<T> delegateConverter) {
        return new EmptyValueConverter<>(delegateConverter, null);
    }

    /**
     * Get a converter which trims the string input before passing it on to the delegate converter.
     *
     * @param delegateConverter the converter to delegate to (must not be {@code null})
     * @param <T> the value type
     * @return the converter
     */
    public static <T> Converter<T> newTrimmingConverter(Converter<T> delegateConverter) {
        return new TrimmingConverter<>(delegateConverter);
    }

    /**
     * Get a wrapping converter which verifies that the configuration value is greater than, or optionally equal to,
     * the given minimum value.
     *
     * @param delegate the delegate converter (must not be {@code null})
     * @param minimumValue the minimum value (must not be {@code null})
     * @param inclusive {@code true} if the minimum value is inclusive, {@code false} otherwise
     * @param <T> the converter target type
     * @return a range-validating converter
     */
    public static <T extends Comparable<T>> Converter<T> minimumValueConverter(Converter<? extends T> delegate, T minimumValue,
            boolean inclusive) {
        return new RangeCheckConverter<>(Comparator.naturalOrder(), delegate, minimumValue, inclusive, null, false);
    }

    /**
     * Get a wrapping converter which verifies that the configuration value is greater than, or optionally equal to,
     * the given minimum value.
     *
     * @param comparator the comparator to use (must not be {@code null})
     * @param delegate the delegate converter (must not be {@code null})
     * @param minimumValue the minimum value (must not be {@code null})
     * @param inclusive {@code true} if the minimum value is inclusive, {@code false} otherwise
     * @param <T> the converter target type
     * @return a range-validating converter
     */
    public static <T> Converter<T> minimumValueConverter(Comparator<? super T> comparator, Converter<? extends T> delegate,
            T minimumValue, boolean inclusive) {
        return new RangeCheckConverter<>(comparator, delegate, minimumValue, inclusive, null, false);
    }

    /**
     * Get a wrapping converter which verifies that the configuration value is greater than, or optionally equal to,
     * the given minimum value (in string form).
     *
     * @param delegate the delegate converter (must not be {@code null})
     * @param minimumValue the minimum value (must not be {@code null})
     * @param inclusive {@code true} if the minimum value is inclusive, {@code false} otherwise
     * @param <T> the converter target type
     * @return a range-validating converter
     * @throws IllegalArgumentException if the given minimum value fails conversion
     */
    public static <T extends Comparable<T>> Converter<T> minimumValueStringConverter(Converter<? extends T> delegate,
            String minimumValue, boolean inclusive) {
        return minimumValueConverter(delegate, delegate.convert(minimumValue), inclusive);
    }

    /**
     * Get a wrapping converter which verifies that the configuration value is greater than, or optionally equal to,
     * the given minimum value (in string form).
     *
     * @param comparator the comparator to use (must not be {@code null})
     * @param delegate the delegate converter (must not be {@code null})
     * @param minimumValue the minimum value (must not be {@code null})
     * @param inclusive {@code true} if the minimum value is inclusive, {@code false} otherwise
     * @param <T> the converter target type
     * @return a range-validating converter
     * @throws IllegalArgumentException if the given minimum value fails conversion
     */
    public static <T> Converter<T> minimumValueStringConverter(Comparator<? super T> comparator,
            Converter<? extends T> delegate, String minimumValue, boolean inclusive) {
        return minimumValueConverter(comparator, delegate, delegate.convert(minimumValue), inclusive);
    }

    /**
     * Get a wrapping converter which verifies that the configuration value is less than, or optionally equal to,
     * the given maximum value.
     *
     * @param delegate the delegate converter (must not be {@code null})
     * @param maximumValue the maximum value (must not be {@code null})
     * @param inclusive {@code true} if the maximum value is inclusive, {@code false} otherwise
     * @param <T> the converter target type
     * @return a range-validating converter
     */
    public static <T extends Comparable<T>> Converter<T> maximumValueConverter(Converter<? extends T> delegate, T maximumValue,
            boolean inclusive) {
        return new RangeCheckConverter<>(Comparator.naturalOrder(), delegate, null, false, maximumValue, inclusive);
    }

    /**
     * Get a wrapping converter which verifies that the configuration value is less than, or optionally equal to,
     * the given maximum value.
     *
     * @param comparator the comparator to use (must not be {@code null})
     * @param delegate the delegate converter (must not be {@code null})
     * @param maximumValue the maximum value (must not be {@code null})
     * @param inclusive {@code true} if the maximum value is inclusive, {@code false} otherwise
     * @param <T> the converter target type
     * @return a range-validating converter
     */
    public static <T> Converter<T> maximumValueConverter(Comparator<? super T> comparator, Converter<? extends T> delegate,
            T maximumValue, boolean inclusive) {
        return new RangeCheckConverter<>(comparator, delegate, null, false, maximumValue, inclusive);
    }

    /**
     * Get a wrapping converter which verifies that the configuration value is less than, or optionally equal to,
     * the given maximum value (in string form).
     *
     * @param delegate the delegate converter (must not be {@code null})
     * @param maximumValue the maximum value (must not be {@code null})
     * @param inclusive {@code true} if the maximum value is inclusive, {@code false} otherwise
     * @param <T> the converter target type
     * @return a range-validating converter
     * @throws IllegalArgumentException if the given maximum value fails conversion
     */
    public static <T extends Comparable<T>> Converter<T> maximumValueStringConverter(Converter<? extends T> delegate,
            String maximumValue, boolean inclusive) {
        return maximumValueConverter(delegate, delegate.convert(maximumValue), inclusive);
    }

    /**
     * Get a wrapping converter which verifies that the configuration value is less than, or optionally equal to,
     * the given maximum value (in string form).
     *
     * @param comparator the comparator to use (must not be {@code null})
     * @param delegate the delegate converter (must not be {@code null})
     * @param maximumValue the maximum value (must not be {@code null})
     * @param inclusive {@code true} if the maximum value is inclusive, {@code false} otherwise
     * @param <T> the converter target type
     * @return a range-validating converter
     * @throws IllegalArgumentException if the given maximum value fails conversion
     */
    public static <T> Converter<T> maximumValueStringConverter(Comparator<? super T> comparator,
            Converter<? extends T> delegate, String maximumValue, boolean inclusive) {
        return maximumValueConverter(comparator, delegate, delegate.convert(maximumValue), inclusive);
    }

    /**
     * Get a wrapping converter which verifies that the configuration value is within the given range.
     *
     * @param delegate the delegate converter (must not be {@code null})
     * @param maximumValue the maximum value (must not be {@code null})
     * @param maxInclusive {@code true} if the maximum value is inclusive, {@code false} otherwise
     * @param <T> the converter target type
     * @return a range-validating converter
     */
    public static <T extends Comparable<T>> Converter<T> rangeValueConverter(Converter<? extends T> delegate, T minimumValue,
            boolean minInclusive, T maximumValue, boolean maxInclusive) {
        return new RangeCheckConverter<>(Comparator.naturalOrder(), delegate, minimumValue, minInclusive, maximumValue,
                maxInclusive);
    }

    /**
     * Get a wrapping converter which verifies that the configuration value is within the given range.
     *
     * @param comparator the comparator to use (must not be {@code null})
     * @param delegate the delegate converter (must not be {@code null})
     * @param maximumValue the maximum value (must not be {@code null})
     * @param maxInclusive {@code true} if the maximum value is inclusive, {@code false} otherwise
     * @param <T> the converter target type
     * @return a range-validating converter
     */
    public static <T> Converter<T> rangeValueConverter(Comparator<? super T> comparator, Converter<? extends T> delegate,
            T minimumValue, boolean minInclusive, T maximumValue, boolean maxInclusive) {
        return new RangeCheckConverter<>(comparator, delegate, minimumValue, minInclusive, maximumValue, maxInclusive);
    }

    /**
     * Get a wrapping converter which verifies that the configuration value is within the given range (in string form).
     *
     * @param delegate the delegate converter (must not be {@code null})
     * @param maximumValue the maximum value (must not be {@code null})
     * @param maxInclusive {@code true} if the maximum value is inclusive, {@code false} otherwise
     * @param <T> the converter target type
     * @return a range-validating converter
     * @throws IllegalArgumentException if the given minimum or maximum value fails conversion
     */
    public static <T extends Comparable<T>> Converter<T> rangeValueStringConverter(Converter<? extends T> delegate,
            String minimumValue, boolean minInclusive, String maximumValue, boolean maxInclusive) {
        return rangeValueConverter(delegate, delegate.convert(minimumValue), minInclusive, delegate.convert(maximumValue),
                maxInclusive);
    }

    /**
     * Get a wrapping converter which verifies that the configuration value is within the given range (in string form).
     *
     * @param comparator the comparator to use (must not be {@code null})
     * @param delegate the delegate converter (must not be {@code null})
     * @param maximumValue the maximum value (must not be {@code null})
     * @param maxInclusive {@code true} if the maximum value is inclusive, {@code false} otherwise
     * @param <T> the converter target type
     * @return a range-validating converter
     * @throws IllegalArgumentException if the given minimum or maximum value fails conversion
     */
    public static <T> Converter<T> rangeValueStringConverter(Comparator<? super T> comparator, Converter<? extends T> delegate,
            String minimumValue, boolean minInclusive, String maximumValue, boolean maxInclusive) {
        return rangeValueConverter(comparator, delegate, delegate.convert(minimumValue), minInclusive,
                delegate.convert(maximumValue), maxInclusive);
    }

    /**
     * Get a wrapping converter which verifies that the configuration value matches the given pattern.
     *
     * @param delegate the delegate converter (must not be {@code null})
     * @param pattern the pattern to match (must not be {@code null})
     * @param <T> the converter target type
     * @return a pattern-validating converter
     */
    public static <T> Converter<T> patternValidatingConverter(Converter<? extends T> delegate, Pattern pattern) {
        return new PatternCheckConverter<>(delegate, pattern);
    }

    /**
     * Get a wrapping converter which verifies that the configuration value matches the given pattern.
     *
     * @param delegate the delegate converter (must not be {@code null})
     * @param pattern the pattern string to match (must not be {@code null})
     * @param <T> the converter target type
     * @return a pattern-validating converter
     * @throws PatternSyntaxException if the given pattern has invalid syntax
     */
    public static <T> Converter<T> patternValidatingConverter(Converter<? extends T> delegate, String pattern) {
        return patternValidatingConverter(delegate, Pattern.compile(pattern));
    }

    static <T> boolean isOptionalConverter(Converter<T> converter) {
        return converter instanceof Converters.OptionalConverter<?> ||
                converter.equals(Converters.OPTIONAL_INT_CONVERTER) ||
                converter.equals(Converters.OPTIONAL_LONG_CONVERTER) ||
                converter.equals(Converters.OPTIONAL_DOUBLE_CONVERTER);
    }

    /**
     * This Converter should not be used directly. This is only used as a marker to use to return a ConfigValue directly
     * after a configuration property lookup.
     */
    static final class ConfigValueConverter implements Converter<ConfigValue> {
        @Serial
        private static final long serialVersionUID = -5005688684588039934L;

        @Override
        public ConfigValue convert(final String value) throws IllegalArgumentException, NullPointerException {
            throw new IllegalStateException();
        }
    }

    static final class PatternCheckConverter<T> implements Converter<T>, Serializable {
        @Serial
        private static final long serialVersionUID = 358813973126582008L;

