import $ from "jquery";
import _ from "lodash";
import tippy from "tippy.js";

import render_dropdown_list from "../templates/settings/dropdown_list.hbs";

import * as blueslip from "./blueslip";
import {$t} from "./i18n";
import * as keydown_util from "./keydown_util";
import * as ListWidget from "./list_widget";

export class DropdownListWidget {
    constructor({
        widget_name,
        data,
        default_text,
        render_text = (item_name) => item_name,
        null_value = null,
        include_current_item = true,
        value,
        on_update = () => {},
    }) {
        // Initializing values
        this.widget_name = widget_name;
        this.data = data;
        this.default_text = default_text;
        this.render_text = render_text;
        this.null_value = null_value;
        this.include_current_item = include_current_item;
        this.initial_value = value;
        this.on_update = on_update;

        this.container_id = `${widget_name}_widget`;
        this.value_id = `id_${widget_name}`;

        if (value === undefined) {
            this.initial_value = null_value;
            blueslip.warn("dropdown-list-widget: Called without a default value; using null value");
        }
    }

    render_default_text($elem) {
        $elem.text(this.default_text);
        $elem.addClass("text-warning");
        $elem.closest(".input-group").find(".dropdown_list_reset_button").hide();
    }

    render(value) {
        $(`#${CSS.escape(this.container_id)} #${CSS.escape(this.value_id)}`).data("value", value);

        const $elem = $(`#${CSS.escape(this.container_id)} #${CSS.escape(this.widget_name)}_name`);

        if (!value || value === this.null_value) {
            this.render_default_text($elem);
            return;
        }

        // Happy path
        const item = this.data.find((x) => x.value === value.toString());

        if (item === undefined) {
            this.render_default_text($elem);
            return;
        }

        const text = this.render_text(item.name);
        $elem.text(text);
        $elem.removeClass("text-warning");
        $elem.closest(".input-group").find(".dropdown_list_reset_button").show();
    }

    update(value) {
        this.render(value);
        this.on_update(value);
    }

    register_event_handlers() {
        $(`#${CSS.escape(this.container_id)} .dropdown-list-body`).on(
            "click keypress",
            ".list_item",
            (e) => {
                const $setting_elem = $(e.currentTarget).closest(
                    `.${CSS.escape(this.widget_name)}_setting`,
                );
                if (e.type === "keypress") {
                    if (keydown_util.is_enter_event(e)) {
                        $setting_elem.find(".dropdown-menu").dropdown("toggle");
                    } else {
                        return;
                    }
                }
                const value = $(e.currentTarget).attr("data-value");
                this.update(value);
            },
        );
        $(`#${CSS.escape(this.container_id)} .dropdown_list_reset_button`).on("click", (e) => {
            this.update(this.null_value);
            e.preventDefault();
        });
    }

    setup_dropdown_widget(data) {
        const $dropdown_list_body = $(
            `#${CSS.escape(this.container_id)} .dropdown-list-body`,
        ).expectOne();
        const $search_input = $(
            `#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`,
        );
        const get_data = () => {
            if (this.include_current_item) {
                return data;
            }
            return data.filter((x) => x.value !== this.value.toString());
        };

        ListWidget.create($dropdown_list_body, get_data(data), {
            name: `${CSS.escape(this.widget_name)}_list`,
            modifier(item) {
                return render_dropdown_list({item});
            },
            filter: {
                $element: $search_input,
                predicate(item, value) {
                    return item.name.toLowerCase().includes(value);
                },
            },
            $simplebar_container: $(`#${CSS.escape(this.container_id)} .dropdown-list-wrapper`),
        });
    }

    // Sets the focus to the ListWidget input once the dropdown button is clicked.
    dropdown_toggle_click_handler() {
        const $dropdown_toggle = $(`#${CSS.escape(this.container_id)} .dropdown-toggle`);
        const $search_input = $(
            `#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`,
        );

        $dropdown_toggle.on("click", () => {
            $search_input.val("").trigger("input");
        });
    }

