/*
 * Copyright (C) 2018-2023 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/>.
 */

/*
	Change log
	2.0.1   removes System.out.println calls
    2.5.0   support sales
    2.6.0   support storyline retrofit by Drag&Drop
            allow leaf items to be disconnected from ancestors
    2.6.1   code tidy
    2.9.6	When a Diary entry is added/changed, make sure updated comments are shown
    3.0.0	Support editing Sale.purchasedBy, Sale.totalPrice, SaleItem.itemPrice, Purchase.totalPrice
    			(see TextTableCell)
    		Support Journal entries
	3.0.1	Don't allow edit of Husbandry items with a 'watch for' event
			Move the icons to a separate class to make it easier to use them in the History displays
	3.0.3	Add Location as the Detail column where appropriate.
    3.0.4	Set first row of catalogue selected.
	3.0.5	Use factory pattern DiaryBean
			Group SaleItems under Sale (cf Purchases)
	3.1.0	Pass extra info when adding descendants to check dates
			Disallow addition of certain descendants
 */

package uk.co.gardennotebook;

import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.scene.layout.Region;
import javafx.scene.shape.SVGPath;
import uk.co.gardennotebook.fxbean.*;

import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javafx.beans.binding.StringBinding;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Control;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.VBox;
import javafx.stage.WindowEvent;
import uk.co.gardennotebook.spi.NotebookEntryType;
import uk.co.gardennotebook.util.SimpleMoney;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.EntryMessage;
import uk.co.gardennotebook.spi.GNDBException;
import uk.co.gardennotebook.util.StoryLineTree;

/**
 * Controller for the Diary display tab
 *
 * @author Andy Gegg
*	@version	3.1.0
*	@since	1.0
 */
final class DiaryTab extends AnchorPane implements INotebookLoadable
{

	private static final Logger LOGGER = LogManager.getLogger();

	private static final SVGPath afflictionGraphic = DiaryIcons.afflictionGraphic;
	private static final SVGPath groundworkGraphic = DiaryIcons.groundworkGraphic;
	private static final SVGPath husbandryGraphic = DiaryIcons.husbandryGraphic;
	private static final SVGPath journalGraphic = DiaryIcons.journalGraphic;
	private static final SVGPath purchaseGraphic = DiaryIcons.purchaseGraphic;
	private static final SVGPath saleGraphic = DiaryIcons.saleGraphic;
	private static final SVGPath weatherGraphic = DiaryIcons.weatherGraphic;
	private static final SVGPath wildlifeGraphic = DiaryIcons.wildlifeGraphic;

	//	@FXML
//	private AnchorPane anchorPane;
	@FXML
	private MenuButton btnAdd;
//	@FXML
//	private MenuItem mnuHusbandry;
//	@FXML
//	private MenuItem mnuWeather;
	@FXML
	private MenuItem ctxmnuDelete;
	@FXML
	private MenuItem ctxmnuTopHistory;
	@FXML
	private MenuItem ctxmnuAncestors;
	@FXML
	private MenuItem ctxmnuDescendants;
	@FXML
	private MenuItem ctxmnuNewDescendant;
	@FXML
	private MenuItem ctxmnuNewHusbandry;
//	@FXML
//	private MenuItem ctxmnuNewAffliction;
	@FXML
	private MenuItem ctxmnuNewSale;
	@FXML
	private MenuItem ctxmnuDropLeaf;
	@FXML
	private Button btnChange;
	@FXML
	private Button btnDelete;
	@FXML
	private TableView<IndexedDiaryBean> diaryTable;
	@FXML
	private TableColumn<IndexedDiaryBean, IndexedDiaryBean> diaryColDate;
	@FXML
	private TableColumn<IndexedDiaryBean, IndexedDiaryBean> colMainTitle;
	@FXML
	private TableColumn<IndexedDiaryBean, IndexedDiaryBean> colSubTitle;
	@FXML
	private TableColumn<IndexedDiaryBean, IndexedDiaryBean> colVariety;
	@FXML
	private TableColumn<IndexedDiaryBean, IndexedDiaryBean> colDetail;
	@FXML
	private TableColumn<IndexedDiaryBean, IndexedDiaryBean> colComment;

	@FXML
	private ResourceBundle resources;

	private Consumer<Node> loadSplit;
	private Consumer<Node> clearSplit;
	private BiConsumer<String, Node> loadTab;
	private Consumer<Node> clearTab;
	
	private List<DiaryBean> theDiary = null;
	
//	@FXML
//	private MenuButton tblctxmnuChange;

	DiaryTab()
	{
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/DiaryTab.fxml"),
			ResourceBundle.getBundle("notebook") );
        fxmlLoader.setRoot(this);
        fxmlLoader.setController(this);

