/*
 * Copyright (C) 2018-2022 Andrew Gegg
 *
 *	This file is part of the Garden Notebook application
 *
 * The Garden Notebook application is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl.html>.
 */

/*
	Change log
    2.2.5   Guard against occasional NPE on item delete removing base listeners
    2.6.0   on setAncestor(), ensure hasAncestor() is set true
            allow leaf items to be disconnected from their ancestors
    2.9.6	When a Diary entry is added/changed, make sure updated comments are shown
    3.0.0	Support Locations and new Quantity and Severity fields.
    3.0.4	Comment handling
 */

package uk.co.gardennotebook.fxbean;

import javafx.beans.property.*;
import uk.co.gardennotebook.spi.*;
import uk.co.gardennotebook.util.StoryLineTree;

//import java.time.format.DateTimeFormatter;
//import java.time.format.FormatStyle;
import java.util.Optional;
//import java.util.List;
//import java.util.ArrayList;
import java.beans.PropertyChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.ChangeListener;

//import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.EntryMessage;
//import org.apache.logging.log4j.Level;

import java.time.*;

import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.ObjectProperty;
//import javafx.beans.property.SimpleObjectProperty;

/**
	*	Records an observation of an affliction - for instance, an outbreak of red spider mite
	*
	*	@author	Andy Gegg
	*	@version	3.0.4
	*	@since	1.0
*/
final public class AfflictionEventBean implements INotebookBean
{
	private static final Logger LOGGER = LogManager.getLogger();

	private IAfflictionEvent baseItem = null;

	private Integer itemKey = 0;
	private boolean newItem = false;
	private boolean explicitSave = false;
	private final SimpleBooleanProperty saveRequiredProperty = new SimpleBooleanProperty(this, "saveRequired", explicitSave);
	private IAfflictionEventBuilder explicitBuilder = null;

		// handle changes to the base item itself
	private PropertyChangeListener baseItemDeleted;
	private PropertyChangeListener baseItemReplaced;

	private final SimpleObjectProperty<AfflictionBean> parentAfflictionProperty = new SimpleObjectProperty<>(this, "affliction", null);
	private final ChangeListener<AfflictionBean> afflictionIdListener = this::onAfflictionIdChange;

	/*
	*	The problem occurred on plants of this species.
@apiNote
If present, plantVarietyId may or may not be given.  This 'denormalises' the model but enables easy searches for e.g. 'all tomatoes'
	*/
	private final SimpleObjectProperty<PlantSpeciesBean> parentPlantSpeciesProperty = new SimpleObjectProperty<>(this, "plantSpecies", null);
	private final ChangeListener<PlantSpeciesBean> plantSpeciesIdListener = this::onPlantSpeciesIdChange;
	private final ReadOnlyBooleanWrapper hasParentPlantSpeciesProperty = new ReadOnlyBooleanWrapper(this, "hasPlantSpecies", false);

	/*
	*	The problem occurred on plants of this variety.
@apiNote
If present, plantSpeciesId must be given.  This 'denormalises' the model but enables easy searches for e.g. 'all tomatoes'
	*/
	private final SimpleObjectProperty<PlantVarietyBean> parentPlantVarietyProperty = new SimpleObjectProperty<>(this, "plantVariety", null);
	private final ChangeListener<PlantVarietyBean> plantVarietyIdListener = this::onPlantVarietyIdChange;
	private final ReadOnlyBooleanWrapper hasParentPlantVarietyProperty = new ReadOnlyBooleanWrapper(this, "hasPlantVariety", false);

	/*
	 *	Location where the activity took place
	 */
	private final SimpleObjectProperty<LocationBean> parentLocationProperty = new SimpleObjectProperty<>(this, "location", null);
	private final ChangeListener<LocationBean> locationIdListener = this::onLocationIdChange;
	private final ReadOnlyBooleanWrapper hasParentLocationProperty = new ReadOnlyBooleanWrapper(this, "hasLocation", false);

	/*
	*	When an outbreak is spotted or there is some development
	*/
	private final SimpleObjectProperty<LocalDate> dateProperty = new SimpleObjectProperty<>(this, "date", LocalDate.now());
	private final ChangeListener<LocalDate> dateListener = this::onDateChange;

	private final SimpleStringProperty quantityProperty = new SimpleStringProperty(this, "quantity", "");
	private final ChangeListener<String> quantityListener = this::onQuantityChange;

	private final SimpleStringProperty severityProperty = new SimpleStringProperty(this, "severity", "");
	private final ChangeListener<String> severityListener = this::onSeverityChange;

	private final ReadOnlyObjectWrapper<LocalDateTime> lastUpdatedProperty = new ReadOnlyObjectWrapper<>(this, "lastUpdated", LocalDateTime.now());
	private final ReadOnlyObjectWrapper<LocalDateTime> createdProperty = new ReadOnlyObjectWrapper<>(this, "created", LocalDateTime.now());
	private ReadOnlyBooleanWrapper canDeleteProperty = null;
	private ReadOnlyBooleanWrapper hasAncestorProperty = null;
	private ReadOnlyBooleanWrapper hasDescendantProperty = null;

	private ReadOnlyBooleanWrapper isNewProperty = new ReadOnlyBooleanWrapper(this, "isNew", newItem);	//	2.9.6

	private BeanCommentHandler<IAfflictionEvent> beanCommentHandler;	//	2.9.6
	private final ReadOnlyStringWrapper commentTextProperty = new ReadOnlyStringWrapper(this, "commentText", "");	//	2.9.6