    dropdown_focus_events() {
        const $search_input = $(
            `#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`,
        );
        const $dropdown_menu = $(`.${CSS.escape(this.widget_name)}_setting .dropdown-menu`);

        const dropdown_elements = () => {
            const $dropdown_list_body = $(
                `#${CSS.escape(this.container_id)} .dropdown-list-body`,
            ).expectOne();

            return $dropdown_list_body.children().find("a");
        };

        // Rest of the key handlers are supported by our
        // bootstrap library.
        $dropdown_menu.on("keydown", (e) => {
            function trigger_element_focus($element) {
                e.preventDefault();
                e.stopPropagation();
                $element.trigger("focus");
            }

            switch (e.key) {
                case "ArrowDown": {
                    switch (e.target) {
                        case dropdown_elements().last()[0]:
                            trigger_element_focus($search_input);
                            break;
                        case $search_input[0]:
                            trigger_element_focus(dropdown_elements().first());
                            break;
                    }

                    break;
                }
                case "ArrowUp": {
                    switch (e.target) {
                        case dropdown_elements().first()[0]:
                            trigger_element_focus($search_input);
                            break;
                        case $search_input[0]:
                            trigger_element_focus(dropdown_elements().last());
                    }

                    break;
                }
                case "Tab": {
                    switch (e.target) {
                        case $search_input[0]:
                            trigger_element_focus(dropdown_elements().first());
                            break;
                        case dropdown_elements().last()[0]:
                            trigger_element_focus($search_input);
                            break;
                    }

                    break;
                }
            }
        });
    }

    setup() {
        // populate the dropdown
        const $dropdown_list_body = $(
            `#${CSS.escape(this.container_id)} .dropdown-list-body`,
        ).expectOne();
        const $search_input = $(
            `#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`,
        );
        const $dropdown_toggle = $(`#${CSS.escape(this.container_id)} .dropdown-toggle`);

        this.setup_dropdown_widget(this.data);

        $(`#${CSS.escape(this.container_id)} .dropdown-search`).on("click", (e) => {
            e.stopPropagation();
        });

        this.dropdown_toggle_click_handler();

        $dropdown_toggle.on("focus", (e) => {
            // On opening a Bootstrap Dropdown, the parent element receives focus.
            // Here, we want our search input to have focus instead.
            e.preventDefault();
            // This function gets called twice when focusing the
            // dropdown, and only in the second call is the input
            // field visible in the DOM; so the following visibility
            // check ensures we wait for the second call to focus.
            if ($dropdown_list_body.is(":visible")) {
                $search_input.trigger("focus");
            }
        });

        this.dropdown_focus_events();

        this.render(this.initial_value);
        this.register_event_handlers();
    }

    // Returns the updated value
    value() {
        let val = $(`#${CSS.escape(this.container_id)} #${CSS.escape(this.value_id)}`).data(
            "value",
        );
        if (val === null) {
            val = "";
        }
        return val;
    }
}

// A widget mostly similar to `DropdownListWidget` but
// used in cases of multiple dropdown selection.
export class MultiSelectDropdownListWidget extends DropdownListWidget {
    constructor({
        widget_name,
        data,
        default_text,
        null_value = null,
        on_update = () => {},
        on_close,
        value,
        limit,
    }) {
        super({
            widget_name,
            data,
            default_text,
            null_value,
            on_update,
            value,
        });

        // Initializing values specific to `MultiSelectDropdownListWidget`.
        this.limit = limit;
        this.on_close = on_close;

        // Important thing to note is that this needs to be maintained as
        // a reference type and not to deep clone it/assign it to a
        // different variable, so that it can be later referenced within
        // `list_widget` as well. The way we manage dropdown elements are
        // essentially by just modifying the values in `data_selected` variable.
        this.data_selected = []; // Populate the dropdown values selected by user.

        if (limit === undefined) {
            this.limit = 2;
            blueslip.warn(
                "Multiselect dropdown-list-widget: Called without limit value; using 2 as the limit",
            );
        }
    }

    setup() {
        super.setup(this);
        this.initialize_dropdown_values();
    }

    initialize_dropdown_values() {
        // Stop the execution if value parameter is undefined and null_value is passed.
        if (!this.initial_value || this.initial_value === this.null_value) {
            return;
        }
        const $elem = $(`#${CSS.escape(this.container_id)} #${CSS.escape(this.widget_name)}_name`);

        // Push values from initial valued array to `data_selected`.
        this.data_selected.push(...this.initial_value);
        this.render_button_text($elem, this.limit);
    }

