package io.xpipe.app.hub.comp;

import io.xpipe.app.comp.RegionBuilder;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.ext.DataStore;
import io.xpipe.app.ext.DataStoreCreationCategory;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.platform.BindingsHelper;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.platform.MenuHelper;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;

import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ListChangeListener;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;

import atlantafx.base.controls.Popover;
import lombok.RequiredArgsConstructor;
import org.kordamp.ikonli.javafx.FontIcon;

import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;

@RequiredArgsConstructor
public class StoreChoicePopover<T extends DataStore> {

    private final DataStoreEntry self;
    private final Property<DataStoreEntryRef<T>> selected;
    private final Class<?> storeClass;
    private final Predicate<DataStoreEntryRef<T>> applicableCheck;
    private final StoreCategoryWrapper rootCategory;
    private final StoreCategoryWrapper explicitCategory;
    private final boolean requireComplete;
    private final String titleKey;
    private final String noMatchKey;
    private final DataStoreCreationCategory creationCategory;
    private Consumer<Popover> consumer;
    private Popover popover;

    public void withPopover(Consumer<Popover> consumer) {
        this.consumer = consumer;
    }

    public void show(Node node) {
        var p = getPopover();
        if (!p.isShowing()) {
            p.show(node);
        } else {
            p.hide();
        }
    }

    public void hide() {
        if (popover != null) {
            popover.hide();
        }
    }

    private Popover getPopover() {
        // Rebuild popover if we have a non-null condition to allow for the content to be updated in case the condition
        // changed
        if (popover == null || applicableCheck != null) {
            var cur = StoreViewState.get().getActiveCategory().getValue();
            var selectedCategory = new SimpleObjectProperty<>(
                    explicitCategory != null
                            ? explicitCategory
                            : (rootCategory != null
                                    ? (rootCategory.getRoot().equals(cur.getRoot()) ? cur : rootCategory)
                                    : cur));
            var storeFilter = new SimpleObjectProperty<StoreFilter>();
            popover = new Popover();
            Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
                var e = storeEntryWrapper.getEntry();

                if (self != null
                        && (e.equals(self)
                                || DataStorage.get().getStoreParentHierarchy(e).contains(self))) {
                    return false;
                }

                // Check if load failed
                if (e.getStore() == null) {
                    return false;
                }

                return storeClass.isAssignableFrom(e.getStore().getClass())
                        && (!requireComplete || e.getValidity().isUsable())
                        && (applicableCheck == null || applicableCheck.test(e.ref()));
            };

            var applicableMatch =
                    StoreViewState.get().getCurrentTopLevelSection().anyMatches(applicable);
            if (!applicableMatch) {
                selectedCategory.set(rootCategory);
            }

            var applicableCount = StoreViewState.get().getAllEntries().getList().stream()
                    .filter(applicable)
                    .count();
            var initialExpanded = applicableCount < 20;

            var enabled = popover.showingProperty();
            var section = new StoreSectionMiniComp(
                    StoreSection.createTopLevel(
                            StoreViewState.get().getAllEntries(),
                            Set.of(),
                            applicable,
                            storeFilter,
                            selectedCategory,
                            StoreViewState.get().getEntriesListVisibilityObservable(),
                            StoreViewState.get().getEntriesListUpdateObservable(),
                            enabled),
                    (s, comp) -> {
                        if (!applicable.test(s.getWrapper())) {
                            comp.disable(new SimpleBooleanProperty(true));
                        }
                    },
                    sec -> {
                        if (applicable.test(sec.getWrapper())) {
                            this.selected.setValue(sec.getWrapper().getEntry().ref());
                            popover.hide();
                        }
                    },
                    initialExpanded);

            var category = new StoreCategoryChoiceComp(
                    rootCategory != null ? rootCategory.getRoot() : null,
                    StoreViewState.get().getActiveCategory(),
                    selectedCategory,
                    explicitCategory == null,
                    ignored -> true);
            var filter = FilterComp.ofStoreFilter(storeFilter).hgrow();

            var addButton = creationCategory != null && creationCategory.getDefaultProvider() != null
                    ? new ButtonComp(null, new LabelGraphic.IconGraphic("mdi2p-plus"), () -> {
                                StoreCreationDialog.showCreation(DataStoreProviders.byId(creationCategory.getDefaultProvider()).orElseThrow(),
                                        creationCategory);
                            })
                            .describe(d -> d.nameKey("addConnection"))
                    : RegionBuilder.of(() -> {
                                var m = MenuHelper.createMenuButton();
                                m.setGraphic(new FontIcon("mdi2p-plus-box-outline"));
                                m.setMaxHeight(100);
                                m.setMinHeight(0);
                                StoreCreationMenu.addButtons(m.getItems(), false);
                                return m;
                            })
                            .describe(d -> d.nameKey("addConnection"))
                            .padding(new Insets(-5));

            var top = new InputGroupComp(List.of(category, filter, addButton))
                    .setMainReference(category)
                    .style("top")
                    .apply(struc -> struc.setFillHeight(true))
                    .apply(struc -> {
                        var first = ((Region) struc.getChildren().get(0));
                        var second = ((Region) struc.getChildren().get(1));
                        var third = ((Region) struc.getChildren().get(1));
                        second.prefHeightProperty().bind(first.heightProperty());
                        second.minHeightProperty().bind(first.heightProperty());
                        second.maxHeightProperty().bind(first.heightProperty());
                        third.prefHeightProperty().bind(first.heightProperty());
                    })
                    .apply(struc -> {
                        // Ugly solution to focus the text field
                        // Somehow this does not work through the normal on shown listeners
                        struc.getChildren().get(0).focusedProperty().addListener((observable, oldValue, newValue) -> {
                            if (newValue) {
                                struc.getChildren().get(1).requestFocus();
                            }
                        });
                    })
                    .build();

            var emptyText = Bindings.createStringBinding(
                    () -> {
                        var count = StoreViewState.get().getAllEntries().getList().stream()
                                .filter(applicable)
                                .count();
                        return count == 0 ? AppI18n.get(noMatchKey) : null;
                    },
                    StoreViewState.get().getAllEntries().getList());
            var emptyLabel =
                    new LabelComp(emptyText, new SimpleObjectProperty<>(new LabelGraphic.IconGraphic("mdi2f-filter")));
            emptyLabel.apply(struc -> AppFontSizes.sm(struc));
            emptyLabel.hide(BindingsHelper.map(emptyText, s -> s == null));
            emptyLabel.minHeight(80);

            var listStack = new StackComp(List.of(emptyLabel, section));
            listStack.vgrow();

            var r = listStack.build();
            var content = new VBox(top, r);
            content.setFillWidth(true);
            content.getStyleClass().add("choice-comp-content");
            content.setPrefWidth(480);
            content.setMaxHeight(430);

            popover.setContentNode(content);
            popover.setCloseButtonEnabled(true);
            popover.setArrowLocation(Popover.ArrowLocation.TOP_CENTER);
            popover.setHeaderAlwaysVisible(true);
            popover.setDetachable(true);
            popover.setTitle(AppI18n.get(titleKey));
            popover.setAutoHide(!AppPrefs.get().limitedTouchscreenMode().get());
            AppFontSizes.xs(popover.getContentNode());

            popover.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
                if (event.getCode() == KeyCode.ESCAPE) {
                    popover.hide();
                    event.consume();
                }
            });

            // Hide on connection creation dialog
            AppDialog.getModalOverlays().addListener((ListChangeListener<? super ModalOverlay>) c -> {
                popover.hide();
            });

            if (consumer != null) {
                consumer.accept(popover);
            }
        }

        return popover;
    }
}
