/*
 * 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 field.
    3.0.1	Add 'watch for' handling so that the plant species/variety cannot be changed if there's a 'watch for'.
    		'Watch for' handling moved here rather than in the editor.
    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.util.Optional;
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 java.time.*;

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

/**
	*	Diary entries for events in the life of a crop.
	*
	*	@author	Andy Gegg
	*	@version	3.0.4
	*	@since	1.0
*/
final public class HusbandryBean implements INotebookBean
{
	private static final Logger LOGGER = LogManager.getLogger();

	private IHusbandry baseItem = null;

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

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

	private final SimpleObjectProperty<HusbandryClassBean> parentHusbandryClassProperty = new SimpleObjectProperty<>(this, "husbandryClass", null);
	private final ChangeListener<HusbandryClassBean> husbandryClassIdListener = this::onHusbandryClassIdChange;

	/*
	*	Always required as Husbandry always refers to a plant.
The activity is for plants of this species.
@apiNote
plantVarietyId may or may not be given; if absent the activity is for all (current) e.g. tomato varieties, e.g for spraying.
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;

	/*
	*	The activity is for 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);

	/*
	*	If this Diary entry is for the demise of a plant, this can be used to record the guilty pest or disease.
	*/
	private final SimpleObjectProperty<AfflictionBean> parentAfflictionProperty = new SimpleObjectProperty<>(this, "affliction", null);
	private final ChangeListener<AfflictionBean> terminalAfflictionIdListener = this::onTerminalAfflictionIdChange;
	private final ReadOnlyBooleanWrapper hasParentAfflictionProperty = new ReadOnlyBooleanWrapper(this, "hasAffliction", 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);

	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 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<IHusbandry> beanCommentHandler;	//	2.9.6
	private final ReadOnlyStringWrapper commentTextProperty = new ReadOnlyStringWrapper(this, "commentText", "");	//	2.9.6

	private ObservableList<ToDoListBean> childrenToDoList = null;
	private PropertyChangeListener baseItemToDoListChanged;

	private ObservableList<ReminderBean> childrenReminder = null;
	private PropertyChangeListener baseItemReminderChanged;

	private ReadOnlyBooleanWrapper hasWatchForProperty = null;
	private final ReadOnlyObjectWrapper<HusbandryClassBean> watchForHusbandryClassProperty = new ReadOnlyObjectWrapper<>(this, "watchForHusbandryClass", null);
	private final ReadOnlyObjectWrapper<LocalDate> watchForDateProperty = new ReadOnlyObjectWrapper<>(this, "watchForDate", null);

