/*
 * 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.5	Default singleShot to true to match the database and the Builder
    2.9.6	When a Diary entry is added/changed, make sure updated comments are shown
    3.0.0	Support 'watch for' entries on PurchaseItems
    		Support delaying ToDoList items by converting them (back) to Reminders
    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.*;
import java.util.function.Consumer;

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

/**
	*	A list of reminders of things to do. <BR>Reminders can be 'one-off' or repeated at various intervals (e.g. 'sow lettuce in March').
	*
	*	@author	Andy Gegg
	*	@version	3.0.4
	*	@since	1.0
*/
final public class ReminderBean implements INotebookBean
{
	private static final Logger LOGGER = LogManager.getLogger();

	private IReminder baseItem = null;

	private Integer itemKey = 0;
	private boolean newItem = false;
	private boolean explicitSave = false;
	private final SimpleBooleanProperty saveRequiredProperty = new SimpleBooleanProperty(this, "saveRequired", explicitSave);
	private final ChangeListener<Boolean> saveRequiredListener;
	private IReminderBuilder explicitBuilder = null;

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

	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<HusbandryClassBean> parentHusbandryClassProperty = new SimpleObjectProperty<>(this, "husbandryClass", null);
	private final ChangeListener<HusbandryClassBean> husbandryClassIdListener = this::onHusbandryClassIdChange;
	private final ReadOnlyBooleanWrapper hasParentHusbandryClassProperty = new ReadOnlyBooleanWrapper(this, "hasHusbandryClass", false);
	private final SimpleObjectProperty<GroundworkActivityBean> parentGroundworkActivityProperty = new SimpleObjectProperty<>(this, "groundworkActivity", null);
	private final ChangeListener<GroundworkActivityBean> groundWorkActivityIdListener = this::onGroundWorkActivityIdChange;
	private final ReadOnlyBooleanWrapper hasParentGroundworkActivityProperty = new ReadOnlyBooleanWrapper(this, "hasGroundworkActivity", false);

	/*
	*	The Husbandry editor can set up a ToDoList entry intended as a 'watch for this to happen' (e.g. on sowing, add a 'watch for germination').<BR>
This field is the id of that parent/ancestor Husbandry event so that if the ToDoList entry is delayed, the value can be retained.
	*/
	private final SimpleObjectProperty<HusbandryBean> parentHusbandryProperty = new SimpleObjectProperty<>(this, "husbandry", null);
	private final ChangeListener<HusbandryBean> husbandryIdListener = this::onHusbandryIdChange;
	private final ReadOnlyBooleanWrapper hasParentHusbandryProperty = new ReadOnlyBooleanWrapper(this, "hasHusbandry", false);

	/*
	*	The Purchase editor can set up a ToDoList entry intended as a 'watch for this to happen' (e.g. for a seed purchase, watch for sowing).<BR>
This field is the id of that parent/ancestor PurchaseItem event so that if the ToDoList entry is delayed, the value can be retained.
	*/
	private final SimpleObjectProperty<PurchaseItemBean> parentPurchaseItemProperty = new SimpleObjectProperty<>(this, "purchaseItem", null);
	private final ChangeListener<PurchaseItemBean> purchaseItemIdListener = this::onPurchaseItemIdChange;
	private final ReadOnlyBooleanWrapper hasParentPurchaseItemProperty = new ReadOnlyBooleanWrapper(this, "purchaseItem", false);

	/*
	*	For a single shot reminder, the first date it will be shown on.<BR>
For a repeating reminder, the current or next interval when the reminder will be active.
	*/
	private final SimpleObjectProperty<LocalDate> showFromProperty = new SimpleObjectProperty<>(this, "showFrom", LocalDate.now());
	private final ChangeListener<LocalDate> showFromListener = this::onShowFromChange;
	private final SimpleBooleanProperty singleShotProperty = new SimpleBooleanProperty(this, "singleShot", true);
	private final ChangeListener<Boolean> singleShotListener = this::onSingleShotChange;

