package ui.components.pickers;

import backend.resource.TurboIssue;
import backend.resource.TurboLabel;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;

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

import org.apache.logging.log4j.Logger;

import ui.IdGenerator;
import ui.UI;
import util.HTLog;

/**
 * Serves as a presenter that synchronizes changes in labels with dialog view
 */
public class LabelPickerDialog extends Dialog<List<String>> {

    private static final int ELEMENT_MAX_WIDTH = 400;
    private static final Insets GROUPLESS_PAD = new Insets(5, 0, 0, 0);
    private static final Insets GROUP_PAD = new Insets(0, 0, 10, 10);
    private static final Logger logger = HTLog.get(LabelPickerDialog.class);

    private final List<TurboLabel> allLabels;
    private final TurboIssue issue;
    private LabelPickerState state;

    @FXML
    private VBox mainLayout;
    @FXML
    private Label title;
    @FXML
    private FlowPane assignedLabels;
    @FXML
    private TextField queryField;
    @FXML
    private VBox feedbackLabels;

    LabelPickerDialog(TurboIssue issue, List<TurboLabel> allLabels, Stage stage) {
        this.allLabels = allLabels;
        this.issue = issue;

        initUI(stage, issue);
        Platform.runLater(queryField::requestFocus);
    }

    // Initialisation of UI

    @FXML
    public void initialize() {
        queryField.textProperty().addListener((observable, oldText, newText) -> handleUserInput(queryField.getText()));
        queryField.setId(IdGenerator.getLabelPickerTextFieldId());
    }

    private void initUI(Stage stage, TurboIssue issue) {
        initialiseDialog(stage, issue);
        setDialogPaneContent(issue);
        title.setTooltip(createTitleTooltip(issue));
        createButtons();

        state = new LabelPickerState(TurboLabel.getMatchedLabels(allLabels, issue.getLabels()), allLabels, "");
        populatePanes(state);
    }

    private void initialiseDialog(Stage stage, TurboIssue issue) {
        initOwner(stage);
        initModality(Modality.APPLICATION_MODAL);
        setTitle("Edit Labels for " + (issue.isPullRequest() ? "PR #" : "Issue #")
                + issue.getId() + " in " + issue.getRepoId());
        // Ensures height and width of dialog has been initialized before positioning
        Platform.runLater(() -> positionDialog(stage));
    }

    private void setDialogPaneContent(TurboIssue issue) {
        createMainLayout();
        setTitleLabel(issue);
        getDialogPane().setContent(mainLayout);
    }

    // Population of UI elements

    /**
     * Populates respective panes with labels that matches current user input
     *
     * @param state
     */
    private final void populatePanes(LabelPickerState state) {
        // Population of UI elements
        populateAssignedLabels(state.getInitialLabels(), state.getRemovedLabels(), state.getAddedLabels(),
                               state.getCurrentSuggestion());
        populateFeedbackLabels(state.getAssignedLabels(), state.getMatchedLabels(), state.getCurrentSuggestion());
        // Ensures dialog pane resize according to content
        getDialogPane().getScene().getWindow().sizeToScene();
    }

    private final void populateAssignedLabels(List<TurboLabel> initialLabels, List<TurboLabel> removedLabels,
                                              List<TurboLabel> addedLabels, Optional<TurboLabel> suggestion) {
        assignedLabels.getChildren().clear();
        populateInitialLabels(initialLabels, removedLabels, suggestion);
        populateToBeAddedLabels(addedLabels, suggestion);
    }

    private final void populateInitialLabels(List<TurboLabel> initialLabels, List<TurboLabel> removedLabels,
                                             Optional<TurboLabel> suggestion) {
        initialLabels.stream()
                .forEach(label -> assignedLabels.getChildren()
                        .add(processInitialLabel(label, removedLabels, suggestion)));
    }

