/*
 * 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.commons.configuration2.interpol;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import org.apache.commons.text.StringSubstitutor;
import org.apache.commons.text.lookup.DefaultStringLookup;

/**
 * <p>
 * A class that handles interpolation (variable substitution) for configuration
 * objects.
 * </p>
 * <p>
 * Each instance of {@code AbstractConfiguration} is associated with an object
 * of this class. All interpolation tasks are delegated to this object.
 * </p>
 * <p>
 * {@code ConfigurationInterpolator} internally uses the {@code StringSubstitutor}
 * class from <a href="https://commons.apache.org/text">Commons Text</a>. Thus it
 * supports the same syntax of variable expressions.
 * </p>
 * <p>
 * The basic idea of this class is that it can maintain a set of primitive
 * {@link Lookup} objects, each of which is identified by a special prefix. The
 * variables to be processed have the form {@code ${prefix:name}}.
 * {@code ConfigurationInterpolator} will extract the prefix and determine,
 * which primitive lookup object is registered for it. Then the name of the
 * variable is passed to this object to obtain the actual value. It is also
 * possible to define an arbitrary number of default lookup objects, which are
 * used for variables that do not have a prefix or that cannot be resolved by
 * their associated lookup object. When adding default lookup objects their
 * order matters; they are queried in this order, and the first non-<b>null</b>
 * variable value is used.
 * </p>
 * <p>
 * After an instance has been created it does not contain any {@code Lookup}
 * objects. The current set of lookup objects can be modified using the
 * {@code registerLookup()} and {@code deregisterLookup()} methods. Default
 * lookup objects (that are invoked for variables without a prefix) can be added
 * or removed with the {@code addDefaultLookup()} and
 * {@code removeDefaultLookup()} methods respectively. (When a
 * {@code ConfigurationInterpolator} instance is created by a configuration
 * object, a default lookup object is added pointing to the configuration
 * itself, so that variables are resolved using the configuration's properties.)
 * </p>
 * <p>
 * The default usage scenario is that on a fully initialized instance the
 * {@code interpolate()} method is called. It is passed an object value which
 * may contain variables. All these variables are substituted if they can be
 * resolved. The result is the passed in value with variables replaced.
 * Alternatively, the {@code resolve()} method can be called to obtain the
 * values of specific variables without performing interpolation.
 * </p>
 * <p>
 * Implementation node: This class is thread-safe. Lookup objects can be added
 * or removed at any time concurrent to interpolation operations.
 * </p>
 *
 * @since 1.4
 */
public class ConfigurationInterpolator
{
    /** Constant for the prefix separator. */
    private static final char PREFIX_SEPARATOR = ':';

    /** The variable prefix. */
    private static final String VAR_START = "${";

    /** The length of {@link #VAR_START}. */
    private static final int VAR_START_LENGTH = VAR_START.length();

    /** The variable suffix. */
    private static final String VAR_END = "}";

    /** The length of {@link #VAR_END}. */
    private static final int VAR_END_LENGTH = VAR_END.length();

    /** A map containing the default prefix lookups. */
    private static final Map<String, Lookup> DEFAULT_PREFIX_LOOKUPS;

    static
    {
        // TODO Perhaps a 3.0 version should only use Commons Text lookups.
        // Add our own lookups.
        final Map<String, Lookup> lookups = new HashMap<>();
        for (final DefaultLookups lookup : DefaultLookups.values())
        {
            lookups.put(lookup.getPrefix(), lookup.getLookup());
        }
        // Add Apache Commons Text lookups but don't override existing keys.
        for (final DefaultStringLookup lookup : DefaultStringLookup.values())
        {
            lookups.putIfAbsent(lookup.getKey(), new StringLookupAdapter(lookup.getStringLookup()));
        }
        DEFAULT_PREFIX_LOOKUPS = Collections.unmodifiableMap(lookups);
    }

    /** A map with the currently registered lookup objects. */
    private final Map<String, Lookup> prefixLookups;

    /** Stores the default lookup objects. */
    private final List<Lookup> defaultLookups;

    /** The helper object performing variable substitution. */
    private final StringSubstitutor substitutor;

    /** Stores a parent interpolator objects if the interpolator is nested hierarchically. */
    private volatile ConfigurationInterpolator parentInterpolator;