    // Set the button text as per the selected data.
    render_button_text($elem, limit) {
        const items_selected = this.data_selected.length;
        let text = "";

        // Destroy the tooltip once the button text reloads.
        this.destroy_tooltip();

        if (items_selected === 0) {
            this.render_default_text($elem);
            return;
        } else if (limit >= items_selected) {
            const data_selected = this.data.filter((data) =>
                this.data_selected.includes(data.value),
            );
            text = data_selected.map((data) => data.name).toString();
        } else {
            text = $t({defaultMessage: "{items_selected} selected"}, {items_selected});
            this.render_tooltip();
        }

        $elem.text(text);
        $elem.removeClass("text-warning");
        $elem.closest(".input-group").find(".dropdown_list_reset_button").show();
    }

    // Override the DrodownListWidget `render` function.
    render(value) {
        const $elem = $(`#${CSS.escape(this.container_id)} #${CSS.escape(this.widget_name)}_name`);

        if (!value || value === this.null_value) {
            this.render_default_text($elem);
            return;
        }
        this.render_button_text($elem, this.limit);
    }

    dropdown_toggle_click_handler() {
        const $dropdown_toggle = $(`#${CSS.escape(this.container_id)} .dropdown-toggle`);
        const $search_input = $(
            `#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`,
        );

        $dropdown_toggle.on("click", () => {
            this.reset_dropdown_items();
            $search_input.val("").trigger("input");
        });
    }

    // Cases where a user presses any dropdown item but accidentally closes
    // the dropdown list.
    reset_dropdown_items() {
        // Clear the data selected and stop the execution once the user has
        // pressed the `reset` button.
        if (this.is_reset) {
            this.data_selected.splice(0, this.data_selected.length);
            return;
        }

        const original_items = this.checked_items ?? this.initial_value;
        const items_added = _.difference(this.data_selected, original_items);

        // Removing the unnecessary items from dropdown.
        for (const val of items_added) {
            const index = this.data_selected.indexOf(val);
            if (index > -1) {
                this.data_selected.splice(index, 1);
            }
        }

        // Items that are removed in dropdown but should have been a part of it
        const items_removed = _.difference(original_items, this.data_selected);
        this.data_selected.push(...items_removed);
    }

    // Override the DrodownListWidget `setup_dropdown_widget` function.
    setup_dropdown_widget(data) {
        const $dropdown_list_body = $(
            `#${CSS.escape(this.container_id)} .dropdown-list-body`,
        ).expectOne();
        const $search_input = $(
            `#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`,
        );

        ListWidget.create($dropdown_list_body, data, {
            name: `${CSS.escape(this.widget_name)}_list`,
            modifier(item) {
                return render_dropdown_list({item});
            },
            multiselect: {
                selected_items: this.data_selected,
            },
            filter: {
                $element: $search_input,
                predicate(item, value) {
                    return item.name.toLowerCase().includes(value);
                },
            },
            $simplebar_container: $(`#${CSS.escape(this.container_id)} .dropdown-list-wrapper`),
        });
    }

    // Add the check mark to dropdown element passed.
    add_check_mark($element) {
        const value = $element.attr("data-value");
        const $link_elem = $element.find("a").expectOne();
        $link_elem.prepend($("<i>").addClass(["fa", "fa-check"]));
        $element.addClass("checked");
        this.data_selected.push(value);
    }

    // Remove the check mark from dropdown element.
    remove_check_mark($element) {
        const $icon = $element.find("i").expectOne();
        const value = $element.attr("data-value");
        const index = this.data_selected.indexOf(value);

        if (index > -1) {
            $icon.remove();
            $element.removeClass("checked");
            this.data_selected.splice(index, 1);
        }
    }

    // Render the tooltip once the text changes to `n` selected.
    render_tooltip() {
        const $elem = $(`#${CSS.escape(this.container_id)}`);
        const selected_items = this.data.filter((item) => this.checked_items.includes(item.value));

        tippy($elem[0], {
            content: selected_items.map((item) => item.name).join(", "),
            placement: "top",
        });
    }

