package milkman.ui.components;

import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.RecursiveTreeItem;
import com.jfoenix.controls.cells.editors.TextFieldEditorBuilder;
import com.jfoenix.controls.cells.editors.base.GenericEditableTreeTableCell;
import com.jfoenix.controls.cells.editors.base.JFXTreeTableCell;
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.input.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import lombok.EqualsAndHashCode;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import milkman.PlatformUtil;
import milkman.utils.fxml.GenericBinding;
import milkman.utils.javafx.JavaFxUtils;
import milkman.utils.javafx.ResizableJfxTreeTableView;

import java.util.Comparator;
import java.util.List;
import java.util.function.*;
import java.util.stream.Collectors;

@Value
@EqualsAndHashCode(callSuper = true)
class RecursiveWrapper<T> extends RecursiveTreeObject<RecursiveWrapper<T>>{
	T data;
}

@Slf4j
public class JfxTableEditor<T> extends StackPane {
	
	
	private ResizableJfxTreeTableView<RecursiveWrapper<T>> table = new ResizableJfxTreeTableView<RecursiveWrapper<T>>();

	private ObservableList<RecursiveWrapper<T>> obsWrappedItems;

	private JFXButton addItemBtn;

	
	private Function<T, String> rowToStringConverter = null;

	private Function<String, T> stringToRowConverter;
	
	private Integer firstEditableColumn = null;

	private Supplier<T> newItemCreator;


	public JfxTableEditor() {
		table.setShowRoot(false);
		table.setEditable(true);
		table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

		JavaFxUtils.publishEscToParent(table);
		this.getChildren().add(table);
		addItemBtn = new JFXButton();
		addItemBtn.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
		addItemBtn.setGraphic(new FontAwesomeIconView(FontAwesomeIcon.PLUS, "1.5em"));
		addItemBtn.getStyleClass().add("btn-add-entry");
		StackPane.setAlignment(addItemBtn, Pos.BOTTOM_RIGHT);
		StackPane.setMargin(addItemBtn, new Insets(0, 20, 20, 0));
		this.getChildren().add(addItemBtn);

		final KeyCombination keyCodeCopy = PlatformUtil.getControlKeyCombination(KeyCode.C);
		final KeyCombination keyCodePaste = PlatformUtil.getControlKeyCombination(KeyCode.V);
	    table.setOnKeyPressed(event -> {
	        if (keyCodeCopy.match(event)) {
	            copySelectionToClipboard();
	        }
	        if (keyCodePaste.match(event)) {
	        	pasteSelectionFromClipboard();
	        }
	    });
	}
	

	private void copySelectionToClipboard() {
		if (rowToStringConverter == null)
			return;
		StringBuilder b = new StringBuilder();
		boolean first = true;
		for (TreeItem<RecursiveWrapper<T>> treeItm : table.getSelectionModel().getSelectedItems()) {
			if (!first)
				b.append(System.lineSeparator());
			first = false;
			b.append(rowToStringConverter.apply(treeItm.getValue().getData()));	
		}
		
		final ClipboardContent clipboardContent = new ClipboardContent();
	    clipboardContent.putString(b.toString());
	    Clipboard.getSystemClipboard().setContent(clipboardContent);
	}
	
	
	private void pasteSelectionFromClipboard() {
		if (stringToRowConverter == null)
			return;
		
	    String content = (String) Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
		if (content != null) {
			String lines[] = content.split("\\r?\\n");
			try {
				for (String line : lines) {
					T newEntry = stringToRowConverter.apply(line);
					if (newEntry != null) {
						obsWrappedItems.add(new RecursiveWrapper<>(newEntry));
					}
				}
			} catch (Throwable t) {
				log.error("Failed to parse clipboard content: {}", t.getMessage());
			}
		
		}
		
		
	}
	
	

	/**
	 * used for converting selected rows to clipboard content
	 * 
	 * @param rowToStringConverter
	 */
	public void setRowToStringConverter(Function<T, String> rowToStringConverter) {
		this.rowToStringConverter = rowToStringConverter;
	}
	
	public void setStringToRowConverter(Function<String, T> stringToRowConverter) {
		this.stringToRowConverter = stringToRowConverter;
	}
	

	public void addReadOnlyColumn(String name, Function<T, String> getter) {
		TreeTableColumn<RecursiveWrapper<T>, String> column = new TreeTableColumn<>(name);
		column.setCellFactory((TreeTableColumn<RecursiveWrapper<T>, String> param) -> {
			return new GenericEditableTreeTableCell<RecursiveWrapper<T>, String>(new SelectableTextFieldBuilder());
		});
		column.setCellValueFactory(param -> GenericBinding.of(getter, (e, o) -> {}, param.getValue().getValue().getData()));
		
		column.setMaxWidth(400);
		column.setMinWidth(100);
//		column.setPrefWidth(Control.USE_COMPUTED_SIZE);
		table.getColumns().add(column);
	}
	
