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

import static java.util.Collections.*;

import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.*;

import org.apache.juneau.PropertyStore.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.json.*;
import org.apache.juneau.marshall.*;
import org.apache.juneau.reflect.*;
import org.apache.juneau.svl.*;

/**
 * A builder for {@link PropertyStore} objects.
 */
public class PropertyStoreBuilder {

	// Contains a cache of all created PropertyStore objects keyed by hashcode.
	// Used to minimize memory consumption by reusing identical PropertyStores.
	private static final Map<Integer,PropertyStore> CACHE = new ConcurrentHashMap<>();

	// Maps property suffixes (e.g. "lc") to PropertyType (e.g. LIST_CLASS)
	static final Map<String,PropertyType> SUFFIX_MAP = new ConcurrentHashMap<>();
	static {
		for (PropertyType pt : PropertyType.values())
			SUFFIX_MAP.put(pt.getSuffix(), pt);
	}

	private final Map<String,PropertyGroupBuilder> groups = new ConcurrentSkipListMap<>();

	// Previously-created property store.
	private volatile PropertyStore propertyStore;

	// Called by PropertyStore.builder()
	PropertyStoreBuilder(PropertyStore ps) {
		apply(ps);
	}

	// Called by PropertyStore.create()
	PropertyStoreBuilder() {}

	/**
	 * Creates a new {@link PropertyStore} based on the values in this builder.
	 *
	 * @return A new {@link PropertyStore} based on the values in this builder.
	 */
	public synchronized PropertyStore build() {

		// Reused the last one if we haven't change this builder.
		if (propertyStore == null)
			propertyStore = new PropertyStore(groups);

		PropertyStore ps = CACHE.get(propertyStore.hashCode());
		if (ps == null)
			CACHE.put(propertyStore.hashCode(), propertyStore);
		else if (! ps.equals(propertyStore))
			throw new RuntimeException("Property store mismatch!  This shouldn't happen.  hashCode=["+propertyStore.hashCode()+"]\n---PS#1---\n" + ps + "\n---PS#2---\n" + propertyStore);
		else
			propertyStore = ps;

		return propertyStore;
	}

	/**
	 * Copies all the values in the specified property store into this builder.
	 *
	 * @param copyFrom The property store to copy the values from.
	 * @return This object (for method chaining).
	 */
	public synchronized PropertyStoreBuilder apply(PropertyStore copyFrom) {
		propertyStore = null;

		if (copyFrom != null)
			for (Map.Entry<String,PropertyGroup> e : copyFrom.groups.entrySet()) {
				String gName = e.getKey();
				PropertyGroupBuilder g1 = this.groups.get(gName);
				PropertyGroup g2 = e.getValue();
				if (g1 == null)
					this.groups.put(gName, g2.builder());
				else
					g1.apply(g2);
			}
		return this;
	}

	/**
	 * Applies the settings in the specified annotations to this property store.
	 *
	 * @param al The list of annotations to apply.
	 * @param r The string resolver used to resolve any variables in the annotations.
	 * @return This object (for method chaining).
	 */
	@SuppressWarnings("unchecked")
	public PropertyStoreBuilder applyAnnotations(AnnotationList al, VarResolverSession r) {
		for (AnnotationInfo<?> ai : al.sort()) {
			try {
				ai.getConfigApply(r).apply((AnnotationInfo<Annotation>)ai, this);
			} catch (ConfigException ex) {
				throw ex;
			} catch (Exception ex) {
				throw new ConfigException(ex, "Could not instantiate ConfigApply class {0}", ai);
			}
		}
		return this;
	}