    destroy_tooltip() {
        const $elem = $(`#${CSS.escape(this.container_id)}`);
        const tippy_instance = $elem[0]._tippy;
        if (!tippy_instance) {
            return;
        }

        tippy_instance.destroy();
    }

    dropdown_focus_events() {
        // Main keydown event handler which transfers the focus from one child element
        // to another.

        const $search_input = $(
            `#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`,
        );
        const $dropdown_menu = $(`.${CSS.escape(this.widget_name)}_setting .dropdown-menu`);
        const $filter_button = $(`#${CSS.escape(this.container_id)} .multiselect_btn`);

        const dropdown_elements = () => {
            const $dropdown_list_body = $(
                `#${CSS.escape(this.container_id)} .dropdown-list-body`,
            ).expectOne();

            return $dropdown_list_body.children().find("a");
        };

        $dropdown_menu.on("keydown", (e) => {
            function trigger_element_focus($element) {
                e.preventDefault();
                e.stopPropagation();
                $element.trigger("focus");
            }

            switch (e.key) {
                case "ArrowDown": {
                    switch (e.target) {
                        case dropdown_elements().last()[0]:
                            trigger_element_focus($filter_button);
                            break;
                        case $(`#${CSS.escape(this.container_id)} .multiselect_btn`)[0]:
                            trigger_element_focus($search_input);
                            break;
                        case $search_input[0]:
                            trigger_element_focus(dropdown_elements().first());
                            break;
                    }

                    break;
                }
                case "ArrowUp": {
                    switch (e.target) {
                        case dropdown_elements().first()[0]:
                            trigger_element_focus($search_input);
                            break;
                        case $search_input[0]:
                            trigger_element_focus($filter_button);
                            break;
                        case $(`#${CSS.escape(this.container_id)} .multiselect_btn`)[0]:
                            trigger_element_focus(dropdown_elements().last());
                            break;
                    }

                    break;
                }
                case "Tab": {
                    switch (e.target) {
                        case $search_input[0]:
                            trigger_element_focus(dropdown_elements().first());
                            break;
                        case $filter_button[0]:
                            trigger_element_focus($search_input);
                            break;
                    }

                    break;
                }
            }
        });
    }

    // Override the `register_event_handlers` function.
    register_event_handlers() {
        const $dropdown_list_body = $(
            `#${CSS.escape(this.container_id)} .dropdown-list-body`,
        ).expectOne();

        $dropdown_list_body.on("click keypress", ".list_item", (e) => {
            if (e.type === "keypress" && !keydown_util.is_enter_event(e)) {
                return;
            }

            const $element = e.target.closest("li");
            if ($element.hasClass("checked")) {
                this.remove_check_mark($element);
            } else {
                this.add_check_mark($element);
            }

            e.stopPropagation();
        });

        $(`#${CSS.escape(this.container_id)} .dropdown_list_reset_button`).on("click", (e) => {
            // Default back the values.
            this.is_reset = true;
            this.checked_items = undefined;

            this.update(this.null_value);
            e.preventDefault();
        });

        $(`#${CSS.escape(this.container_id)} .multiselect_btn`).on("click", (e) => {
            e.preventDefault();

            // Set the value to `false` to end the scope of the
            // `reset` button.
            this.is_reset = false;
            // We deep clone the values of `data_selected` to a new
            // variable. This is so because arrays are reference types
            // and modifying the parent array can change the values
            // within the child array. Here, `checked_items` copies over the
            // value and not just the reference.
            this.checked_items = _.cloneDeep(this.data_selected);
            this.update(this.data_selected);

            // Cases when the user wants to pass a successful event after
            // the dropdown is closed.
            if (this.on_close) {
                e.stopPropagation();
                const $setting_elem = $(e.currentTarget).closest(
                    `.${CSS.escape(this.widget_name)}_setting`,
                );
                $setting_elem.find(".dropdown-menu").dropdown("toggle");

                this.on_close();
            }
        });
    }

    // Returns array of values selected by user.
    value() {
        let val = this.checked_items;
        // Cases taken care of -
        // - User never pressed the filter button -> We return the initial value.
        // - User pressed the `reset` button -> We return an empty array.
        if (val === undefined) {
            val = this.is_reset ? [] : this.initial_value;
        }
        return val;
    }
}