        try {
            fxmlLoader.load();
        } catch (IOException exception) {
            throw new RuntimeException(exception);
        }
	}

	/**
	 * Initializes the controller class.
	 */
	@FXML
	public void initialize()
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("initialize()");
		diaryColDate.setCellValueFactory(cdf -> new SimpleObjectProperty<>(cdf.getValue()));
		diaryColDate.setCellFactory(x -> new DateTableCell());
		colMainTitle.setCellValueFactory(cdf -> new SimpleObjectProperty<>(cdf.getValue()));
		colMainTitle.setCellFactory(x -> new TextTableCell());
		colSubTitle.setCellValueFactory(cdf -> new SimpleObjectProperty<>(cdf.getValue()));
		colSubTitle.setCellFactory(x -> new TextTableCell());
		colVariety.setCellValueFactory(cdf -> new SimpleObjectProperty<>(cdf.getValue()));
		colVariety.setCellFactory(x -> new TextTableCell());
		colDetail.setCellValueFactory(cdf -> new SimpleObjectProperty<>(cdf.getValue()));
		colDetail.setCellFactory(x -> new TextTableCell());
		colComment.setCellValueFactory(cdf -> new SimpleObjectProperty<>(cdf.getValue()));
		colComment.setCellFactory(x -> new CommentTableCell());

		// set a custom resize policy so that the Comments column takes up all available space on the right
		diaryTable.setColumnResizePolicy(NotebookResizer.using(diaryTable));
		
		// only allow change and delete if there is a row selected
		btnChange.disableProperty().bind(diaryTable.getSelectionModel().selectedItemProperty().isNull());
		diaryTable.getSelectionModel().selectedItemProperty().addListener((bean, old, nval)->{
				if (nval != null)
				{
					try {
						btnDelete.disableProperty().setValue(!(nval.canDelete()));
					} catch (GNDBException ex) {
						PanicHandler.panic(ex);
					}
				}
		});
        
        //  2.6.0
        //  Use a RowFactory to catch Drag&Drop events on rows
        diaryTable.setRowFactory(ev -> {
            DiaryTabDDTableRow tabRow = new DiaryTabDDTableRow(resources);
            tabRow.setLoadSplit(loadSplit);
            tabRow.setClearSplit(clearSplit);
            tabRow.setLoadTab(loadTab);
            return tabRow;
        });

		LOGGER.traceExit(log4jEntryMsg);
	}

	private void selectNewBean(final DiaryBean bean)
	{
		for (var idb : diaryTable.getItems())
		{
			if (bean.getItemType() == idb.beanType() &&
				bean.getItem().sameAs(idb.getItem().getItem()))
			{
				diaryTable.getSelectionModel().select(idb);
				return;
			}
		}
	}

	@FXML
	private void mnubtnAddHusbandryOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("mnubtnAddHusbandryOnAction()");
		HusbandryEditor tabCon = new HusbandryEditor();
		loadTab.accept(resources.getString("tab.husbandry"), tabCon);
		tabCon.newBeanProperty().addListener((obs, oldVal, newVal) -> {
//			theDiary.add(DiaryBean.from(newVal));
			DiaryBean db = DiaryBean.from(newVal);
			theDiary.add(db);
			updateDiary();
			selectNewBean(db);
		});
		LOGGER.traceExit(log4jEntryMsg);
	}

	@FXML
	private void mnubtnAddPurchaseOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("mnubtnAddPurchaseOnAction()");
		PurchaseEditor tabCon = new PurchaseEditor();
		loadTab.accept(resources.getString("tab.purchase"), tabCon);
		tabCon.newBeanProperty().addListener((obs, oldVal, newVal) -> {
//			theDiary.add(DiaryBean.from(newVal));
			DiaryBean db = DiaryBean.from(newVal);
			theDiary.add(db);
            try {
                for (PurchaseItemBean newItem : newVal.getPurchaseItem())
                {
                    theDiary.add(DiaryBean.from(newItem));
                }
            } catch (GNDBException ex) {
                PanicHandler.panic(ex);
            }
			updateDiary();
			selectNewBean(db);
		});
		LOGGER.traceExit(log4jEntryMsg);
	}

	@FXML
	private void mnubtnAddGroundworkOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("mnubtnAddGroundworkOnAction()");
		GroundworkEditor tabCon = new GroundworkEditor();
		loadTab.accept(resources.getString("tab.groundwork"), tabCon);
		tabCon.newBeanProperty().addListener((obs, oldVal, newVal) -> {
//			theDiary.add(DiaryBean.from(newVal));
//			updateDiary();
			DiaryBean db = DiaryBean.from(newVal);
			theDiary.add(db);
			updateDiary();
			selectNewBean(db);
		});
		LOGGER.traceExit(log4jEntryMsg);
	}

	@FXML
	private void mnubtnAddAfflictionOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("mnubtnAddAfflictionOnAction()");
		AfflictionEventEditor tabCon = new AfflictionEventEditor();
		loadTab.accept(resources.getString("tab.affliction"), tabCon);
		tabCon.newBeanProperty().addListener((obs, oldVal, newVal) -> {
//			theDiary.add(DiaryBean.from(newVal));
//			updateDiary();
			DiaryBean db = DiaryBean.from(newVal);
			theDiary.add(db);
			updateDiary();
			selectNewBean(db);
		});
		LOGGER.traceExit(log4jEntryMsg);
	}

	@FXML
	private void mnubtnAddWeatherOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("mnubtnAddWeatherOnAction()");
		WeatherEditor tabCon = new WeatherEditor();
		loadTab.accept(resources.getString("tab.weather"), tabCon);
		tabCon.newBeanProperty().addListener((obs, oldVal, newVal) -> {
//			theDiary.add(DiaryBean.from(newVal));
//			updateDiary();
			DiaryBean db = DiaryBean.from(newVal);
			theDiary.add(db);
			updateDiary();
			selectNewBean(db);
		});
		LOGGER.traceExit(log4jEntryMsg);
	}

	@FXML
	private void mnubtnAddWildlifeOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("mnubtnAddWildlifeOnAction()");
		WildlifeEditor tabCon = new WildlifeEditor();
		loadTab.accept(resources.getString("tab.wildlife"), tabCon);
		tabCon.newBeanProperty().addListener((obs, oldVal, newVal) -> {
//			theDiary.add(DiaryBean.from(newVal));
//			updateDiary();
			DiaryBean db = DiaryBean.from(newVal);
			theDiary.add(db);
			updateDiary();
			selectNewBean(db);
		});
		LOGGER.traceExit(log4jEntryMsg);
	}

	@FXML
	private void mnubtnAddSaleOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("mnubtnAddSaleOnAction()");
		SaleEditor tabCon = new SaleEditor();
		loadTab.accept(resources.getString("tab.sales"), tabCon);
		tabCon.newBeanProperty().addListener((obs, oldVal, newVal) -> {
//			theDiary.add(DiaryBean.from(newVal));
			DiaryBean db = DiaryBean.from(newVal);
			theDiary.add(db);
            try {
                for (SaleItemBean newItem : newVal.getSaleItem())
                {
					theDiary.add(DiaryBean.from(newItem));
                }
            } catch (GNDBException ex) {
                PanicHandler.panic(ex);
            }
			updateDiary();
			selectNewBean(db);
		});
		LOGGER.traceExit(log4jEntryMsg);
	}

	@FXML
	private void mnubtnAddJournalOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("mnubtnAddJournalOnAction()");
		JournalEditor tabCon = new JournalEditor();
		loadTab.accept(resources.getString("tab.journal"), tabCon);
		tabCon.newBeanProperty().addListener((obs, oldVal, newVal) -> {
//			theDiary.add(DiaryBean.from(newVal));
//			updateDiary();
			DiaryBean db = DiaryBean.from(newVal);
			theDiary.add(db);
			updateDiary();
			selectNewBean(db);
		});
		LOGGER.traceExit(log4jEntryMsg);
	}

	@FXML
	private void btnChangeOnAction(ActionEvent event) 
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("btnChangeOnAction()");
		ctxmnuChangeOnAction(event);
		LOGGER.traceExit(log4jEntryMsg);
	}

	@FXML
	private void btnDeleteOnAction(ActionEvent event) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("btnDeleteOnAction()");
		ctxmnuDeleteOnAction(event);
		LOGGER.traceExit(log4jEntryMsg);
	}

	@FXML
	private void ctxmnuTopOnAction(WindowEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuTopOnAction()");
		IndexedDiaryBean ixBean = diaryTable.getSelectionModel().getSelectedItem();
		if (ixBean == null)
		{
			LOGGER.debug("no item selected in Table");
			return;
		}
		try {
			ctxmnuDelete.setDisable(!(ixBean.canDelete()));
		} catch (GNDBException ex) {
			PanicHandler.panic(ex);
		}
		ctxmnuTopHistory.setDisable(ixBean.getItem().getItemType() == NotebookEntryType.WEATHER ||
									ixBean.getItem().getItemType() == NotebookEntryType.WILDLIFE ||
									ixBean.getItem().getItemType() == NotebookEntryType.PURCHASE ||
									ixBean.getItem().getItemType() == NotebookEntryType.JOURNAL ||
									ixBean.getItem().getItemType() == NotebookEntryType.SALE);
        boolean hasAncestor = false;
        boolean hasDescendant = false;
		try {
            hasAncestor = ixBean.hasAncestor();
            hasDescendant = ixBean.hasDescendant();
		} catch (GNDBException ex) {
			PanicHandler.panic(ex);
		}

        ctxmnuAncestors.setDisable(!hasAncestor);
        ctxmnuDescendants.setDisable(!hasDescendant);
        
        ctxmnuNewDescendant.setDisable(ixBean.getItem().getItemType() == NotebookEntryType.SALEITEM);
        ctxmnuNewSale.setDisable(ixBean.getItem().getItemType() == NotebookEntryType.PURCHASE ||
                                 ixBean.getItem().getItemType() == NotebookEntryType.PURCHASEITEM );

		final boolean noPlants = ixBean.getItem().getItemType() == NotebookEntryType.PURCHASE ||
						(ixBean.getItem().getItemType() == NotebookEntryType.PURCHASEITEM && !((PurchaseItemBean)(ixBean.getItem().getItem())).hasPlantSpecies()) ||
						(ixBean.getItem().getItemType() == NotebookEntryType.AFFLICTIONEVENT && !((AfflictionEventBean)(ixBean.getItem().getItem())).hasPlantSpecies()) ||
						(ixBean.getItem().getItemType() == NotebookEntryType.GROUNDWORK && !((GroundworkBean)(ixBean.getItem().getItem())).hasPlantSpecies());
		ctxmnuNewHusbandry.setDisable(noPlants);
		ctxmnuNewSale.setDisable(noPlants);

        ctxmnuDropLeaf.setDisable(ixBean.getItem().getItemType() == NotebookEntryType.PURCHASE ||
                                 ixBean.getItem().getItemType() == NotebookEntryType.PURCHASEITEM ||
                                 hasDescendant  || !hasAncestor );

		LOGGER.traceExit(log4jEntryMsg);
	}

	@FXML
	private void ctxmnuChangeOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuChangeOnAction()");
		IndexedDiaryBean ixBean = diaryTable.getSelectionModel().getSelectedItem();
		DiaryBean selBean = ixBean.getItem();
		if (selBean == null)
		{
			return;
		}
		switch (selBean.getItemType())
		{
			case HUSBANDRY -> {
				LOGGER.debug("DiaryTabController: ctxmnuChangeOnAction: husbandry: {}", selBean);
				HusbandryEditor tabCon = new HusbandryEditor((HusbandryBean) (selBean.getItem()));
				loadTab.accept(resources.getString("tab.husbandry"), tabCon);
				tabCon.deletedBeanProperty().addListener((obs, oldVal, newVal) -> diaryTable.getItems().remove(ixBean));
			}
			case PURCHASE -> {
				LOGGER.debug("DiaryTabController: ctxmnuChangeOnAction: purchase: {}", selBean);
				final PurchaseEditor tabCon = new PurchaseEditor((PurchaseBean) (selBean.getItem()));
				loadTab.accept(resources.getString("tab.purchase"), tabCon);
				tabCon.deletedBeanProperty().addListener((obs, oldVal, newVal) -> diaryTable.getItems().remove(ixBean));
				//	2.9.6
				//	newBeanProperty is changed when the editor is Saved - whether for a new purchase or a changed, extant purchase
				//	The logic here is to drop the current purchase and all its PIs from the Diary then add them all back from
				//	the returned PurchaseBean - this handles all the updates, including added, changed and deleted PIs
				//	The Purchase is dropped and re-added to keep it with the PIs
				//
				//	With the subclassed DiaryBean* implementation, the removeAll() deleted EVERYTHING in theDiary.  This
				//	might be because ALL the beans had the same hashCode().  It is IMPERATIVE that each sub-classed
				//	DiaryBean has a proper hashCode() and equals() implemented (IntelliJ Alt+Insert)
				tabCon.newBeanProperty().addListener((obs, oldVal, newVal) -> {
					if (newVal == null) return;
					final PurchaseBean pb = (PurchaseBean) (selBean.getItem());
					LOGGER.debug("newval==pb: {}", newVal==pb);
					final List<DiaryBean> ldb = new ArrayList<>();	//	the DiaryBeans to drop from the Diary
					for (var db : theDiary)
					{
						if (db.getItemType() == NotebookEntryType.PURCHASEITEM)
						{
							PurchaseItemBean pib = (PurchaseItemBean)(db.getItem());
							if (pib.getPurchase().sameAs(pb) )
							{
								ldb.add(db);
							}
						}
					}
					theDiary.removeAll(ldb);
					theDiary.remove(selBean);
					theDiary.add(selBean);
					try {
						var pibs = newVal.getPurchaseItem();

						for (var pi : pibs)
						{
							theDiary.add(DiaryBean.from(pi));
						}

					} catch (GNDBException ex) {
						PanicHandler.panic(ex);
					}
					updateDiary();
				});
			}
			case PURCHASEITEM -> {
				LOGGER.debug("DiaryTabController: ctxmnuChangeOnAction: purchaseItem: {}", selBean);
				final PurchaseItemBean pur = (PurchaseItemBean) (selBean.getItem());
				final PurchaseEditor tabCon = new PurchaseEditor(pur.getPurchase());
				loadTab.accept(resources.getString("tab.purchase"), tabCon);
				tabCon.deletedBeanProperty().addListener((obs, oldVal, newVal) -> diaryTable.getItems().remove(ixBean));
				//	2.9.6
				//	newBeanProperty is changed when the editor is Saved - whether for a new purchase or a changed, extant purchase
				//	Here, the item returned is the PurchaseBean owning the PI
				//	The logic here is to drop the current purchase and all its PIs from the Diary then add them all back from
				//	the returned PurchaseBean - this handles all the updates, including added, changed and deleted PIs
				//	The Purchase is dropped and re-added to keep it with the PIs
				//
				//	With the subclassed DiaryBean* implementation, the removeAll() deleted EVERYTHING in theDiary.  This
				//	might be because ALL the beans had the same hashCode().  It is IMPERATIVE that each sub-classed
				//	DiaryBean has a proper hashCode() and equals() implemented (IntelliJ Alt+Insert)
				tabCon.newBeanProperty().addListener((obs, oldVal, newVal) -> {
					if (newVal == null) return;
					final PurchaseBean pb = pur.getPurchase();
					LOGGER.debug("newval==pb: {}", newVal==pb);
					final List<DiaryBean> ldb = new ArrayList<>();	//	the DiaryBeans to drop from the Diary
					DiaryBean pdb = null;	//	the PU owning the selected PI
					for (var db : theDiary)
					{
						if (db.getItemType() == NotebookEntryType.PURCHASEITEM)
						{
							PurchaseItemBean pib = (PurchaseItemBean)(db.getItem());
							if (pib.getPurchase().sameAs(pb) )
							{
								ldb.add(db);
							}
						}
						else if (db.getItemType() == NotebookEntryType.PURCHASE)
						{
							if (db.getItem().sameAs(pb))
							{
								ldb.add(db);
								pdb = db;
							}
						}

					}
//					LOGGER.debug("ldb: {}", ldb);
//					LOGGER.debug("theDiary: before removeAll: {}", theDiary);
					theDiary.removeAll(ldb);
//					LOGGER.debug("theDiary: after removeAll: {}", theDiary);
					theDiary.add(pdb);
					try {
						var pibs = newVal.getPurchaseItem();

						for (var pi : pibs)
						{
							theDiary.add(DiaryBean.from(pi));
						}

					} catch (GNDBException ex) {
						PanicHandler.panic(ex);
					}
//					LOGGER.debug("theDiary: before update: {}", theDiary);
					updateDiary();
				});
			}
			case SALE -> {
				LOGGER.debug("DiaryTabController: ctxmnuChangeOnAction: sale: {}", selBean);
				final SaleEditor tabCon = new SaleEditor((SaleBean) (selBean.getItem()));
				loadTab.accept(resources.getString("tab.sales"), tabCon);
				tabCon.deletedBeanProperty().addListener((obs, oldVal, newVal) -> diaryTable.getItems().remove(ixBean));
				//	2.9.6
				//	newBeanProperty is changed when the editor is Saved - whether for a new sale or a changed, extant sale
				//	The logic here is to drop the current sale and all its SIs from the Diary then add them all back from
				//	the returned SaleBean - this handles all the updates, including added, changed and deleted SIs
				//	The Sale is dropped and re-added to keep it with the SIs
				tabCon.newBeanProperty().addListener((obs, oldVal, newVal) -> {
					LOGGER.debug("DiaryTabController: ctxmnuChangeOnAction(Sale): changeListener: oldVal: {}, newVal: {}", oldVal, newVal);
					if (newVal == null) return;
					final SaleBean sb = (SaleBean)(selBean.getItem());
					final List<DiaryBean> ldb = new ArrayList<>();	//	the DiaryBeans to drop from the Diary
					for (var db : theDiary)
					{
						if (db.getItemType() == NotebookEntryType.SALEITEM)
						{
							SaleItemBean sib = (SaleItemBean)(db.getItem());
							if (sib.getSale().sameAs(sb) )
							{
								ldb.add(db);
							}
						}
					}
					theDiary.removeAll(ldb);
					theDiary.remove(selBean);
					theDiary.add(selBean);
					LOGGER.debug("DiaryTabController: ctxmnuChangeOnAction(Sale): changeListener: about to add SIs");
					try {
						var sibs = newVal.getSaleItem();

						for (var si : sibs)
						{
							LOGGER.debug("adding SI: {}", si);
							theDiary.add(DiaryBean.from(si));
						}
					} catch (GNDBException ex) {
						PanicHandler.panic(ex);
					}
					updateDiary();
				});
			}
			case SALEITEM -> {
				LOGGER.debug("DiaryTabController: ctxmnuChangeOnAction: saleItem: {}", selBean);
				final SaleEditor tabCon = new SaleEditor(((SaleItemBean) (selBean.getItem())).getSale());
				final SaleItemBean sib = (SaleItemBean)(selBean.getItem());
				loadTab.accept(resources.getString("tab.sales"), tabCon);
				tabCon.deletedBeanProperty().addListener((obs, oldVal, newVal) -> diaryTable.getItems().remove(ixBean));
				//	2.9.6
				//	newBeanProperty is changed when the editor is Saved - whether for a new sale or a changed, extant sale
				//	Here, the item returned is the SaleBean owning the SI
				//	The logic here is to drop the current sale and all its SIs from the Diary then add them all back from
				//	the returned SaleBean - this handles all the updates, including added, changed and deleted SIs
				//	The Sale is dropped and re-added to keep it with the SIs
				tabCon.newBeanProperty().addListener((obs, oldVal, newVal) -> {
					LOGGER.debug("DiaryTabController: ctxmnuChangeOnAction(Sale): changeListener: oldVal: {}, newVal: {}", oldVal, newVal);
					if (newVal == null) return;
					final SaleBean sb = sib.getSale();	//	the Sale for this SaleItem
					final List<DiaryBean> ldb = new ArrayList<>();	//	the DiaryBeans to drop from the Diary
					DiaryBean sdb = null;	//	the SA owning the selected SI
					for (var db : theDiary)
					{
						if (db.getItemType() == NotebookEntryType.SALEITEM)
						{
							SaleItemBean saleib = (SaleItemBean)(db.getItem());
							if (saleib.getSale().sameAs(sb) )
							{
								ldb.add(db);
							}
						}
						else if (db.getItemType() == NotebookEntryType.SALE)
						{
							if (db.getItem().sameAs(sb))
							{
								ldb.add(db);
								sdb = db;
							}
						}

					}
					theDiary.removeAll(ldb);
					theDiary.add(sdb);
					LOGGER.debug("DiaryTabController: ctxmnuChangeOnAction(Sale): changeListener: about to add SIs");
					try {
						var sibs = newVal.getSaleItem();

						for (var si : sibs)
						{
							LOGGER.debug("adding SI: {}", si);
							theDiary.add(DiaryBean.from(si));
						}

					} catch (GNDBException ex) {
						PanicHandler.panic(ex);
					}
					updateDiary();
				});
			}
			case GROUNDWORK -> {
				LOGGER.debug("DiaryTabController: ctxmnuChangeOnAction: groundwork: {}", selBean);
				GroundworkEditor tabCon = new GroundworkEditor((GroundworkBean) (selBean.getItem()));
				loadTab.accept(resources.getString("tab.groundwork"), tabCon);
				tabCon.deletedBeanProperty().addListener((obs, oldVal, newVal) -> diaryTable.getItems().remove(ixBean));
			}
			case AFFLICTIONEVENT -> {
				LOGGER.debug("DiaryTabController: ctxmnuChangeOnAction: afflictionEvent: {}", selBean);
				AfflictionEventEditor tabCon = new AfflictionEventEditor((AfflictionEventBean) (selBean.getItem()));
				loadTab.accept(resources.getString("tab.affliction"), tabCon);
				tabCon.deletedBeanProperty().addListener((obs, oldVal, newVal) -> diaryTable.getItems().remove(ixBean));
			}
			case WEATHER -> {
				LOGGER.debug("DiaryTabController: ctxmnuChangeOnAction: weather: {}", selBean);
				WeatherEditor tabCon = new WeatherEditor((WeatherBean) (selBean.getItem()));
				loadTab.accept(resources.getString("tab.weather"), tabCon);
				tabCon.deletedBeanProperty().addListener((obs, oldVal, newVal) -> diaryTable.getItems().remove(ixBean));
			}
			case WILDLIFE -> {
				LOGGER.debug("DiaryTabController: ctxmnuChangeOnAction: wildlife: {}", selBean);
				WildlifeEditor tabCon = new WildlifeEditor((WildlifeBean) (selBean.getItem()));
				loadTab.accept(resources.getString("tab.wildlife"), tabCon);
				tabCon.deletedBeanProperty().addListener((obs, oldVal, newVal) -> diaryTable.getItems().remove(ixBean));
			}
			case JOURNAL -> {
				LOGGER.debug("DiaryTabController: ctxmnuChangeOnAction: journal: {}", selBean);
				JournalEditor tabCon = new JournalEditor((JournalBean) (selBean.getItem()));
				loadTab.accept(resources.getString("tab.journal"), tabCon);
				tabCon.deletedBeanProperty().addListener((obs, oldVal, newVal) -> diaryTable.getItems().remove(ixBean));
			}
		}

		Platform.runLater(() -> diaryTable.requestFocus());	//	this enables keyboard navigation with the up/down arrows

		LOGGER.traceExit(log4jEntryMsg);
	}   //  ctxmnuChangeOnAction

	@FXML
	private void ctxmnuDeleteOnAction(ActionEvent event) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuDeleteOnAction()");
		final IndexedDiaryBean ixBean = diaryTable.getSelectionModel().getSelectedItem();
		int currIx = diaryTable.getItems().indexOf(ixBean);
		if (ixBean == null)
		{
			return;
		}
		if (!ixBean.canDelete())
		{
			return;
		}
		DiaryBean selBean = ixBean.getItem();
		if (selBean == null)
		{
			return;
		}
		Alert checkDelete = new Alert(Alert.AlertType.CONFIRMATION, resources.getString("alert.confirmdelete"), ButtonType.NO, ButtonType.YES);
		Optional<ButtonType> result = checkDelete.showAndWait();
		LOGGER.debug("after delete dialog: result:{}, result.get:{}", result, result.orElse(null));
		if (result.isPresent() && result.get() == ButtonType.YES)
		{
			LOGGER.debug("after delete confirmed: bean type:{}", selBean.getItemType());
			switch (selBean.getItemType())
			{
				case HUSBANDRY -> ((HusbandryBean) (selBean.getItem())).delete();
				case PURCHASE -> ((PurchaseBean) (selBean.getItem())).delete();
				case PURCHASEITEM -> ((PurchaseItemBean) (selBean.getItem())).delete();
				case SALE -> ((SaleBean) (selBean.getItem())).delete();
				case SALEITEM -> ((SaleItemBean) (selBean.getItem())).delete();
				case GROUNDWORK -> ((GroundworkBean) (selBean.getItem())).delete();
				case AFFLICTIONEVENT -> ((AfflictionEventBean) (selBean.getItem())).delete();
				case WEATHER -> ((WeatherBean) (selBean.getItem())).delete();
				case WILDLIFE -> ((WildlifeBean) (selBean.getItem())).delete();
				case JOURNAL -> ((JournalBean) (selBean.getItem())).delete();
			}
			theDiary.remove(selBean);
			updateDiary();
			diaryTable.scrollTo(currIx);
		}
		LOGGER.traceExit(log4jEntryMsg);
	}	//	ctxmnuDeleteOnAction()

	@FXML
	private void ctxmnuAncestorsOnAction(ActionEvent event) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuAncestorsOnAction()");
		final IndexedDiaryBean ixBean = diaryTable.getSelectionModel().getSelectedItem();
		if (ixBean == null)
		{
			return;
		}
		if (!ixBean.hasAncestor())
		{
			return;
		}
		DiaryBean selBean = ixBean.getItem();
		if (selBean == null)
		{
			return;
		}
		StoryLineTree<DiaryBean> history = selBean.getAncestors();
		StoryLineTab tabCon = new StoryLineTab();
		loadTab.accept(resources.getString("tab.ancestors"), tabCon);
		tabCon.newBeanProperty().addListener((obs, oldVal, newVal) -> {
			LOGGER.debug("ctxmnuAncestorsOnAction(): newBean listener: newVal: {}", newVal);
			theDiary.add(newVal);
			updateDiary();
		});
		tabCon.setHistory(history);
		LOGGER.traceExit(log4jEntryMsg);
	}	//	ctxmnuAncestorsOnAction()
	
	@FXML
	private void ctxmnuDescendantsOnAction(ActionEvent event) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuDescendantsOnAction()");
		final IndexedDiaryBean ixBean = diaryTable.getSelectionModel().getSelectedItem();

		if (ixBean == null)
		{
			return;
		}
		if (!ixBean.hasDescendant())
		{
			return;
		}
		DiaryBean selBean = ixBean.getItem();
		if (selBean == null)
		{
			return;
		}
		StoryLineTree<DiaryBean> history = selBean.getDescendants();
		StoryLineTab tabCon = new StoryLineTab();
		loadTab.accept(resources.getString("tab.descendants"), tabCon);
		tabCon.newBeanProperty().addListener((obs, oldVal, newVal) -> {
			LOGGER.debug("ctxmnuDescendantsOnAction(): newBean listener: newVal: {}", newVal);
			theDiary.add(newVal);
			updateDiary();
		});
		tabCon.setHistory(history);
		LOGGER.traceExit(log4jEntryMsg);
	}	//	ctxmnuDescendantsOnAction()

	@FXML
	private void ctxmnuDescendantHusbandryOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuDescendantHusbandryOnAction()");
		final IndexedDiaryBean ixBean = diaryTable.getSelectionModel().getSelectedItem();
		if (ixBean == null)
		{
			return;
		}
		final DiaryBean selBean = ixBean.getItem();
		if (selBean == null)
		{
			return;
		}
		