	/**
	 * Sets a configuration property value on this object.
	 *
	 * @param key
	 * 	The configuration property key.
	 * 	<br>(e.g <js>"BeanContext.foo.ss/add.1"</js>)
	 * 	<br>If name ends with <l>/add</l>, then the specified value is added to the existing property value as an entry
	 * 	in a SET or LIST property.
	 * 	<br>If name ends with <l>/add.{key}</l>, then the specified value is added to the existing property value as a
	 * 	key/value pair in a MAP property.
	 * 	<br>If name ends with <l>/remove</l>, then the specified value is removed from the existing property property
	 * 	value in a SET or LIST property.
	 * @param value
	 * 	The new value.
	 * 	If <jk>null</jk>, the property value is deleted.
	 * 	In general, the value type can be anything.
	 * @return This object (for method chaining).
	 */
	public synchronized PropertyStoreBuilder set(String key, Object value) {
		propertyStore = null;

		String g = group(key);

		int i = key.indexOf('/');
		if (i != -1) {
			String command = key.substring(i+1), arg = null;
			String key2 = key.substring(0, i);
			int j = command.indexOf('.');
			if (j != -1) {
				arg = command.substring(j+1);
				command = command.substring(0, j);
			}

			if ("add".equals(command)) {
				return addTo(key2, arg, value);
			} else if ("remove".equals(command)) {
				if (arg != null)
					throw new ConfigException("Invalid key specified: ''{0}''", key);
				return removeFrom(key2, value);
			} else {
				throw new ConfigException("Invalid key specified: ''{0}''", key);
			}
		}

		String n = g.isEmpty() ? key : key.substring(g.length()+1);

		PropertyGroupBuilder gb = groups.get(g);
		if (gb == null) {
			gb = new PropertyGroupBuilder();
			groups.put(g, gb);
		}

		gb.set(n, value);

		if (gb.isEmpty())
			groups.remove(g);

		return this;
	}

	/**
	 * Removes the property with the specified key.
	 *
	 * <p>
	 * This is equivalent to calling <code>set(key, <jk>null</jk>);</code>
	 *
	 * @param key The property key.
	 * @return This object (for method chaining).
	 */
	public synchronized PropertyStoreBuilder remove(String key) {
		propertyStore = null;
		return set(key, null);
	}

	/**
	 * Convenience method for setting multiple properties in one call.
	 *
	 * <p>
	 * This replaces any previous configuration properties set on this store.
	 *
	 * @param newProperties The new properties to set.
	 * @return This object (for method chaining).
	 */
	public synchronized PropertyStoreBuilder set(Map<String,Object> newProperties) {
		propertyStore = null;
		clear();
		add(newProperties);
		return this;
	}

	/**
	 * Convenience method for setting multiple properties in one call.
	 *
	 * <p>
	 * This appends to any previous configuration properties set on this store.
	 *
	 * @param newProperties The new properties to set.
	 * @return This object (for method chaining).
	 */
	public synchronized PropertyStoreBuilder add(Map<String,Object> newProperties) {
		propertyStore = null;

		if (newProperties != null)
			for (Map.Entry<String,Object> e : newProperties.entrySet())
				set(e.getKey(), e.getValue());

		return this;
	}

	/**
	 * Adds one or more values to a SET, LIST, or MAP property.
	 *
	 * @param key The property key.
	 * @param arg
	 * 	The argument.
	 * 	<br>For SETs, this must always be <jk>null</jk>.
	 * 	<br>For LISTs, this can be <jk>null</jk> or a numeric index.
	 * 		Out-of-range indexes are simply 'adjusted' to the beginning or the end of the list.
	 * 		So, for example, a value of <js>"-100"</js> will always just cause the entry to be added to the beginning
	 * 		of the list.
	 * 		<br>NOTE:  If <jk>null</jk>, value will be inserted at position 0.
	 * 	<br>For MAPs, this can be <jk>null</jk> if we're adding a map, or a string key if we're adding an entry.
	 * @param value
	 * 	The new value to add to the property.
	 * 	<br>For SETs and LISTs, this can be a single value, Collection, array, or JSON array string.
	 * 	<br>For MAPs, this can be a single value, Map, or JSON object string.
	 * 	<br><jk>null</jk> values have the effect of removing entries.
	 * @return This object (for method chaining).
	 * @throws ConfigException If property is not a SET/LIST/MAP property, or the argument is invalid.
	 */
	public synchronized PropertyStoreBuilder addTo(String key, String arg, Object value) {
		propertyStore = null;
		String g = group(key);
		String n = g.isEmpty() ? key : key.substring(g.length()+1);

		PropertyGroupBuilder gb = groups.get(g);
		if (gb == null) {
			gb = new PropertyGroupBuilder();
			groups.put(g, gb);
		}

		gb.addTo(n, arg, value);

		if (gb.isEmpty())
			groups.remove(g);

		return this;
	}

