/*
 * Copyright (C) 2018-2022 Andrew Gegg
 *
 *	This file is part of the Gardeners Notebook application
 *
 * The Gardeners 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.0.1   Bug fix
            Better handling of key fields during product creation
    2.2.5   Guard against occasional NPE on item delete removing base listeners
    2.8.0   Support ShoppingList and PurchaseItem product editing.
    2.9.6	When a Diary entry is added/changed, make sure updated comments are shown
    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.util.List;
import java.util.ArrayList;
import java.beans.PropertyChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.ChangeListener;

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

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

import java.time.*;

/**
	*	A purchasable product
	*
	*	@author	Andy Gegg
	*	@version	3.0.4
	*	@since	1.0
*/
final public class ProductBean implements INotebookBean
{
	private static final Logger LOGGER = LogManager.getLogger();

	private IProduct baseItem = null;

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

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

	private final SimpleObjectProperty<ProductCategoryBean> parentProductCategoryProperty = new SimpleObjectProperty<>(this, "productCategory", null);
	private final ChangeListener<ProductCategoryBean> productCategoryIdListener = this::onProductCategoryIdChange;

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

	/*
	*	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);
	private final SimpleObjectProperty<ProductBrandBean> parentProductBrandProperty = new SimpleObjectProperty<>(this, "productBrand", null);
	private final ChangeListener<ProductBrandBean> productBrandIdListener = this::onProductBrandIdChange;
	private final ReadOnlyBooleanWrapper hasParentProductBrandProperty = new ReadOnlyBooleanWrapper(this, "hasProductBrand", false);

	/*
	*	For plant like products (seeds, etc) the plant species common name
	*/
	private final SimpleStringProperty nameProperty = new SimpleStringProperty(this, "name", "");
	private final ChangeListener<String> nameListener = this::onNameChange;

	/*
	*	For plant-like products (seeds, etc) the plant variety (if present) common name
	*/
	private final SimpleStringProperty nameDetail_1Property = new SimpleStringProperty(this, "nameDetail_1", "");
	private final ChangeListener<String> nameDetail_1Listener = this::onNameDetail_1Change;
	private final SimpleStringProperty nameDetail_2Property = new SimpleStringProperty(this, "nameDetail_2", "");
	private final ChangeListener<String> nameDetail_2Listener = this::onNameDetail_2Change;
	private final SimpleStringProperty descriptionProperty = new SimpleStringProperty(this, "description", "");
	private final ChangeListener<String> descriptionListener = this::onDescriptionChange;
	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<IProduct> beanCommentHandler;	//	2.9.6
	private final ReadOnlyStringWrapper commentTextProperty = new ReadOnlyStringWrapper(this, "commentText", "");

	private ObservableList<PurchaseItemBean> childrenPurchaseItem = null;
	private PropertyChangeListener baseItemPurchaseItemChanged;

	private ObservableList<RetailerHasProductBean> childrenRetailerHasProduct = null;
	private PropertyChangeListener baseItemRetailerHasProductChanged;

	private ObservableList<ShoppingListBean> childrenShoppingList = null;
	private PropertyChangeListener baseItemShoppingListChanged;

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

		saveRequiredProperty.addListener(saveRequiredListener);

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

		baseItem = initialValue;

		itemKey = baseItem.getKey();

		newItem = false;
		setValues();

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

