/*
 * 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.1.0   Add fetchAll()
            Add isReady property so that the catalogue can monitor new additions
    2.2.5   Guard against occasional NPE on item delete removing base listeners
    2.8.0   Support shopping list item creation in the ShoppingListCat.
                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.4	Comment handling
 */

package uk.co.gardennotebook.fxbean;

import javafx.beans.property.*;
import uk.co.gardennotebook.spi.*;
import uk.co.gardennotebook.util.StoryLineTree;
import java.util.Optional;
import java.util.List;
import java.util.ArrayList;
import java.beans.PropertyChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.ChangeListener;

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

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

import java.time.*;

/**
	*	A list of items which need to be acquired.
	*
	*	@author	Andy Gegg
	*	@version	3.0.4
	*	@since	1.0
*/
final public class ShoppingListBean implements INotebookBean
{
	private static final Logger LOGGER = LogManager.getLogger();

	private IShoppingList baseItem = null;

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

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


	/*
	*	A specific product, e.g. 'Jeyes Fluid 1l'
	*/
	private final ChangeListener<ProductBean> productListener = this::onProductChange;
    
    /*  2.8.0
        Support in-line editing in the ShoppingListCat
    */
    private final ProxyProductBean proxyProduct = new ProxyProductBean();

	/*
	*	Any item which is not an identified product - e.g. 'spring bulbs'
	*/
	private final SimpleStringProperty nonspecificItemProperty = new SimpleStringProperty(this, "nonspecificItem", "");
	private final ChangeListener<String> nonspecificItemListener = this::onNonspecificItemChange;
    
	private final ReadOnlyObjectWrapper<LocalDateTime> lastUpdatedProperty = new ReadOnlyObjectWrapper<>(this, "lastUpdated", LocalDateTime.now());
	private final ReadOnlyObjectWrapper<LocalDateTime> createdProperty = new ReadOnlyObjectWrapper<>(this, "created", LocalDateTime.now());
	private ReadOnlyBooleanWrapper canDeleteProperty = null;
	private final ReadOnlyBooleanWrapper hasAncestorProperty = new ReadOnlyBooleanWrapper(this, "hasAncestor", false);
	private final ReadOnlyBooleanWrapper hasDescendantProperty = new ReadOnlyBooleanWrapper(this, "hasDescendant", false);

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

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

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

