/*
 * Copyright (C) 2018, 2019, 2021-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.2.6   Fix problem with tool-tips
    2.9.6	When a Diary entry is added/changed, make sure updated comments are shown
    3.0.4	Use new convenience class NotebookBeanDeleter for deletion.
    		Handle Delete action in Editor correctly
    		Fix bug when adding Varieties to a Species
    		Set first row of catalogue selected.
	3.1.0	Show updated list of varieties when added in PS editor.
	3.1.1	Make sure all PVs are available
    3.1.2	Quick search for plant species
    3.2.0	Add Lifecycle Analysis for PlantSpecies and single variety
    3.2.1	Changed interface in LifecycleAnalysisTab
 */

package uk.co.gardennotebook;

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

import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.cell.TextFieldTreeTableCell;
import javafx.scene.layout.AnchorPane;
import javafx.stage.WindowEvent;
import uk.co.gardennotebook.fxbean.PlantSpeciesBean;
import uk.co.gardennotebook.fxbean.PlantVarietyBean;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.EntryMessage;
import uk.co.gardennotebook.spi.*;

/**
 *  Controller class for the PlantCatalogue tab.
 *
	*	@author	Andy Gegg
	*	@version	3.2.1
	*	@since	1.0
 */
final class PlantCatalogueTab extends AnchorPane implements INotebookLoadable {

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

	@FXML
	private MenuItem ctxmnuAnalysis;
	@FXML
	private MenuItem ctxmnuPlantSpecies;
	@FXML
	private MenuItem ctxmnuPlantVariety;
	@FXML
	private MenuButton btnAdd;
	@FXML
	private MenuItem mnuPlantSpecies;
	@FXML
	private MenuItem mnuPlantVariety;
	@FXML
	private Button btnChange;
	@FXML
	private Button btnDelete;
	@FXML
	private MenuItem ctxmnuChange;
	@FXML
	private MenuItem ctxmnuDelete;
	@FXML
	private TreeTableView<PlantCatalogueBean> tvCatalogue;
	@FXML
	private TreeTableColumn<PlantCatalogueBean, String> tvCatalogueSpecies;
	@FXML
	private TreeTableColumn<PlantCatalogueBean, String> tvCatalogueLatinName;
	@FXML
	private TreeTableColumn<PlantCatalogueBean, String> tvCatalogueDescription;
	@FXML
	private TreeTableColumn<PlantCatalogueBean, PlantCatalogueBean> tvCatalogueComments;

	@FXML
	private ResourceBundle resources;
	
	private Consumer<Node> loadSplit;
	private Consumer<Node> clearSplit;
	private BiConsumer<String, Node> loadTab;
	private Consumer<Node> clearTab;
	
