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

/*
	Change log
    2.5.0   First version
    2.9.1   Make total price field show entered value properly formatted
    3.0.0	Fixed bug whereby plant species did not show
    		Fixed bug whereby new Sale was initialised with all existing SaleItems.
    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 EditorCommentTableHandler to handle Comment table construction.
    		Set focus on first field.
	3.0.5	Use factory pattern DiaryBean
			Code tidy
    3.1.0	Use SafeDatePicker.
    		Change constructor for new descendant entries (to get the date)
 */

package uk.co.gardennotebook;

import java.io.IOException;
import java.time.LocalDate;
import java.util.ResourceBundle;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

import javafx.application.Platform;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import uk.co.gardennotebook.fxbean.*;
import javafx.scene.layout.GridPane;
import javafx.scene.control.DatePicker;
import java.math.BigDecimal;
//import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
//import java.time.format.FormatStyle;
import java.util.List;
import java.util.Optional;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableCell;
import javafx.scene.control.TextFormatter;
import javafx.scene.layout.VBox;
//import javafx.scene.text.Text;
import javafx.stage.WindowEvent;
import javafx.util.converter.BigDecimalStringConverter;
import uk.co.gardennotebook.util.SimpleMoney;
import uk.co.gardennotebook.util.SimpleMoneyStringConverter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.EntryMessage;
//import uk.co.gardennotebook.fxbean.RetailerBean;
import uk.co.gardennotebook.spi.GNDBException;
import uk.co.gardennotebook.util.StoryLineTree;

/**
	*	Controller class for create/update of Purchases
	*
	*	@author Andy Gegg
	*	@version	3.1.0
	*	@since	2.5.0
 */
final class SaleEditor extends GridPane implements INotebookLoadable
{
	private static final Logger LOGGER = LogManager.getLogger();

	private final SaleBean thisValueBean;
    
    private PlantSpeciesBean initialSpecies = null;
    private PlantVarietyBean initialVariety = null;
    private boolean hasLinkedItem = false;
    private SaleItemBean linkedItem;

	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 Sale is being created
	private SimpleObjectProperty<SaleBean> newBean;
	private final boolean addingItem;

	private LocalDate earliestDate = LocalDate.MIN;

	//	an observable property that the caller can monitor when a new Sale is being created
    //  with a SaleItem as a Descendant of an existing Diary item
	private SimpleObjectProperty<SaleItemBean> newLinkedItemBean;

	//	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 SafeDatePicker dtpDate;
	@FXML
	private TextField purchasedBy;
	@FXML
	private TextField numTotalPrice;

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

	// purchase items
	@FXML
	private TableView<SaleItemBean> tbvItems;
	@FXML
	private TableColumn<SaleItemBean, PlantSpeciesBean> tbvPlantSpecies;
	@FXML
	private TableColumn<SaleItemBean, PlantVarietyBean> tbvPlantVariety;
	@FXML
	private TableColumn<SaleItemBean, BigDecimal> tbvQuantity;
	@FXML
	private TableColumn<SaleItemBean, String> tbvUnit;
	@FXML
	private TableColumn<SaleItemBean, SimpleMoney> tbvItemCost;
	@FXML
	private TableColumn<SaleItemBean, SaleItemBean> tbvSIComment;
	@FXML
	private MenuItem ctxmnuDelete;
	@FXML
	private MenuItem ctxmnuTopHistory;
	@FXML
	private MenuItem ctxmnuAncestors;

	//	handles addition of multiple sale items
	private final ChangeListener<PlantSpeciesBean> saleItemAdder = new ChangeListener<>() {
		@Override
		public void changed(ObservableValue<? extends PlantSpeciesBean> obj, PlantSpeciesBean oldVal, PlantSpeciesBean newVal) {
            LOGGER.trace("saleItemAdder: newVal: {}", newVal);
			if (newVal != null) 
			{
				SaleItemBean pib_new = new SaleItemBean();
				if (!addingItem)
				{
					pib_new.setSale(thisValueBean);
				}
                pib_new.plantSpeciesProperty().addListener(this);
				pib_new.setSaveRequired(true);
				tbvItems.getItems().add(pib_new);
				obj.removeListener(saleItemAdder);
			}
		}
	};