	public void addColumn(String name, Function<T, String> getter, BiConsumer<T, String> setter) {
		TreeTableColumn<RecursiveWrapper<T>, String> column = new TreeTableColumn<>(name);
		column.setCellFactory((TreeTableColumn<RecursiveWrapper<T>, String> param) -> {
			var cell = new GenericEditableTreeTableCell<RecursiveWrapper<T>, String>(new TextFieldEditorBuilderPatch());
			cell.setStepFunction(getStepFunction());
			return cell;
		});
		column.setCellValueFactory(param -> GenericBinding.of(getter, setter, param.getValue().getValue().getData()));
		column.setMaxWidth(400);
		column.setMinWidth(100);
		table.getColumns().add(column);
//		column.setPrefWidth(Control.USE_COMPUTED_SIZE);
		if (firstEditableColumn == null)
			firstEditableColumn = table.getColumns().size() -1; 
	}


	//returns the number of rows to advance.
	protected BiFunction<Integer, Integer, Integer> getStepFunction() {
		return (index, direction) -> {
			if (obsWrappedItems.size()-1 == index && direction > 0) {
				var newItemAdded = addNewItem();
				if (newItemAdded) {
					Platform.runLater(() -> {
						if (firstEditableColumn != null)
							table.edit(index+direction, table.getColumns().get(firstEditableColumn));
					});
				}
				return newItemAdded ? direction : 0;
			}
			return direction;
		};
	}
	
	public void addColumn(String name, Function<T, String> getter, BiConsumer<T, String> setter, Consumer<TextField> textFieldInitializer) {
		TreeTableColumn<RecursiveWrapper<T>, String> column = new TreeTableColumn<>(name);
		column.setCellFactory((TreeTableColumn<RecursiveWrapper<T>, String> param) -> {
			var cell = new GenericEditableTreeTableCell<RecursiveWrapper<T>, String>(new InitializingCellBuilder(textFieldInitializer));
			cell.setStepFunction(getStepFunction());
			return cell;
		});
		column.setCellValueFactory(param -> GenericBinding.of(getter, setter, param.getValue().getValue().getData()));
		column.setMaxWidth(400);
		column.setMinWidth(100);
		table.getColumns().add(column);
//		column.setPrefWidth(Control.USE_COMPUTED_SIZE);
		if (firstEditableColumn == null)
			firstEditableColumn = table.getColumns().size() -1;
	}

	public void addCheckboxColumn(String name, Function<T, Boolean> getter, BiConsumer<T, Boolean> setter) {
		TreeTableColumn<RecursiveWrapper<T>, Boolean> column = new TreeTableColumn<>(name);
		column.setCellValueFactory(param -> {
			return GenericBinding.of(getter, setter, param.getValue().getValue().getData());
		});
		column.setCellFactory(param -> new BooleanCell<>(column));
		column.setMinWidth(100);
		column.setEditable(false);
		table.getColumns().add(column);
//		column.setPrefWidth(Control.USE_COMPUTED_SIZE);
	}

	public void addDeleteColumn(String name) {
		addDeleteColumn(name, null);
	}
	
	public void addDeleteColumn(String name, Consumer<T> listener) {
		TreeTableColumn<RecursiveWrapper<T>, String> column = new TreeTableColumn<>(name);
		column.setCellFactory(c -> new DeleteEntryCell(listener));
		column.setMinWidth(100);
		column.setEditable(false);
		table.getColumns().add(column);
//		column.setPrefWidth(Control.USE_COMPUTED_SIZE);
	}

	public void enableAddition(Supplier<T> newItemCreator) {
		this.newItemCreator = newItemCreator;
		addItemBtn.setVisible(true);
		this.addItemBtn.setOnAction(e -> { 
			addNewItem();
		});
	}


	protected boolean addNewItem() {
		if (newItemCreator != null) {
			obsWrappedItems.add(new RecursiveWrapper<>(newItemCreator.get()));
			return true;
		}
		return false;
	}
	public void disableAddition() {
		addItemBtn.setVisible(false);
	}
	
	public void setItems(List<T> items) {
		setItems(items, null);
	}
	public void setItems(List<T> items, Comparator<T> comparator) {
		List<RecursiveWrapper<T>> wrappedItems = items.stream().map(i -> new RecursiveWrapper<>(i)).collect(Collectors.toList());
		obsWrappedItems = FXCollections.observableList(wrappedItems);
		if (comparator != null) {
			FXCollections.sort(obsWrappedItems, (ra, rb) -> comparator.compare(ra.getData(), rb.getData()));
		}
		
		
		obsWrappedItems.addListener(new ListChangeListener<RecursiveWrapper<T>>() {

			@Override
			public void onChanged(Change<? extends RecursiveWrapper<T>> c) {
				//forward removals:
				if (!c.next())
					return;
				
				if (c.wasRemoved()) {
					for(var ri : c.getRemoved()) {
						items.remove(ri.getData());
					}
				}
				
				if (c.wasAdded()) {
					RecursiveWrapper<T> newEntry = c.getAddedSubList().get(0);
					items.add(newEntry.getData());
				}
			}
		});
		
		final TreeItem<RecursiveWrapper<T>> root = new RecursiveTreeItem<>(obsWrappedItems, RecursiveTreeObject::getChildren); 
		table.setRoot(root);
		
		Platform.runLater(() -> {
			table.resizeColumns();
		});
		
		//register double-click listener for empty rows, to add a new instance
//		this.setRowFactory(view -> {
//		    TableRow<T> row = new TableRow<T>();
//		    row.setOnMouseClicked(event -> {
//			    if (row.isEmpty() && (event.getClickCount() == 2)) {
//			        T myItem = newItemCreator.get();
//			        getItems().add(myItem);
//			    } 
//			});
//		    return row;
//		});
		
		//we cant click on row if there is no row, so we have to register another event handler
		//for when the table is empty
//		this.setOnMouseClicked(event -> {
//		    if (getItems().isEmpty() && (event.getClickCount() == 2)) {
//		        T myItem = newItemCreator.get();
//		        getItems().add(myItem);
//		    } 
//		});
		
	}
	