	PlantCatalogueTab()
	{
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/PlantCatalogueTab.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()");
		tvCatalogue.setEditable(true);
		
		tvCatalogue.setRowFactory(_ -> new TreeTableRow<>()
        {
            @Override
            public void updateItem(PlantCatalogueBean value, boolean empty)
            {
                super.updateItem(value, empty);
                if (value == null)
                    return;
                boolean noTip =
                        (value.getUtility() == null || value.getUtility().isEmpty()) &&
                                (value.getHardiness() == null || value.getHardiness().isEmpty()) &&
                                (value.getLifeType() == null || value.getLifeType().isEmpty()) &&
                                (value.getPlantType() == null || value.getPlantType().isEmpty());
                if (noTip)
                    setTooltip(null);   //  2.2.6
                else
                    setTooltip(new PlantDetailsTooltip(value));
            }
        });

		tvCatalogue.setOnKeyTyped(ev -> {
			// Thanks to James_D https://stackoverflow.com/questions/34791652/how-to-add-keylistener-on-a-treeitem-javafx for hint
			LOGGER.debug("onKeyTyped: char: {}", ev.getCharacter());
			final String prefix = ev.getCharacter().toLowerCase();
			final TreeItem<PlantCatalogueBean> root = tvCatalogue.getRoot();
			for (var treeItem : root.getChildren())
			{
				int ix = tvCatalogue.getRow(treeItem);
				var pcb = treeItem.getValue();
				String name = pcb.getCommonName();
				if (name.toLowerCase().startsWith(prefix))
				{
					tvCatalogue.scrollTo(ix);
					tvCatalogue.getSelectionModel().select(ix);
					return;
				}
			}
		});

		tvCatalogueSpecies.setCellValueFactory((e)-> e.getValue().getValue().commonNameProperty() );
		tvCatalogueSpecies.setCellFactory(TextFieldTreeTableCell.forTreeTableColumn());

		tvCatalogueLatinName.setCellValueFactory((e)->e.getValue().getValue().latinNameProperty());
		tvCatalogueLatinName.setCellFactory(TextFieldTreeTableCell.forTreeTableColumn());

		tvCatalogueDescription.setCellValueFactory((e)->e.getValue().getValue().descriptionProperty());
		tvCatalogueDescription.setCellFactory(TextFieldTreeTableCell.forTreeTableColumn());

		tvCatalogueComments.setCellValueFactory((e)->e.getValue().valueProperty());
		tvCatalogueComments.setCellFactory(_ -> new CommentCell());
		
		tvCatalogue.setColumnResizePolicy(NotebookResizer.using(tvCatalogue));

		// only allow change and delete if there is a row selected
		btnChange.disableProperty().bind(tvCatalogue.getSelectionModel().selectedItemProperty().isNull());
		tvCatalogue.getSelectionModel().selectedItemProperty().addListener((_, _, nval)->{
				if (nval != null)btnDelete.disableProperty().setValue(!(nval.getValue().canDeleteProperty().get()));});
		
		// can only add a variety if a species (or a variety of a species) is selected so that we know the parent species
		mnuPlantVariety.disableProperty().bind(tvCatalogue.getSelectionModel().selectedItemProperty().isNull());
		ctxmnuPlantVariety.disableProperty().bind(tvCatalogue.getSelectionModel().selectedItemProperty().isNull());
		
		loadTree();
        tvCatalogue.getSortOrder().add(tvCatalogueSpecies);

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

		LOGGER.traceExit(log4jEntryMsg);
	}

	private void loadTree()
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("loadTree()");
		TreeItem<PlantCatalogueBean> root = new TreeItem<>();
		for (PlantCatalogueBean ps : PlantCatalogueBean.getPlantCatalogue(true))
		{
			PrivatePlantCatalogueTreeItem newItem = new PrivatePlantCatalogueTreeItem(ps);
			root.getChildren().add(newItem);
		}
		tvCatalogue.setRoot(root);
		LOGGER.traceExit(log4jEntryMsg);
	}
	
	@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;
	}
	
	@FXML
	private void mnubtnAddPlantSpeciesOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("mnubtnAddPlantSpeciesOnAction()");
		ctxmnuAddPlantSpeciesOnAction(event);
	}

	@FXML
	private void ctxmnuAddPlantSpeciesOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuAddPlantSpeciesOnAction()");
		PlantSpeciesEditor tabCon = new PlantSpeciesEditor();
		loadTab.accept(resources.getString("tab.plantspecies"), tabCon);
		tabCon.newBeanProperty().addListener((_, _, newVal) -> {
			LOGGER.debug("in cat: processing newBeanProperty listener: newVal: {}", newVal);
            var newItem = new PrivatePlantCatalogueTreeItem(new PlantCatalogueBean(newVal));
			tvCatalogue.getRoot().getChildren().add(newItem);
			tvCatalogue.sort();
            tvCatalogue.scrollTo(tvCatalogue.getRow(newItem));
			newItem.setExpanded(true);
            tvCatalogue.getSelectionModel().select(newItem);
		});
		LOGGER.traceExit(log4jEntryMsg);
	}
	
	@FXML
	private void mnubtnAddPlantVarietyOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("mnubtnAddPlantVarietyOnAction()");
		ctxmnuAddPlantVarietyOnAction(event);
	}
	
	@FXML
	private void ctxmnuAddPlantVarietyOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuAddPlantVarietyOnAction()");
		PrivatePlantCatalogueTreeItem psParent = null;
		PrivatePlantCatalogueTreeItem ixBean = (PrivatePlantCatalogueTreeItem)(tvCatalogue.getSelectionModel().getSelectedItem());
