/*
 * Copyright (C) 2021-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
    3.0.0	First version
    3.0.1	CommentAdder handling moved to separate class
    3.0.2	Check before Cancel if changes have been made
    3.0.4	Use new convenience class NotebookBeanDeleter for deletion.
    		Use new convenience class NotebookBeanCanceller to handle edit cancellation.
    		Use new convenience class EditorCommentTableHandler to handle Comment table construction.
    		Set focus on first field and handle tabbing better.
 */

package uk.co.gardennotebook;

import javafx.application.Platform;
import javafx.beans.property.SimpleObjectProperty;
//import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.GridPane;
//import javafx.scene.text.Text;
import javafx.stage.WindowEvent;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.EntryMessage;
import uk.co.gardennotebook.fxbean.*;
import uk.co.gardennotebook.spi.GNDBException;
import uk.co.gardennotebook.spi.NotebookEntryType;

import java.io.IOException;
import java.time.Year;
//import java.time.format.DateTimeFormatter;
//import java.time.format.FormatStyle;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

/**
	*	Controller class for create/update of CroppingPlan items
	*
	*	@author Andy Gegg
	*	@version	3.0.4
	*	@since	3.0.0
 */
final public class CroppingPlanEditor extends GridPane implements INotebookLoadable
{

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

	private final CroppingPlanBean thisValueBean;

	private Consumer<Node> loadSplit;
	private Consumer<Node> clearSplit;
	private BiConsumer<String, Node> loadTab;
	private Consumer<Node> clearTab;

	//	an observable property that the caller can monitor when a new item is being created
	private SimpleObjectProperty<CroppingPlanBean> newBean;
	private final boolean addingItem;

	//	an observable property that the caller can monitor when an item is deleted
	private SimpleObjectProperty<Object> deletedBean;

	@FXML
	private ResourceBundle resources;

	@FXML
	private Button btnSave;
	@FXML
	private Button btnDelete;
	@FXML
	private Spinner<Year> numYearOfPlan;
	@FXML
	private LocationCombo parentLocation;
	@FXML
	private CropRotationGroupCombo parentCropRotationGroup;
	@FXML
	private TreeView<CroppingActualBean> treeActualPlanting;
	@FXML
	MenuItem ctxmnuCopyToActual;
	@FXML
	private TreeView<PlantCatalogueBean> treePossiblePlanting;
	@FXML
	MenuItem ctxmnuDelete;

	@FXML
	private TableView<CommentBean> tbvComment;
	@FXML
	private TableColumn<CommentBean, CommentBean> tbvCommentDate;
	@FXML
	private TableColumn<CommentBean, String> tbvCommentText;

	//	hold the PlantCatalogueBeans which can be used for this CropRotationGroup
	private ObservableList<PlantCatalogueBean> acceptablePlants;

	CroppingPlanEditor()
	{
		this(null, null);
	}

	CroppingPlanEditor(Year yearOfPlan)
	{
		this(yearOfPlan, null);
	}

	CroppingPlanEditor(CroppingPlanBean plan)
	{
		this(null, plan);
	}