	/**
	 * Adds/prepends a value to a SET, LIST, or MAP property.
	 *
	 * <p>
	 * Shortcut for calling <code>addTo(key, <jk>null</jk>, value);</code>.
	 *
	 * <p>
	 * NOTE:  When adding to a list, the value is inserted at the beginning of the list.
	 *
	 * @param key The property key.
	 * @param value
	 * 	The new value to add to the property.
	 * 	<br>For SETs and LISTs, this can be a single value, Collection, array, or JSON array string.
	 * 	<br>for MAPs, this can be a single value, Map, or JSON object string.
	 * 	<br><jk>null</jk> values have the effect of removing entries.
	 * @return This object (for method chaining).
	 * @throws ConfigException If property is not a SET/LIST/MAP property, or the argument is invalid.
	 */
	public synchronized PropertyStoreBuilder addTo(String key, Object value) {
		propertyStore = null;
		return addTo(key, null, value);
	}

	/**
	 * Removes a value from a SET or LIST property.
	 *
	 * @param key The property key.
	 * @param value The property value in the property.
	 * @return This object (for method chaining).
	 * @throws ConfigException If property is not a SET or LIST property.
	 */
	public synchronized PropertyStoreBuilder removeFrom(String key, Object value) {
		propertyStore = null;
		String g = group(key);
		String n = g.isEmpty() ? key : key.substring(g.length()+1);

		PropertyGroupBuilder gb = groups.get(g);

		// Create property group anyway to generate a good error message.
		if (gb == null)
			gb = new PropertyGroupBuilder();

		gb.removeFrom(n, value);

		if (gb.isEmpty())
			groups.remove(g);

		return this;
	}

	/**
	 * Peeks at a property value.
	 *
	 * <p>
	 * Used for debugging purposes.
	 *
	 * @param key The property key.
	 * @return The property value, or <jk>null</jk> if it doesn't exist.
	 */
	public Object peek(String key) {
		String g = group(key);
		String n = g.isEmpty() ? key : key.substring(g.length()+1);

		PropertyGroupBuilder gb = groups.get(g);

		// Create property group anyway to generate a good error message.
		if (gb == null)
			return null;

		MutableProperty bp = gb.properties.get(n);
		if (bp == null)
			return null;

		return bp.peek();
	}

	/**
	 * Same as {@link #peek(String)} but converts the value to the specified type.
	 *
	 * @param <T> The type to convert to.
	 * @param c The type to convert to.
	 * @param key The property key.
	 * @return The property value, or <jk>null</jk> if it doesn't exist.
	 */
	public <T> T peek(Class<T> c, String key)  {
		Object o = peek(key);
		if (o == null)
			return null;
		return BeanContext.DEFAULT.createBeanSession().convertToType(o, c);
	}

	/**
	 * Clears all entries in this property store.
	 */
	public synchronized void clear() {
		propertyStore = null;
		this.groups.clear();
	}

	//-------------------------------------------------------------------------------------------------------------------
	// PropertyGroupBuilder
	//-------------------------------------------------------------------------------------------------------------------

	static class PropertyGroupBuilder {
		final Map<String,MutableProperty> properties = new ConcurrentSkipListMap<>();

		PropertyGroupBuilder() {
			this(emptyMap());
		}

		synchronized void apply(PropertyGroup copyFrom) {
			for (Map.Entry<String,Property> e : copyFrom.properties.entrySet()) {
				String pName = e.getKey();
				MutableProperty p1 = properties.get(pName);
				Property p2 = e.getValue();
				if (p1 == null)
					properties.put(pName, p2.mutable());
				else
					p1.apply(p2.value);
			}
		}