//		PlantSpeciesBean psBean = null;
//		PlantVarietyBean pvBean = null;
//		switch (selBean.getItemType())
//		{
//			case HUSBANDRY -> {
//				psBean = ((HusbandryBean) (selBean.getItem())).getPlantSpecies();
//				pvBean = ((HusbandryBean) (selBean.getItem())).getPlantVariety();
//			}
//			case PURCHASEITEM -> {
//				PurchaseItemBean piBean = ((PurchaseItemBean) (selBean.getItem()));
//				if (piBean.getProduct().getProductCategory().isPlantLike())
//				{
//					psBean = piBean.getProduct().getPlantSpecies();
//					pvBean = piBean.getProduct().getPlantVariety();
//				}
//			}
//			case GROUNDWORK -> {
//				psBean = ((GroundworkBean) (selBean.getItem())).getPlantSpecies();
//				pvBean = ((GroundworkBean) (selBean.getItem())).getPlantVariety();
//			}
//			case AFFLICTIONEVENT -> {
//				psBean = ((AfflictionEventBean) (selBean.getItem())).getPlantSpecies();
//				pvBean = ((AfflictionEventBean) (selBean.getItem())).getPlantVariety();
//			}
//			default -> {
//				LOGGER.debug("ctxmnuDescendantHusbandryOnAction(): illegal parent type: {}", selBean);
//				return;
//			}
//		}
//		final HusbandryEditor tabCon = new HusbandryEditor(psBean, pvBean);

		//	For a Husbandry, there must be a PlantSpecies to inherit
		switch (selBean.getItemType())
		{
			case PURCHASE -> {
				return;
			}
			case PURCHASEITEM -> {if ( !((PurchaseItemBean)(selBean.getItem())).getProductCategory().isPlantLike() )
								{
									return;
								}}
			case GROUNDWORK -> {
				if (!((GroundworkBean) (selBean.getItem())).hasPlantSpecies())
					return;
			}
			case AFFLICTIONEVENT -> {
				if (!((AfflictionEventBean) (selBean.getItem())).hasPlantSpecies())
					return;
			}
			default ->	{}
		}

		final HusbandryEditor tabCon = new HusbandryEditor(selBean.getItem(), true);
		loadTab.accept(resources.getString("tab.husbandry"), tabCon);
		LOGGER.debug("ctxmnuDescendantHusbandryOnAction(): after pop-up");
		tabCon.newBeanProperty().addListener((obs, oldVal, newVal) -> {
			LOGGER.debug("ctxmnuDescendantHusbandryOnAction(): newBean listener: newVal: {}", newVal);
			theDiary.add(DiaryBean.from(newVal));
			try {
				switch (selBean.getItemType())
				{
					case HUSBANDRY -> newVal.setAncestor((HusbandryBean) (selBean.getItem()));
					case PURCHASEITEM -> newVal.setAncestor((PurchaseItemBean) (selBean.getItem()));
					case GROUNDWORK -> newVal.setAncestor((GroundworkBean) (selBean.getItem()));
					case AFFLICTIONEVENT -> newVal.setAncestor((AfflictionEventBean) (selBean.getItem()));
					default -> {
						LOGGER.debug("ctxmnuDescendantHusbandryOnAction(): illegal parent type: {}", selBean);
						return;
					}
				}
				updateDiary();
			} catch (GNDBException ex) {
				PanicHandler.panic(ex);
			}
		});
		LOGGER.traceExit(log4jEntryMsg);
	}	//	ctxmnuDescendantHusbandryOnAction()

	@FXML
	private void ctxmnuDescendantGroundworkOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuNewGroundworkOnAction()");
		final IndexedDiaryBean ixBean = diaryTable.getSelectionModel().getSelectedItem();
		if (ixBean == null)
		{
			return;
		}
		final DiaryBean selBean = ixBean.getItem();
		if (selBean == null)
		{
			return;
		}
		
