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

import static org.apache.juneau.assertions.Assertions.*;
import static org.apache.juneau.internal.StringUtils.*;
import static org.apache.juneau.internal.ThrowableUtils.*;
import static org.apache.juneau.internal.CollectionUtils.*;
import static org.apache.juneau.internal.IOUtils.*;

import java.io.*;
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;

import org.apache.juneau.*;
import org.apache.juneau.collections.*;
import org.apache.juneau.config.event.*;
import org.apache.juneau.config.internal.*;
import org.apache.juneau.config.mod.*;
import org.apache.juneau.config.store.*;
import org.apache.juneau.config.vars.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.json.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.serializer.*;
import org.apache.juneau.svl.*;
import org.apache.juneau.utils.*;

/**
 * Main configuration API class.
 *
 * <ul class='notes'>
 * 	<li class='note'>This class is thread safe and reusable.
 * </ul>
 *
 * <ul class='seealso'>
 * 	<li class='link'>{@doc juneau-config}
 * 	<li class='extlink'>{@source}
 * </ul>
 */
public final class Config extends Context implements ConfigEventListener {

	//-----------------------------------------------------------------------------------------------------------------
	// Static
	//-----------------------------------------------------------------------------------------------------------------

	private static boolean DISABLE_AUTO_SYSTEM_PROPS = Boolean.getBoolean("juneau.disableAutoSystemProps");
	private static volatile Config SYSTEM_DEFAULT = findSystemDefault();

	/**
	 * Sets a system default configuration.
	 *
	 * @param systemDefault The new system default configuration.
	 */
	public synchronized static void setSystemDefault(Config systemDefault) {
		SYSTEM_DEFAULT = systemDefault;
	}

	/**
	 * Returns the system default configuration.
	 *
	 * @return The system default configuration, or <jk>null</jk> if it doesn't exist.
	 */
	public synchronized static Config getSystemDefault() {
		return SYSTEM_DEFAULT;
	}

	private synchronized static Config findSystemDefault() {

		for (String n : getCandidateSystemDefaultConfigNames()) {
			Config config = find(n);
			if (config != null) {
				if (! DISABLE_AUTO_SYSTEM_PROPS)
					config.setSystemProperties();
				return config;
			}
		}

		return null;
	}

	/**
	 * Returns the list of candidate system default configuration file names.
	 *
	 * <p>
	 * If the <js>"juneau.configFile"</js> system property is set, returns a singleton of that value.
	 * <br>Otherwise, returns a list consisting of the following values:
	 * <ol>
	 * 	<li>File with same name as jar file but with <js>".cfg"</js> extension.  (e.g. <js>"myjar.cfg"</js>)
	 * 	<li>Any file ending in <js>".cfg"</js> in the home directory (names ordered alphabetically).
	 * 	<li><js>"juneau.cfg"</js>
	 * 	<li><js>"default.cfg"</js>
	 * 	<li><js>"application.cfg"</js>
	 * 	<li><js>"app.cfg"</js>
	 * 	<li><js>"settings.cfg"</js>
	 * 	<li><js>"application.properties"</js>
	 * </ol>
	 * <p>
	 *
	 * @return
	 * 	A list of candidate file names.
	 * 	<br>The returned list is modifiable.
	 * 	<br>Each call constructs a new list.
	 */
	public synchronized static List<String> getCandidateSystemDefaultConfigNames() {
		List<String> l = list();

		String s = System.getProperty("juneau.configFile");
		if (s != null) {
			l.add(s);
			return l;
		}

		String cmd = System.getProperty("sun.java.command", "not_found").split("\\s+")[0];
		if (cmd.endsWith(".jar") && ! cmd.contains("surefirebooter")) {
			cmd = cmd.replaceAll(".*?([^\\\\\\/]+)\\.jar$", "$1");
			l.add(cmd + ".cfg");
			cmd = cmd.replaceAll("[\\.\\_].*$", "");  // Try also without version in jar name.
			l.add(cmd + ".cfg");
		}

		Set<File> files = sortedSet(new File(".").listFiles());
		for (File f : files)
			if (f.getName().endsWith(".cfg"))
				l.add(f.getName());

		l.add("juneau.cfg");
		l.add("default.cfg");
		l.add("application.cfg");
		l.add("app.cfg");
		l.add("settings.cfg");
		l.add("application.properties");

		return l;
	}

	private synchronized static Config find(String name) {
		if (name == null)
			return null;
		if (FileStore.DEFAULT.exists(name))
			return Config.create(name).store(FileStore.DEFAULT).build();
		if (ClasspathStore.DEFAULT.exists(name))
			return Config.create(name).store(ClasspathStore.DEFAULT).build();
		return null;
	}

