/*
 * 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.2.5   Guard against occasional NPE on item delete removing base listeners
    2.6.0   PurchaseItems cannot be descendants - block attempts to link them
    2.8.0   Support editing PIs in the PurchaseEditor
                A set of 'proxy' properties is added to shadow Product properties.
                These proxies can be freely manipulated without affecting the original
                Product (if any) and used to find or create an appropriate Product
                on save.
    2.9.6	When a Diary entry is added/changed, make sure updated comments are shown
    3.0.1	Add 'watch for' handling so that the plant species/variety cannot be changed if there's a 'watch for'.
    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 uk.co.gardennotebook.util.SimpleMoney;

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.*;
import java.math.BigDecimal;

/**
	*	An item in a Purchase
	*
	*	@author	Andy Gegg
	*	@version	3.0.4
	*	@since	1.0
*/
final public class PurchaseItemBean implements INotebookBean
{
	private static final Logger LOGGER = LogManager.getLogger();

	private IPurchaseItem baseItem = null;

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

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

    /*  2.8.0
        Support in-line editing in the PurchaseEditor
    */
    private final ProxyProductBean proxyProduct = new ProxyProductBean();
    private ProductBrandBean defaultBrand = null;

	private final SimpleObjectProperty<PurchaseBean> parentPurchaseProperty = new SimpleObjectProperty<>(this, "purchase", null);
	private final ChangeListener<PurchaseBean> purchaseListener = this::onPurchaseChange;
//	private final SimpleObjectProperty<ProductBean> parentProductProperty = new SimpleObjectProperty<>(this, "product", null);
	private final ChangeListener<ProductBean> productListener = this::onProductChange;
	private final SimpleObjectProperty<BigDecimal> quantityProperty = new SimpleObjectProperty<>(this, "quantity", BigDecimal.ZERO);
	private final ChangeListener<BigDecimal> quantityListener = this::onQuantityChange;
	private final SimpleStringProperty unitProperty = new SimpleStringProperty(this, "unit", "");
	private final ChangeListener<String> unitListener = this::onUnitChange;
	private final SimpleObjectProperty<BigDecimal> itemCostProperty = new SimpleObjectProperty<>(this, "itemCost", BigDecimal.ZERO);
	private final ChangeListener<BigDecimal> itemCostListener = this::onItemCostChange;

	/*
	*	ISO 4217 standard currency code (GBP, USD, EUR, etc).  Null means the local currency.
	*/
	private final SimpleStringProperty currencyProperty = new SimpleStringProperty(this, "currency", "");
	private final ChangeListener<String> currencyListener = this::onCurrencyChange;
	private final ReadOnlyObjectWrapper<LocalDateTime> lastUpdatedProperty = new ReadOnlyObjectWrapper<>(this, "lastUpdated", LocalDateTime.now());
	private final ReadOnlyObjectWrapper<LocalDateTime> createdProperty = new ReadOnlyObjectWrapper<>(this, "created", LocalDateTime.now());
	private final SimpleObjectProperty<SimpleMoney> itemPriceProperty = new SimpleObjectProperty<>(this, "itemPrice", new SimpleMoney());
	private final ChangeListener<SimpleMoney> itemPriceListener = this::onItemPriceChange;
	private ReadOnlyBooleanWrapper canDeleteProperty = null;
	private ReadOnlyBooleanWrapper hasAncestorProperty = null;
	private ReadOnlyBooleanWrapper hasDescendantProperty = null;

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

    private final ReadOnlyBooleanWrapper isReadyProperty = new ReadOnlyBooleanWrapper(this, "isReady", false);    //  2.1.0

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

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

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

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

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

		saveRequiredProperty.addListener(saveRequiredListener);

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

		baseItem = initialValue;

		itemKey = baseItem.getKey();

		newItem = false;
		setValues();

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

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

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

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

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