	private CroppingPlanEditor(Year yearOfPlan, CroppingPlanBean initialVal)
	{
		this.thisValueBean = (initialVal != null ? initialVal : new CroppingPlanBean());
		this.addingItem = (initialVal == null);
		if (initialVal == null && yearOfPlan != null)
		{
			this.thisValueBean.setYearOfPlan(yearOfPlan);
		}
		FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/CroppingPlanEditor.fxml"),
			ResourceBundle.getBundle("notebook") );
		fxmlLoader.setRoot(this);
		fxmlLoader.setController(this);
		try {
			fxmlLoader.load();	// NB initialize() is called from in here
		} catch (IOException exception) {
			throw new RuntimeException(exception);
		}
	}

	/*
	*	Initialises the controller class.
	*/
	@FXML
	private void initialize()
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("initialize()");
//		tbvCommentDate.setCellValueFactory(e-> new SimpleObjectProperty<>( e.getValue()));
//		tbvCommentDate.setCellFactory(x -> new EditorCommentDateTableCell(resources));
//		tbvCommentText.setCellValueFactory(e->e.getValue().commentProperty());
//		tbvCommentText.setCellFactory(x -> new EditorCommentTextTableCell(resources));  //  2.4.0
//		tbvComment.setColumnResizePolicy(NotebookResizer.using(tbvComment));
//		tbvComment.getItems().setAll(thisValueBean.getComments());
//		CommentBean cb_new = new CommentBean(this.thisValueBean);
//
//		ChangeListener<String> commentAdder = new EditorCommentAdder<>(thisValueBean, tbvComment);
//		cb_new.commentProperty().addListener(commentAdder);
//		tbvComment.getItems().add(cb_new);
//		CommentBean cb_one = tbvComment.getItems().get(0);
//		Text t = new Text(cb_one.getDate().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)));
//		double wid = t.getLayoutBounds().getWidth();
//		tbvCommentDate.setPrefWidth(wid+10);

		EditorCommentTableHandler<CroppingPlanBean> cth = new EditorCommentTableHandler<>(resources);
		cth.constructCommentTable(tbvComment, tbvCommentDate, tbvCommentText, thisValueBean);


		thisValueBean.setSaveRequired(true);

		SpinnerValueFactory<Year> yearSpinnerValueFactory = new SpinnerValueFactory<Year>()
		{
			@Override
			public void decrement(int i)
			{
				if (getValue() != null)
				{
					setValue(getValue().minusYears(i));
				}
				else
				{
					setValue(Year.now());
				}
			}

			@Override
			public void increment(int i)
			{
				if (getValue() != null)
				{
					setValue(getValue().plusYears(i));
				}
				else
				{
					setValue(Year.now());
				}
			}
		};

		yearSpinnerValueFactory.setValue(thisValueBean.getYearOfPlan());
		numYearOfPlan.setValueFactory(yearSpinnerValueFactory);
		thisValueBean.yearOfPlanProperty().bind(numYearOfPlan.valueProperty());	// NB bidirectional binding NOT allowed on Spinners!

		parentLocation.valueProperty().bindBidirectional(thisValueBean.locationProperty());

		parentCropRotationGroup.valueProperty().addListener(this::onCropRotationGroupIdChange);
		parentCropRotationGroup.valueProperty().bindBidirectional(thisValueBean.cropRotationGroupProperty());

		treePossiblePlanting.setCellFactory(c -> new PossibleTreeCell());
		treeActualPlanting.setCellFactory(c -> new ActualTreeCell());

		if (!thisValueBean.isNew())
		{	//	if a new, that is null, CroppingActual is passed, all extant CroppingActuals will be returned, regardless of CroppingPlan owner
			try
			{
				addDropItemsToActual(thisValueBean.getCroppingActual());
			}
			catch (GNDBException e)
			{
				PanicHandler.panic(e);
			}
		}

		btnSave.disableProperty().bind(numYearOfPlan.valueProperty().isNull()
					.or(parentLocation.valueProperty().isNull()
						.or(parentCropRotationGroup.valueProperty().isNull())));

		try
		{
			btnDelete.setDisable(addingItem || !(this.thisValueBean.canDelete()));
		} catch (GNDBException ex) {
			PanicHandler.panic(ex);
		}

		//  select first row when user tabs into table
		treePossiblePlanting.focusedProperty().addListener((obj, wasFocused, isFocused)-> {
					if (!wasFocused && isFocused)
					{
						treePossiblePlanting.getSelectionModel().select(0);
					}
					else if (wasFocused && !isFocused)
					{
						treePossiblePlanting.getSelectionModel().clearSelection();
					}
				}
			);
		//  select first row when user tabs into table
		treeActualPlanting.focusedProperty().addListener((obj, wasFocused, isFocused)-> {
					if (!wasFocused && isFocused)
					{
						treeActualPlanting.getSelectionModel().selectFirst();
					}
					else if (wasFocused && !isFocused)
					{
						treeActualPlanting.getSelectionModel().clearSelection();
					}
				}
		);

		Platform.runLater(() -> numYearOfPlan.requestFocus());

		LOGGER.traceExit(log4jEntryMsg);
	}	//	initialize()

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

	@Override
	public void clearUpOnClose(Event e)
	{
		if (isCancelDenied())
		{
			e.consume();
			return;
		}

		// need to explicitly unbind the spinner or thisValueBean.cancelEdit() faults: 'cannot set bound value' in thisValueBean.setValues()
		thisValueBean.yearOfPlanProperty().unbind();
		thisValueBean.cancelEdit();
	}

	@FXML
	private void btnCancelOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("btnCancelOnAction()");
		if (thisValueBean == null)
		{
			LOGGER.debug("thisValueBean is null");
			LOGGER.traceExit(log4jEntryMsg);
			return;
		}
		if (isCancelDenied())
		{
			return;
		}

		// need to explicitly unbind the spinner or thisValueBean.cancelEdit() faults: 'cannot set bound value' in thisValueBean.setValues()
		thisValueBean.yearOfPlanProperty().unbind();
		thisValueBean.cancelEdit();
		clearTab.accept(this);
		LOGGER.traceExit(log4jEntryMsg);
	}	//	btnCancelOnAction()

	/**
	 * Check if user really wants to quit if changes have been made
	 *
	 * @return	true	user does NOT want to quit
	 */
	private boolean isCancelDenied()
	{
		if (thisValueBean.needSave())
		{
//			Alert checkQuit = new Alert(Alert.AlertType.CONFIRMATION, resources.getString("alert.confirmquit"), ButtonType.NO, ButtonType.YES);
//			Optional<ButtonType> result = checkQuit.showAndWait();
//			LOGGER.debug("after delete dialog: result:{}, result.get:{}",result, result.orElse(null));
//			if (result.isPresent() && result.get() == ButtonType.NO)
//			{
//				LOGGER.debug("after quit denied");
//				return true;
//			}
			NotebookEditorCanceller<CroppingPlanBean> cancelChecker = new NotebookEditorCanceller<>(resources);
			return cancelChecker.isCancelDenied(thisValueBean);
		}
		return false;
	}

	@FXML
	private void btnDeleteOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("btnDeleteOnAction()");
		if (thisValueBean == null)
		{
			LOGGER.debug("thisValueBean is null");
			LOGGER.traceExit(log4jEntryMsg);
			return;
		}