    /**
     * Creates a new instance of {@code ConfigurationInterpolator}.
     */
    public ConfigurationInterpolator()
    {
        prefixLookups = new ConcurrentHashMap<>();
        defaultLookups = new CopyOnWriteArrayList<>();
        substitutor = initSubstitutor();
    }

    /**
     * Creates a new instance based on the properties in the given specification
     * object.
     *
     * @param spec the {@code InterpolatorSpecification}
     * @return the newly created instance
     */
    private static ConfigurationInterpolator createInterpolator(
            final InterpolatorSpecification spec)
    {
        final ConfigurationInterpolator ci = new ConfigurationInterpolator();
        ci.addDefaultLookups(spec.getDefaultLookups());
        ci.registerLookups(spec.getPrefixLookups());
        ci.setParentInterpolator(spec.getParentInterpolator());
        return ci;
    }

    /**
     * Extracts the variable name from a value that consists of a single
     * variable.
     *
     * @param strValue the value
     * @return the extracted variable name
     */
    private static String extractVariableName(final String strValue)
    {
        return strValue.substring(VAR_START_LENGTH,
                strValue.length() - VAR_END_LENGTH);
    }

    /**
     * Creates a new {@code ConfigurationInterpolator} instance based on the
     * passed in specification object. If the {@code InterpolatorSpecification}
     * already contains a {@code ConfigurationInterpolator} object, it is used
     * directly. Otherwise, a new instance is created and initialized with the
     * properties stored in the specification.
     *
     * @param spec the {@code InterpolatorSpecification} (must not be
     *        <b>null</b>)
     * @return the {@code ConfigurationInterpolator} obtained or created based
     *         on the given specification
     * @throws IllegalArgumentException if the specification is <b>null</b>
     * @since 2.0
     */
    public static ConfigurationInterpolator fromSpecification(
            final InterpolatorSpecification spec)
    {
        if (spec == null)
        {
            throw new IllegalArgumentException(
                    "InterpolatorSpecification must not be null!");
        }
        return spec.getInterpolator() != null ? spec.getInterpolator()
                : createInterpolator(spec);
    }

    /**
     * Returns a map containing the default prefix lookups. Every configuration
     * object derived from {@code AbstractConfiguration} is by default
     * initialized with a {@code ConfigurationInterpolator} containing these
     * {@code Lookup} objects and their prefixes. The map cannot be modified
     *
     * @return a map with the default prefix {@code Lookup} objects and their
     *         prefixes
     * @since 2.0
     */
    public static Map<String, Lookup> getDefaultPrefixLookups()
    {
        return DEFAULT_PREFIX_LOOKUPS;
    }

    /**
     * Utility method for obtaining a {@code Lookup} object in a safe way. This
     * method always returns a non-<b>null</b> {@code Lookup} object. If the
     * passed in {@code Lookup} is not <b>null</b>, it is directly returned.
     * Otherwise, result is a dummy {@code Lookup} which does not provide any
     * values.
     *
     * @param lookup the {@code Lookup} to check
     * @return a non-<b>null</b> {@code Lookup} object
     * @since 2.0
     */
    public static Lookup nullSafeLookup(Lookup lookup)
    {
        if (lookup == null)
        {
            lookup = DummyLookup.INSTANCE;
        }
        return lookup;
    }

    /**
     * Adds a default {@code Lookup} object. Default {@code Lookup} objects are
     * queried (in the order they were added) for all variables without a
     * special prefix. If no default {@code Lookup} objects are present, such
     * variables won't be processed.
     *
     * @param defaultLookup the default {@code Lookup} object to be added (must
     *        not be <b>null</b>)
     * @throws IllegalArgumentException if the {@code Lookup} object is
     *         <b>null</b>
     */
    public void addDefaultLookup(final Lookup defaultLookup)
    {
        defaultLookups.add(defaultLookup);
    }

    /**
     * Adds all {@code Lookup} objects in the given collection as default
     * lookups. The collection can be <b>null</b>, then this method has no
     * effect. It must not contain <b>null</b> entries.
     *
     * @param lookups the {@code Lookup} objects to be added as default lookups
     * @throws IllegalArgumentException if the collection contains a <b>null</b>
     *         entry
     */
    public void addDefaultLookups(final Collection<? extends Lookup> lookups)
    {
        if (lookups != null)
        {
            defaultLookups.addAll(lookups);
        }
    }

