package jace.config;

import jace.config.Configuration.ConfigNode;
import java.io.File;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javafx.beans.Observable;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SplitPane;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.util.StringConverter;

public class ConfigurationUIController {
    public static final String DELIMITER = "~!~";

    @FXML
    private ResourceBundle resources;

    @FXML
    private URL location;

    @FXML
    private VBox settingsVbox;

    @FXML
    private SplitPane splitPane;

    @FXML
    private ScrollPane settingsScroll;

    @FXML
    private TreeView<ConfigNode> deviceTree;

    @FXML
    private ScrollPane treeScroll;

    @FXML
    void reloadConfig(MouseEvent event) {
        Configuration.loadSettings();
        resetDeviceTree();
    }

    @FXML
    void saveConfig(MouseEvent event) {
        applyConfig(event);
        Configuration.saveSettings();
    }

    @FXML
    void applyConfig(MouseEvent event) {
        Configuration.applySettings(Configuration.BASE);
        resetDeviceTree();
    }

    @FXML
    void cancelConfig(MouseEvent event) {
        Configuration.buildTree();
        resetDeviceTree();
    }

    @FXML
    public void initialize() {
        assert settingsVbox != null : "fx:id=\"settingsVbox\" was not injected: check your FXML file 'Configuration.fxml'.";
        assert splitPane != null : "fx:id=\"splitPane\" was not injected: check your FXML file 'Configuration.fxml'.";
        assert settingsScroll != null : "fx:id=\"settingsScroll\" was not injected: check your FXML file 'Configuration.fxml'.";
        assert deviceTree != null : "fx:id=\"deviceTree\" was not injected: check your FXML file 'Configuration.fxml'.";
        assert treeScroll != null : "fx:id=\"treeScroll\" was not injected: check your FXML file 'Configuration.fxml'.";
        resetDeviceTree();
        deviceTree.getSelectionModel().selectedItemProperty().addListener(this::selectionChanged);
        deviceTree.maxWidthProperty().bind(treeScroll.widthProperty());
    }
    
    private void resetDeviceTree() {
        Set<String> expanded = new HashSet<>();
        String current = getCurrentNodePath();
        getExpandedNodes("", deviceTree.getRoot(), expanded);
        deviceTree.setRoot(Configuration.BASE);
        setExpandedNodes("", deviceTree.getRoot(), expanded);
        setCurrentNodePath(current);
    }

    private void getExpandedNodes(String prefix, TreeItem<ConfigNode> root, Set<String> expanded) {
        if (root == null) return;
        root.getChildren().stream().filter((item) -> (item.isExpanded())).forEach((item) -> {
            String name = prefix+item.toString();
            expanded.add(name);
            getExpandedNodes(name+DELIMITER, item, expanded);
        });
    }

    private void setExpandedNodes(String prefix, TreeItem<ConfigNode> root, Set<String> expanded) {
        if (root == null) return;
        root.getChildren().stream().forEach((item) -> {
            String name = prefix+item.toString();
            if (expanded.contains(name)) {
                item.setExpanded(true);
            }
            setExpandedNodes(name+DELIMITER, item, expanded);
        });
    }

    private String getCurrentNodePath() {
        TreeItem<ConfigNode> current = deviceTree.getSelectionModel().getSelectedItem();
        if (current == null) return null;
        String out = current.toString();
        while (current.getParent() != null) {
            out = current.getParent().toString()+DELIMITER+current;
            current = current.getParent();
        }
        return out;
    }
    
    private void setCurrentNodePath(String value) {
        if (value == null) return;
        String[] parts = value.split(Pattern.quote(DELIMITER));
        TreeItem<ConfigNode> current = deviceTree.getRoot();
        for (String part : parts) {
            for (TreeItem child : current.getChildren()) {
                if (child.toString().equals(part)) {
                    current = child;
                }
            }
        }
        deviceTree.getSelectionModel().select(current);
    }
    
    private void selectionChanged(
            ObservableValue<? extends TreeItem<ConfigNode>> observable,
            TreeItem<ConfigNode> oldValue,
            TreeItem<ConfigNode> newValue) {
        clearForm();
        buildForm((ConfigNode) newValue);
    }

    private void clearForm() {
        settingsVbox.getChildren().clear();
    }

    private void buildForm(ConfigNode node) {
        if (node == null) {
            return;
        }
        node.hotkeys.forEach((name, values) -> {
            settingsVbox.getChildren().add(buildKeyShortcutRow(node, name, values));
        });
        node.settings.forEach((name, value) -> {
            settingsVbox.getChildren().add(buildSettingRow(node, name, value));
        });
    }

