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

import java.util.Iterator;
import java.util.Objects;

import org.apache.commons.configuration2.convert.ListDelimiterHandler;
import org.apache.commons.lang3.StringUtils;

/**
 * <p>
 * A subset of another configuration. The new Configuration object contains every key from the parent Configuration that
 * starts with prefix. The prefix is removed from the keys in the subset.
 * </p>
 * <p>
 * It is usually not necessary to use this class directly. Instead the {@link Configuration#subset(String)} method
 * should be used, which will return a correctly initialized instance.
 * </p>
 */
public class SubsetConfiguration extends AbstractConfiguration {
    /**
     * A specialized iterator to be returned by the {@code getKeys()} methods. This implementation wraps an iterator from
     * the parent configuration. The keys returned by this iterator are correspondingly transformed.
     */
    private final class SubsetIterator implements Iterator<String> {
        /** Stores the wrapped iterator. */
        private final Iterator<String> parentIterator;

        /**
         * Creates a new instance of {@code SubsetIterator} and initializes it with the parent iterator.
         *
         * @param it the iterator of the parent configuration
         */
        public SubsetIterator(final Iterator<String> it) {
            parentIterator = it;
        }

        /**
         * Checks whether there are more elements. Delegates to the parent iterator.
         *
         * @return a flag whether there are more elements
         */
        @Override
        public boolean hasNext() {
            return parentIterator.hasNext();
        }

        /**
         * Returns the next element in the iteration. This is the next key from the parent configuration, transformed to
         * correspond to the point of view of this subset configuration.
         *
         * @return the next element
         */
        @Override
        public String next() {
            return getChildKey(parentIterator.next());
        }

        /**
         * Removes the current element from the iteration. Delegates to the parent iterator.
         */
        @Override
        public void remove() {
            parentIterator.remove();
        }
    }

    /** The parent configuration. */
    protected Configuration parent;

    /** The prefix used to select the properties. */
    protected String prefix;

    /** The prefix delimiter */
    protected String delimiter;

    /**
     * Create a subset of the specified configuration
     *
     * @param parent The parent configuration (must not be <b>null</b>)
     * @param prefix The prefix used to select the properties
     * @throws IllegalArgumentException if the parent configuration is <b>null</b>
     */
    public SubsetConfiguration(final Configuration parent, final String prefix) {
        this(parent, prefix, null);
    }

    /**
     * Create a subset of the specified configuration
     *
     * @param parent The parent configuration (must not be <b>null</b>)
     * @param prefix The prefix used to select the properties
     * @param delimiter The prefix delimiter
     * @throws NullPointerException if the parent configuration is <b>null</b>
     */
    public SubsetConfiguration(final Configuration parent, final String prefix, final String delimiter) {
        this.parent = Objects.requireNonNull(parent, "parent");
        this.prefix = prefix;
        this.delimiter = delimiter;
        initInterpolator();
    }

    @Override
    public void addPropertyDirect(final String key, final Object value) {
        parent.addProperty(getParentKey(key), value);
    }

    @Override
    protected void clearPropertyDirect(final String key) {
        parent.clearProperty(getParentKey(key));
    }

    @Override
    protected boolean containsKeyInternal(final String key) {
        return parent.containsKey(getParentKey(key));
    }

    /**
     * Tests whether this configuration contains one or more matches to this value. This operation stops at first match
     * but may be more expensive than the containsKey method.
     * @since 2.11.0
     */
    @Override
    protected boolean containsValueInternal(final Object value) {
        return parent.containsValue(value);
    }

    /**
     * Gets the key in the subset configuration associated to the specified key in the parent configuration.
     *
     * @param key The key in the parent configuration.
     * @return the key in the context of this subset configuration
     */
    protected String getChildKey(final String key) {
        if (!key.startsWith(prefix)) {
            throw new IllegalArgumentException("The parent key '" + key + "' is not in the subset.");
        }
        String modifiedKey = null;
        if (key.length() == prefix.length()) {
            modifiedKey = "";
        } else {
            final int i = prefix.length() + (delimiter != null ? delimiter.length() : 0);
            modifiedKey = key.substring(i);
        }

        return modifiedKey;
    }