	/**
	*	Construct an 'empty' Bean.  Set the various property values then call save() to create the new AfflictionEventBean
	*/
	public AfflictionEventBean()
	{
		this(null);
	}
	/**
	*	Construct a Bean wrapping the given AfflictionEvent
	*	If the parameter is null a new 'empty' Bean will be constructed
	*
	*	@param	initialValue	the AfflictionEvent to wrap.  If null an 'empty' bean will be constructed
	*
	*/
	public AfflictionEventBean(final IAfflictionEvent initialValue)
	{
		ChangeListener<Boolean> saveRequiredListener = (obs, old, nval) -> {
			if (nval && !explicitSave)
			{
				explicitSave = true;
				ITrug server = TrugServer.getTrugServer().getTrug();
				explicitBuilder = server.getAfflictionEventBuilder(baseItem);
			}
			if (!nval && explicitSave && (baseItem != null))
			{
				explicitSave = false;
				explicitBuilder = null;
			}
		};

		saveRequiredProperty.addListener(saveRequiredListener);

		if(initialValue == null)
		{
			newItem = true;
			//	add the listeners BEFORE setting values, or default values never get sent to the builder!
			addListeners();
			setDefaults();
			saveRequiredProperty.set(true);
			return;
		}

		baseItem = initialValue;

		itemKey = baseItem.getKey();

		newItem = false;
		setValues();

		addListeners();
		declareBaseListeners();
		addBaseListeners();
	}

	/**
	*	Returns the underlying AfflictionEvent, if present
	*
	*	@return	the underlying AfflictionEvent, if present
	*/
	public Optional<IAfflictionEvent> get()
	{
		return getValue();
	}

	/**
	*	Returns the underlying AfflictionEvent if present
	*
	*	@return	the underlying AfflictionEvent, if present
	*/
	public Optional<IAfflictionEvent> getValue()
	{
		return Optional.ofNullable(baseItem);
	}

	@Override
	public NotebookEntryType getType()
	{
		return NotebookEntryType.AFFLICTIONEVENT;
	}

	@Override
	public Integer getKey()
	{
		return itemKey;
	}

	@Override
	public boolean sameAs(final INotebookBean other)
	{
		if (other == null || ((AfflictionEventBean)other).baseItem == null || baseItem == null)
		{
			return false;
		}
		if (other.getType() != NotebookEntryType.AFFLICTIONEVENT)
		{
			return false;
		}
		return baseItem.sameAs(((AfflictionEventBean)other).baseItem);
	}

	@Override
	public boolean isNew()
	{
		return isNewProperty().get();
	}

	@Override
	public ReadOnlyBooleanProperty isNewProperty()
	{
		if (isNewProperty == null)
		{
			isNewProperty = new ReadOnlyBooleanWrapper(this, "isNew", newItem);
		}
		return isNewProperty.getReadOnlyProperty();
	}

	@Override
	public boolean canDelete() throws GNDBException
	{
		return canDeleteProperty().get();
	}

	@Override
	public ReadOnlyBooleanProperty canDeleteProperty() throws GNDBException
	{
		if (canDeleteProperty == null)
		{
			ITrug server = TrugServer.getTrugServer().getTrug();
			boolean canDel = server.getAfflictionEventBuilder(baseItem).canDelete();
			canDeleteProperty = new ReadOnlyBooleanWrapper(this, "canDelete", canDel);
		}
		return canDeleteProperty.getReadOnlyProperty();
	}

	@Override
	public boolean hasAncestor() throws GNDBException
	{
		return hasAncestorProperty().get();
	}

	@Override
	public ReadOnlyBooleanProperty hasAncestorProperty() throws GNDBException
	{
		if (hasAncestorProperty == null)
		{
			ITrug server = TrugServer.getTrugServer().getTrug();
			boolean gotOne = server.getAfflictionEventBuilder(baseItem).hasAncestor();
            getHasAncestorProperty().set(gotOne);
		}
		return hasAncestorProperty.getReadOnlyProperty();
	}	//	hasAncestorProperty()

    private ReadOnlyBooleanWrapper getHasAncestorProperty()
    {
		if (hasAncestorProperty == null)
		{
			hasAncestorProperty = new ReadOnlyBooleanWrapper(this, "hasAncestor", false);
        }
        return hasAncestorProperty;
    }