	/**
	 * Creates a new builder for this object.
	 *
	 * @return A new builder.
	 */
	public static Builder create() {
		return new Builder();
	}

	/**
	 * Same as {@link #create()} but initializes the builder with the specified config name.
	 *
	 * @param name The configuration name.
	 * @return A new builder.
	 */
	public static Builder create(String name) {
		return new Builder().name(name);
	}

	//-------------------------------------------------------------------------------------------------------------------
	// Builder
	//-------------------------------------------------------------------------------------------------------------------

	/**
	 * Builder class.
	 */
	@FluentSetters
	public static class Builder extends Context.Builder {

		String name;
		ConfigStore store;
		WriterSerializer serializer;
		ReaderParser parser;
		Map<Character,Mod> mods;
		VarResolver varResolver;
		int binaryLineLength;
		BinaryFormat binaryFormat;
		boolean multiLineValuesOnSeparateLines, readOnly;

		/**
		 * Constructor, default settings.
		 */
		protected Builder() {
			super();
			name = env("Config.name", "Configuration.cfg");
			store = FileStore.DEFAULT;
			serializer = SimpleJsonSerializer.DEFAULT;
			parser = JsonParser.DEFAULT;
			mods = map();
			mods(XorEncodeMod.INSTANCE);
			varResolver = VarResolver.DEFAULT;
			binaryLineLength = env("Config.binaryLineLength", -1);
			binaryFormat = env("Config.binaryFormat", BinaryFormat.BASE64);
			multiLineValuesOnSeparateLines = env("Config.multiLineValuesOnSeparateLines", false);
			readOnly = env("Config.readOnly", false);
		}

		/**
		 * Copy constructor.
		 *
		 * @param copyFrom The bean to copy from.
		 */
		protected Builder(Config copyFrom) {
			super(copyFrom);
			name = copyFrom.name;
			store = copyFrom.store;
			serializer = copyFrom.serializer;
			parser = copyFrom.parser;
			mods = copyOf(copyFrom.mods);
			varResolver = copyFrom.varResolver;
			binaryLineLength = copyFrom.binaryLineLength;
			binaryFormat = copyFrom.binaryFormat;
			multiLineValuesOnSeparateLines = copyFrom.multiLineValuesOnSeparateLines;
			readOnly = copyFrom.readOnly;
		}

		/**
		 * Copy constructor.
		 *
		 * @param copyFrom The builder to copy from.
		 */
		protected Builder(Builder copyFrom) {
			super(copyFrom);
			name = copyFrom.name;
			store = copyFrom.store;
			serializer = copyFrom.serializer;
			parser = copyFrom.parser;
			mods = copyOf(copyFrom.mods);
			varResolver = copyFrom.varResolver;
			binaryLineLength = copyFrom.binaryLineLength;
			binaryFormat = copyFrom.binaryFormat;
			multiLineValuesOnSeparateLines = copyFrom.multiLineValuesOnSeparateLines;
			readOnly = copyFrom.readOnly;
		}

		@Override /* Context.Builder */
		public Builder copy() {
			return new Builder(this);
		}

		@Override /* Context.Builder */
		public Config build() {
			return build(Config.class);
		}

		//-----------------------------------------------------------------------------------------------------------------
		// Properties
		//-----------------------------------------------------------------------------------------------------------------

		/**
		 * Configuration name.
		 *
		 * <p>
		 * Specifies the configuration name.
		 * <br>This is typically the configuration file name, although
		 * the name can be anything identifiable by the {@link ConfigStore} used for retrieving and storing the configuration.
		 *
		 * @param value
		 * 	The new value for this property.
		 * 	<br>The default is the first value found:
		 * 	<ul>
		 * 		<li>System property <js>"Config.name"
		 * 		<li>Environment variable <js>"CONFIG_NAME"
		 * 		<li><js>"Configuration.cfg"</js>
		 * 	</ul>
		 * @return This object.
		 */
		public Builder name(String value) {
			name = value;
			return this;
		}

		/**
		 * Configuration store.
		 *
		 * <p>
		 * The configuration store used for retrieving and storing configurations.
		 *
		 * @param value
		 * 	The new value for this property.
		 * 	<br>The default is {@link FileStore#DEFAULT}.
		 * @return This object.
		 */
		public Builder store(ConfigStore value) {
			store = value;
			return this;
		}

		/**
		 * Configuration store.
		 *
		 * <p>
		 * Convenience method for calling <code>store(ConfigMemoryStore.<jsf>DEFAULT</jsf>)</code>.
		 *
		 * @return This object.
		 */
		public Builder memStore() {
			store = MemoryStore.DEFAULT;
			return this;
		}

