/*
 * Copyright (C) 2018-2023 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.0   Better handling for duplicate names
    2.2.5   Guard against occasional NPE on item delete removing base listeners
    2.9.6	When a Diary entry is added/changed, make sure updated comments are shown
    3.0.0	Fix bug whereby ALL extant PlantNotes were added to a new PlantSpecies
    		Add CropRotationGroup handling
    3.0.4	Comment handling
    3.1.0	Make sure new PVs are shown.
    3.1.1	Add mechanism to just load all PSs
 */

package uk.co.gardennotebook.fxbean;

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

import java.util.Objects;
import java.util.Optional;
import java.util.List;
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.*;

/**
	*	A species of plant grown in the garden, for instance, 'tomato'.  A species will have zero or more varieties.
 *
 * @apiNote
Note the use of a Trigger to reflect changes to commonName into Product.
	*
	*	@author	Andy Gegg
	*	@version	3.1.1
	*	@since	1.0
*/
final public class PlantSpeciesBean implements INotebookBean
{
	private static final Logger LOGGER = LogManager.getLogger();

	private IPlantSpecies baseItem = null;

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

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

	private final SimpleObjectProperty<CropRotationGroupBean> parentCropRotationGroupProperty = new SimpleObjectProperty<>(this, "cropRotationGroup", null);
	private final ChangeListener<CropRotationGroupBean> cropRotationGroupIdListener = this::onCropRotationGroupIdChange;
	private final ReadOnlyBooleanWrapper hasParentCropRotationGroupProperty = new ReadOnlyBooleanWrapper(this, "hasCropRotationGroup", false);

	/*
	*	The name by which plants of this type are usually known.
	*/
	private final SimpleStringProperty commonNameProperty = new SimpleStringProperty(this, "commonName", "");
	private final ChangeListener<String> commonNameListener = this::onCommonNameChange;
    private final ReadOnlyBooleanWrapper duplicateCommonNameProperty = new ReadOnlyBooleanWrapper(this, "duplicateCommonName", false);  //  2.2.0

	/*
	*	The formal horticultural name for this species.
The common name is language specific, the latin name is internationally standardised.
	*/
	private final SimpleStringProperty latinNameProperty = new SimpleStringProperty(this, "latinName", "");
	private final ChangeListener<String> latinNameListener = this::onLatinNameChange;
	private final SimpleStringProperty descriptionProperty = new SimpleStringProperty(this, "description", "");
	private final ChangeListener<String> descriptionListener = this::onDescriptionChange;

	/*
	*	The plant's function in the garden, typically vegetable, ornamental, weed.
	*/
	private final SimpleStringProperty utilityProperty = new SimpleStringProperty(this, "utility", "");
	private final ChangeListener<String> utilityListener = this::onUtilityChange;

	/*
	*	The plant's hardiness or tenderness, typically hardy, half hardy, tender.
	*/
	private final SimpleStringProperty hardinessProperty = new SimpleStringProperty(this, "hardiness", "");
	private final ChangeListener<String> hardinessListener = this::onHardinessChange;

	/*
	*	Typically annual, biennial or perennial.  Variations such as 'perennial grown as annual' (e.g. runner bean).
	*/
	private final SimpleStringProperty lifeTypeProperty = new SimpleStringProperty(this, "lifeType", "");
	private final ChangeListener<String> lifeTypeListener = this::onLifeTypeChange;

	/*
	*	Such as climber, shrub, tree.
	*/
	private final SimpleStringProperty plantTypeProperty = new SimpleStringProperty(this, "plantType", "");
	private final ChangeListener<String> plantTypeListener = this::onPlantTypeChange;
	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 final ReadOnlyBooleanWrapper hasAncestorProperty = new ReadOnlyBooleanWrapper(this, "hasAncestor", false);
	private final ReadOnlyBooleanWrapper hasDescendantProperty = new ReadOnlyBooleanWrapper(this, "hasDescendant", false);

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

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

	private ObservableList<AfflictionEventBean> childrenAfflictionEvent = null;
	private PropertyChangeListener baseItemAfflictionEventChanged;

	private ObservableList<GroundworkBean> childrenGroundwork = null;
	private PropertyChangeListener baseItemGroundworkChanged;

	private ObservableList<HusbandryBean> childrenHusbandry = null;
	private PropertyChangeListener baseItemHusbandryChanged;

	private ObservableList<PlantNoteBean> childrenPlantNote = null;
	private PropertyChangeListener baseItemPlantNoteChanged;

