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

/*
	Change log
    2.2.5   Guard against occasional NPE on item delete removing base listeners
    2.6.0   on setAncestor(), ensure hasAncestor() is set true
            SaleItems cannot have descendants
            Allow leaf items to be disconnected from their ancestors
    2.9.6	When a Diary entry is added/changed, make sure updated comments are shown
 */

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.beans.PropertyChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.ChangeListener;

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 sale of produce.
	*
	*	@author	Andy Gegg
	*	@version	2.9.6
	*	@since	1.0
*/
final public class SaleItemBean implements INotebookBean
{
	private static final Logger LOGGER = LogManager.getLogger();

	private ISaleItem baseItem = null;

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

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

	private final SimpleObjectProperty<SaleBean> parentSaleProperty = new SimpleObjectProperty<>(this, "sale", null);
	private final ChangeListener<SaleBean> saleIdListener = this::onSaleIdChange;
	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);
	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<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 BeanCommentHandler<ISaleItem> beanCommentHandler;	//	2.9.6

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

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

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

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

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

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

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

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

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

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

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

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

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

	@Override
	public ReadOnlyBooleanProperty hasDescendantProperty() throws GNDBException
	{
		if (hasDescendantProperty == null)
		{
            //  2.6.0   SaleItems cannot have descendants
			hasDescendantProperty = new ReadOnlyBooleanWrapper(this, "hasDescendant", false);
		}
		return hasDescendantProperty.getReadOnlyProperty();
	}	//	hasDescendantProperty()

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

	public SaleBean getSale()
	{
		return saleProperty().getValue();
	}
	public void setSale(final SaleBean bean)
	{
		saleProperty().setValue(bean);
	}
	public void setSale(final ISale item)
	{
		saleProperty().setValue(new SaleBean(item));
	}
	/**
	*	Returns the Sale parent of the SaleItem this Bean wraps
	*
	*	@return	the Sale parent of the SaleItem this Bean wraps
	*/
	public ObjectProperty<SaleBean> saleProperty()
	{
		return parentSaleProperty;
	}

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

		LOGGER.traceExit(log4jEntryMsg);
	}


	public boolean hasPlantSpecies()
	{
		return hasPlantSpeciesProperty().getValue();
	}
	/**
	*	Use this to check if the PlantSpecies parent of the SaleItem this Bean wraps is present
	*
	*	@return	true if this SaleItem is linked to a PlantSpecies
	*/
	public ReadOnlyBooleanProperty hasPlantSpeciesProperty()
	{
		return hasParentPlantSpeciesProperty.getReadOnlyProperty();
	}
	public PlantSpeciesBean getPlantSpecies()
	{
		return plantSpeciesProperty().getValue();
	}
	public void setPlantSpecies(final PlantSpeciesBean bean)
	{
		plantSpeciesProperty().setValue(bean);
	}
	public void setPlantSpecies(final IPlantSpecies item)
	{
		plantSpeciesProperty().setValue(new PlantSpeciesBean(item));
	}
	/**
	*	Returns the PlantSpecies parent of the SaleItem this Bean wraps
	*	Call hasPlantSpecies() first to check if this value is set
	*
	*	@return	the PlantSpecies parent of the SaleItem 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.getSaleItemBuilder(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.getSaleItemBuilder(baseItem).plantSpecies(null).save();
				} catch (GNDBException ex) {
					throw new GNDBRuntimeException(ex);
				}
			}
		}

		LOGGER.traceExit(log4jEntryMsg);
	}


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

		LOGGER.traceExit(log4jEntryMsg);
	}


	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.getSaleItemBuilder(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.getSaleItemBuilder(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 SaleItem
	*
	*	@return	a writable property wrapping the itemCost attribute
	*/
	public ObjectProperty<BigDecimal> itemCostProperty()
	{
		return itemCostProperty;
	}

	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.getSaleItemBuilder(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 SaleItem
	*
	*	@return	a writable property wrapping the currency attribute
	*/
	public StringProperty currencyProperty()
	{
		return currencyProperty;
	}

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

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

	@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 SaleItem
LOGGER.debug("addComment(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getSaleItemBuilder(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);
		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.getSaleItemBuilder(baseItem).addComment(comment.getComment()).save();
			setValues();	//	2.9.6
		}

		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 SaleItem
LOGGER.debug("changeCommentText(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getSaleItemBuilder(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 SaleItem
LOGGER.debug("changeCommentDate(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getSaleItemBuilder(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 SaleItem
LOGGER.debug("deleteComment(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getSaleItemBuilder(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();
	}

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

		return explicitBuilder.canSave();
	}

	/**
	*	Save changes to the underlying SaleItem 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("SaleItemBean: cannot save at this time - mandatory values not set");
		}

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

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

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

	/**
	*	Delete the underlying SaleItem 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.getSaleItemBuilder(baseItem).delete();
		}
	}	//	delete()

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

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

	private void setDefaults()
	{
		saveRequiredProperty.setValue(false);
		hasParentPlantSpeciesProperty.set(false);
		parentPlantSpeciesProperty.setValue(new PlantSpeciesBean());
        hasParentPlantVarietyProperty.set(false);
        parentPlantVarietyProperty.setValue(new PlantVarietyBean());
		quantityProperty.setValue(BigDecimal.ZERO);
		unitProperty.setValue("");
		itemCostProperty.setValue(BigDecimal.ZERO);
		currencyProperty.setValue("");
		lastUpdatedProperty.setValue(LocalDateTime.now());
		createdProperty.setValue(LocalDateTime.now());
		itemPriceProperty.setValue(new SimpleMoney());

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

	private void setValues()
	{
		saveRequiredProperty.setValue(false);
		parentSaleProperty.setValue(new SaleBean(baseItem.getSale()));
		hasParentPlantSpeciesProperty.set(true);
		parentPlantSpeciesProperty.setValue(new PlantSpeciesBean(baseItem.getPlantSpecies()));
		if (baseItem.getPlantVariety().isPresent())
		{
			hasParentPlantVarietyProperty.set(true);
			parentPlantVarietyProperty.setValue(new PlantVarietyBean(baseItem.getPlantVariety().get()));
		}
		else
		{
			hasParentPlantVarietyProperty.set(false);
			parentPlantVarietyProperty.setValue(null);
		}
		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);
	}

	private void addListeners()
	{
		parentSaleProperty.addListener(saleIdListener);
		parentPlantSpeciesProperty.addListener(plantSpeciesIdListener);
		parentPlantVarietyProperty.addListener(plantVarietyIdListener);
		quantityProperty.addListener(quantityListener);
		unitProperty.addListener(unitListener);
		itemCostProperty.addListener(itemCostListener);
		currencyProperty.addListener(currencyListener);
		itemPriceProperty.addListener(itemPriceListener);
	}
	private void removeListeners()
	{
		parentSaleProperty.removeListener(saleIdListener);
		parentPlantSpeciesProperty.removeListener(plantSpeciesIdListener);
		parentPlantVarietyProperty.removeListener(plantVarietyIdListener);
		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 = (ISaleItem)(evt.getNewValue());
					setValues();
					addBaseListeners();
				}
			};

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

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

	}

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

}

