/**
 * Generate a self-closed notification using Bootstrap toasts
 *
 * :param message: The message to show: it can be a string or an HTML element.
 * :param close_button: Whether to show a close button
 * :param delay: Delay in milliseconds after which the notification disappears.
 */
export function toast_notify({message, close_button=true, delay=2000})
{
    let main_toast_container = document.getElementById("user-message-container").children[0];

    let toast = document.createElement("div");
    toast.classList.add("toast", "align-items-center");
    toast.attributes["role"] = "alert";
    toast.attributes["aria-live"] = "assertive";
    toast.attributes["aria-atomic"] = true;

    let toast_body = document.createElement("div");
    toast_body.classList.add("toast-body");

    if (typeof(message) == "string")
        toast_body.innerText = message;
    else
        toast_body.append(message);

    if (close_button)
    {
        let dflex = document.createElement("div");
        dflex.classList.add("d-flex");
        toast.append(dflex);
        dflex.append(toast_body);

        let close_button = document.createElement("button");
        close_button.classList.add("btn-close", "me-2", "m-auto");
        close_button.attributes["type"] = "button";
        close_button.attributes["data-bs-dismiss"] = "toast";
        close_button.attributes["aria-label"] = "Close";
        dflex.append(close_button);
    } else {
        toast.append(toast_body);
    }

    main_toast_container.append(toast);

    toast.addEventListener("hidden.bs.toast", () => {
        toast.remove();
    });

    let bs_toast = new bootstrap.Toast(toast, {
        autohide: true,
        delay: delay,
    });
    bs_toast.show();
}

/// Base class for Debusine DOM widgets
export class DebusineWidget
{
    constructor(element)
    {
        this.element = element;
    }

    /// Set up the widget on the given DOM element
    static setup(element)
    {
        // Make sure element.debusine exists
        const debusine_data = element.debusine;
        if (debusine_data === undefined)
            element.debusine = {};

        // Stop if the element is already setup as this widget
        const data_key = this.name;
        if (element.debusine[data_key])
            return;

        let widget;
        try {
            widget = new this(element);
        } catch (e) {
            console.error("%s: widget constructor failed on %o: %o", this.name, element, e);
            return;
        }

        element.debusine[data_key] = widget;
    }

    /// Set up DebusineRemoteDropdown on all elements matching the given selector
    static setup_all(selector)
    {
        if (!selector)
            selector = this.default_selector;
        if (!selector)
            console.error("setup_all called for default selector but class %s does not define default_selector", this.name);
        for (const element of document.querySelectorAll(selector))
            this.setup(element);
    }
}

/// Remotely toggle a Bootstrap dropdown menu
export class DebusineRemoteDropdown extends DebusineWidget
{
    static default_selector = ".debusine-dropdown-remote";

    constructor(element)
    {
        super(element);
        const target_name = this.element.dataset.bsTarget;
        if (!target_name)
            throw new Error(`DebusineRemoteDropdown element found without data-bs-target attribute`);
        this.target = document.querySelector(target_name);
        if (!this.target)
            throw new Error(`${element.dataset.bsTarget} not found as DebusineRemoteDropdown target`);

        // Look for a parent dropdown we may want to close
        this.parent_dropdown = null;
        const containing_dropdown = this.element.closest(".dropdown-menu")
        if (containing_dropdown)
            this.parent_dropdown = containing_dropdown.parentElement.querySelector("*[data-bs-toggle='dropdown']");

        // Install the click handler on the remote control element
        this.element.addEventListener("click", evt => {
            evt.stopPropagation();
            this.activate(evt);
        });
    }

    /// Activate the remote
    activate(evt)
    {
        // If we are inside another dropdown, close it
        if (this.parent_dropdown)
        {
            const parent_dropdown = bootstrap.Dropdown.getInstance(this.parent_dropdown);
            parent_dropdown.hide();
        }

        // Dropdown that we control
        const dropdown = bootstrap.Dropdown.getOrCreateInstance(this.target);
        dropdown.toggle();
    }
}

/// Close a containing dropdown menu
export class DebusineCloseDropdown extends DebusineWidget
{
    static default_selector = ".debusine-dropdown-close";

