package be.iminds.ilabt.jfed.ui.javafx.probe_gui.command_arguments;

import be.iminds.ilabt.jfed.util.TextUtil;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * MapStringToObjectArgumentChooser
 */
public class MapStringToObjectArgumentChooser extends CommandArgumentChooser<Map<String, Object>> {
    private final static String validationErrorCssClass = "validation_error";

    class MyEntry {
        public final StringProperty key = new SimpleStringProperty();
        public final ObjectProperty value = new SimpleObjectProperty();

        public MyEntry(String key, Object value) {
            this.key.set(key);
            this.value.set(value);
        }

        public void addListeners() {
            this.key.addListener(changeListener);
            this.value.addListener(changeListener);
        }

        public void removeListeners() {
            this.key.removeListener(changeListener);
            this.value.removeListener(changeListener);
        }
    }

    private final ChangeListener<Object> changeListener = (observableValue, s, s1) -> updateMap();

    @FXML
    protected TableView<MyEntry> table;

    final ObservableList<MyEntry> argument = FXCollections.observableArrayList();
    private final ObjectProperty<Map<String, Object>> valueProperty = new SimpleObjectProperty<>();

    public MapStringToObjectArgumentChooser(Map<String, Object> defaultArguments) {
        this(defaultArguments, "Key", "Value");
    }

    public MapStringToObjectArgumentChooser(Map<String, Object> defaultArguments, String keyColHeader, String valueColHeader) {
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("MapStringToObjectArgumentChooser.fxml"));
        fxmlLoader.setRoot(this);
        fxmlLoader.setController(this);