	SaleEditor()
	{
//		this((SaleBean)null, null, null);
		this(null);
	}

//    /**
//     * Specify a species and variety for the first SaleItem - used to link a sale to a StoryLine.
//     *
//     * @since 2.5.0
//     * @param species   the initial PlantSpecies (mandatory)
//     * @param variety   the initial PlantVariety (optional)
//     */
//    SaleEditor(final PlantSpeciesBean species, final PlantVarietyBean variety)
//    {
//        this(null, species, variety);
//    }

	/**
	 * Use this to add a new descendant to an entry
	 *
	 * @param parent	The ancestor for the new entry
	 * @param asParent	Not used - needed to distinguish this constructor from the one to just edit an extant item
	 */
	SaleEditor(INotebookBean parent, boolean asParent)
	{
		LOGGER.debug("INotebookBean constructor");

		this.addingItem = true;
//		newStorylineEntry = true;

		this.thisValueBean = new SaleBean();
		switch (parent)
		{
			case null -> {
				LOGGER.debug("null");
				throw new IllegalArgumentException("parent must not be null");
			}
			case HusbandryBean hu -> {
				LOGGER.debug("Husbandry: {}", hu);
				this.initialSpecies = hu.getPlantSpecies();
				this.initialVariety = hu.getPlantVariety();
				this.hasLinkedItem =true;
				earliestDate = hu.getDate();
			}
			case PurchaseItemBean pi -> {
				LOGGER.debug("Purchase Item: {}", pi);
				throw new IllegalArgumentException("parent must not be a Purchase Item");
			}
			case GroundworkBean gw -> {
				LOGGER.debug("Groundwork: {}", gw);
				if (!gw.hasPlantSpecies())
				{
					return;
				}
				this.initialSpecies = gw.getPlantSpecies();
				this.initialVariety = gw.getPlantVariety();
				this.hasLinkedItem =true;
				earliestDate = gw.getDate();
			}
			case AfflictionEventBean ae -> {
				LOGGER.debug("Affliction event: {}", ae);
				if (!ae.hasPlantSpecies())
				{
					return;
				}
				this.initialSpecies = ae.getPlantSpecies();
				this.initialVariety = ae.getPlantVariety();
				this.hasLinkedItem =true;
				earliestDate = ae.getDate();
			}
			default -> {
				LOGGER.debug("unknown");
				throw new IllegalArgumentException("unexpected parent type: " + parent.getType());
			}
		}

		loadForm();
	}

	/**
     * Used to modify an existing Sale.
     * 
     * @param initialVal    the Sale to modify. 
     */
	SaleEditor(final SaleBean initialVal)
	{
		LOGGER.debug("SaleBean constructor: {}", initialVal);
//        this(initialVal, null, null);
		this.thisValueBean = (initialVal != null ? initialVal : new SaleBean());
		this.addingItem = (initialVal == null);
		this.initialSpecies = null;
		this.initialVariety = null;
		this.hasLinkedItem = false;
		LOGGER.trace("constructor: just before FXMLLoader: ");

		loadForm();
    }


	/**
	 * Use this to drag-drop a diary entry onto a sale for a new sale item.
	 *
 	 * @param initialVal	the existing Sle
	 * @param linkedItem	the item giving rise to the new SaleItem
	 */
	SaleEditor(final SaleBean initialVal, final INotebookBean linkedItem)
	{
        LOGGER.traceEntry("DnD constructor: initialVal: {}, linkedItem: {}", initialVal, linkedItem);

		if (initialVal == null || linkedItem == null)
		{
			throw new IllegalArgumentException("Drag'n'Drop constructor being misused");
		}
//		this.thisValueBean = (initialVal != null ? initialVal : new SaleBean());
		this.thisValueBean = initialVal;
//		this.addingItem = (initialVal == null);
		this.addingItem = true;
//        this.initialSpecies = species;
//        this.initialVariety = variety;
//        this.hasLinkedItem = (species != null);
        this.hasLinkedItem = true;
		switch (linkedItem)
		{
			case HusbandryBean hu -> {
				LOGGER.debug("Husbandry: {}", hu);
				this.initialSpecies = hu.getPlantSpecies();
				this.initialVariety = hu.getPlantVariety();
				earliestDate = hu.getDate();
			}
			case PurchaseItemBean pi -> {
				LOGGER.debug("Purchase Item: {}", pi);
				throw new IllegalArgumentException("parent must not be a Purchase Item");
			}
			case GroundworkBean gw -> {
				LOGGER.debug("Groundwork: {}", gw);
				if (!gw.hasPlantSpecies())
				{
					return;
				}
				this.initialSpecies = gw.getPlantSpecies();
				this.initialVariety = gw.getPlantVariety();
				earliestDate = gw.getDate();
			}
			case AfflictionEventBean ae -> {
				LOGGER.debug("Affliction event: {}", ae);
				if (!ae.hasPlantSpecies())
				{
					return;
				}
				this.initialSpecies = ae.getPlantSpecies();
				this.initialVariety = ae.getPlantVariety();
				earliestDate = ae.getDate();
			}
			default -> {
				LOGGER.debug("unknown");
				throw new IllegalArgumentException("unexpected parent type: " + linkedItem.getType());
			}
		}
        LOGGER.trace("constructor: just before FXMLLoader: ");

		loadForm();
	}