    private final Node processInitialLabel(TurboLabel initialLabel, List<TurboLabel> removedLabels,
                                           Optional<TurboLabel> suggestion) {
        TurboLabel repoInitialLabel = TurboLabel.getFirstMatchingTurboLabel(allLabels, initialLabel.getFullName());
        if (!removedLabels.contains(initialLabel)) {
            if (suggestion.isPresent() && initialLabel.equals(suggestion.get())) {
                return getPickerLabelNode(
                        new PickerLabel(repoInitialLabel, true).faded(true).removed(true));
            }
            return getPickerLabelNode(new PickerLabel(repoInitialLabel, true));
        }

        if (suggestion.isPresent() && initialLabel.equals(suggestion.get())) {
            return getPickerLabelNode(new PickerLabel(repoInitialLabel, true).faded(true));
        }

        return getPickerLabelNode(new PickerLabel(repoInitialLabel, true).removed(true));
    }

    /**
     * @param label
     * @return Node from label after registering mouse handler
     */
    private final Node getPickerLabelNode(PickerLabel label) {
        Node node = label.getNode();
        node.setOnMouseClicked(e -> handleLabelClick(label.getFullName()));
        return node;
    }

    private final void populateToBeAddedLabels(List<TurboLabel> addedLabels, Optional<TurboLabel> suggestion) {
        if (!addedLabels.isEmpty() || hasNewSuggestion(addedLabels, suggestion)) {
            assignedLabels.getChildren().add(new Label("|"));
        }
        populateAddedLabels(addedLabels, suggestion);
        populateSuggestedLabel(addedLabels, suggestion);
    }

    private final void populateAddedLabels(List<TurboLabel> addedLabels, Optional<TurboLabel> suggestion) {
        addedLabels.stream()
                .forEach(label -> {
                    assignedLabels.getChildren().add(processAddedLabel(label, suggestion));
                });
    }

    private final Node processAddedLabel(TurboLabel addedLabel, Optional<TurboLabel> suggestion) {
        if (!suggestion.isPresent() || !addedLabel.equals(suggestion.get())) {
            return getPickerLabelNode(
                    new PickerLabel(TurboLabel.getFirstMatchingTurboLabel(allLabels, addedLabel.getFullName()), true));
        }
        return getPickerLabelNode(
                new PickerLabel(TurboLabel.getFirstMatchingTurboLabel(allLabels, addedLabel.getFullName()), true)
                        .faded(true).removed(true));
    }

    private final void populateSuggestedLabel(List<TurboLabel> addedLabels, Optional<TurboLabel> suggestion) {
        if (hasNewSuggestion(addedLabels, suggestion)) {
            assignedLabels.getChildren().add(processSuggestedLabel(suggestion.get()));
        }
    }

    private final boolean hasNewSuggestion(List<TurboLabel> addedLabels, Optional<TurboLabel> suggestion) {
        return suggestion.isPresent()
                && !(TurboLabel.getMatchedLabels(allLabels, issue.getLabels())).contains(suggestion.get())
                && !addedLabels.contains(suggestion.get());
    }

    private final Node processSuggestedLabel(TurboLabel suggestedLabel) {
        return getPickerLabelNode(
                new PickerLabel(TurboLabel.getFirstMatchingTurboLabel(allLabels, suggestedLabel.getFullName()), true)
                        .faded(true));
    }

    private final void populateFeedbackLabels(List<TurboLabel> assignedLabels, List<TurboLabel> matchedLabels,
                                              Optional<TurboLabel> suggestion) {
        feedbackLabels.getChildren().clear();
        populateGroupLabels(assignedLabels, matchedLabels, suggestion);
        populateGrouplessLabels(assignedLabels, matchedLabels, suggestion);
    }

    private final void populateGroupLabels(List<TurboLabel> finalLabels, List<TurboLabel> matchedLabels,
                                           Optional<TurboLabel> suggestion) {

        Map<String, FlowPane> groupContent = getGroupContent(finalLabels, matchedLabels, suggestion);
        groupContent.entrySet().forEach(entry -> {
            feedbackLabels.getChildren().addAll(
                    createGroupTitle(entry.getKey()), entry.getValue());
        });
    }

    private final Map<String, FlowPane> getGroupContent(List<TurboLabel> finalLabels, List<TurboLabel> matchedLabels,
                                                        Optional<TurboLabel> suggestion) {
        Map<String, FlowPane> groupContent = new HashMap<>();
        allLabels.stream().sorted()
                .filter(label -> label.isInGroup())
                .forEach(label -> {
                    String group = label.getGroupName();
                    if (!groupContent.containsKey(group)) {
                        groupContent.put(group, createGroupPane(GROUP_PAD));
                    }
                    groupContent.get(group).getChildren().add(processMatchedLabel(
                            label, matchedLabels, finalLabels, suggestion));
                });
        return groupContent;
    }