    /**
     * Deregisters the {@code Lookup} object for the specified prefix at this
     * instance. It will be removed from this instance.
     *
     * @param prefix the variable prefix
     * @return a flag whether for this prefix a lookup object had been
     *         registered
     */
    public boolean deregisterLookup(final String prefix)
    {
        return prefixLookups.remove(prefix) != null;
    }

    /**
     * Obtains the lookup object for the specified prefix. This method is called
     * by the {@code lookup()} method. This implementation will check
     * whether a lookup object is registered for the given prefix. If not, a
     * <b>null</b> lookup object will be returned (never <b>null</b>).
     *
     * @param prefix the prefix
     * @return the lookup object to be used for this prefix
     */
    protected Lookup fetchLookupForPrefix(final String prefix)
    {
        return nullSafeLookup(prefixLookups.get(prefix));
    }

    /**
     * Returns a collection with the default {@code Lookup} objects
     * added to this {@code ConfigurationInterpolator}. These objects are not
     * associated with a variable prefix. The returned list is a snapshot copy
     * of the internal collection of default lookups; so manipulating it does
     * not affect this instance.
     *
     * @return the default lookup objects
     */
    public List<Lookup> getDefaultLookups()
    {
        return new ArrayList<>(defaultLookups);
    }

    /**
     * Returns a map with the currently registered {@code Lookup} objects and
     * their prefixes. This is a snapshot copy of the internally used map. So
     * modifications of this map do not effect this instance.
     *
     * @return a copy of the map with the currently registered {@code Lookup}
     *         objects
     */
    public Map<String, Lookup> getLookups()
    {
        return new HashMap<>(prefixLookups);
    }

    /**
     * Returns the parent {@code ConfigurationInterpolator}.
     *
     * @return the parent {@code ConfigurationInterpolator} (can be <b>null</b>)
     */
    public ConfigurationInterpolator getParentInterpolator()
    {
        return this.parentInterpolator;
    }

    /**
     * Creates and initializes a {@code StringSubstitutor} object which is used for
     * variable substitution. This {@code StringSubstitutor} is assigned a
     * specialized lookup object implementing the correct variable resolving
     * algorithm.
     *
     * @return the {@code StringSubstitutor} used by this object
     */
    private StringSubstitutor initSubstitutor()
    {
        return new StringSubstitutor(key -> Objects.toString(resolve(key), null));
    }

    /**
     * Performs interpolation of the passed in value. If the value is of type
     * String, this method checks whether it contains variables. If so, all
     * variables are replaced by their current values (if possible). For non
     * string arguments, the value is returned without changes.
     *
     * @param value the value to be interpolated
     * @return the interpolated value
     */
    public Object interpolate(final Object value)
    {
        if (value instanceof String)
        {
            final String strValue = (String) value;
            if (looksLikeSingleVariable(strValue))
            {
                final Object resolvedValue = resolveSingleVariable(strValue);
                if (resolvedValue != null && !(resolvedValue instanceof String))
                {
                    // If the value is again a string, it needs no special
                    // treatment; it may also contain further variables which
                    // must be resolved; therefore, the default mechanism is
                    // applied.
                    return resolvedValue;
                }
            }
            return substitutor.replace(strValue);
        }
        return value;
    }

    /**
     * Sets a flag that variable names can contain other variables. If enabled,
     * variable substitution is also done in variable names.
     *
     * @return the substitution in variables flag
     */
    public boolean isEnableSubstitutionInVariables()
    {
        return substitutor.isEnableSubstitutionInVariables();
    }

    /**
     * Checks whether a value to be interpolated seems to be a single variable.
     * In this case, it is resolved directly without using the
     * {@code StringSubstitutor}. Note that it is okay if this method returns a
     * false positive: In this case, resolving is going to fail, and standard
     * mechanism is used.
     *
     * @param strValue the value to be interpolated
     * @return a flag whether this value seems to be a single variable
     */
    private boolean looksLikeSingleVariable(final String strValue)
    {
        return strValue.startsWith(VAR_START) && strValue.endsWith(VAR_END);
    }

    /**
     * Returns an unmodifiable set with the prefixes, for which {@code Lookup}
     * objects are registered at this instance. This means that variables with
     * these prefixes can be processed.
     *
     * @return a set with the registered variable prefixes
     */
    public Set<String> prefixSet()
    {
        return Collections.unmodifiableSet(prefixLookups.keySet());
    }