	@Override
	public StoryLineTree<? extends INotebookBean> getAncestors() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getAncestors()");
		if (baseItem == null)
		{
			return StoryLineTree.emptyTree();
		}
		ITrug server = TrugServer.getTrugServer().getTrug();
		StoryLineTree<? extends INotebookEntry> tree = server.getAfflictionEventBuilder(baseItem).getAncestors();
		StoryLineTree<? extends INotebookBean> beanTree = tree.copyTree((item) -> switch (item.getType())
				{
					case PURCHASEITEM -> new PurchaseItemBean((IPurchaseItem) item);
					case GROUNDWORK -> new GroundworkBean((IGroundwork) item);
					case AFFLICTIONEVENT -> new AfflictionEventBean((IAfflictionEvent) item);
					case HUSBANDRY -> new HusbandryBean((IHusbandry) item);
					case SALEITEM -> new SaleItemBean((ISaleItem) item);
					default -> null;
				});
		return LOGGER.traceExit(log4jEntryMsg, beanTree);
	}	//	getAncestors()

	/**
	*	Make the current AfflictionEvent bean a descendant of the given PurchaseItem bean
	*
	*	@param	parent	the PurchaseItem bean to be the parent
	*
	*	@throws	GNDBException	if the underlying persisted storage engine (e.g. database server) throws an exception
	*				The original error can be retrieved by <code>getCause()</code>
	*/
	public void setAncestor(final PurchaseItemBean parent) throws GNDBException
	{
		LOGGER.debug("setAncestor(): explicitSave: {}, parent: {}", explicitSave, parent);
		if (explicitSave)
		{
			explicitBuilder.ancestor(parent.get().get());
		}
		else
		{
			ITrug server = TrugServer.getTrugServer().getTrug();
			server.getAfflictionEventBuilder(baseItem).ancestor(parent.get().get()).save();
		}
        getHasAncestorProperty().set(true);  //  2.6.0
        parent.notifyDescendantAdded(); //  2.6.0
        LOGGER.debug("[ {} ] made descendant of [ {} ]", baseItem, parent.get().get());
	}
	/**
	*	Make the current AfflictionEvent bean a descendant of the given Groundwork bean
	*
	*	@param	parent	the Groundwork bean to be the parent
	*
	*	@throws	GNDBException	if the underlying persisted storage engine (e.g. database server) throws an exception
	*				The original error can be retrieved by <code>getCause()</code>
	*/
	public void setAncestor(final GroundworkBean parent) throws GNDBException
	{
		LOGGER.debug("setAncestor(): explicitSave: {}, parent: {}", explicitSave, parent);
		if (explicitSave)
		{
			explicitBuilder.ancestor(parent.get().get());
		}
		else
		{
			ITrug server = TrugServer.getTrugServer().getTrug();
			server.getAfflictionEventBuilder(baseItem).ancestor(parent.get().get()).save();
		}
        getHasAncestorProperty().set(true);  //  2.6.0
        parent.notifyDescendantAdded(); //  2.6.0
        LOGGER.debug("[ {} ] made descendant of [ {} ]", baseItem, parent.get().get());
	}
	/**
	*	Make the current AfflictionEvent bean a descendant of the given AfflictionEvent bean
	*
	*	@param	parent	the AfflictionEvent bean to be the parent
	*
	*	@throws	GNDBException	if the underlying persisted storage engine (e.g. database server) throws an exception
	*				The original error can be retrieved by <code>getCause()</code>
	*/
	public void setAncestor(final AfflictionEventBean parent) throws GNDBException
	{
		LOGGER.debug("setAncestor(): explicitSave: {}, parent: {}", explicitSave, parent);
		if (explicitSave)
		{
			explicitBuilder.ancestor(parent.get().get());
		}
		else
		{
			ITrug server = TrugServer.getTrugServer().getTrug();
			server.getAfflictionEventBuilder(baseItem).ancestor(parent.get().get()).save();
		}
        getHasAncestorProperty().set(true);  //  2.6.0
        parent.notifyDescendantAdded(); //  2.6.0
        LOGGER.debug("[ {} ] made descendant of [ {} ]", baseItem, parent.get().get());
	}
	/**
	*	Make the current AfflictionEvent bean a descendant of the given Husbandry bean
	*
	*	@param	parent	the Husbandry bean to be the parent
	*
	*	@throws	GNDBException	if the underlying persisted storage engine (e.g. database server) throws an exception
	*				The original error can be retrieved by <code>getCause()</code>
	*/
	public void setAncestor(final HusbandryBean parent) throws GNDBException
	{
		LOGGER.debug("setAncestor(): explicitSave: {}, parent: {}", explicitSave, parent);
		if (explicitSave)
		{
			explicitBuilder.ancestor(parent.get().get());
		}
		else
		{
			ITrug server = TrugServer.getTrugServer().getTrug();
			server.getAfflictionEventBuilder(baseItem).ancestor(parent.get().get()).save();
		}
        getHasAncestorProperty().set(true);  //  2.6.0
        parent.notifyDescendantAdded(); //  2.6.0
        LOGGER.debug("[ {} ] made descendant of [ {} ]", baseItem, parent.get().get());
	}

    /**
     * Disconnect this item from its ancestors.
     * Item must be a leaf item.
     * 
	*	@throws	GNDBException	if the underlying persisted storage engine (e.g. database server) throws an exception
	*				The original error can be retrieved by <code>getCause()</code>
     * 
     * @since 2.6.0
     */
	public void dropLeaf() throws GNDBException
	{
		LOGGER.debug("dropLeaf(): explicitSave: {}", explicitSave);
        
        if (newItem)
            return;
        
        if (hasDescendant())
            return;
        if (!hasAncestor())
            return;

        if (explicitSave)
		{
			explicitBuilder.dropLeaf();
			saveRequiredProperty.set(false);
		}
		else
		{
			ITrug server = TrugServer.getTrugServer().getTrug();
			server.getAfflictionEventBuilder(baseItem).dropLeaf();
		}
        
        hasAncestorProperty.set(false);
        // cannot assume parent has no descendants!
        
        LOGGER.info("[ {} ] orphaned", baseItem);
	}

    @Override
	public boolean hasDescendant() throws GNDBException
	{
		return hasDescendantProperty().get();
	}

	@Override
	public ReadOnlyBooleanProperty hasDescendantProperty() throws GNDBException
	{
		if (hasDescendantProperty == null)
		{
			ITrug server = TrugServer.getTrugServer().getTrug();
			boolean gotOne = server.getAfflictionEventBuilder(baseItem).hasDescendant();
			hasDescendantProperty = new ReadOnlyBooleanWrapper(this, "hasDescendant", gotOne);
		}
		return hasDescendantProperty.getReadOnlyProperty();
	}	//	hasDescendantProperty()

    /**
     * Used to notify the bean that this PurchaseItem has had a descendant added
     * by the UI.
     * 
     * @since   2.6.0
     */
    void notifyDescendantAdded()
    {
		if (hasDescendantProperty == null)
		{
			hasDescendantProperty = new ReadOnlyBooleanWrapper(this, "hasDescendant", true);
		}
        else
        {
            hasDescendantProperty.set(true);
        }
    }

	@Override
	public StoryLineTree<? extends INotebookBean> getDescendants() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getDescendants()");
		if (baseItem == null)
		{
			return StoryLineTree.emptyTree();
		}
		ITrug server = TrugServer.getTrugServer().getTrug();
		StoryLineTree<? extends INotebookEntry> tree = server.getAfflictionEventBuilder(baseItem).getDescendants();
		if (tree == null)
		{
			return StoryLineTree.emptyTree();
		}
		StoryLineTree<? extends INotebookBean> beanTree = tree.copyTree((item) -> switch (item.getType())
				{
					case PURCHASEITEM -> new PurchaseItemBean((IPurchaseItem) item);
					case GROUNDWORK -> new GroundworkBean((IGroundwork) item);
					case AFFLICTIONEVENT -> new AfflictionEventBean((IAfflictionEvent) item);
					case HUSBANDRY -> new HusbandryBean((IHusbandry) item);
					case SALEITEM -> new SaleItemBean((ISaleItem) item);
					default -> null;
				});
		return LOGGER.traceExit(log4jEntryMsg, beanTree);
	}	//	getDescendants()

	public AfflictionBean getAffliction()
	{
		return afflictionProperty().getValue();
	}
	public void setAffliction(final AfflictionBean bean)
	{
		afflictionProperty().setValue(bean);
	}
	public void setAffliction(final IAffliction item)
	{
		afflictionProperty().setValue(new AfflictionBean(item));
	}
	/**
	*	Returns the Affliction parent of the AfflictionEvent this Bean wraps
	*
	*	@return	the Affliction parent of the AfflictionEvent this Bean wraps
	*/
	public ObjectProperty<AfflictionBean> afflictionProperty()
	{
		return parentAfflictionProperty;
	}

	/**
	*	Handle changes to the AfflictionId value
	*
	*	@throws	GNDBRuntimeException	if the underlying persisted storage engine (e.g. database server) throws an exception
	*				The original error can be retrieved by <code>getCause()</code>
	*/
	private void onAfflictionIdChange(ObservableValue<? extends AfflictionBean> obs, AfflictionBean old, AfflictionBean nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onAfflictionIdChange(): old={}, new={}", old, nval);
		if (nval == null)
		{	// this is an error condition and should be flagged
LOGGER.debug("onAfflictionIdChange(): nval is null");
			return;
		}
		if (nval.sameAs(old))
		{
LOGGER.debug("onAfflictionIdChange(): nval is sameAs old");
			return;
		}
		if (!nval.isNew())
		{
			if (explicitSave)
			{
				explicitBuilder.affliction(nval.get().get());
			}
			else
			{
LOGGER.debug("onAfflictionIdChange(): NOT explicitSave");
				ITrug server = TrugServer.getTrugServer().getTrug();
				//	the Builder will send an event to the baseItem to say it's been replaced
			try
			{
				server.getAfflictionEventBuilder(baseItem).affliction(nval.get().get()).save();
			} catch (GNDBException ex) {
				throw new GNDBRuntimeException(ex);
			}
			}
		}

		LOGGER.traceExit(log4jEntryMsg);
	}


	public boolean hasPlantSpecies()
	{
		return hasPlantSpeciesProperty().getValue();
	}
	/**
	*	Use this to check if the PlantSpecies parent of the AfflictionEvent this Bean wraps is present
	*
	*	@return	true if this AfflictionEvent is linked to a PlantSpecies
	*/
	public ReadOnlyBooleanProperty hasPlantSpeciesProperty()
	{
		return hasParentPlantSpeciesProperty.getReadOnlyProperty();
	}
	public PlantSpeciesBean getPlantSpecies()
	{
		return plantSpeciesProperty().getValue();
	}
	public void setPlantSpecies(final PlantSpeciesBean bean)
	{
		plantSpeciesProperty().setValue(bean);
	}
	public void setPlantSpecies(final IPlantSpecies item)
	{
		plantSpeciesProperty().setValue(new PlantSpeciesBean(item));
	}
	/**
	*	Returns the PlantSpecies parent of the AfflictionEvent this Bean wraps
	*	Call hasPlantSpecies() first to check if this value is set
	*
	*	@return	the PlantSpecies parent of the AfflictionEvent this Bean wraps
	*/
	public ObjectProperty<PlantSpeciesBean> plantSpeciesProperty()
	{
		return parentPlantSpeciesProperty;
	}

	/**
	*	Handle changes to the PlantSpeciesId value
	*
	*	@throws	GNDBRuntimeException	if the underlying persisted storage engine (e.g. database server) throws an exception
	*				The original error can be retrieved by <code>getCause()</code>
	*/
	private void onPlantSpeciesIdChange(ObservableValue<? extends PlantSpeciesBean> obs, PlantSpeciesBean old, PlantSpeciesBean nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onPlantSpeciesIdChange(): old={}, new={}", old, nval);
		if (nval != null && nval.sameAs(old))
		{
LOGGER.debug("onPlantSpeciesIdChange(): nval is sameAs old");
			return;
		}
		hasParentPlantSpeciesProperty.set(nval != null);

		if ((nval != null) && !nval.isNew())
		{
			if (explicitSave)
			{
LOGGER.debug("onPlantSpeciesIdChange(): explicitSave");
				explicitBuilder.plantSpecies(nval.get().get());
			}
			else
			{
LOGGER.debug("onPlantSpeciesIdChange(): NOT explicitSave");
				ITrug server = TrugServer.getTrugServer().getTrug();
				//	the Builder will send an event to the baseItem to say it's been replaced
				try
				{
					server.getAfflictionEventBuilder(baseItem).plantSpecies(nval.get().get()).save();
				} catch (GNDBException ex) {
					throw new GNDBRuntimeException(ex);
				}
			}
		}
		else if (nval == null)
		{
			if (explicitSave)
			{
				explicitBuilder.plantSpecies(null);
			}
			else
			{
LOGGER.debug("onPlantSpeciesIdChange(): NOT explicitSave");
				ITrug server = TrugServer.getTrugServer().getTrug();
				//	the Builder will send an event to the baseItem to say it's been replaced
				try
				{
					server.getAfflictionEventBuilder(baseItem).plantSpecies(null).save();
				} catch (GNDBException ex) {
					throw new GNDBRuntimeException(ex);
				}
			}
		}

		LOGGER.traceExit(log4jEntryMsg);
	}


	public boolean hasPlantVariety()
	{
		return hasPlantVarietyProperty().getValue();
	}
	/**
	*	Use this to check if the PlantVariety parent of the AfflictionEvent this Bean wraps is present
	*
	*	@return	true if this AfflictionEvent is linked to a PlantVariety
	*/
	public ReadOnlyBooleanProperty hasPlantVarietyProperty()
	{
		return hasParentPlantVarietyProperty.getReadOnlyProperty();
	}
	public PlantVarietyBean getPlantVariety()
	{
		return plantVarietyProperty().getValue();
	}
	public void setPlantVariety(final PlantVarietyBean bean)
	{
		plantVarietyProperty().setValue(bean);
	}
	public void setPlantVariety(final IPlantVariety item)
	{
		plantVarietyProperty().setValue(new PlantVarietyBean(item));
	}
	/**
	*	Returns the PlantVariety parent of the AfflictionEvent this Bean wraps
	*	Call hasPlantVariety() first to check if this value is set
	*
	*	@return	the PlantVariety parent of the AfflictionEvent this Bean wraps
	*/
	public ObjectProperty<PlantVarietyBean> plantVarietyProperty()
	{
		return parentPlantVarietyProperty;
	}

	/**
	*	Handle changes to the PlantVarietyId value
	*
	*	@throws	GNDBRuntimeException	if the underlying persisted storage engine (e.g. database server) throws an exception
	*				The original error can be retrieved by <code>getCause()</code>
	*/
	private void onPlantVarietyIdChange(ObservableValue<? extends PlantVarietyBean> obs, PlantVarietyBean old, PlantVarietyBean nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onPlantVarietyIdChange(): old={}, new={}", old, nval);
		if (nval != null && nval.sameAs(old))
		{
LOGGER.debug("onPlantVarietyIdChange(): nval is sameAs old");
			return;
		}
		hasParentPlantVarietyProperty.set(nval != null);

		if ((nval != null) && !nval.isNew())
		{
			if (explicitSave)
			{
LOGGER.debug("onPlantVarietyIdChange(): explicitSave");
				explicitBuilder.plantVariety(nval.get().get());
			}
			else
			{
LOGGER.debug("onPlantVarietyIdChange(): NOT explicitSave");
				ITrug server = TrugServer.getTrugServer().getTrug();
				//	the Builder will send an event to the baseItem to say it's been replaced
				try
				{
					server.getAfflictionEventBuilder(baseItem).plantVariety(nval.get().get()).save();
				} catch (GNDBException ex) {
					throw new GNDBRuntimeException(ex);
				}
			}
		}
		else if (nval == null)
		{
			if (explicitSave)
			{
				explicitBuilder.plantVariety(null);
			}
			else
			{
LOGGER.debug("onPlantVarietyIdChange(): NOT explicitSave");
				ITrug server = TrugServer.getTrugServer().getTrug();
				//	the Builder will send an event to the baseItem to say it's been replaced
				try
				{
					server.getAfflictionEventBuilder(baseItem).plantVariety(null).save();
				} catch (GNDBException ex) {
					throw new GNDBRuntimeException(ex);
				}
			}
		}

		LOGGER.traceExit(log4jEntryMsg);
	}


	public boolean hasLocation()
	{
		return hasLocationProperty().getValue();
	}
	/**
	 *	Use this to check if the Location parent of the Husbandry this Bean wraps is present
	 *
	 *	@return	true if this Husbandry is linked to a Location
	 */
	public ReadOnlyBooleanProperty hasLocationProperty()
	{
		return hasParentLocationProperty.getReadOnlyProperty();
	}
	public LocationBean getLocation()
	{
		return locationProperty().getValue();
	}
	public void setLocation(final LocationBean bean)
	{
		locationProperty().setValue(bean);
	}
	public void setLocation(final ILocation item)
	{
		locationProperty().setValue(new LocationBean(item));
	}
	/**
	 *	Returns the Location parent of the Husbandry this Bean wraps
	 *	Call hasLocation() first to check if this value is set
	 *
	 *	@return	the Location parent of the Husbandry this Bean wraps
	 */
	public ObjectProperty<LocationBean> locationProperty()
	{
		return parentLocationProperty;
	}

	/**
	 *	Handle changes to the LocationId value
	 *
	 *	@throws	GNDBRuntimeException	if the underlying persisted storage engine (e.g. database server) throws an exception
	 *				The original error can be retrieved by <code>getCause()</code>
	 */
	private void onLocationIdChange(ObservableValue<? extends LocationBean> obs, LocationBean old, LocationBean nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onLocationIdChange(): old={}, new={}", old, nval);
		if (nval != null && nval.sameAs(old))
		{
			LOGGER.debug("onLocationIdChange(): nval is sameAs old");
			return;
		}
		hasParentLocationProperty.set(nval != null);

		if ((nval != null) && !nval.isNew())
		{
			if (explicitSave)
			{
				LOGGER.debug("onLocationIdChange(): explicitSave");
				explicitBuilder.location(nval.get().get());
			}
			else
			{
				LOGGER.debug("onLocationIdChange(): NOT explicitSave");
				ITrug server = TrugServer.getTrugServer().getTrug();
				//	the Builder will send an event to the baseItem to say it's been replaced
				try
				{
					server.getAfflictionEventBuilder(baseItem).location(nval.get().get()).save();
				} catch (GNDBException ex) {
					throw new GNDBRuntimeException(ex);
				}
			}
		}
		else if (nval == null)
		{
			if (explicitSave)
			{
				explicitBuilder.location(null);
			}
			else
			{
				LOGGER.debug("onLocationIdChange(): NOT explicitSave");
				ITrug server = TrugServer.getTrugServer().getTrug();
				//	the Builder will send an event to the baseItem to say it's been replaced
				try
				{
					server.getAfflictionEventBuilder(baseItem).location(null).save();
				} catch (GNDBException ex) {
					throw new GNDBRuntimeException(ex);
				}
			}
		}

		LOGGER.traceExit(log4jEntryMsg);
	}	//	onLocationIdChange()

	public LocalDate getDate()
	{
		return dateProperty.get();
	}
	public void setDate(final LocalDate newVal)
	{
		dateProperty.set(newVal);
	}
	/**
	*	Wraps the Date value of the AfflictionEvent
	*
	*	@return	a writable property wrapping the date attribute
	*/
	public ObjectProperty<LocalDate> dateProperty()
	{
		return dateProperty;
	}

	private void onDateChange(ObservableValue<? extends LocalDate> obs, LocalDate old, LocalDate nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onDateChange(): old={}, new={}", old, nval);
		if (explicitSave)
		{
LOGGER.debug("onDateChange(): explicitSave");
			explicitBuilder.date(nval);
		}
		else
		{
LOGGER.debug("onDateChange(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			try
			{
				baseItem = server.getAfflictionEventBuilder(baseItem).date(nval).save();
			} catch (GNDBException ex) {
				throw new GNDBRuntimeException(ex);
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
	}

	public String getQuantity()
	{
		return quantityProperty.get();
	}
	public void setQuantity(final String newVal)
	{
		quantityProperty.set(newVal);
	}
	/**
	 *	Wraps the Quantity value of the AfflictionEvent
	 *
	 *	@return	a writable property wrapping the quantity attribute
	 */
	public StringProperty quantityProperty()
	{
		return quantityProperty;
	}

	private void onQuantityChange(ObservableValue<? extends String> obs, String old, String nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onQuantityChange(): old={}, new={}", old, nval);
		if (explicitSave)
		{
			LOGGER.debug("onQuantityChange(): explicitSave");
			explicitBuilder.quantity(nval);
		}
		else
		{
			LOGGER.debug("onQuantityChange(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			try
			{
				baseItem = server.getAfflictionEventBuilder(baseItem).quantity(nval).save();
			} catch (GNDBException ex) {
				throw new GNDBRuntimeException(ex);
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
	}

	public String getSeverity()
	{
		return severityProperty.get();
	}
	public void setSeverity(final String newVal)
	{
		severityProperty.set(newVal);
	}
	/**
	 *	Wraps the Severity value of the AfflictionEvent
	 *
	 *	@return	a writable property wrapping the quantity attribute
	 */
	public StringProperty severityProperty()
	{
		return severityProperty;
	}

	private void onSeverityChange(ObservableValue<? extends String> obs, String old, String nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onSeverityChange(): old={}, new={}", old, nval);
		if (explicitSave)
		{
			LOGGER.debug("onSeverityChange(): explicitSave");
			explicitBuilder.severity(nval);
		}
		else
		{
			LOGGER.debug("onSeverityChange(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			try
			{
				baseItem = server.getAfflictionEventBuilder(baseItem).severity(nval).save();
			} catch (GNDBException ex) {
				throw new GNDBRuntimeException(ex);
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
	}

	public LocalDateTime getLastUpdated()
	{
		return lastUpdatedProperty.get();
	}
	/**
	*	Wraps the LastUpdated value of the AfflictionEvent
	*	Note that this value cannot be changed by the user
	*
	*	@return	a read-only property wrapping the lastUpdated attribute
	*/
	public ReadOnlyObjectProperty<LocalDateTime> lastUpdatedProperty()
	{
		return lastUpdatedProperty.getReadOnlyProperty();
	}

	public LocalDateTime getCreated()
	{
		return createdProperty.get();
	}
	/**
	*	Wraps the Created value of the AfflictionEvent
	*	Note that this value cannot be changed by the user
	*
	*	@return	a read-only property wrapping the created attribute
	*/
	public ReadOnlyObjectProperty<LocalDateTime> createdProperty()
	{
		return createdProperty.getReadOnlyProperty();
	}

	@Override
	public ObservableList<CommentBean> getComments()
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getComments()");

		return LOGGER.traceExit(log4jEntryMsg, beanCommentHandler.getComments());
	}	//	getComments()

	//	2.9.6
	@Override
	public ReadOnlyStringProperty commentTextProperty()
	{
//		return beanCommentHandler.commentTextProperty();
		commentTextProperty.set(beanCommentHandler.commentTextProperty().get());
		return commentTextProperty;
	}

	@Override
	public void addComment(final String text) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("addComment({})", text);
		if (text == null || text.isBlank()) return;	//	2.9.6

		beanCommentHandler.addComment(text);	//	2.9.6

		if (explicitSave)
		{
LOGGER.debug("addComment(): explicitSave");
		}
		else
		{	//	this cannot be a new instance of the parent AfflictionEvent
LOGGER.debug("addComment(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getAfflictionEventBuilder(baseItem).addComment(text).save();
			setValues();	//	2.9.6
		}
		LOGGER.traceExit(log4jEntryMsg);
	}	//	addComment()

	//	2.9.6
	@Override
	public void addComment(CommentBean comment) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("addComment(comment bean): comment: {}, text : {}", comment, comment==null ? "null" :comment.getComment());
		if (comment == null) return;
		if (comment.getParentType() != this.getType()) return;
		if (comment.getComment() == null || comment.getComment().isBlank()) return;

		beanCommentHandler.addComment(comment);

		if (explicitSave)
		{
			LOGGER.debug("addComment(): explicitSave");
		}
		else
		{	//	this cannot be a new instance of the parent AfflictionEvent
			LOGGER.debug("addComment(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getAfflictionEventBuilder(baseItem).addComment(comment.getComment()).save();
			setValues();
		}
		LOGGER.debug("addComment(comment bean): commentTextProperty: {}", commentTextProperty().get());

		LOGGER.traceExit(log4jEntryMsg);
	}

	@Override
	public void changeCommentText(final CommentBean comment, final String text) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("changeCommentText(): comment={}, text={}", comment, text);
		if (text == null || text.isBlank()) return;

		if (comment == null)	//	2.9.6
		{
			addComment(text);
			return;
		}

		if (comment.getParentType() != this.getType()) return;

		beanCommentHandler.changeCommentText(comment, text);

		if (explicitSave)
		{
LOGGER.debug("changeCommentText(): explicitSave");
		}
		else
		{	//	this cannot be a new instance of the parent AfflictionEvent
LOGGER.debug("changeCommentText(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getAfflictionEventBuilder(baseItem).changeComment(comment.get().get(), text).save();
			setValues();	//	2.9.6
		}
		LOGGER.traceExit(log4jEntryMsg);
	}	//	changeCommentText()

	@Override
	public void changeCommentDate(CommentBean comment, final LocalDate date) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("changeCommentDate(): comment={}, date={}", comment, date);
		if (date == null) return;

		//	2.9.6
		if (comment == null)
		{
			return;
		}

		if (comment.getParentType() != this.getType()) return;

		beanCommentHandler.changeCommentDate(comment, date);

		if (explicitSave)
		{
LOGGER.debug("changeCommentDate(): explicitSave");
		}
		else
		{	//	this cannot be a new instance of the parent AfflictionEvent
LOGGER.debug("changeCommentDate(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getAfflictionEventBuilder(baseItem).changeComment(comment.get().get(), date).save();
			setValues();	//	2.9.6
		}
		LOGGER.traceExit(log4jEntryMsg);
	}	//	changeCommentDate()

	@Override
	public void deleteComment(CommentBean comment) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("deleteComment(): comment={}", comment);
		if (comment == null) return;

		if (comment.getParentType() != this.getType()) return;

		beanCommentHandler.deleteComment(comment);

		if (explicitSave)
		{
LOGGER.debug("deleteComment(): explicitSave");
		}
		else
		{	//	this cannot be a new instance of the parent AfflictionEvent
LOGGER.debug("deleteComment(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getAfflictionEventBuilder(baseItem).deleteComment(comment.get().get()).save();
			setValues();	//	2.9.6
		}
		LOGGER.traceExit(log4jEntryMsg);
	}	//	deleteComment()

	public boolean isSaveRequired()
	{
		return explicitSave;
	}
	public void setSaveRequired(boolean reqd)
	{
		saveRequiredProperty.set(reqd);
	}
	public BooleanProperty saveRequiredProperty()
	{
		return saveRequiredProperty;
	}

	public boolean needSave()
	{
		if (!explicitSave)
			return false;

		return explicitBuilder.needSave() || beanCommentHandler.needSave();
	}

	public boolean canSave()
	{
		if (!explicitSave)
			return true;

		return explicitBuilder.canSave();
	}

	/**
	*	Save changes to the underlying AfflictionEvent item
	*
	*	@throws	GNDBException	if the underlying persisted storage engine (e.g. database server) throws an exception
	*				The original error can be retrieved by <code>getCause()</code>
	*/
	public void save() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("save()");
		if (!explicitSave) return;

		saveComments();	//	2.9.6 - do this here so that explicitBuilder knows there's a change

		if (!explicitBuilder.needSave())
		{
			return;
		}
		if (!explicitBuilder.canSave())
		{
			throw new IllegalStateException("AfflictionEventBean: cannot save at this time - mandatory values not set");
		}

		baseItem = explicitBuilder.save();
		LOGGER.debug("save(): after explicitBuilder.save(): comments: {}", baseItem.getComments());	//	2.9.6
		setValues();	//	2.9.6
		saveRequiredProperty.set(false);
		LOGGER.traceExit(log4jEntryMsg);
	}	//	save()

	//	2.9.6
	private void saveComments()
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("saveComments()");

		beanCommentHandler.saveComments(	cb -> explicitBuilder.addComment(cb.getComment()),	//	add
				cb -> explicitBuilder.changeComment(cb.get().get(), cb.getComment()),	//	change text
				cb -> explicitBuilder.changeComment(cb.get().get(), cb.getDate()),	//	change date
				cb -> explicitBuilder.deleteComment(cb.get().get())		//	delete
		);
	}

	/**
	*	Delete the underlying AfflictionEvent item
	*
	*	@throws	GNDBException	if the underlying persisted storage engine (e.g. database server) throws an exception
	*				The original error can be retrieved by <code>getCause()</code>
	*/
	public void delete() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("delete()");
		if (newItem) return;

		if (explicitSave)
		{
			explicitBuilder.delete();
			saveRequiredProperty.set(false);
		}
		else
		{
			ITrug server = TrugServer.getTrugServer().getTrug();
			server.getAfflictionEventBuilder(baseItem).delete();
		}
	}	//	delete()

	public void cancelEdit()
	{
		if (!explicitSave) return;
		if (newItem) return;

		saveRequiredProperty.set(false);
		setValues();
	}

	private void setDefaults()
	{
		saveRequiredProperty.setValue(false);
		dateProperty.setValue(LocalDate.now());
		lastUpdatedProperty.setValue(LocalDateTime.now());
		createdProperty.setValue(LocalDateTime.now());


		//	2.9.6
		isNewProperty.set(true);
		beanCommentHandler = new BeanCommentHandler<>(this, baseItem);
		commentTextProperty.set(beanCommentHandler.commentTextProperty().get());
	}

	private void setValues()
	{
		saveRequiredProperty.setValue(false);
		parentAfflictionProperty.setValue(new AfflictionBean(baseItem.getAffliction()));
		if (baseItem.getPlantSpecies().isPresent())
		{
			hasParentPlantSpeciesProperty.set(true);
			parentPlantSpeciesProperty.setValue(new PlantSpeciesBean(baseItem.getPlantSpecies().get()));
		}
		else
		{
			hasParentPlantSpeciesProperty.set(false);
			parentPlantSpeciesProperty.setValue(null);
		}
		if (baseItem.getPlantVariety().isPresent())
		{
			hasParentPlantVarietyProperty.set(true);
			parentPlantVarietyProperty.setValue(new PlantVarietyBean(baseItem.getPlantVariety().get()));
		}
		else
		{
			hasParentPlantVarietyProperty.set(false);
			parentPlantVarietyProperty.setValue(null);
		}

		if (baseItem.getLocation().isPresent())
		{
			hasParentLocationProperty.set(true);
			parentLocationProperty.setValue(new LocationBean(baseItem.getLocation().get()));
		}
		else
		{
			hasParentLocationProperty.set(false);
			parentLocationProperty.setValue(null);
		}

		dateProperty.setValue(baseItem.getDate());
		quantityProperty.setValue(baseItem.getQuantity().orElse(""));
		severityProperty.setValue(baseItem.getSeverity().orElse(""));
		lastUpdatedProperty.setValue(baseItem.getLastUpdated());
		createdProperty.setValue(baseItem.getCreated());
//		setCommentValues();	//	2.9.6

		itemKey = baseItem.getKey();
		newItem = false;
		isNewProperty.set(false);	//	2.9.6

		LOGGER.debug("setvalues(): about to change BeanCommentHandler");
		beanCommentHandler = new BeanCommentHandler<>(this, baseItem);
		commentTextProperty.set(beanCommentHandler.commentTextProperty().get());
	}

	private void addListeners()
	{
		parentAfflictionProperty.addListener(afflictionIdListener);
		parentPlantSpeciesProperty.addListener(plantSpeciesIdListener);
		parentPlantVarietyProperty.addListener(plantVarietyIdListener);
		parentLocationProperty.addListener(locationIdListener);
		dateProperty.addListener(dateListener);
		quantityProperty.addListener(quantityListener);
		severityProperty.addListener(severityListener);
	}
	private void removeListeners()
	{
		parentAfflictionProperty.removeListener(afflictionIdListener);
		parentPlantSpeciesProperty.removeListener(plantSpeciesIdListener);
		parentPlantVarietyProperty.removeListener(plantVarietyIdListener);
		parentLocationProperty.removeListener(locationIdListener);
		dateProperty.removeListener(dateListener);
		quantityProperty.removeListener(quantityListener);
		severityProperty.removeListener(severityListener);
	}
	private void declareBaseListeners()
	{
		// handle changes to the base item itself
		baseItemDeleted = evt -> {
				removeListeners();
				removeBaseListeners();
				setDefaults();
				baseItem = null;
			};
		baseItemReplaced = evt -> {
				if (evt.getNewValue() != null)
				{
					removeBaseListeners();
					baseItem = (IAfflictionEvent)(evt.getNewValue());
					setValues();
					addBaseListeners();
				}
			};

	}
	private void addBaseListeners()
	{
        if (baseItem == null) return;
		baseItem.addPropertyChangeListener("deleted", baseItemDeleted);
		baseItem.addPropertyChangeListener("replaced", baseItemReplaced);

	}
	private void removeBaseListeners()
	{
        if (baseItem == null) return;
		baseItem.removePropertyChangeListener("deleted", baseItemDeleted);
		baseItem.removePropertyChangeListener("replaced", baseItemReplaced);

	}

	@Override
	public String toString()
	{
		return "AfflictionEventBean wrapping " + baseItem;
	}

}