//System.out.println("ixBean: "+ixBean);
		if (ixBean == null)
		{
			LOGGER.debug("no item selected in TreeTable");
			return;
		}
		PlantSpeciesBean ps = null;
		if (null != ixBean.getValue().getItemType())
		{
//			System.out.println("ixBean.getValue(): "+ixBean.getValue());
//			System.out.println("ixBean.getValue().getItemType(): "+ixBean.getValue().getItemType());
			switch (ixBean.getValue().getItemType())
			{
				case PLANTSPECIES -> {
					ps = (PlantSpeciesBean) (ixBean.getValue().getItem());
					psParent = ixBean;
				}
				case PLANTVARIETY -> {
					ps = ((PlantVarietyBean) (ixBean.getValue().getItem())).getPlantSpecies();
//					System.out.println("ps: "+ ps);
					psParent = (PrivatePlantCatalogueTreeItem) (ixBean.getParent());
				}
				default -> {
					LOGGER.debug("impossible situation! ixBean of type: {}", ixBean.getValue().getItemType());
					return;
				}
			}
		}
//System.out.println("psParent: " +psParent);
		final PrivatePlantCatalogueTreeItem psParent2 = psParent;
		PlantVarietyEditor tabCon = new PlantVarietyEditor(ps);
		loadTab.accept(resources.getString("tab.plantvariety"), tabCon);
		if (psParent2.isExpanded())
		{
			tabCon.newBeanProperty().addListener((_, _, _) -> {
				tvCatalogue.sort();
			});
		}
		else
		{
			tabCon.newBeanProperty().addListener((_, _, _) -> {
				psParent2.expandedProperty().set(true);
				tvCatalogue.sort();
			});
		}
		LOGGER.traceExit(log4jEntryMsg);
	}

	@FXML
	private void btnChangeOnAction(ActionEvent event) 
	{
		ctxmnuChangeOnAction(event);
	}

	@FXML
	private void btnDeleteOnAction(ActionEvent event)
	{
		ctxmnuDeleteOnAction(event);
	}

	@FXML
	private void ctxmnuTopOnAction(WindowEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuTopOnAction()");
		PrivatePlantCatalogueTreeItem ixBean = (PrivatePlantCatalogueTreeItem)(tvCatalogue.getSelectionModel().getSelectedItem());
		if (ixBean == null)
		{
			LOGGER.debug("no item selected in TreeTable");
			return;
		}
		if (null != ixBean.getValue().getItemType())
		{
			switch (ixBean.getValue().getItemType()) 
			{
				case PLANTSPECIES:
				{
					PlantSpeciesBean ps = (PlantSpeciesBean)(ixBean.getValue().getItem());
                    try {
                        ctxmnuDelete.setDisable(!(ps.canDelete()));
                    } catch (GNDBException ex) {
                        PanicHandler.panic(ex);
                    }
					ctxmnuAnalysis.setDisable(false);
					break;
				}
				case PLANTVARIETY:
				{
					PlantVarietyBean pvb = ((PlantVarietyBean)(ixBean.getValue().getItem()));
                    try {
                        ctxmnuDelete.setDisable(!(pvb.canDelete()));
                    } catch (GNDBException ex) {
                        PanicHandler.panic(ex);
                    }
//					ctxmnuAnalysis.setDisable(true);
					break;
				}
				default:
					LOGGER.debug("impossible situation! ixBean of type: {}", ixBean.getValue().getItemType());
					return;
			}
		}

		LOGGER.traceExit(log4jEntryMsg);
	}

	@FXML
	private void ctxmnuChangeOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuChangeOnAction()");