	private void loadForm()
	{
		FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/SaleEditor.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);
		}
	}

	/*
	* Initializes the controller class.
	*/
	@FXML
	private void initialize()
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("initialize()");

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

		thisValueBean.setSaveRequired(true);
		

		tbvPlantSpecies.setCellValueFactory(cdf -> cdf.getValue().plantSpeciesProperty());
		tbvPlantSpecies.setCellFactory(x -> new NotebookEditCellType<>()
		{
			@Override
			protected boolean mayEdit(SaleItemBean rowItem, PlantSpeciesBean cellItem)
			{// NB rowItem is NULL unless we are in edit mode
				if (rowItem == null) return false;
				if (hasLinkedItem && rowItem == linkedItem) return false;
				return true;
			}

			@Override
			void updateViewMode(SaleItemBean rowItem, PlantSpeciesBean cellItem)
			{// NB rowItem is NULL unless we are in edit mode
				LOGGER.debug("tbvPlantSpecies: updateViewMode(): rowItem: {}, cellItem: {}", rowItem, cellItem);
				setGraphic(null);
				setText(null);
				if (cellItem == null)
					return;

				// cannot change the name of an existing product
				if (isEditing() && mayEdit(rowItem, cellItem))
				{
					VBox vBox = new VBox();

					PlantSpeciesCombo cb = new PlantSpeciesCombo(rowItem.getPlantSpecies());
					cb.setEditable(true);
					cb.setOnAction(ev -> {
						LOGGER.debug("plant species: setOnAction: selection: {}", cb.getSelectionModel().getSelectedItem());
						if (cb.getValue() == null) return;
						rowItem.setPlantSpecies(cb.getValue());
						commitEdit(rowItem.getPlantSpecies());
					});
					vBox.getChildren().add(cb);
					setGraphic(vBox);
				} else
				{
					setText(cellItem.getCommonName());
				}

			}
		});

		tbvPlantVariety.setCellValueFactory(cdf -> cdf.getValue().plantVarietyProperty());
		tbvPlantVariety.setCellFactory(x -> new NotebookEditCellType<>()
		{
			@Override
			protected boolean mayEdit(SaleItemBean rowItem, PlantVarietyBean cellItem)
			{// NB rowItem is NULL unless we are in edit mode
				if (rowItem == null) return false;
				if (hasLinkedItem && rowItem == linkedItem) return false;
				return true;
			}

			@Override
			void updateViewMode(SaleItemBean rowItem, PlantVarietyBean cellItem)
			{// NB rowItem is NULL unless we are in edit mode
				LOGGER.traceEntry("tbvPlantVariety: updateViewMode(): rowItem: {}, cellItem: {}", rowItem, cellItem);
				setGraphic(null);
				setText(null);
				if (cellItem == null)
					return;

				// cannot change the secondary name of an existing product
				if (isEditing() && mayEdit(rowItem, cellItem))
				{
					VBox vBox = new VBox();
					PlantVarietyCombo cb = new PlantVarietyCombo(rowItem.getPlantSpecies(), rowItem.getPlantVariety());
					cb.setEditable(true);
					cb.setOnAction(ev -> {
						LOGGER.debug("plant variety: setOnAction: selection: {}", cb.getSelectionModel().getSelectedItem());
						if (cb.getValue() == null) return;
						rowItem.setPlantVariety(cb.getValue());
						commitEdit(rowItem.getPlantVariety());
					});
					vBox.getChildren().add(cb);
					setGraphic(vBox);
				} else
				{
					setText(cellItem.getCommonName());
				}

			}
		});

		tbvQuantity.setCellValueFactory(cdf -> cdf.getValue().quantityProperty());
		tbvQuantity.setCellFactory( x -> new TextFieldTableCellTabOut<>(new BigDecimalStringConverter()) );   //  2.4.0
		tbvUnit.setCellValueFactory(cdf -> cdf.getValue().unitProperty());
		tbvUnit.setCellFactory( x -> new TextFieldTableCellTabOut<>() );
		tbvItemCost.setCellValueFactory(cdf -> cdf.getValue().itemPriceProperty());
		tbvItemCost.setCellFactory( x -> new TextFieldTableCellTabOut<>(new SimpleMoneyStringConverter()) );
		tbvSIComment.setCellValueFactory(cdf -> new SimpleObjectProperty<>(cdf.getValue()));
		tbvSIComment.setCellFactory(c -> new CommentCell<>());
		try {
			tbvItems.getItems().setAll(thisValueBean.getSaleItem());
		} catch (GNDBException ex) {
			PanicHandler.panic(ex);
		}
		for (final SaleItemBean item: tbvItems.getItems())
		{
			item.setSaveRequired(true);
		}

		SaleItemBean pib_new = new SaleItemBean();
		if (!addingItem)
		{
			pib_new.setSale(thisValueBean);
		}
		pib_new.setSaveRequired(true);
        pib_new.plantSpeciesProperty().addListener(saleItemAdder);
		tbvItems.getItems().add(pib_new);
        if (hasLinkedItem)
        {
            LOGGER.trace("initialize(): hasLinkedItem: setting linked item");
            pib_new.setPlantSpecies(this.initialSpecies);
            if (this.initialVariety != null) pib_new.setPlantVariety(initialVariety);
            linkedItem = pib_new;
        }
		tbvItems.setColumnResizePolicy(NotebookResizer.using(tbvItems));


		dtpDate.setConverter(null);	// resets to default converter
		dtpDate.valueProperty().bindBidirectional(thisValueBean.dateProperty());
