package p.studio.controls.banks;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.input.MouseButton;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;

import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.IntConsumer;

public abstract class StudioDualListView<T> extends HBox {
    private final ObservableList<T> leftItems = FXCollections.observableArrayList();
    private final ObservableList<T> rightItems = FXCollections.observableArrayList();
    private final ListView<T> leftListView = new ListView<>(leftItems);
    private final ListView<T> rightListView = new ListView<>(rightItems);
    private final Label leftTitleLabel = new Label("Available");
    private final Label rightTitleLabel = new Label("Selected");
    private final Button moveToRightButton = new Button(">");
    private final Button moveToLeftButton = new Button("<");
    private final Button moveUpButton = new Button("Up");
    private final Button moveDownButton = new Button("Down");

    private Consumer<List<T>> onMoveToRight = ignored -> { };
    private Consumer<List<T>> onMoveToLeft = ignored -> { };
    private Consumer<T> onLeftSelectionChanged = ignored -> { };
    private Consumer<T> onRightSelectionChanged = ignored -> { };
    private Consumer<T> onLeftItemActivated = ignored -> { };
    private Consumer<T> onRightItemActivated = ignored -> { };
    private IntConsumer onMoveUp = ignored -> { };
    private IntConsumer onMoveDown = ignored -> { };
    private boolean interactionEnabled = true;
    private boolean moveToRightAllowed = true;

    protected StudioDualListView() {
        setSpacing(12);
        setFillHeight(true);
        setMinHeight(Region.USE_PREF_SIZE);
        setPrefHeight(Region.USE_COMPUTED_SIZE);
        setMaxHeight(Double.MAX_VALUE);
        getStyleClass().add("studio-dual-list-view");

        leftListView.getStyleClass().add("studio-dual-list-left");
        rightListView.getStyleClass().add("studio-dual-list-right");
        leftListView.setMaxHeight(Double.MAX_VALUE);
        rightListView.setMaxHeight(Double.MAX_VALUE);
        leftListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        rightListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        leftListView.setCellFactory(ignored -> createCell(false));
        rightListView.setCellFactory(ignored -> createCell(true));

        final VBox leftColumn = createColumn(leftTitleLabel, leftListView);
        final VBox rightColumn = createColumn(rightTitleLabel, rightListView);
        final VBox centerActions = new VBox(8, moveToRightButton, moveToLeftButton, moveUpButton, moveDownButton);
        centerActions.setAlignment(Pos.CENTER);
        centerActions.setFillWidth(true);
        centerActions.setMinHeight(Region.USE_PREF_SIZE);
        centerActions.setMaxHeight(Double.MAX_VALUE);
        centerActions.getStyleClass().add("studio-dual-list-actions");

        HBox.setHgrow(leftColumn, Priority.ALWAYS);
        HBox.setHgrow(rightColumn, Priority.ALWAYS);
        getChildren().setAll(leftColumn, centerActions, rightColumn);

        moveToRightButton.getStyleClass().addAll("studio-button", "studio-button-secondary");
        moveToLeftButton.getStyleClass().addAll("studio-button", "studio-button-secondary");
        moveUpButton.getStyleClass().addAll("studio-button", "studio-button-secondary");
        moveDownButton.getStyleClass().addAll("studio-button", "studio-button-secondary");
        moveToRightButton.setFocusTraversable(false);
        moveToLeftButton.setFocusTraversable(false);
        moveUpButton.setFocusTraversable(false);
        moveDownButton.setFocusTraversable(false);

        moveToRightButton.setOnAction(ignored -> onMoveToRight.accept(List.copyOf(leftListView.getSelectionModel().getSelectedItems())));
        moveToLeftButton.setOnAction(ignored -> onMoveToLeft.accept(List.copyOf(rightListView.getSelectionModel().getSelectedItems())));
        moveUpButton.setOnAction(ignored -> {
            final int selectedIndex = rightListView.getSelectionModel().getSelectedIndex();
            if (selectedIndex >= 0) {
                onMoveUp.accept(selectedIndex);
            }
        });
        moveDownButton.setOnAction(ignored -> {
            final int selectedIndex = rightListView.getSelectionModel().getSelectedIndex();
            if (selectedIndex >= 0) {
                onMoveDown.accept(selectedIndex);
            }
        });

        updateActionState();
        leftListView.getSelectionModel().getSelectedItems().addListener((javafx.collections.ListChangeListener<? super T>) ignored -> updateActionState());
        rightListView.getSelectionModel().getSelectedItems().addListener((javafx.collections.ListChangeListener<? super T>) ignored -> updateActionState());
        leftListView.getSelectionModel().selectedItemProperty().addListener((ignored, oldValue, newValue) -> onLeftSelectionChanged.accept(newValue));
        rightListView.getSelectionModel().selectedItemProperty().addListener((ignored, oldValue, newValue) -> onRightSelectionChanged.accept(newValue));
    }