    @Override
    protected Iterator<String> getKeysInternal() {
        return new SubsetIterator(parent.getKeys(prefix, delimiter));
    }

    @Override
    protected Iterator<String> getKeysInternal(final String prefix) {
        return new SubsetIterator(parent.getKeys(getParentKey(prefix)));
    }

    /**
     * {@inheritDoc} If the parent configuration extends {@link AbstractConfiguration}, the list delimiter handler is
     * obtained from there.
     */
    @Override
    public ListDelimiterHandler getListDelimiterHandler() {
        return parent instanceof AbstractConfiguration ? ((AbstractConfiguration) parent).getListDelimiterHandler() : super.getListDelimiterHandler();
    }

    /**
     * Gets the parent configuration for this subset.
     *
     * @return the parent configuration
     */
    public Configuration getParent() {
        return parent;
    }

    /**
     * Gets the key in the parent configuration associated to the specified key in this subset.
     *
     * @param key The key in the subset.
     * @return the key as to be used by the parent
     */
    protected String getParentKey(final String key) {
        if (StringUtils.isEmpty(key)) {
            return prefix;
        }
        return delimiter == null ? prefix + key : prefix + delimiter + key;
    }

    /**
     * Gets the prefix used to select the properties in the parent configuration.
     *
     * @return the prefix used by this subset
     */
    public String getPrefix() {
        return prefix;
    }

    @Override
    protected Object getPropertyInternal(final String key) {
        return parent.getProperty(getParentKey(key));
    }

    /**
     * Initializes the {@code ConfigurationInterpolator} for this sub configuration. This is a standard
     * {@code ConfigurationInterpolator} which also references the {@code ConfigurationInterpolator} of the parent
     * configuration.
     */
    private void initInterpolator() {
        getInterpolator().setParentInterpolator(getParent().getInterpolator());
    }

    @Override
    protected boolean isEmptyInternal() {
        return !getKeysInternal().hasNext();
    }

    /**
     * {@inheritDoc}
     *
     * The subset inherits this feature from its parent if it supports this feature.
     */
    @Override
    public boolean isThrowExceptionOnMissing() {
        if (parent instanceof AbstractConfiguration) {
            return ((AbstractConfiguration) parent).isThrowExceptionOnMissing();
        }
        return super.isThrowExceptionOnMissing();
    }

    /**
     * {@inheritDoc} If the parent configuration extends {@link AbstractConfiguration}, the list delimiter handler is passed
     * to the parent.
     */
    @Override
    public void setListDelimiterHandler(final ListDelimiterHandler listDelimiterHandler) {
        if (parent instanceof AbstractConfiguration) {
            ((AbstractConfiguration) parent).setListDelimiterHandler(listDelimiterHandler);
        } else {
            super.setListDelimiterHandler(listDelimiterHandler);
        }
    }

    /**
     * Sets the prefix used to select the properties in the parent configuration.
     *
     * @param prefix the prefix
     */
    public void setPrefix(final String prefix) {
        this.prefix = prefix;
    }

    /**
     * {@inheritDoc}
     *
     * Change the behavior of the parent configuration if it supports this feature.
     */
    @Override
    public void setThrowExceptionOnMissing(final boolean throwExceptionOnMissing) {
        if (parent instanceof AbstractConfiguration) {
            ((AbstractConfiguration) parent).setThrowExceptionOnMissing(throwExceptionOnMissing);
        } else {
            super.setThrowExceptionOnMissing(throwExceptionOnMissing);
        }
    }

    @Override
    public Configuration subset(final String prefix) {
        return parent.subset(getParentKey(prefix));
    }
}