//		dtpDate.setMandatory();
//        //  2.3.0
//        //  if user tabs out after editing, make sure the DatePicker updates
//        dtpDate.getEditor().focusedProperty().addListener((obj, wasFocused, isFocused)->{
//            if (wasFocused && !isFocused)
//            {
//                try
//                {
//                    dtpDate.setValue(dtpDate.getConverter().fromString(dtpDate.getEditor().getText()));
//                } catch (DateTimeParseException e) {
//                    dtpDate.getEditor().setText(dtpDate.getConverter().toString(dtpDate.getValue()));
//                }
//            }
//        });
		final EditorDateRangeChecker rangeChecker = new EditorDateRangeChecker(thisValueBean);
		LocalDate minDate = rangeChecker.getMinDate();
		if (earliestDate.isAfter(minDate))
		{
			minDate = earliestDate;
		}
		final LocalDate maxDate = rangeChecker.getMaxDate();
		LOGGER.debug("minDate: {}, maxDate: {}", minDate, maxDate);
		dtpDate.setMandatory().setMinValidDate(minDate).setMaxValidDate(maxDate);

		purchasedBy.textProperty().bindBidirectional(thisValueBean.purchasedByProperty());
		numTotalPrice.textProperty().bindBidirectional(thisValueBean.totalPriceProperty(), new SimpleMoneyStringConverter());
        // a default value must be given or it tries to convert null
        // the obvious default of new SimpleMoney() results in £0.00 being shown when you Change an extant Sale - 
        // and it sets that value into the bean.  Hence the rather weird looking bodge.
        numTotalPrice.setTextFormatter(new TextFormatter<>(new SimpleMoneyStringConverter(), thisValueBean.getTotalPrice()));
		btnSave.disableProperty().bind(dtpDate.valueProperty().isNull()
			.or(purchasedBy.textProperty().isEmpty()));
		try {
			btnDelete.setDisable(addingItem || !(this.thisValueBean.canDelete()));
		} catch (GNDBException ex) {
			PanicHandler.panic(ex);
		}

		Platform.runLater(() -> {purchasedBy.requestFocus(); purchasedBy.deselect();});

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

		thisValueBean.cancelEdit();
	}

	@FXML
	private void ctxmnuTopOnAction(WindowEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuTopOnAction()");
		SaleItemBean piBean = tbvItems.getSelectionModel().getSelectedItem();
		if (piBean == null)
		{
			LOGGER.debug("no item selected in Table");
			return;
		}
		try {
			ctxmnuDelete.setDisable( (piBean == linkedItem) || !(piBean.canDelete()) );
		} catch (GNDBException ex) {
			PanicHandler.panic(ex);
		}
		
		try {
			ctxmnuAncestors.setDisable(!(piBean.hasAncestor()));
		} catch (GNDBException ex) {
			PanicHandler.panic(ex);
		}

		LOGGER.traceExit(log4jEntryMsg);
	}

	@FXML
	private void ctxmnuDeleteOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuDeleteOnAction()");
		SaleItemBean piBean = tbvItems.getSelectionModel().getSelectedItem();
		if (piBean == null)
		{
			return;
		}
		if (piBean == linkedItem)
		{
			return;
		}
		
		boolean canDelete = false;
		try {
			canDelete = piBean.canDelete();
		} catch (GNDBException ex) {
			PanicHandler.panic(ex);
		}
		if (!canDelete)
		{
			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)
		{
			LOGGER.debug("after delete confirmed");
			try {
				piBean.delete();
			} catch (GNDBException ex) {
				PanicHandler.panic(ex);
			}
			tbvItems.getItems().remove(piBean);
		}
		LOGGER.traceExit(log4jEntryMsg);
	}	//	ctxmnuDeleteOnAction()

	@FXML
	private void ctxmnuAncestorsOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuAncestorsOnAction()");
		SaleItemBean ixBean = tbvItems.getSelectionModel().getSelectedItem();
		if (ixBean == null)
		{
			return;
		}
		
		boolean hasAncestor = false;
		try {
			hasAncestor = ixBean.hasAncestor();
		} catch (GNDBException ex) {
				PanicHandler.panic(ex);
		}
		if (!hasAncestor)
		{
			return;
		}
		
		StoryLineTree<? extends INotebookBean> history = StoryLineTree.emptyTree();
		try {
			history = ixBean.getAncestors();
		} catch (GNDBException ex) {
			PanicHandler.panic(ex);
		}
		StoryLineTree<DiaryBean> beanTree = history.<DiaryBean>copyTree(bean -> {
			LOGGER.debug("copying: {}", bean);
			return switch (bean.getType())
					{
						case PURCHASEITEM -> DiaryBean.from((PurchaseItemBean) bean);
						case HUSBANDRY -> DiaryBean.from((HusbandryBean) bean);
						case AFFLICTIONEVENT -> DiaryBean.from((AfflictionEventBean) bean);
						case GROUNDWORK -> DiaryBean.from((GroundworkBean) bean);
						default -> null;
					};
			});
		StoryLineTab tabCon = new StoryLineTab();
		loadTab.accept(resources.getString("tab.descendants"), tabCon);
		editorTabSetUp(tabCon);
		tabCon.setHistory(beanTree);
		LOGGER.traceExit(log4jEntryMsg);
	}	//	ctxmnuDescendantsOnAction()


	private void editorTabSetUp(INotebookLoadable tabCon)
	{
		tabCon.setLoadSplit(loadSplit);
		tabCon.setClearSplit(clearSplit);
		tabCon.setLoadTab(loadTab);
	}

	@FXML
	private void btnCancelOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("btnCancelOnAction()");
		if (thisValueBean == null)
		{
			LOGGER.debug("SaleEditorController: Cancel: Bean is null!");
			throw new IllegalStateException("SaleEditorController: Cancel: Bean is null!");
		}

		if (isCancelDenied())
		{
			return;
		}

		thisValueBean.cancelEdit();
		for (SaleItemBean item : tbvItems.getItems())
		{
			item.cancelEdit();
		}
		clearTab.accept(this);
		LOGGER.traceExit(log4jEntryMsg);
	}

	/**
	 * 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() || checkSINeedSave())
		{
			LOGGER.debug("isCancelDenied(): need save is true");
			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;
			}
		}
		return false;
	}

	private boolean checkSINeedSave()
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("checkSINeedSave()");
		for (SaleItemBean item : tbvItems.getItems().subList(0,tbvItems.getItems().size()-1))
		{
			if (item.needSave()) return LOGGER.traceExit(log4jEntryMsg, true);
		}
		return LOGGER.traceExit(log4jEntryMsg, false);
//		return false;
	}

	@FXML
	private void btnDeleteOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("btnDeleteOnAction()");
		if (thisValueBean == null)
		{
			LOGGER.debug("SaleEditorController: Delete: Bean is null!");
			throw new IllegalStateException("SaleEditorController: Delete: Bean is null!");
		}

		NotebookBeanDeleter<SaleBean> deleterImpl = new NotebookBeanDeleter<>(resources);
		if (deleterImpl.deleteItemImpl(thisValueBean))
		{
			deletedBeanProperty().setValue(new Object());
			clearTab.accept(this);
		}

		LOGGER.traceExit(log4jEntryMsg);
	}

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

		// check everything CAN be saved!
		if (!thisValueBean.canSave())
			return;
		try {
			thisValueBean.save();
		} catch (GNDBException ex) {
			PanicHandler.panic(ex);
		}
		LOGGER.debug("Save(): after save Sale, thisValueBean: {}", thisValueBean);
		
		if (tbvItems.getItems().size() <2)
		{// only the dummy item
			clearTab.accept(this);
			LOGGER.debug("Save(): about to set newBeanProperty: old: {}, new: {}", newBeanProperty().get(), thisValueBean);
			newBeanProperty().setValue(thisValueBean);	//	2.9.6
			return;
		}
		final List<SaleItemBean> pibs = tbvItems.getItems().subList(0, tbvItems.getItems().size()-1);

		if (addingItem)
		{// a new sale, so need to set the Sale parent of all the SaleItems - EXCEPT the last, dummy item
			LOGGER.debug("SaleEditorController(): Save: setting parent of SIs for new Sale");
			for (SaleItemBean pib : pibs)
			{
				LOGGER.debug("Save(): setting Sale for PI: {}", pib);
				pib.setSale(thisValueBean);
				LOGGER.debug("Save(): after setting sale, sale: {}", pib.getSale());
			}
		}
		
		for (SaleItemBean pib : pibs)
		{
			LOGGER.debug("SaleEditorController(): Save: checking can save SI: {}", pib);
			if (!pib.canSave())
			{
				LOGGER.debug("SaleEditorController(): Save: cannot save SI: {}", pib);
				return;
			}
		}

		for (SaleItemBean pib : pibs)
		{
			LOGGER.debug("SaleEditorController(): Save: save SI {}", pib);
			try {
				pib.save();
			} catch (GNDBException ex) {
				PanicHandler.panic(ex);
			}
		}
		LOGGER.debug("Save(): about to set newBeanProperty: old: {}, new: {}", newBeanProperty().get(), thisValueBean);
		newBeanProperty().setValue(thisValueBean);
        if (hasLinkedItem)
        {
            newLinkedItemBeanProperty().setValue(linkedItem);
        }
		clearTab.accept(this);
		LOGGER.traceExit(log4jEntryMsg);
	}
	
	SimpleObjectProperty<SaleBean> newBeanProperty()
	{
		if (newBean == null)
		{
			newBean = new SimpleObjectProperty<>();
		}
		return newBean;
	}

	SimpleObjectProperty<SaleItemBean> newLinkedItemBeanProperty()
	{
		if (newLinkedItemBean == null)
		{
			newLinkedItemBean = new SimpleObjectProperty<>();
		}
		return newLinkedItemBean;
	}

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

	private class CommentCell<T extends INotebookBean> extends TableCell<T, T>
	{
		private final CommentCellImpl trueCell = new CommentCellImpl(resources);	//	2.9.6
		private double cellHeight;
				
		@Override
		protected void updateItem(T item, boolean empty)
		{
			super.updateItem(item, empty);
			if (item == null || empty)
			{
				setText(null);
				setGraphic(null);
				return;
			}
			cellHeight = this.getHeight();
			trueCell.setParent(item);	//	2.9.6
			trueCell.updateViewMode(this, item.getComments());
		}

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

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