		PropertyGroupBuilder(Map<String,Property> properties) {
			for (Map.Entry<String,Property> p : properties.entrySet())
				this.properties.put(p.getKey(), p.getValue().mutable());
		}

		synchronized void set(String key, Object value) {
			MutableProperty p = properties.get(key);
			if (p == null) {
				p = MutableProperty.create(key, value);
				if (! p.isEmpty())
					properties.put(key, p);
			} else {
				p.set(value);
				if (p.isEmpty())
					properties.remove(key);
				else
					properties.put(key, p);
			}
		}

		synchronized void addTo(String key, String arg, Object value) {
			MutableProperty p = properties.get(key);
			if (p == null) {
				p = MutableProperty.create(key, null);
				p.add(arg, value);
				if (! p.isEmpty())
					properties.put(key, p);
			} else {
				p.add(arg, value);
				if (p.isEmpty())
					properties.remove(key);
				else
					properties.put(key, p);
			}
		}

		synchronized void removeFrom(String key, Object value) {
			MutableProperty p = properties.get(key);
			if (p == null) {
				// Create property anyway to generate a good error message.
				p = MutableProperty.create(key, null);
				p.remove(value);
			} else {
				p.remove(value);
				if (p.isEmpty())
					properties.remove(key);
				else
					properties.put(key, p);
			}
		}

		synchronized boolean isEmpty() {
			return properties.isEmpty();
		}

		synchronized PropertyGroup build() {
			return new PropertyGroup(properties);
		}

		/**
		 * Used by the <c>toString()</c> method for debugging.
		 *
		 * @param bs The bean session.
		 * @return This object converted to a map.
		 */
		public Map<String,MutableProperty> swap(BeanSession bs) {
			return properties;
		}

		@Override
		public String toString() {
			return (SimpleJson.DEFAULT == null ? "" : SimpleJson.DEFAULT.toString(properties));
		}
	}

	//-------------------------------------------------------------------------------------------------------------------
	// MutableProperty
	//-------------------------------------------------------------------------------------------------------------------

	static abstract class MutableProperty {
		final String name;
		final PropertyType type;

		MutableProperty(String name, PropertyType type) {
			this.name = name;
			this.type = type;
		}

		static MutableProperty create(String name, Object value) {
			int i = name.lastIndexOf('.');
			String type = i == -1 ? "s" : name.substring(i+1);
			PropertyType pt = SUFFIX_MAP.get(type);

			if (pt == null)
				throw new ConfigException("Invalid type specified on property ''{0}''", name);

			switch (pt) {
				case STRING:
				case BOOLEAN:
				case INTEGER:
				case CLASS:
				case OBJECT:  return new MutableSimpleProperty(name, pt, value);
				case SET_STRING:
				case SET_INTEGER:
				case SET_CLASS: return new MutableSetProperty(name, pt, value);
				case LIST_STRING:
				case LIST_INTEGER:
				case LIST_CLASS:
				case LIST_OBJECT: return new MutableListProperty(name, pt, value);
				case SORTED_MAP_STRING:
				case SORTED_MAP_INTEGER:
				case SORTED_MAP_CLASS:
				case SORTED_MAP_OBJECT: return new MutableMapProperty(name, pt, value);
				case ORDERED_MAP_STRING:
				case ORDERED_MAP_INTEGER:
				case ORDERED_MAP_CLASS:
				case ORDERED_MAP_OBJECT: return new MutableLinkedMapProperty(name, pt, value);
				default: return new MutableSimpleProperty(name, PropertyType.STRING, value);
			}
		}

		abstract Property build();

		abstract boolean isEmpty();

		abstract void set(Object value);

		abstract void apply(Object value);

		abstract Object peek();

		void add(String arg, Object value) {
			throw new ConfigException("Cannot add value {0} ({1}) to property ''{2}'' ({3}).", string(value), className(value), name, type);
		}

		void remove(Object value) {
			throw new ConfigException("Cannot remove value {0} ({1}) from property ''{2}'' ({3}).", string(value), className(value), name, type);
		}

