package ctbrec.ui.settings.api;

import static java.util.Optional.*;

import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ctbrec.ui.controls.SearchBox;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;

public class Preferences {

    private static final Logger LOG = LoggerFactory.getLogger(Preferences.class);

    private Category[] categories;

    private TreeView<Category> categoryTree;

    private PreferencesStorage preferencesStorage;

    private Runnable restartRequiredCallback = () -> {};

    private Preferences(PreferencesStorage preferencesStorage, Category...categories) {
        this.preferencesStorage = preferencesStorage;
        this.categories = categories;
        for (Category category : categories) {
            assignPreferencesStorage(category, preferencesStorage);
        }
    }

    private void assignPreferencesStorage(Category cat, PreferencesStorage preferencesStorage) {
        if(cat.hasSubCategories()) {
            for (Category sub : cat.getSubCategories()) {
                assignPreferencesStorage(sub, preferencesStorage);
            }
        } else {
            for (Group group : cat.getGroups()) {
                for (Setting setting : group.getSettings()) {
                    setting.setPreferencesStorage(preferencesStorage);
                }
            }
        }
    }

    public static Preferences of(PreferencesStorage preferencesStorage, Category... categories) {
        return new Preferences(preferencesStorage, categories);
    }

    public void save() throws IOException {
        preferencesStorage.save(this);
    }

    Category[] getCategories() {
        return categories;
    }

    public Region getView(boolean withNavigation) {
        SearchBox search = new SearchBox(true);
        search.textProperty().addListener(this::filterTree);
        TreeItem<Category> categoryTreeItems = createCategoryTree(categories, new TreeItem<>(), null);
        categoryTree = new TreeView<>(categoryTreeItems);
        categoryTree.showRootProperty().set(false);
        VBox leftSide = new VBox(search, categoryTree);
        VBox.setVgrow(categoryTree, Priority.ALWAYS);
        VBox.setMargin(search, new Insets(2));
        VBox.setMargin(categoryTree, new Insets(2));

        BorderPane main = new BorderPane();
        if (withNavigation) {
            main.setLeft(leftSide);
        }
        main.setCenter(new Label("Center"));
        BorderPane.setMargin(leftSide, new Insets(2));

        categoryTree.getSelectionModel().selectedItemProperty().addListener((obs, oldV, newV) -> {
            if (newV != null) {
                Category cat = newV.getValue();
                Node gui = cat.getGuiOrElse(() -> createGui(cat));
                BorderPane.setMargin(gui, new Insets(10));
                main.setCenter(gui);
            }
        });
        categoryTree.getSelectionModel().select(0);
        return main;
    }

    public Region getView() {
        return getView(true);
    }

    private void filterTree(ObservableValue<? extends String> obs, String oldV, String newV) {
        String q = ofNullable(newV).orElse("").toLowerCase().trim();
        TreeItem<Category> filteredCategoryTree = createCategoryTree(categories, new TreeItem<>(), q);
        categoryTree.setRoot(filteredCategoryTree);
        expandAll(categoryTree.getRoot());

        TreeItem<Category> parent = categoryTree.getRoot();
        while (!parent.getChildren().isEmpty()) {
            parent = parent.getChildren().get(0);
            categoryTree.getSelectionModel().select(parent);
        }

        for (Category category : categories) {
            if (q.length() > 2) {
                HighlightingSupport.highlightMatches(category, q);
            } else {
                HighlightingSupport.removeHighlights(category);
            }
        }
    }

    private void expandAll(TreeItem<Category> treeItem) {
        treeItem.setExpanded(true);
        for (TreeItem<Category> child : treeItem.getChildren()) {
            expandAll(child);
        }
    }

    private Node createGui(Category cat) {
        try {
            if (cat.hasSubCategories()) {
                return new Label(cat.getName());
            } else if(cat.hasGroups()) {
                return createPaneWithGroups(cat);
            } else {
                return createGrid(cat.getGroups()[0].getSettings());
            }
        } catch(Exception e) {
            LOG.error("Error creating the GUI", e);
            return new Label(e.getLocalizedMessage());
        }
    }

    private Node createPaneWithGroups(Category cat) throws Exception {
        VBox pane = new VBox();
        for (Group grp : cat.getGroups()) {
            Label groupLabel = new Label(grp.getName());
            groupLabel.getStyleClass().add("settings-group-label");
            VBox.setMargin(groupLabel, new Insets(20, 0, 10, 20));
            pane.getChildren().add(groupLabel);
            Node parameterGrid = createGrid(grp.getSettings());
            pane.getChildren().add(parameterGrid);
            VBox.setMargin(parameterGrid, new Insets(0, 0, 0, 40));
        }
        return pane;
    }

    private Node createGrid(Setting[] settings) throws Exception {
        GridPane pane = new GridPane();
        pane.setHgap(2);
        pane.vgapProperty().bind(pane.hgapProperty());
        int row = 0;
        for (Setting setting : settings) {
            Node node = setting.getGui();
            Label label = new Label(setting.getName());
            label.setMinHeight(34);
            label.labelForProperty().set(node);
            label.setTooltip(new Tooltip(setting.getName()));
            pane.addRow(row++, label, node);
            GridPane.setVgrow(label, Priority.ALWAYS);
            GridPane.setValignment(label, VPos.CENTER);
            GridPane.setMargin(node, new Insets(5, 0, 5, 10));
            GridPane.setValignment(node, VPos.CENTER);
            GridPane.setHgrow(node, Priority.ALWAYS);
        }
        return pane;
    }

    /**
     * Creates a tree of the given categories. Filters out categories, which don't match the filter
     * @param filter may be null
     */
    private TreeItem<Category> createCategoryTree(Category[] categories, TreeItem<Category> parent, String filter) {
        for (Category category : categories) {
            TreeItem<Category> child = new TreeItem<>(category);
            if (category.hasSubCategories()) {
                createCategoryTree(category.getSubCategories(), child, filter);
                if (!child.getChildren().isEmpty()) {
                    parent.getChildren().add(child);
                }
            } else if(category.contains(filter)) {
                parent.getChildren().add(child);
            }
        }
        return parent;
    }

    public void expandTree() {
        expandAll(categoryTree.getRoot());
    }

    public void traverse(Consumer<Setting> visitor) {
        for (Category category : categories) {
            visit(category, visitor);
        }
    }

    private void visit(Category cat, Consumer<Setting> visitor) {
        for (Group group : cat.getGroups()) {
            for (Setting setting : group.getSettings()) {
                visitor.accept(setting);
            }
        }
        if (cat.hasSubCategories()) {
            for (Category subcat : cat.getSubCategories()) {
                visit(subcat, visitor);
            }
        }
    }

    public Optional<Setting> getSetting(String key) {
        SettingSearchVisitor search = new SettingSearchVisitor(key);
        traverse(search);
        return search.getResult();
    }

    private class SettingSearchVisitor implements Consumer<Setting> {
        Optional<Setting> result = Optional.empty();
        private String key;

        public SettingSearchVisitor(String key) {
            this.key = key;
        }

        @Override
        public void accept(Setting s) {
            if (Objects.equals(key, ofNullable(s.getKey()).orElse(""))) {
                result = Optional.of(s);
            }
        }

        public Optional<Setting> getResult() {
            return result;
        }
    }

    public void onRestartRequired(Runnable callback) {
        this.restartRequiredCallback = callback;
    }

    public Runnable getRestartRequiredCallback() {
        return restartRequiredCallback;
    }
}