	public void clearContent() {
		table.getColumns().clear();
	}



	private final class DeleteEntryCell extends TreeTableCell<RecursiveWrapper<T>, String> {
		final JFXButton btn;
		private Consumer<T> listener;
		
		public DeleteEntryCell(Consumer<T> listener) {
			this.listener = listener;
			btn = new JFXButton();
			btn.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
			btn.setGraphic(new FontAwesomeIconView(FontAwesomeIcon.TIMES, "1.5em"));
		}
		
		@Override
		public void updateItem(String item, boolean empty) {
		    super.updateItem(item, empty);
		    if (empty) {
		        setGraphic(null);
		        setText(null);
		    } else {
		        btn.setOnAction(event -> {

					Platform.runLater( () -> {
					
//						table.build().getChildren().remove(getTreeTableRow().getIndex());
//						obsWrappedItems.remove(getTreeTableRow().getIndex());
						RecursiveWrapper<T> removedItem = getTreeTableRow().getItem();
						obsWrappedItems.remove(removedItem);
//						table.build().getValue().setChildren(obsWrappedItems);
//						table.setRoot(table.build());
//						table.refresh();
						if (listener != null) {
							listener.accept(removedItem.getData());
						}
					});
//                        T element = getTreeTableView().build().getChildren().get(getTreeTableRow().getIndex()).getValue();
//                        getItems().remove(element);
		        	
		        });
		        var hbox = new HBox(btn);
		        hbox.setAlignment(Pos.CENTER);
				setGraphic(hbox);
		        setText(null);
		    }
		}
	}




	public class BooleanCell<T2> extends JFXTreeTableCell<T2, Boolean> {
        private CheckBox checkBox;
        
        public BooleanCell(TreeTableColumn<RecursiveWrapper<T>, ?> column) {
            checkBox = new CheckBox();
            
//            checkBox.setDisable(true);
            checkBox.setOnAction(e -> {
        			var row = BooleanCell.this.getTreeTableRow().getIndex();
					table.edit(row, column);
//        			itemProperty().setValue(newValue == null ? false : newValue);

					//we cannot use commitEdit because we would need to set column editable
					// but we dont want this as <tab> should not select this column,
//					commitEdit(!checkBox.isSelected());
					//hacky way to set value without column being editable:
					GenericBinding<T2, Boolean> binding = (GenericBinding<T2, Boolean>) column.getCellObservableValue(row);
					binding.set(checkBox.isSelected());
            });
            this.setGraphic(checkBox);
            this.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
            this.setEditable(true);
        }

        @Override
        public void updateItem(Boolean item, boolean empty) {
            super.updateItem(item, empty);
            if (empty || item == null) {
                this.setGraphic(null);
            } else {
                checkBox.setSelected(item);
            	var hbox = new HBox(checkBox);
            	hbox.setAlignment(Pos.CENTER);
				this.setGraphic(hbox);
            }
        }
    }
	
	
	public static class TextFieldEditorBuilderPatch extends TextFieldEditorBuilder {


	    @Override
	    public void startEdit() {
	        Platform.runLater(() -> {
	        	if (textField != null) { //added nullcheck
		            textField.selectAll();
		            textField.requestFocus();
	        	}
	        });
	    }

	    
	    @Override
	    public void updateItem(String item, boolean empty) {
	        Platform.runLater(() -> {
	        	if (textField != null) { //added nullcheck
		            textField.selectAll();
		            textField.requestFocus();
	        	}
	        });
	    }
	}
	
	public static class SelectableTextFieldBuilder extends JfxTableEditor.TextFieldEditorBuilderPatch {

		@Override
		public Region createNode(String value, EventHandler<KeyEvent> keyEventsHandler,
				ChangeListener<Boolean> focusChangeListener) {
			Region node = super.createNode(value, keyEventsHandler, focusChangeListener);
			
			this.textField.setEditable(false);
			
			return node;
		}
		
		
	}

	public void setStringToRowConverter(Object stringToRowConverter2) {
		// TODO Auto-generated method stub
		
	}
	
	
}