content: /**
   * Copyright 2020 Alexander Herzog
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   * 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 ui.modeleditor.elements;
  
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Map;
  import java.util.TreeMap;
  import java.util.function.Consumer;
  
  import org.apache.commons.math3.distribution.AbstractRealDistribution;
  import org.apache.commons.math3.distribution.ExponentialDistribution;
  import org.w3c.dom.Document;
  import org.w3c.dom.Element;
  
  import language.Language;
  import mathtools.distribution.tools.DistributionTools;
  import simulator.editmodel.FullTextSearch;
  import ui.modeleditor.coreelements.ModelElementBox;
  import ui.modeleditor.descriptionbuilder.ModelDescriptionBuilder;
  
  /**
   * Diese Klasse kapselt die Daten zu Bedien-, Nachbearbeitungs- und Abbruchzeiten fr
   * ein <code>ModelElementProcess</code>-Objekt. Es wird ermglicht, die Zeiten ber eine
   * Verteilung oder ber einen Ausdruck zu definieren. Auerdem knnen optional Daten
   * pro Kundentyp hinterlegt werden.
   * @author Alexander Herzog
   * @see ModelElementProcess
   */
  public final class DistributionSystem implements Cloneable {
  	/** Language-Key fr den Bezeichner fr die Unterelemente (z.B. fr Kundentypen) */
  	private final String subLangKey;
  	/** Language-Key fr den Bezeichnung der Zeiten, der beim Speichern verwendet wird (z.B. Bedienzeiten/Nachbearbeitungszeiten/...) */
  	private final String typeLangKey;
  	/** Gibt an, ob auch der Basis-Typ (fr alle Kundentypen) <code>null</code> sein darf. */
  	private final boolean canBeNull;
  
  	/** Globale Verteilung */
  	private AbstractRealDistribution distribution;
  	/** Globale Rechenausdruck */
  	private String expression;
  
  	/** Untertypen-abhngige Verteilungen */
  	private final Map<String,AbstractRealDistribution> distributionByType;
  	/** Untertypen-abhngige Rechenausdrcke */
  	private final Map<String,String> expressionByType;
  
  	/**
  	 * Konstruktor der Klasse<br>
  	 * <b>Achtung:</b> Die Strings, die als <code>subLangKey</code> und als <code>typeLangKey</code>
  	 * bergeben werden, sollten vorab einmal per <code>Language.tr(...)</code> verwendet werden,
  	 * damit diese auch sicher in die Sprachdateien aufgenommen werden. Der Konstruktor von
  	 * <code>DistributionSystem</code> selbst ist <b>kein</b> Scan-Schlsselwort bei der
  	 * Zusammenstellung der Sprachdateien.
  	 * @param subLangKey	Language-Key fr den Bezeichner fr die Unterelemente (z.B. fr Kundentypen)
  	 * @param typeLangKey	Language-Key fr den Bezeichnung der Zeiten, der beim Speichern verwendet wird (z.B. Bedienzeiten/Nachbearbeitungszeiten/...)
  	 * @param canBeNull	Gibt an, ob auch der Basis-Typ (fr alle Kundentypen) <code>null</code> sein darf.
  	 */
  	public DistributionSystem(final String subLangKey, final String typeLangKey, final boolean canBeNull) {
  		this.subLangKey=subLangKey;
  		this.typeLangKey=typeLangKey;
  		this.canBeNull=canBeNull;
  
  		if (!canBeNull) distribution=new ExponentialDistribution(null,50,ExponentialDistribution.DEFAULT_INVERSE_ABSOLUTE_ACCURACY);
  
  		distributionByType=new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
  		expressionByType=new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
  	}
  
  	/**
  	 * Gibt an, ob der Basis-Typ (fr alle Kundentypen) <code>null</code> sein darf.
  	 * @return Gibt <code>true</code> zurck, wenn der Basis-Typ (fr alle Kundentypen) <code>null</code> sein darf.
  	 */
  	public boolean canBeNull() {
  		return canBeNull;
  	}
  
  	/**
  	 * Liefert die Daten fr den globalen Fall.
  	 * @return	Liefert einen <code>String</code>, eine <code>AbstractRealDistribution</code> oder <code>null</code> zurck.
  	 */
  	public Object get() {
  		if (expression!=null) return expression;
  		return distribution;
  	}
  
  	/**
  	 * Gibt an, ob berhaupt Daten in dem Element hinterlegt sind
  	 * @return	Gibt <code>true</code> zurck, wenn Daten in dem Element hinterlegt sind
  	 */
  	public boolean hasData() {
  		if (expression!=null) return true;
  		if (distribution!=null) return true;
  		for (Map.Entry<String,AbstractRealDistribution> entry: distributionByType.entrySet()) if (entry.getValue()!=null) return true;
  		for (Map.Entry<String,String> entry: expressionByType.entrySet()) if (entry.getValue()!=null) return true;
  		return false;
  	}
  
  	/**
  	 * Stellt die Daten fr den allgemeinen Fall.
  	 * @param data	Einzustellende Daten (kann ein <code>String</code>, eine <code>AbstractRealDistribution</code> oder - wenn dies zulssig ist - <code>null</code> sein).
  	 * @return	Gibt <code>true</code> zurck, wenn die Daten bernommen werden konnten.
  	 */
  	public boolean set(final Object data) {
  		if (data==null) {
  			if (!canBeNull) return false;
  			distribution=null;
  			expression=null;
  			distributionByType.clear();
  			expressionByType.clear();
  			return true;
  		}
  
  		if (data instanceof String) {
  			expression=(String)data;
  			return true;
  		}
  
  		if (data instanceof AbstractRealDistribution) {
  			distribution=DistributionTools.cloneDistribution((AbstractRealDistribution)data);
  			expression=null;
  			return true;
  		}
  
  		return false;
  	}
  
  	/**
  	 * Liefert die Daten fr einen bestimmten Subtyp.
  	 * @param subType	Kundentyp o.., fr den die Daten geliefert werden sollen. Wird <code>null</code> bergeben, so werden die Daten fr den globalen Fall geliefert.
  	 * @return	Liefert einen <code>String</code>, eine <code>AbstractRealDistribution</code> oder <code>null</code> zurck.
  	 */
  	public Object get(final String subType) {
  		if (subType==null || subType.isBlank()) return get();
  		final String s=expressionByType.get(subType);
  		if (s!=null) return s;
  		return distributionByType.get(subType);
  	}
  
  	/**
  	 * Sind Daten zu mindestens einem Subtyp vorhanden?
  	 * @return	Liefert <code>true</code>, wenn Daten zu mindestens einem Subtyp vorhanden sind.
  	 * @see #get(String)
  	 */
  	public boolean hasSubTypeData() {
  		return (expressionByType.size()>0) || (distributionByType.size()>0);
  	}
  
  	/**
  	 * Liefert die Daten fr einen bestimmten Subtyp oder als Fallback die Daten fr den globalen Fall.
  	 * @param subType	Kundentyp o.., fr den die Daten geliefert werden sollen. Wird <code>null</code> bergeben, so werden die Daten fr den globalen Fall geliefert.
  	 * @return	Liefert einen <code>String</code>, eine <code>AbstractRealDistribution</code> oder <code>null</code> zurck.
  	 */
  	public Object getOrDefault(final String subType) {
  		final Object obj=get(subType);
  		if (obj!=null) return obj;
  		return get();
  	}
  
  	/**
  	 * Stellt die Daten fr einen bestimmten Subtyp ein.
  	 * @param subType	Kundentyp o.., fr den die Daten eingestellt werden sollen. Wird <code>null</code> bergeben, so werden die Daten fr den globalen Fall gesetzt.
  	 * @param data	Einzustellende Daten (kann ein <code>String</code>, eine <code>AbstractRealDistribution</code> oder <code>null</code> sein).
  	 * @return	Gibt <code>true</code> zurck, wenn die Daten bernommen werden konnten.
  	 */
  	public boolean set(final String subType, final Object data) {
  		if (subType==null) return set(data);
  
  		if (data==null) {
  			distributionByType.remove(subType);
  			expressionByType.remove(subType);
  			return true;
  		}
  
  		if (data instanceof String) {
  			distributionByType.remove(subType);
  			expressionByType.put(subType,(String)data);
  			return true;
  		}
  
  		if (data instanceof AbstractRealDistribution) {
  			distributionByType.put(subType,(AbstractRealDistribution)data);
  			expressionByType.remove(subType);
  			return true;
  		}
  
  		return false;
  	}
  
  	/**
  	 * Prft, ob bereits Daten fr einen Subtyp mit dem angegebenen Namen vorhanden sind.
  	 * @param subType	Kundentyp o.., bei dem geprft werden soll, ob bereits Daten vorhanden sind.
  	 * @return	Gibt <code>true</code> zurck, wenn Daten fr den angegebenen Subtyp vorliegen.
  	 */
  	public boolean nameInUse(final String subType) {
  		if (distributionByType.get(subType)!=null) return true;
  		if (expressionByType.get(subType)!=null) return true;
  		return false;
  	}
  
  	/**
  	 * Liefert eine Liste aller Subtypen zu denen Daten vorhanden sind
  	 * @return	Liste aller Subtypen zu denen Daten vorhanden sind
  	 */
  	public String[] getNames() {
  		final List<String> names=new ArrayList<>();
  		for (Map.Entry<String,AbstractRealDistribution> entry: distributionByType.entrySet()) if (names.indexOf(entry.getKey())<0) names.add(entry.getKey());
  		for (Map.Entry<String,String> entry: expressionByType.entrySet()) if (names.indexOf(entry.getKey())<0) names.add(entry.getKey());
  		return names.toArray(String[]::new);
  	}
  
  	/**
  	 * ndert den Namen eines Subtyps in den Daten
  	 * @param oldName	Alter Name
  	 * @param newName	Neuer Name
  	 */
  	public void renameSubType(final String oldName, final String newName) {
  		final Object data=get(oldName);
  		set(newName,data);
  		set(oldName,null);
  	}
  
  	/**
  	 * Prft, ob die in einem anderen <code>DistributionSystem</code>-Objekt gespeicherten Daten mit den Daten in diesem Objekt bereinstimmen.
  	 * @param other	Anderes Objekt, das mit diesem Objekt verglichen werden soll.
  	 * @return	Gibt <code>true</code> zurck, wenn die beiden Objekte inhaltlich identisch sind.
  	 */
  	public boolean equalsDistributionSystem(final DistributionSystem other) {
  		if (other==null) return false;
  		if (canBeNull!=other.canBeNull) return false;
  
  		if (expression!=null) {
  			if (other.expression==null) return false;
  			if (!expression.equals(other.expression)) return false;
  		} else {
  			if (other.expression!=null) return false;
  			if (!DistributionTools.compare(distribution,other.distribution)) return false;
  		}
  
  		for (Map.Entry<String,String> entry: expressionByType.entrySet()) if (!entry.getValue().equals(other.expressionByType.get(entry.getKey()))) return false;
  		for (Map.Entry<String,String> entry: other.expressionByType.entrySet()) if (!entry.getValue().equals(expressionByType.get(entry.getKey()))) return false;
  
  		for (Map.Entry<String,AbstractRealDistribution> entry: distributionByType.entrySet()) if (!DistributionTools.compare(entry.getValue(),other.distributionByType.get(entry.getKey()))) return false;
  		for (Map.Entry<String,AbstractRealDistribution> entry: other.distributionByType.entrySet()) if (!DistributionTools.compare(entry.getValue(),distributionByType.get(entry.getKey()))) return false;
  
  		return true;
  	}
  
  	/**
  	 * Erstellt eine Kopie des Objekts.
  	 */
  	@Override
  	public DistributionSystem clone() {
  		final DistributionSystem clone=new DistributionSystem(subLangKey,typeLangKey,canBeNull);
  		clone.setData(this);
  
  		return clone;
  	}
  
  	/**
  	 * bernimmt (abgesehen von den Daten, die nur im Konstruktor eingestellt werden knnen,) alle Daten von einem anderen <code>DistributionSystem</code>-Objekt.
  	 * @param source	Anderes <code>DistributionSystem</code>-Objhekt, von dem die Daten bernommen werden sollen
  	 */
  	public void setData(final DistributionSystem source) {
  		if (source==null) return;
  
  		distribution=DistributionTools.cloneDistribution(source.distribution);
  		expression=source.expression;
  
  		expressionByType.clear();
  		distributionByType.clear();
  
  		for (Map.Entry<String,String> entry: source.expressionByType.entrySet()) expressionByType.put(entry.getKey(),entry.getValue());
  		for (Map.Entry<String,AbstractRealDistribution> entry: source.distributionByType.entrySet()) distributionByType.put(entry.getKey(),entry.getValue());
  	}
  
  	/**
  	 * Speichert die Daten in einer XML-Datei
  	 * @param doc	XML-Dokument
  	 * @param parent	bergeordnetes XML-Element
  	 * @param additionalData	Wird aufgerufen, um zustzliche Daten in dem gerade angelegten XML-Unterelement zu hinterlegen (kann <code>null</code> sein)
  	 */
  	public void save(final Document doc, final Element parent, final Consumer<Element> additionalData) {
  		Element sub;
  
  		if (distribution!=null || expression!=null) {
  			if (expression!=null) {
  				parent.appendChild(sub=doc.createElement(Language.trPrimary("Surface.DistributionSystem.XML.Expression")));
  				sub.setTextContent(expression);
  			} else {
  				parent.appendChild(sub=doc.createElement(Language.trPrimary("Surface.DistributionSystem.XML.Distribution")));
  				sub.setTextContent(DistributionTools.distributionToString(distribution));
  			}
  			if (typeLangKey!=null && !typeLangKey.isBlank()) sub.setAttribute(Language.trPrimary("Surface.DistributionSystem.XML.Distribution.Type"),Language.tr(typeLangKey));
  			if (additionalData!=null) additionalData.accept(sub);
  		}
  		if (subLangKey!=null && !subLangKey.isBlank()) {
  			for (Map.Entry<String,AbstractRealDistribution> entry : distributionByType.entrySet()) if (entry.getValue()!=null) {
  				if (expressionByType.get(entry.getKey())!=null) continue;
  				parent.appendChild(sub=doc.createElement(Language.trPrimary("Surface.DistributionSystem.XML.Distribution")));
  				sub.setAttribute(Language.trPrimary(subLangKey),entry.getKey());
  				if (typeLangKey!=null && !typeLangKey.isBlank()) sub.setAttribute(Language.trPrimary("Surface.DistributionSystem.XML.Distribution.Type"),Language.tr(typeLangKey));
  				sub.setTextContent(DistributionTools.distributionToString(entry.getValue()));
  			}
  			for (Map.Entry<String,String> entry : expressionByType.entrySet()) if (entry.getValue()!=null) {
  				parent.appendChild(sub=doc.createElement(Language.trPrimary("Surface.DistributionSystem.XML.Expression")));
  				sub.setAttribute(Language.trPrimary(subLangKey),entry.getKey());
  				if (typeLangKey!=null && !typeLangKey.isBlank()) sub.setAttribute(Language.trPrimary("Surface.DistributionSystem.XML.Distribution.Type"),Language.tr(typeLangKey));
  				sub.setTextContent(entry.getValue());
  			}
  		}
  	}
  
  	/**
  	 * Liefert den 0-basierten Index des Untereintrags beim Speichern der Verteilungen.
  	 * @param key	Schlssel fr den Untereintrag
  	 * @return	0-basierter Index oder -1, wenn fr den Schlssel keine Verteilung hinterlegt ist
  	 */
  	public int getSubNumber(final String key) {
  		int nr=0;
  		for (Map.Entry<String,AbstractRealDistribution> entry : distributionByType.entrySet()) if (entry.getValue()!=null) {
  			if (entry.getKey().equals(key)) return nr;
  			nr++;
  		}
  		return -1;
  	}
  
  	/**
  	 * Zhlt, wie viele Untereintrge (d.h. ohne den Haupteintrag) existieren.
  	 * @return	Anzahl an Untereintrgen
  	 */
  	public int getSubNumberCount() {
  		int count=0;
  		for (Map.Entry<String,AbstractRealDistribution> entry : distributionByType.entrySet()) if (entry.getValue()!=null) count++;
  		return count;
  	}
  
  	/**
  	 * Speichert die Daten in einer XML-Datei
  	 * @param doc	XML-Dokument
  	 * @param parent	bergeordnetes XML-Element
  	 */
  	public void save(final Document doc, final Element parent) {
  		save(doc,parent,null);
  	}
  
  	/**
  	 * Ldt Teil-Daten aus dem angegebenen XML-Element.<br>
  	 * Durch den Aufruf von <code>loadDistribution</code> ist bereits festgelegt, dass es sich um eine Verteilung und nicht um einen Ausdruck handelt.
  	 * Die Information Global/Kundentyp wird durch die Methode selbst aus dem XML-Element ermittelt.
  	 * @param node	XML-Element, aus dem die Daten geladen werden sollen
  	 * @return	Gibt im Erfolgsfall <code>null</code> zurck, sonst eine Fehlermeldung.
  	 */
  	public String loadDistribution(final Element node) {
  		final String type=(subLangKey==null || subLangKey.isBlank())?"":Language.trAllAttribute(subLangKey,node);
  		final AbstractRealDistribution dist=DistributionTools.distributionFromString(node.getTextContent(),3000);
  
  		if (dist==null) return String.format(Language.tr("Surface.XML.ElementSubError"),node.getNodeName(),node.getParentNode().getNodeName());
  		if (type==null || type.isBlank()) {
  			distribution=dist;
  		} else {
  			distributionByType.put(type,dist);
  		}
  
  		return null;
  	}
  
  	/**
  	 * Ldt Teil-Daten aus dem angegebenen XML-Element.<br>
  	 * Durch den Aufruf von <code>loadExpression</code> ist bereits festgelegt, dass es sich um einen Ausdruck und nicht um eine Verteilung handelt.
  	 * Die Information Global/Kundentyp wird durch die Methode selbst aus dem XML-Element ermittelt.
  	 * @param node	XML-Element, aus dem die Daten geladen werden sollen
  	 * @return	Gibt im Erfolgsfall <code>null</code> zurck, sonst eine Fehlermeldung.
  	 */
  	public String loadExpression(final Element node) {
  		final String type=(subLangKey==null || subLangKey.isBlank())?"":Language.trAllAttribute(subLangKey,node);
  		final String expr=node.getTextContent();
  
  		if (type==null || type.isBlank()) {
  			expression=expr;
  		} else {
  			expressionByType.put(type,expr);
  		}
  		return null;
  	}
  
  	/**
  	 * Liefert den Type-Attribute des XML-Elements
  	 * @param node	XML-Element fr das der Typ ausgelesen werden soll
  	 * @return	Typ (der Verteilung/des Ausdrucks)
  	 */
  	public static String getTypeAttribute(final Element node) {
  		return Language.trAllAttribute("Surface.DistributionSystem.XML.Distribution.Type",node);
  	}
  
  	/**
  	 * Prft, ob das Element ein ladbares Verteilungs-Element ist
  	 * @param node	Zu prfendes Element
  	 * @return	Gibt <code>true</code> zurck, wenn es sich um eine Verteilung handelt
  	 */
  	public static boolean isDistribution(final Element node) {
  		return Language.trAll("Surface.DistributionSystem.XML.Distribution",node.getNodeName());
  	}
  
  	/**
  	 * Prft, ob das Element ein ladbares Ausdrucks-Element ist
  	 * @param node	Zu prfendes Element
  	 * @return	Gibt <code>true</code> zurck, wenn es sich um einen Ausdruck handelt
  	 */
  	public static boolean isExpression(final Element node) {
  		return Language.trAll("Surface.DistributionSystem.XML.Expression",node.getNodeName());
  	}
  
  	/**
  	 * Liefert den Wert des Subtyp-Attributes des XML-Element
  	 * @param node	XML-Element zu dem das Subtyp-Attribut ausgelesen werden soll
  	 * @return	Wert des Subtyp-Attribues
  	 */
  	public String getSubAttribute(final Element node) {
  		if (subLangKey==null || subLangKey.isBlank()) return "";
  		return Language.trAllAttribute(subLangKey,node);
  	}
  
  	/**
  	 * Gibt an, ob ein XML-Element Daten zu dem gloalen oder zu einem Subtyp-Element enthlt
  	 * @param node	XML-Element bei dem berprft werden soll, ob es Daten zu dem gloalen oder zu einem Subtyp-Element enthlt
  	 * @return	Gibt <code>true</code> zurck, wenn das XML-Element Daten zu dem globalen Element enthlt
  	 */
  	public boolean isGlobal(final Element node) {
  		final String sub=getSubAttribute(node);
  		return (sub==null || sub.isBlank());
  	}
  
  	/**
  	 * Fgt die Beschreibung fr die Daten dieses Objekts als Eigenschaft zu der Beschreibung hinzu
  	 * @param descriptionBuilder	Description-Builder, der die Beschreibungsdaten zusammenfasst
  	 * @param labelClient	Beschriftung fr kundenspezifische Verteilungen (muss einen Platzhalter fr den Kundentypnamen enthalten)
  	 * @param labelGeneralCase	Beschriftung fr die globale Verteilungen
  	 * @param position	 Position in der Reihenfolge der Description-Builder Eigenschaften
  	 */
  	public void buildDescriptionProperty(final ModelDescriptionBuilder descriptionBuilder, final String labelClient, final String labelGeneralCase, final int position) {
  		Object obj;
  
  		for (final String clientTypeName: descriptionBuilder.getClientTypes()) {
  			obj=get(clientTypeName);
  			if (obj instanceof AbstractRealDistribution) descriptionBuilder.addProperty(String.format(labelClient,clientTypeName),ModelDescriptionBuilder.getDistributionInfo((AbstractRealDistribution)obj),position);
  			if (obj instanceof String) descriptionBuilder.addProperty(String.format(labelClient,clientTypeName),Language.tr("ModelDescription.Expression")+": "+(String)obj,position);
  		}
  
  		obj=get();
  		if (obj instanceof AbstractRealDistribution) descriptionBuilder.addProperty(labelGeneralCase,ModelDescriptionBuilder.getDistributionInfo((AbstractRealDistribution)obj),position);
  		if (obj instanceof String) descriptionBuilder.addProperty(labelGeneralCase,Language.tr("ModelDescription.Expression")+": "+(String)obj,position);
  	}
  
  	/**
  	 * Sucht einen Text in den Daten dieses Datensatzes.
  	 * @param searcher	Such-System
  	 * @param station	Station an der dieser Datensatz verwendet wird
  	 * @param typeName	Bestimmung (z.B. "Bedienzeiten") dieses Datensatzes
  	 * @see FullTextSearch
  	 */
  	public void search(final FullTextSearch searcher, final ModelElementBox station, final String typeName) {
  		/* Globale Werte */
  		if (expression!=null) {
  			searcher.testString(station,typeName+" - "+Language.tr("Editor.DialogBase.Search.GlobalExpression"),expression,newExpression->{expression=newExpression;});
  		} else {
  			searcher.testDistribution(station,typeName+" - "+Language.tr("Editor.DialogBase.Search.GlobalDistribution"),distribution);
  		}
  
  		/* Kundentypspezifische Werte */
  		for (Map.Entry<String,String> expression: expressionByType.entrySet()) {
  			final String clientType=expression.getKey();
  			searcher.testString(station,typeName+" - "+String.format(Language.tr("Editor.DialogBase.Search.ClientTypeExpression"),clientType),expression.getValue(),newExpression->expressionByType.put(clientType,newExpression));
  		}
  		for (Map.Entry<String,AbstractRealDistribution> distribution: distributionByType.entrySet()) {
  			final String clientType=distribution.getKey();
  			if (expressionByType.get(clientType)!=null) continue;
  			searcher.testDistribution(station,typeName+" - "+String.format(Language.tr("Editor.DialogBase.Search.ClientTypeDistribution"),clientType),distribution.getValue());
  		}
  	}
  }