//		PrivatePlantCatalogueTreeItem psParent = null;
		PrivatePlantCatalogueTreeItem ixBean = (PrivatePlantCatalogueTreeItem)(tvCatalogue.getSelectionModel().getSelectedItem());
		if (ixBean == null)
		{
			LOGGER.debug("no item selected in TreeTable");
			return;
		}
		if (null != ixBean.getValue().getItemType())
		{
			switch (ixBean.getValue().getItemType()) 
			{
				case PLANTSPECIES:
				{
					PlantSpeciesBean ps = (PlantSpeciesBean)(ixBean.getValue().getItem());
					PlantSpeciesEditor tabCon = new PlantSpeciesEditor(ps);
					loadTab.accept(resources.getString("tab.plantspecies"), tabCon);
					tabCon.deletedBeanProperty().addListener((_, _, _) -> {
						ixBean.getParent().getChildren().remove(ixBean);	//	3.0.4
						tvCatalogue.refresh();
					});
					tabCon.newBeanProperty().addListener((_, _, newVal) -> {
						//	3.1.0	To show the (possibly) amended list of varieties, just drop the node and re-add it
						tvCatalogue.getRoot().getChildren().remove(ixBean);
						var newItem = new PrivatePlantCatalogueTreeItem(new PlantCatalogueBean(newVal));
						tvCatalogue.getRoot().getChildren().add(newItem);
						tvCatalogue.sort();
						tvCatalogue.scrollTo(tvCatalogue.getRow(newItem));
						newItem.setExpanded(true);
						tvCatalogue.getSelectionModel().select(newItem);
					});
					break;
				}
				case PLANTVARIETY:
				{
					PlantVarietyBean pvb = ((PlantVarietyBean)(ixBean.getValue().getItem()));
					PlantSpeciesBean ps = pvb.getPlantSpecies();
					PlantVarietyEditor tabCon = new PlantVarietyEditor(ps, pvb);
					loadTab.accept(resources.getString("tab.plantvariety"), tabCon);
					tabCon.deletedBeanProperty().addListener((_, _, _) -> {
						ixBean.getParent().getChildren().remove(ixBean);	//	3.0.4
						tvCatalogue.refresh();
					});
					break;
				}
				default:
					LOGGER.debug("impossible situation! ixBean of type: {}", ixBean.getValue().getItemType());
					return;
			}
		}

		LOGGER.traceExit(log4jEntryMsg);
	}
	
	@FXML
	private void ctxmnuDeleteOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuDeleteOnAction()");
//		PrivatePlantCatalogueTreeItem psParent = null;
		PrivatePlantCatalogueTreeItem ixBean = (PrivatePlantCatalogueTreeItem)(tvCatalogue.getSelectionModel().getSelectedItem());
		if (ixBean == null)
		{
			LOGGER.debug("no item selected in TreeTable");
			return;
		}
		if (null != ixBean.getValue().getItemType())
		{
			switch (ixBean.getValue().getItemType()) 
			{
				case PLANTSPECIES:
				{
					PlantSpeciesBean ps = (PlantSpeciesBean)(ixBean.getValue().getItem());

					NotebookBeanDeleter<PlantSpeciesBean> deleterImpl = new NotebookBeanDeleter<>(resources);
					if (deleterImpl.deleteItemImpl(ps))
					{
							ixBean.getParent().getChildren().remove(ixBean);	//	2.9.6
							tvCatalogue.refresh();
					}

					break;
				}
				case PLANTVARIETY:
				{
					PlantVarietyBean pvb = ((PlantVarietyBean)(ixBean.getValue().getItem()));

					NotebookBeanDeleter<PlantVarietyBean> deleterImpl = new NotebookBeanDeleter<>(resources);
					if (deleterImpl.deleteItemImpl(pvb))
					{
						ixBean.getParent().getChildren().remove(ixBean);	//	2.9.6
						tvCatalogue.refresh();
					}
					break;
				}
				default:
					LOGGER.debug("impossible situation! ixBean of type: {}", ixBean.getValue().getItemType());
					return;
			}
		}

		LOGGER.traceExit(log4jEntryMsg);
	}

	@FXML
	private void ctxmnuAnalysisOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ctxmnuAnalysisOnAction()");