	/**
	*	returns all Product items wrapped as ProductBeans
	*
	*	@return	a collection of ProductBean 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<ProductBean> fetchAll() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("fetchAll(): no param");
		ITrug server = TrugServer.getTrugServer().getTrug();
		IProductLister gal = server.getProductLister();
		List<ProductBean> ll = gal.fetch().stream()
					.collect(ArrayList::new, (c, e) -> c.add(new ProductBean(e)), ArrayList::addAll);
        
        //  2.1.0   DB returns items ordered on ProductCategoryId but we really want them in ProductCategory NAME order
        ll.sort((b1, b2) -> b1.parentProductCategoryProperty.get().getName().compareToIgnoreCase(b2.parentProductCategoryProperty.get().getName()));
		LOGGER.traceExit(log4jEntryMsg);
		return FXCollections.observableArrayList(ll);
	}

	/**
	*	Returns all Product items for a ProductCategory wrapped as ProductBeans.
    *   This is typically used to populate product tree-views for the given category.
	*
	*	@param	category	fetch all Products in this ProductCategory
	* 
	*	@return	a collection of ProductBean 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<ProductBean> fetchAll(final ProductCategoryBean category) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("fetchAll(): parent={}", category);
		if (category == null) return fetchAll();
        if (category.get().isEmpty()) return fetchAll();
		
		ITrug server = TrugServer.getTrugServer().getTrug();
		IProductLister gal = server.getProductLister().productCategory(category.get().get());
		List<ProductBean> ll = gal.fetch().stream()
					.collect(ArrayList::new, (c, e) -> c.add(new ProductBean(e)), ArrayList::addAll);
		LOGGER.traceExit(log4jEntryMsg);
		return FXCollections.observableArrayList(ll);
	}

	/**
	*	Returns all Product items for a ProductCategory with this Brand wrapped as ProductBeans.
	*
	*	@param	category	fetch all Products in this ProductCategory
	*	@param	brand	fetch all Products in this ProductCategory with this ProductBrand
	* 
	*	@return	a collection of ProductBean 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<ProductBean> fetchAll(final ProductCategoryBean category, final ProductBrandBean brand) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("fetchAll(): parent={}, brand: {}", category, brand);
		if (category == null) return fetchAll();
		if (brand == null) return fetchAll(category);
		
		ITrug server = TrugServer.getTrugServer().getTrug();
		IProductLister gal = server.getProductLister().
			productCategory(category.get().get()).
			productBrand(brand.get().get());
		List<ProductBean> ll = gal.fetch().stream()
					.collect(ArrayList::new, (c, e) -> c.add(new ProductBean(e)), ArrayList::addAll);
		LOGGER.traceExit(log4jEntryMsg);
		return FXCollections.observableArrayList(ll);
	}

	/**
	*	Returns all Product items for a ProductCategory with this Brand, PlantSpecies and PlantVariety wrapped as ProductBeans
	*
	*	@param	category	fetch all Products in this ProductCategory
	*	@param	brand	fetch all Products in this ProductCategory with this ProductBrand
    *                       may be null in which case a null requirement will be forced.
    *   @param  plantSpecies fetch products for this PlantSpecies
    *                           may be null (in which case plantVariety will be ignored).
    *   @param  plantVariety    fetch Products matching this plantVariety
    *                               may be null in which case a null requirement will be forced.
	* 
	*	@return	a collection of ProductBean 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>
    * 
    *   @since   2.8.0
	*/
	public static ObservableList<ProductBean> fetchAll(final ProductCategoryBean category, final ProductBrandBean brand, final PlantSpeciesBean plantSpecies, final PlantVarietyBean plantVariety) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("fetchAll(): parent={}, brand: {}, plantSpecies: {}, plantVariety: {}", category, brand, plantSpecies, plantVariety);
		if (category == null) return fetchAll();
//		if (brand == null) return fetchAll(category);
		
		ITrug server = TrugServer.getTrugServer().getTrug();
		IProductLister gal = server.getProductLister().
			productCategory(category.get().get());
        if (brand == null)
            gal.productBrandRequireNull(true);
        else
            gal.productBrand(brand.get().get());
        