	@Override
	public boolean sameAs(final INotebookBean other)
	{
		if (other == null || ((PurchaseItemBean)other).baseItem == null || baseItem == null)
		{
			return false;
		}
		if (other.getType() != NotebookEntryType.PURCHASEITEM)
		{
			return false;
		}
		return baseItem.sameAs(((PurchaseItemBean)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();
	}

    /**
     * Monitor if this PurchaseItem is ready to be saved
     * 
     * @return true if sufficient and consistent values have been given
     * 
     * @since 2.1.0
     */
    public boolean isReady()
    {
        return isReadyProperty().get();
    }

    public ReadOnlyBooleanProperty isReadyProperty()
    {
        return isReadyProperty.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.getPurchaseItemBuilder(baseItem).canDelete();
			canDeleteProperty = new ReadOnlyBooleanWrapper(this, "canDelete", canDel);
		}
		return canDeleteProperty.getReadOnlyProperty();
	}

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

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

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

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

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

	/**
	 * Get the 'watch for' property.
	 * This is a read-only property.
	 *
	 * @return	The read only 'watch for' property
	 * @throws GNDBException - if the underlying persisted storage engine (e.g. database server) throws an exception.
	 *							The original error can be retrieved by getCause()
	 *
	 * @since 3.0.1
	 */
	public 	ReadOnlyObjectProperty<LocalDate> watchForDateProperty() throws GNDBException
	{
		if (hasWatchFor())
		{
			if (!childrenToDoList.isEmpty())
			{
				watchForDateProperty.setValue(childrenToDoList.get(0).getHusbandry().getDate());
			}
			else if (!childrenReminder.isEmpty())
			{
				watchForDateProperty.setValue(childrenReminder.get(0).getShowFrom());
			}
		}
		//	the property is initialised with a null value
		return watchForDateProperty.getReadOnlyProperty();
	}

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

	@Override
	public ReadOnlyBooleanProperty hasAncestorProperty() throws GNDBException
	{
        //   2.6.0 - PurchaseItems cannot have ancestors
		if (hasAncestorProperty == null)
		{
			hasAncestorProperty = new ReadOnlyBooleanWrapper(this, "hasAncestor", false);
		}
		return hasAncestorProperty.getReadOnlyProperty();
	}	//	hasAncestorProperty()

	@Override
	public StoryLineTree<? extends INotebookBean> getAncestors() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getAncestors()");
		if (baseItem == null)
		{
			return StoryLineTree.emptyTree();
		}
        //   2.6.0 - PurchaseItems cannot have ancestors
        StoryLineTree<? extends INotebookBean> beanTree = StoryLineTree.emptyTree();
		return LOGGER.traceExit(log4jEntryMsg, beanTree);
	}	//	getAncestors()


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

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

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

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

	public PurchaseBean getPurchase()
	{
		return purchaseProperty().getValue();
	}
	public void setPurchase(final PurchaseBean bean)
	{
		purchaseProperty().setValue(bean);
	}
	public void setPurchase(final IPurchase item)
	{
		purchaseProperty().setValue(new PurchaseBean(item));
	}
	/**
	*	Returns the Purchase parent of the PurchaseItem this Bean wraps
	*
	*	@return	the Purchase parent of the PurchaseItem this Bean wraps
	*/
	public ObjectProperty<PurchaseBean> purchaseProperty()
	{
		return parentPurchaseProperty;
	}

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

