public interface AqdefConstants {

	/**
	 * Separates lines of data file
	 */
	String LINE_SEPARATOR = "\r\n";

	/**
	 * Separates value from k-key on single line
	 */
	String VALUES_SEPARATOR = " ";

	/**
	 * Separates fields of single characteristic in lines with measured values (notation without the use of K-Keys)<br>
	 * The sequence of fields is:<br>
	 * 1 Value<br>
	 * 2 Attribute<br>
	 * 3 Date/Time<br>
	 * 4 Events<br>
	 * 5 Batch number<br>
	 * 6 Nest number<br>
	 * 7 Operator number<br>
	 * 8 Machine number<br>
	 * 9 Process parameter<br>
	 * 10 Gage number<br>
	 */
	String MEASURED_VALUES_DATA_SEPARATOR = String.valueOf((char) 20);

	/**
	 * Separates characteristic portions in lines with measured values (notation without the use of K-Keys)
	 */
	String MEASURED_VALUES_CHARACTERISTIC_SEPARATOR = String.valueOf((char) 15);

} public final class KKey implements Serializable, Comparable<KKey> {
	//*******************************************
	// Attributes
	//*******************************************

	private static final Logger LOG = LoggerFactory.getLogger(KKey.class);

	public enum Level {
		PART, CHARACTERISTIC, VALUE,
		CUSTOM_PART, CUSTOM_CHARACTERISTIC, CUSTOM_VALUE,
		GROUP, HIERARCHY, SIMPLE_HIERARCHY, CATALOG, UNKNOWN
	}

	private static final LoadingCache<String, KKey> CACHE = CacheBuilder.newBuilder().weakValues().build(new CacheLoader<>() {

		@Override
		public KKey load(String key) throws Exception {
			return new KKey(key);
		}
	});

	private String key;

	private transient Level level;
	private transient KKeyMetadata metadata;

	//*******************************************
	// Constructors
	//*******************************************

	private KKey(String key) {
		this.key = key;
	}

	/**
	 * Gets the instance of {@link KKey} for the given {@code key}.
	 * Returned instance may not be a new instance, but reused instance from cache.
	 *
	 * @param key
	 * @return
	 */
	public static KKey of(String key) {
		try {
			return CACHE.get(key);
		} catch (ExecutionException e) {
			throw new RuntimeException(e);
		}
	}

	//*******************************************
	// Methods
	//*******************************************

	public String getKey() {
		return key;
	}

	/**
	 * See {@link KKeyMetadata#getConverter()}
	 *
	 * @return
	 */
	public IKKeyValueConverter<?> getConverter() {
		if (metadata == null) {
			metadata = findMetadata();
		}

		if (metadata == null || metadata.getConverter() == null) {
			LOG.error("Can't find converter for k-key: " + key);
			return null;
		} else {
			return metadata.getConverter();
		}
	}

	/**
	 * See {@link KKeyMetadata#getColumnName()}
	 *
	 * @return
	 */
	public String getDbColumnName() {
		if (metadata == null) {
			metadata = findMetadata();
		}

		if (metadata == null || metadata.getColumnName() == null) {
			LOG.error("Can't find DB column name for k-key: " + key);
			return null;
		} else {
			return metadata.getColumnName();
		}
	}

	/**
	 * See {@link KKeyMetadata#getDataType()}
	 *
	 * @return
	 */
	public Class<?> getDataType() {
		if (metadata == null) {
			metadata = findMetadata();
		}

		if (metadata == null || metadata.getDataType() == null) {
			LOG.error("Can't find data type of k-key: " + key);
			return null;
		} else {
			return metadata.getDataType();
		}
	}

	/**
	 * See {@link KKeyMetadata#isRespectsCharacteristicDecimalSettings()}
	 *
	 * @return
	 */
	public boolean isRespectsCharacteristicDecimalSettings() {
		if (metadata == null) {
			metadata = findMetadata();
		}

		if (metadata == null) {
			LOG.error("Can't find metadata of k-key: " + key);
			return false;
		} else {
			return metadata.isRespectsCharacteristicDecimalSettings();
		}
	}

	/**
	 * Gets {@link KKeyMetadata metadata} of this K-key
	 *
	 * @return
	 */
	public KKeyMetadata getMetadata() {
		if (metadata == null) {
			metadata = findMetadata();

			if (metadata == null) {
				LOG.error("Can't find metadata for k-key: " + key);
				return null;
			}
		}

		return metadata;
	}

	private KKeyMetadata findMetadata() {
		if (getLevel() == Level.CATALOG) {
			return CatalogField.getMetadataFor(this);
		} else {
			return KKeyRepository.getInstance().getMetadataFor(this);
		}
	}

	/**
	 * Returns {@link Level} of the AQDEF structure this K-key belongs to.
	 *
	 * @return
	 */
	public Level getLevel() {
		if (level == null) {
			level = determineLevel();
		}

		return level;
	}

	private Level determineLevel() {
		if (key == null) {
			LOG.error("K-key with missing key.");
			return Level.UNKNOWN;
		}

		if (key.equalsIgnoreCase("K2030") || key.equalsIgnoreCase("K2031")) {

			return Level.SIMPLE_HIERARCHY;

		} else if (key.startsWith("K1")) {

			return Level.PART;

		} else if (key.startsWith("K2") // characteristic properties
				|| key.startsWith("K8")) { // control chart properties

			return Level.CHARACTERISTIC;

		} else if (key.startsWith("K0")) {

			return Level.VALUE;

		} else if (key.startsWith("K4")) {

			return Level.CATALOG;

		} else if (key.startsWith("K50")) {

			return Level.GROUP;

		} else if (key.startsWith("K51")) {

			return Level.HIERARCHY;

		} else if (key.startsWith("KX0")) {

			return Level.CUSTOM_VALUE;

		} else if (key.startsWith("KX2")) {

			return Level.CUSTOM_CHARACTERISTIC;

		} else if (key.startsWith("KX1")) {

			return Level.CUSTOM_PART;

		} else {

			LOG.error("Unknown level of k-key " + key);
			return Level.UNKNOWN;

		}
	}
	/**
	 * @return whether this key represents information for part (dil / teil)
	 */
	public boolean isPartLevel() {
		return getLevel() == Level.PART;
	}

	/**
	 * @return whether this key represents information for part (dil / teil) and is defined as custom
	 */
	public boolean isCustomPartLevel() {
		return getLevel() == Level.CUSTOM_PART;
	}

	/**
	 * @return whether this key represents information for characterstic (znak / merkmal)
	 */
	public boolean isCharacteristicLevel() {
		return getLevel() == Level.CHARACTERISTIC;
	}

	/**
	 * @return whether this key represents information for characterstic (znak / merkmal) and is defined as custom
	 */
	public boolean isCustomCharacteristicLevel() {
		return getLevel() == Level.CUSTOM_CHARACTERISTIC;
	}

	/**
	 * @return whether this key represents information for value (hodnota / wertevar)
	 */
	public boolean isValueLevel() {
		return getLevel() == Level.VALUE;
	}

	/**
	 * @return whether this key represents information for value (hodnota / wertevar) and is defined as custom
	 */
	public boolean isCustomValueLevel() {
		return getLevel() == Level.CUSTOM_VALUE;
	}

	/**
	 * @return whether this key represents information about logical group
	 */
	public boolean isGroupLevel() {
		return getLevel() == Level.GROUP;
	}

	/**
	 * @return whether this key represents information about hierarchy
	 */
	public boolean isHierarchyLevel() {
		return getLevel() == Level.HIERARCHY;
	}

	/**
	 * @return whether this key represents information about simple hierarchy
	 */
	public boolean isSimpleHierarchyLevel() {
		return getLevel() == Level.SIMPLE_HIERARCHY;
	}

	/**
	 * @return whether this key represents information about catalog record
	 */
	public boolean isCatalogLevel() {
		return getLevel() == Level.CATALOG;
	}

	/**
	 * @return whether this key represents our custom key
	 */
	public boolean isCustom() {
		return isCustomPartLevel() || isCustomCharacteristicLevel() || isCustomValueLevel();
	}

	/**
	 * @return whether this k-key should be written to DFQ file
	 */
	public boolean shouldBeWrittenToDfq() {
		if (isCustom()) {
			return false;
		}

		return true;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((key == null) ? 0 : key.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (!(obj instanceof KKey)) {
			return false;
		}
		KKey other = (KKey) obj;
		if (key == null) {
			if (other.key != null) {
				return false;
			}
		} else if (!key.equals(other.key)) {
			return false;
		}
		return true;
	}

	@Override
	public String toString() {
		return key;
	}

	@Override
	public int compareTo(KKey o) {
		if (key == null && o.key == null) {
			return 0;
		}
		if (key == null && o.key != null) {
			return 1;
		}
		if (key != null && o.key == null) {
			return -1;
		}

		String thisKey = keyForCompare(key);
		String otherKey = keyForCompare(o.key);
		return thisKey.compareTo(otherKey);
	}

	private String keyForCompare(String key) {
		// K0020 and K0021 has to be written of the same position as K0001 - this will change the order for these 2 keys
		if (key.equals("K0020")) {
			return "K0001.20";
		}

		if (key.equals("K0021")) {
			return "K0001.21";
		}

		return key;
	}

} public interface IKKeyValueConverter<T> {

	public T convert(String value) throws KKeyValueConversionException;

	public String toString(T value);

} public static class HierarchyEntry extends AbstractEntry<NodeIndex> {

	public HierarchyEntry(KKey key, NodeIndex index, Integer value) {
		super(validateKey(key), index, value);
	}

	private static KKey validateKey(KKey key) {
		if (!key.isHierarchyLevel()) {
			throw new IllegalArgumentException("K-Key of hierarchy type expected, but found: " + key);
		}
		return key;
	}
} @EqualsAndHashCode
public class AqdefObjectModel {

	//*******************************************
	// Attributes
	//*******************************************

	private final Map<PartIndex, PartEntries> partEntries = newEntriesMap();
	private final Map<PartIndex, Map<CharacteristicIndex, CharacteristicEntries>> characteristicEntries = newEntriesMap();
	private final Map<PartIndex, Map<GroupIndex, GroupEntries>> groupEntries = newEntriesMap();
	private final Map<PartIndex, Map<CharacteristicIndex, Map<ValueIndex, ValueEntries>>> valueEntries = newEntriesMap();

	private final Map<CatalogRecordIndex, CatalogRecordEntries> catalogEntries = newEntriesMap();

	@Getter
	@Setter
	private AqdefHierarchy hierarchy = new AqdefHierarchy();

	//*******************************************
	// Methods
	//*******************************************

	public void putPartEntry(KKey key, PartIndex index, Object value) {
		if (value == null) {
			return;
		}

		PartEntries entriesWithIndex = partEntries.computeIfAbsent(index, PartEntries::new);
		entriesWithIndex.put(key, value);
	}

	public void putPartEntries(PartEntries newPartEntries) {
		PartEntries entriesWithIndex = partEntries.computeIfAbsent(newPartEntries.getIndex(), PartEntries::new);
		entriesWithIndex.putAll(newPartEntries, true);
	}

	/**
	 * Removes part entries with the given index.
	 * Characteristic and value entries of given part are preserved!
	 *
	 * @param index
	 */
	private PartEntries removePartEntries(PartIndex index) {
		return partEntries.remove(index);
	}

	public void putCatalogRecordEntry(KKey key, CatalogRecordIndex index, Object value) {
		if (value == null) {
			return;
		}

		CatalogRecordEntries entriesWithIndex = catalogEntries.computeIfAbsent(index, CatalogRecordEntries::new);
		entriesWithIndex.put(key, value);
	}

	public void putCatalogRecordEntries(CatalogRecordEntries newCatalogRecordEntries) {
		CatalogRecordEntries entriesWithIndex =
				catalogEntries.computeIfAbsent(newCatalogRecordEntries.getIndex(), CatalogRecordEntries::new);
		entriesWithIndex.putAll(newCatalogRecordEntries, true);
	}

	/**
	 * Removes catalog record entries with the given index.
	 *
	 * @param index
	 */
	private CatalogRecordEntries removeCatalogRecordEntries(CatalogRecordIndex index) {
		return catalogEntries.remove(index);
	}

	public void putCharacteristicEntry(KKey key, CharacteristicIndex characteristicIndex, Object value) {
		if (value == null) {
			return;
		}

		CharacteristicEntries entriesWithIndex = computeCharacteristicEntriesIfAbsent(characteristicIndex);
		entriesWithIndex.put(key, value);
	}

	public void putCharacteristicEntries(CharacteristicEntries newCharacteristicEntries) {
		CharacteristicIndex characteristicIndex = newCharacteristicEntries.getIndex();
		CharacteristicEntries entriesWithIndex = computeCharacteristicEntriesIfAbsent(characteristicIndex);
		entriesWithIndex.putAll(newCharacteristicEntries, true);
	}

	private CharacteristicEntries computeCharacteristicEntriesIfAbsent(CharacteristicIndex characteristicIndex) {
		PartIndex partIndex = characteristicIndex.getPartIndex();

		Map<CharacteristicIndex, CharacteristicEntries> entriesWithPartIndex =
				characteristicEntries.computeIfAbsent(partIndex, i -> newEntriesMap());

		return entriesWithPartIndex.computeIfAbsent(characteristicIndex, CharacteristicEntries::new);
	}

	/**
	 * Removes characteristic entries with the given index.
	 * Value entries of the given characteristic are preserved!
	 *
	 * @param index
	 * @return
	 */
	private CharacteristicEntries removeCharacteristicEntries(CharacteristicIndex index) {
		Map<CharacteristicIndex, CharacteristicEntries> entriesWithPartIndex = characteristicEntries.get(index.getPartIndex());

		if (entriesWithPartIndex != null) {
			CharacteristicEntries removedEntries = entriesWithPartIndex.remove(index);

			// cleanup empty entries
			if (entriesWithPartIndex.isEmpty()) {
				characteristicEntries.remove(index.getPartIndex());
			}

			return removedEntries;
		}

		return null;
	}

	public void putGroupEntry(KKey key, GroupIndex groupIndex, Object value) {
		if (value == null) {
			return;
		}

		PartIndex partIndex = groupIndex.getPartIndex();

		Map<GroupIndex, GroupEntries> entriesWithPartIndex =
				groupEntries.computeIfAbsent(partIndex, i -> newEntriesMap());

		GroupEntries entriesWithIndex =
				entriesWithPartIndex.computeIfAbsent(groupIndex, GroupEntries::new);
		entriesWithIndex.put(key, value);
	}

	public void putValueEntry(KKey key, ValueIndex valueIndex, Object value) {
		if (value == null) {
			return;
		}

		ValueEntries entriesWithIndex = computeValueEntriesIfAbsent(valueIndex);
		entriesWithIndex.put(key, value);
	}

	public void putValueEntries(ValueEntries newValueEntries) {
		ValueIndex valueIndex = newValueEntries.getIndex();
		ValueEntries entriesWithIndex = computeValueEntriesIfAbsent(valueIndex);
		entriesWithIndex.putAll(newValueEntries, true);
	}

	private ValueEntries computeValueEntriesIfAbsent(ValueIndex valueIndex) {
		PartIndex partIndex = valueIndex.getPartIndex();

		Map<CharacteristicIndex, Map<ValueIndex, ValueEntries>> entriesWithPartIndex =
				valueEntries.computeIfAbsent(partIndex, i -> newEntriesMap());

		CharacteristicIndex characteristicIndex = valueIndex.getCharacteristicIndex();

		Map<ValueIndex, ValueEntries> entriesWithCharacteristicIndex =
				entriesWithPartIndex.computeIfAbsent(characteristicIndex, i -> newEntriesMap());

		return entriesWithCharacteristicIndex.computeIfAbsent(valueIndex, ValueEntries::new);
	}

	/**
	 * Removes all values of characteristic with the given index.
	 *
	 * @param index
	 * @return
	 */
	private List<ValueEntries> removeValueEntries(CharacteristicIndex index) {
		Map<CharacteristicIndex, Map<ValueIndex, ValueEntries>> entriesWithPartIndex = valueEntries.get(index.getPartIndex());

		if (entriesWithPartIndex != null) {
			Map<ValueIndex, ValueEntries> removedValueEntries = entriesWithPartIndex.remove(index);

			// cleanup empty entries
			if (entriesWithPartIndex.isEmpty()) {
				valueEntries.remove(index.getPartIndex());
			}

			if (removedValueEntries != null) {
				return new ArrayList<>(removedValueEntries.values());
			}
		}

		return new ArrayList<>();
	}

	public void putHierarchyEntry(KKey kKey, Integer nodeIndex, Object value) {
		hierarchy.putEntry(kKey, nodeIndex, value);
	}

	/**
	 * Returns indexes of all parts in this object model.
	 *
	 * @return
	 */
	public List<PartIndex> getPartIndexes() {
		return new ArrayList<>(partEntries.keySet());
	}

	public PartEntries getPartEntries(int index) {
		return getPartEntries(PartIndex.of(index));
	}

	public PartEntries getPartEntries(PartIndex index) {
		return partEntries.get(index);
	}

	/**
	 * @return all parts in this object model
	 */
	public List<PartEntries> getParts() {
		return new ArrayList<>(partEntries.values());
	}

	/**
	 * Returns indexes of all catalog records in this object model.
	 *
	 * @return
	 */
	public List<CatalogRecordIndex> getCatalogRecordIndexes() {
		return new ArrayList<>(catalogEntries.keySet());
	}

	public CatalogRecordEntries getCatalogRecordEntries(int index) {
		return getCatalogRecordEntries(CatalogRecordIndex.of(index));
	}

	public CatalogRecordEntries getCatalogRecordEntries(CatalogRecordIndex index) {
		return catalogEntries.get(index);
	}

	/**
	 * @return all catalog records in this object model
	 */
	public List<CatalogRecordEntries> getCatalogRecords() {
		return new ArrayList<>(catalogEntries.values());
	}

	/**
	 * Returns indexes of all characteristics of a part with the given index.
	 *
	 * @return
	 */
	public List<CharacteristicIndex> getCharacteristicIndexes(PartIndex partIndex) {
		Map<CharacteristicIndex, CharacteristicEntries> entriesWithPartIndex = characteristicEntries.get(partIndex);

		if (entriesWithPartIndex == null) {
			return new ArrayList<>();
		} else {
			return new ArrayList<>(entriesWithPartIndex.keySet());
		}
	}

	public CharacteristicEntries getCharacteristicEntries(int partIndex, int characteristicIndex) {
		return getCharacteristicEntries(CharacteristicIndex.of(PartIndex.of(partIndex), characteristicIndex));
	}

	public CharacteristicEntries getCharacteristicEntries(CharacteristicIndex characteristicIndex) {
		Map<CharacteristicIndex, CharacteristicEntries> entriesWithPartIndex = characteristicEntries.get(characteristicIndex.getPartIndex());

		if (entriesWithPartIndex == null) {
			return null;
		} else {
			return entriesWithPartIndex.get(characteristicIndex);
		}
	}

	/**
	 * Returns all the characteristics of a part with the given index.
	 *
	 * @param partIndex
	 * @return
	 */
	public List<CharacteristicEntries> getCharacteristics(PartIndex partIndex) {
		Map<CharacteristicIndex, CharacteristicEntries> entriesWithPartIndex = characteristicEntries.get(partIndex);

		if (entriesWithPartIndex == null) {
			return new ArrayList<>();
		} else {
			return new ArrayList<>(entriesWithPartIndex.values());
		}
	}

	/**
	 * Returns all the characteristics of a part with the given index that are root of characteristic
	 * hierarchy (has no parent characteristics).
	 *
	 * @param partIndex
	 * @return
	 */
	public List<CharacteristicEntries> getRootCharacteristics(PartIndex partIndex) {
		return getCharacteristics(partIndex).stream()
											.filter(characteristic -> {
												Optional<Object> parentIndex =
														hierarchy.getParentIndex(characteristic.getIndex());
												return parentIndex.isEmpty();
											})
											.collect(toList());
	}

	/**
	 * Returns all child characteristics of the characteristic with the given index.
	 *
	 * @param characteristicIndex
	 * @return
	 */
	public List<CharacteristicEntries> getChildCharacteristics(CharacteristicIndex characteristicIndex) {
		List<Object> childrenIndexes = hierarchy.getChildIndexes(characteristicIndex);

		return childrenIndexes.stream()
							  .filter(index -> index instanceof CharacteristicIndex)
							  .map(index -> (CharacteristicIndex) index)
							  .map(this::getCharacteristicEntries)
							  .collect(toList());
	}

	/**
	 * Returns all child groups of the characteristic with the given index.
	 *
	 * @param characteristicIndex
	 * @return
	 */
	public List<GroupEntries> getChildGroups(CharacteristicIndex characteristicIndex) {
		List<Object> childrenIndexes = hierarchy.getChildIndexes(characteristicIndex);

		return childrenIndexes.stream()
							  .filter(index -> index instanceof GroupIndex)
							  .map(index -> (GroupIndex) index)
							  .map(this::getGroupEntries)
							  .collect(toList());
	}

	public GroupEntries getGroupEntries(GroupIndex groupIndex) {
		Map<GroupIndex, GroupEntries> entriesWithPartIndex = groupEntries.get(groupIndex.getPartIndex());

		if (entriesWithPartIndex == null) {
			return null;
		} else {
			return entriesWithPartIndex.get(groupIndex);
		}
	}

	/**
	 * Returns all the groups of a part with the given index.
	 *
	 * @param partIndex
	 * @return
	 */
	public List<GroupEntries> getGroups(PartIndex partIndex) {
		Map<GroupIndex, GroupEntries> entriesWithPartIndex = groupEntries.get(partIndex);

		if (entriesWithPartIndex == null) {
			return new ArrayList<>();
		} else {
			return new ArrayList<>(entriesWithPartIndex.values());
		}
	}

	/**
	 * Returns all the groups of a part with the given index that are root of group
	 * hierarchy (has no parent group).
	 *
	 * @param partIndex
	 * @return
	 */
	public List<GroupEntries> getRootGroups(PartIndex partIndex) {
		return getGroups(partIndex).stream()
								   .filter(group -> {
									   Optional<Object> parentIndex = hierarchy.getParentIndex(group.getIndex());
									   return parentIndex.isEmpty();
								   })
								   .collect(toList());
	}

	/**
	 * Returns all child characteristics of the characteristic with the given index.
	 *
	 * @param groupIndex
	 * @return
	 */
	public List<CharacteristicEntries> getChildCharacteristics(GroupIndex groupIndex) {
		List<Object> childrenIndexes = hierarchy.getChildIndexes(groupIndex);

		return childrenIndexes.stream()
							  .filter(index -> index instanceof CharacteristicIndex)
							  .map(index -> (CharacteristicIndex) index)
							  .map(this::getCharacteristicEntries)
							  .collect(toList());
	}

	/**
	 * Returns all child groups of the characteristic with the given index.
	 *
	 * @param groupIndex
	 * @return
	 */
	public List<GroupEntries> getChildGroups(GroupIndex groupIndex) {
		List<Object> childrenIndexes = hierarchy.getChildIndexes(groupIndex);

		return childrenIndexes.stream()
							  .filter(index -> index instanceof GroupIndex)
							  .map(index -> (GroupIndex) index)
							  .map(this::getGroupEntries)
							  .collect(toList());
	}

	/**
	 * Returns indexes of all values of a characteristics with the given index.
	 *
	 * @return
	 */
	public List<ValueIndex> getValueIndexes(CharacteristicIndex characteristicIndex) {
		Map<CharacteristicIndex, Map<ValueIndex, ValueEntries>> entriesWithPartIndex = valueEntries.get(characteristicIndex.getPartIndex());

		if (entriesWithPartIndex == null) {
			return new ArrayList<>();
		} else {
			Map<ValueIndex, ValueEntries> entriesWithCharacteristicIndex = entriesWithPartIndex.get(characteristicIndex);

			if (entriesWithCharacteristicIndex == null) {
				return new ArrayList<>();
			} else {
				return new ArrayList<>(entriesWithCharacteristicIndex.keySet());
			}
		}
	}

	/**
	 * Returns indexes of all values in this object model.
	 * @return
	 */
	public List<ValueIndex> getValueIndexes() {
		return valueEntries.values()
						   .stream()
						   .flatMap(e -> e.values().stream())
						   .flatMap(e -> e.keySet().stream())
						   .collect(toList());
	}

	public ValueEntries getValueEntries(int partIndex, int characteristicIndex, int valueIndex) {
		return getValueEntries(ValueIndex.of(CharacteristicIndex.of(PartIndex.of(partIndex), characteristicIndex), valueIndex));
	}

	public ValueEntries getValueEntries(ValueIndex valueIndex) {
		Map<CharacteristicIndex, Map<ValueIndex, ValueEntries>> entriesWithPartIndex = valueEntries.get(valueIndex.getPartIndex());

		if (entriesWithPartIndex == null) {
			return null;
		} else {
			Map<ValueIndex, ValueEntries> entriesWithCharacteristicIndex = entriesWithPartIndex.get(valueIndex.getCharacteristicIndex());

			if (entriesWithCharacteristicIndex == null) {
				return null;
			} else {
				return entriesWithCharacteristicIndex.get(valueIndex);
			}
		}
	}

	/**
	 * @return all the values in this model object
	 */
	public List<ValueEntries> getValues() {
		return valueEntries.values()
						   .stream()
						   .flatMap(e -> e.values().stream())
						   .flatMap(e -> e.values().stream())
						   .collect(toList());
	}

	/**
	 *
	 * @param characteristicIndex
	 * @return
	 * @deprecated use {@link #getValues(CharacteristicIndex)} instead
	 */
	@Deprecated
	public List<ValueEntries> getValueEntries(CharacteristicIndex characteristicIndex) {
		return getValues(characteristicIndex);
	}

	public List<ValueEntries> getValues(CharacteristicIndex characteristicIndex) {
		Map<CharacteristicIndex, Map<ValueIndex, ValueEntries>> entriesWithPartIndex = valueEntries.get(characteristicIndex.getPartIndex());

		if (entriesWithPartIndex == null) {
			return new ArrayList<>();
		} else {
			Map<ValueIndex, ValueEntries> entriesWithCharacteristicIndex = entriesWithPartIndex.get(characteristicIndex);

			if (entriesWithCharacteristicIndex == null) {
				return new ArrayList<>();
			} else {
				return new ArrayList<>(entriesWithCharacteristicIndex.values());
			}
		}
	}


	public List<ValueSet> getValueSets(PartIndex partIndex) {
		Map<CharacteristicIndex, Map<ValueIndex, ValueEntries>> entriesWithPartIndex = valueEntries.get(partIndex);

		List<ValueSet> valueSets = new ArrayList<>();

		for (Entry<CharacteristicIndex, Map<ValueIndex, ValueEntries>> entriesOfCharacteristic : entriesWithPartIndex.entrySet()) {
			CharacteristicIndex characteristicIndex = entriesOfCharacteristic.getKey();
			Map<ValueIndex, ValueEntries> valuesOfCharacteristic = entriesOfCharacteristic.getValue();

			int counter = 0;
			for (Entry<ValueIndex, ValueEntries> valueEntries : valuesOfCharacteristic.entrySet()) {
				while (counter >= valueSets.size()) {
					valueSets.add(new ValueSet());
				}
				ValueSet valueSet = valueSets.get(counter);
				valueSet.addValueOfCharacteristic(characteristicIndex, valueEntries.getValue());
				counter++;
			}
		}

		return valueSets;
	}

	/**
	 * Finds part index to which the given characteristic index belongs.
	 * <p>
	 * You should call this method only after this {@link AqdefObjectModel} is fully created.
	 * </p>
	 *
	 * @param characteristicIndex
	 * @return
	 */
	public PartIndex findPartIndexForCharacteristic(int characteristicIndex) {
		for (Map<CharacteristicIndex, CharacteristicEntries> characteristics : characteristicEntries.values()) {
			for (CharacteristicIndex characteristic : characteristics.keySet()) {
				if (characteristic.getCharacteristicIndex() == characteristicIndex) {
					return characteristic.getPartIndex();
				}
			}
		}

		return null;
	}

	public Set<CharacteristicIndex> findCharacteristicIndexesForPart(PartIndex partIndex, CharacteristicOfSinglePartPredicate predicate) {
		Set<CharacteristicIndex> characteristicIndexes = new HashSet<>();

		Map<CharacteristicIndex, CharacteristicEntries> partCharacteristics = characteristicEntries.get(partIndex);

		if (partCharacteristics != null) {
			for (CharacteristicEntries entries : partCharacteristics.values()) {
				if (predicate.test(entries)) {
					characteristicIndexes.add(entries.getIndex());
				}
			}
		}

		return characteristicIndexes;
	}

	/**
	 * Iterates through all parts
	 *
	 * @param consumer
	 */
	public void forEachPart(PartConsumer consumer) {
		partEntries.forEach((partIndex, part) -> {
			consumer.accept(part);
		});
	}

	/**
	 * Iterates through all catalog records
	 *
	 * @param consumer
	 */
	public void forEachCatalogRecord(CatalogRecordConsumer consumer) {
		catalogEntries.forEach((catalogRecordIndex, catalogRecord) -> {
			consumer.accept(catalogRecord);
		});
	}

	/**
	 * Iterates through all characteristics of all parts
	 *
	 * @param consumer
	 */
	public void forEachCharacteristic(CharacteristicConsumer consumer) {
		partEntries.forEach((partIndex, part) -> {
			Map<CharacteristicIndex, CharacteristicEntries> characteristicsOfPart = characteristicEntries.get(part.getIndex());

			if (characteristicsOfPart != null) {
				characteristicsOfPart.forEach((characteristicIndex, characteristic) -> {
					consumer.accept(part, characteristic);
				});
			}
		});
	}

	/**
	 * Iterates through all characteristics of the given part. Most of the time it will be used together with {@link #forEachPart(PartConsumer)}.
	 * <pre>
	 * model.forEachPart(part -> {
	 *     // do something with the part
	 *
	 *     model.forEachCharacteristic(part, characteristic -> {
	 *         // do something with the characteristic
	 *     });
	 * })
	 * </pre>
	 * @param part
	 * @param consumer
	 */
	public void forEachCharacteristic(PartEntries part, CharacteristicOfSinglePartConsumer consumer) {
		Map<CharacteristicIndex, CharacteristicEntries> characteristicsOfPart = characteristicEntries.get(part.getIndex());

		if (characteristicsOfPart != null) {
			characteristicsOfPart.forEach((characteristicIndex, characteristic) -> {
				consumer.accept(characteristic);
			});
		}
	}

	/**
	 * Iterates through all logical groups of all parts
	 *
	 * @param consumer
	 */
	public void forEachGroup(GroupConsumer consumer) {
		partEntries.forEach((partIndex, part) -> {
			Map<GroupIndex, GroupEntries> groupsOfPart = groupEntries.get(part.getIndex());

			if (groupsOfPart != null) {
				groupsOfPart.forEach((groupIndex, group) -> {
					consumer.accept(part, group);
				});
			}
		});
	}

	/**
	 * Iterates through all logical groups of the given part. Similar to {@link #forEachCharacteristic(PartEntries, CharacteristicOfSinglePartConsumer)}
	 *
	 * @param part
	 * @param consumer
	 */
	public void forEachGroup(PartEntries part, GroupOfSinglePartConsumer consumer) {
		Map<GroupIndex, GroupEntries> groupsOfPart = groupEntries.get(part.getIndex());

		if (groupsOfPart != null) {
			groupsOfPart.forEach((groupIndex, group) -> {
				consumer.accept(group);
			});
		}
	}

	/**
	 * Iterates through all values.
	 *
	 * @param consumer
	 */
	public void forEachValue(ValueConsumer consumer) {
		partEntries.forEach((partIndex, part) -> {
			Map<CharacteristicIndex, CharacteristicEntries> characteristicsOfPart = characteristicEntries.get(part.getIndex());

			if (characteristicsOfPart != null) {
				characteristicsOfPart.forEach((characteristicIndex, characteristic) -> {
					Map<CharacteristicIndex, Map<ValueIndex, ValueEntries>> valuesOfPart = valueEntries.get(part.getIndex());

					if (valuesOfPart != null) {
						Map<ValueIndex, ValueEntries> values = valuesOfPart.get(characteristic.getIndex());

						if (values != null) {
							values.forEach((valueIndex, value) -> {
								consumer.accept(part, characteristic, value);
							});
						}
					}
				});
			}
		});
	}

	/**
	 * Iterates through all values of the given part. Most of the time it will be used together with {@link #forEachPart(PartConsumer)}.
	 * <pre>
	 * model.forEachPart(part -> {
	 *     // do something with the part
	 *
	 *     model.forEachValue(part, (characteristic, value) -> {
	 *         // do something with the value
	 *     });
	 * })
	 * </pre>
	 *
	 * @param part
	 * @param consumer
	 */
	public void forEachValue(PartEntries part, ValueOfSinglePartConsumer consumer) {
		Map<CharacteristicIndex, CharacteristicEntries> characteristicsOfPart = characteristicEntries.get(part.getIndex());

		if (characteristicsOfPart != null) {
			characteristicsOfPart.forEach((characteristicIndex, characteristic) -> {
				Map<CharacteristicIndex, Map<ValueIndex, ValueEntries>> valuesOfPart = valueEntries.get(part.getIndex());

				if (valuesOfPart != null) {
					Map<ValueIndex, ValueEntries> values = valuesOfPart.get(characteristic.getIndex());

					if (values != null) {
						values.forEach((valueIndex, value) -> {
							consumer.accept(characteristic, value);
						});
					}
				}
			});
		}
	}

	/**
	 * Iterates through all values of the given characteristic. Most of the time it will be used together with {@link #forEachCharacteristic(CharacteristicConsumer)}.
	 * <pre>
	 * model.forEachCharacteristic(part, characteristic -> {
	 *     // do something with the characteristic
	 *
	 *     model.forEachValue(part, characteristic, value -> {
	 *         // do something with the value
	 *     });
	 * })
	 * </pre>
	 *
	 * @param part
	 * @param characteristic
	 * @param consumer
	 */
	public void forEachValue(PartEntries part, CharacteristicEntries characteristic, ValueOfSingleCharacteristicConsumer consumer) {
		Map<CharacteristicIndex, Map<ValueIndex, ValueEntries>> valuesOfPart = valueEntries.get(part.getIndex());

		if (valuesOfPart != null) {
			Map<ValueIndex, ValueEntries> values = valuesOfPart.get(characteristic.getIndex());

			if (values != null) {
				values.forEach((valueIndex, value) -> {
					consumer.accept(value);
				});
			}
		}
	}

	/**
	 * Removes all parts that do not match the given predicate.
	 * If a part is removed it's characteristics and values are also removed.
	 *
	 * @param predicate
	 */
	public void filterParts(PartPredicate predicate) {
		Iterator<Map.Entry<PartIndex, PartEntries>> iterator = partEntries.entrySet().iterator();
		while (iterator.hasNext()) {

			Entry<PartIndex, PartEntries> entry = iterator.next();
			PartIndex partIndex = entry.getKey();
			PartEntries part = entry.getValue();

			if (!predicate.test(part)) {
				iterator.remove();

				hierarchy.removeHierarchyForPart(partIndex);

				// remove characteristics and values for that part
				characteristicEntries.remove(partIndex);
				valueEntries.remove(partIndex);
			}
		}
	}

	/**
	 * Removes all catalog records that do not match the given predicate.
	 *
	 * @param predicate
	 */
	public void filterCatalogRecords(CatalogRecordPredicate predicate) {
		Iterator<Map.Entry<CatalogRecordIndex, CatalogRecordEntries>> iterator = catalogEntries.entrySet().iterator();
		while (iterator.hasNext()) {

			Entry<CatalogRecordIndex, CatalogRecordEntries> entry = iterator.next();
			CatalogRecordEntries catalogRecord = entry.getValue();

			if (!predicate.test(catalogRecord)) {
				iterator.remove();
			}
		}
	}

	/**
	 * Removes all characteristics that do not match the given predicate.
	 * If a characteristic is removed it's values are also removed.
	 *
	 * @param predicate
	 */
	public void filterCharacteristics(CharacteristicPredicate predicate) {
		partEntries.forEach((partIndex, part) -> {
			Map<CharacteristicIndex, CharacteristicEntries> characteristicsOfPart = characteristicEntries.get(part.getIndex());

			if (characteristicsOfPart != null) {
				Iterator<Entry<CharacteristicIndex, CharacteristicEntries>> iterator = characteristicsOfPart.entrySet().iterator();
				while (iterator.hasNext()) {

					Entry<CharacteristicIndex, CharacteristicEntries> entry = iterator.next();
					CharacteristicIndex characteristicIndex = entry.getKey();
					CharacteristicEntries characteristic = entry.getValue();

					if (!predicate.test(part, characteristic)) {
						iterator.remove();
						hierarchy.removeHierarchyForCharacteristic(characteristicIndex);

						// remove values for that characteristic
						removeValueEntries(characteristicIndex);
					}
				}
			}
		});
	}

	/**
	 * Removes all characteristics of the given part that do not match the given predicate.
	 * If a characteristic is removed it's values are also removed.
	 *
	 * @param part
	 * @param predicate
	 */
	public void filterCharacteristics(PartEntries part, CharacteristicOfSinglePartPredicate predicate) {
		Map<CharacteristicIndex, CharacteristicEntries> characteristicsOfPart = characteristicEntries.get(part.getIndex());

		if (characteristicsOfPart != null) {
			Iterator<Entry<CharacteristicIndex, CharacteristicEntries>> iterator = characteristicsOfPart.entrySet().iterator();
			while (iterator.hasNext()) {

				Entry<CharacteristicIndex, CharacteristicEntries> entry = iterator.next();
				CharacteristicIndex characteristicIndex = entry.getKey();
				CharacteristicEntries characteristic = entry.getValue();

				if (!predicate.test(characteristic)) {
					iterator.remove();
					hierarchy.removeHierarchyForCharacteristic(characteristicIndex);

					// remove values for that characteristic
					removeValueEntries(characteristicIndex);
				}
			}
		}
	}

	/**
	 * Removes all groups that do not match the given predicate.
	 *
	 * @param predicate
	 */
	public void filterGroups(GroupPredicate predicate) {
		partEntries.forEach((partIndex, part) -> {
			Map<GroupIndex, GroupEntries> groupsOfPart = groupEntries.get(part.getIndex());

			if (groupsOfPart != null) {
				Iterator<Entry<GroupIndex, GroupEntries>> iterator = groupsOfPart.entrySet().iterator();

				while (iterator.hasNext()) {
					Entry<GroupIndex, GroupEntries> entry = iterator.next();
					GroupIndex groupIndex = entry.getKey();
					GroupEntries group = entry.getValue();

					if (!predicate.test(part, group)) {
						iterator.remove();
						hierarchy.removeHierarchyForGroup(groupIndex);
					}
				}
			}
		});
	}

	/**
	 * Removes all groups of the given part that do not match the given predicate.
	 *
	 * @param part
	 * @param predicate
	 */
	public void filterGroups(PartEntries part, GroupOfSinglePartPredicate predicate) {
		Map<GroupIndex, GroupEntries> groupsOfPart = groupEntries.get(part.getIndex());

		if (groupsOfPart != null) {
			Iterator<Entry<GroupIndex, GroupEntries>> iterator = groupsOfPart.entrySet().iterator();

			while (iterator.hasNext()) {
				Entry<GroupIndex, GroupEntries> entry = iterator.next();
				GroupIndex groupIndex = entry.getKey();
				GroupEntries group = entry.getValue();

				if (!predicate.test(group)) {
					iterator.remove();
					hierarchy.removeHierarchyForGroup(groupIndex);
				}
			}
		}
	}

	/**
	 * Removes all values that do not match the given predicate.
	 *
	 * @param predicate
	 */
	public void filterValues(ValuePredicate predicate) {
		partEntries.forEach((partIndex, part) -> {
			Map<CharacteristicIndex, CharacteristicEntries> characteristicsOfPart = characteristicEntries.get(part.getIndex());

			if (characteristicsOfPart != null) {
				characteristicsOfPart.forEach((characteristicIndex, characteristic) -> {
					Map<CharacteristicIndex, Map<ValueIndex, ValueEntries>> valuesOfPart = valueEntries.get(part.getIndex());

					if (valuesOfPart != null) {
						Map<ValueIndex, ValueEntries> values = valuesOfPart.get(characteristic.getIndex());

						if (values != null) {
							Iterator<Entry<ValueIndex, ValueEntries>> iterator = values.entrySet().iterator();
							while (iterator.hasNext()) {

								Entry<ValueIndex, ValueEntries> entry = iterator.next();
								ValueEntries value = entry.getValue();

								if (!predicate.test(part, characteristic, value)) {
									iterator.remove();
								}
							}
						}
					}
				});
			}
		});
	}

	/**
	 * Removes all values of the given part that do not match the given predicate.
	 *
	 * @param part
	 * @param predicate
	 */
	public void filterValues(PartEntries part, ValueOfSinglePartPredicate predicate) {
		Map<CharacteristicIndex, CharacteristicEntries> characteristicsOfPart = characteristicEntries.get(part.getIndex());

		if (characteristicsOfPart != null) {
			characteristicsOfPart.forEach((characteristicIndex, characteristic) -> {
				Map<CharacteristicIndex, Map<ValueIndex, ValueEntries>> valuesOfPart = valueEntries.get(part.getIndex());

				if (valuesOfPart != null) {
					Map<ValueIndex, ValueEntries> values = valuesOfPart.get(characteristic.getIndex());

					if (values != null) {
						Iterator<Entry<ValueIndex, ValueEntries>> iterator = values.entrySet().iterator();
						while (iterator.hasNext()) {

							Entry<ValueIndex, ValueEntries> entry = iterator.next();
							ValueEntries value = entry.getValue();

							if (!predicate.test(characteristic, value)) {
								iterator.remove();
							}
						}
					}
				}
			});
		}
	}

	/**
	 * Removes all values of the given characteristic that do not match the given predicate.
	 *
	 * @param part
	 * @param characteristic
	 * @param predicate
	 */
	public void filterValues(PartEntries part, CharacteristicEntries characteristic, ValueOfSingleCharacteristicPredicate predicate) {
		Map<CharacteristicIndex, Map<ValueIndex, ValueEntries>> valuesOfPart = valueEntries.get(part.getIndex());

		if (valuesOfPart != null) {
			Map<ValueIndex, ValueEntries> values = valuesOfPart.get(characteristic.getIndex());

			if (values != null) {
				Iterator<Entry<ValueIndex, ValueEntries>> iterator = values.entrySet().iterator();
				while (iterator.hasNext()) {

					Entry<ValueIndex, ValueEntries> entry = iterator.next();
					ValueEntries value = entry.getValue();

					if (!predicate.test(value)) {
						iterator.remove();
					}
				}
			}
		}
	}

	public boolean containsPart(PartEntries part) {
		Objects.requireNonNull(part);
		return partEntries.containsKey(part.getIndex());
	}

	public boolean containsCharacteristic(CharacteristicEntries characteristic) {
		Objects.requireNonNull(characteristic);

		CharacteristicIndex characteristicIndex = characteristic.getIndex();
		Map<CharacteristicIndex, CharacteristicEntries> characteristicsOfPart =
				characteristicEntries.get(characteristicIndex.getPartIndex());

		if (characteristicsOfPart == null) {
			return false;
		} else {
			return characteristicsOfPart.containsKey(characteristicIndex);
		}
	}

	public boolean containsValue(ValueEntries value) {
		Objects.requireNonNull(value);

		ValueIndex valueIndex = value.getIndex();

		Map<CharacteristicIndex, Map<ValueIndex, ValueEntries>> valuesOfPart = valueEntries.get(valueIndex.getPartIndex());

		if (valuesOfPart == null) {
			return false;
		} else {
			Map<ValueIndex, ValueEntries> valuesOfCharacteristic =
					valuesOfPart.get(valueIndex.getCharacteristicIndex());

			if (valuesOfCharacteristic == null) {
				return false;
			} else {
				return valuesOfCharacteristic.containsKey(valueIndex);
			}
		}
	}

	/**
	 * Finds value of the given K-key from any part / characteristic / value.
	 * <p>
	 * There is no guarantee from which part / characteristic / value this K-key value will be taken (whether from the first or other).
	 * </p>
	 *
	 * @param key any K-key of part / characteristic / value
	 * @return
	 * @throws IllegalArgumentException if the K-key is not for part / characteristic / value lavel.
	 */
	public Object getAnyValueOf(KKey key) {
		if (key.isPartLevel()) {
			return partEntries.values().stream().findAny().map((entries) -> entries.getValue(key)).orElse(null);

		} else if (key.isCharacteristicLevel()) {

			return characteristicEntries.values().stream().findAny().map((entriesOfPart) -> {
				return entriesOfPart.values().stream().findAny().map((entries) -> entries.getValue(key)).orElse(null);
			}).orElse(null);

		} else if (key.isGroupLevel()) {

			return groupEntries.values().stream().findAny().map((entriesOfPart) -> {
				return entriesOfPart.values().stream().findAny().map((entries) -> entries.getValue(key)).orElse(null);
			}).orElse(null);

		} else if (key.isValueLevel()) {

			return valueEntries.values().stream().findAny().map((entriesOfPart) -> {
				return entriesOfPart.values().stream().findAny().map((entriesOfCharacteristic) -> {
					return entriesOfCharacteristic.values().stream().findAny().map((entries) -> entries.getValue(key)).orElse(null);
				}).orElse(null);
			}).orElse(null);

		} else {
			throw new IllegalArgumentException(String.format("Invalid k-key %s. Value can be obtained only for part / characteristic / value keys.", key));
		}
	}

	/**
	 * Normalize the AQDEF content.
	 * <ul>
	 * <li>Apply all /0 K-keys on all parts / characteristics / values and then
	 * remove them from object model.</li>
	 * <li>Complement the hierarchy so there are no nodes/characteristics wihout a
	 * parent part node. This may happen when hierarchy was created from simple
	 * characteristics grouping (K2030/K2031).</li>
	 * </ul>
	 */
	public void normalize() {
		// normalize part entries
		PartEntries entriesForAllParts = removePartEntries(PartIndex.of(0));

		if (MapUtils.isNotEmpty(entriesForAllParts)) {
			if (getPartIndexes().isEmpty()) {

				// if there are no part entries then /0 entries become /1 entries
				PartIndex partIndex = PartIndex.of(1);
				partEntries.put(partIndex, entriesForAllParts.withIndex(partIndex));

			} else {

				// put /0 entries to all existing parts
				forEachPart((part) -> {
					part.putAll(entriesForAllParts.withIndex(part.getIndex()), false);
				});

			}
		}

		// normalize characteristic and value entries
		CharacteristicIndex indexForAllCharacteristicsOfAllParts = CharacteristicIndex.of(PartIndex.of(0), 0);
		CharacteristicEntries entriesForAllCharacteristicsOfAllParts = new CharacteristicEntries(indexForAllCharacteristicsOfAllParts);

		CharacteristicEntries characteristicEntriesForAllCharacteristicsOfAllParts =
				removeCharacteristicEntries(indexForAllCharacteristicsOfAllParts);
		if (MapUtils.isNotEmpty(characteristicEntriesForAllCharacteristicsOfAllParts)) {
			entriesForAllCharacteristicsOfAllParts.putAll(characteristicEntriesForAllCharacteristicsOfAllParts, false);
		}

		List<ValueEntries> entriesForAllValuesOfAllParts = removeValueEntries(indexForAllCharacteristicsOfAllParts);
		Map<Integer, List<ValueEntries>> entriesForAllValuesByValueIndex =
				entriesForAllValuesOfAllParts
											.stream()
											.collect(groupingBy(e -> e.getIndex().getValueIndex()));

		forEachPart((part) -> {
			CharacteristicIndex indexForAllCharacteristics = CharacteristicIndex.of(part.getIndex(), 0);

			CharacteristicEntries entriesForAllCharacteristics = removeCharacteristicEntries(indexForAllCharacteristics);
			if (MapUtils.isNotEmpty(entriesForAllCharacteristics)) {
				entriesForAllCharacteristicsOfAllParts.putAll(entriesForAllCharacteristics, false);
			}
		});

		forEachCharacteristic((part, characteristic) -> {
			characteristic.putAll(entriesForAllCharacteristicsOfAllParts.withIndex(characteristic.getIndex()), false);

			forEachValue(part, characteristic, (value) -> {
				// value entries for all values are applied to single value set for all characteristic values
				List<ValueEntries> entriesForValueIndex = entriesForAllValuesByValueIndex.get(value.getIndex().getValueIndex());

				if (entriesForValueIndex != null) {
					for (ValueEntries valueEntries : entriesForValueIndex) {
						value.putAll(valueEntries.withIndex(value.getIndex()), false);
					}
				}
			});
		});

		hierarchy = hierarchy.normalize(this);
	}

	/**
	 * Returns total number of characteristics of all parts in this object model
	 *
	 * @return
	 */
	public int getCharacteristicCount() {
		AtomicInteger characteristicCount = new AtomicInteger();

		forEachCharacteristic((part, characteristic) -> characteristicCount.incrementAndGet());

		return characteristicCount.get();
	}

	/**
	 * Returns total number of values of all parts and characteristics in this object model
	 *
	 * @return
	 */
	public int getValueCount() {
		AtomicInteger count = new AtomicInteger();

		forEachValue((part, characteristic, value) -> count.incrementAndGet());

		return count.get();
	}

	private <K, V> Map<K, V> newEntriesMap() {
		return new ConcurrentSkipListMap<>();
	}

	//*******************************************
	// Inner classes
	//*******************************************

	@FunctionalInterface
	public interface PartConsumer {
		void accept(PartEntries part);
	}

	@FunctionalInterface
	public interface CatalogRecordConsumer {
		void accept(CatalogRecordEntries catalogRecord);
	}

	@FunctionalInterface
	public interface CharacteristicConsumer {
		void accept(PartEntries part, CharacteristicEntries characteristic);
	}

	@FunctionalInterface
	public interface CharacteristicOfSinglePartConsumer {
		void accept(CharacteristicEntries characteristic);
	}

	@FunctionalInterface
	public interface GroupConsumer {
		void accept(PartEntries part, GroupEntries characteristic);
	}

	@FunctionalInterface
	public interface GroupOfSinglePartConsumer {
		void accept(GroupEntries characteristic);
	}

	@FunctionalInterface
	public interface ValueConsumer {
		void accept(PartEntries part, CharacteristicEntries characteristic, ValueEntries value);
	}

	@FunctionalInterface
	public interface ValueOfSinglePartConsumer {
		void accept(CharacteristicEntries characteristic, ValueEntries value);
	}

	@FunctionalInterface
	public interface ValueOfSingleCharacteristicConsumer {
		void accept(ValueEntries value);
	}

	@FunctionalInterface
	public interface PartPredicate {
		boolean test(PartEntries part);
	}

	@FunctionalInterface
	public interface CatalogRecordPredicate {
		boolean test(CatalogRecordEntries catalogRecord);
	}

	@FunctionalInterface
	public interface CharacteristicPredicate {
		boolean test(PartEntries part, CharacteristicEntries characteristic);
	}

	@FunctionalInterface
	public interface CharacteristicOfSinglePartPredicate {
		boolean test(CharacteristicEntries characteristic);
	}

	@FunctionalInterface
	public interface GroupPredicate {
		boolean test(PartEntries part, GroupEntries group);
	}

	@FunctionalInterface
	public interface GroupOfSinglePartPredicate {
		boolean test(GroupEntries group);
	}

	@FunctionalInterface
	public interface ValuePredicate {
		boolean test(PartEntries part, CharacteristicEntries characteristic, ValueEntries value);
	}

	@FunctionalInterface
	public interface ValueOfSinglePartPredicate {
		boolean test(CharacteristicEntries characteristic, ValueEntries value);
	}

	@FunctionalInterface
	public interface ValueOfSingleCharacteristicPredicate {
		boolean test(ValueEntries value);
	}

	public static class PartEntries extends Entries<PartEntry, PartIndex> {

		public PartEntries(PartIndex index) {
			super(index);
		}

		@Override
		public PartEntries withIndex(PartIndex index) {
			PartEntries copy = new PartEntries(index);

			List<PartEntry> entriesCopy = values()
												.stream()
												.map(e -> new PartEntry(e.getKey(), index, e.getValue()))
												.collect(toList());
			copy.putAll(entriesCopy, true);
			return copy;
		}

		@Override
		protected PartEntry newEntry(KKey key, PartIndex index, Object value) {
			return new PartEntry(key, index, value);
		}

	}

	public static class CharacteristicEntries extends Entries<CharacteristicEntry, CharacteristicIndex> {

		public CharacteristicEntries(CharacteristicIndex index) {
			super(index);
		}

		@Override
		public CharacteristicEntries withIndex(CharacteristicIndex index) {
			CharacteristicEntries copy = new CharacteristicEntries(index);

			List<CharacteristicEntry> entriesCopy = values()
														.stream()
														.map(e -> new CharacteristicEntry(e.getKey(), index, e.getValue()))
														.collect(toList());
			copy.putAll(entriesCopy, true);
			return copy;
		}

		@Override
		protected CharacteristicEntry newEntry(KKey key, CharacteristicIndex index, Object value) {
			return new CharacteristicEntry(key, index, value);
		}

	}

	public static class GroupEntries extends Entries<GroupEntry, GroupIndex> {

		public GroupEntries(GroupIndex index) {
			super(index);
		}

		@Override
		public GroupEntries withIndex(GroupIndex index) {
			GroupEntries copy = new GroupEntries(index);

			List<GroupEntry> entriesCopy = values()
												.stream()
												.map(e -> new GroupEntry(e.getKey(), index, e.getValue()))
												.collect(toList());
			copy.putAll(entriesCopy, true);
			return copy;
		}

		@Override
		protected GroupEntry newEntry(KKey key, GroupIndex index, Object value) {
			return new GroupEntry(key, index, value);
		}

	}

	public static class ValueEntries extends Entries<ValueEntry, ValueIndex> {

		public ValueEntries(ValueIndex index) {
			super(index);
		}

		@Override
		public ValueEntries withIndex(ValueIndex index) {
			ValueEntries copy = new ValueEntries(index);

			List<ValueEntry> entriesCopy = values()
												.stream()
												.map(e -> new ValueEntry(e.getKey(), index, e.getValue()))
												.collect(toList());
			copy.putAll(entriesCopy, true);
			return copy;
		}

		@Override
		protected ValueEntry newEntry(KKey key, ValueIndex index, Object value) {
			return new ValueEntry(key, index, value);
		}

	}

	public static class CatalogRecordEntries extends Entries<CatalogRecordEntry, CatalogRecordIndex> {

		public CatalogRecordEntries(CatalogRecordIndex index) {
			super(index);
		}

		@Override
		public CatalogRecordEntries withIndex(CatalogRecordIndex index) {
			CatalogRecordEntries copy = new CatalogRecordEntries(index);

			List<CatalogRecordEntry> entriesCopy =
					values().stream()
							.map(e -> new CatalogRecordEntry(e.getKey(), index, e.getValue()))
							.collect(toList());
			copy.putAll(entriesCopy, true);
			return copy;
		}

		@Override
		protected CatalogRecordEntry newEntry(KKey key, CatalogRecordIndex index, Object value) {
			return new CatalogRecordEntry(key, index, value);
		}

	}

	@Data
	@EqualsAndHashCode(callSuper = true)
	public static abstract class Entries<E extends AbstractEntry<I>, I> extends HashMap<KKey, E> implements IHasKKeyValues {

		private final I index;

		public void put(String key, Object value) {
			put(KKey.of(key), value);
		}

		public void put(KKey key, Object value) {
			put(newEntry(key, index, value));
		}

		public void put(E entry) {
			KKey key = entry.getKey();
			put(key, entry);
		}

		@Override
		public E put(KKey key, E entry) {
			if (!Objects.equals(index, entry.getIndex())) {
				throw new IllegalArgumentException("Index of the entry (" + entry.getIndex() + ") does not match entries index (" + index + ")");
			}

			return super.put(key, entry);
		}

		public void putAll(Collection<? extends E> entries, boolean overwriteExisting) {
			for (E entry : entries) {
				if (overwriteExisting) {
					put(entry.getKey(), entry);
				} else {
					putIfAbsent(entry.getKey(), entry);
				}
			}
		}

		public void putAll(Map<? extends KKey, ? extends E> entries, boolean overwriteExisting) {
			putAll(entries.values(), overwriteExisting);
		}

		public E get(KKey key) {
			return super.get(key);
		}

		public <T> T getValue(String key) {
			return getValue(KKey.of(key));
		}

		public <T> T getValue(String key, T defaultValue) {
			return getValue(KKey.of(key), defaultValue);
		}

		public <T> T getValue(KKey key, T defaultValue) {
			T value = getValue(key);

			return value == null ? defaultValue : value;
		}

		@Override
		public <T> T getValue(KKey key) {
			E entry = get(key);

			if (entry == null) {
				return null;
			}

			return (T) entry.getValue();
		}

		public E remove(String key) {
			return remove(KKey.of(key));
		}

		public E remove(KKey key) {
			return super.remove(key);
		}

		public void forEachEntry(Consumer<E> action) {
			values().forEach(action);
		}

		protected abstract E newEntry(KKey key, I index, Object value);

		/**
		 * Creates a copy of this entries with a given index.
		 *
		 * @param index
		 * @return
		 */
		public abstract Entries<E, I> withIndex(I index);

	}

	@Data
	public static abstract class AbstractEntry<I> {

		private final KKey key;
		private final I index;
		private final Object value;

		/**
		 * Whether this entry has given key.
		 *
		 * @param otherkey
		 * @return
		 */
		public boolean hasKey(KKey otherkey) {
			return key.equals(otherkey);
		}

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

	}

	public static class PartEntry extends AbstractEntry<PartIndex> {

		public PartEntry(KKey key, PartIndex index, Object value) {
			super(validateKey(key), index, value);
		}

		private static KKey validateKey(KKey key) {
			if (!key.isPartLevel() && !key.isCustomPartLevel()) {
				throw new IllegalArgumentException("K-Key of part type expected, but found: " + key);
			}
			return key;
		}
	}

	public static class CharacteristicEntry extends AbstractEntry<CharacteristicIndex> {

		public CharacteristicEntry(KKey key, CharacteristicIndex index, Object value) {
			super(validateKey(key), index, value);
		}

		private static KKey validateKey(KKey key) {
			if (!key.isCharacteristicLevel() && !key.isCustomCharacteristicLevel()) {
				throw new IllegalArgumentException("K-Key of characteristic type expected, but found: " + key);
			}
			return key;
		}
	}

	public static class ValueEntry extends AbstractEntry<ValueIndex>{

		public ValueEntry(KKey key, ValueIndex index, Object value) {
			super(validateKey(key), index, value);
		}

		private static KKey validateKey(KKey key) {
			if (!key.isValueLevel() && !key.isCustomValueLevel()) {
				throw new IllegalArgumentException("K-Key of value type expected, but found: " + key);
			}
			return key;
		}
	}

	public static class GroupEntry extends AbstractEntry<GroupIndex> {

		public GroupEntry(KKey key, GroupIndex index, Object value) {
			super(validateKey(key), index, value);
		}

		private static KKey validateKey(KKey key) {
			if (!key.isGroupLevel()) {
				throw new IllegalArgumentException("K-Key of group type expected, but found: " + key);
			}
			return key;
		}
	}

	public static class CatalogRecordEntry extends AbstractEntry<CatalogRecordIndex> {

		public CatalogRecordEntry(KKey key, CatalogRecordIndex index, Object value) {
			super(validateKey(key), index, value);
		}

		private static KKey validateKey(KKey key) {
			if (!key.isCatalogLevel()) {
				throw new IllegalArgumentException("K-Key of catalog type expected, but found: " + key);
			}
			return key;
		}
	}

	/**
	 * Contains one "set" of values for all characteristics. <br>
	 *
	 * @author Vlastimil Dolejs
	 *
	 */
	@RequiredArgsConstructor
	public static class ValueSet {

		private final TreeMap<CharacteristicIndex, ValueEntries> valuesOfCharacteristics;

		public ValueSet() {
			this(new TreeMap<>());
		}

		public void addValueOfCharacteristic(CharacteristicIndex characteristicIndex, ValueEntries valueEntries) {
			valuesOfCharacteristics.put(characteristicIndex, valueEntries);
		}

		public List<CharacteristicIndex> getCharacteristicIndexes() {
			return new ArrayList<>(valuesOfCharacteristics.keySet());
		}

		public ValueEntries getValuesOfCharacteristic(CharacteristicIndex characteristicIndex) {
			return valuesOfCharacteristics.get(characteristicIndex);
		}

		public List<ValueEntries> getValues() {
			return new ArrayList<>(valuesOfCharacteristics.values());
		}
	}

} public static class CharacteristicEntries extends Entries<CharacteristicEntry, CharacteristicIndex> {

	public CharacteristicEntries(CharacteristicIndex index) {
		super(index);
	}

	@Override
	public CharacteristicEntries withIndex(CharacteristicIndex index) {
		CharacteristicEntries copy = new CharacteristicEntries(index);

		List<CharacteristicEntry> entriesCopy = values()
													.stream()
													.map(e -> new CharacteristicEntry(e.getKey(), index, e.getValue()))
													.collect(toList());
		copy.putAll(entriesCopy, true);
		return copy;
	}

	@Override
	protected CharacteristicEntry newEntry(KKey key, CharacteristicIndex index, Object value) {
		return new CharacteristicEntry(key, index, value);
	}

} public static class CharacteristicEntry extends AbstractEntry<CharacteristicIndex> {

	public CharacteristicEntry(KKey key, CharacteristicIndex index, Object value) {
		super(validateKey(key), index, value);
	}

	private static KKey validateKey(KKey key) {
		if (!key.isCharacteristicLevel() && !key.isCustomCharacteristicLevel()) {
			throw new IllegalArgumentException("K-Key of characteristic type expected, but found: " + key);
		}
		return key;
	}
} public static class GroupEntries extends Entries<GroupEntry, GroupIndex> {

	public GroupEntries(GroupIndex index) {
		super(index);
	}

	@Override
	public GroupEntries withIndex(GroupIndex index) {
		GroupEntries copy = new GroupEntries(index);

		List<GroupEntry> entriesCopy = values()
											.stream()
											.map(e -> new GroupEntry(e.getKey(), index, e.getValue()))
											.collect(toList());
		copy.putAll(entriesCopy, true);
		return copy;
	}

	@Override
	protected GroupEntry newEntry(KKey key, GroupIndex index, Object value) {
		return new GroupEntry(key, index, value);
	}

} public static class GroupEntry extends AbstractEntry<GroupIndex> {

	public GroupEntry(KKey key, GroupIndex index, Object value) {
		super(validateKey(key), index, value);
	}

	private static KKey validateKey(KKey key) {
		if (!key.isGroupLevel()) {
			throw new IllegalArgumentException("K-Key of group type expected, but found: " + key);
		}
		return key;
	}
} public static class PartEntries extends Entries<PartEntry, PartIndex> {

	public PartEntries(PartIndex index) {
		super(index);
	}

	@Override
	public PartEntries withIndex(PartIndex index) {
		PartEntries copy = new PartEntries(index);

		List<PartEntry> entriesCopy = values()
											.stream()
											.map(e -> new PartEntry(e.getKey(), index, e.getValue()))
											.collect(toList());
		copy.putAll(entriesCopy, true);
		return copy;
	}

	@Override
	protected PartEntry newEntry(KKey key, PartIndex index, Object value) {
		return new PartEntry(key, index, value);
	}

} public static class PartEntry extends AbstractEntry<PartIndex> {

	public PartEntry(KKey key, PartIndex index, Object value) {
		super(validateKey(key), index, value);
	}

	private static KKey validateKey(KKey key) {
		if (!key.isPartLevel() && !key.isCustomPartLevel()) {
			throw new IllegalArgumentException("K-Key of part type expected, but found: " + key);
		}
		return key;
	}
} public static class ValueEntries extends Entries<ValueEntry, ValueIndex> {

	public ValueEntries(ValueIndex index) {
		super(index);
	}

	@Override
	public ValueEntries withIndex(ValueIndex index) {
		ValueEntries copy = new ValueEntries(index);

		List<ValueEntry> entriesCopy = values()
											.stream()
											.map(e -> new ValueEntry(e.getKey(), index, e.getValue()))
											.collect(toList());
		copy.putAll(entriesCopy, true);
		return copy;
	}

	@Override
	protected ValueEntry newEntry(KKey key, ValueIndex index, Object value) {
		return new ValueEntry(key, index, value);
	}

} public static class ValueEntry extends AbstractEntry<ValueIndex>{

	public ValueEntry(KKey key, ValueIndex index, Object value) {
		super(validateKey(key), index, value);
	}

	private static KKey validateKey(KKey key) {
		if (!key.isValueLevel() && !key.isCustomValueLevel()) {
			throw new IllegalArgumentException("K-Key of value type expected, but found: " + key);
		}
		return key;
	}
}
import java.io.IOException;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.util.Comparator;
import java.util.Objects;
import org.apache.commons.io.output.StringBuilderWriter;
import org.apache.commons.lang3.StringUtils;
import cz.diribet.aqdef.AqdefConstants;
import cz.diribet.aqdef.KKey;
import cz.diribet.aqdef.convert.IKKeyValueConverter;
import cz.diribet.aqdef.model.AqdefHierarchy.HierarchyEntry;
import cz.diribet.aqdef.model.AqdefObjectModel;
import cz.diribet.aqdef.model.AqdefObjectModel.CharacteristicEntries;
import cz.diribet.aqdef.model.AqdefObjectModel.CharacteristicEntry;
import cz.diribet.aqdef.model.AqdefObjectModel.GroupEntries;
import cz.diribet.aqdef.model.AqdefObjectModel.GroupEntry;
import cz.diribet.aqdef.model.AqdefObjectModel.PartEntries;
import cz.diribet.aqdef.model.AqdefObjectModel.PartEntry;
import cz.diribet.aqdef.model.AqdefObjectModel.ValueEntries;
import cz.diribet.aqdef.model.AqdefObjectModel.ValueEntry;
package cz.diribet.aqdef.writer;




/**
 * Writes {@link AqdefObjectModel} to AQDFQ text structure.
 * <p>
 * You can call {@link #writeTo(Writer)} to write DFQ content to a given writer
 * or {@link #getData()} to get DFQ content as a String. DFQ content is created
 * lazily when one of these methods is called.
 * </p>
 *
 * @author Vlastimil Dolejs
 *
 */
public class AqdefWriter implements AqdefConstants {
	//*******************************************
	// Methods
	//*******************************************

	/**
	 * Creates AQDFQ structure and returns it as a String
	 *
	 * @param aqdefObjectModel
	 *            model to be written, must not be {@code null}
	 * @return AQDFQ content as a String, never {@code null}
	 */
	public String writeToString(AqdefObjectModel aqdefObjectModel) {
		Objects.requireNonNull(aqdefObjectModel);

		StringBuilderWriter fileContent = new StringBuilderWriter();

		try {
			writeTo(aqdefObjectModel, fileContent);

		} catch (IOException e) {
			throw new RuntimeException("Failed to write AQDFQ model content", e);
		}

		return fileContent.toString();
	}

	/**
	 * Creates AQDEF structure and writes it to a given <code>writer</code>
	 *
	 * @param aqdefObjectModel
	 *            model to be written, must not be {@code null}
	 * @param writer
	 *            writer to write model to, must not be {@code null}
	 * @throws IOException
	 *             thrown when some I/O error occur
	 */
	public void writeTo(AqdefObjectModel aqdefObjectModel, Writer writer) throws IOException {
		Objects.requireNonNull(aqdefObjectModel);
		Objects.requireNonNull(writer);

		writeEntries(aqdefObjectModel, writer);
	}

	private void writeEntries(AqdefObjectModel aqdefObjectModel, Writer writer) throws IOException {
		aqdefObjectModel.normalize();

		try {
			// AQDEF structure always starts with the total number of characteristics
			write("K0100", null, Integer.toString(aqdefObjectModel.getCharacteristicCount()), writer);

			aqdefObjectModel.forEachPart(part -> {
				write(part, writer);

				aqdefObjectModel.forEachCharacteristic(part, (characteristic) -> {
					write(characteristic, writer);

					aqdefObjectModel.forEachValue(part, characteristic, (value) -> {
						write(value, writer);
					});
				});

				aqdefObjectModel.forEachGroup(part, (group) -> {
					write(group, writer);
				});
			});

			aqdefObjectModel.getHierarchy().forEachNodeDefinition(nodeDefinition -> {
				write(nodeDefinition, writer);
			});

			aqdefObjectModel.getHierarchy().forEachNodeBinding(nodeBinding -> {
				write(nodeBinding, writer);
			});

		} catch (UncheckedIOException e) {
			throw e.getCause();
		}
	}

	private void write(PartEntries part, Writer writer) throws UncheckedIOException {
		part.values()
			.stream()
			.sorted(Comparator.comparing(PartEntry::getKey))
			.forEach(partEntry -> write(partEntry, writer));
	}

	private void write(PartEntry entry, Writer writer) throws UncheckedIOException {
		KKey kKey = entry.getKey();

		write(kKey.getKey(), entry.getIndex().getIndex(), convertValueOfKKey(kKey, entry.getValue()), writer);
	}

	private void write(CharacteristicEntries characteristic, Writer writer) throws UncheckedIOException {
		characteristic.values()
					  .stream()
					  .sorted(Comparator.comparing(CharacteristicEntry::getKey))
					  .forEach(characteristicEntry -> write(characteristicEntry, writer));
	}

	private void write(CharacteristicEntry entry, Writer writer) throws UncheckedIOException {
		KKey kKey = entry.getKey();

		write(kKey.getKey(), entry.getIndex().getCharacteristicIndex(), convertValueOfKKey(kKey, entry.getValue()), writer);
	}

	private void write(GroupEntries group, Writer writer) throws UncheckedIOException {