		/**
		 * POJO serializer.
		 *
		 * <p>
		 * The serializer to use for serializing POJO values.
		 *
		 * @param value
		 * 	The new value for this property.
		 * 	<br>The default is {@link SimpleJsonSerializer#DEFAULT}
		 * @return This object.
		 */
		public Builder serializer(WriterSerializer value) {
			serializer = value;
			return this;
		}

		/**
		 * POJO parser.
		 *
		 * <p>
		 * The parser to use for parsing values to POJOs.
		 *
		 * @param value
		 * 	The new value for this property.
		 * 	<br>The default is {@link JsonParser#DEFAULT}.
		 * @return This object.
		 */
		public Builder parser(ReaderParser value) {
			parser = value;
			return this;
		}

		/**
		 * Adds a value modifier.
		 *
		 * <p>
		 * Modifiers are used to modify entry value before being persisted.
		 *
		 * @param values
		 * 	The mods to apply to this config.
		 * @return This object.
		 */
		public Builder mods(Mod...values) {
			for (Mod value : values)
				mods.put(value.getId(), value);
			return this;
		}

		/**
		 * SVL variable resolver.
		 *
		 * <p>
		 * The resolver to use for resolving SVL variables.
		 *
		 * @param value
		 * 	The new value for this property.
		 * 	<br>The default is {@link VarResolver#DEFAULT}.
		 * @return This object.
		 */
		public Builder varResolver(VarResolver value) {
			varResolver = value;
			return this;
		}

		/**
		 * Binary value line length.
		 *
		 * <p>
		 * When serializing binary values, lines will be split after this many characters.
		 * <br>Use <c>-1</c> to represent no line splitting.
		 *
		 * @param value
		 * 	The new value for this property.
		 * 	<br>The default is the first value found:
		 * 	<ul>
		 * 		<li>System property <js>"Config.binaryLineLength"
		 * 		<li>Environment variable <js>"CONFIG_BINARYLINELENGTH"
		 * 		<li><c>-1</c>
		 * 	</ul>
		 * @return This object.
		 */
		public Builder binaryLineLength(int value) {
			binaryLineLength = value;
			return this;
		}

		/**
		 * Binary value format.
		 *
		 * <p>
		 * The format to use when persisting byte arrays.
		 *
		 * <ul class='values'>
		 * 	<li>{@link BinaryFormat#BASE64} - BASE64-encoded string.
		 * 	<li>{@link BinaryFormat#HEX} - Hexadecimal.
		 * 	<li>{@link BinaryFormat#SPACED_HEX} - Hexadecimal with spaces between bytes.
		 * </ul>
		 *
		 * @param value
		 * 	The new value for this property.
		 * 	<br>The default is the first value found:
		 * 	<ul>
		 * 		<li>System property <js>"Config.binaryFormat"
		 * 		<li>Environment variable <js>"CONFIG_BINARYFORMAT"
		 * 		<li>{@link BinaryFormat#BASE64}
		 * 	</ul>
		 * @return This object.
		 */
		public Builder binaryFormat(BinaryFormat value) {
			binaryFormat = value;
			return this;
		}

		/**
		 * Multi-line values on separate lines.
		 *
		 * <p>
		 * When enabled, multi-line values will always be placed on a separate line from the key.
		 *
		 * <p>
		 * The default is the first value found:
		 * 	<ul>
		 * 		<li>System property <js>"Config.multiLineValuesOnSeparateLine"
		 * 		<li>Environment variable <js>"CONFIG_MULTILINEVALUESONSEPARATELINE"
		 * 		<li><jk>false</jk>
		 * 	</ul>
		 *
		 * @return This object.
		 */
		public Builder multiLineValuesOnSeparateLines() {
			multiLineValuesOnSeparateLines = true;
			return this;
		}

		/**
		 * Read-only mode.
		 *
		 * <p>
		 * When enabled, attempts to call any setters on this object will throw an {@link UnsupportedOperationException}.
		 *
		 * <p>
		 * 	The default is the first value found:
		 * 	<ul>
		 * 		<li>System property <js>"Config.readOnly"
		 * 		<li>Environment variable <js>"CONFIG_READONLY"
		 * 		<li><jk>false</jk>
		 * 	</ul>
		 *
		 * @return This object.
		 */
		public Builder readOnly() {
			readOnly = true;
			return this;
		}

		// <FluentSetters>

		@Override /* GENERATED - org.apache.juneau.Context.Builder */
		public Builder annotations(Annotation...values) {
			super.annotations(values);
			return this;
		}

		@Override /* GENERATED - org.apache.juneau.Context.Builder */
		public Builder apply(AnnotationWorkList work) {
			super.apply(work);
			return this;
		}

		@Override /* GENERATED - org.apache.juneau.Context.Builder */
		public Builder applyAnnotations(java.lang.Class<?>...fromClasses) {
			super.applyAnnotations(fromClasses);
			return this;
		}

