/*
 * Copyright 2012-2015 the original author or authors.
 *
 * 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
 *
 *      https://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.springframework.boot.env;

import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * Utility that can be used to {@link MutablePropertySources} using
 * {@link PropertySourceLoader}s.
 *
 * @author Phillip Webb
 */
public class PropertySourcesLoader {

	private static final Log logger = LogFactory.getLog(PropertySourcesLoader.class);

	private final MutablePropertySources propertySources;

	private final List<PropertySourceLoader> loaders;

	/**
	 * Create a new {@link PropertySourceLoader} instance backed by a new
	 * {@link MutablePropertySources}.
	 */
	public PropertySourcesLoader() {
		this(new MutablePropertySources());
	}

	/**
	 * Create a new {@link PropertySourceLoader} instance backed by the specified
	 * {@link MutablePropertySources}.
	 * @param propertySources the destination property sources
	 */
	public PropertySourcesLoader(MutablePropertySources propertySources) {
		Assert.notNull(propertySources, "PropertySources must not be null");
		this.propertySources = propertySources;
		this.loaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
				getClass().getClassLoader());
	}

	/**
	 * Load the specified resource (if possible) and add it as the first source.
	 * @param resource the source resource (may be {@code null}).
	 * @return the loaded property source or {@code null}
	 * @throws IOException if the source cannot be loaded
	 */
	public PropertySource<?> load(Resource resource) throws IOException {
		return load(resource, null);
	}

	/**
	 * Load the profile-specific properties from the specified resource (if any) and add
	 * it as the first source.
	 * @param resource the source resource (may be {@code null}).
	 * @param profile a specific profile to load or {@code null} to load the default.
	 * @return the loaded property source or {@code null}
	 * @throws IOException if the source cannot be loaded
	 */
	public PropertySource<?> load(Resource resource, String profile) throws IOException {
		return load(resource, resource.getDescription(), profile);
	}

	/**
	 * Load the profile-specific properties from the specified resource (if any), give the
	 * name provided and add it as the first source.
	 * @param resource the source resource (may be {@code null}).
	 * @param name the root property name (may be {@code null}).
	 * @param profile a specific profile to load or {@code null} to load the default.
	 * @return the loaded property source or {@code null}
	 * @throws IOException if the source cannot be loaded
	 */
	public PropertySource<?> load(Resource resource, String name, String profile)
			throws IOException {
		return load(resource, null, name, profile);
	}

	/**
	 * Load the profile-specific properties from the specified resource (if any), give the
	 * name provided and add it to a group of property sources identified by the group
	 * name. Property sources are added to the end of a group, but new groups are added as
	 * the first in the chain being assembled. This means the normal sequence of calls is
	 * to first create the group for the default (null) profile, and then add specific
	 * groups afterwards (with the highest priority last). Property resolution from the
	 * resulting sources will consider all keys for a given group first and then move to
	 * the next group.
	 * @param resource the source resource (may be {@code null}).
	 * @param group an identifier for the group that this source belongs to
	 * @param name the root property name (may be {@code null}).
	 * @param profile a specific profile to load or {@code null} to load the default.
	 * @return the loaded property source or {@code null}
	 * @throws IOException if the source cannot be loaded
	 */
	public PropertySource<?> load(Resource resource, String group, String name,
			String profile) throws IOException {
		if (isFile(resource)) {
			String sourceName = generatePropertySourceName(name, profile);
			for (PropertySourceLoader loader : this.loaders) {
				if (canLoadFileExtension(loader, resource)) {
					PropertySource<?> specific = loader.load(sourceName, resource,
							profile);
					addPropertySource(group, specific, profile);
					return specific;
				}
			}
		}
		return null;
	}

	private boolean isFile(Resource resource) {
		return resource != null && resource.exists() && StringUtils
				.hasText(StringUtils.getFilenameExtension(resource.getFilename()));
	}

	private String generatePropertySourceName(String name, String profile) {
		return (profile == null ? name : name + "#" + profile);
	}

	private boolean canLoadFileExtension(PropertySourceLoader loader, Resource resource) {
		String filename = resource.getFilename().toLowerCase();
		for (String extension : loader.getFileExtensions()) {
			if (filename.endsWith("." + extension.toLowerCase())) {
				return true;
			}
		}
		return false;
	}

	private void addPropertySource(String basename, PropertySource<?> source,
			String profile) {

		if (source == null) {
			return;
		}

		if (basename == null) {
			this.propertySources.addLast(source);
			return;
		}

		EnumerableCompositePropertySource group = getGeneric(basename);
		group.add(source);
		logger.trace("Adding PropertySource: " + source + " in group: " + basename);
		if (this.propertySources.contains(group.getName())) {
			this.propertySources.replace(group.getName(), group);
		}
		else {
			this.propertySources.addFirst(group);
		}

	}

	private EnumerableCompositePropertySource getGeneric(String name) {
		PropertySource<?> source = this.propertySources.get(name);
		if (source instanceof EnumerableCompositePropertySource) {
			return (EnumerableCompositePropertySource) source;
		}
		EnumerableCompositePropertySource composite = new EnumerableCompositePropertySource(
				name);
		return composite;
	}

	/**
	 * Return the {@link MutablePropertySources} being loaded.
	 * @return the property sources
	 */
	public MutablePropertySources getPropertySources() {
		return this.propertySources;
	}

	/**
	 * Returns all file extensions that could be loaded.
	 * @return the file extensions
	 */
	public Set<String> getAllFileExtensions() {
		Set<String> fileExtensions = new LinkedHashSet<String>();
		for (PropertySourceLoader loader : this.loaders) {
			fileExtensions.addAll(Arrays.asList(loader.getFileExtensions()));
		}
		return fileExtensions;
	}

}