		Object convert(Object value) {
			return value == null ? null : type.converter.convert(value);
		}

		@Override /* Object */
		public String toString() {
			return StringUtils.stringify(peek());
		}
	}

	//-------------------------------------------------------------------------------------------------------------------
	// MutableSimpleProperty
	//-------------------------------------------------------------------------------------------------------------------

	static class MutableSimpleProperty extends MutableProperty {
		private Object value;

		MutableSimpleProperty(String name, PropertyType type, Object value) {
			super(name, type);
			set(value);
		}

		@Override /* MutableProperty */
		synchronized Property build() {
			return new Property(name, value, type);
		}

		@Override /* MutableProperty */
		synchronized void set(Object value) {
			this.value = convert(value);
		}

		@Override /* MutableProperty */
		synchronized void apply(Object value) {
			this.value = convert(value);
		}

		@Override /* MutableProperty */
		synchronized boolean isEmpty() {
			return this.value == null;
		}

		@Override /* MutableProperty */
		synchronized Object peek() {
			return value;
		}
	}

	//-------------------------------------------------------------------------------------------------------------------
	// MutableSetProperty
	//-------------------------------------------------------------------------------------------------------------------

	static class MutableSetProperty extends MutableProperty {
		private final Set<Object> set;

		MutableSetProperty(String name, PropertyType type, Object value) {
			super(name, type);
			set = new ConcurrentSkipListSet<>(type.comparator());
			set(value);
		}

		@Override /* MutableProperty */
		synchronized Property build() {
			return new Property(name, unmodifiableSet(new LinkedHashSet<>(set)), type);
		}

		@Override /* MutableProperty */
		synchronized void set(Object value) {
			try {
				Set<Object> newSet = merge(set, type.converter, value);
				set.clear();
				set.addAll(newSet);
			} catch (Exception e) {
				throw new ConfigException(e, "Cannot set value {0} ({1}) on property ''{2}'' ({3}).", string(value), className(value), name, type);
			}
		}

		@Override /* MutableProperty */
		synchronized void apply(Object values) {
			set.addAll((Set<?>)values);
		}

		@Override /* MutableProperty */
		synchronized void add(String arg, Object o) {
			if (arg != null)
				throw new ConfigException("Cannot use argument ''{0}'' on add command for property ''{1}'' ({2})", arg, name, type);
			try {
				set.addAll(normalize(type.converter, o));
			} catch (Exception e) {
				throw new ConfigException(e, "Cannot add value {0} ({1}) to property ''{2}'' ({3}).", string(o), className(o), name, type);
			}
		}

		@Override /* MutableProperty */
		synchronized void remove(Object o) {
			try {
				set.removeAll(normalize(type.converter, o));
			} catch (Exception e) {
				throw new ConfigException(e, "Cannot remove value {0} ({1}) from property ''{2}'' ({3}).", string(o), className(o), name, type);
			}
		}

		@Override /* MutableProperty */
		synchronized boolean isEmpty() {
			return set.isEmpty();
		}

		@Override /* MutableProperty */
		synchronized Object peek() {
			return set;
		}
	}

	//-------------------------------------------------------------------------------------------------------------------
	// MutableListProperty
	//-------------------------------------------------------------------------------------------------------------------

	static class MutableListProperty extends MutableProperty {
		private final List<Object> list = synchronizedList(new LinkedList<>());

		MutableListProperty(String name, PropertyType type, Object value) {
			super(name, type);
			set(value);
		}

		@Override /* MutableProperty */
		synchronized Property build() {
			return new Property(name, unmodifiableList(new ArrayList<>(list)), type);
		}

		@Override /* MutableProperty */
		synchronized void set(Object value) {
			try {
				List<Object> newList = merge(list, type.converter, value);
				list.clear();
				list.addAll(newList);
			} catch (Exception e) {
				throw new ConfigException(e, "Cannot set value {0} ({1}) on property ''{2}'' ({3}).", string(value), className(value), name, type);
			}
		}

		@Override /* MutableProperty */
		synchronized void apply(Object values) {
			list.addAll((List<?>)values);
		}