    constructor(element)
    {
        super(element);

        // Look for a parent dropdown we may want to close
        this.target = null;
        const containing_dropdown = this.element.closest(".dropdown-menu")
        if (containing_dropdown)
            this.target = containing_dropdown.parentElement.querySelector("*[data-bs-toggle='dropdown']");

        if (!this.target)
            throw new Error("DebusineCloseDropdown element is not inside a dropdown menu");

        // Install the click handler on the remote control element
        this.element.addEventListener("click", evt => {
            evt.stopPropagation();
            this.activate(evt);
        });
    }

    /// Activate the remote
    activate(evt)
    {
        const dropdown = bootstrap.Dropdown.getOrCreateInstance(this.target);
        dropdown.hide();
    }
}

/// Copy text to clipboard when the element is clicked and show a toast
export class DebusineClipboardCopy extends DebusineWidget {
    static default_selector = ".debusine-clipboard-copy"

    constructor(element) {
        super(element);

        this.copy_text = element.dataset.copyText;
        this.copy_target = element.dataset.copyTarget ? document.querySelector(element.dataset.copyTarget) : null

        if (!this.copy_text && !this.copy_target) {
            throw new Error("DebusineClipboardCopy requires data-copy-text or data-copy-target");
        }

        element.addEventListener("click", (evt) => {
            evt.preventDefault();
            evt.stopPropagation();
            this.copyToClipboard();
            toast_notify({message: "Copied to clipboard"});
        });
    }

    async copyToClipboard() {
        const text = this.copy_text ?? this.copy_target.textContent;
        await navigator.clipboard.writeText(text);
    }
}

/// Drag and drop interface to the workspace inheritance editor formset
export class DebusineWorkspaceInheritanceEditor extends DebusineWidget {
    static default_selector = ".debusine-workspace-inheritance-editor";

    constructor(element) {
        super(element);
        // Collect backing data
        for (let script of element.getElementsByTagName("script")) {
            if (script.dataset.type == "current")
                this.current = JSON.parse(script.textContent);
            else if (script.dataset.type == "available")
                this.available = JSON.parse(script.textContent);
            else
                console.error("Unsupported script %o in %o", script, this.element);
        }
        // Current parent list
        if (this.current === undefined)
            console.error("current parents not found in %o", this.element);
        // List of all possible parents
        if (this.available === undefined)
            console.error("available parents not found in %o", this.element);
        // Initial parent list
        this.initial = this.current;

        // Collect UI elements
        for (let el of element.getElementsByClassName("card")) {
            if (el.dataset.type == "current")
                this.card_current = el;
            else if (el.dataset.type == "available")
                this.card_available = el;
        }
        // List of current elements
        if (this.card_current === undefined)
            console.error("current list not found in %o", this.element);
        // List of all available elements
        if (this.card_available === undefined)
            console.error("available list not found in %o", this.element);

        for (let el of element.getElementsByTagName("input"))
        {
            if (el.getAttribute("name") == "form-TOTAL_FORMS")
                this.input_total_forms = el;
        }
        // Total number of forms in Django formset management form
        if (this.input_total_forms === undefined)
            console.error("form-TOTAL_FORMS input element not found in %o", this.element);

        // Setup drag and drop
        for (let el of [this.card_available, this.card_current])
        {
            el.addEventListener("dragover", ev => {
                ev.preventDefault();
                ev.dataTransfer.dropEffect = "move";
            });
        }
        this.card_available.addEventListener("drop", ev => {
            ev.preventDefault();
            const id = +ev.dataTransfer.getData("text/plain");
            this.remove_id(id)
        });
        this.card_current.addEventListener("drop", ev => {
            ev.preventDefault();
            const id = +ev.dataTransfer.getData("text/plain");
            if (ev.target.tagName == "LI")
                this.insert_id_after(id, ev.target.dataset.id);
            else
                this.prepend_id(id);
        });
        for (let el of [this.card_current, this.card_available])
        {
            for (let el1 of el.getElementsByClassName("card-header"))
                this._add_drag_highlight(el1);
        }

        // Fill the columns
        this.refresh();

        // Show the drag and drop editor
        this.element.classList.remove("d-none");
    }

    // Get the workspace name for a workspace ID
    get_name(id) {
        for (let [cand_id, name] of this.available)
            if (id == cand_id)
                return name
    }

    // Insert a parent workspace at the beginning of the chain
    prepend_id(id) {
        let new_current = [[id, this.get_name(id)]];
        for (let [el_id, el_name] of this.current)
        {
            // Remove id if it's in the list
            if (el_id == id)
                continue;
            new_current.push([el_id, el_name]);
        }
        this.current = new_current;
        this.refresh();
    }