		LOGGER.traceExit(log4jEntryMsg);
	}


	public ProductBean getProduct()
	{
		return productProperty().getValue();
	}
	public void setProduct(final ProductBean bean)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("setProduct(): bean: {}", bean);
        proxyProduct.setProduct(bean);
	}
	public void setProduct(final IProduct item)
	{
		productProperty().setValue(new ProductBean(item));
	}
	/**
	*	Returns the Product parent of the PurchaseItem this Bean wraps
	*
	*	@return	the Product parent of the PurchaseItem this Bean wraps
	*/
	public ObjectProperty<ProductBean> productProperty()
	{
        return proxyProduct.productProperty();
	}

	/**
	*	Handle changes to the Product associated with this PurchaseItem
	*
	*	@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 onProductChange(ObservableValue<? extends ProductBean> obs, ProductBean old, ProductBean nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onProductChange(): old={}, new={}", old, nval);

        isReadyProperty.set(nval != null);
LOGGER.debug("onProductChange(): isReadyProperty: {}", isReadyProperty.get());

        if (nval == null)
        {
            return;
        }
        
		if (nval.sameAs(old))
		{
LOGGER.debug("onProductChange(): nval is sameAs old");
			return;
		}
        
		if (!nval.isNew())
		{
			if (explicitSave)
			{
				explicitBuilder.product(nval.get().get());
			}
			else
			{
LOGGER.debug("onProductChange(): NOT explicitSave");
				ITrug server = TrugServer.getTrugServer().getTrug();
				//	the Builder will send an event to the baseItem to say it's been replaced
                //  but NOT if the baseItem is null - so handle that explicitly
				try
				{
					var newItem = server.getPurchaseItemBuilder(baseItem).product(nval.get().get()).save();
                    if (baseItem == null)
                    {
                        baseItem = newItem;
                    }
				} catch (GNDBException ex) {
					throw new GNDBRuntimeException(ex);
				}
			}
		}
        
        if (newItem)
        {
            if (quantityProperty().get().equals(BigDecimal.ZERO))
            {
                setQuantity(BigDecimal.ONE);
            }
            if (productBrandProperty().get() == null  && defaultBrand != null)
            {
                setProductBrand(defaultBrand);
            }
        }

		LOGGER.traceExit(log4jEntryMsg);
	}   //  onProductChange()
	
    //  2.8.0
	public boolean hasValidProduct()
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("PurchaseItemBean: hasValidProduct()");
		return hasValidProductProperty().getValue();
	}
	public ReadOnlyBooleanProperty hasValidProductProperty()
	{
		return proxyProduct.hasValidProductProperty();
	}
    
	public boolean hasProductCategory()
	{
		return hasProductCategoryProperty().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 ProductCategory
	*/
	public ReadOnlyBooleanProperty hasProductCategoryProperty()
	{
		return proxyProduct.hasProductCategoryProperty();
	}

    public ProductCategoryBean getProductCategory()
    {
        return productCategoryProperty().getValue();
    }
    
    public void setProductCategory(final ProductCategoryBean bean)
    {
        productCategoryProperty().setValue(bean);
    }
    
    public ObjectProperty<ProductCategoryBean> productCategoryProperty()
    {
        return proxyProduct.productCategoryProperty();
    }
    
    public void setDefaultProductBrand(ProductBrandBean brand)
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("PurchaseItemBean: setDefaultProductBrand(): brand: {}", brand);
        defaultBrand = brand;
    }
    
	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 proxyProduct.hasProductBrandProperty();
	}

    public ProductBrandBean getProductBrand()
    {
        return productBrandProperty().getValue();
    }
    
    public void setProductBrand(final ProductBrandBean bean)
    {
        productBrandProperty().setValue(bean);
    }
    
    public ObjectProperty<ProductBrandBean> productBrandProperty()
    {
        return proxyProduct.productBrandProperty();
    }

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

    public PlantSpeciesBean getPlantSpecies()
    {
        return plantSpeciesProperty().getValue();
    }
    
    public void setPlantSpecies(final PlantSpeciesBean bean)
    {
        plantSpeciesProperty().setValue(bean);
    }
    
    public ObjectProperty<PlantSpeciesBean> plantSpeciesProperty()
    {
        return proxyProduct.plantSpeciesProperty();
    }


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

    public PlantVarietyBean getPlantVariety()
    {
        return plantVarietyProperty().getValue();
    }
    
    public void setPlantVariety(final PlantVarietyBean bean)
    {
        plantVarietyProperty().setValue(bean);
    }
    
    public ObjectProperty<PlantVarietyBean> plantVarietyProperty()
    {
        return proxyProduct.plantVarietyProperty();
    }

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

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


	public BigDecimal getQuantity()
	{
		return quantityProperty.get();
	}
	public void setQuantity(final BigDecimal newVal)
	{
		quantityProperty.set(newVal);
	}
	/**
	*	Wraps the Quantity value of the PurchaseItem
	*
	*	@return	a writable property wrapping the quantity attribute
	*/
	public ObjectProperty<BigDecimal> quantityProperty()
	{
		return quantityProperty;
	}

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

	public String getUnit()
	{
		return unitProperty.get();
	}
	public void setUnit(final String newVal)
	{
		unitProperty.set(newVal);
	}
	/**
	*	Wraps the Unit value of the PurchaseItem
	*
	*	@return	a writable property wrapping the unit attribute
	*/
	public StringProperty unitProperty()
	{
		return unitProperty;
	}

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

	public BigDecimal getItemCost()
	{
		return itemCostProperty.get();
	}
	public void setItemCost(final BigDecimal newVal)
	{
		itemCostProperty.set(newVal);
	}
	/**
	*	Wraps the ItemCost value of the PurchaseItem
	*
	*	@return	a writable property wrapping the itemCost attribute
	*/
	public ObjectProperty<BigDecimal> itemCostProperty()
	{
		return itemCostProperty;
	}

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

	public String getCurrency()
	{
		return currencyProperty.get();
	}
	public void setCurrency(final String newVal)
	{
		currencyProperty.set(newVal);
	}
	/**
	*	Wraps the Currency value of the PurchaseItem
	*
	*	@return	a writable property wrapping the currency attribute
	*/
	public StringProperty currencyProperty()
	{
		return currencyProperty;
	}

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

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

	public SimpleMoney getItemPrice()
	{
		return itemPriceProperty.get();
	}
	public void setItemPrice(final SimpleMoney newVal)
	{
		itemPriceProperty.set(newVal);
	}
	/**
	*	Wraps the itemPrice value of the PurchaseItem
	*
	*	@return	a writable property wrapping the itemPrice attribute
	*/
	public ObjectProperty<SimpleMoney> itemPriceProperty()
	{
		return itemPriceProperty;
	}

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

	@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 beanCommentHandler.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 PurchaseItem
