/*
 * 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.builder;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.commons.configuration2.ConfigurationUtils;
import org.apache.commons.configuration2.ImmutableConfiguration;
import org.apache.commons.configuration2.Initializable;
import org.apache.commons.configuration2.beanutils.BeanDeclaration;
import org.apache.commons.configuration2.beanutils.BeanHelper;
import org.apache.commons.configuration2.beanutils.ConstructorArg;
import org.apache.commons.configuration2.event.Event;
import org.apache.commons.configuration2.event.EventListener;
import org.apache.commons.configuration2.event.EventListenerList;
import org.apache.commons.configuration2.event.EventListenerRegistrationData;
import org.apache.commons.configuration2.event.EventSource;
import org.apache.commons.configuration2.event.EventType;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
import org.apache.commons.configuration2.reloading.ReloadingController;

/**
 * <p>
 * An implementation of the {@code ConfigurationBuilder} interface which is able
 * to create different concrete {@code ImmutableConfiguration} implementations based on
 * reflection.
 * </p>
 * <p>
 * When constructing an instance of this class the concrete
 * {@code ImmutableConfiguration} implementation class has to be provided. Then
 * properties for the new {@code ImmutableConfiguration} instance can be set. The first
 * call to {@code getConfiguration()} creates and initializes the new
 * {@code ImmutableConfiguration} object. It is cached and returned by subsequent calls.
 * This cache - and also the initialization properties set so far - can be
 * flushed by calling one of the {@code reset()} methods. That way other
 * {@code ImmutableConfiguration} instances with different properties can be created.
 * </p>
 * <p>
 * If the newly created {@code ImmutableConfiguration} object implements the
 * {@code Initializable} interface, its {@code initialize()} method is called
 * after all initialization properties have been set. This way a concrete
 * implementation class can perform arbitrary initialization steps.
 * </p>
 * <p>
 * There are multiple options for setting up a {@code BasicConfigurationBuilder}
 * instance:
 * </p>
 * <ul>
 * <li>All initialization properties can be set in one or multiple calls of the
 * {@code configure()} method. In each call an arbitrary number of
 * {@link BuilderParameters} objects can be passed. The API allows method
 * chaining and is intended to be used from Java code.</li>
 * <li>If builder instances are created by other means - e.g. using a dependency
 * injection framework -, the fluent API approach may not be suitable. For those
 * use cases it is also possible to pass in all initialization parameters as a
 * map. The keys of the map have to match initialization properties of the
 * {@code ImmutableConfiguration} object to be created, the values are the corresponding
 * property values. For instance, the key <em>throwExceptionOnMissing</em> in
 * the map will cause the method {@code setThrowExceptionOnMissing()} on the
 * {@code ImmutableConfiguration} object to be called with the corresponding value as
 * parameter.</li>
 * </ul>
 * <p>
 * A builder instance can be constructed with an <em>allowFailOnInit</em>
 * flag. If set to <strong>true</strong>, exceptions during initialization
 * of the configuration are ignored; in such a case an empty configuration
 * object is returned. A use case for this flag is a scenario in which a
 * configuration is optional and created on demand the first time configuration
 * data is to be stored. Consider an application that stores user-specific
 * configuration data in the user's home directory: When started for the first
 * time by a new user there is no configuration file; so it makes sense to
 * start with an empty configuration object. On application exit, settings
 * can be stored in this object and written to the associated file. Then they
 * are available on next application start.
 * </p>
 * <p>
 * This class is thread-safe. Multiple threads can modify initialization
 * properties and call {@code getConfiguration()}. However, the intended use
 * case is that the builder is configured by a single thread first. Then
 * {@code getConfiguration()} can be called concurrently, and it is guaranteed
 * that always the same {@code ImmutableConfiguration} instance is returned until the
 * builder is reset.
 * </p>
 *
 * @version $Id: BasicConfigurationBuilder.java 1806681 2017-08-30 09:56:37Z kinow $
 * @since 2.0
 * @param <T> the concrete type of {@code ImmutableConfiguration} objects created by this
 *        builder
 */