    // Insert a parent workspace after the given one
    insert_id_after(id, reference_id) {
        if (id == reference_id)
            return;

        let new_current = [];
        for (let [el_id, el_name] of this.current)
        {
            // Remove id if it's in the list
            if (el_id == id)
                continue;

            new_current.push([el_id, el_name]);

            // Append id after the reference element
            if (el_id == reference_id)
                new_current.push([id, this.get_name(id)]);
        }
        this.current = new_current;
        this.refresh();
    }

    // Remove a parent workspace
    remove_id(id) {
        this.current = this.current.filter(([el_id, el_name]) => el_id != id);
        this.refresh();
    }

    // Update the UI to reflect this.current and this.available
    refresh() {
        this.refresh_current();
        this.refresh_available();
    }

    // Update the UI to reflect this.current
    refresh_current() {
        let ul = this.card_current.getElementsByTagName("ul")[0];
        let elements = [];
        for (let [id, name] of this.current)
        {
            elements.push(this._li(id, name));
        }
        ul.replaceChildren(...elements);

        // Regenerate hidden input elements
        for (let el of this.card_current.getElementsByTagName("input"))
            el.remove();
        // Field with the current parent list
        let pos = 0;
        for (let [id, name] of this.current)
        {
            this.card_current.append(this._hidden_field(`form-${pos}-parent`, id));
            this.card_current.append(this._hidden_field(`form-${pos}-ORDER`, pos));
            ++pos;
        }
        // Denylist with elements in the current list
        let current = {};
        for (let [id, name] of this.current)
            current[id] = true;
        // Fields that remove deleted elements
        for (let [id, name] of this.initial)
        {
            if (current[id])
                continue;
            this.card_current.append(this._hidden_field(`form-${pos}-parent`, id));
            this.card_current.append(this._hidden_field(`form-${pos}-ORDER`, pos));
            this.card_current.append(this._hidden_field(`form-${pos}-DELETE`, "true"));
            ++pos;
        }
        // Update count of total forms in the formset
        this.input_total_forms.setAttribute("value", ""+pos);

    }

    // Update the UI to reflect this.available
    refresh_available() {
        // Denylist with elements in the current list
        let current = {};
        for (let [id, name] of this.current)
            current[id] = true;

        let ul = this.card_available.getElementsByTagName("ul")[0];
        let elements = [];
        for (let [id, name] of this.available)
        {
            if (current[id])
                continue;
            elements.push(this._li(id, name));
        }
        ul.replaceChildren(...elements);
    }

    // Highlight under the element when targeted with a drop
    _add_drag_highlight(el) {
        el.addEventListener("dragenter", (ev) => {
            el.classList.add("border-bottom", "border-primary");
        });
        el.addEventListener("dragleave", (ev) => {
            el.classList.remove("border-bottom", "border-primary");
        });
        // See https://www.exchangetuts.com/dragleave-of-parent-element-fires-when-dragging-over-children-elements-1639482905484444
        // and https://sqlpey.com/javascript/how-to-fix-html5-dragleave-issue/
        for (let child of el.children)
            child.style["pointer-events"] = "none";
    }

    // Remove drag target markers, since elements don't always get a dragleave event
    _reset_drag_target_markers()
    {
        for (let el of this.element.getElementsByClassName("border-bottom"))
            el.classList.remove("border-bottom", "border-primary");
    }

    // Create a current/available list item
    _li(id, name) {
        let li = document.createElement("li");
        li.classList.add("list-group-item");
        li.dataset["id"] = id;
        li.append(name);
        li.setAttribute("draggable", true);
        li.style["cursor"] = "grab"
        li.addEventListener("dragstart", (ev) => {
            ev.dataTransfer.setData("text/plain", id);
            ev.dataTransfer.effectAllowed = "move";
            ev.dataTransfer.dropEffect = "move";

        });
        li.addEventListener("dragend", evt => {
            this._reset_drag_target_markers();
        });
        this._add_drag_highlight(li);
        return li;
    }

    // Create a hidden form field
    _hidden_field(name, value) {
        let input = document.createElement("input");
        input.setAttribute("type", "hidden");
        input.setAttribute("name", name);
        input.setAttribute("value", value);
        return input;
    }
}