LOGGER.debug("addComment(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getPurchaseItemBuilder(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 ? comment.getComment() : "null");
		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 Purchase Item
			LOGGER.debug("addComment(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getPurchaseItemBuilder(baseItem).addComment(comment.getComment()).save();
			setValues();	//	2.9.6
		}
		LOGGER.debug("addComment(comment): commentTextProperty: {}", commentTextProperty().get());

		LOGGER.traceExit(log4jEntryMsg);
	}

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

		//	2.9.6
		if (comment == null)
		{
			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 PurchaseItem
LOGGER.debug("changeCommentText(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getPurchaseItemBuilder(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 PurchaseItem
LOGGER.debug("changeCommentDate(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getPurchaseItemBuilder(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.getParentType() != this.getType()) return;

		beanCommentHandler.deleteComment(comment);

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

	/**
	 *	Get any ToDoList entry which refers to this PurchaseItem.
	 *
	 *	@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("getPurchaseItem()");
//		ObservableList<ToDoListBean> childrenToDoList = FXCollections.observableArrayList();
//		ITrug server = TrugServer.getTrugServer().getTrug();
//		for (IToDoList ix : server.getToDoListLister().purchaseItem(baseItem).fetch())
//		{
//			childrenToDoList.add(new ToDoListBean(ix));
//		}
		if (childrenToDoList == null)
		{
			childrenToDoList = FXCollections.observableArrayList();
			if (!newItem)
			{	//	a new bean will return all known ToDos...
				ITrug server = TrugServer.getTrugServer().getTrug();
				for (IToDoList ix : server.getToDoListLister().purchaseItem(baseItem).fetch())
				{
					childrenToDoList.add(new ToDoListBean(ix));
				}
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
		return childrenToDoList;
	}

	/**
	 *	Get any Reminder entry which refers to this PurchaseItem.
	 *
	 *	@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<ReminderBean> getReminder() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getReminder()");
//		ObservableList<ReminderBean> childrenToDoList = FXCollections.observableArrayList();
//		ITrug server = TrugServer.getTrugServer().getTrug();
//		for (IReminder ix : server.getReminderLister().purchaseItem(baseItem).fetch())
//		{
//			childrenToDoList.add(new ReminderBean(ix));
//		}
		if (childrenReminder == null)
		{
			childrenReminder = FXCollections.observableArrayList();
			if (!newItem)
			{	//	a new bean will return all known Reminders
				ITrug server = TrugServer.getTrugServer().getTrug();
				for (IReminder ix : server.getReminderLister().purchaseItem(baseItem).fetch())
				{
					childrenReminder.add(new ReminderBean(ix));
				}
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
		return childrenReminder;
	}

	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 PurchaseItem 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

		// need to save the product first or there's been no change to the PurchaseItem and it won't get saved!
		if (!getProduct().canSave())
		{
			throw new IllegalStateException("PurchaseItemBean: cannot save at this time - product cannot be saved");
		}
		getProduct().save();
            
        explicitBuilder.product(getProduct().get().get());  //  2.8.0   onProductChange does not set this for a NEW product
            
		if (!explicitBuilder.needSave())
		{
			return;
		}
		if (!explicitBuilder.canSave())
		{
			throw new IllegalStateException("PurchaseItemBean: cannot save at this time - mandatory values not set");
		}

//		getProduct().save();

		baseItem = explicitBuilder.save();
		setValues();	//	2.9.6
		saveRequiredProperty.set(false);
		childrenReminder = null;
		childrenToDoList = null;
		hasWatchForProperty = null;

		//	make the Retailer - Product link
		List<RetailerHasProductBean> rhpbs = RetailerHasProductBean.fetchAll(getPurchase().getRetailer(), getProduct());

		if (rhpbs.isEmpty())
		{
			RetailerHasProductBean newLink = new RetailerHasProductBean();
			newLink.setRetailer(getPurchase().getRetailer());
			newLink.setProduct(getProduct());
			newLink.save();
		}

		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 PurchaseItem 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.getPurchaseItemBuilder(baseItem).delete();
		}
	}	//	delete()

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

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

	private void setDefaults()
	{
		saveRequiredProperty.setValue(false);
//		parentProductProperty.setValue(new ProductBean());
		quantityProperty.setValue(BigDecimal.ZERO);
		unitProperty.setValue("");
		itemCostProperty.setValue(BigDecimal.ZERO);
		currencyProperty.setValue("");
		lastUpdatedProperty.setValue(LocalDateTime.now());
		createdProperty.setValue(LocalDateTime.now());
		itemPriceProperty.setValue(new SimpleMoney());
		childrenToDoList = null;
		childrenReminder = null;
		baseItemToDoListChanged = null;
		baseItemReminderChanged = null;

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

	private void setValues()
	{
		saveRequiredProperty.setValue(false);
		parentPurchaseProperty.setValue(new PurchaseBean(baseItem.getPurchase()));

        proxyProduct.setProduct(new ProductBean(baseItem.getProduct()));
        
		quantityProperty.setValue(baseItem.getQuantity().orElse(BigDecimal.ZERO));
		unitProperty.setValue(baseItem.getUnit().orElse(""));
		itemCostProperty.setValue(baseItem.getItemCost().orElse(BigDecimal.ZERO));
		currencyProperty.setValue(baseItem.getCurrency().orElse(""));
		lastUpdatedProperty.setValue(baseItem.getLastUpdated());
		createdProperty.setValue(baseItem.getCreated());
		itemPriceProperty.setValue(baseItem.getItemPrice());

		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()
	{
		parentPurchaseProperty.addListener(purchaseListener);
		proxyProduct.productProperty().addListener(productListener);
		quantityProperty.addListener(quantityListener);
		unitProperty.addListener(unitListener);
		itemCostProperty.addListener(itemCostListener);
		currencyProperty.addListener(currencyListener);
		itemPriceProperty.addListener(itemPriceListener);
	}
	private void removeListeners()
	{
		parentPurchaseProperty.removeListener(purchaseListener);
		quantityProperty.removeListener(quantityListener);
		unitProperty.removeListener(unitListener);
		itemCostProperty.removeListener(itemCostListener);
		currencyProperty.removeListener(currencyListener);
	}
	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 = (IPurchaseItem)(evt.getNewValue());
					setValues();
					addBaseListeners();
				}
			};

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

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

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

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

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

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

	}

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

}