	/*
	*	For a repeating reminder, the unit of the repeat interval (see repeatQuantifier).<UL>
<LI>D - daily
<LI>W - weekly
<LI>M - monthly
<LI>Y - annually
</UL>
A value is required for repeating reminders so set a default for safety
	*/
	private final SimpleObjectProperty<ReminderIntervalType> repeatIntervalProperty = new SimpleObjectProperty<>(this, "repeatInterval", null);
	private final ChangeListener<ReminderIntervalType> repeatIntervalListener = this::onRepeatIntervalChange;

	/*
	*	For a repeating Reminder, the number of repeatIntervals between repeats (see repeatInterval).<BR>
So 'weekly' and '2' means every fortnight.  0 means no repetition (avoids complications with nullable ints in the code - an impossible combination!)<BR>
A value is required for repeating reminders so set non-null with a default for safety
	*/
	private final SimpleIntegerProperty repeatQuantifierProperty = new SimpleIntegerProperty(this, "repeatQuantifier", 0);
	private final ChangeListener<Number> repeatQuantifierListener = this::onRepeatQuantifierChange;

	/*
	*	For a repeating reminder, the LAST time this reminder will be shown.<BR>
Null means 'repeat forever'.
	*/
	private final SimpleObjectProperty<LocalDate> repeatUntilProperty = new SimpleObjectProperty<>(this, "repeatUntil", LocalDate.now());
	private final ChangeListener<LocalDate> repeatUntilListener = this::onRepeatUntilChange;

	/*
	*	The actual text of the reminder.
	*/
	private final SimpleStringProperty descriptionProperty = new SimpleStringProperty(this, "description", "");
	private final ChangeListener<String> descriptionListener = this::onDescriptionChange;
	private final ReadOnlyObjectWrapper<LocalDateTime> lastUpdatedProperty = new ReadOnlyObjectWrapper<>(this, "lastUpdated", LocalDateTime.now());
	private final ReadOnlyObjectWrapper<LocalDateTime> createdProperty = new ReadOnlyObjectWrapper<>(this, "created", LocalDateTime.now());
	private ReadOnlyBooleanWrapper canDeleteProperty = null;
	private final ReadOnlyBooleanWrapper hasAncestorProperty = new ReadOnlyBooleanWrapper(this, "hasAncestor", false);
	private final ReadOnlyBooleanWrapper hasDescendantProperty = new ReadOnlyBooleanWrapper(this, "hasDescendant", false);

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

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

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