//		PrivatePlantCatalogueTreeItem psParent = null;
		PrivatePlantCatalogueTreeItem ixBean = (PrivatePlantCatalogueTreeItem)(tvCatalogue.getSelectionModel().getSelectedItem());
		if (ixBean == null)
		{
			LOGGER.debug("no item selected in TreeTable");
			return;
		}
		PlantSpeciesBean psBean = null;
		PlantVarietyBean pvBean = null;
		if (ixBean.getValue().getItemType() == NotebookEntryType.PLANTVARIETY)
		{
			pvBean = (PlantVarietyBean) (ixBean.getValue().getItem());
//			psBean = pvBean.getPlantSpecies();
		}
		else
		{
			psBean = (PlantSpeciesBean) (ixBean.getValue().getItem());
		}

		LifecycleAnalysisTab tabCon = new LifecycleAnalysisTab(psBean, pvBean, null);
		loadTab.accept(resources.getString("tab.analysis.lifecycle"), tabCon);
	}

	private static class PrivatePlantCatalogueTreeItem extends TreeItem<PlantCatalogueBean> {

		PrivatePlantCatalogueTreeItem(PlantCatalogueBean val){
			super(val);
		}

		PrivatePlantCatalogueTreeItem(){
			super();
		}

		boolean isFirstCall = true;

		@Override
		public ObservableList<TreeItem<PlantCatalogueBean>> getChildren()
		{
			EntryMessage log4jEntryMsg = LOGGER.traceEntry("PrivatePlantCatalogueTreeItem.getChildren(): isFirstCall: {}", isFirstCall);
			if (isFirstCall)
			{
				isFirstCall = false;
				PlantCatalogueBean val = this.getValue();
				if (val == null) return FXCollections.emptyObservableList();
				ObservableList<TreeItem<PlantCatalogueBean>> kids = FXCollections.observableArrayList();
				ObservableList<PlantCatalogueBean> theKids = val.getChildren();
				theKids.addListener(new ListChangeListener<PlantCatalogueBean>(){
					@Override
					public void onChanged(ListChangeListener.Change<? extends PlantCatalogueBean> c)
					{
						LOGGER.debug("PrivatePlantCatalogueTreeItem.getChildren(): ListChangeListener: onChanged()");
						while (c.next())
						{
							LOGGER.debug("getChildren(): ListChangeListener: onChanged(): top of loop");
							if (c.wasPermutated())
							{
								LOGGER.debug("PrivatePlantCatalogueTreeItem.getChildren(): ListChangeListener: onChanged(): wasPermutated()");
								return;
							}
							if (c.wasRemoved())
							{
								LOGGER.debug("PrivatePlantCatalogueTreeItem.getChildren(): ListChangeListener: onChanged(): wasRemoved()");
								LOGGER.debug("PrivatePlantCatalogueTreeItem.getChildren(): ListChangeListener: onChanged(): from: {}, to:{}", c.getFrom(), c.getTo());
								PrivatePlantCatalogueTreeItem.this.getChildren().remove(c.getFrom(), c.getTo());
								return;
							}
							if (c.wasAdded())
							{
								LOGGER.debug("PlantCatalogueTab: PrivatePlantCatalogueTreeItem.getChildren(): ListChangeListener: onChanged(): wasAdded()");
								int from = c.getFrom();
								for (PlantCatalogueBean pv : c.getAddedSubList())
								{
									LOGGER.debug("PlantCatalogueTab: PrivatePlantCatalogueTreeItem.getChildren(): ListChangeListener: onChanged(): wasAdded(): PV: {}", pv);
									PrivatePlantCatalogueTreeItem.this.getChildren().add(from++, new PrivatePlantCatalogueTreeItem(pv));
								}
								return;
							}
						}
					}
				});
				for (PlantCatalogueBean pc : val.getChildren())
					kids.add(new PrivatePlantCatalogueTreeItem(pc));
				super.getChildren().setAll(kids);
			}
			return LOGGER.traceExit(log4jEntryMsg, super.getChildren());
		}
		@Override
		public boolean isLeaf() {
			return (this.getValue()).isLeaf();
		}

	}
	
	private class CommentCell extends TreeTableCell<PlantCatalogueBean, PlantCatalogueBean>
	{
		private final CommentCellImpl trueCell = new CommentCellImpl(resources);	//	2.9.6
		private double cellHeight;
				
		@Override
		protected void updateItem(PlantCatalogueBean item, boolean empty)
		{
			LOGGER.traceEntry("CommentCell: updateItem(): item: {}", item);
			super.updateItem(item, empty);
			if (item == null || empty)
			{
				setText(null);
				setGraphic(null);
				return;
			}
			cellHeight = this.getHeight();
			LOGGER.debug("PlantCatalogueTab: CommentCell(): setParent: {}", item.getItem());
			trueCell.setParent(item.getItem());	//	2.9.6
//			LOGGER.debug("PlantCatalogueTab: CommentTableCell(): getCommentst: {}", item.getItem().getComments());
//			trueCell.updateViewMode(this, item.getItem().getComments());
			LOGGER.debug("PlantCatalogueTab: CommentTableCell(): getCommentst: {}", item.getComments());
			trueCell.updateViewMode(this, item.getComments());
		}

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

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