    public void setLeftItems(List<T> items) {
        final List<T> previouslySelected = List.copyOf(leftListView.getSelectionModel().getSelectedItems());
        leftItems.setAll(Objects.requireNonNull(items, "items"));
        restoreSelection(leftListView, leftItems, previouslySelected);
        leftListView.refresh();
        updateActionState();
    }

    public void setRightItems(List<T> items) {
        final List<T> previouslySelected = List.copyOf(rightListView.getSelectionModel().getSelectedItems());
        rightItems.setAll(Objects.requireNonNull(items, "items"));
        restoreSelection(rightListView, rightItems, previouslySelected);
        rightListView.refresh();
        updateActionState();
    }

    public void setInteractionEnabled(boolean enabled) {
        interactionEnabled = enabled;
        leftListView.setDisable(!enabled);
        rightListView.setDisable(!enabled);
        updateActionState();
    }

    public void setMoveToRightAllowed(boolean allowed) {
        moveToRightAllowed = allowed;
        updateActionState();
    }

    public void setOnMoveToRight(Consumer<List<T>> onMoveToRight) {
        this.onMoveToRight = Objects.requireNonNull(onMoveToRight, "onMoveToRight");
    }

    public void setOnMoveToLeft(Consumer<List<T>> onMoveToLeft) {
        this.onMoveToLeft = Objects.requireNonNull(onMoveToLeft, "onMoveToLeft");
    }

    public void setOnLeftSelectionChanged(Consumer<T> onLeftSelectionChanged) {
        this.onLeftSelectionChanged = Objects.requireNonNull(onLeftSelectionChanged, "onLeftSelectionChanged");
    }

    public void setOnRightSelectionChanged(Consumer<T> onRightSelectionChanged) {
        this.onRightSelectionChanged = Objects.requireNonNull(onRightSelectionChanged, "onRightSelectionChanged");
    }

    public void setOnLeftItemActivated(Consumer<T> onLeftItemActivated) {
        this.onLeftItemActivated = Objects.requireNonNull(onLeftItemActivated, "onLeftItemActivated");
    }

    public void setOnRightItemActivated(Consumer<T> onRightItemActivated) {
        this.onRightItemActivated = Objects.requireNonNull(onRightItemActivated, "onRightItemActivated");
    }

    public void setOnMoveUp(IntConsumer onMoveUp) {
        this.onMoveUp = Objects.requireNonNull(onMoveUp, "onMoveUp");
    }

    public void setOnMoveDown(IntConsumer onMoveDown) {
        this.onMoveDown = Objects.requireNonNull(onMoveDown, "onMoveDown");
    }

    public void setLeftTitle(String title) {
        leftTitleLabel.setText(Objects.requireNonNull(title, "title"));
    }

    public void setRightTitle(String title) {
        rightTitleLabel.setText(Objects.requireNonNull(title, "title"));
    }

    public void selectLeftItem(T item) {
        selectItem(leftListView, leftItems, item);
    }

    public void selectRightItem(T item) {
        selectItem(rightListView, rightItems, item);
    }