//		PlantSpeciesBean psBean = null;
//		PlantVarietyBean pvBean = null;
//		switch (selBean.getItemType())
//		{
//			case HUSBANDRY -> {
//				psBean = ((HusbandryBean) (selBean.getItem())).getPlantSpecies();
//				pvBean = ((HusbandryBean) (selBean.getItem())).getPlantVariety();
//			}
//			case PURCHASEITEM -> {
//				PurchaseItemBean piBean = ((PurchaseItemBean) (selBean.getItem()));
//				if (piBean.getProduct().getProductCategory().isPlantLike())
//				{
//					psBean = piBean.getProduct().getPlantSpecies();
//					pvBean = piBean.getProduct().getPlantVariety();
//				}
//			}
//			case GROUNDWORK -> {
//				psBean = ((GroundworkBean) (selBean.getItem())).getPlantSpecies();
//				pvBean = ((GroundworkBean) (selBean.getItem())).getPlantVariety();
//			}
//			case AFFLICTIONEVENT -> {
//				psBean = ((AfflictionEventBean) (selBean.getItem())).getPlantSpecies();
//				pvBean = ((AfflictionEventBean) (selBean.getItem())).getPlantVariety();
//			}
//			default -> {
//				LOGGER.debug("ctxmnuDescendantGroundworkOnAction(): illegal parent type: {}", selBean);
//				return;
//			}
//		}
//		final GroundworkEditor tabCon = new GroundworkEditor(psBean, pvBean);
		final GroundworkEditor tabCon = new GroundworkEditor(selBean.getItem(), true);
		loadTab.accept(resources.getString("tab.groundwork"), tabCon);
		LOGGER.debug("ctxmnuDescendantGroundworkOnAction(): after pop-up");
		tabCon.newBeanProperty().addListener((obs, oldVal, newVal) -> {
			LOGGER.debug("ctxmnuDescendantGroundworkOnAction(): newBean listener: newVal: {}", newVal);
			theDiary.add(DiaryBean.from(newVal));
			try {
				switch (selBean.getItemType())
				{
					case HUSBANDRY -> newVal.setAncestor((HusbandryBean) (selBean.getItem()));
					case PURCHASEITEM -> newVal.setAncestor((PurchaseItemBean) (selBean.getItem()));
					case GROUNDWORK -> newVal.setAncestor((GroundworkBean) (selBean.getItem()));
					case AFFLICTIONEVENT -> newVal.setAncestor((AfflictionEventBean) (selBean.getItem()));
					default -> {
						LOGGER.debug("ctxmnuDescendantGroundworkOnAction(): illegal parent type: {}", selBean);
						return;
					}
				}
				updateDiary();
			} catch (GNDBException ex) {
				PanicHandler.panic(ex);
			}
		});
		LOGGER.traceExit(log4jEntryMsg);
	}	//	ctxmnuDescendantGroundworkOnAction()

	@FXML
	private void ctxmnuDescendantAfflictionOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuNewAfflictionOnAction()");
		final IndexedDiaryBean ixBean = diaryTable.getSelectionModel().getSelectedItem();
		if (ixBean == null)
		{
			return;
		}
		final DiaryBean selBean = ixBean.getItem();
		if (selBean == null)
		{
			return;
		}
		