		@Override /* GENERATED - org.apache.juneau.Context.Builder */
		public Builder applyAnnotations(Method...fromMethods) {
			super.applyAnnotations(fromMethods);
			return this;
		}

		@Override /* GENERATED - org.apache.juneau.Context.Builder */
		public Builder cache(Cache<HashKey,? extends org.apache.juneau.Context> value) {
			super.cache(value);
			return this;
		}

		@Override /* GENERATED - org.apache.juneau.Context.Builder */
		public Builder debug() {
			super.debug();
			return this;
		}

		@Override /* GENERATED - org.apache.juneau.Context.Builder */
		public Builder debug(boolean value) {
			super.debug(value);
			return this;
		}

		@Override /* GENERATED - org.apache.juneau.Context.Builder */
		public Builder impl(Context value) {
			super.impl(value);
			return this;
		}

		@Override /* GENERATED - org.apache.juneau.Context.Builder */
		public Builder type(Class<? extends org.apache.juneau.Context> value) {
			super.type(value);
			return this;
		}

		// </FluentSetters>
	}

	//-------------------------------------------------------------------------------------------------------------------
	// Instance
	//-------------------------------------------------------------------------------------------------------------------

	final String name;
	final ConfigStore store;
	final WriterSerializer serializer;
	final ReaderParser parser;
	final Map<Character,Mod> mods;
	final VarResolver varResolver;
	final int binaryLineLength;
	final BinaryFormat binaryFormat;
	final boolean multiLineValuesOnSeparateLines, readOnly;
	final BeanSession beanSession;
	final VarResolverSession varSession;

	private final ConfigMap configMap;
	private final List<ConfigEventListener> listeners = synced(linkedList());


	@Override /* Context */
	public Builder copy() {
		return new Builder(this);
	}

	/**
	 * Constructor.
	 *
	 * @param builder The builder for this object.
	 * @throws IOException Thrown by underlying stream.
	 */
	public Config(Builder builder) throws IOException {
		super(builder);

		name = builder.name;
		store = builder.store;
		configMap = store.getMap(name);
		configMap.register(this);
		serializer = builder.serializer;
		parser = builder.parser;
		beanSession = parser.getBeanContext().getSession();
		mods = unmodifiable(copyOf(builder.mods));
		varResolver = builder.varResolver;
		varSession = varResolver
			.copy()
			.vars(ConfigVar.class)
			.bean(Config.class, this)
			.build()
			.createSession();
		binaryLineLength = builder.binaryLineLength;
		binaryFormat = builder.binaryFormat;
		multiLineValuesOnSeparateLines = builder.multiLineValuesOnSeparateLines;
		readOnly = builder.readOnly;
	}

	Config(Config copyFrom, VarResolverSession varSession) {
		super(copyFrom);
		name = copyFrom.name;
		store = copyFrom.store;
		configMap = copyFrom.configMap;
		configMap.register(this);
		serializer = copyFrom.serializer;
		parser = copyFrom.parser;
		mods = copyFrom.mods;
		varResolver = copyFrom.varResolver;
		this.varSession = varSession;
		binaryLineLength = copyFrom.binaryLineLength;
		binaryFormat = copyFrom.binaryFormat;
		multiLineValuesOnSeparateLines = copyFrom.multiLineValuesOnSeparateLines;
		readOnly = copyFrom.readOnly;
		beanSession = copyFrom.beanSession;
	}

	/**
	 * Creates a copy of this config using the specified var session for resolving variables.
	 *
	 * <p>
	 * This creates a shallow copy of the config but replacing the variable resolver.
	 *
	 * @param varSession The var session used for resolving string variables.
	 * @return A new config object.
	 */
	public Config resolving(VarResolverSession varSession) {
		return new Config(this, varSession);
	}