    protected abstract String itemText(T item);

    protected Node itemGraphic(T item) {
        return null;
    }

    private VBox createColumn(Label titleLabel, ListView<T> listView) {
        titleLabel.getStyleClass().add("studio-dual-list-title");
        final VBox column = new VBox(8, titleLabel, listView);
        column.getStyleClass().add("studio-dual-list-column");
        column.setFillWidth(true);
        column.setMinHeight(Region.USE_PREF_SIZE);
        column.setMaxHeight(Double.MAX_VALUE);
        VBox.setVgrow(listView, Priority.ALWAYS);
        return column;
    }

    private ListCell<T> createCell(boolean indexed) {
        return new ListCell<>() {
            {
                setOnMouseClicked(event -> {
                    if (event.getButton() != MouseButton.PRIMARY || event.getClickCount() != 2 || getItem() == null) {
                        return;
                    }
                    if (indexed) {
                        onRightItemActivated.accept(getItem());
                    } else {
                        onLeftItemActivated.accept(getItem());
                    }
                });
            }

            @Override
            protected void updateItem(T item, boolean empty) {
                super.updateItem(item, empty);
                if (empty || item == null) {
                    setText(null);
                    setGraphic(null);
                    return;
                }
                final Node graphic = itemGraphic(item);
                if (graphic != null) {
                    setText(null);
                    setGraphic(indexed ? createIndexedGraphic(graphic, getIndex()) : graphic);
                    return;
                }
                final String text = indexed
                        ? getIndex() + ". " + itemText(item)
                        : itemText(item);
                setText(text);
                setGraphic(null);
            }
        };
    }

    private void updateActionState() {
        final boolean leftSelected = !leftListView.getSelectionModel().isEmpty();
        final int rightSelectedIndex = rightListView.getSelectionModel().getSelectedIndex();
        final boolean singleRightSelection = rightListView.getSelectionModel().getSelectedIndices().size() == 1;
        moveToRightButton.setDisable(!interactionEnabled || !moveToRightAllowed || !leftSelected || leftListView.isDisabled());
        moveToLeftButton.setDisable(!interactionEnabled || rightSelectedIndex < 0 || rightListView.isDisabled());
        moveUpButton.setDisable(!interactionEnabled || rightSelectedIndex <= 0 || rightListView.isDisabled() || !singleRightSelection);
        moveDownButton.setDisable(
                !interactionEnabled
                        ||
                rightSelectedIndex < 0
                        || rightSelectedIndex >= rightItems.size() - 1
                        || rightListView.isDisabled()
                        || !singleRightSelection);
    }

    private void restoreSelection(ListView<T> listView, List<T> items, List<T> previouslySelected) {
        final var selectionModel = listView.getSelectionModel();
        selectionModel.clearSelection();
        for (T selectedItem : previouslySelected) {
            final int index = items.indexOf(selectedItem);
            if (index >= 0) {
                selectionModel.select(index);
            }
        }
        final int focusedIndex = selectionModel.getSelectedIndex();
        if (focusedIndex >= 0) {
            listView.scrollTo(focusedIndex);
        }
    }

    private void selectItem(ListView<T> listView, List<T> items, T item) {
        final var selectionModel = listView.getSelectionModel();
        if (item == null) {
            selectionModel.clearSelection();
            return;
        }
        final int index = items.indexOf(item);
        if (index < 0) {
            selectionModel.clearSelection();
            return;
        }
        selectionModel.clearSelection();
        selectionModel.select(index);
        listView.scrollTo(index);
    }

    private Node createIndexedGraphic(Node graphic, int index) {
        final Label chip = new Label(index + ".");
        chip.getStyleClass().add("studio-dual-list-index-chip");
        final HBox wrapper = new HBox(8, chip, graphic);
        wrapper.getStyleClass().add("studio-dual-list-indexed-cell");
        wrapper.setAlignment(Pos.CENTER_LEFT);
        HBox.setHgrow(graphic, Priority.ALWAYS);
        return wrapper;
    }
}