//		PlantSpeciesBean psBean = null;
//		PlantVarietyBean pvBean = null;
//		switch (selBean.getItemType())
//		{
//			case HUSBANDRY -> {
//				psBean = ((HusbandryBean) (selBean.getItem())).getPlantSpecies();
//				pvBean = ((HusbandryBean) (selBean.getItem())).getPlantVariety();
//			}
//			case PURCHASEITEM -> {
//				PurchaseItemBean piBean = ((PurchaseItemBean) (selBean.getItem()));
//				if (piBean.getProduct().getProductCategory().isPlantLike())
//				{
//					psBean = piBean.getProduct().getPlantSpecies();
//					pvBean = piBean.getProduct().getPlantVariety();
//				}
//			}
//			case GROUNDWORK -> {
//				psBean = ((GroundworkBean) (selBean.getItem())).getPlantSpecies();
//				pvBean = ((GroundworkBean) (selBean.getItem())).getPlantVariety();
//			}
//			case AFFLICTIONEVENT -> {
//				psBean = ((AfflictionEventBean) (selBean.getItem())).getPlantSpecies();
//				pvBean = ((AfflictionEventBean) (selBean.getItem())).getPlantVariety();
//			}
//			default -> {
//				LOGGER.debug("ctxmnuDescendantAfflictionOnAction(): illegal parent type: {}", selBean);
//				return;
//			}
//		}
//		final AfflictionEventEditor tabCon = new AfflictionEventEditor(psBean, pvBean);
		final AfflictionEventEditor tabCon = new AfflictionEventEditor(selBean.getItem(), true);
		loadTab.accept(resources.getString("tab.affliction"), tabCon);
		LOGGER.debug("ctxmnuDescendantAfflictionOnAction(): after pop-up");
		tabCon.newBeanProperty().addListener((obs, oldVal, newVal) -> {
			LOGGER.debug("ctxmnuDescendantAfflictionOnAction(): newBean listener: newVal: {}", newVal);
			theDiary.add(DiaryBean.from(newVal));
			try {
				switch (selBean.getItemType())
				{
					case HUSBANDRY -> newVal.setAncestor((HusbandryBean) (selBean.getItem()));
					case PURCHASEITEM -> newVal.setAncestor((PurchaseItemBean) (selBean.getItem()));
					case GROUNDWORK -> newVal.setAncestor((GroundworkBean) (selBean.getItem()));
					case AFFLICTIONEVENT -> newVal.setAncestor((AfflictionEventBean) (selBean.getItem()));
					default -> {
						LOGGER.debug("ctxmnuDescendantAfflictionOnAction(): illegal parent type: {}", selBean);
						return;
					}
				}
				updateDiary();
			} catch (GNDBException ex) {
				PanicHandler.panic(ex);
			}
		});
		LOGGER.traceExit(log4jEntryMsg);
	}	//	ctxmnuDescendantAfflictionOnAction()

	@FXML
	private void ctxmnuDescendantSaleOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuDescendantSaleOnAction()");
		final IndexedDiaryBean ixBean = diaryTable.getSelectionModel().getSelectedItem();
		if (ixBean == null)
		{
			return;
		}
		final DiaryBean selBean = ixBean.getItem();
		if (selBean == null)
		{
			return;
		}
        
        if (selBean.getItemType() == NotebookEntryType.PURCHASEITEM)
        {// SaleItems cannot be direct descendants of PurchaseItems
            return;
        }
		