	/**
	*	Construct an 'empty' Bean.  Set the various property values then call save() to create the new HusbandryBean
	*/
	public HusbandryBean()
	{
		this(null);
	}
	/**
	*	Construct a Bean wrapping the given Husbandry
	*	If the parameter is null a new 'empty' Bean will be constructed
	*
	*	@param	initialValue	the Husbandry to wrap.  If null an 'empty' bean will be constructed
	*
	*/
	public HusbandryBean(final IHusbandry initialValue)
	{
		ChangeListener<Boolean> saveRequiredListener = (obs, old, nval) -> {
			if (nval && !explicitSave)
			{
				explicitSave = true;
				ITrug server = TrugServer.getTrugServer().getTrug();
				explicitBuilder = server.getHusbandryBuilder(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 Husbandry, if present
	*
	*	@return	the underlying Husbandry, if present
	*/
	public Optional<IHusbandry> get()
	{
		return getValue();
	}

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

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

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

	@Override
	public boolean sameAs(final INotebookBean other)
	{
		if (other == null || ((HusbandryBean)other).baseItem == null || baseItem == null)
		{
			return false;
		}
		if (other.getType() != NotebookEntryType.HUSBANDRY)
		{
			return false;
		}
		return baseItem.sameAs(((HusbandryBean)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.getHusbandryBuilder(baseItem).canDelete();
			canDeleteProperty = new ReadOnlyBooleanWrapper(this, "canDelete", canDel);
		}
		return canDeleteProperty.getReadOnlyProperty();
	}

	/**
	 * Check if the item has a 'watch for' event set up.
	 * If there is such a 'watch for' the plant species/variety must not be changed
	 * @return	true if a watch for event has been set up
	 * @throws GNDBException - if the underlying persisted storage engine (e.g. database server) throws an exception.
	 * 							The original error can be retrieved by getCause()
	 *
	 * @since 3.0.1
	 */
	public boolean hasWatchFor() throws GNDBException
	{
		return hasWatchForProperty().get();
	}

	/**
	 * Check if the item has a 'watch for' event set up.
	 * If there is such a watch for the plant species/variety must not be changed
	 * @return	true if a watch for event has been set up
	 * @throws GNDBException - if the underlying persisted storage engine (e.g. database server) throws an exception.
	 * 							The original error can be retrieved by getCause()
	 *
	 * @since 3.0.1
	 */
	public ReadOnlyBooleanProperty hasWatchForProperty() throws GNDBException
	{
		if (hasWatchForProperty == null)
		{
			ITrug server = TrugServer.getTrugServer().getTrug();
			getReminder();
			getToDoList();
			boolean gotOne = (!childrenReminder.isEmpty() || !childrenToDoList.isEmpty());
			hasWatchForProperty = new ReadOnlyBooleanWrapper(this, "hasWatchFor", gotOne);
		}
		return hasWatchForProperty.getReadOnlyProperty();
	}	//	hasWatchForProperty()

	/**
	 * If there is a 'watch for' event, return the associated Husbandry Class
	 *
 	 * @return	the HusbandryCLassBean if there is a watch for, else null
	 * @throws GNDBException - if the underlying persisted storage engine (e.g. database server) throws an exception.
	 *							The original error can be retrieved by getCause()
	 *
	 * @since 3.0.1
	 */
	public HusbandryClassBean getWatchForHusbandryClass() throws GNDBException
	{
		return watchForHusbandryClassProperty().get();
	}

	public 	ReadOnlyObjectProperty<HusbandryClassBean> watchForHusbandryClassProperty() throws GNDBException
	{
		if (hasWatchFor())
		{
			if (!childrenToDoList.isEmpty())
			{
				watchForHusbandryClassProperty.setValue(childrenToDoList.get(0).getHusbandryClass());
			}
			else if (!childrenReminder.isEmpty())
			{
				watchForHusbandryClassProperty.setValue(childrenReminder.get(0).getHusbandryClass());
			}
		}
		//	the property is initialised with a null value
		return watchForHusbandryClassProperty.getReadOnlyProperty();
	}

	/**
	 * If there is a 'watch for' event, return the associated date (which may be in the past)
	 *
	 * @return	the LocalDate if there is a watch for, else null
	 * @throws GNDBException - if the underlying persisted storage engine (e.g. database server) throws an exception.
	 *							The original error can be retrieved by getCause()
	 *
	 * @since 3.0.1
	 */
	public LocalDate getWatchForDate() throws GNDBException
	{
		return watchForDateProperty().get();
	}

	/**
	 * Get the 'watch for' property.
	 * This is a read-only property.
	 *
	 * @return	The read only 'watch for' property
	 * @throws GNDBException - if the underlying persisted storage engine (e.g. database server) throws an exception.
	 *							The original error can be retrieved by getCause()
	 *
	 * @since 3.0.1
	 */
	public 	ReadOnlyObjectProperty<LocalDate> watchForDateProperty() throws GNDBException
	{
		if (hasWatchFor())
		{
			if (!childrenToDoList.isEmpty())
			{
				watchForDateProperty.setValue(childrenToDoList.get(0).getHusbandry().getDate());
			}
			else if (!childrenReminder.isEmpty())
			{
				watchForDateProperty.setValue(childrenReminder.get(0).getShowFrom());
			}
		}
		//	the property is initialised with a null value
		return watchForDateProperty.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.getHusbandryBuilder(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();
		}
        if (!hasAncestor())
		{
			return StoryLineTree.emptyTree();
		}
		ITrug server = TrugServer.getTrugServer().getTrug();
		StoryLineTree<? extends INotebookEntry> tree = server.getHusbandryBuilder(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 Husbandry 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.getHusbandryBuilder(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 Husbandry 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.getHusbandryBuilder(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 Husbandry 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.getHusbandryBuilder(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 Husbandry 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.getHusbandryBuilder(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.getHusbandryBuilder(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.getHusbandryBuilder(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.getHusbandryBuilder(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 HusbandryClassBean getHusbandryClass()
	{
		return husbandryClassProperty().getValue();
	}
	public void setHusbandryClass(final HusbandryClassBean bean)
	{
		husbandryClassProperty().setValue(bean);
	}
	public void setHusbandryClass(final IHusbandryClass item)
	{
		husbandryClassProperty().setValue(new HusbandryClassBean(item));
	}
	/**
	*	Returns the HusbandryClass parent of the Husbandry this Bean wraps
	*
	*	@return	the HusbandryClass parent of the Husbandry this Bean wraps
	*/
	public ObjectProperty<HusbandryClassBean> husbandryClassProperty()
	{
		return parentHusbandryClassProperty;
	}

	/**
	*	Handle changes to the HusbandryClassId 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 onHusbandryClassIdChange(ObservableValue<? extends HusbandryClassBean> obs, HusbandryClassBean old, HusbandryClassBean nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onHusbandryClassIdChange(): old={}, new={}", old, nval);
		if (nval == null)
		{	// this is an error condition and should be flagged
LOGGER.debug("onHusbandryClassIdChange(): nval is null");
			return;
		}
		if (nval.sameAs(old))
		{
LOGGER.debug("onHusbandryClassIdChange(): nval is sameAs old");
			return;
		}
		if (!nval.isNew())
		{
			if (explicitSave)
			{
				explicitBuilder.husbandryClass(nval.get().get());
			}
			else
			{
LOGGER.debug("onHusbandryClassIdChange(): NOT explicitSave");
				ITrug server = TrugServer.getTrugServer().getTrug();
				//	the Builder will send an event to the baseItem to say it's been replaced
				try
				{
					server.getHusbandryBuilder(baseItem).husbandryClass(nval.get().get()).save();
				} catch (GNDBException ex) {
					throw new GNDBRuntimeException(ex);
				}
			}
		}

		LOGGER.traceExit(log4jEntryMsg);
	}


	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 Husbandry this Bean wraps
	*
	*	@return	the PlantSpecies parent of the Husbandry 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)
		{	// this is an error condition and should be flagged
LOGGER.debug("onPlantSpeciesIdChange(): nval is null");
			return;
		}
		if (nval.sameAs(old))
		{
LOGGER.debug("onPlantSpeciesIdChange(): nval is sameAs old");
			return;
		}
		if (!nval.isNew())
		{
			if (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.getHusbandryBuilder(baseItem).plantSpecies(nval.get().get()).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 Husbandry this Bean wraps is present
	*
	*	@return	true if this Husbandry 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 Husbandry this Bean wraps
	*	Call hasPlantVariety() first to check if this value is set
	*
	*	@return	the PlantVariety parent of the Husbandry 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.getHusbandryBuilder(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.getHusbandryBuilder(baseItem).plantVariety(null).save();
				} catch (GNDBException ex) {
					throw new GNDBRuntimeException(ex);
				}
			}
		}

		LOGGER.traceExit(log4jEntryMsg);
	}


	public boolean hasAffliction()
	{
		return hasAfflictionProperty().getValue();
	}
	/**
	*	Use this to check if the Affliction parent of the Husbandry this Bean wraps is present
	*
	*	@return	true if this Husbandry is linked to a Affliction
	*/
	public ReadOnlyBooleanProperty hasAfflictionProperty()
	{
		return hasParentAfflictionProperty.getReadOnlyProperty();
	}
	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 Husbandry this Bean wraps
	*	Call hasAffliction() first to check if this value is set
	*
	*	@return	the Affliction parent of the Husbandry this Bean wraps
	*/
	public ObjectProperty<AfflictionBean> afflictionProperty()
	{
		return parentAfflictionProperty;
	}

	/**
	*	Handle changes to the TerminalAfflictionId 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 onTerminalAfflictionIdChange(ObservableValue<? extends AfflictionBean> obs, AfflictionBean old, AfflictionBean nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onTerminalAfflictionIdChange(): old={}, new={}", old, nval);
		if (nval != null && nval.sameAs(old))
		{
LOGGER.debug("onTerminalAfflictionIdChange(): nval is sameAs old");
			return;
		}
		hasParentAfflictionProperty.set(nval != null);

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

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

	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.getHusbandryBuilder(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.getHusbandryBuilder(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 Husbandry
	*
	*	@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.getHusbandryBuilder(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 Husbandry
	 *
	 *	@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.getHusbandryBuilder(baseItem).quantity(nval).save();
			} catch (GNDBException ex) {
				throw new GNDBRuntimeException(ex);
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
	}

	public LocalDateTime getLastUpdated()
	{
		return lastUpdatedProperty().get();
	}
	/**
	*	Wraps the LastUpdated value of the Husbandry
	*	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 Husbandry
	*	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();
	}

	/**
	*	Return a list of any ToDoList of this Husbandry or an empty list
	*
	*	@return	A list of ToDoList items, possibly empty
	*
	*	@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 ObservableList<ToDoListBean> getToDoList() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getToDoList(): childrenToDoList: {}", childrenToDoList);

		if (childrenToDoList == null)
		{
			childrenToDoList = FXCollections.observableArrayList();
			if (!newItem)
			{	//	a new bean will return all known ToDos...
				ITrug server = TrugServer.getTrugServer().getTrug();
				for (IToDoList ix : server.getToDoListLister().husbandry(baseItem).fetch())
				{
					childrenToDoList.add(new ToDoListBean(ix));
				}
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
		return childrenToDoList;
	}

	/**
	 *	Return a list of any Reminders of this Husbandry or an empty list
	 *
	 *	@return	A list of Reminder items, possibly empty
	 *
	 *	@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 ObservableList<ReminderBean> getReminder() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getReminder(): childrenReminder: {}", childrenReminder);
		if (childrenReminder == null)
		{
			childrenReminder = FXCollections.observableArrayList();
			if (!newItem)
			{	//	a new bean will return all known Reminders
				ITrug server = TrugServer.getTrugServer().getTrug();
				for (IReminder ix : server.getReminderLister().husbandry(baseItem).fetch())
				{
					childrenReminder.add(new ReminderBean(ix));
				}
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
		return childrenReminder;
	}

	@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 Husbandry
LOGGER.debug("addComment(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getHusbandryBuilder(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 Husbandry item
			LOGGER.debug("addComment(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getHusbandryBuilder(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;	//	2.9.6

		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 Husbandry
LOGGER.debug("changeCommentText(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getHusbandryBuilder(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 Husbandry
LOGGER.debug("changeCommentDate(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getHusbandryBuilder(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 Husbandry
LOGGER.debug("deleteComment(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getHusbandryBuilder(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 Husbandry 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())
		{
			LOGGER.debug("save not needed");
			return;
		}
		if (!explicitBuilder.canSave())
		{
			throw new IllegalStateException("HusbandryBean: 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);
		childrenReminder = null;
		childrenToDoList = null;
		hasWatchForProperty = null;
		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 Husbandry 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.getHusbandryBuilder(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());
		quantityProperty.setValue("");
		lastUpdatedProperty.setValue(LocalDateTime.now());
		createdProperty.setValue(LocalDateTime.now());
		childrenToDoList = null;
		childrenReminder = null;
		baseItemToDoListChanged = null;
		baseItemReminderChanged = null;

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

	private void setValues()
	{
		saveRequiredProperty.setValue(false);
		parentHusbandryClassProperty.setValue(new HusbandryClassBean(baseItem.getHusbandryClass()));
		parentPlantSpeciesProperty.setValue(new PlantSpeciesBean(baseItem.getPlantSpecies()));
		if (baseItem.getPlantVariety().isPresent())
		{
			hasParentPlantVarietyProperty.set(true);
			parentPlantVarietyProperty.setValue(new PlantVarietyBean(baseItem.getPlantVariety().get()));
		}
		else
		{
			hasParentPlantVarietyProperty.set(false);
			parentPlantVarietyProperty.setValue(null);
		}
		if (baseItem.getAffliction().isPresent())
		{
			hasParentAfflictionProperty.set(true);
			parentAfflictionProperty.setValue(new AfflictionBean(baseItem.getAffliction().get()));
		}
		else
		{
			hasParentAfflictionProperty.set(false);
			parentAfflictionProperty.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(""));
		lastUpdatedProperty.setValue(baseItem.getLastUpdated());
		createdProperty.setValue(baseItem.getCreated());

		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()
	{
		parentHusbandryClassProperty.addListener(husbandryClassIdListener);
		parentPlantSpeciesProperty.addListener(plantSpeciesIdListener);
		parentPlantVarietyProperty.addListener(plantVarietyIdListener);
		parentAfflictionProperty.addListener(terminalAfflictionIdListener);
		parentLocationProperty.addListener(locationIdListener);
		dateProperty.addListener(dateListener);
		quantityProperty.addListener(quantityListener);
	}

	private void removeListeners()
	{
		parentHusbandryClassProperty.removeListener(husbandryClassIdListener);
		parentPlantSpeciesProperty.removeListener(plantSpeciesIdListener);
		parentPlantVarietyProperty.removeListener(plantVarietyIdListener);
		parentAfflictionProperty.removeListener(terminalAfflictionIdListener);
		parentLocationProperty.removeListener(locationIdListener);
		dateProperty.removeListener(dateListener);
		quantityProperty.removeListener(quantityListener);
	}

	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 = (IHusbandry)(evt.getNewValue());
					setValues();
					addBaseListeners();
				}
			};

		baseItemToDoListChanged = evt -> {
				if (childrenToDoList == null)
				{
					return;
				}
				if (evt.getNewValue() != null)
				{
					if (!(evt.getNewValue() instanceof IToDoList))
					{
						throw new IllegalArgumentException("baseItemToDoListChanged: newVal wrong type");
					}
					childrenToDoList.add(new ToDoListBean((IToDoList)(evt.getNewValue())));
				}
				else if (evt.getOldValue() != null)
				{
					if (!(evt.getOldValue() instanceof IToDoList))
					{
						throw new IllegalArgumentException("baseItemToDoListChanged: oldVal wrong type");
					}
					//	When the db item is deleted it fires an event which is picked up here AND in the child bean
					//	The child bean sets its underlying baseItem to null so getValue() returns an Optional of null
					//	The order in which the event handlers are called is unpredictable
					childrenToDoList.removeIf(pr -> (pr.getValue().isEmpty()) ||
						(pr.getValue().
							get().
							getKey().
							equals(((IToDoList)(evt.getOldValue())).getKey())));
				}
			};

		baseItemReminderChanged = evt -> {
			if (childrenReminder == null)
			{
				return;
			}
			if (evt.getNewValue() != null)
			{
				if (!(evt.getNewValue() instanceof IReminder))
				{
					throw new IllegalArgumentException("baseItemReminderChanged: newVal wrong type");
				}
				childrenReminder.add(new ReminderBean((IReminder) (evt.getNewValue())));
			}
			else if (evt.getOldValue() != null)
			{
				if (!(evt.getOldValue() instanceof IReminder))
				{
					throw new IllegalArgumentException("baseItemReminderChanged: oldVal wrong type");
				}
				//	When the db item is deleted it fires an event which is picked up here AND in the child bean
				//	The child bean sets its underlying baseItem to null so getValue() returns an Optional of null
				//	The order in which the event handlers are called is unpredictable
				childrenReminder.removeIf(pr -> (pr.getValue().isEmpty()) ||
						(pr.getValue().
								get().
								getKey().
								equals(((IReminder)(evt.getOldValue())).getKey())));
			}
		};

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

		baseItem.addPropertyChangeListener("ToDoList", baseItemToDoListChanged);
		baseItem.addPropertyChangeListener("Reminder", baseItemReminderChanged);

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

		baseItem.removePropertyChangeListener("ToDoList", baseItemToDoListChanged);
		baseItem.removePropertyChangeListener("Reminder", baseItemReminderChanged);

	}

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

}