        try {
            fxmlLoader.load();

            assert table != null;
            if (defaultArguments != null)
                for (Map.Entry<String, Object> e : defaultArguments.entrySet())
                    argument.add(new MyEntry(e.getKey(), e.getValue()));
            table.setItems(this.argument);

            table.setEditable(true);

            final TableColumn<MyEntry, String> keyCol = new TableColumn<>(keyColHeader);
            keyCol.setMinWidth(100);
            keyCol.setCellValueFactory(myEntryStringCellDataFeatures -> myEntryStringCellDataFeatures.getValue().key);
            keyCol.setCellFactory(p -> new StringEditingCell());

            final TableColumn<MyEntry, Object> valueCol = new TableColumn<>(valueColHeader);
            valueCol.setMinWidth(200);
            valueCol.setCellValueFactory(myEntryStringCellDataFeatures -> myEntryStringCellDataFeatures.getValue().value);
            valueCol.setCellFactory(p -> new ObjectEditingCell());

            table.getColumns().add(keyCol);
            table.getColumns().add(valueCol);

            argument.addListener((ListChangeListener<MyEntry>) change -> {
                while (change.next()) {
                    for (MyEntry r : change.getRemoved())
                        r.removeListeners();
                    for (MyEntry a : change.getAddedSubList())
                        a.addListeners();
                }

                //not the best way to link this, but I want to keep "value" in CommandArgumentChooser as it is
                updateMap();
            });

            //init
            updateMap();
            for (MyEntry e : argument)
                e.addListeners();

            value = valueProperty;
            assert value != null;
        } catch (IOException exception) {
            throw new RuntimeException(exception);
        }
    }

    private void updateMap() {
        Map<String, Object> newMap = new HashMap<>();
        for (MyEntry e : argument)
            newMap.put(e.key.get(), e.value.get());
        valueProperty.set(newMap);
    }

    public void add() {
        argument.add(new MyEntry("key", "value"));
    }

    public void remove() {
        MyEntry r = table.getSelectionModel().getSelectedItem();
        if (r != null)
            argument.remove(r);
    }


    static class StringEditingCell extends TableCell<MyEntry, String> {
        private TextField textField;

        public StringEditingCell() {
        }

        @Override
        public void startEdit() {
//            System.out.println("startEdit");
            if (!isEmpty()) {
                super.startEdit();
                createTextField();
                setText(null);
                setGraphic(textField);
                Platform.runLater(() -> {
                    //bug: somehow the caret isn't always shown, which is confusing
                    textField.requestFocus();
                    textField.selectAll();
                    textField.end();

                    Platform.runLater(() -> {
                        textField.requestFocus();
                        textField.end();
                    });
                });
            }
        }

        @Override
        public void cancelEdit() {
            super.cancelEdit();

            setText(getItem());
            setGraphic(null);
        }

        @Override
        public void updateItem(String item, boolean empty) {
//            System.out.println("updateItem "+item);
            super.updateItem(item, empty);

            if (empty) {
                setText(null);
                setGraphic(null);
            } else {
                if (isEditing()) {
                    if (textField != null) {
                        textField.setText(getString());
                    }
                    setText(null);
                    setGraphic(textField);
                } else {
                    setText(getString());
                    setGraphic(null);
                }
            }
        }

        private void createTextField() {
            if (textField == null) {
                textField = new TextField(getString());
                textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
                textField.focusedProperty().addListener((arg0, arg1, arg2) -> {
                    if (!arg2) {
                        commitEdit(textField.getText());
                    }
                });
            }
        }

        private String getString() {
            return getItem() == null ? "" : getItem();
        }
    }

    private static class ObjectEditingCell extends TableCell<MyEntry, Object> {
        private ComboBox<ObjectType> typeComboBox;
        private TextField textField;
        private HBox hbox;

        private enum ObjectType {STRING, INTEGER, DOUBLE, BOOLEAN}

        private final ObservableList<ObjectType> allObjectTypes =
                FXCollections.observableArrayList(ObjectType.values());

        //always editing
        public ObjectEditingCell() {
            setEditable(false);
            createHBox();
            setGraphic(null);
            setText(null);
        }

        @Override
        public void updateItem(Object item, boolean empty) {
            super.updateItem(item, empty);

            if (empty) {
                setText(null);
                setGraphic(null);
            } else {
                if (textField != null && item != null) {
                    textField.getStyleClass().removeAll(validationErrorCssClass);
                    textField.setText(getString());
                    typeComboBox.getSelectionModel().select(getType());
                }

                setText(null);
                setGraphic(hbox);

            }
        }

        private Object calcValue() {
            return calcValue(typeComboBox.getValue(), textField.getText());
        }

        /**
         * return null if invalid value.
         */
        private Object calcValue(ObjectType selectedObjectType, String curString) {
            switch (selectedObjectType) {
                case STRING: {
                    return curString;
                }
                case INTEGER: {
                    try {
                        return Integer.parseInt(curString);
                    } catch (NumberFormatException e) {
                        return null;
                    }
                }
                case BOOLEAN: {
                    return TextUtil.stringToBoolean(curString);
                }
                case DOUBLE: {
                    try {
                        return Double.parseDouble(curString);
                    } catch (NumberFormatException e) {
                        return null;
                    }
                }
                default:
                    throw new RuntimeException("Unsupported Object type: " + selectedObjectType);
            }
        }

        private void confirmEditItem() {
            ObservableValue<Object> ov = getTableColumn().getCellObservableValue(getIndex());
            SimpleObjectProperty op = (SimpleObjectProperty) ov;

            Object item = calcValue();
            textField.getStyleClass().removeAll(validationErrorCssClass);
            if (item == null) {
                textField.getStyleClass().add(validationErrorCssClass);
            } else {
                //commit change
                op.setValue(item);
            }

        }

        private void createHBox() {
            if (textField == null) {
                typeComboBox = new ComboBox<>();
                hbox = new HBox();
                textField = new TextField(getString());

                textField.focusedProperty().addListener((arg0, oldIsFocused, newIsFocused) -> {
                    if (!newIsFocused) {
//                            commitEdit(calcValue());
                        confirmEditItem();
                    }
                });
                textField.setOnAction(actionEvent -> confirmEditItem());

                typeComboBox.setItems(allObjectTypes);
                typeComboBox.getSelectionModel().select(getType());
                typeComboBox.getSelectionModel().selectedItemProperty().addListener((arg0, oldItem, newItem) -> confirmEditItem());

                hbox.getChildren().addAll(typeComboBox, textField);
                hbox.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
            }
        }

        private String getString() {
            return getItem() == null ? "" : getItem().toString();
        }

        private ObjectType getType() {
            Object item = getItem();
            if (item == null) return ObjectType.STRING; //fall back to string instead   //return null;
            if (item instanceof String) return ObjectType.STRING;
            if (item instanceof Integer) return ObjectType.INTEGER;
            if (item instanceof Double) return ObjectType.DOUBLE;
            if (item instanceof Boolean) return ObjectType.BOOLEAN;
            throw new RuntimeException("Unsupported Object type: " + item.getClass().getName());
        }
    }

}