    private final void populateGrouplessLabels(List<TurboLabel> finalLabels, List<TurboLabel> matchedLabels,
                                               Optional<TurboLabel> suggestion) {
        FlowPane groupless = createGroupPane(GROUPLESS_PAD);
        allLabels.stream()
                .filter(label -> !label.isInGroup())
                .forEach(label -> groupless.getChildren().add(processMatchedLabel(
                        label, matchedLabels, finalLabels, suggestion)));

        feedbackLabels.getChildren().add(groupless);
    }

    private final Node processMatchedLabel(TurboLabel repoLabel, List<TurboLabel> matchedLabels,
                                           List<TurboLabel> assignedLabels, Optional<TurboLabel> suggestion) {

        return getPickerLabelNode(
                new PickerLabel(TurboLabel.getFirstMatchingTurboLabel(allLabels, repoLabel.getFullName()), false)
                        .faded(!matchedLabels.contains(repoLabel))
                        .highlighted(suggestion.isPresent() && suggestion.get().equals(repoLabel))
                        .selected(assignedLabels.contains(repoLabel)));
    }

    /**
     * Positions dialog based on width and height of stage to avoid dialog appearing off-screen on certain computers
     * if default position is used
     *
     * @param stage
     */
    private final void positionDialog(Stage stage) {
        setX(stage.getX() + stage.getWidth() / 2);
        setY(stage.getY() + stage.getHeight() / 2 - getHeight() / 2);
    }

    private void createMainLayout() {
        FXMLLoader loader = new FXMLLoader(UI.class.getResource("fxml/LabelPickerView.fxml"));
        loader.setController(this);
        try {
            mainLayout = (VBox) loader.load();
        } catch (IOException e) {
            logger.error("Failure to load FXML. " + e.getMessage());
            close();
        }
    }

    private void createButtons() {
        ButtonType confirmButtonType = new ButtonType("Confirm", ButtonBar.ButtonData.OK_DONE);
        getDialogPane().getButtonTypes().addAll(confirmButtonType, ButtonType.CANCEL);

        // defines what happens when user confirms/presses enter
        setResultConverter(dialogButton -> {
            if (dialogButton == confirmButtonType) {
                // Ensures the last keyword in the query is toggled after confirmation
                if (!queryField.isDisabled()) queryField.appendText(" ");
                return TurboLabel.getLabelNames(state.getAssignedLabels());
            }
            return null;
        });
    }

    private Tooltip createTitleTooltip(TurboIssue issue) {
        Tooltip titleTooltip = new Tooltip(
                (issue.isPullRequest() ? "PR #" : "Issue #") + issue.getId() + ": " + issue.getTitle());
        titleTooltip.setWrapText(true);
        titleTooltip.setMaxWidth(500);
        return titleTooltip;
    }

    private void setTitleLabel(TurboIssue issue) {
        title.setText((issue.isPullRequest() ? "PR #" : "Issue #")
                + issue.getId() + ": " + issue.getTitle());
    }

    private Label createGroupTitle(String name) {
        Label groupName = new Label(name);
        groupName.setPadding(new Insets(0, 5, 5, 0));
        groupName.setMaxWidth(ELEMENT_MAX_WIDTH - 10);
        groupName.setStyle("-fx-font-size: 110%; -fx-font-weight: bold; ");
        return groupName;
    }

    private FlowPane createGroupPane(Insets padding) {
        FlowPane group = new FlowPane();
        group.setHgap(5);
        group.setVgap(5);
        group.setPadding(padding);
        return group;
    }

    // Event handling 

    /**
     * Updates state of the label picker based on the entire query
     */
    private final void handleUserInput(String query) {
        state = new LabelPickerState(
                TurboLabel.getMatchedLabels(allLabels, issue.getLabels()), allLabels, query.toLowerCase());
        populatePanes(state);
    }

    private void handleLabelClick(String labelName) {
        queryField.setDisable(true);
        TurboLabel.getMatchedLabels(allLabels, labelName)
                .stream().findFirst().ifPresent(state::updateAssignedLabels);
        populatePanes(state);
    }
}