        if (category.isPlantLike() && plantSpecies != null)
        {
            gal.plantSpecies(plantSpecies.get().get());
            if (plantVariety == null)
                gal.plantVarietyRequireNull(true);
            else
                gal.plantVariety(plantVariety.get().get());
        }
		List<ProductBean> ll = gal.fetch().stream()
					.collect(ArrayList::new, (c, e) -> c.add(new ProductBean(e)), ArrayList::addAll);
		LOGGER.traceExit(log4jEntryMsg);
		return FXCollections.observableArrayList(ll);
	}

	/**
	*	Returns all Product items for a ProductCategory with this Brand, PlantSpecies and PlantVariety wrapped as ProductBeans
	*
	*	@param	category	fetch all Products in this ProductCategory
	*	@param	brand	fetch all Products in this ProductCategory with this ProductBrand
    *                       may be null in which case a null requirement will be forced.
    *   @param  name fetch products with this name property
    *                           may be null (in which case nameDetail_1 will be ignored).
    *   @param  nameDetail_1    fetch Products matching this nameDetail_1
    *                               may be null.
	* 
	*	@return	a collection of ProductBean 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>
    * 
    *   @since   2.8.0
	*/
	public static ObservableList<ProductBean> fetchAll(final ProductCategoryBean category, final ProductBrandBean brand, final String name, final String nameDetail_1) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("fetchAll(): parent={}, brand: {}, name: {}, detail: {}", category, brand, name, nameDetail_1);
		if (category == null) return fetchAll();