	public ReminderBean(final ToDoListBean todolistBean)
	{
		saveRequiredListener = (obs, old, nval) -> {
			if (nval && !explicitSave)
			{
				explicitSave = true;
				ITrug server = TrugServer.getTrugServer().getTrug();
				explicitBuilder = server.getReminderBuilder(baseItem);
			}
			if (!nval && explicitSave && (baseItem != null))
			{
				explicitSave = false;
				explicitBuilder = null;
			}
		};

		saveRequiredProperty.addListener(saveRequiredListener);


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

		newItem = true;
		saveRequiredProperty.set(true);

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

//		setValues();
		if (todolistBean.hasPlantSpecies())
		{
			hasParentPlantSpeciesProperty.set(true);
			parentPlantSpeciesProperty.setValue(todolistBean.getPlantSpecies());
		}
		else
		{
			hasParentPlantSpeciesProperty.set(false);
			parentPlantSpeciesProperty.setValue(null);
		}
		if (todolistBean.hasPlantVariety())
		{
			hasParentPlantVarietyProperty.set(true);
			parentPlantVarietyProperty.setValue(todolistBean.getPlantVariety());
		}
		else
		{
			hasParentPlantVarietyProperty.set(false);
			parentPlantVarietyProperty.setValue(null);
		}
		if (todolistBean.hasHusbandryClass())
		{
			hasParentHusbandryClassProperty.set(true);
			parentHusbandryClassProperty.setValue(todolistBean.getHusbandryClass());
		}
		else
		{
			hasParentHusbandryClassProperty.set(false);
			parentHusbandryClassProperty.setValue(null);
		}
		if (todolistBean.hasGroundworkActivity())
		{
			hasParentGroundworkActivityProperty.set(true);
			parentGroundworkActivityProperty.setValue(todolistBean.getGroundworkActivity());
		}
		else
		{
			hasParentGroundworkActivityProperty.set(false);
			parentGroundworkActivityProperty.setValue(null);
		}
		showFromProperty.setValue(LocalDate.now());
		singleShotProperty.setValue(true);
		repeatIntervalProperty.setValue(ReminderIntervalType.fromString(""));
		repeatQuantifierProperty.setValue(0);
		repeatUntilProperty.setValue(null);
		descriptionProperty.setValue(todolistBean.getDescription());
		lastUpdatedProperty.setValue(LocalDateTime.now());
		createdProperty.setValue(LocalDateTime.now());

		isNewProperty.set(true);

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

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

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

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

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

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

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

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

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

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

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

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

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

	public boolean hasPlantSpecies()
	{
		return hasPlantSpeciesProperty().getValue();
	}
	/**
	*	Use this to check if the PlantSpecies parent of the Reminder this Bean wraps is present
	*
	*	@return	true if this Reminder 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 Reminder this Bean wraps
	*	Call hasPlantSpecies() first to check if this value is set
	*
	*	@return	the PlantSpecies parent of the Reminder 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.getReminderBuilder(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.getReminderBuilder(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 Reminder this Bean wraps is present
	*
	*	@return	true if this Reminder 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 Reminder this Bean wraps
	*	Call hasPlantVariety() first to check if this value is set
	*
	*	@return	the PlantVariety parent of the Reminder 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.getReminderBuilder(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.getReminderBuilder(baseItem).plantVariety(null).save();
				} catch (GNDBException ex) {
					throw new GNDBRuntimeException(ex);
				}
			}
		}

		LOGGER.traceExit(log4jEntryMsg);
	}


	public boolean hasHusbandryClass()
	{
		return hasHusbandryClassProperty().getValue();
	}
	/**
	*	Use this to check if the HusbandryClass parent of the Reminder this Bean wraps is present
	*
	*	@return	true if this Reminder is linked to a HusbandryClass
	*/
	public ReadOnlyBooleanProperty hasHusbandryClassProperty()
	{
		return hasParentHusbandryClassProperty.getReadOnlyProperty();
	}
	public HusbandryClassBean getHusbandryClass()
	{
		return husbandryClassProperty().getValue();
	}
	public void setHusbandryClass(final HusbandryClassBean bean)
	{
		husbandryClassProperty().setValue(bean);
	}
	public void setHusbandryClass(final IHusbandryClass item)
	{
		husbandryClassProperty().setValue(new HusbandryClassBean(item));
	}
	/**
	*	Returns the HusbandryClass parent of the Reminder this Bean wraps
	*	Call hasHusbandryClass() first to check if this value is set
	*
	*	@return	the HusbandryClass parent of the Reminder this Bean wraps
	*/
	public ObjectProperty<HusbandryClassBean> husbandryClassProperty()
	{
		return parentHusbandryClassProperty;
	}

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

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

		LOGGER.traceExit(log4jEntryMsg);
	}


	public boolean hasGroundworkActivity()
	{
		return hasGroundworkActivityProperty().getValue();
	}
	/**
	*	Use this to check if the GroundworkActivity parent of the Reminder this Bean wraps is present
	*
	*	@return	true if this Reminder is linked to a GroundworkActivity
	*/
	public ReadOnlyBooleanProperty hasGroundworkActivityProperty()
	{
		return hasParentGroundworkActivityProperty.getReadOnlyProperty();
	}
	public GroundworkActivityBean getGroundworkActivity()
	{
		return groundworkActivityProperty().getValue();
	}
	public void setGroundworkActivity(final GroundworkActivityBean bean)
	{
		groundworkActivityProperty().setValue(bean);
	}
	public void setGroundworkActivity(final IGroundworkActivity item)
	{
		groundworkActivityProperty().setValue(new GroundworkActivityBean(item));
	}
	/**
	*	Returns the GroundworkActivity parent of the Reminder this Bean wraps
	*	Call hasGroundworkActivity() first to check if this value is set
	*
	*	@return	the GroundworkActivity parent of the Reminder this Bean wraps
	*/
	public ObjectProperty<GroundworkActivityBean> groundworkActivityProperty()
	{
		return parentGroundworkActivityProperty;
	}

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

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