		@Override /* MutableProperty */
		synchronized void add(String arg, Object o) {
			if (arg != null && ! StringUtils.isNumeric(arg))
				throw new ConfigException("Invalid argument ''{0}'' on add command for property ''{1}'' ({2})", arg, name, type);

			int index = arg == null ? 0 : Integer.parseInt(arg);
			if (index < 0)
				index = 0;
			else if (index > list.size())
				index = list.size();

			try {
				List<Object> l = normalize(type.converter, o);
				list.removeAll(l);
				list.addAll(index, l);
			} catch (Exception e) {
				throw new ConfigException(e, "Cannot add value {0} ({1}) to property ''{2}'' ({3}).", string(o), className(o), name, type);
			}
		}

		@Override /* MutableProperty */
		synchronized void remove(Object o) {
			try {
				list.removeAll(normalize(type.converter, o));
			} catch (Exception e) {
				throw new ConfigException(e, "Cannot remove value {0} ({1}) from property ''{2}'' ({3}).", string(o), className(o), name, type);
			}
		}

		@Override /* MutableProperty */
		synchronized boolean isEmpty() {
			return list.isEmpty();
		}

		@Override /* MutableProperty */
		synchronized Object peek() {
			return list;
		}
	}

	//-------------------------------------------------------------------------------------------------------------------
	// MutableMapProperty
	//-------------------------------------------------------------------------------------------------------------------

	@SuppressWarnings("rawtypes")
	static class MutableMapProperty extends MutableProperty {
		protected Map<String,Object> map;

		MutableMapProperty(String name, PropertyType type, Object value) {
			super(name, type);
			this.map = createMap();
			set(value);
		}

		protected Map<String,Object> createMap() {
			return new ConcurrentHashMap<>();
		}

		@Override /* MutableProperty */
		synchronized Property build() {
			return new Property(name, unmodifiableMap(new TreeMap<>(map)), type);
		}

		@Override /* MutableProperty */
		synchronized void set(Object value) {
			this.map.clear();
			add(null, value);
		}

		@SuppressWarnings("unchecked")
		@Override /* MutableProperty */
		synchronized void apply(Object values) {
			for (Map.Entry<String,Object> e : ((Map<String,Object>)values).entrySet())
				add(e.getKey(), e.getValue());
		}

		@Override /* MutableProperty */
		synchronized void add(String arg, Object o) {
			if (arg != null) {
				o = convert(o);
				if (o == null)
					this.map.remove(arg);
				else
					this.map.put(arg, o);

			} else if (o != null) {
				if (o instanceof Map) {
					Map m = (Map)o;
					for (Map.Entry e : (Set<Map.Entry>)m.entrySet())
						if (e.getKey() != null)
							add(e.getKey().toString(), e.getValue());
				} else if (isObjectMap(o)) {
					try {
						add(null, new ObjectMap(o.toString()));
					} catch (Exception e) {
						throw new ConfigException(e, "Cannot add {0} ({1}) to property ''{2}'' ({3}).", string(o), className(o), name, type);
					}
				} else {
					throw new ConfigException("Cannot add {0} ({1}) to property ''{2}'' ({3}).", string(o), className(o), name, type);
				}
			}
		}

		@Override /* MutableProperty */
		synchronized boolean isEmpty() {
			return this.map.isEmpty();
		}

		@Override /* MutableProperty */
		synchronized Object peek() {
			return map;
		}
	}

	//-------------------------------------------------------------------------------------------------------------------
	// MutableLinkedMapProperty
	//-------------------------------------------------------------------------------------------------------------------

	static class MutableLinkedMapProperty extends MutableMapProperty {

		MutableLinkedMapProperty(String name, PropertyType type, Object value) {
			super(name, type, value);
			set(value);
		}

		@Override
		protected Map<String,Object> createMap() {
			return synchronizedMap(new LinkedHashMap<String,Object>());
		}

		@Override /* MutableProperty */
		synchronized Property build() {
			return new Property(name, unmodifiableMap(new LinkedHashMap<>(map)), type);
		}
	}


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