//		if (brand == null) return fetchAll(category);
		
		ITrug server = TrugServer.getTrugServer().getTrug();
		IProductLister gal = server.getProductLister().
			productCategory(category.get().get());
        if (brand == null)
            gal.productBrandRequireNull(true);
        else
            gal.productBrand(brand.get().get());
        if (!category.isPlantLike() && name != null)
        {
            gal.name(name, nameDetail_1, null);
        }
		List<ProductBean> ll = gal.fetch().stream()
					.collect(ArrayList::new, (c, e) -> c.add(new ProductBean(e)), ArrayList::addAll);
		LOGGER.traceExit(log4jEntryMsg);
		return FXCollections.observableArrayList(ll);
	}

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

	/**
	*	returns the underlying Product if present
	*
	*	@return	the underlying Product, if present
	*/
	public Optional<IProduct> getValue()
	{
		return Optional.ofNullable(baseItem);
	}

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

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

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

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

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

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

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

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

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

	public ProductCategoryBean getProductCategory()
	{
		return productCategoryProperty().getValue();
	}
	public void setProductCategory(ProductCategoryBean bean)
	{
		productCategoryProperty().setValue(bean);
	}
	public void setProductCategory(IProductCategory item)
	{
		productCategoryProperty().setValue(new ProductCategoryBean(item));
	}
	/**
	*	Returns the ProductCategory parent of the Product this Bean wraps
	*
	*	@return	the ProductCategory parent of the Product this Bean wraps
	*/
	public ObjectProperty<ProductCategoryBean> productCategoryProperty()
	{
		return parentProductCategoryProperty;
	}

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


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

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

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

		LOGGER.traceExit(log4jEntryMsg);
	}


	public boolean hasPlantVariety()
	{
		return hasPlantVarietyProperty().getValue();
	}
	/**
	*	Use this to check if the PlantVariety parent of the Product this Bean wraps is present
	*
	*	@return	true if this Product is linked to a PlantVariety
	*/
	public ReadOnlyBooleanProperty hasPlantVarietyProperty()
	{
		return hasParentPlantVarietyProperty.getReadOnlyProperty();
	}
	public PlantVarietyBean getPlantVariety()
	{
		return plantVarietyProperty().getValue();
	}
	public void setPlantVariety(PlantVarietyBean bean)
	{
		plantVarietyProperty().setValue(bean);
	}
	public void setPlantVariety(IPlantVariety item)
	{
		plantVarietyProperty().setValue(new PlantVarietyBean(item));
	}
	/**
	*	Returns the PlantVariety parent of the Product this Bean wraps
	*	Call hasPlantVariety() first to check if this value is set
	*
	*	@return	the PlantVariety parent of the Product 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.getProductBuilder(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.getProductBuilder(baseItem).plantVariety(null).save();
				} catch (GNDBException ex) {
					throw new GNDBRuntimeException(ex);
				}
			}
		}
		if (nval == null)
		{
			setNameDetail_1("");    //  2.0.1
		}
		else
		{
			setNameDetail_1(nval.getCommonName());
		}

		LOGGER.traceExit(log4jEntryMsg);
	}


	public boolean hasProductBrand()
	{
		return hasProductBrandProperty().getValue();
	}
	/**
	*	Use this to check if the ProductBrand parent of the Product this Bean wraps is present
	*
	*	@return	true if this Product is linked to a ProductBrand
	*/
	public ReadOnlyBooleanProperty hasProductBrandProperty()
	{
		return hasParentProductBrandProperty.getReadOnlyProperty();
	}
	public ProductBrandBean getProductBrand()
	{
		return productBrandProperty().getValue();
	}
	public void setProductBrand(ProductBrandBean bean)
	{
		productBrandProperty().setValue(bean);
	}
	public void setProductBrand(IProductBrand item)
	{
		productBrandProperty().setValue(new ProductBrandBean(item));
	}
	/**
	*	Returns the ProductBrand parent of the Product this Bean wraps
	*	Call hasProductBrand() first to check if this value is set
	*
	*	@return	the ProductBrand parent of the Product this Bean wraps
	*/
	public ObjectProperty<ProductBrandBean> productBrandProperty()
	{
		return parentProductBrandProperty;
	}

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

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


	public String getName()
	{
		return nameProperty.get();
	}
	public void setName(final String newVal)
	{
		nameProperty.set(newVal);
	}
	/**
	*	Wraps the Name value of the Product
	*
	*	@return	a writable property wrapping the name attribute
	*/
	public StringProperty nameProperty()
	{
		return nameProperty;
	}

	/**
	*	Handle changes to the Name 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 onNameChange(ObservableValue<? extends String> obs, String old, String nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onNameChange(): old={}, new={}", old, nval);
		if (explicitSave)
		{
LOGGER.debug("onNameChange(): explicitSave");
			explicitBuilder.name(nval);
		}
		else
		{
LOGGER.debug("onNameChange(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			try
			{
				baseItem = server.getProductBuilder(baseItem).name(nval).save();
			} catch (GNDBException ex) {
				throw new GNDBRuntimeException(ex);
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
	}
    
	public String getNameDetail_1()
	{
		return nameDetail_1Property.get();
	}
	public void setNameDetail_1(final String newVal)
	{
		nameDetail_1Property.set(newVal);
	}
	/**
	*	Wraps the NameDetail_1 value of the Product
	*
	*	@return	a writable property wrapping the nameDetail_1 attribute
	*/
	public StringProperty nameDetail_1Property()
	{
		return nameDetail_1Property;
	}

	/**
	*	Handle changes to the NameDetail_1 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 onNameDetail_1Change(ObservableValue<? extends String> obs, String old, String nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onNameDetail_1Change(): old={}, new={}", old, nval);
		if (explicitSave)
		{
LOGGER.debug("onNameDetail_1Change(): explicitSave");
			explicitBuilder.nameDetail_1(nval);
		}
		else
		{
LOGGER.debug("onNameDetail_1Change(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			try
			{
				baseItem = server.getProductBuilder(baseItem).nameDetail_1(nval).save();
			} catch (GNDBException ex) {
				throw new GNDBRuntimeException(ex);
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
	}

	public String getNameDetail_2()
	{
		return nameDetail_2Property.get();
	}
	public void setNameDetail_2(final String newVal)
	{
		nameDetail_2Property.set(newVal);
	}
	/**
	*	Wraps the NameDetail_2 value of the Product
	*
	*	@return	a writable property wrapping the nameDetail_2 attribute
	*/
	public StringProperty nameDetail_2Property()
	{
		return nameDetail_2Property;
	}

	/**
	*	Handle changes to the NameDetail_2 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 onNameDetail_2Change(ObservableValue<? extends String> obs, String old, String nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onNameDetail_2Change(): old={}, new={}", old, nval);
		if (explicitSave)
		{
LOGGER.debug("onNameDetail_2Change(): explicitSave");
			explicitBuilder.nameDetail_2(nval);
		}
		else
		{
LOGGER.debug("onNameDetail_2Change(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			try
			{
				baseItem = server.getProductBuilder(baseItem).nameDetail_2(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 Product
	*
	*	@return	a writable property wrapping the description attribute
	*/
	public StringProperty descriptionProperty()
	{
		return descriptionProperty;
	}

	/**
	*	Handle changes to the NameDetail_2 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 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.getProductBuilder(baseItem).description(nval).save();
			} catch (GNDBException ex) {
				throw new GNDBRuntimeException(ex);
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
	}

	public LocalDateTime getLastUpdated()
	{
		return lastUpdatedProperty.get();
	}
	/**
	*	Wraps the LastUpdated value of the Product
	*	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 Product
	*	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 PurchaseItem of this Product or an empty list
	*
	*	@return	A list of PurchaseItem 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<PurchaseItemBean> getPurchaseItem() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getPurchaseItem()");
		if (childrenPurchaseItem == null)
		{
			childrenPurchaseItem = FXCollections.observableArrayList();
			ITrug server = TrugServer.getTrugServer().getTrug();
			for (IPurchaseItem ix : server.getPurchaseItemLister().product(baseItem).fetch())
			{
				childrenPurchaseItem.add(new PurchaseItemBean(ix));
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
		return childrenPurchaseItem;
	}

	/**
	*	Return a list of any RetailerHasProduct of this Product or an empty list
	*
	*	@return	A list of RetailerHasProduct 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<RetailerHasProductBean> getRetailerHasProduct() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getRetailerHasProduct()");
		if (childrenRetailerHasProduct == null)
		{
			childrenRetailerHasProduct = FXCollections.observableArrayList();
			ITrug server = TrugServer.getTrugServer().getTrug();
			for (IRetailerHasProduct ix : server.getRetailerHasProductLister().product(baseItem).fetch())
			{
				childrenRetailerHasProduct.add(new RetailerHasProductBean(ix));
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
		return childrenRetailerHasProduct;
	}

	/**
	*	Return a list of any ShoppingList of this Product or an empty list
	*
	*	@return	A list of ShoppingList 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<ShoppingListBean> getShoppingList() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getShoppingList()");
		if (childrenShoppingList == null)
		{
			childrenShoppingList = FXCollections.observableArrayList();
			ITrug server = TrugServer.getTrugServer().getTrug();
			for (IShoppingList ix : server.getShoppingListLister().product(baseItem).fetch())
			{
				childrenShoppingList.add(new ShoppingListBean(ix));
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
		return childrenShoppingList;
	}

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

		//	2.9.6
		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(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 Product
LOGGER.debug("addComment(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getProductBuilder(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.getProductBuilder(baseItem).addComment(comment.getComment()).save();
			setValues();	//	2.9.6
		}
		LOGGER.debug("addComment(comment bean): commentTextProperty: {}", ()->commentTextProperty().get());

		LOGGER.traceExit(log4jEntryMsg);
	}

	@Override
	public void changeCommentText(CommentBean comment, 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 Product
LOGGER.debug("changeCommentText(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getProductBuilder(baseItem).changeComment(comment.get().get(), text).save();
			setValues();	//	2.9.6
		}
		LOGGER.traceExit(log4jEntryMsg);
	}	//	changeCommentText()

	@Override
	public void changeCommentDate(CommentBean comment, 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 Product
LOGGER.debug("changeCommentDate(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getProductBuilder(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 Product
LOGGER.debug("deleteComment(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getProductBuilder(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 Product item
	*
	*	@throws	GNDBException	if the underlying persisted storage engine (e.g. database server) throws an exception
	*				The original error can be retrieved by <code>getCause()</code>
	*/
	public void save() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("save()");
		if (!explicitSave) return;

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

		if (!explicitBuilder.needSave())
		{
			return;
		}
		if (!explicitBuilder.canSave())
		{
			throw new IllegalStateException("ProductBean: cannot save at this time - mandatory values not set");
		}
		
		if (newItem)
		{
			ITrug server = TrugServer.getTrugServer().getTrug();
			IProductLister pil = server.getProductLister();
			pil.productCategory(getProductCategory().get().get());
			if (getProductBrand() != null && getProductBrand().get().isPresent())
			{
				pil.productBrand(getProductBrand().get().get());
			}
			else
			{
				pil.productBrandRequireNull(true);
			}
			if (getProductCategory().isPlantLike())
			{
				// NB the semantics of plant species & variety specification to the product lister
				// Like all listers the selection is species OR variety as variety imples the parent species
				// Therefore, here we only need to pass the variety (if present) or species if it's NOT
				if (getPlantVariety() != null && getPlantVariety().get().isPresent())
				{
					pil.plantVariety(getPlantVariety().get().get());
				}
				else if (getPlantSpecies() != null && getPlantSpecies().get().isPresent())
				{
					pil.plantSpecies(getPlantSpecies().get().get());
                    //  2.8.0
                    if (getPlantVariety() == null || getPlantVariety().get().isEmpty())
                    {
                        pil.plantVarietyRequireNull(true);
                    }
				}
				else
				{// illegal to save!
					LOGGER.debug("save(): plantLike but no plant species!");
					throw new IllegalStateException("ProductBean: cannot save at this time - plant like product needs a plant!");
				}
			}
			else
			{
				if ((getName() == null) || getName().isEmpty())
				{
					LOGGER.debug("save(): not plantLike but no name!");
					throw new IllegalStateException("ProductBean: cannot save at this time - not plant like product needs a name!");
				}
				String name = getName();
				String nameDetail_1 = getNameDetail_1();
				String nameDetail_2 = getNameDetail_2();
				pil.name(name, nameDetail_1, nameDetail_2);
			}
			// check if this product already exists
			List<IProduct> candidates = pil.fetch();
			LOGGER.debug("save(): candidate products: {}", candidates);
			if (candidates.isEmpty())
			{// it's a new one
				LOGGER.debug("save(): saving new product");
				baseItem = explicitBuilder.save();
			}
			else if (candidates.size() == 1)
			{
				LOGGER.debug("save(): matched single product");
				if (newItem)
				{
					baseItem = candidates.get(0);
					// do the set-up normally handled by the listener on flagReplaced()
//					System.out.println("save(): saveRequired: " + saveRequiredProperty.get());
					setValues();
//					System.out.println("save(): saveRequired: " + saveRequiredProperty.get());
					declareBaseListeners();
					addBaseListeners();
				}
				else
				{
					baseItem.flagReplaced(candidates.get(0));
				}
			}
			else
			{// multiple matches!
				LOGGER.debug("save(): new item: matches many existing products");
				throw new IllegalStateException("ProductBean: cannot save at this time - new item but matches many existing products!");
			}
			
			newItem = false;
            if (isNewProperty != null)  //  2.0.1
                isNewProperty.set(false);
//			System.out.println("save(): before changing saveRequiredProperty, value: " + saveRequiredProperty.get());
			saveRequiredProperty.set(false);
//			System.out.println("save(): saveRequired: " + saveRequiredProperty.get());
//			setValues();
			return;
		}

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

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

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

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

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

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

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

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

	private void setDefaults()
	{
		saveRequiredProperty.setValue(false);
		nameProperty.setValue("");
		nameDetail_1Property.setValue("");
		nameDetail_2Property.setValue("");
		descriptionProperty.setValue("");
		lastUpdatedProperty.setValue(LocalDateTime.now());
		createdProperty.setValue(LocalDateTime.now());
		childrenPurchaseItem = null;
		baseItemPurchaseItemChanged = null;
		childrenRetailerHasProduct = null;
		baseItemRetailerHasProductChanged = null;
		childrenShoppingList = null;
		baseItemShoppingListChanged = null;

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

	private void setValues()
	{
//		System.out.println("setValues(): before changing saveRequiredProperty");
		saveRequiredProperty.setValue(false);
		parentProductCategoryProperty.setValue(new ProductCategoryBean(baseItem.getProductCategory()));
		if (baseItem.getPlantSpecies().isPresent())
		{
			hasParentPlantSpeciesProperty.set(true);
			parentPlantSpeciesProperty.setValue(new PlantSpeciesBean(baseItem.getPlantSpecies().get()));
		}
		else
		{
			hasParentPlantSpeciesProperty.set(false);
			parentPlantSpeciesProperty.setValue(null);
		}
		if (baseItem.getPlantVariety().isPresent())
		{
			hasParentPlantVarietyProperty.set(true);
			parentPlantVarietyProperty.setValue(new PlantVarietyBean(baseItem.getPlantVariety().get()));
		}
		else
		{
			hasParentPlantVarietyProperty.set(false);
			parentPlantVarietyProperty.setValue(null);
		}
		if (baseItem.getProductBrand().isPresent())
		{
			hasParentProductBrandProperty.set(true);
			parentProductBrandProperty.setValue(new ProductBrandBean(baseItem.getProductBrand().get()));
		}
		else
		{
			hasParentProductBrandProperty.set(false);
			parentProductBrandProperty.setValue(null);
		}
		nameProperty.setValue(baseItem.getName());
		nameDetail_1Property.setValue(baseItem.getNameDetail_1().orElse(""));
		nameDetail_2Property.setValue(baseItem.getNameDetail_2().orElse(""));
		descriptionProperty.setValue(baseItem.getDescription().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()
	{
		parentProductCategoryProperty.addListener(productCategoryIdListener);
		parentPlantSpeciesProperty.addListener(plantSpeciesIdListener);
		parentPlantVarietyProperty.addListener(plantVarietyIdListener);
		parentProductBrandProperty.addListener(productBrandIdListener);
		nameProperty.addListener(nameListener);
		nameDetail_1Property.addListener(nameDetail_1Listener);
		nameDetail_2Property.addListener(nameDetail_2Listener);
		descriptionProperty.addListener(descriptionListener);
	}
	private void removeListeners()
	{
		parentProductCategoryProperty.removeListener(productCategoryIdListener);
		parentPlantSpeciesProperty.removeListener(plantSpeciesIdListener);
		parentPlantVarietyProperty.removeListener(plantVarietyIdListener);
		parentProductBrandProperty.removeListener(productBrandIdListener);
		nameProperty.removeListener(nameListener);
		nameDetail_1Property.removeListener(nameDetail_1Listener);
		nameDetail_2Property.removeListener(nameDetail_2Listener);
		descriptionProperty.removeListener(descriptionListener);
	}
	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 = (IProduct)(evt.getNewValue());
					setValues();
					addBaseListeners();
				}
			};

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

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

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

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

		baseItem.addPropertyChangeListener("PurchaseItem", baseItemPurchaseItemChanged);

		baseItem.addPropertyChangeListener("RetailerHasProduct", baseItemRetailerHasProductChanged);

		baseItem.addPropertyChangeListener("ShoppingList", baseItemShoppingListChanged);

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

		baseItem.removePropertyChangeListener("PurchaseItem", baseItemPurchaseItemChanged);

		baseItem.removePropertyChangeListener("RetailerHasProduct", baseItemRetailerHasProductChanged);

		baseItem.removePropertyChangeListener("ShoppingList", baseItemShoppingListChanged);

	}

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

}