//		PlantSpeciesBean psBean = null;
//		PlantVarietyBean pvBean = null;
//		switch (selBean.getItemType())
//		{
//			case HUSBANDRY -> {
//				psBean = ((HusbandryBean) (selBean.getItem())).getPlantSpecies();
//				pvBean = ((HusbandryBean) (selBean.getItem())).getPlantVariety();
//			}
//			case GROUNDWORK -> {
//				psBean = ((GroundworkBean) (selBean.getItem())).getPlantSpecies();
//				pvBean = ((GroundworkBean) (selBean.getItem())).getPlantVariety();
//			}
//			case AFFLICTIONEVENT -> {
//				psBean = ((AfflictionEventBean) (selBean.getItem())).getPlantSpecies();
//				pvBean = ((AfflictionEventBean) (selBean.getItem())).getPlantVariety();
//			}
//			default -> {
//				LOGGER.debug("ctxmnuDescendantSaleOnAction(): illegal parent type: {}", selBean);
//				return;
//			}
//		}
//		final SaleEditor tabCon = new SaleEditor(psBean, pvBean);

		//	For a Sale, there must be a PlantSpecies to inherit
		switch (selBean.getItemType())
		{
			case PURCHASE, PURCHASEITEM -> {
				return;
			}
			case GROUNDWORK -> {
				if (!((GroundworkBean) (selBean.getItem())).hasPlantSpecies())
					return;
			}
			case AFFLICTIONEVENT -> {
				if (!((AfflictionEventBean) (selBean.getItem())).hasPlantSpecies())
					return;
			}
			default ->	{}
		}

		final SaleEditor tabCon = new SaleEditor(selBean.getItem(), true);
		loadTab.accept(resources.getString("tab.sales"), tabCon);
		LOGGER.debug("ctxmnuDescendantSaleOnAction(): after pop-up");
		tabCon.newLinkedItemBeanProperty().addListener((obs, oldVal, newVal) -> {
			LOGGER.debug("ctxmnuDescendantSaleOnAction(): newBean listener: newVal: {}", newVal);
            {
                SaleBean newSale = newVal.getSale();
				theDiary.add(DiaryBean.from(newSale));
                try {
                    for (SaleItemBean newItem : newSale.getSaleItem())
                    {
						theDiary.add(DiaryBean.from(newItem));
                    }
                } catch (GNDBException ex) {
                    PanicHandler.panic(ex);
                }
            }
			try {
				switch (selBean.getItemType())
				{
					case HUSBANDRY -> newVal.setAncestor((HusbandryBean) (selBean.getItem()));
					case GROUNDWORK -> newVal.setAncestor((GroundworkBean) (selBean.getItem()));
					case AFFLICTIONEVENT -> newVal.setAncestor((AfflictionEventBean) (selBean.getItem()));
					default -> {
						LOGGER.debug("ctxmnuDescendantSaleOnAction(): illegal parent type: {}", selBean);
						return;
					}
				}
				updateDiary();
			} catch (GNDBException ex) {
				PanicHandler.panic(ex);
			}
		});
		LOGGER.traceExit(log4jEntryMsg);
	}	//	ctxmnuDescendantSaleOnAction()

    @FXML
    /**
     * Disconnect a leaf item from its ancestor
     * @since 2.6.0
     */    
	private void ctxmnuDropLeafOnAction(ActionEvent event) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuDropLeafOnAction()");
		final IndexedDiaryBean ixBean = diaryTable.getSelectionModel().getSelectedItem();
		if (ixBean == null)
		{
			return;
		}
		final DiaryBean selBean = ixBean.getItem();
		if (selBean == null)
		{
			return;
		}
        
        if (selBean.getItemType() == NotebookEntryType.PURCHASEITEM)
        {// PurchaseItems cannot have ancestors
            return;
        }
        
        if (selBean.hasDescendant())
        {// not a leaf, cannot disconnect a subtree
            return;
        }
        if (!selBean.hasAncestor())
        {// nothing to do!
            return;
        }
		
		Alert checkDelete = new Alert(Alert.AlertType.CONFIRMATION, resources.getString("alert.confirmdropleaf"), ButtonType.NO, ButtonType.YES);
		Optional<ButtonType> result = checkDelete.showAndWait();
		LOGGER.debug("after dropleaf dialog: result:{}, result.get:{}",result, result.orElse(null));
		if (result.isPresent() && result.get() == ButtonType.YES)
		{
			LOGGER.debug("after delete confirmed: bean type:{}", selBean.getItemType());
			switch (selBean.getItemType())
			{
				case HUSBANDRY -> ((HusbandryBean) (selBean.getItem())).dropLeaf();
				case SALEITEM -> ((SaleItemBean) (selBean.getItem())).dropLeaf();
				case GROUNDWORK -> ((GroundworkBean) (selBean.getItem())).dropLeaf();
				case AFFLICTIONEVENT -> ((AfflictionEventBean) (selBean.getItem())).dropLeaf();
				default -> {
					LOGGER.debug("ctxmnuDropLeafOnAction(): illegal parent type: {}", selBean);
					return;
				}
			}
			updateDiary();
		}
		LOGGER.traceExit(log4jEntryMsg);
    }   //  ctxmnuDropLeafOnAction()

	public void setDiary(List<DiaryBean> diary)
	{
//		LOGGER.debug("setDiary(): diary: {}", diary);
		theDiary = diary;
		updateDiary();
		diaryTable.getSelectionModel().selectFirst();
		Platform.runLater(() -> diaryTable.requestFocus());	//	this enables keyboard navigation with the up/down arrows
	}
	
	private void updateDiary()
	{
		ObservableList<IndexedDiaryBean> theIndex = buildIndexes(theDiary);
		diaryTable.getItems().setAll(theIndex);
	}
	
	@Override
	public void setLoadSplit(Consumer<Node> code)
	{
		loadSplit = code;
	}
	
	@Override
	public void setClearSplit(Consumer<Node> code)
	{
		clearSplit = code;
	}
	
	@Override
	public void setLoadTab(BiConsumer<String, Node> code)
	{
		loadTab = code;
	}

	@Override
	public void setClearTab(Consumer<Node> code)
	{
		clearTab = code;
	}

	private ObservableList<IndexedDiaryBean> buildIndexes(List<DiaryBean> theDiary)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("buildIndexes(): theDiary: {}", theDiary);
		ObservableList<IndexedDiaryBean> theIndex = FXCollections.observableArrayList();
		
		theDiary.sort(Comparator.comparing(DiaryBean::getDate));

		// number of rows in this date
		Map<LocalDate, Long> dateSpan = new TreeMap<>(theDiary.stream().collect(Collectors.groupingBy(DiaryBean::getDate, Collectors.counting())));

		Map<Integer, Long> purSpan = new TreeMap<>();	// number of rows in this purchase (PU + PIs)
		purSpan.putAll( theDiary.stream().
			filter(item -> item.getItemType()==NotebookEntryType.PURCHASE || item.getItemType()==NotebookEntryType.PURCHASEITEM).
			collect(Collectors.groupingBy(item -> item.getItemType()==NotebookEntryType.PURCHASE ? item.getKey() : ((PurchaseItemBean)(item.getItem())).getPurchase().getKey(), 
				Collectors.counting())) );

		Map<Integer, Long> saleSpan = new TreeMap<>();	// number of rows in this sale (SA + SIs)
		saleSpan.putAll( theDiary.stream().
				filter(item -> item.getItemType()==NotebookEntryType.SALE || item.getItemType()==NotebookEntryType.SALEITEM).
				collect(Collectors.groupingBy(item -> item.getItemType()==NotebookEntryType.SALE ? item.getKey() : ((SaleItemBean)(item.getItem())).getSale().getKey(),
						Collectors.counting())) );

		int dateIndex = -1;		//	the Diary 'page' - the block of entries for a date
		int dateLen;		//	the size of the diary page
		int blockRow = 0;	//	the current row in the diary page
		int purchaseSpan = 1;	//	the length of the block of lines for a Purchase
		int inPurchaseIndex = 0;	//	current line in a Purchase block
		int saleLen = 1;	//	the length of the block of lines for a Sale
		int inSaleIndex = 0;	//	current line in a Sale block
		LocalDate currDate = LocalDate.MIN;
		
		for (DiaryBean db : theDiary)
		{
			if (db.getDate().isAfter(currDate))
			{
				dateIndex++;
				dateLen = 0;
				blockRow = 0;
				currDate = db.getDate();
				inPurchaseIndex = 0;
				purchaseSpan = 1;
				inSaleIndex = 0;
				saleLen = 1;
			}
			dateLen = dateSpan.get(currDate).intValue();
			
			if (db.getItemType() == NotebookEntryType.PURCHASE)
			{
				LOGGER.debug("Purchase: {}", db.getItem());
				inPurchaseIndex = 0;
				purchaseSpan = purSpan.get(db.getKey()).intValue();
			}
			if (db.getItemType() == NotebookEntryType.PURCHASEITEM)
			{
				LOGGER.debug("PurchaseItem: {}", db.getItem());
				inPurchaseIndex += 1;
			}

			if (db.getItemType() == NotebookEntryType.SALE)
			{
				LOGGER.debug("Sale: {}", db.getItem());
				//	need to clear purchase block values
				inPurchaseIndex = 0;
				purchaseSpan = 1;
				//	now set up the sale block
				inSaleIndex = 0;
				saleLen = saleSpan.get(db.getKey()).intValue();
			}
			if (db.getItemType() == NotebookEntryType.SALEITEM)
			{
				LOGGER.debug("SaleItem: {}", db.getItem());
				inSaleIndex += 1;
			}

			IndexedDiaryBean idb = new IndexedDiaryBean(db, dateIndex, dateLen, blockRow++, purchaseSpan, inPurchaseIndex, saleLen, inSaleIndex);

			LOGGER.debug("adding index: {}", idb);
			theIndex.add(idb);
		}
		LOGGER.debug("theIndex: {}", theIndex);
		return theIndex;
	}

	private class DateTableCell extends TableCell<IndexedDiaryBean, IndexedDiaryBean>
	{
		@Override
		protected void updateItem(IndexedDiaryBean item, boolean empty) {
			super.updateItem(item, empty);
			if (item == null || empty)
			{
				setText(null);
				setGraphic(null);
				return;
			}
			updateViewMode();
		}			

		@Override
		public void startEdit() {
			super.startEdit();
			updateViewMode();
		}

		@Override
		public void cancelEdit() {
			super.cancelEdit();
			updateViewMode();
		}
		private void updateViewMode()
		{
			setGraphic(null);
			setText(null);
			if (getItem() == null)
				return;

//			this.setEditable(getItem().getInDateIndex() <= 0);
			this.setEditable(getItem().inDateIndex() <= 0);

			// use CSS to emulate spanned dates
			setCellStyles(this, getItem());

			if (isEditing())
			{
				VBox vBox = new VBox();
				DatePicker dp = new DatePicker(getItem().diaryBean().getDate());
				dp.setOnAction(ev ->  {
					getItem().diaryBean().setDate(dp.getValue());
					commitEdit(getItem());
					});
				vBox.getChildren().add(dp);
				setGraphic(vBox);
			}
//			else if (getItem().getInDateIndex() == 0)
			else if (getItem().inDateIndex() == 0)
			{
				setText(getItem().diaryBean().getDate().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)));
			}

		}
	}
		
	private class TextTableCell extends TableCell<IndexedDiaryBean, IndexedDiaryBean>
	{
		StringBinding thisFieldAsText;
		Function<ObjectProperty<AfflictionBean>, Control> afflictionEditor = (item) -> {
				AfflictionCombo cb = new AfflictionCombo(item.get());
				cb.setEditable(true);
				cb.setMandatory(true);
				cb.setOnAction(ev -> {
					item.set(cb.getSelectionModel().getSelectedItem());
					commitEdit(getItem());
				});
				return cb;
			};
		Function<ObjectProperty<GroundworkActivityBean>, Control> groundworkActivityEditor = (item) -> {
				GroundworkActivityCombo cb = new GroundworkActivityCombo(item.get());
				cb.setEditable(true);
				cb.setMandatory(true);
				cb.setOnAction(ev -> {
					item.set(cb.getSelectionModel().getSelectedItem());
					commitEdit(getItem());
				});
				return cb;
			};
		Function<ObjectProperty<HusbandryClassBean>, Control> husbandryClassEditor = (item) -> {
				HusbandryClassCombo cb = new HusbandryClassCombo(item.get());
				cb.setEditable(true);
				cb.setMandatory(true);
				cb.setOnAction(ev -> {
					item.set(cb.getSelectionModel().getSelectedItem());
					commitEdit(getItem());
				});
				return cb;
			};
//		Function<ObjectProperty<RetailerBean>, Control> retailerEditor = (item) -> {
//				RetailerCombo cb = new RetailerCombo(item.get());
//				cb.setEditable(true);
//				cb.setMandatory(true);
//				cb.setOnAction(ev -> {
//					item.set(cb.getSelectionModel().getSelectedItem());
//					commitEdit(getItem());
//				});
//				return cb;
//			};
		Function<ObjectProperty<WeatherConditionBean>, Control> weatherConditionEditor = (item) -> {
				WeatherConditionCombo cb = new WeatherConditionCombo(item.get());
				cb.setEditable(true);
				cb.setMandatory(true);
				cb.setOnAction(ev -> {
					item.set(cb.getSelectionModel().getSelectedItem());
					commitEdit(getItem());
				});
				return cb;
			};
		Function<ObjectProperty<WildlifeSpeciesBean>, Control> wildlifeSpeciesEditor = (item) -> {
				WildlifeSpeciesCombo cb = new WildlifeSpeciesCombo(item.get());
				cb.setEditable(true);
				cb.setMandatory(true);
				cb.setOnAction(ev -> {
					item.set(cb.getSelectionModel().getSelectedItem());
					commitEdit(getItem());
				});
				return cb;
			};
		Function<ObjectProperty<PlantSpeciesBean>, Control> plantSpeciesEditor = (item) -> {
				PlantSpeciesCombo cb = new PlantSpeciesCombo(item==null ? null : item.get());
				cb.setEditable(true);
				cb.setOnAction(ev -> {
					commitEdit(getItem());
					item.set(cb.getSelectionModel().getSelectedItem());
				});
				return cb;
			};
		BiFunction<ObjectProperty<PlantSpeciesBean>, ObjectProperty<PlantVarietyBean>, Control> plantVarietyEditor = (parent, item) -> {
				if (parent.get() == null)
				{
					return new Label();
				}
				PlantVarietyCombo cb = new PlantVarietyCombo(parent.get(), /*targetItem==null ? null :*/ item.get());
				cb.setEditable(true);
				cb.setOnAction(ev -> {
					commitEdit(getItem());
					item.set(cb.getSelectionModel().getSelectedItem());
				});
				return cb;
			};
		Function<ObjectProperty<LocationBean>, Control> locationEditor = (item) -> {
			LocationCombo cb = new LocationCombo(item==null ? null : item.get());
			cb.setEditable(true);
			cb.setOnAction(ev -> {
				commitEdit(getItem());
				item.set(cb.getSelectionModel().getSelectedItem());
			});
			return cb;
		};
		Function<Property<SimpleMoney>, Control> moneyEditor = (item) -> {
				TextField tf = new TextField(((ObjectProperty<SimpleMoney>)item).getValue().toString());
				tf.setOnAction(ev ->  {
					item.setValue(SimpleMoney.parse(tf.getText()));
					commitEdit(getItem());
					});
				return tf;
			};
		Function<Property<String>, Control> freeTextEditor = (item) -> {
				TextField tf = new TextField(item.getValue());
				tf.setOnAction(ev ->  {
					item.setValue(tf.getText());
					commitEdit(getItem());
					});
				return tf;
			};

		//	freeTextEditor2 is for the specific case of free text in colMainTitle.  It needs further
		//	help in DiaryBean - it's a complicated and far from obvious mess.
		//	See handling of Sale (purchasedBy) and Journal (title).
		Function<ObjectProperty<SimpleStringProperty>, Control> freeTextEditor2 = (item) -> {
			TextField tf = new TextField( ((item.getValue())).getValue() );
			tf.setOnAction(ev ->  {
				item.getValue().setValue(tf.getText());
				commitEdit(getItem());
			});
			return tf;
		};

		//	for unused columns
		Supplier<Control> nullTextEditor = Label::new;
				
		@Override
		protected void updateItem(IndexedDiaryBean item, boolean empty) {
			super.updateItem(item, empty);
			if (item == null || empty)
			{
				setText(null);
				setGraphic(null);
				return;
			}
			updateViewMode();
		}			

		@Override
		public void startEdit() {
			super.startEdit();
			updateViewMode();
		}

		@Override
		public void cancelEdit() {
			super.cancelEdit();
			updateViewMode();
		}
		private void updateViewMode()
		{
			setGraphic(null);
			setText(null);
			if (getItem() == null)
				return;
			
//			// use CSS to emulate spanned dates
			setCellStyles(this, getItem());
			
			if (this.getTableColumn() == colMainTitle)
			{
				thisFieldAsText = getItem().diaryBean().getMainTitleText();
			}
			else if (this.getTableColumn() == colSubTitle)
			{
				thisFieldAsText = getItem().diaryBean().getSubTitleText();
			}
			else if (this.getTableColumn() == colVariety)
			{
				thisFieldAsText = getItem().diaryBean().getVarietyText();
			}
			else if (this.getTableColumn() == colDetail)
			{
				thisFieldAsText = getItem().diaryBean().getDetailText();
			}
            
            //  2.6.1
            boolean inThread = false;
            try {
                LOGGER.debug("TextTableCell: updateViewMode(): check for history");
                inThread =  getItem().hasAncestor() || getItem().hasDescendant();
            }
            catch (GNDBException ex)
            {
                LOGGER.throwing(ex);
            }
            
            boolean isCellEditable = true;
			//	3.0.1
//            switch (getItem().getItem().getItemType())
//            {
//					case AFFLICTIONEVENT:
//					case GROUNDWORK:
//					case HUSBANDRY:
//					case PURCHASEITEM:
//					case SALEITEM:
//						if (this.getTableColumn() == colSubTitle || this.getTableColumn() == colVariety)
//						{
//							isCellEditable = !inThread;
//						}
//							break;
//                    default:
//                        isCellEditable = true;
//                        break;
//            }
//			switch (getItem().getItem().getItemType())
			switch (getItem().beanType())
			{
				case AFFLICTIONEVENT, GROUNDWORK, SALEITEM -> {
					if (this.getTableColumn() == colSubTitle || this.getTableColumn() == colVariety)
					{
						isCellEditable = !inThread;	//	TODO	PurchaseItem, 'watch for' check needed
					}
				}
				case HUSBANDRY -> {
					if (this.getTableColumn() == colSubTitle || this.getTableColumn() == colVariety)
					{
						HusbandryBean hb = (HusbandryBean) (getItem().getItem().getItem());
						try
						{
							isCellEditable = !inThread && !hb.hasWatchFor();
						}
						catch (GNDBException ex)
						{
							LOGGER.throwing(ex);
						}
					}
				}

				case PURCHASE -> {
					if (this.getTableColumn() == colMainTitle || this.getTableColumn() == colSubTitle)
					{	//	mainTitle is the Retailer and that means changing all the products
						//	subTitle is the text 'Total'
						isCellEditable = false;
					}
				}

				case PURCHASEITEM -> {
					if (this.getTableColumn() == colSubTitle || this.getTableColumn() == colVariety || this.getTableColumn() == colDetail)
					{//	ProductType, PlantSpecies and Variety
						isCellEditable = false;	//	it's just too difficult!
					}
				}

				case SALE -> {
					if (this.getTableColumn() == colSubTitle)
					{	//	the text 'Total'
						isCellEditable = false;
					}
				}

			}

			VBox vBox = new VBox();
			if (isEditing() && isCellEditable)
			{
				switch (getItem().getItem().getItemType())
				{
					case AFFLICTIONEVENT:
						if (this.getTableColumn() == colMainTitle)
						{	//	Affliction
							vBox.getChildren().add(this.afflictionEditor.apply(getItem().getItem().mainTitleProperty()));
						}
						else if (this.getTableColumn() == colSubTitle)
						{// PlantSpecies
                            if (inThread)
                            {
                                Label lbl = new Label();
                                lbl.textProperty().bind(thisFieldAsText);
                                vBox.getChildren().add(lbl);
                            }
                            else
                                vBox.getChildren().add(this.plantSpeciesEditor.apply((ObjectProperty<PlantSpeciesBean>) getItem().getItem().subTitleProperty()));
						}
						else if (this.getTableColumn() == colVariety)
						{// PlantVariety
							vBox.getChildren().add(this.plantVarietyEditor.apply((ObjectProperty<PlantSpeciesBean>) getItem().getItem().subTitleProperty(), (ObjectProperty<PlantVarietyBean>)getItem().getItem().varietyProperty()));
						}
						else if (this.getTableColumn() == colDetail)
						{// Location
							vBox.getChildren().add(this.locationEditor.apply((ObjectProperty<LocationBean>) getItem().getItem().detailProperty()));
						}
						else
						{
							vBox.getChildren().add(this.nullTextEditor.get());
						}
						break;
					case GROUNDWORK:
						if (this.getTableColumn() == colMainTitle)
						{
							vBox.getChildren().add(this.groundworkActivityEditor.apply(getItem().getItem().mainTitleProperty()));
						}
						else if (this.getTableColumn() == colSubTitle)
						{// PlantSpecies
							vBox.getChildren().add(this.plantSpeciesEditor.apply((ObjectProperty<PlantSpeciesBean>) getItem().getItem().subTitleProperty()));
						}
						else if (this.getTableColumn() == colVariety)
						{// PlantVariety
							vBox.getChildren().add(this.plantVarietyEditor.apply((ObjectProperty<PlantSpeciesBean>) getItem().getItem().subTitleProperty(), (ObjectProperty<PlantVarietyBean>)getItem().getItem().varietyProperty()));
						}
						else if (this.getTableColumn() == colDetail)
						{// Location
							vBox.getChildren().add(this.locationEditor.apply((ObjectProperty<LocationBean>) getItem().getItem().detailProperty()));
						}
						else
						{
							vBox.getChildren().add(this.nullTextEditor.get());
						}
						break;
					case HUSBANDRY:
						if (this.getTableColumn() == colMainTitle)
						{
							vBox.getChildren().add(this.husbandryClassEditor.apply(getItem().getItem().mainTitleProperty()));
						}
						else if (this.getTableColumn() == colSubTitle)
						{// PlantSpecies
                            if (inThread)
                            {
                                Label lbl = new Label();
                                lbl.textProperty().bind(thisFieldAsText);
                                vBox.getChildren().add(lbl);
                            }
                            else
                                vBox.getChildren().add(this.plantSpeciesEditor.apply((ObjectProperty<PlantSpeciesBean>) getItem().getItem().subTitleProperty()));
						}
						else if (this.getTableColumn() == colVariety)
						{// PlantVariety
							vBox.getChildren().add(this.plantVarietyEditor.apply((ObjectProperty<PlantSpeciesBean>) getItem().getItem().subTitleProperty(), (ObjectProperty<PlantVarietyBean>)getItem().getItem().varietyProperty()));
						}
						else if (this.getTableColumn() == colDetail)
						{// Location
							vBox.getChildren().add(this.locationEditor.apply((ObjectProperty<LocationBean>) getItem().getItem().detailProperty()));
						}
						else
						{
							vBox.getChildren().add(this.nullTextEditor.get());
						}
						break;
					case PURCHASE:
						if (this.getTableColumn() == colMainTitle)
						{
//							vBox.getChildren().add(this.retailerEditor.apply(getItem().getItem().mainTitleProperty()));
						}
//						else if (this.getTableColumn() == colSubTitle)
//						{// Total - the text
////							vBox.getChildren().add(this.plantSpeciesEditor.apply((ObjectProperty<PlantSpeciesBean>) getItem().getItem().subTitleProperty()));
//						}
						else if (this.getTableColumn() == colVariety)
						{// Total value
							vBox.getChildren().add(this.moneyEditor.apply(getItem().getItem().varietyProperty()));
						}
						else
						{
							vBox.getChildren().add(this.nullTextEditor.get());
						}
						break;
					case PURCHASEITEM:
						if (this.getTableColumn() == colMainTitle)
						{
							vBox.getChildren().add(this.nullTextEditor.get());
						}
//						else if (this.getTableColumn() == colSubTitle)
//						{// ProductType
////							vBox.getChildren().add(this.plantSpeciesEditor.apply((ObjectProperty<PlantSpeciesBean>) getItem().getItem().subTitleProperty()));
//						}
//						else if (this.getTableColumn() == colVariety)
//						{// PlantSpecies
////							vBox.getChildren().add(this.plantVarietyEditor.apply((ObjectProperty<PlantSpeciesBean>) getItem().getItem().subTitleProperty(), (ObjectProperty<PlantVarietyBean>)getItem().getItem().varietyProperty()));
//						}
//						else if (this.getTableColumn() == colDetail)
//						{// PlantVariety
//							vBox.getChildren().add(this.nullTextEditor.get());
//						}
						else
						{
							vBox.getChildren().add(this.nullTextEditor.get());
						}
						break;
					case SALE:
						if (this.getTableColumn() == colMainTitle)
						{
//							vBox.getChildren().add(this.freeTextEditor.apply( (StringProperty)(getItem().getItem().mainTitleProperty().get()) ));
							vBox.getChildren().add(this.freeTextEditor2.apply((getItem().getItem().mainTitleProperty()) ));
						}
//						else if (this.getTableColumn() == colSubTitle)
//						{// Total - the text
////							vBox.getChildren().add(this.plantSpeciesEditor.apply((ObjectProperty<PlantSpeciesBean>) getItem().getItem().subTitleProperty()));
//						}
						else if (this.getTableColumn() == colVariety)
						{// Total value
							vBox.getChildren().add(this.moneyEditor.apply(getItem().getItem().varietyProperty()));
						}
						else
						{
							vBox.getChildren().add(this.nullTextEditor.get());
						}
						break;
					case SALEITEM:
						if (this.getTableColumn() == colMainTitle)
						{
							vBox.getChildren().add(this.nullTextEditor.get());
						}
						else if (this.getTableColumn() == colSubTitle)
						{//	plant species
							vBox.getChildren().add(this.plantSpeciesEditor.apply((ObjectProperty<PlantSpeciesBean>) getItem().getItem().subTitleProperty()));
						}
						else if (this.getTableColumn() == colVariety)
						{//	plant variety
							vBox.getChildren().add(this.plantVarietyEditor.apply((ObjectProperty<PlantSpeciesBean>) getItem().getItem().subTitleProperty(), (ObjectProperty<PlantVarietyBean>)getItem().getItem().varietyProperty()));
						}
						else if (this.getTableColumn() == colDetail)
						{//	unit price
							vBox.getChildren().add(this.moneyEditor.apply( (ObjectProperty<SimpleMoney>) getItem().getItem().detailProperty()));
						}
						break;
					case WEATHER:
						if (this.getTableColumn() == colMainTitle)
						{
							vBox.getChildren().add(this.weatherConditionEditor.apply(getItem().getItem().mainTitleProperty()));
						}
						else if (this.getTableColumn() == colSubTitle)
						{
							vBox.getChildren().add(this.freeTextEditor.apply(getItem().getItem().subTitleProperty()));
						}
						else
						{
							vBox.getChildren().add(this.nullTextEditor.get());
						}
						break;
					case WILDLIFE:
						if (this.getTableColumn() == colMainTitle)
						{
							vBox.getChildren().add(this.wildlifeSpeciesEditor.apply(getItem().getItem().mainTitleProperty()));
						}
						else if (this.getTableColumn() == colSubTitle)
						{
							vBox.getChildren().add(this.freeTextEditor.apply(getItem().getItem().subTitleProperty()));
						}
						else if (this.getTableColumn() == colDetail)
						{// Location
							vBox.getChildren().add(this.locationEditor.apply((ObjectProperty<LocationBean>) getItem().getItem().detailProperty()));
						}
						else
						{
							vBox.getChildren().add(this.nullTextEditor.get());
						}
						break;
					case JOURNAL:
						if (this.getTableColumn() == colMainTitle)
						{
							vBox.getChildren().add(this.freeTextEditor2.apply(getItem().getItem().mainTitleProperty()));
						}
						else if (this.getTableColumn() == colSubTitle)
						{
							vBox.getChildren().add(this.freeTextEditor.apply(getItem().getItem().subTitleProperty()));
						}
						else
						{
							vBox.getChildren().add(this.nullTextEditor.get());
						}
//						break;
				}
			}
			else if (getItem() != null)
			{
				Label lbl = new Label();
				lbl.textProperty().bind(thisFieldAsText);
				//	set an indicative graphic on the label
				if (this.getTableColumn() == colMainTitle)
				{
					if (getItem().getItem().getItemType() == NotebookEntryType.WEATHER)
					{
						lbl.setGraphic(setGraphicOnLabel(weatherGraphic, "black"));
					}
					else if (getItem().getItem().getItemType() == NotebookEntryType.HUSBANDRY)
					{
						lbl.setGraphic(setGraphicOnLabel(husbandryGraphic, "black"));
					}
					else if (getItem().getItem().getItemType() == NotebookEntryType.AFFLICTIONEVENT)
					{
						lbl.setGraphic(setGraphicOnLabel(afflictionGraphic, "red"));
					}
					else if (getItem().getItem().getItemType() == NotebookEntryType.PURCHASE)
					{
						lbl.setGraphic(setGraphicOnLabel(purchaseGraphic, "black"));
					}
					else if (getItem().getItem().getItemType() == NotebookEntryType.SALE)
					{
						lbl.setGraphic(setGraphicOnLabel(saleGraphic, "black"));
					}
					else if (getItem().getItem().getItemType() == NotebookEntryType.WILDLIFE)
					{
						lbl.setGraphic(setGraphicOnLabel(wildlifeGraphic, "black"));
					}
					else if (getItem().getItem().getItemType() == NotebookEntryType.GROUNDWORK)
					{
						lbl.setGraphic(setGraphicOnLabel(groundworkGraphic, "black"));
					}
					else if (getItem().getItem().getItemType() == NotebookEntryType.JOURNAL)
					{
						lbl.setGraphic(setGraphicOnLabel(journalGraphic, "black"));
					}
				}
				vBox.getChildren().add(lbl);
			}
			setGraphic(vBox);

		}
	}

	private Region setGraphicOnLabel(SVGPath shape, String colour)
	{
		Region svgRegion = new Region();
		svgRegion.setShape(shape);
		svgRegion.setMinSize(20.0, 20.0);
		svgRegion.setMaxSize(20.0, 20.0);
		svgRegion.setPrefSize(20.0, 20.0);
		svgRegion.setStyle("-fx-background-color: "+colour+";");
		return svgRegion;
	}
		
	private class CommentTableCell extends TableCell<IndexedDiaryBean, IndexedDiaryBean>
	{
		private final CommentCellImpl trueCell = new CommentCellImpl(resources);	//	2.9.6
		private double cellHeight;
		
		@Override
		protected void updateItem(IndexedDiaryBean item, boolean empty)
		{
			LOGGER.traceEntry("CommentTableCell: updateItem(): item: {}", item);
			super.updateItem(item, empty);
			if (item == null || empty)
			{
				setText(null);
				setGraphic(null);
				return;
			}

			// use CSS to emulate spanned dates
			setCellStyles(this, getItem());
			cellHeight = this.getHeight();
			LOGGER.debug("DiaryTab: CommentTableCell(): setParent: {}", item.getItem().getItem());
			trueCell.setParent(item.getItem().getItem());	//	2.9.6
			LOGGER.debug("DiaryTab: CommentTableCell(): getCommentst: {}", item.getItem().getItem().getComments());
			trueCell.updateViewMode(this, item.getItem().getComments());
//			trueCell.updateViewMode(this, item.getItem().getItem().getComments());	//	2.9.6
		}

		@Override
		public void startEdit() {
			super.startEdit();
			cellHeight = this.getHeight();
			trueCell.setParent(getItem().getItem().getItem());
			trueCell.updateViewMode(this, getItem().getItem().getComments());
		}

		@Override
		public void cancelEdit() {
			super.cancelEdit();
			trueCell.updateViewMode(this, getItem().getItem().getComments());
			this.setPrefHeight(cellHeight);
		}
	}

	private void setCellStyles(TableCell<IndexedDiaryBean, IndexedDiaryBean> cell, IndexedDiaryBean item)
	{
			// use CSS to emulate spanned dates
			// clear out any previous CSS classes added (cells get reused!!)
			cell.getStyleClass().removeIf(s -> s.startsWith("notebook-diary"));
			
			// visually separate each day
//			if (item.getDateIndex()%2==0)
			if (item.dateIndex()%2==0)
			{
				cell.getStyleClass().add("notebook-diary-evencell");
			}
			else
			{
				cell.getStyleClass().add("notebook-diary-oddcell");
			}
			// remove borders in the date column within a day
			if (cell.getTableColumn() == diaryColDate)
			{
//				if (item.getDateSpan() > 1)
				if (item.dateSpan() > 1)
				{
//					if (item.getInDateIndex() == 0)
					if (item.inDateIndex() == 0)
					{
						cell.getStyleClass().add("notebook-diary-topcell");
					}
//					else if (item.getInDateIndex() >= item.getDateSpan()-1)
					else if (item.inDateIndex() >= item.dateSpan()-1)
					{
						cell.getStyleClass().add("notebook-diary-bottomcell");
					}
					else
					{
						cell.getStyleClass().add("notebook-diary-midcell");
					}
				}
			}
			
			//attempt to right align cost
			if (cell.getTableColumn() == colVariety)
			{
				if ( (item.getItem().getItemType() == NotebookEntryType.PURCHASE) ||
						(item.getItem().getItemType() == NotebookEntryType.SALE) )
				{// this doesn't work, but should!
					cell.getStyleClass().add("notebook-diary-moneycell");
				}
			}
			// for a purchase, visually group the purchase items with the purchase 'header'
			if (cell.getTableColumn() == colMainTitle)
			{
				if (item.purchaseSpan() > 1)
				{
					if (item.inPurchaseIndex() == 0)
					{// the purchase record
						cell.getStyleClass().add("notebook-diary-topcell");
					}
					else if (item.inPurchaseIndex() >= item.purchaseSpan()-1)
					{
						cell.getStyleClass().add("notebook-diary-bottomcell");
					}
					else
					{
						cell.getStyleClass().add("notebook-diary-midcell");
					}
				}
				// for a sale, visually group the sale items with the sale 'header'
				if (item.saleSpan() > 1)
				{
					if (item.inSaleIndex() == 0)
					{// the Sale record
						cell.getStyleClass().add("notebook-diary-topcell");
					}
					else if (item.inSaleIndex() >= item.saleSpan()-1)
					{
						cell.getStyleClass().add("notebook-diary-bottomcell");
					}
					else
					{
						cell.getStyleClass().add("notebook-diary-midcell");
					}
				}
			}

	}
	
}