	/**
	*	Construct an 'empty' Bean.  Set the various property values then call save() to create the new ShoppingListBean
	*/
	public ShoppingListBean()
	{
		this(null);
	}
	/**
	*	Construct a Bean wrapping the given ShoppingList
	*	If the parameter is null a new 'empty' Bean will be constructed
	*
	*	@param	initialValue	the ShoppingList to wrap.  If null an 'empty' bean will be constructed
	*
	*/
	public ShoppingListBean(final IShoppingList initialValue)
	{
		ChangeListener<Boolean> saveRequiredListener = (obs, old, nval) -> {
			if (nval && !explicitSave)
			{
				explicitSave = true;
				ITrug server = TrugServer.getTrugServer().getTrug();
				explicitBuilder = server.getShoppingListBuilder(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();
            // this line is left as a flag - the behaviour here is different
            // from other *Beans to allow direct editing in the catalogue
//			saveRequiredProperty.set(true);
			return;
		}

		baseItem = initialValue;

		itemKey = baseItem.getKey();

		newItem = false;
		setValues();

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

	/**
	*	Returns all ShoppingList items wrapped as ShoppingListBean.
	*
	*	@return	a collection of ShoppingListBean beans
	*
	*	@throws	GNDBException	if the underlying persisted storage engine (e.g. database server) throws an exception
	*				The original error can be retrieved by <code>getCause()</code>
    * 
    *   @since 2.1.0
	*/
	public static ObservableList<ShoppingListBean> fetchAll() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("fetchAll()");
		ITrug server = TrugServer.getTrugServer().getTrug();
		IShoppingListLister gal = server.getShoppingListLister();
		List<ShoppingListBean> ll = gal.fetch().stream()
					.collect(ArrayList::new, (c, e) -> c.add(new ShoppingListBean(e)), ArrayList::addAll);
		LOGGER.traceExit();
		return FXCollections.observableArrayList(ll);
	}

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

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

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

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

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

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

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

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

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

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

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

	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 ShoppingList this Bean wraps
	*	Call hasProduct() first to check if this value is set
	*
	*	@return	the Product parent of the ShoppingList this Bean wraps
	*/
	public ObjectProperty<ProductBean> productProperty()
	{
        return proxyProduct.productProperty();
	}

	/**
	*	Handle changes to the Product associated with this ShoppingList item
	*
	*	@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);
		if (nval != null && nval.sameAs(old))
		{
LOGGER.debug("onProductChange(): nval is sameAs old");
			return;
		}

		if ((nval != null) && !nval.isNew())
		{
			if (explicitSave)
			{
LOGGER.debug("onProductChange(): 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.getShoppingListBuilder(baseItem).nonspecificItem(null).product(nval.get().get()).save();
                    if (baseItem == null)
                    {
                        baseItem = newItem;
                    }
				} catch (GNDBException ex) {
					throw new GNDBRuntimeException(ex);
				}
			}
		}
		else if (nval == null && !nonspecificItemProperty().get().isBlank())
		{
			if (explicitSave)
			{
				explicitBuilder.product(null);
			}
			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.getShoppingListBuilder(baseItem).product(null).save();
                    if (baseItem == null)
                    {
                        baseItem = newItem;
                    }
				} catch (GNDBException ex) {
					throw new GNDBRuntimeException(ex);
				}
			}
		}
        
        //  2.1.0
LOGGER.debug("onProductChange(): isReadyProperty: {}", isReadyProperty.get());
        if (nval == null || nval.get().isEmpty())
        {
            isReadyProperty.set(!nonspecificItemProperty().get().isBlank());
        }
        else
        {
            isReadyProperty.set(nonspecificItemProperty().get().isBlank());
        }
LOGGER.debug("onProductChange(): isReadyProperty: {}", isReadyProperty.get());

		LOGGER.traceExit(log4jEntryMsg);
	}   //  onProductChange()
    
    //  2.8.0
	public boolean hasValidProduct()
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ShoppingListBean: 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 ProductBrand
	*/
	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 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 String getNonspecificItem()
	{
		return nonspecificItemProperty.get();
	}
	public void setNonspecificItem(final String newVal)
	{
		nonspecificItemProperty.set(newVal);
	}
	/**
	*	Wraps the NonspecificItem value of the ShoppingList
	*
	*	@return	a writable property wrapping the nonspecificItem attribute
	*/
	public StringProperty nonspecificItemProperty()
	{
		return nonspecificItemProperty;
	}

	private void onNonspecificItemChange(ObservableValue<? extends String> obs, String old, String nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onNonspecificItemChange(): old={}, new={}", old, nval);
        if (nval == null || nval.isBlank())
        {
            if (explicitSave)
            {
                LOGGER.debug("onNonspecificItemChange(): explicitSave (null value)");
                explicitBuilder.nonspecificItem(nval).product(null);
            }
        }
		if (nval != null && !nval.isBlank())   // if user sets value blank in Cat and walks away, leave things alone
        {
            if (explicitSave)
            {
    LOGGER.debug("onNonspecificItemChange(): explicitSave");
                explicitBuilder.nonspecificItem(nval).product(null);
            }
            else
            {
    LOGGER.debug("onNonspecificItemChange(): NOT explicitSave");
                ITrug server = TrugServer.getTrugServer().getTrug();
                try
                {
                    baseItem = server.getShoppingListBuilder(baseItem).nonspecificItem(nval).product(null).save();
                } catch (GNDBException ex) {
                    throw new GNDBRuntimeException(ex);
                }
            }
        }
        
        //  2.1.0
LOGGER.debug("onNonspecificItemChange(): isReadyProperty: {}", isReadyProperty.get());
        if (nval == null || nval.isBlank())
        {
            isReadyProperty.set(hasProductCategory());
        }
        else
        {
            isReadyProperty.set(!hasProductCategory());
        }
LOGGER.debug("onNonspecificItemChange(): isReadyProperty: {}", isReadyProperty.get());

        LOGGER.traceExit(log4jEntryMsg);
	}

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

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

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

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

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

		LOGGER.traceExit(log4jEntryMsg);
	}

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

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

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

		beanCommentHandler.changeCommentText(comment, text);

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

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

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

		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 ShoppingList 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.getShoppingListBuilder(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());
		nonspecificItemProperty.setValue("");
		lastUpdatedProperty.setValue(LocalDateTime.now());
		createdProperty.setValue(LocalDateTime.now());

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

	private void setValues()
	{
		saveRequiredProperty.setValue(false);
		if (baseItem.getProduct().isPresent())
		{
            proxyProduct.setProduct(new ProductBean(baseItem.getProduct().get()));
		}
		else
		{
            proxyProduct.setProduct(null);
		}
		nonspecificItemProperty.setValue(baseItem.getNonspecificItem().orElse(""));
		lastUpdatedProperty.setValue(baseItem.getLastUpdated());
		createdProperty.setValue(baseItem.getCreated());

		itemKey = baseItem.getKey();
		newItem = false;
		isNewProperty.set(false);	//	2.9.6

		LOGGER.debug("setvalues(): about to change BeanCommentHandler");
		beanCommentHandler = new BeanCommentHandler<>(this, baseItem);
		commentTextProperty.set(beanCommentHandler.commentTextProperty().get());
	}

	private void addListeners()
	{
		nonspecificItemProperty.addListener(nonspecificItemListener);
        LOGGER.debug("ShoppingListBean: addListeners(): proxyProduct: {}", proxyProduct);
		proxyProduct.productProperty().addListener(productListener);
	}
	private void removeListeners()
	{
		nonspecificItemProperty.removeListener(nonspecificItemListener);
	}
	private void declareBaseListeners()
	{
		// handle changes to the base item itself
		baseItemDeleted = evt -> {
				removeListeners();
				removeBaseListeners();
				setDefaults();
				baseItem = null;
			};
		baseItemReplaced = evt -> {
				if (evt.getNewValue() != null)
				{
                    LOGGER.debug("ShoppingListBean: baseItemReplaced: on entry: baseItem: {}", baseItem);
					removeBaseListeners();
					baseItem = (IShoppingList)(evt.getNewValue());
					setValues();
					addBaseListeners();
                    LOGGER.debug("ShoppingListBean: baseItemReplaced: on exit: baseItem: {}", baseItem);
				}
			};

	}
    
	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 "ShoppingListBean wrapping " + baseItem + ", proxy: "+proxyProduct;
	}

}