    private Node buildSettingRow(ConfigNode node, String settingName, Serializable value) {
        ConfigurableField fieldInfo = Configuration.getConfigurableFieldInfo(node.subject, settingName);
        if (fieldInfo == null) {
            return null;
        }
        HBox row = new HBox();
        row.getStyleClass().add("setting-row");
        Label label = new Label(fieldInfo.name());
        label.getStyleClass().add("setting-label");
        label.setMinWidth(150.0);
        Node widget = buildEditField(node, settingName, value);
        label.setLabelFor(widget);
        row.getChildren().add(label);
        row.getChildren().add(widget);
        return row;
    }

    private Node buildKeyShortcutRow(ConfigNode node, String actionName, String[] values) {
        InvokableAction actionInfo = Configuration.getInvokableActionInfo(node.subject, actionName);
        if (actionInfo == null) {
            return null;
        }
        HBox row = new HBox();
        row.getStyleClass().add("setting-row");
        Label label = new Label(actionInfo.name());
        label.getStyleClass().add("setting-keyboard-shortcut");
        label.setMinWidth(150.0);
        String value = Arrays.stream(values).collect(Collectors.joining(" or "));
        Text widget = new Text(value);
        widget.setWrappingWidth(180.0);
        widget.getStyleClass().add("setting-keyboard-value");
        widget.setOnMouseClicked((event) -> {
            editKeyboardShortcut(node, actionName, widget);
        });
        label.setLabelFor(widget);
        row.getChildren().add(label);
        row.getChildren().add(widget);
        return row;
    }

    private void editKeyboardShortcut(ConfigNode node, String actionName, Text widget) {
        throw new UnsupportedOperationException("Not supported yet.");
    }    
    
    private Node buildEditField(ConfigNode node, String settingName, Serializable value) {
        Field field;
        try {
            field = node.subject.getClass().getField(settingName);
        } catch (NoSuchFieldException | SecurityException ex) {
            return null;
        }
        Class type = field.getType();
        if (type == java.lang.String.class) {
            return buildTextField(node, settingName, value, null);
        } else if (type.isPrimitive()) {
            if (type == Integer.TYPE || type == Short.TYPE || type == Byte.TYPE) {
                return buildTextField(node, settingName, value, "-?[0-9]+");
            } else if (type == Float.TYPE || type == Double.TYPE) {
                return buildTextField(node, settingName, value, "-?[0-9]*(\\.[0-9]+)?");
            } else if (type == Boolean.TYPE) {
                return buildBooleanField(node, settingName, value);
            } else {
                return buildTextField(node, settingName, value, null);
            }
        } else if (type.equals(File.class)) {
            // TODO: Add file support!
        } else if (Class.class.isEnum()) {
            // TODO: Add enumeration support!
        } else if (ISelection.class.isAssignableFrom(type)) {
            return buildDynamicSelectComponent(node, settingName, value);
        }
        return null;
    }

    private Node buildTextField(ConfigNode node, String settingName, Serializable value, String validationPattern) {
        TextField widget = new TextField(String.valueOf(value));
        widget.textProperty().addListener((e) -> {
            node.setFieldValue(settingName, widget.getText());
        });
        return widget;
    }

    private Node buildBooleanField(ConfigNode node, String settingName, Serializable value) {
        CheckBox widget = new CheckBox();
        widget.setSelected(value.equals(Boolean.TRUE));
        widget.selectedProperty().addListener((e) -> {
            node.setFieldValue(settingName, widget.isSelected());
        });
        return widget;
    }

    private Node buildDynamicSelectComponent(ConfigNode node, String settingName, Serializable value) {
        try {
            DynamicSelection sel = (DynamicSelection) node.subject.getClass().getField(settingName).get(node.subject);
            ChoiceBox widget = new ChoiceBox(FXCollections.observableList(new ArrayList(sel.getSelections().keySet())));
            widget.setMinWidth(175.0);
            widget.setConverter(new StringConverter() {
                @Override
                public String toString(Object object) {
                    return (String) sel.getSelections().get(object);
                }
                
                @Override
                public Object fromString(String string) {
                    return sel.findValueByMatch(string);
                }
            });
            Object selected = value == null ? null : widget.getConverter().fromString(String.valueOf(value));
            if (selected == null) {
                widget.getSelectionModel().selectFirst();
            } else {
                widget.setValue(selected);
            }
            widget.valueProperty().addListener((Observable e) -> {
                node.setFieldValue(settingName, widget.getConverter().toString(widget.getValue()));
            });
            return widget;
        } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
            Logger.getLogger(ConfigurationUIController.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }
    }
}