		LOGGER.traceExit(log4jEntryMsg);
	}

	public boolean hasHusbandry()
	{
		return hasHusbandryProperty().getValue();
	}
	/**
	 *	Use this to check if the Husbandry parent of the ToDoList this Bean wraps is present
	 *
	 *	@return	true if this ToDoList is linked to a Husbandry
	 */
	public ReadOnlyBooleanProperty hasHusbandryProperty()
	{
		return hasParentHusbandryProperty.getReadOnlyProperty();
	}
	public HusbandryBean getHusbandry()
	{
		return husbandryProperty().getValue();
	}
	public void setHusbandry(final HusbandryBean bean)
	{
		husbandryProperty().setValue(bean);
	}
	public void setHusbandry(final IHusbandry item)
	{
		husbandryProperty().setValue(new HusbandryBean(item));
	}
	/**
	 *	Returns the Husbandry parent of the ToDoList this Bean wraps
	 *	Call hasHusbandry() first to check if this value is set
	 *
	 *	@return	the Husbandry parent of the ToDoList this Bean wraps
	 */
	public ObjectProperty<HusbandryBean> husbandryProperty()
	{
		return parentHusbandryProperty;
	}

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

		//	cannot have Husbandry AND PurchaseItem as parents!
		if (nval != null)
		{
			purchaseItemProperty().setValue(null);
		}

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

		LOGGER.traceExit(log4jEntryMsg);
	}

	public boolean hasPurchaseItem()
	{
		return hasPurchaseItemProperty().getValue();
	}
	/**
	 *	Use this to check if the PurchaseItem parent of the ToDoList this Bean wraps is present
	 *
	 *	@return	true if this ToDoList is linked to a PurchaseItem
	 */
	public ReadOnlyBooleanProperty hasPurchaseItemProperty()
	{
		return hasParentPurchaseItemProperty.getReadOnlyProperty();
	}
	public PurchaseItemBean getPurchaseItem()
	{
		return purchaseItemProperty().getValue();
	}
	public void setPurchaseItem(final PurchaseItemBean bean)
	{
		purchaseItemProperty().setValue(bean);
	}
	public void setPurchaseItem(final IPurchaseItem item)
	{
		purchaseItemProperty().setValue(new PurchaseItemBean(item));
	}
	/**
	 *	Returns the PurchaseItem parent of the ToDoList this Bean wraps
	 *	Call hasPurchaseItem() first to check if this value is set
	 *
	 *	@return	the PurchaseItem parent of the ToDoList this Bean wraps
	 */
	public ObjectProperty<PurchaseItemBean> purchaseItemProperty()
	{
		return parentPurchaseItemProperty;
	}

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

		//	cannot have Husbandry AND PurchaseItem as parents!
		if (nval != null)
		{
			husbandryProperty().setValue(null);
		}

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

		LOGGER.traceExit(log4jEntryMsg);
	}


	public LocalDate getShowFrom()
	{
		return showFromProperty.get();
	}
	public void setShowFrom(final LocalDate newVal)
	{
		showFromProperty.set(newVal);
	}
	/**
	*	Wraps the ShowFrom value of the Reminder
	*
	*	@return	a writable property wrapping the showFrom attribute
	*/
	public ObjectProperty<LocalDate> showFromProperty()
	{
		return showFromProperty;
	}

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

	public boolean isSingleShot()
	{
		return singleShotProperty.get();
	}
	public void setSingleShot(final boolean newVal)
	{
		singleShotProperty.set(newVal);
	}
	/**
	*	Wraps the SingleShot value of the Reminder
	*
	*	@return	a writable property wrapping the singleShot attribute
	*/
	public BooleanProperty singleShotProperty()
	{
		return singleShotProperty;
	}

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

	public ReminderIntervalType getRepeatInterval()
	{
		return repeatIntervalProperty.get();
	}
	public void setRepeatInterval(final ReminderIntervalType newVal)
	{
		repeatIntervalProperty.set(newVal);
	}
	/**
	*	Wraps the RepeatInterval value of the Reminder
	*
	*	@return	a writable property wrapping the repeatInterval attribute
	*/
	public ObjectProperty<ReminderIntervalType> repeatIntervalProperty()
	{
		return repeatIntervalProperty;
	}

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

	public int getRepeatQuantifier()
	{
		return repeatQuantifierProperty.get();
	}
	public void setRepeatQuantifier(final int newVal)
	{
		repeatQuantifierProperty.set(newVal);
	}
	/**
	*	Wraps the RepeatQuantifier value of the Reminder
	*
	*	@return	a writable property wrapping the repeatQuantifier attribute
	*/
	public IntegerProperty repeatQuantifierProperty()
	{
		return repeatQuantifierProperty;
	}

	/**
	*	Handle changes to the RepeatQuantifier 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 onRepeatQuantifierChange(ObservableValue<? extends Number> obs, Number old, Number nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onRepeatQuantifierChange(): old={}, new={}", old, nval);
		if (explicitSave)
		{
LOGGER.debug("onRepeatQuantifierChange(): explicitSave");
			explicitBuilder.repeatQuantifier(nval.intValue());
		}
		else
		{
LOGGER.debug("onRepeatQuantifierChange(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			//	the Builder will sent an event to the baseItem to say it's been replaced
			try
			{
				server.getReminderBuilder(baseItem).repeatQuantifier(nval.intValue()).save();
			} catch (GNDBException ex) {
				throw new GNDBRuntimeException(ex);
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
	}

	public LocalDate getRepeatUntil()
	{
		return repeatUntilProperty.get();
	}
	public void setRepeatUntil(final LocalDate newVal)
	{
		repeatUntilProperty.set(newVal);
	}
	/**
	*	Wraps the RepeatUntil value of the Reminder
	*
	*	@return	a writable property wrapping the repeatUntil attribute
	*/
	public ObjectProperty<LocalDate> repeatUntilProperty()
	{
		return repeatUntilProperty;
	}

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

	public String getDescription()
	{
		return descriptionProperty.get();
	}
	public void setDescription(final String newVal)
	{
		descriptionProperty.set(newVal);
	}
	/**
	*	Wraps the Description value of the Reminder
	*
	*	@return	a writable property wrapping the description attribute
	*/
	public StringProperty descriptionProperty()
	{
		return descriptionProperty;
	}

	/**
	*	Handle changes to the Description value
	*
	*	@throws	GNDBRuntimeException	if the underlying persisted storage engine (e.g. database server) throws an exception
	*				The original error can be retrieved by <code>getCause()</code>
	*/
	private void onDescriptionChange(ObservableValue<? extends String> obs, String old, String nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onDescriptionChange(): old={}, new={}", old, nval);
		if (explicitSave)
		{
LOGGER.debug("onDescriptionChange(): explicitSave");
			explicitBuilder.description(nval);
		}
		else
		{
LOGGER.debug("onDescriptionChange(): NOT explicitSave");
			ITrug server = TrugServer.getTrugServer().getTrug();
			try
			{
				baseItem = server.getReminderBuilder(baseItem).description(nval).save();
			} catch (GNDBException ex) {
				throw new GNDBRuntimeException(ex);
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
	}

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

	/**
	 *	The Reminder action has been completed, make an entry in the diary.
	 *	May only be used for 'single shot' Reminders.
	 *
	 * @param showHusbandry A callback to the calling object.  Typically used to pop up a HusbandryEditor with the new Husbandry.
	 *                      May be null.
	 *
	 * @param showGroundwork A callback to the calling object.  Typically used to pop up a GroundworkEditor with the new Groundwork.
	 *                      May be null.
	 *
	 *	@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 completeAction() throws GNDBException
	public void completeAction(Consumer<HusbandryBean> showHusbandry, Consumer<GroundworkBean> showGroundwork) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("completeAction()");

		if (!isSingleShot())	return;

		if (hasHusbandryClass())
		{
			LOGGER.debug("completeAction(): hasHusbandryClass");
			HusbandryBean hb = new HusbandryBean();
			hb.setHusbandryClass(getHusbandryClass());
			if (hasPlantSpecies())
			{
				LOGGER.debug("completeAction(): hasPlantSpecies");
				hb.setPlantSpecies(getPlantSpecies());
				if (hasPlantVariety())
				{
					LOGGER.debug("completeAction(): hasPlantVariety");
					hb.setPlantVariety(getPlantVariety());
				}
			}
			if (hasHusbandry())
			{
				HusbandryBean parentHusbandry = husbandryProperty().get();
//				hb.setAncestor(husbandryProperty().get());
				hb.setAncestor(parentHusbandry);
				if (parentHusbandry.hasLocation())
				{
					hb.setLocation(parentHusbandry.getLocation());
				}
			}
			else if (hasPurchaseItem())
			{
				hb.setAncestor(purchaseItemProperty().get());
			}
//			LOGGER.debug("completeAction(): about to add comment: {}", getDescription());
//			hb.addComment(getDescription());
			CommentBean cb = new CommentBean(hb);
			cb.setComment(getDescription());
//			LOGGER.debug("completeAction(): after adding comment, HB has: {}", hb.getComments());
			hb.save();
			if (showHusbandry != null)
			{
				showHusbandry.accept(hb);
			}
		}

		if (hasGroundworkActivity())
		{
			LOGGER.debug("completeAction(): hasGroundworkActivity");
			GroundworkBean gb = new GroundworkBean();
			gb.setGroundworkActivity(getGroundworkActivity());
			if (hasPlantSpecies())
			{
				LOGGER.debug("completeAction(): hasPlantSpecies");
				gb.setPlantSpecies(getPlantSpecies());
				if (hasPlantVariety())
				{
					LOGGER.debug("completeAction(): hasPlantVariety");
					gb.setPlantVariety(getPlantVariety());
				}
			}
			if (hasHusbandry())
			{
				HusbandryBean parentHusbandry = husbandryProperty().get();
//				gb.setAncestor(husbandryProperty().get());
				gb.setAncestor(parentHusbandry);
				if (parentHusbandry.hasLocation())
				{
					gb.setLocation(parentHusbandry.getLocation());
				}
			}
			else if (hasPurchaseItem())
			{
				gb.setAncestor(purchaseItemProperty().get());
			}
			LOGGER.debug("before adding comment: {}", getDescription());
//			gb.addComment(getDescription());
			CommentBean cb = new CommentBean(gb);
			cb.setComment(getDescription());
			gb.save();
			if (showGroundwork != null)
			{
				showGroundwork.accept(gb);
			}
		}
		delete();
		LOGGER.traceExit(log4jEntryMsg);
	}	// completeAction

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

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

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

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

		beanCommentHandler.addComment(text);	//	2.9.6

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

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

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

	private void setDefaults()
	{
		saveRequiredProperty.setValue(false);
		showFromProperty.setValue(LocalDate.now());
//		singleShotProperty.setValue(false);
		singleShotProperty.setValue(true);
		repeatIntervalProperty.setValue(null);
		repeatQuantifierProperty.setValue(0);
		repeatUntilProperty.setValue(LocalDate.now());
		descriptionProperty.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.getPlantSpecies().isPresent())
		{
			hasParentPlantSpeciesProperty.set(true);
			parentPlantSpeciesProperty.setValue(new PlantSpeciesBean(baseItem.getPlantSpecies().get()));
		}
		else
		{
			hasParentPlantSpeciesProperty.set(false);
			parentPlantSpeciesProperty.setValue(null);
		}
		if (baseItem.getPlantVariety().isPresent())
		{
			hasParentPlantVarietyProperty.set(true);
			parentPlantVarietyProperty.setValue(new PlantVarietyBean(baseItem.getPlantVariety().get()));
		}
		else
		{
			hasParentPlantVarietyProperty.set(false);
			parentPlantVarietyProperty.setValue(null);
		}
		if (baseItem.getHusbandryClass().isPresent())
		{
			hasParentHusbandryClassProperty.set(true);
			parentHusbandryClassProperty.setValue(new HusbandryClassBean(baseItem.getHusbandryClass().get()));
		}
		else
		{
			hasParentHusbandryClassProperty.set(false);
			parentHusbandryClassProperty.setValue(null);
		}
		if (baseItem.getGroundworkActivity().isPresent())
		{
			hasParentGroundworkActivityProperty.set(true);
			parentGroundworkActivityProperty.setValue(new GroundworkActivityBean(baseItem.getGroundworkActivity().get()));
		}
		else
		{
			hasParentGroundworkActivityProperty.set(false);
			parentGroundworkActivityProperty.setValue(null);
		}

		if (baseItem.getHusbandry().isPresent())
		{
			hasParentHusbandryProperty.set(true);
			parentHusbandryProperty.setValue(new HusbandryBean(baseItem.getHusbandry().get()));
		}
		else
		{
			hasParentHusbandryProperty.set(false);
			parentHusbandryProperty.setValue(null);
		}

		if (baseItem.getPurchaseItem().isPresent())
		{
			hasParentPurchaseItemProperty.set(true);
			parentPurchaseItemProperty.setValue(new PurchaseItemBean(baseItem.getPurchaseItem().get()));
		}
		else
		{
			hasParentPurchaseItemProperty.set(false);
			parentPurchaseItemProperty.setValue(null);
		}

		showFromProperty.setValue(baseItem.getShowFrom());
		singleShotProperty.setValue(baseItem.isSingleShot());
		repeatIntervalProperty.setValue(ReminderIntervalType.fromString(baseItem.getRepeatInterval().orElse("")));
		repeatQuantifierProperty.setValue(baseItem.getRepeatQuantifier().orElse(0));
		repeatUntilProperty.setValue(baseItem.getRepeatUntil().orElse(null));
		descriptionProperty.setValue(baseItem.getDescription().orElse(""));
		lastUpdatedProperty.setValue(baseItem.getLastUpdated());
		createdProperty.setValue(baseItem.getCreated());

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

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

	private void addListeners()
	{
		parentPlantSpeciesProperty.addListener(plantSpeciesIdListener);
		parentPlantVarietyProperty.addListener(plantVarietyIdListener);
		parentHusbandryClassProperty.addListener(husbandryClassIdListener);
		parentGroundworkActivityProperty.addListener(groundWorkActivityIdListener);

		parentHusbandryProperty.addListener(husbandryIdListener);
		parentPurchaseItemProperty.addListener(purchaseItemIdListener);

		showFromProperty.addListener(showFromListener);
		singleShotProperty.addListener(singleShotListener);
		repeatIntervalProperty.addListener(repeatIntervalListener);
		repeatQuantifierProperty.addListener(repeatQuantifierListener);
		repeatUntilProperty.addListener(repeatUntilListener);
		descriptionProperty.addListener(descriptionListener);
	}
	private void removeListeners()
	{
		parentPlantSpeciesProperty.removeListener(plantSpeciesIdListener);
		parentPlantVarietyProperty.removeListener(plantVarietyIdListener);
		parentHusbandryClassProperty.removeListener(husbandryClassIdListener);
		parentGroundworkActivityProperty.removeListener(groundWorkActivityIdListener);

		parentHusbandryProperty.removeListener(husbandryIdListener);
		parentPurchaseItemProperty.removeListener(purchaseItemIdListener);

		showFromProperty.removeListener(showFromListener);
		singleShotProperty.removeListener(singleShotListener);
		repeatIntervalProperty.removeListener(repeatIntervalListener);
		repeatQuantifierProperty.removeListener(repeatQuantifierListener);
		repeatUntilProperty.removeListener(repeatUntilListener);
		descriptionProperty.removeListener(descriptionListener);
	}
	private void declareBaseListeners()
	{
		// handle changes to the base item itself
		baseItemDeleted = evt -> {
				removeListeners();
				removeBaseListeners();
				setDefaults();
				baseItem = null;
			};
		baseItemReplaced = evt -> {
				if (evt.getNewValue() != null)
				{
					removeBaseListeners();
					baseItem = (IReminder)(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 "ReminderBean wrapping " + baseItem;
	}

}

