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

/*
	Change log
    2.2.5   Guard against occasional NPE on item delete removing base listeners
    2.9.6	When a Diary entry is added/changed, make sure updated comments are shown
    		Make sure new PurchaseItems are shown
	3.0.0	Fixed problem loading PIs for a new Purchase
    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.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;

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

/**
	*	Description of a purchase from a retailer.  The items purchased are recorded as PurchaseItems.
	*
	*	@author	Andy Gegg
	*	@version	3.0.4
	*	@since	1.0
*/
final public class PurchaseBean implements INotebookBean
{
	private static final Logger LOGGER = LogManager.getLogger();

	private IPurchase baseItem = null;

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

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

	private final SimpleObjectProperty<RetailerBean> parentRetailerProperty = new SimpleObjectProperty<>(this, "retailer", null);
	private final ChangeListener<RetailerBean> retailerIdListener = this::onRetailerIdChange;
	private final SimpleObjectProperty<LocalDate> dateProperty = new SimpleObjectProperty<>(this, "date", LocalDate.now());
	private final ChangeListener<LocalDate> dateListener = this::onDateChange;
	private final SimpleObjectProperty<BigDecimal> totalCostProperty = new SimpleObjectProperty<>(this, "totalCost", BigDecimal.ZERO);
	private final ChangeListener<BigDecimal> totalCostListener = this::onTotalCostChange;

	/*
	*	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 SimpleStringProperty orderNoProperty = new SimpleStringProperty(this, "orderNo", "");
	private final ChangeListener<String> orderNoListener = this::onOrderNoChange;
	private final SimpleStringProperty invoiceNoProperty = new SimpleStringProperty(this, "invoiceNo", "");
	private final ChangeListener<String> invoiceNoListener = this::onInvoiceNoChange;
	private final SimpleObjectProperty<LocalDate> deliveryDateProperty = new SimpleObjectProperty<>(this, "deliveryDate", LocalDate.now());
	private final ChangeListener<LocalDate> deliveryDateListener = this::onDeliveryDateChange;
	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> totalPriceProperty = new SimpleObjectProperty<>(this, "totalPrice", new SimpleMoney());
	private final ChangeListener<SimpleMoney> totalPriceListener = this::onTotalPriceChange;
	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<IPurchase> beanCommentHandler;	//	2.9.6
	private final ReadOnlyStringWrapper commentTextProperty = new ReadOnlyStringWrapper(this, "commentText", "");

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

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

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

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

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

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

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

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

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

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

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

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

	public RetailerBean getRetailer()
	{
		return retailerProperty().getValue();
	}
	public void setRetailer(final RetailerBean bean)
	{
		retailerProperty().setValue(bean);
	}
	public void setRetailer(final IRetailer item)
	{
		retailerProperty().setValue(new RetailerBean(item));
	}
	/**
	*	Returns the Retailer parent of the Purchase this Bean wraps
	*
	*	@return	the Retailer parent of the Purchase this Bean wraps
	*/
	public ObjectProperty<RetailerBean> retailerProperty()
	{
		return parentRetailerProperty;
	}

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