	/**
	 * Returns the name associated with this config (usually a file name).
	 *
	 * @return The name associated with this config, or <jk>null</jk> if it has no name.
	 */
	public String getName() {
		return name;
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Workhorse getters
	//-----------------------------------------------------------------------------------------------------------------

	/**
	 * Returns the specified value as a string from the config file.
	 *
	 * <p>
	 * Unlike {@link #getString(String)}, this method doesn't replace SVL variables.
	 *
	 * @param key The key.
	 * @return The value, or <jk>null</jk> if the section or value doesn't exist.
	 */
	private String getRaw(String key) {

		String sname = sname(key);
		String skey = skey(key);

		ConfigMapEntry ce = configMap.getEntry(sname, skey);

		if (ce == null)
			return null;

		return removeMods(ce.getModifiers(), ce.getValue());
	}

	String applyMods(String mods, String x) {
		if (mods != null && x != null)
			for (int i = 0; i < mods.length(); i++)
				x = getMod(mods.charAt(i)).doApply(x);
		return x;
	}

	String removeMods(String mods, String x) {
		if (mods != null && x != null)
			for (int i = mods.length()-1; i > -1; i--)
				x = getMod(mods.charAt(i)).doRemove(x);
		return x;
	}

	Mod getMod(char id) {
		Mod x = mods.get(id);
		return x == null ? Mod.NO_OP : x;
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Utility methods
	//-----------------------------------------------------------------------------------------------------------------

	/**
	 * Takes the settings defined in this configuration and sets them as system properties.
	 *
	 * @return This object.
	 */
	public Config setSystemProperties() {
		for (String section : getSectionNames()) {
			for (String key : getKeys(section)) {
				String k = (section.isEmpty() ? key : section + '/' + key);
				System.setProperty(k, getRaw(k));
			}
		}
		return this;
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Workhorse setters
	//-----------------------------------------------------------------------------------------------------------------

	/**
	 * Sets a value in this config.
	 *
	 * @param key The key.
	 * @param value The value.
	 * @return This object.
	 * @throws UnsupportedOperationException If configuration is read only.
	 */
	public Config set(String key, String value) {
		checkWrite();
		assertArgNotNull("key", key);
		String sname = sname(key);
		String skey = skey(key);

		ConfigMapEntry ce = configMap.getEntry(sname, skey);
		if (ce == null && value == null)
			return this;

		String s = applyMods(ce == null ? null : ce.getModifiers(), stringify(value));

		configMap.setEntry(sname, skey, s, null, null, null);
		return this;
	}

	/**
	 * Adds or replaces an entry with the specified key with a POJO serialized to a string using the registered
	 * serializer.
	 *
	 * <p>
	 * Equivalent to calling <c>put(key, value, isEncoded(key))</c>.
	 *
	 * @param key The key.
	 * @param value The new value POJO.
	 * @return The previous value, or <jk>null</jk> if the section or key did not previously exist.
	 * @throws SerializeException
	 * 	If serializer could not serialize the value or if a serializer is not registered with this config file.
	 * @throws UnsupportedOperationException If configuration is read only.
	 */
	public Config set(String key, Object value) throws SerializeException {
		return set(key, value, null);
	}

	/**
	 * Same as {@link #set(String, Object)} but allows you to specify the serializer to use to serialize the
	 * value.
	 *
	 * @param key The key.
	 * @param value The new value.
	 * @param serializer
	 * 	The serializer to use for serializing the object.
	 * 	If <jk>null</jk>, then uses the predefined serializer on the config file.
	 * @return The previous value, or <jk>null</jk> if the section or key did not previously exist.
	 * @throws SerializeException
	 * 	If serializer could not serialize the value or if a serializer is not registered with this config file.
	 * @throws UnsupportedOperationException If configuration is read only.
	 */
	public Config set(String key, Object value, Serializer serializer) throws SerializeException {
		return set(key, serialize(value, serializer));
	}

	/**
	 * Same as {@link #set(String, Object)} but allows you to specify all aspects of a value.
	 *
	 * @param key The key.
	 * @param value The new value.
	 * @param serializer
	 * 	The serializer to use for serializing the object.
	 * 	If <jk>null</jk>, then uses the predefined serializer on the config file.
	 * @param modifiers
	 * 	Optional modifiers to apply to the value.
	 * 	<br>If <jk>null</jk>, then previous value will not be replaced.
	 * @param comment
	 * 	Optional same-line comment to add to this value.
	 * 	<br>If <jk>null</jk>, then previous value will not be replaced.
	 * @param preLines
	 * 	Optional comment or blank lines to add before this entry.
	 * 	<br>If <jk>null</jk>, then previous value will not be replaced.
	 * @return The previous value, or <jk>null</jk> if the section or key did not previously exist.
	 * @throws SerializeException
	 * 	If serializer could not serialize the value or if a serializer is not registered with this config file.
	 * @throws UnsupportedOperationException If configuration is read only.
	 */
	public Config set(String key, Object value, Serializer serializer, String modifiers, String comment, List<String> preLines) throws SerializeException {
		checkWrite();
		assertArgNotNull("key", key);
		String sname = sname(key);
		String skey = skey(key);
		modifiers = nullIfEmpty(modifiers);

		String s = applyMods(modifiers, serialize(value, serializer));

		configMap.setEntry(sname, skey, s, modifiers, comment, preLines);
		return this;
	}

	/**
	 * Removes an entry with the specified key.
	 *
	 * @param key The key.
	 * @return The previous value, or <jk>null</jk> if the section or key did not previously exist.
	 * @throws UnsupportedOperationException If configuration is read only.
	 */
	public Config remove(String key) {
		checkWrite();
		String sname = sname(key);
		String skey = skey(key);
		configMap.removeEntry(sname, skey);
		return this;
	}

	/**
	 * Encodes and unencoded entries in this config.
	 *
	 * <p>
	 * If any entries in the config are marked as encoded but not actually encoded,
	 * this will encode them.
	 *
	 * @return This object.
	 * @throws UnsupportedOperationException If configuration is read only.
	 */
	public Config applyMods() {
		checkWrite();
		for (String section : configMap.getSections()) {
			for (String key : configMap.getKeys(section)) {
				ConfigMapEntry ce = configMap.getEntry(section, key);
				if (ce.getModifiers() != null) {
					String mods = ce.getModifiers();
					String value = ce.getValue();
					for (int i = 0; i < mods.length(); i++) {
						Mod mod = getMod(mods.charAt(i));
						if (! mod.isApplied(value)) {
							configMap.setEntry(section, key, mod.apply(value), null, null, null);
						}
					}
				}
			}
		}

		return this;
	}

	//-----------------------------------------------------------------------------------------------------------------
	// API methods
	//-----------------------------------------------------------------------------------------------------------------

	/**
	 * Gets the entry with the specified key.
	 *
	 * <p>
	 * The key can be in one of the following formats...
	 * <ul class='spaced-list'>
	 * 	<li>
	 * 		<js>"key"</js> - A value in the default section (i.e. defined above any <c>[section]</c> header).
	 * 	<li>
	 * 		<js>"section/key"</js> - A value from the specified section.
	 * </ul>
	 *
	 * <p>
	 * If entry does not exist, returns an empty {@link Entry} object.
	 *
	 * @param key The key.
	 * @return The entry bean, never <jk>null</jk>.
	 */
	public Entry get(String key) {
		return new Entry(this, configMap, sname(key), skey(key));
	}

	/**
	 * Gets the entry with the specified key.
	 *
	 * <p>
	 * The key can be in one of the following formats...
	 * <ul class='spaced-list'>
	 * 	<li>
	 * 		<js>"key"</js> - A value in the default section (i.e. defined above any <c>[section]</c> header).
	 * 	<li>
	 * 		<js>"section/key"</js> - A value from the specified section.
	 * </ul>
	 *
	 * <p>
	 * If entry does not exist, returns <jk>null</jk>.
	 *
	 * <ul class='notes'>
	 * 	<li class='note'>This method is equivalent to calling <c>get(<jv>key</jv>).orElse(<jk>null</jk>);</c>.
	 * </ul>
	 *
	 * @param key The key.
	 * @return The entry value, or <jk>null</jk> if it doesn't exist.
	 */
	public String getString(String key) {
		return new Entry(this, configMap, sname(key), skey(key)).orElse(null);
	}

	/**
	 * Gets the section with the specified name.
	 *
	 * <p>
	 * If section does not exist, returns an empty {@link Section} object.
	 *
	 * @param name The section name.  <jk>null</jk> and blank refer to the default section.
	 * @return The section bean, never <jk>null</jk>.
	 */
	public Section getSection(String name) {
		return new Section(this, configMap, emptyIfNull(name));
	}

	/**
	 * Returns the keys of the entries in the specified section.
	 *
	 * @param section
	 * 	The section name to write from.
	 * 	<br>If empty, refers to the default section.
	 * 	<br>Must not be <jk>null</jk>.
	 * @return
	 * 	An unmodifiable set of keys, or an empty set if the section doesn't exist.
	 */
	public Set<String> getKeys(String section) {
		return configMap.getKeys(section(section));
	}

	/**
	 * Returns the section names defined in this config.
	 *
	 * @return The section names defined in this config.
	 */
	public Set<String> getSectionNames() {
		return unmodifiable(configMap.getSections());
	}

	/**
	 * Returns <jk>true</jk> if this section contains the specified key and the key has a non-blank value.
	 *
	 * @param key The key.
	 * @return <jk>true</jk> if this section contains the specified key and the key has a non-blank value.
	 */
	public boolean exists(String key) {
		return isNotEmpty(get(key).as(String.class).orElse(null));
	}

	/**
	 * Creates the specified section if it doesn't exist.
	 *
	 * <p>
	 * Returns the existing section if it already exists.
	 *
	 * @param name
	 * 	The section name.
	 * 	<br>Must not be <jk>null</jk>.
	 * 	<br>Use blank for the default section.
	 * @param preLines
	 * 	Optional comment and blank lines to add immediately before the section.
	 * 	<br>If <jk>null</jk>, previous pre-lines will not be replaced.
	 * @return The appended or existing section.
	 * @throws UnsupportedOperationException If configuration is read only.
	 */
	public Config setSection(String name, List<String> preLines) {
		try {
			return setSection(section(name), preLines, null);
		} catch (SerializeException e) {
			throw runtimeException(e);  // Impossible.
		}
	}

	/**
	 * Creates the specified section if it doesn't exist.
	 *
	 * @param name
	 * 	The section name.
	 * 	<br>Must not be <jk>null</jk>.
	 * 	<br>Use blank for the default section.
	 * @param preLines
	 * 	Optional comment and blank lines to add immediately before the section.
	 * 	<br>If <jk>null</jk>, previous pre-lines will not be replaced.
	 * @param contents
	 * 	Values to set in the new section.
	 * 	<br>Can be <jk>null</jk>.
	 * @return The appended or existing section.
	 * @throws SerializeException Contents could not be serialized.
	 * @throws UnsupportedOperationException If configuration is read only.
	 */
	public Config setSection(String name, List<String> preLines, Map<String,Object> contents) throws SerializeException {
		checkWrite();
		configMap.setSection(section(name), preLines);

		if (contents != null)
			for (Map.Entry<String,Object> e : contents.entrySet())
				set(section(name) + '/' + e.getKey(), e.getValue());

		return this;
	}

	/**
	 * Removes the section with the specified name.
	 *
	 * @param name The name of the section to remove
	 * @return This object.
	 * @throws UnsupportedOperationException If configuration is read only.
	 */
	public Config removeSection(String name) {
		checkWrite();
		configMap.removeSection(name);
		return this;
	}

	/**
	 * Creates the specified import statement if it doesn't exist.
	 *
	 * @param sectionName
	 * 	The section name where to place the import statement.
	 * 	<br>Must not be <jk>null</jk>.
	 * 	<br>Use blank for the default section.
	 * @param importName
	 * 	The import name.
	 * 	<br>Must not be <jk>null</jk>.
	 * @param preLines
	 * 	Optional comment and blank lines to add immediately before the import statement.
	 * 	<br>If <jk>null</jk>, previous pre-lines will not be replaced.
	 * @return The appended or existing import statement.
	 * @throws UnsupportedOperationException If configuration is read only.
	 */
	public Config setImport(String sectionName, String importName, List<String> preLines) {
		checkWrite();
		configMap.setImport(section(name), importName, preLines);
		return this;
	}

	/**
	 * Removes the import statement with the specified name from the specified section.
	 *
	 * @param sectionName
	 * 	The section name where to place the import statement.
	 * 	<br>Must not be <jk>null</jk>.
	 * 	<br>Use blank for the default section.
	 * @param importName
	 * 	The import name.
	 * 	<br>Must not be <jk>null</jk>.
	 * @return This object.
	 * @throws UnsupportedOperationException If configuration is read only.
	 */
	public Config removeImport(String sectionName, String importName) {
		checkWrite();
		configMap.removeImport(sectionName, importName);
		return this;
	}

	/**
	 * Loads the contents of the specified map of maps into this config.
	 *
	 * @param m The maps to load.
	 * @return This object.
	 * @throws SerializeException Value could not be serialized.
	 */
	public Config load(Map<String,Map<String,Object>> m) throws SerializeException {
		if (m != null)
			for (Map.Entry<String,Map<String,Object>> e : m.entrySet()) {
				setSection(e.getKey(), null, e.getValue());
			}
		return this;
	}

	/**
	 * Commit the changes in this config to the store.
	 *
	 * @return This object.
	 * @throws IOException Thrown by underlying stream.
	 * @throws UnsupportedOperationException If configuration is read only.
	 */
	public Config commit() throws IOException {
		checkWrite();
		configMap.commit();
		return this;
	}

	/**
	 * Saves this config file to the specified writer as an INI file.
	 *
	 * <p>
	 * The writer will automatically be closed.
	 *
	 * @param w The writer to send the output to.
	 * @return This object.
	 * @throws IOException If a problem occurred trying to send contents to the writer.
	 */
	public Writer writeTo(Writer w) throws IOException {
		return configMap.writeTo(w);
	}

	/**
	 * Add a listener to this config to react to modification events.
	 *
	 * <p>
	 * Listeners should be removed using {@link #removeListener(ConfigEventListener)}.
	 *
	 * @param listener The new listener to add.
	 * @return This object.
	 */
	public synchronized Config addListener(ConfigEventListener listener) {
		listeners.add(listener);
		return this;
	}

	/**
	 * Removes a listener from this config.
	 *
	 * @param listener The listener to remove.
	 * @return This object.
	 */
	public synchronized Config removeListener(ConfigEventListener listener) {
		listeners.remove(listener);
		return this;
	}

	/**
	 * Closes this configuration object by unregistering it from the underlying config map.
	 *
	 * @throws IOException Thrown by underlying stream.
	 */
	public void close() throws IOException {
		configMap.unregister(this);
	}

	/**
	 * Overwrites the contents of the config file.
	 *
	 * @param contents The new contents of the config file.
	 * @param synchronous Wait until the change has been persisted before returning this map.
	 * @return This object.
	 * @throws IOException Thrown by underlying stream.
	 * @throws InterruptedException Thread was interrupted.
	 * @throws UnsupportedOperationException If configuration is read only.
	 */
	public Config load(Reader contents, boolean synchronous) throws IOException, InterruptedException {
		checkWrite();
		configMap.load(read(contents), synchronous);
		return this;
	}

	/**
	 * Overwrites the contents of the config file.
	 *
	 * @param contents The new contents of the config file.
	 * @param synchronous Wait until the change has been persisted before returning this map.
	 * @return This object.
	 * @throws IOException Thrown by underlying stream.
	 * @throws InterruptedException Thread was interrupted.
	 * @throws UnsupportedOperationException If configuration is read only.
	 */
	public Config load(String contents, boolean synchronous) throws IOException, InterruptedException {
		checkWrite();
		configMap.load(contents, synchronous);
		return this;
	}

	/**
	 * Does a rollback of any changes on this config currently in memory.
	 *
	 * @return This object.
	 * @throws UnsupportedOperationException If configuration is read only.
	 */
	public Config rollback() {
		checkWrite();
		configMap.rollback();
		return this;
	}

	/**
	 * Returns the contents of this config as a simple map.
	 *
	 * @return The contents of this config as a simple map.
	 */
	public JsonMap toMap() {
		return configMap.asMap();
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Test methods
	//-----------------------------------------------------------------------------------------------------------------

	ConfigMap getConfigMap() {
		return configMap;
	}

	List<ConfigEventListener> getListeners() {
		return unmodifiable(listeners);
	}


	//-----------------------------------------------------------------------------------------------------------------
	// Interface methods
	//-----------------------------------------------------------------------------------------------------------------

	@Override /* ConfigEventListener */
	public synchronized void onConfigChange(ConfigEvents events) {
		for (ConfigEventListener l : listeners)
			l.onConfigChange(events);
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Private methods
	//-----------------------------------------------------------------------------------------------------------------

	private String serialize(Object value, Serializer serializer) throws SerializeException {
		if (value == null)
			return "";
		if (serializer == null)
			serializer = this.serializer;
		Class<?> c = value.getClass();
		if (value instanceof CharSequence)
			return nlIfMl((CharSequence)value);
		if (isSimpleType(c))
			return value.toString();

		if (value instanceof byte[]) {
			String s = null;
			byte[] b = (byte[])value;
			if (binaryFormat == BinaryFormat.HEX)
				s = toHex(b);
			else if (binaryFormat == BinaryFormat.SPACED_HEX)
				s = toSpacedHex(b);
			else
				s = base64Encode(b);
			int l = binaryLineLength;
			if (l <= 0 || s.length() <= l)
				return s;
			StringBuilder sb = new StringBuilder();
			for (int i = 0; i < s.length(); i += l)
				sb.append(binaryLineLength > 0 ? "\n" : "").append(s.substring(i, Math.min(s.length(), i + l)));
			return sb.toString();
		}

		String r = null;
		if (multiLineValuesOnSeparateLines)
			r = "\n" + (String)serializer.serialize(value);
		else
			r = (String)serializer.serialize(value);

		if (r.startsWith("'"))
			return r.substring(1, r.length()-1);
		return r;
	}

	private String nlIfMl(CharSequence cs) {
		String s = cs.toString();
		if (s.indexOf('\n') != -1 && multiLineValuesOnSeparateLines)
			return "\n" + s;
		return s;
	}

	private boolean isSimpleType(Type t) {
		if (! (t instanceof Class))
			return false;
		Class<?> c = (Class<?>)t;
		return (c == String.class || c.isPrimitive() || c.isAssignableFrom(Number.class) || c == Boolean.class || c.isEnum());
	}

	private String sname(String key) {
		assertArgNotNull("key", key);
		int i = key.indexOf('/');
		if (i == -1)
			return "";
		return key.substring(0, i);
	}

	private String skey(String key) {
		int i = key.indexOf('/');
		if (i == -1)
			return key;
		return key.substring(i+1);
	}

	private String section(String section) {
		assertArgNotNull("section", section);
		if (isEmpty(section))
			return "";
		return section;
	}

	void checkWrite() {
		if (readOnly)
			throw unsupportedOperationException("Cannot call this method on a read-only configuration.");
	}


	//-----------------------------------------------------------------------------------------------------------------
	// Other methods
	//-----------------------------------------------------------------------------------------------------------------

	@Override /* Object */
	public String toString() {
		return configMap.toString();
	}

	@Override /* Object */
	protected void finalize() throws Throwable {
		close();
	}
}