	private ObservableList<PlantVarietyBean> childrenPlantVariety = null;
	private PropertyChangeListener baseItemPlantVarietyChanged;

	private ObservableList<ProductBean> childrenProduct = null;
	private PropertyChangeListener baseItemProductChanged;

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

	private ObservableList<SaleItemBean> childrenSaleItem = null;
	private PropertyChangeListener baseItemSaleItemChanged;

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

	private ObservableList<CroppingActualBean> childrenCroppingActual = null;
	private PropertyChangeListener baseItemCroppingActualChanged;

	/**
	*	Construct an 'empty' Bean.  Set the various property values then call save() to create the new PlantSpeciesBean
	*/
	public PlantSpeciesBean()
	{
		this(null);
	}
	/**
	*	Construct a Bean wrapping the given PlantSpecies
	*	If the parameter is null a new 'empty' Bean will be constructed
	*
	*	@param	initialValue	the PlantSpecies to wrap.  If null an 'empty' bean will be constructed
	*
	*/
	public PlantSpeciesBean(final IPlantSpecies initialValue)
	{
		LOGGER.debug("constructor: {}", initialValue);
		ChangeListener<Boolean> saveRequiredListener = (obs, old, nval) -> {
			if (nval && !explicitSave)
			{
				explicitSave = true;
				ITrug server = TrugServer.getTrugServer().getTrug();
				explicitBuilder = server.getPlantSpeciesBuilder(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 all PlantSpecies items wrapped as PlantSpeciesBean.
	*
	*	@return	a collection of PlantSpeciesBean beans
	*
	*	@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 static ObservableList<PlantSpeciesBean> fetchAll() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("fetchAll()");
		ITrug server = TrugServer.getTrugServer().getTrug();
		IPlantSpeciesLister gal = server.getPlantSpeciesLister();
		List<PlantSpeciesBean> ll = gal.fetch().stream().map(PlantSpeciesBean::new).toList();
		LOGGER.traceExit();
		return FXCollections.observableArrayList(ll);
	}

	/**
	 *	Loads all PlantSpecies items.
	 *
	 *	@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 3.1.1
	 */
	public static void forceLoad() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("forceLoad()");
		ITrug server = TrugServer.getTrugServer().getTrug();
		IPlantSpeciesLister gal = server.getPlantSpeciesLister();
		gal.fetch();
		LOGGER.traceExit();
		return;
	}

	/**
	 *	Returns PlantSpecies in a given CropRotationGroup wrapped as PlantSpeciesBean.
	 *
	 *	@return	a collection of PlantSpeciesBean beans
	 *
	 *	@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 static ObservableList<PlantSpeciesBean> fetchByCropRotationGroup(CropRotationGroupBean cropRotationGroupBean) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("fetchByCropRotationGroup(): cropRotationGroupBean: {}", cropRotationGroupBean);

		Objects.requireNonNull(cropRotationGroupBean);

		ITrug server = TrugServer.getTrugServer().getTrug();
		IPlantSpeciesLister gal = server.getPlantSpeciesLister().
				cropRotationGroup(cropRotationGroupBean.get().orElseThrow(()->new IllegalArgumentException("PlantSpeciesBean: fetchByCropRotationGroup(): null CRG")));
		List<PlantSpeciesBean> ll = gal.fetch().stream().map(PlantSpeciesBean::new).toList();
		LOGGER.traceExit();
		return FXCollections.observableArrayList(ll);
	}

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

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

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

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

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

	@Override
	public boolean hasAncestor() throws GNDBException
	{
		//	PlantSpecies items do not participate in story lines
		return false;
	}

	@Override
	public ReadOnlyBooleanProperty hasAncestorProperty() throws GNDBException
	{
		//	PlantSpecies items do not participate in story lines
		return hasAncestorProperty.getReadOnlyProperty();
	}	//	hasAncestorProperty()

	@Override
	public StoryLineTree<? extends INotebookBean> getAncestors() throws GNDBException
	{
		//	PlantSpecies items do not participate in story lines
			return StoryLineTree.emptyTree();
	}	//	getAncestors()

	@Override
	public boolean hasDescendant() throws GNDBException
	{
		//	PlantSpecies items do not participate in story lines
		return false;
	}

	@Override
	public ReadOnlyBooleanProperty hasDescendantProperty() throws GNDBException
	{
		//	PlantSpecies items do not participate in story lines
		return hasDescendantProperty.getReadOnlyProperty();
	}	//	hasDescendantProperty()

	@Override
	public StoryLineTree<? extends INotebookBean> getDescendants() throws GNDBException
	{
		//	PlantSpecies items do not participate in story lines
			return StoryLineTree.emptyTree();
	}	//	getDescendants()

	public boolean hasCropRotationGroup()
	{
		return hasCropRotationGroupProperty().getValue();
	}
	/**
	 *	Use this to check if the CropRotationGroup parent of the PlantSpecies of this Bean wraps is present
	 *
	 *	@return	true if this PlantSpecies is linked to a CropRotationGroup
	 */
	public ReadOnlyBooleanProperty hasCropRotationGroupProperty()
	{
		return hasParentCropRotationGroupProperty.getReadOnlyProperty();
	}
	public CropRotationGroupBean getCropRotationGroup()
	{
		return cropRotationGroupProperty().getValue();
	}
	public void setCropRotationGroup(final CropRotationGroupBean bean)
	{
		cropRotationGroupProperty().setValue(bean);
	}
	public void setCropRotationGroup(final ICropRotationGroup item)
	{
		cropRotationGroupProperty().setValue(new CropRotationGroupBean(item));
	}

	/**
	 * Returns the CropRotationGroup this PlantSPecies belongs to.
	 *
	 * @return	the CropRotationGroup parent of this PlantSpecies
	 */
	public ObjectProperty<CropRotationGroupBean> cropRotationGroupProperty()
	{
		return parentCropRotationGroupProperty;
	}

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

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

		LOGGER.traceExit(log4jEntryMsg);
	}

	public String getCommonName()
	{
		return commonNameProperty.get();
	}
	public void setCommonName(final String newVal)
	{
		commonNameProperty.set(newVal);
	}
	/**
	*	Wraps the CommonName value of the PlantSpecies
	*
	*	@return	a writable property wrapping the commonName attribute
	*/
	public StringProperty commonNameProperty()
	{
		return commonNameProperty;
	}

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

    /**
     * Use this to check if the common name being applied is a duplicate of an existing name.
     * NB relies on calling {@code checkForDuplicateCommonName} on focusLost.
     * 
     * @return a read-only indication that the common name being set is a duplicate of an existing name
     * 
     * @since 2.2.0
     */
    public ReadOnlyBooleanProperty duplicateCommonNameProperty()
    {
        return duplicateCommonNameProperty.getReadOnlyProperty();
    }

    /**
     *  Must be called from the editor when the user has finished entering a new  common name value,
     * typically on a lost focus event.
     * 
     * @param newVal    the common name the user is attempting to give
     * @return  true if newVal duplicates an existing common name
     * 
     * @since 2.2.0
     */    
    public boolean checkForDuplicateCommonName(final String newVal)
    {
        boolean duplicate = false;
    
		if (explicitSave)
		{
LOGGER.debug("checkForDuplicateCommonName(): explicitSave");
			try
			{
				duplicate = explicitBuilder.isCommonNameDuplicate(newVal);
			} catch (GNDBException ex) {
				throw new GNDBRuntimeException(ex);
			}
		}
		else
		{
LOGGER.debug("checkForDuplicateCommonName(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			try
			{
				duplicate = server.getPlantSpeciesBuilder(baseItem).isCommonNameDuplicate(newVal);
			} catch (GNDBException ex) {
				throw new GNDBRuntimeException(ex);
			}
        }
        duplicateCommonNameProperty.set(duplicate);
        return duplicate;
    }

	public String getLatinName()
	{
		return latinNameProperty.get();
	}
	public void setLatinName(final String newVal)
	{
		latinNameProperty.set(newVal);
	}
	/**
	*	Wraps the LatinName value of the PlantSpecies
	*
	*	@return	a writable property wrapping the latinName attribute
	*/
	public StringProperty latinNameProperty()
	{
		return latinNameProperty;
	}

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

	public String getDescription()
	{
		return descriptionProperty.get();
	}
	public void setDescription(final String newVal)
	{
		descriptionProperty.set(newVal);
	}
	/**
	*	Wraps the Description value of the PlantSpecies
	*
	*	@return	a writable property wrapping the description attribute
	*/
	public StringProperty descriptionProperty()
	{
		return descriptionProperty;
	}

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

	public String getUtility()
	{
		return utilityProperty.get();
	}
	public void setUtility(final String newVal)
	{
		utilityProperty.set(newVal);
	}
	/**
	*	Wraps the Utility value of the PlantSpecies
	*
	*	@return	a writable property wrapping the utility attribute
	*/
	public StringProperty utilityProperty()
	{
		return utilityProperty;
	}

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

	public String getHardiness()
	{
		return hardinessProperty.get();
	}
	public void setHardiness(final String newVal)
	{
		hardinessProperty.set(newVal);
	}
	/**
	*	Wraps the Hardiness value of the PlantSpecies
	*
	*	@return	a writable property wrapping the hardiness attribute
	*/
	public StringProperty hardinessProperty()
	{
		return hardinessProperty;
	}

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

	public String getLifeType()
	{
		return lifeTypeProperty.get();
	}
	public void setLifeType(final String newVal)
	{
		lifeTypeProperty.set(newVal);
	}
	/**
	*	Wraps the LifeType value of the PlantSpecies
	*
	*	@return	a writable property wrapping the lifeType attribute
	*/
	public StringProperty lifeTypeProperty()
	{
		return lifeTypeProperty;
	}

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

	public String getPlantType()
	{
		return plantTypeProperty.get();
	}
	public void setPlantType(final String newVal)
	{
		plantTypeProperty.set(newVal);
	}
	/**
	*	Wraps the PlantType value of the PlantSpecies
	*
	*	@return	a writable property wrapping the plantType attribute
	*/
	public StringProperty plantTypeProperty()
	{
		return plantTypeProperty;
	}

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

	public LocalDateTime getLastUpdated()
	{
		return lastUpdatedProperty.get();
	}
	/**
	*	Wraps the LastUpdated value of the PlantSpecies
	*	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 PlantSpecies
	*	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 AfflictionEvent of this PlantSpecies or an empty list
	*
	*	@return	A list of AfflictionEvent 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<AfflictionEventBean> getAfflictionEvent() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getAfflictionEvent()");
		if (childrenAfflictionEvent == null)
		{
			childrenAfflictionEvent = FXCollections.observableArrayList();
			ITrug server = TrugServer.getTrugServer().getTrug();
			for (IAfflictionEvent ix : server.getAfflictionEventLister().plantSpecies(baseItem).fetch())
			{
				childrenAfflictionEvent.add(new AfflictionEventBean(ix));
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
		return childrenAfflictionEvent;
	}

	/**
	*	Return a list of any Groundwork of this PlantSpecies or an empty list
	*
	*	@return	A list of Groundwork 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<GroundworkBean> getGroundwork() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getGroundwork()");
		if (childrenGroundwork == null)
		{
			childrenGroundwork = FXCollections.observableArrayList();
			ITrug server = TrugServer.getTrugServer().getTrug();
			for (IGroundwork ix : server.getGroundworkLister().plantSpecies(baseItem).fetch())
			{
				childrenGroundwork.add(new GroundworkBean(ix));
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
		return childrenGroundwork;
	}

	/**
	*	Return a list of any Husbandry of this PlantSpecies or an empty list
	*
	*	@return	A list of Husbandry 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<HusbandryBean> getHusbandry() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getHusbandry()");
		if (childrenHusbandry == null)
		{
			childrenHusbandry = FXCollections.observableArrayList();
			ITrug server = TrugServer.getTrugServer().getTrug();
			for (IHusbandry ix : server.getHusbandryLister().plantSpecies(baseItem).fetch())
			{
				childrenHusbandry.add(new HusbandryBean(ix));
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
		return childrenHusbandry;
	}

	/**
	*	Return a list of any PlantNote of this PlantSpecies or an empty list
	*
	*	@return	A list of PlantNote 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<PlantNoteBean> getPlantNote() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getPlantNote()");
		if (childrenPlantNote == null)
		{
			childrenPlantNote = FXCollections.observableArrayList();
			//	3.0.0	if baseItem is null, this will retrieve ALL extant plantNotes
			if (baseItem != null)
			{
				ITrug server = TrugServer.getTrugServer().getTrug();
				for (IPlantNote ix : server.getPlantNoteLister().plantSpecies(baseItem).fetch())
				{
					childrenPlantNote.add(new PlantNoteBean(ix));
				}
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
		return childrenPlantNote;
	}

	/**
	*	Return a list of any PlantVariety of this PlantSpecies or an empty list
	*
	*	@return	A list of PlantVariety 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<PlantVarietyBean> getPlantVariety() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getPlantVariety()");
		if (childrenPlantVariety == null)
		{
			childrenPlantVariety = FXCollections.observableArrayList();
			ITrug server = TrugServer.getTrugServer().getTrug();
			for (IPlantVariety ix : server.getPlantVarietyLister().plantSpecies(baseItem).fetch())
			{
				childrenPlantVariety.add(new PlantVarietyBean(ix));
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
		return childrenPlantVariety;
	}

	/**
	*	Return a list of any Product of this PlantSpecies or an empty list
	*
	*	@return	A list of Product 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<ProductBean> getProduct() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getProduct()");
		if (childrenProduct == null)
		{
			childrenProduct = FXCollections.observableArrayList();
			ITrug server = TrugServer.getTrugServer().getTrug();
			for (IProduct ix : server.getProductLister().plantSpecies(baseItem).fetch())
			{
				childrenProduct.add(new ProductBean(ix));
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
		return childrenProduct;
	}

	/**
	*	Return a list of any Reminder of this PlantSpecies 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()");
		if (childrenReminder == null)
		{
			childrenReminder = FXCollections.observableArrayList();
			ITrug server = TrugServer.getTrugServer().getTrug();
			for (IReminder ix : server.getReminderLister().plantSpecies(baseItem).fetch())
			{
				childrenReminder.add(new ReminderBean(ix));
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
		return childrenReminder;
	}

	/**
	*	Return a list of any SaleItem of this PlantSpecies or an empty list
	*
	*	@return	A list of SaleItem 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<SaleItemBean> getSaleItem() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getSaleItem()");
		if (childrenSaleItem == null)
		{
			childrenSaleItem = FXCollections.observableArrayList();
			ITrug server = TrugServer.getTrugServer().getTrug();
			for (ISaleItem ix : server.getSaleItemLister().plantSpecies(baseItem).fetch())
			{
				childrenSaleItem.add(new SaleItemBean(ix));
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
		return childrenSaleItem;
	}

	/**
	*	Return a list of any ToDoList of this PlantSpecies 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()");
		if (childrenToDoList == null)
		{
			childrenToDoList = FXCollections.observableArrayList();
			ITrug server = TrugServer.getTrugServer().getTrug();
			for (IToDoList ix : server.getToDoListLister().plantSpecies(baseItem).fetch())
			{
				childrenToDoList.add(new ToDoListBean(ix));
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
		return childrenToDoList;
	}

	/**
	 *	Return a list of any Reviews of this PlantSpecies or an empty list
	 *
	 *	@return	A list of Review 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<ReviewBean> getReview() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getReview()");
		//	TODO	get list of Reviews naming this PlantSpecies
//		if (childrenToDoList == null)
//		{
//			childrenToDoList = FXCollections.observableArrayList();
//			ITrug server = TrugServer.getTrugServer().getTrug();
//			for (IToDoList ix : server.getToDoListLister().plantSpecies(baseItem).fetch())
//			{
//				childrenToDoList.add(new ToDoListBean(ix));
//			}
//		}
		LOGGER.traceExit(log4jEntryMsg);
		return FXCollections.emptyObservableList();
	}

	/**
	 *	Return a list of any CroppingActual items of this PlantSpecies or an empty list
	 *
	 *	@return	A list of CroppingActual 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<CroppingActualBean> getCroppingActual() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getCroppingActual()");
		if (childrenCroppingActual == null)
		{
			childrenCroppingActual = FXCollections.observableArrayList();
			ITrug server = TrugServer.getTrugServer().getTrug();
			for (ICroppingActual ix : server.getCroppingActualLister().plantSpecies(baseItem).fetch())
			{
				childrenCroppingActual.add(new CroppingActualBean(ix));
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
		return childrenCroppingActual;
	}

	@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 PlantSpecies
LOGGER.debug("addComment(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getPlantSpeciesBuilder(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 Wildlife
			LOGGER.debug("addComment(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getPlantSpeciesBuilder(baseItem).addComment(comment.getComment()).save();
			setValues();	//	2.9.6
		}
		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 PlantSpecies
LOGGER.debug("changeCommentText(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getPlantSpeciesBuilder(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 PlantSpecies
LOGGER.debug("changeCommentDate(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getPlantSpeciesBuilder(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 PlantSpecies
LOGGER.debug("deleteComment(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getPlantSpeciesBuilder(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 PlantSpecies 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())
		{
			childrenPlantVariety = null;
			return;
		}
		if (!explicitBuilder.canSave())
		{
			throw new IllegalStateException("PlantSpeciesBean: 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
		childrenPlantVariety = null;
		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 PlantSpecies 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.getPlantSpeciesBuilder(baseItem).delete();
		}
	}	//	delete()

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

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

	private void setDefaults()
	{
		saveRequiredProperty.setValue(false);
		parentCropRotationGroupProperty.setValue(null);
		hasParentCropRotationGroupProperty.set(false);
		commonNameProperty.setValue("");
		latinNameProperty.setValue("");
		descriptionProperty.setValue("");
		utilityProperty.setValue("");
		hardinessProperty.setValue("");
		lifeTypeProperty.setValue("");
		plantTypeProperty.setValue("");
		lastUpdatedProperty.setValue(LocalDateTime.now());
		createdProperty.setValue(LocalDateTime.now());
		childrenAfflictionEvent = null;
		baseItemAfflictionEventChanged = null;
		childrenGroundwork = null;
		baseItemGroundworkChanged = null;
		childrenHusbandry = null;
		baseItemHusbandryChanged = null;
		childrenPlantNote = null;
		baseItemPlantNoteChanged = null;
		childrenPlantVariety = null;
		baseItemPlantVarietyChanged = null;
		childrenProduct = null;
		baseItemProductChanged = null;
		childrenReminder = null;
		baseItemReminderChanged = null;
		childrenSaleItem = null;
		baseItemSaleItemChanged = null;
		childrenToDoList = null;
		baseItemToDoListChanged = null;

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

	private void setValues()
	{
		LOGGER.debug("setValues()");
		saveRequiredProperty.setValue(false);
		if (baseItem.getCropRotationGroup().isPresent())
		{
//			LOGGER.debug("crg is present");
			hasParentCropRotationGroupProperty.set(true);
//			LOGGER.debug("after has...");
			parentCropRotationGroupProperty.setValue(new CropRotationGroupBean(baseItem.getCropRotationGroup().get()));
//			LOGGER.debug("after parent...");
		}
		else
		{
			hasParentCropRotationGroupProperty.set(false);
			parentCropRotationGroupProperty.setValue(null);
		}
		LOGGER.debug("setValues: hasParentCropRotationGroupProperty: {}, parentCropRotationGroupProperty: {}", hasParentCropRotationGroupProperty.get(),parentCropRotationGroupProperty.get());
		commonNameProperty.setValue(baseItem.getCommonName());
		latinNameProperty.setValue(baseItem.getLatinName().orElse(""));
		descriptionProperty.setValue(baseItem.getDescription().orElse(""));
		utilityProperty.setValue(baseItem.getUtility().orElse(""));
		hardinessProperty.setValue(baseItem.getHardiness().orElse(""));
		lifeTypeProperty.setValue(baseItem.getLifeType().orElse(""));
		plantTypeProperty.setValue(baseItem.getPlantType().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: {}", baseItem);
		beanCommentHandler = new BeanCommentHandler<>(this, baseItem);
		commentTextProperty.set(beanCommentHandler.commentTextProperty().get());
	}

	private void addListeners()
	{
		commonNameProperty.addListener(commonNameListener);
		latinNameProperty.addListener(latinNameListener);
		descriptionProperty.addListener(descriptionListener);
		utilityProperty.addListener(utilityListener);
		hardinessProperty.addListener(hardinessListener);
		lifeTypeProperty.addListener(lifeTypeListener);
		plantTypeProperty.addListener(plantTypeListener);
		parentCropRotationGroupProperty.addListener(cropRotationGroupIdListener);
	}
	private void removeListeners()
	{
		commonNameProperty.removeListener(commonNameListener);
		latinNameProperty.removeListener(latinNameListener);
		descriptionProperty.removeListener(descriptionListener);
		utilityProperty.removeListener(utilityListener);
		hardinessProperty.removeListener(hardinessListener);
		lifeTypeProperty.removeListener(lifeTypeListener);
		plantTypeProperty.removeListener(plantTypeListener);
		parentCropRotationGroupProperty.removeListener(cropRotationGroupIdListener);
	}
	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 = (IPlantSpecies)(evt.getNewValue());
					setValues();
					addBaseListeners();
				}
			};

		baseItemAfflictionEventChanged = evt -> {
				if (childrenAfflictionEvent == null)
				{
					return;
				}
				if (evt.getNewValue() != null)
				{
					if (!(evt.getNewValue() instanceof IAfflictionEvent))
					{
						throw new IllegalArgumentException("baseItemAfflictionEventChanged: newVal wrong type");
					}
					childrenAfflictionEvent.add(new AfflictionEventBean((IAfflictionEvent)(evt.getNewValue())));
				}
				else if (evt.getOldValue() != null)
				{
					if (!(evt.getOldValue() instanceof IAfflictionEvent))
					{
						throw new IllegalArgumentException("baseItemAfflictionEventChanged: 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
					childrenAfflictionEvent.removeIf(pr -> (pr.getValue().isEmpty()) ||
						(pr.getValue().
							get().
							getKey().
							equals(((IAfflictionEvent)(evt.getOldValue())).getKey())));
				}
			};

		baseItemGroundworkChanged = evt -> {
				if (childrenGroundwork == null)
				{
					return;
				}
				if (evt.getNewValue() != null)
				{
					if (!(evt.getNewValue() instanceof IGroundwork))
					{
						throw new IllegalArgumentException("baseItemGroundworkChanged: newVal wrong type");
					}
					childrenGroundwork.add(new GroundworkBean((IGroundwork)(evt.getNewValue())));
				}
				else if (evt.getOldValue() != null)
				{
					if (!(evt.getOldValue() instanceof IGroundwork))
					{
						throw new IllegalArgumentException("baseItemGroundworkChanged: 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
					childrenGroundwork.removeIf(pr -> (pr.getValue().isEmpty()) ||
						(pr.getValue().
							get().
							getKey().
							equals(((IGroundwork)(evt.getOldValue())).getKey())));
				}
			};

		baseItemHusbandryChanged = evt -> {
				if (childrenHusbandry == null)
				{
					return;
				}
				if (evt.getNewValue() != null)
				{
					if (!(evt.getNewValue() instanceof IHusbandry))
					{
						throw new IllegalArgumentException("baseItemHusbandryChanged: newVal wrong type");
					}
					childrenHusbandry.add(new HusbandryBean((IHusbandry)(evt.getNewValue())));
				}
				else if (evt.getOldValue() != null)
				{
					if (!(evt.getOldValue() instanceof IHusbandry))
					{
						throw new IllegalArgumentException("baseItemHusbandryChanged: 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
					childrenHusbandry.removeIf(pr -> (pr.getValue().isEmpty()) ||
						(pr.getValue().
							get().
							getKey().
							equals(((IHusbandry)(evt.getOldValue())).getKey())));
				}
			};

		baseItemPlantNoteChanged = evt -> {
				if (childrenPlantNote == null)
				{
					return;
				}
				if (evt.getNewValue() != null)
				{
					if (!(evt.getNewValue() instanceof IPlantNote))
					{
						throw new IllegalArgumentException("baseItemPlantNoteChanged: newVal wrong type");
					}
					childrenPlantNote.add(new PlantNoteBean((IPlantNote)(evt.getNewValue())));
				}
				else if (evt.getOldValue() != null)
				{
					if (!(evt.getOldValue() instanceof IPlantNote))
					{
						throw new IllegalArgumentException("baseItemPlantNoteChanged: 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
					childrenPlantNote.removeIf(pr -> (pr.getValue().isEmpty()) ||
						(pr.getValue().
							get().
							getKey().
							equals(((IPlantNote)(evt.getOldValue())).getKey())));
				}
			};

		baseItemPlantVarietyChanged = evt -> {
				LOGGER.debug("baseItemPlantVarietyChanged: childrenPlantVariety: {}", childrenPlantVariety);
				if (childrenPlantVariety == null)
				{
					return;
				}
				if (evt.getNewValue() != null)
				{
					LOGGER.debug("add new value: {}", evt.getNewValue());
					if (!(evt.getNewValue() instanceof IPlantVariety))
					{
						throw new IllegalArgumentException("baseItemPlantVarietyChanged: newVal wrong type");
					}
					childrenPlantVariety.add(new PlantVarietyBean((IPlantVariety)(evt.getNewValue())));
				}
				else if (evt.getOldValue() != null)
				{
					if (!(evt.getOldValue() instanceof IPlantVariety))
					{
						throw new IllegalArgumentException("baseItemPlantVarietyChanged: 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
					childrenPlantVariety.removeIf(pr -> (pr.getValue().isEmpty()) ||
						(pr.getValue().
							get().
							getKey().
							equals(((IPlantVariety)(evt.getOldValue())).getKey())));
				}
			};

		baseItemProductChanged = evt -> {
				if (childrenProduct == null)
				{
					return;
				}
				if (evt.getNewValue() != null)
				{
					if (!(evt.getNewValue() instanceof IProduct))
					{
						throw new IllegalArgumentException("baseItemProductChanged: newVal wrong type");
					}
					childrenProduct.add(new ProductBean((IProduct)(evt.getNewValue())));
				}
				else if (evt.getOldValue() != null)
				{
					if (!(evt.getOldValue() instanceof IProduct))
					{
						throw new IllegalArgumentException("baseItemProductChanged: 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
					childrenProduct.removeIf(pr -> (pr.getValue().isEmpty()) ||
						(pr.getValue().
							get().
							getKey().
							equals(((IProduct)(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())));
				}
			};

		baseItemSaleItemChanged = evt -> {
				if (childrenSaleItem == null)
				{
					return;
				}
				if (evt.getNewValue() != null)
				{
					if (!(evt.getNewValue() instanceof ISaleItem))
					{
						throw new IllegalArgumentException("baseItemSaleItemChanged: newVal wrong type");
					}
					childrenSaleItem.add(new SaleItemBean((ISaleItem)(evt.getNewValue())));
				}
				else if (evt.getOldValue() != null)
				{
					if (!(evt.getOldValue() instanceof ISaleItem))
					{
						throw new IllegalArgumentException("baseItemSaleItemChanged: 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
					childrenSaleItem.removeIf(pr -> (pr.getValue().isEmpty()) ||
						(pr.getValue().
							get().
							getKey().
							equals(((ISaleItem)(evt.getOldValue())).getKey())));
				}
			};

		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())));
				}
			};

		baseItemCroppingActualChanged = evt -> {
			if (childrenCroppingActual == null)
			{
				return;
			}
			if (evt.getNewValue() != null)
			{
				if (!(evt.getNewValue() instanceof ICroppingActual))
				{
					throw new IllegalArgumentException("baseItemCroppingActualChanged: newVal wrong type");
				}
				childrenCroppingActual.add(new CroppingActualBean((ICroppingActual)(evt.getNewValue())));
			}
			else if (evt.getOldValue() != null)
			{
				if (!(evt.getOldValue() instanceof ICroppingActual))
				{
					throw new IllegalArgumentException("baseItemCroppingActualChanged: 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
				childrenCroppingActual.removeIf(pr -> (pr.getValue().isEmpty()) ||
						(pr.getValue().
								get().
								getKey().
								equals(((ICroppingActual)(evt.getOldValue())).getKey())));
			}
		};

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

		baseItem.addPropertyChangeListener("AfflictionEvent", baseItemAfflictionEventChanged);

		baseItem.addPropertyChangeListener("Groundwork", baseItemGroundworkChanged);

		baseItem.addPropertyChangeListener("Husbandry", baseItemHusbandryChanged);

		baseItem.addPropertyChangeListener("PlantNote", baseItemPlantNoteChanged);

		baseItem.addPropertyChangeListener("PlantVariety", baseItemPlantVarietyChanged);

		baseItem.addPropertyChangeListener("Product", baseItemProductChanged);

		baseItem.addPropertyChangeListener("Reminder", baseItemReminderChanged);

		baseItem.addPropertyChangeListener("SaleItem", baseItemSaleItemChanged);

		baseItem.addPropertyChangeListener("ToDoList", baseItemToDoListChanged);

		baseItem.addPropertyChangeListener("CroppingActual", baseItemCroppingActualChanged);

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

		baseItem.removePropertyChangeListener("AfflictionEvent", baseItemAfflictionEventChanged);

		baseItem.removePropertyChangeListener("Groundwork", baseItemGroundworkChanged);

		baseItem.removePropertyChangeListener("Husbandry", baseItemHusbandryChanged);

		baseItem.removePropertyChangeListener("PlantNote", baseItemPlantNoteChanged);

		baseItem.removePropertyChangeListener("PlantVariety", baseItemPlantVarietyChanged);

		baseItem.removePropertyChangeListener("Product", baseItemProductChanged);

		baseItem.removePropertyChangeListener("Reminder", baseItemReminderChanged);

		baseItem.removePropertyChangeListener("SaleItem", baseItemSaleItemChanged);

		baseItem.removePropertyChangeListener("ToDoList", baseItemToDoListChanged);

		baseItem.removePropertyChangeListener("CroppingActual", baseItemCroppingActualChanged);

		//	TODO	handle Review
	}

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

}