		LOGGER.traceExit(log4jEntryMsg);
	}


	public LocalDate getDate()
	{
		return dateProperty.get();
	}
	public void setDate(final LocalDate newVal)
	{
		dateProperty.set(newVal);
	}
	/**
	*	Wraps the Date value of the Purchase
	*
	*	@return	a writable property wrapping the date attribute
	*/
	public ObjectProperty<LocalDate> dateProperty()
	{
		return dateProperty;
	}

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

	public BigDecimal getTotalCost()
	{
		return totalCostProperty.get();
	}
	public void setTotalCost(final BigDecimal newVal)
	{
		totalCostProperty.set(newVal);
	}
	/**
	*	Wraps the TotalCost value of the Purchase
	*
	*	@return	a writable property wrapping the totalCost attribute
	*/
	public ObjectProperty<BigDecimal> totalCostProperty()
	{
		return totalCostProperty;
	}

	private void onTotalCostChange(ObservableValue<? extends BigDecimal> obs, BigDecimal old, BigDecimal nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onTotalCostChange(): old={}, new={}", old, nval);
		if (explicitSave)
		{
LOGGER.debug("onTotalCostChange(): explicitSave");
			explicitBuilder.totalCost(nval);
		}
		else
		{
LOGGER.debug("onTotalCostChange(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			try
			{
				baseItem = server.getPurchaseBuilder(baseItem).totalCost(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 Purchase
	*
	*	@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.getPurchaseBuilder(baseItem).currency(nval).save();
			} catch (GNDBException ex) {
				throw new GNDBRuntimeException(ex);
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
	}

	public String getOrderNo()
	{
		return orderNoProperty.get();
	}
	public void setOrderNo(final String newVal)
	{
		orderNoProperty.set(newVal);
	}
	/**
	*	Wraps the OrderNo value of the Purchase
	*
	*	@return	a writable property wrapping the orderNo attribute
	*/
	public StringProperty orderNoProperty()
	{
		return orderNoProperty;
	}

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

	public String getInvoiceNo()
	{
		return invoiceNoProperty.get();
	}
	public void setInvoiceNo(final String newVal)
	{
		invoiceNoProperty.set(newVal);
	}
	/**
	*	Wraps the InvoiceNo value of the Purchase
	*
	*	@return	a writable property wrapping the invoiceNo attribute
	*/
	public StringProperty invoiceNoProperty()
	{
		return invoiceNoProperty;
	}

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

	public LocalDate getDeliveryDate()
	{
		return deliveryDateProperty.get();
	}
	public void setDeliveryDate(final LocalDate newVal)
	{
		deliveryDateProperty.set(newVal);
	}
	/**
	*	Wraps the DeliveryDate value of the Purchase
	*
	*	@return	a writable property wrapping the deliveryDate attribute
	*/
	public ObjectProperty<LocalDate> deliveryDateProperty()
	{
		return deliveryDateProperty;
	}

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

	public LocalDateTime getLastUpdated()
	{
		return lastUpdatedProperty.get();
	}
	/**
	*	Wraps the LastUpdated value of the Purchase
	*	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 Purchase
	*	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 getTotalPrice()
	{
		return totalPriceProperty.get();
	}
	public void setTotalPrice(final SimpleMoney newVal)
	{
		totalPriceProperty.set(newVal);
	}
	/**
	*	Wraps the totalPrice value of the Purchase
	*
	*	@return	a writable property wrapping the totalPrice attribute
	*/
	public ObjectProperty<SimpleMoney> totalPriceProperty()
	{
		return totalPriceProperty;
	}

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

	/**
	*	Return a list of any PurchaseItem of this Purchase 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();
			//	3.0.0	FOr a new bean, baseItem is null and the call to getPILister will return EVERY PI
			if (baseItem != null)
			{
				ITrug server = TrugServer.getTrugServer().getTrug();
				LOGGER.debug("baseItem: {}", baseItem);
				for (IPurchaseItem ix : server.getPurchaseItemLister().purchase(baseItem).fetch())
				{
					childrenPurchaseItem.add(new PurchaseItemBean(ix));
				}
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
		return childrenPurchaseItem;
	}

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

		return LOGGER.traceExit(log4jEntryMsg, beanCommentHandler.getComments());
	}	//	getComments()

	//	2.9.6
	@Override
	public ReadOnlyStringProperty commentTextProperty()
	{
//		return beanCommentHandler.commentTextProperty();
		commentTextProperty.set(beanCommentHandler.commentTextProperty().get());
		return commentTextProperty;
	}

	@Override
	public void addComment(final String text) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("addComment({})", text);
		if (text == null || text.isBlank()) return;	//	2.9.6

		beanCommentHandler.addComment(text);	//	2.9.6

		if (explicitSave)
		{
LOGGER.debug("addComment(): explicitSave");
		}
		else
		{	//	this cannot be a new instance of the parent Purchase
LOGGER.debug("addComment(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getPurchaseBuilder(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.getPurchaseBuilder(baseItem).addComment(comment.getComment()).save();
			setValues();
		}
		LOGGER.debug("addComment(comment bean): commentTextProperty: {}", commentTextProperty().get());

		LOGGER.traceExit(log4jEntryMsg);
	}

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

		if (comment == null)	//	2.9.6
		{
			addComment(text);
			return;
		}

		if (comment.getParentType() != this.getType()) return;

		beanCommentHandler.changeCommentText(comment, text);

		if (explicitSave)
		{
LOGGER.debug("changeCommentText(): explicitSave");
		}
		else
		{	//	this cannot be a new instance of the parent Purchase
LOGGER.debug("changeCommentText(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getPurchaseBuilder(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 Purchase
LOGGER.debug("changeCommentDate(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getPurchaseBuilder(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;	//	2.9.6

		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 Purchase
LOGGER.debug("deleteComment(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			baseItem = server.getPurchaseBuilder(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 Purchase 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())
		{
			childrenPurchaseItem = null;	//	2.9.6
			return;
		}
		if (!explicitBuilder.canSave())
		{
			throw new IllegalStateException("PurchaseBean: 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
		childrenPurchaseItem = null;	//	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 Purchase 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.getPurchaseBuilder(baseItem).delete();
		}
	}	//	delete()

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

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

	private void setDefaults()
	{
		saveRequiredProperty.setValue(false);
		dateProperty.setValue(LocalDate.now());
		totalCostProperty.setValue(BigDecimal.ZERO);
		currencyProperty.setValue("");
		orderNoProperty.setValue("");
		invoiceNoProperty.setValue("");
		deliveryDateProperty.setValue(LocalDate.now());
		lastUpdatedProperty.setValue(LocalDateTime.now());
		createdProperty.setValue(LocalDateTime.now());
		totalPriceProperty.setValue(new SimpleMoney());
		childrenPurchaseItem = null;
		baseItemPurchaseItemChanged = null;

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

	private void setValues()
	{
		saveRequiredProperty.setValue(false);
		parentRetailerProperty.setValue(new RetailerBean(baseItem.getRetailer()));
		dateProperty.setValue(baseItem.getDate());
		totalCostProperty.setValue(baseItem.getTotalCost().orElse(BigDecimal.ZERO));
		currencyProperty.setValue(baseItem.getCurrency().orElse(""));
		orderNoProperty.setValue(baseItem.getOrderNo().orElse(""));
		invoiceNoProperty.setValue(baseItem.getInvoiceNo().orElse(""));
		deliveryDateProperty.setValue(baseItem.getDeliveryDate().orElse(null));
		lastUpdatedProperty.setValue(baseItem.getLastUpdated());
		createdProperty.setValue(baseItem.getCreated());
		totalPriceProperty.setValue(baseItem.getTotalPrice());

		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()
	{
		parentRetailerProperty.addListener(retailerIdListener);
		dateProperty.addListener(dateListener);
		totalCostProperty.addListener(totalCostListener);
		currencyProperty.addListener(currencyListener);
		orderNoProperty.addListener(orderNoListener);
		invoiceNoProperty.addListener(invoiceNoListener);
		deliveryDateProperty.addListener(deliveryDateListener);
		totalPriceProperty.addListener(totalPriceListener);
	}
	private void removeListeners()
	{
		parentRetailerProperty.removeListener(retailerIdListener);
		dateProperty.removeListener(dateListener);
		totalCostProperty.removeListener(totalCostListener);
		currencyProperty.removeListener(currencyListener);
		orderNoProperty.removeListener(orderNoListener);
		invoiceNoProperty.removeListener(invoiceNoListener);
		deliveryDateProperty.removeListener(deliveryDateListener);
	}
	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 = (IPurchase)(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())));
				}
			};

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

		baseItem.addPropertyChangeListener("PurchaseItem", baseItemPurchaseItemChanged);

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

		baseItem.removePropertyChangeListener("PurchaseItem", baseItemPurchaseItemChanged);

	}

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

}