	static Set<Object> merge(Set<Object> oldSet, PropertyConverter<?> pc, Object o) throws Exception {
		return merge(oldSet, new LinkedHashSet<>(), normalize(pc, o));
	}

	private static Set<Object> merge(Set<Object> oldSet, Set<Object> newSet, List<Object> l) {
		for (Object o : l) {
			if (isNone(o))
				newSet.clear();
			else if (isInherit(o))
				newSet.addAll(oldSet);
			else
				newSet.add(o);
		}
		return newSet;
	}

	static List<Object> merge(List<Object> oldList, PropertyConverter<?> pc, Object o) throws Exception {
		return merge(oldList, new ArrayList<>(), normalize(pc, o));
	}

	private static List<Object> merge(List<Object> oldList, List<Object> newList, List<Object> l) {
		for (Object o : l) {
			if (isIndexed(o)) {
				Matcher lm = INDEXED_LINK_PATTERN.matcher(o.toString());
				lm.matches();
				String key = lm.group(1);
				int i2 = Math.min(newList.size(), Integer.parseInt(lm.group(2)));
				String remainder = lm.group(3);
				newList.add(i2, key.isEmpty() ? remainder : key + ":" + remainder);
			} else if (isNone(o)) {
				newList.clear();
			} else if (isInherit(o)) {
				if (oldList != null)
					for (Object o2 : oldList)
						newList.add(o2);
			} else {
				newList.remove(o);
				newList.add(o);
			}
		}

		return newList;
	}

	static List<Object> normalize(PropertyConverter<?> pc, Object o) throws Exception {
		return normalize(new ArrayList<>(), pc, o);
	}

	@SuppressWarnings("unchecked")
	static List<Object> normalize(List<Object> l, PropertyConverter<?> pc, Object o) throws Exception {
		if (o != null) {
			if (o.getClass().isArray()) {
				for (int i = 0; i < Array.getLength(o); i++)
					normalize(l, pc, Array.get(o, i));
			} else if (o instanceof Collection) {
				for (Object o2 : (Collection<Object>)o)
					normalize(l, pc, o2);
			} else if (isObjectList(o)) {
				normalize(l, pc, new ObjectList(o.toString()));
			} else {
				l.add(pc == null ? o : pc.convert(o));
			}
		}
		return l;
	}

	static String string(Object value) {
		return SimpleJsonSerializer.DEFAULT.toString(value);
	}

	static String className(Object value) {
		return value.getClass().getSimpleName();
	}

	static boolean isObjectMap(Object o) {
		if (o instanceof CharSequence) {
			String s = o.toString();
			return (s.startsWith("{") && s.endsWith("}") && BeanContext.DEFAULT != null);
		}
		return false;
	}

	private static String group(String key) {
		if (key == null || key.indexOf('.') == -1)
			return "";
		return key.substring(0, key.indexOf('.'));
	}

	static boolean isObjectList(Object o) {
		if (o instanceof CharSequence) {
			String s = o.toString();
			return (s.startsWith("[") && s.endsWith("]") && BeanContext.DEFAULT != null);
		}
		return false;
	}

	private static boolean isNone(Object o) {
		if (o instanceof CharSequence) {
			String s = o.toString();
			return "NONE".equals(s);
		}
		return false;
	}

	private static boolean isIndexed(Object o) {
		if (o instanceof CharSequence) {
			String s = o.toString();
			return s.indexOf('[') != -1 && INDEXED_LINK_PATTERN.matcher(s).matches();
		}
		return false;
	}

	private static final Pattern INDEXED_LINK_PATTERN = Pattern.compile("(?s)(\\S*)\\[(\\d+)\\]\\:(.*)");

	private static boolean isInherit(Object o) {
		if (o instanceof CharSequence) {
			String s = o.toString();
			return "INHERIT".equals(s);
		}
		return false;
	}

	@Override /* Object */
	public String toString() {
		return SimpleJson.DEFAULT_READABLE.toString(groups);
	}
}