public class BasicConfigurationBuilder<T extends ImmutableConfiguration> implements
        ConfigurationBuilder<T>
{
    /** The class of the objects produced by this builder instance. */
    private final Class<? extends T> resultClass;

    /** An object managing the event listeners registered at this builder. */
    private final EventListenerList eventListeners;

    /** A flag whether exceptions on initializing configurations are allowed. */
    private final boolean allowFailOnInit;

    /** The map with current initialization parameters. */
    private Map<String, Object> parameters;

    /** The current bean declaration. */
    private BeanDeclaration resultDeclaration;

    /** The result object of this builder. */
    private volatile T result;

    /**
     * Creates a new instance of {@code BasicConfigurationBuilder} and
     * initializes it with the given result class. No initialization properties
     * are set.
     *
     * @param resCls the result class (must not be <b>null</b>)
     * @throws IllegalArgumentException if the result class is <b>null</b>
     */
    public BasicConfigurationBuilder(Class<? extends T> resCls)
    {
        this(resCls, null);
    }

    /**
     * Creates a new instance of {@code BasicConfigurationBuilder} and
     * initializes it with the given result class and an initial set of builder
     * parameters. The <em>allowFailOnInit</em> flag is set to
     * <strong>false</strong>.
     *
     * @param resCls the result class (must not be <b>null</b>)
     * @param params a map with initialization parameters
     * @throws IllegalArgumentException if the result class is <b>null</b>
     */
    public BasicConfigurationBuilder(Class<? extends T> resCls, Map<String, Object> params)
    {
        this(resCls, params, false);
    }

    /**
     * Creates a new instance of {@code BasicConfigurationBuilder} and
     * initializes it with the given result class, an initial set of builder
     * parameters, and the <em>allowFailOnInit</em> flag. The map with
     * parameters may be <b>null</b>, in this case no initialization parameters
     * are set.
     *
     * @param resCls the result class (must not be <b>null</b>)
     * @param params a map with initialization parameters
     * @param allowFailOnInit a flag whether exceptions on initializing a newly
     *        created {@code ImmutableConfiguration} object are allowed
     * @throws IllegalArgumentException if the result class is <b>null</b>
     */
    public BasicConfigurationBuilder(Class<? extends T> resCls,
            Map<String, Object> params, boolean allowFailOnInit)
    {
        if (resCls == null)
        {
            throw new IllegalArgumentException("Result class must not be null!");
        }

        resultClass = resCls;
        this.allowFailOnInit = allowFailOnInit;
        eventListeners = new EventListenerList();
        updateParameters(params);
    }

    /**
     * Returns the result class of this builder. The objects produced by this
     * builder have the class returned here.
     *
     * @return the result class of this builder
     */
    public Class<? extends T> getResultClass()
    {
        return resultClass;
    }

    /**
     * Returns the <em>allowFailOnInit</em> flag. See the header comment for
     * information about this flag.
     *
     * @return the <em>allowFailOnInit</em> flag
     */
    public boolean isAllowFailOnInit()
    {
        return allowFailOnInit;
    }

    /**
     * Sets the initialization parameters of this builder. Already existing
     * parameters are replaced by the content of the given map.
     *
     * @param params the new initialization parameters of this builder; can be
     *        <b>null</b>, then all initialization parameters are removed
     * @return a reference to this builder for method chaining
     */
    public synchronized BasicConfigurationBuilder<T> setParameters(
            Map<String, Object> params)
    {
        updateParameters(params);
        return this;
    }

    /**
     * Adds the content of the given map to the already existing initialization
     * parameters.
     *
     * @param params the map with additional initialization parameters; may be
     *        <b>null</b>, then this call has no effect
     * @return a reference to this builder for method chaining
     */
    public synchronized BasicConfigurationBuilder<T> addParameters(
            Map<String, Object> params)
    {
        Map<String, Object> newParams =
                new HashMap<>(getParameters());
        if (params != null)
        {
            newParams.putAll(params);
        }
        updateParameters(newParams);
        return this;
    }

    /**
     * Appends the content of the specified {@code BuilderParameters} objects to
     * the current initialization parameters. Calling this method multiple times
     * will create a union of the parameters provided.
     *
     * @param params an arbitrary number of objects with builder parameters
     * @return a reference to this builder for method chaining
     * @throws NullPointerException if a <b>null</b> array is passed
     */
    public BasicConfigurationBuilder<T> configure(BuilderParameters... params)
    {
        Map<String, Object> newParams = new HashMap<>();
        for (BuilderParameters p : params)
        {
            newParams.putAll(p.getParameters());
            handleEventListenerProviders(p);
        }

        return setParameters(newParams);
    }

    /**
     * {@inheritDoc} This implementation creates the result configuration on
     * first access. Later invocations return the same object until this builder
     * is reset. The double-check idiom for lazy initialization is used (Bloch,
     * Effective Java, item 71).
     */
    @Override
    public T getConfiguration() throws ConfigurationException
    {
        fireBuilderEvent(new ConfigurationBuilderEvent(this,
                ConfigurationBuilderEvent.CONFIGURATION_REQUEST));

        T resObj = result;
        boolean created = false;
        if (resObj == null)
        {
            synchronized (this)
            {
                resObj = result;
                if (resObj == null)
                {
                    result = resObj = createResult();
                    created = true;
                }
            }
        }

        if (created)
        {
            fireBuilderEvent(new ConfigurationBuilderResultCreatedEvent(this,
                    ConfigurationBuilderResultCreatedEvent.RESULT_CREATED,
                    resObj));
        }
        return resObj;
    }

    /**
     * {@inheritDoc} This implementation also takes care that the event listener
     * is added to the managed configuration object.
     *
     * @throws IllegalArgumentException if the event type or the listener is
     *         <b>null</b>
     */
    @Override
    public <E extends Event> void addEventListener(
            EventType<E> eventType, EventListener<? super E> listener)
    {
        installEventListener(eventType, listener);
    }

    /**
     * {@inheritDoc} This implementation also takes care that the event listener
     * is removed from the managed configuration object.
     */
    @Override
    public <E extends Event> boolean removeEventListener(
            EventType<E> eventType, EventListener<? super E> listener)
    {
        fetchEventSource().removeEventListener(eventType, listener);
        return eventListeners.removeEventListener(eventType, listener);
    }

    /**
     * Clears an existing result object. An invocation of this method causes a
     * new {@code ImmutableConfiguration} object to be created the next time
     * {@link #getConfiguration()} is called.
     */
    public void resetResult()
    {
        T oldResult;
        synchronized (this)
        {
            oldResult = result;
            result = null;
            resultDeclaration = null;
        }

        if (oldResult != null)
        {
            removeEventListeners(oldResult);
        }
        fireBuilderEvent(new ConfigurationBuilderEvent(this,
                ConfigurationBuilderEvent.RESET));
    }

    /**
     * Removes all initialization parameters of this builder. This method can be
     * called if this builder is to be reused for creating result objects with a
     * different configuration.
     */
    public void resetParameters()
    {
        setParameters(null);
    }

    /**
     * Resets this builder. This is a convenience method which combines calls to
     * {@link #resetResult()} and {@link #resetParameters()}.
     */
    public synchronized void reset()
    {
        resetParameters();
        resetResult();
    }

    /**
     * Connects this builder with a {@code ReloadingController}. With this
     * method support for reloading can be added to an arbitrary builder object.
     * Event listeners are registered at the reloading controller and this
     * builder with connect both objects:
     * <ul>
     * <li>When the reloading controller detects that a reload is required, the
     * builder's {@link #resetResult()} method is called; so the managed result
     * object is invalidated.</li>
     * <li>When a new result object has been created the controller's reloading
     * state is reset, so that new changes can be detected again.</li>
     * </ul>
     *
     * @param controller the {@code ReloadingController} to connect to (must not
     *        be <b>null</b>)
     * @throws IllegalArgumentException if the controller is <b>null</b>
     */
    public final void connectToReloadingController(
            ReloadingController controller)
    {
        if (controller == null)
        {
            throw new IllegalArgumentException(
                    "ReloadingController must not be null!");
        }
        ReloadingBuilderSupportListener.connect(this, controller);
    }

    /**
     * Creates a new, initialized result object. This method is called by
     * {@code getConfiguration()} if no valid result object exists. This base
     * implementation performs two steps:
     * <ul>
     * <li>{@code createResultInstance()} is called to create a new,
     * uninitialized result object.</li>
     * <li>{@code initResultInstance()} is called to process all initialization
     * parameters.</li>
     * </ul>
     * It also evaluates the <em>allowFailOnInit</em> flag, i.e. if
     * initialization causes an exception and this flag is set, the exception is
     * ignored, and the newly created, uninitialized configuration is returned.
     * Note that this method is called in a synchronized block.
     *
     * @return the newly created result object
     * @throws ConfigurationException if an error occurs
     */
    protected T createResult() throws ConfigurationException
    {
        T resObj = createResultInstance();

        try
        {
            initResultInstance(resObj);
        }
        catch (ConfigurationException cex)
        {
            if (!isAllowFailOnInit())
            {
                throw cex;
            }
        }

        return resObj;
    }

    /**
     * Creates the new, uninitialized result object. This is the first step of
     * the process of producing a result object for this builder. This
     * implementation uses the {@link BeanHelper} class to create a new object
     * based on the {@link BeanDeclaration} returned by
     * {@link #getResultDeclaration()}. Note: This method is invoked in a
     * synchronized block.
     *
     * @return the newly created, yet uninitialized result object
     * @throws ConfigurationException if an exception occurs
     */
    protected T createResultInstance() throws ConfigurationException
    {
        Object bean = fetchBeanHelper().createBean(getResultDeclaration());
        checkResultInstance(bean);
        return getResultClass().cast(bean);
    }

    /**
     * Initializes a newly created result object. This is the second step of the
     * process of producing a result object for this builder. This
     * implementation uses the {@link BeanHelper} class to initialize the
     * object's property based on the {@link BeanDeclaration} returned by
     * {@link #getResultDeclaration()}. Note: This method is invoked in a
     * synchronized block. This is required because internal state is accessed.
     * Sub classes must not call this method without proper synchronization.
     *
     * @param obj the object to be initialized
     * @throws ConfigurationException if an error occurs
     */
    protected void initResultInstance(T obj) throws ConfigurationException
    {
        fetchBeanHelper().initBean(obj, getResultDeclaration());
        registerEventListeners(obj);
        handleInitializable(obj);
    }

    /**
     * Returns the {@code BeanDeclaration} that is used to create and initialize
     * result objects. The declaration is created on first access (by invoking
     * {@link #createResultDeclaration(Map)}) based on the current
     * initialization parameters.
     *
     * @return the {@code BeanDeclaration} for dynamically creating a result
     *         object
     * @throws ConfigurationException if an error occurs
     */
    protected final synchronized BeanDeclaration getResultDeclaration()
            throws ConfigurationException
    {
        if (resultDeclaration == null)
        {
            resultDeclaration = createResultDeclaration(getFilteredParameters());
        }
        return resultDeclaration;
    }

    /**
     * Returns a (unmodifiable) map with the current initialization parameters
     * set for this builder. The map is populated with the parameters set using
     * the various configuration options.
     *
     * @return a map with the current set of initialization parameters
     */
    protected final synchronized Map<String, Object> getParameters()
    {
        if (parameters != null)
        {
            return parameters;
        }
        return Collections.emptyMap();
    }

    /**
     * Obtains the {@code BeanHelper} object to be used when dealing with bean
     * declarations. This method checks whether this builder was configured with
     * a specific {@code BeanHelper} instance. If so, this instance is used.
     * Otherwise, the default {@code BeanHelper} is returned.
     *
     * @return the {@code BeanHelper} to be used
     */
    protected final BeanHelper fetchBeanHelper()
    {
        BeanHelper helper =
                BasicBuilderParameters.fetchBeanHelper(getParameters());
        return (helper != null) ? helper : BeanHelper.INSTANCE;
    }

    /**
     * Creates a new {@code BeanDeclaration} which is used for creating new
     * result objects dynamically. This implementation creates a specialized
     * {@code BeanDeclaration} object that is initialized from the given map of
     * initialization parameters. The {@code BeanDeclaration} must be
     * initialized with the result class of this builder, otherwise exceptions
     * will be thrown when the result object is created. Note: This method is
     * invoked in a synchronized block.
     *
     * @param params a snapshot of the current initialization parameters
     * @return the {@code BeanDeclaration} for creating result objects
     * @throws ConfigurationException if an error occurs
     */
    protected BeanDeclaration createResultDeclaration(
            final Map<String, Object> params) throws ConfigurationException
    {
        return new BeanDeclaration()
        {
            @Override
            public Map<String, Object> getNestedBeanDeclarations()
            {
                // no nested beans
                return Collections.emptyMap();
            }

            @Override
            public Collection<ConstructorArg> getConstructorArgs()
            {
                // no constructor arguments
                return Collections.emptySet();
            }

            @Override
            public Map<String, Object> getBeanProperties()
            {
                // the properties are equivalent to the parameters
                return params;
            }

            @Override
            public Object getBeanFactoryParameter()
            {
                return null;
            }

            @Override
            public String getBeanFactoryName()
            {
                return null;
            }

            @Override
            public String getBeanClassName()
            {
                return getResultClass().getName();
            }
        };
    }

    /**
     * Copies all {@code EventListener} objects registered at this builder to
     * the specified target configuration builder. This method is intended to be
     * used by derived classes which support inheritance of their properties to
     * other builder objects.
     *
     * @param target the target configuration builder (must not be <b>null</b>)
     * @throws NullPointerException if the target builder is <b>null</b>
     */
    protected synchronized void copyEventListeners(
            BasicConfigurationBuilder<?> target)
    {
        copyEventListeners(target, eventListeners);
    }

    /**
     * Copies all event listeners in the specified list to the specified target
     * configuration builder. This method is intended to be used by derived
     * classes which have to deal with managed configuration builders that need
     * to be initialized with event listeners.
     *
     * @param target the target configuration builder (must not be <b>null</b>)
     * @param listeners the event listeners to be copied over
     * @throws NullPointerException if the target builder is <b>null</b>
     */
    protected void copyEventListeners(BasicConfigurationBuilder<?> target,
            EventListenerList listeners)
    {
        target.eventListeners.addAll(listeners);
    }

    /**
     * Adds the specified event listener to this object. This method is called
     * by {@code addEventListener()}, it does the actual listener registration.
     * Because it is final it can be called by sub classes in the constructor if
     * there is already the need to register an event listener.
     *
     * @param eventType the event type object
     * @param listener the listener to be registered
     * @param <E> the event type
     */
    protected final <E extends Event> void installEventListener(
            EventType<E> eventType, EventListener<? super E> listener)
    {
        fetchEventSource().addEventListener(eventType, listener);
        eventListeners.addEventListener(eventType, listener);
    }

    /**
     * Sends the specified builder event to all registered listeners.
     *
     * @param event the event to be fired
     */
    protected void fireBuilderEvent(ConfigurationBuilderEvent event)
    {
        eventListeners.fire(event);
    }

    /**
     * Replaces the current map with parameters by a new one.
     *
     * @param newParams the map with new parameters (may be <b>null</b>)
     */
    private void updateParameters(Map<String, Object> newParams)
    {
        Map<String, Object> map = new HashMap<>();
        if (newParams != null)
        {
            map.putAll(newParams);
        }
        parameters = Collections.unmodifiableMap(map);
    }

    /**
     * Registers the available event listeners at the given object. This method
     * is called for each result object created by the builder.
     *
     * @param obj the object to initialize
     */
    private void registerEventListeners(T obj)
    {
        EventSource evSrc = ConfigurationUtils.asEventSource(obj, true);
        for (EventListenerRegistrationData<?> regData : eventListeners
                .getRegistrations())
        {
            registerListener(evSrc, regData);
        }
    }

    /**
     * Removes all available event listeners from the given result object. This
     * method is called when the result of this builder is reset. Then the old
     * managed configuration should no longer generate events.
     *
     * @param obj the affected result object
     */
    private void removeEventListeners(T obj)
    {
        EventSource evSrc = ConfigurationUtils.asEventSource(obj, true);
        for (EventListenerRegistrationData<?> regData : eventListeners
                .getRegistrations())
        {
            removeListener(evSrc, regData);
        }
    }

    /**
     * Returns an {@code EventSource} for the current result object. If there is
     * no current result or if it does not extend {@code EventSource}, a dummy
     * event source is returned.
     *
     * @return the {@code EventSource} for the current result object
     */
    private EventSource fetchEventSource()
    {
        return ConfigurationUtils.asEventSource(result, true);
    }

    /**
     * Checks whether the specified parameters object implements the
     * {@code EventListenerProvider} interface. If so, the event listeners it
     * provides are added to this builder.
     *
     * @param params the parameters object
     */
    private void handleEventListenerProviders(BuilderParameters params)
    {
        if (params instanceof EventListenerProvider)
        {
            eventListeners.addAll(((EventListenerProvider) params)
                    .getListeners());
        }
    }

    /**
     * Checks whether the class of the result configuration is compatible with
     * this builder's result class. This is done to ensure that only objects of
     * the expected result class are created.
     *
     * @param inst the result instance to be checked
     * @throws ConfigurationRuntimeException if an invalid result class is
     *         detected
     */
    private void checkResultInstance(Object inst)
    {
        if (!getResultClass().isInstance(inst))
        {
            throw new ConfigurationRuntimeException(
                    "Incompatible result object: " + inst);
        }
    }

    /**
     * Returns a map with initialization parameters where all parameters
     * starting with the reserved prefix have been filtered out.
     *
     * @return the filtered parameters map
     */
    private Map<String, Object> getFilteredParameters()
    {
        Map<String, Object> filteredMap =
                new HashMap<>(getParameters());
        for (Iterator<String> it = filteredMap.keySet().iterator(); it
                .hasNext();)
        {
            String key = it.next();
            if (key.startsWith(BuilderParameters.RESERVED_PARAMETER_PREFIX))
            {
                it.remove();
            }
        }
        return filteredMap;
    }

    /**
     * Performs special initialization of the result object. This method is
     * called after parameters have been set on a newly created result instance.
     * If supported by the result class, the {@code initialize()} method is now
     * called.
     *
     * @param obj the newly created result object
     */
    private void handleInitializable(T obj)
    {
        if (obj instanceof Initializable)
        {
            ((Initializable) obj).initialize();
        }
    }

    /**
     * Registers an event listener at an event source object.
     *
     * @param evSrc the event source
     * @param regData the registration data object
     * @param <E> the type of the event listener
     */
    private static <E extends Event> void registerListener(EventSource evSrc,
            EventListenerRegistrationData<E> regData)
    {
        evSrc.addEventListener(regData.getEventType(), regData.getListener());
    }

    /**
     * Removes an event listener from an event source object.
     *
     * @param evSrc the event source
     * @param regData the registration data object
     * @param <E> the type of the event listener
     */
    private static <E extends Event> void removeListener(EventSource evSrc,
            EventListenerRegistrationData<E> regData)
    {
        evSrc.removeEventListener(regData.getEventType(), regData.getListener());
    }
}