//		boolean canDelete = false;
//		try
//		{
//			canDelete = thisValueBean.canDelete();
//		} catch (GNDBException ex) {
//			PanicHandler.panic(ex);
//		}
//		if (!canDelete)
//		{
//			Alert checkDelete = new Alert(Alert.AlertType.INFORMATION, resources.getString("alert.cannotdelete"), ButtonType.OK);
//			Optional<ButtonType> result = checkDelete.showAndWait();
//			LOGGER.debug("item cannot be deleted");
//			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.get());
//		if (result.isPresent() && result.get() == ButtonType.YES)
//		{
//			//	unbind the yearOfPlan spinner as the delete action (in the bean) tries to set the yearOfPlan value (in the bean) to the default
//			thisValueBean.yearOfPlanProperty().unbind();
//			LOGGER.debug("after delete confirmed");
//			try
//			{
//				thisValueBean.delete();
//			} catch (GNDBException ex) {
//				PanicHandler.panic(ex);
//			}
//			deletedBeanProperty().setValue(new Object());
//			clearTab.accept(this);
//		}

		//	unbind the yearOfPlan spinner as the delete action (in the bean) tries to set the yearOfPlan value (in the bean) to the default
//		thisValueBean.yearOfPlanProperty().unbind();
		NotebookBeanDeleter<CroppingPlanBean> deleterImpl = new NotebookBeanDeleter<>(resources, ()->thisValueBean.yearOfPlanProperty().unbind());
		if (deleterImpl.deleteItemImpl(thisValueBean))
		{
			deletedBeanProperty().setValue(new Object());
			clearTab.accept(this);
		}

		LOGGER.traceExit(log4jEntryMsg);
	}	//	btnDeleteOnAction()

	@FXML
	private void btnSaveOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("btnSaveOnAction()");
		if (thisValueBean == null)
		{
			LOGGER.debug("thisValueBean is null");
			LOGGER.traceExit(log4jEntryMsg);
			return;
		}

		if ( (numYearOfPlan.getValue() == null) ||
				(parentLocation.getValue() == null) ||
				(parentCropRotationGroup.getValue() == null) )
		{
			LOGGER.debug("thisValueBean has null key(s)");
			LOGGER.traceExit(log4jEntryMsg);
			return;
		}

		if (thisValueBean.checkForDuplicatePlan())
		{
			LOGGER.debug("thisValueBean is a duplicate Plan");
			Alert notifyDuplicate = new Alert(Alert.AlertType.INFORMATION, resources.getString("alert.croppingplan.duplicate"), ButtonType.OK);
			Optional<ButtonType> result = notifyDuplicate.showAndWait();
			LOGGER.traceExit(log4jEntryMsg);
			return;
		}
		// need to explicitly unbind the spinner or thisValueBean.save() faults: 'cannot set bound value' in thisValueBean.setValues()
		thisValueBean.yearOfPlanProperty().unbind();

		if (!thisValueBean.canSave())
		{
			LOGGER.debug("Cannot save thisValueBean");
			return;
		}

		LOGGER.debug("btnSaveOnAction(): thisValueBean: key: {}", thisValueBean.getKey());
		try
		{
			thisValueBean.save();
		} catch (GNDBException ex) {
			PanicHandler.panic(ex);
		}

		//	now save any Actuals
		//	only need to save new items
		if (treeActualPlanting.getRoot() != null)
		{
			for (var actual : treeActualPlanting.getRoot().getChildren())
			{
				CroppingActualBean cab = actual.getValue();
				LOGGER.debug("saving actual: {}", cab);
				if (cab.isNew())
				{
//				LOGGER.debug("about to setCroppingPlan");
					cab.setCroppingPlan(thisValueBean);
					try
					{
						cab.save();
					}
					catch (GNDBException e)
					{
						PanicHandler.panic(e);
					}
				}
				for (var actualVar : actual.getChildren())
				{
					CroppingActualBean cabVar = actualVar.getValue();
					if (cabVar.isNew())
					{
						cabVar.setCroppingPlan(thisValueBean);
						try
						{
							cabVar.save();
						}
						catch (GNDBException e)
						{
							PanicHandler.panic(e);
						}
					}
				}
			}
		}

		newBeanProperty().setValue(thisValueBean);
		clearTab.accept(this);
		LOGGER.traceExit(log4jEntryMsg);
	}	//	btnSaveOnAction()

	@FXML
	private void ctxmnuPossibleTopOnAction(WindowEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuTopOnAction()");
		if (treePossiblePlanting.getSelectionModel().isEmpty())
		{
			LOGGER.debug("no item selected in Table");
			ctxmnuCopyToActual.setDisable(true);
			return;
		}

		ctxmnuCopyToActual.setDisable(false);
		LOGGER.traceExit(log4jEntryMsg);
	}

	@FXML
	private void ctxmnuCopyToActualOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuCopyToActualOnAction()");

		dropPossibleOnActual();
		treeActualPlanting.refresh();
	}

	@FXML
	private void ctxmnuTopOnAction(WindowEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuTopOnAction()");
		if (treeActualPlanting.getSelectionModel().isEmpty())
		{
			LOGGER.debug("no item selected in Table");
			ctxmnuDelete.setDisable(true);
			return;
		}

		ctxmnuDelete.setDisable(false);
		LOGGER.traceExit(log4jEntryMsg);
	}

	@FXML
	private void ctxmnuDeleteOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuDeleteOnAction()");

		var selected = treeActualPlanting.getSelectionModel().getSelectedItems();
		if (selected.isEmpty())
		{
			return;
		}
		for (var sel : selected)
		{
			deleteActualFromDB(sel);
		}
		//	now clear the selected entries from the table display
		List<TreeItem<CroppingActualBean>> actuals = new ArrayList<>(selected);
		selected = null;
		for (var sel : actuals)
		{
			CroppingActualBean cab = sel.getValue();
			if (cab.getEnclosedItemType() == NotebookEntryType.PLANTVARIETY)
			{
				sel.getParent().getChildren().remove(sel);
			}
		}
		for (var sel : actuals)
		{
			CroppingActualBean cab = sel.getValue();
			if (cab.getEnclosedItemType() == NotebookEntryType.PLANTSPECIES)
			{
				sel.getParent().getChildren().remove(sel);
			}
		}
	}

	private void deleteActualFromDB(TreeItem<CroppingActualBean> actual)
	{
		CroppingActualBean cab = actual.getValue();
		switch (cab.getEnclosedItemType())
		{
			case PLANTSPECIES -> deleteActualSpeciesFromDB(actual);
			case PLANTVARIETY -> deleteActualVarietyFromDB(actual);
			default -> throw new IllegalStateException("Unexpected value: " + cab.getEnclosedItemType());
		};
	}

	private void deleteActualSpeciesFromDB(TreeItem<CroppingActualBean> actual)
	{
		CroppingActualBean cab = actual.getValue();
		if (!addingItem && !cab.isNew())
		{
			//	first explicitly remove any existing CroppingActualBeans from the DB
			for (var child : actual.getChildren())
			{
				deleteActualVarietyFromDB(child);
			}
			try{
				cab.delete();
			}
			catch (GNDBException e)
			{
				PanicHandler.panic(e);
			}
			actual.getChildren().clear();
		}
//		treeActualPlanting.getRoot().getChildren().remove(actual);
	}

	private void deleteActualVarietyFromDB(TreeItem<CroppingActualBean> actual)
	{
		CroppingActualBean cab = actual.getValue();
		if (!addingItem && !cab.isNew())
		{
			try
			{
				cab.delete();
			}
			catch (GNDBException e)
			{
				PanicHandler.panic(e);
			}
		}
	}

	@FXML
	private void tbvActualOnDragOver(DragEvent ev)
	{
		LOGGER.debug("tbvActualOnDragOver()");
		if (!(ev.getGestureSource() instanceof TreeCell) ||
				((TreeCell)ev.getGestureSource()).getTreeView() != treePossiblePlanting)
		{
			return;
		}
		Dragboard db = ev.getDragboard();
		if ( db.hasString() &&
				(db.getString() != null) &&
				(!db.getString().isEmpty()) &&
				(db.getString().startsWith("tbvProducts:")))
		{
			ev.acceptTransferModes(TransferMode.COPY);
		}
		ev.consume();

	}

	@FXML
	private void tbvActualOnDragDropped(DragEvent ev)
	{
		Dragboard db = ev.getDragboard();
		boolean success = false;
		if ( db.hasString() &&
				(db.getString() != null) &&
				(!db.getString().isEmpty()) &&
				(db.getString().startsWith("tbvProducts:")))
		{
			success = true;
			dropPossibleOnActual();
		}
		ev.setDropCompleted(success);
		ev.consume();
	}

	/*
	 * Create Actual cropping items using the selected Possibles
	 */
	private void dropPossibleOnActual()
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("dropPossibleOnActual()");

		if (treeActualPlanting.getRoot() == null)
		{
			treeActualPlanting.setRoot(new TreeItem<>());
		}

		var possibles = treePossiblePlanting.getSelectionModel().getSelectedItems();
		LOGGER.debug("selected possibles: {}", possibles);

		List<CroppingActualBean> droppables = getDropItems(possibles);
		addDropItemsToActual(droppables);
	}

	/**
	 * Turn the selected items in the Possibles tree into CroppingActualBeans.
	 *
	 * @param possibles	the selected items in the Possibles tree
	 * @return	a set of corresponding CroppingActualBeans
	 */
	private List<CroppingActualBean> getDropItems(ObservableList<TreeItem<PlantCatalogueBean>> possibles)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getDropItems()");

		LOGGER.debug("possibles: {}", possibles);

		List<CroppingActualBean> droppables = new ArrayList<>(possibles.size());

		for (var draggedItem : possibles)
		{
			if (draggedItem.getValue().getItem().getType() == NotebookEntryType.PLANTSPECIES)
			{
				PlantSpeciesBean draggedSpecies = (PlantSpeciesBean)draggedItem.getValue().getItem();
				CroppingActualBean newSpec = new CroppingActualBean();
				newSpec.setPlantSpecies(draggedSpecies);
				droppables.add(newSpec);
			}
			if (draggedItem.getValue().getItem().getType() == NotebookEntryType.PLANTVARIETY)
			{
				PlantVarietyBean draggedVariety = (PlantVarietyBean) draggedItem.getValue().getItem();
				PlantSpeciesBean draggedSpecies = ((PlantVarietyBean)draggedItem.getValue().getItem()).getPlantSpecies();
				CroppingActualBean pcb = new CroppingActualBean();
				pcb.setPlantSpecies(draggedSpecies);
				pcb.setPlantVariety (draggedVariety);
				droppables.add(pcb);
			}
		}
		return droppables;
	}

	/*
	 * Add a list of CroppingActualBeans to the Actuals tree
	 */
	private void addDropItemsToActual(List<CroppingActualBean> droppables)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("addDropItemsToActual(): {}", droppables);

		if (treeActualPlanting.getRoot() == null)
		{
			treeActualPlanting.setRoot(new TreeItem<>());
		}

		for (CroppingActualBean cab : droppables)
		{
			if (cab.getEnclosedItemType() == NotebookEntryType.PLANTSPECIES)
			{
				if (isSpeciesInActuals(cab)) continue;

				treeActualPlanting.getRoot().getChildren().add(new TreeItem<>(cab));
			}
			else	//	it's a PlantVariety
			{
				if (isVarietyInActuals(cab)) continue;

				if (!isSpeciesInActuals(cab))
				{
					addSpeciesAndVarietyToActuals(cab);
					continue;
				}
				for (var curr : treeActualPlanting.getRoot().getChildren())
				{
					if ( curr.getValue().getEnclosedItemType() == NotebookEntryType.PLANTSPECIES &&
							curr.getValue().getPlantSpecies().sameAs(cab.getPlantSpecies()) )
					{
						curr.getChildren().add(new TreeItem<>(cab));
						curr.setExpanded(true);
						break;
					}
				}
			}
		}
	}

	/*
	*	Check if there is an entry in the Actuals tree for the PlantSpecies of this CroppingActualBean.
	* 	NB The CroppingActualBean may hold either a PlantSpecies or a PlantVariety.
	 */
	private boolean isSpeciesInActuals(CroppingActualBean cab)
	{
		for (var curr : treeActualPlanting.getRoot().getChildren())
		{
			if ( curr.getValue().getEnclosedItemType() == NotebookEntryType.PLANTSPECIES &&
					curr.getValue().getPlantSpecies().sameAs(cab.getPlantSpecies()) )
			{
				return true;
			}
		}
		return false;
	}

	/*
	 *	Check if there is an entry in the Actuals tree for the PlantVariety of this CroppingActualBean.
	 */
	private boolean isVarietyInActuals(CroppingActualBean cab)
	{
		if (!cab.hasPlantVariety()) return false;

		for (var curr : treeActualPlanting.getRoot().getChildren())
		{
			if ( curr.getValue().getEnclosedItemType() == NotebookEntryType.PLANTVARIETY &&
					curr.getValue().getPlantVariety().sameAs(cab.getPlantVariety()) )
			{
				return true;
			}
			for (var sub : curr.getChildren())
			{
				if ( sub.getValue().getEnclosedItemType() == NotebookEntryType.PLANTVARIETY &&
						sub.getValue().getPlantVariety().sameAs(cab.getPlantVariety()) )
				{
					return true;
				}
			}
		}
		return false;
	}

	private void addSpeciesAndVarietyToActuals(CroppingActualBean cab)
	{
		PlantSpeciesBean draggedSpecies = cab.getPlantSpecies();
		CroppingActualBean newSpec = new CroppingActualBean();
		newSpec.setPlantSpecies(draggedSpecies);
		TreeItem<CroppingActualBean> newTreeItem = new TreeItem<>(newSpec);
		treeActualPlanting.getRoot().getChildren().add(newTreeItem);
		newTreeItem.getChildren().add(new TreeItem<>(cab));
		newTreeItem.setExpanded(true);
	}

	SimpleObjectProperty<CroppingPlanBean> newBeanProperty()
	{
		if (newBean == null)
		{
			newBean = new SimpleObjectProperty<>();
		}
		return newBean;
	}

	SimpleObjectProperty<Object> deletedBeanProperty()
	{
		if (deletedBean == null)
		{
			deletedBean = new SimpleObjectProperty<>();
		}
		return deletedBean;
	}

	private void onCropRotationGroupIdChange(ObservableValue<? extends CropRotationGroupBean> obs, CropRotationGroupBean old, CropRotationGroupBean nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onCropRotationGroupIdChange(): old={}, new={}", old, nval);

		if (nval != null && nval.sameAs(old))
		{
			return;
		}

		if (treeActualPlanting.getRoot() != null)
		{
			for (var sel : treeActualPlanting.getRoot().getChildren())
			{
				deleteActualFromDB(sel);
			}
		}
		treeActualPlanting.setRoot(null);

		if (nval == null)
		{//	delete current Actual values
			treePossiblePlanting.setRoot(null);
			return;
		}

		//	nval is not null and has changed
		if (treePossiblePlanting.getRoot() == null)
		{
			treePossiblePlanting.setRoot(new TreeItem<>());
		}
//		LOGGER.debug("onCropRotationGroupIdChange(): about to reload acceptablePlants");
		acceptablePlants = PlantCatalogueBean.getPlantCatalogue(nval, true);
//		LOGGER.debug("acceptablePlants: {}", acceptablePlants);
		treePossiblePlanting.getRoot().getChildren().setAll(acceptablePlants.stream().map(PlantCatalogueBeanSimpleTreeItem::new).toList());
//		LOGGER.debug("first child: {}", treePossiblePlanting.getRoot().getChildren().get(0));
	}

	/*
	 * Implement Drag&Drop to copy Possible cropping items to the Actuals list.
	 */
	private class PossibleTreeCell extends TreeCell<PlantCatalogueBean>
	{
		PossibleTreeCell()
		{
			setOnDragDetected(ev -> {
				LOGGER.debug("PossibleTreeCell: onDragDetected()");
				if (treePossiblePlanting.getSelectionModel().isEmpty())
				{
					ev.consume();
					return;
				}
				Dragboard db = startDragAndDrop(TransferMode.COPY);
				db.setDragView(snapshot(null, null));
				ClipboardContent content = new ClipboardContent();
				content.putString("tbvPossible");
				db.setContent(content);
				ev.consume();
			});

		}
		@Override protected void updateItem(PlantCatalogueBean item, boolean empty)
		{
			super.updateItem(item, empty);
			if (empty || item==null)
			{
				setText("");
				return;
			}
			setText(item.getCommonName());
		}

	}

	/*
	 * Implement Drag&Drop to copy Possible cropping items to the Actuals list.
	 */
	private class ActualTreeCell extends TreeCell<CroppingActualBean>
	{
		ActualTreeCell()
		{
			setOnDragOver(ev ->
				{
//					LOGGER.debug("ActualTreeCell: onDragOver()");
					if (!(ev.getGestureSource() instanceof TreeCell tc) ||
							(tc.getTreeView() != treePossiblePlanting) )
					{
						LOGGER.debug("ActualTreeCell: onDragOver(): not valid source");
						return;
					}
					Dragboard db = ev.getDragboard();
					if ( db.hasString() &&
							(db.getString() != null) &&
							(!db.getString().isEmpty()) &&
							(db.getString().startsWith("tbvPossible")))
					{
						ev.acceptTransferModes(TransferMode.COPY);
					}
					ev.consume();

				}
			);

			setOnDragDropped(ev ->
				{
					Dragboard db = ev.getDragboard();
					boolean success = false;
					if ( db.hasString() &&
							(db.getString() != null) &&
							(!db.getString().isEmpty()) &&
							(db.getString().startsWith("tbvPossible")))
					{
						success = true;
						dropPossibleOnActual();
					}
					ev.setDropCompleted(success);
					ev.consume();
				});
		}

		@Override protected void updateItem(CroppingActualBean item, boolean empty)
		{
			super.updateItem(item, empty);
			if (empty || item==null)
			{
				setText("");
				return;
			}
			setText(item.getCommonName());
		}

	}

}