    /**
     * Registers the given {@code Lookup} object for the specified prefix at
     * this instance. From now on this lookup object will be used for variables
     * that have the specified prefix.
     *
     * @param prefix the variable prefix (must not be <b>null</b>)
     * @param lookup the {@code Lookup} object to be used for this prefix (must
     *        not be <b>null</b>)
     * @throws IllegalArgumentException if either the prefix or the
     *         {@code Lookup} object is <b>null</b>
     */
    public void registerLookup(final String prefix, final Lookup lookup)
    {
        if (prefix == null)
        {
            throw new IllegalArgumentException(
                    "Prefix for lookup object must not be null!");
        }
        if (lookup == null)
        {
            throw new IllegalArgumentException(
                    "Lookup object must not be null!");
        }
        prefixLookups.put(prefix, lookup);
    }

    /**
     * Registers all {@code Lookup} objects in the given map with their prefixes
     * at this {@code ConfigurationInterpolator}. Using this method multiple
     * {@code Lookup} objects can be registered at once. If the passed in map is
     * <b>null</b>, this method does not have any effect.
     *
     * @param lookups the map with lookups to register (may be <b>null</b>)
     * @throws IllegalArgumentException if the map contains <b>entries</b>
     */
    public void registerLookups(final Map<String, ? extends Lookup> lookups)
    {
        if (lookups != null)
        {
            prefixLookups.putAll(lookups);
        }
    }

    /**
     * Removes the specified {@code Lookup} object from the list of default
     * {@code Lookup}s.
     *
     * @param lookup the {@code Lookup} object to be removed
     * @return a flag whether this {@code Lookup} object actually existed and
     *         was removed
     */
    public boolean removeDefaultLookup(final Lookup lookup)
    {
        return defaultLookups.remove(lookup);
    }

    /**
     * Resolves the specified variable. This implementation tries to extract
     * a variable prefix from the given variable name (the first colon (':') is
     * used as prefix separator). It then passes the name of the variable with
     * the prefix stripped to the lookup object registered for this prefix. If
     * no prefix can be found or if the associated lookup object cannot resolve
     * this variable, the default lookup objects are used. If this is not
     * successful either and a parent {@code ConfigurationInterpolator} is
     * available, this object is asked to resolve the variable.
     *
     * @param var the name of the variable whose value is to be looked up which may contain a prefix.
     * @return the value of this variable or <b>null</b> if it cannot be
     * resolved
     */
    public Object resolve(final String var)
    {
        if (var == null)
        {
            return null;
        }

        final int prefixPos = var.indexOf(PREFIX_SEPARATOR);
        if (prefixPos >= 0)
        {
            final String prefix = var.substring(0, prefixPos);
            final String name = var.substring(prefixPos + 1);
            final Object value = fetchLookupForPrefix(prefix).lookup(name);
            if (value != null)
            {
                return value;
            }
        }

        for (final Lookup lookup : defaultLookups)
        {
            final Object value = lookup.lookup(var);
            if (value != null)
            {
                return value;
            }
        }

        final ConfigurationInterpolator parent = getParentInterpolator();
        if (parent != null)
        {
            return getParentInterpolator().resolve(var);
        }
        return null;
    }

    /**
     * Interpolates a string value that seems to be a single variable.
     *
     * @param strValue the string to be interpolated
     * @return the resolved value or <b>null</b> if resolving failed
     */
    private Object resolveSingleVariable(final String strValue)
    {
        return resolve(extractVariableName(strValue));
    }

    /**
     * Sets the flag whether variable names can contain other variables. This
     * flag corresponds to the {@code enableSubstitutionInVariables} property of
     * the underlying {@code StringSubstitutor} object.
     *
     * @param f the new value of the flag
     */
    public void setEnableSubstitutionInVariables(final boolean f)
    {
        substitutor.setEnableSubstitutionInVariables(f);
    }

    /**
     * Sets the parent {@code ConfigurationInterpolator}. This object is used if
     * the {@code Lookup} objects registered at this object cannot resolve a
     * variable.
     *
     * @param parentInterpolator the parent {@code ConfigurationInterpolator}
     *        object (can be <b>null</b>)
     */
    public void setParentInterpolator(
            final ConfigurationInterpolator parentInterpolator)
    {
        this.parentInterpolator = parentInterpolator;
    }
}
