import * as hoot from "@odoo/hoot-dom";
import { patch } from "@web/core/utils/patch";
import { TourHelpers } from "./tour_helpers";

patch(TourHelpers.prototype, {
    /**
     * Ensures that the given {@link Selector} is checked.
     * @description
     * If it is not checked, a click is triggered on the input.
     * If the input is still not checked after the click, an error is thrown.
     *
     * @param {string|Node} selector
     * @example
     *  run: "check", //Checks the action element
     * @example
     *  run: "check input[type=checkbox]", // Checks the selector
     */
    async check(selector) {
        const element = this._get_action_element(selector);
        await hoot.check(element);
    },

    /**
     * Clears the **value** of the **{@link Selector}**.
     * @description
     * This is done using the following sequence:
     * - pressing "Control" + "A" to select the whole value;
     * - pressing "Backspace" to delete the value;
     * - (optional) triggering a "change" event by pressing "Enter".
     *
     * @param {Selector} selector
     * @example
     *  run: "clear", // Clears the value of the action element
     * @example
     *  run: "clear input#my_input", // Clears the value of the selector
     */
    async clear(selector) {
        const element = this._get_action_element(selector);
        await hoot.click(element);
        await hoot.clear();
    },

    /**
     * Performs a click sequence on the given **{@link Selector}**
     * @description Let's see more informations about click sequence here: {@link hoot.click}
     * @param {Selector} selector
     * @param {import("@odoo/hoot-dom").PointerOptions} options
     * @example
     *  run: "click", // Click on the action element
     * @example
     *  run: "click .o_rows:first", // Click on the selector
     */
    async click(selector, options = { interactive: false }) {
        const element = this._get_action_element(selector);
        // FIXME: should always target interactive element, but some tour steps are
        // targetting elements affected by 'pointer-events: none' for some reason.
        // This option should ultimately disappear, with all affected cased fixed
        // individually (no common cause found during a quick investigation).
        await hoot.click(element, options);
    },

    /**
     * Performs two click sequences on the given **{@link Selector}**.
     * @description Let's see more informations about click sequence here: {@link hoot.dblclick}
     * @param {Selector} selector
     * @example
     *  run: "dblclick", // Double click on the action element
     * @example
     *  run: "dblclick .o_rows:first", // Double click on the selector
     */
    async dblclick(selector) {
        const element = this._get_action_element(selector);
        await hoot.dblclick(element);
    },

    /**
     * Starts a drag sequence on the active element (anchor) and drop it on the given **{@link Selector}**.
     * @param {Selector} selector
     * @param {hoot.PointerOptions} options
     * @example
     *  run: "drag_and_drop .o_rows:first", // Drag the active element and drop it in the selector
     * @example
     *  async run(helpers) {
     *      await helpers.drag_and_drop(".o_rows:first", {
     *          position: {
     *              top: 40,
     *              left: 5,
     *          },
     *          relative: true,
     *      });
     *  }
     */
    async drag_and_drop(selector, options) {
        if (typeof options !== "object") {
            options = { position: "top", relative: true };
        }
        const dragEffectDelay = async () => {
            await hoot.animationFrame();
            await hoot.delay(this.delay);
        };

        const element = this.anchor;
        const { drop, moveTo } = await hoot.drag(element);
        await dragEffectDelay();
        await hoot.hover(element, {
            position: {
                top: 20,
                left: 20,
            },
            relative: true,
        });
        await dragEffectDelay();
        const target = await hoot.waitFor(selector, {
            visible: true,
            timeout: 1000,
        });
        await moveTo(target, options);
        await dragEffectDelay();
        await drop(target, options);
        await dragEffectDelay();
    },

    /**
     * Edit input or textarea given by **{@link selector}**
     * @param {string} text
     * @param {Selector} selector
     * @example
     *  run: "edit Hello Mr. Doku",
     */
    async edit(text, selector) {
        const element = this._get_action_element(selector);
        await hoot.click(element);
        await hoot.edit(text);
    },

    /**
     * Edit only editable wysiwyg element given by **{@link Selector}**
     * @param {string} text
     * @param {Selector} selector
     */
    async editor(text, selector) {
        const element = this._get_action_element(selector);
        const InEditor = Boolean(element.closest(".odoo-editor-editable"));
        if (!InEditor) {
            throw new Error("run 'editor' always on an element in an editor");
        }
        await hoot.click(element);
        this._set_range(element, "start");
        await hoot.keyDown("_");
        element.textContent = text;
        await hoot.manuallyDispatchProgrammaticEvent(element, "input");
        this._set_range(element, "stop");
        await hoot.keyUp("_");
        await hoot.manuallyDispatchProgrammaticEvent(element, "change");
    },

    /**
     * Fills the **{@link Selector}** with the given `value`.
     * @description This helper is intended for `<input>` and `<textarea>` elements,
     * with the exception of `"checkbox"` and `"radio"` types, which should be
     * selected using the {@link check} helper.
     * In tour, it's mainly usefull for autocomplete components.
     * @param {string} value
     * @param {Selector} selector
     */
    async fill(value, selector) {
        const element = this._get_action_element(selector);
        await hoot.click(element);
        await hoot.fill(value);
    },

    /**
     * Performs a hover sequence on the given **{@link Selector}**.
     * @param {Selector} selector
     * @param {import("@odoo/hoot-dom").PointerOptions} options
     * @example
     *  run: "hover",
     */
    async hover(selector, options) {
        const element = this._get_action_element(selector);
        await hoot.hover(element, options);
    },

    /**
     * Only for input[type="range"]
     * @param {string|number} value
     * @param {Selector} selector
     */
    async range(value, selector) {
        const element = this._get_action_element(selector);
        await hoot.click(element);
        await hoot.setInputRange(element, value);
    },

    /**
     * Performs a keyboard event sequence.
     * @example
     *  run : "press Enter",
     */
    async press(...args) {
        await hoot.press(args.flatMap((arg) => typeof arg === "string" && arg.split("+")));
    },

    /**
     * Performs a selection event sequence on **{@link Selector}**. This helper is intended
     * for `<select>` elements only.
     * @description Select the option by its value
     * @param {string} value
     * @param {Selector} selector
     * @example
     * run(helpers) => {
     *  helpers.select("Kevin17", "select#mySelect");
     * },
     * @example
     * run: "select Foden47",
     */
    async select(value, selector) {
        const element = this._get_action_element(selector);
        await hoot.click(element);
        await hoot.select(value, { target: element });
    },

    /**
     * Performs a selection event sequence on **{@link Selector}**
     * @description Select the option by its index
     * @param {number} index starts at 0
     * @param {Selector} selector
     * @example
     *  run: "selectByIndex 2", //Select the third option
     */
    async selectByIndex(index, selector) {
        const element = this._get_action_element(selector);
        await hoot.click(element);
        const value = hoot.queryValue(`option:eq(${index})`, { root: element });
        if (value) {
            await hoot.select(value, { target: element });
            await hoot.manuallyDispatchProgrammaticEvent(element, "input");
        }
    },

    /**
     * Performs a selection event sequence on **{@link Selector}**
     * @description Select option(s) by there labels
     * @param {string|RegExp} contains
     * @param {Selector} selector
     * @example
     *  run: "selectByLabel Jeremy Doku", //Select all options where label contains Jeremy Doku
     */
    async selectByLabel(contains, selector) {
        const element = this._get_action_element(selector);
        await hoot.click(element);
        const values = hoot.queryAllValues(`option:contains(${contains})`, { root: element });
        await hoot.select(values, { target: element });
    },

    /**
     * Ensures that the given {@link Selector} is unchecked.
     * @description
     * If it is checked, a click is triggered on the input.
     * If the input is still checked after the click, an error is thrown.
     *
     * @param {string|Node} selector
     * @example
     *  run: "uncheck", // Unchecks the action element
     * @example
     *  run: "uncheck input[type=checkbox]", // Unchecks the selector
     */
    async uncheck(selector) {
        const element = this._get_action_element(selector);
        await hoot.uncheck(element);
    },

    /**
     * Navigate to {@link url}.
     *
     * @param {string} url
     * @example
     *  run: "goToUrl /shop", // Go to /shop
     */
    async goToUrl(url) {
        const linkEl = document.createElement("a");
        linkEl.href = url;
        await hoot.click(linkEl);
    },

    /**
     * Ensures that the given canvas selector **{@link Selector}** contains pixels.
     * @param {string|Node} selector
     */
    async canvasNotEmpty(selector) {
        const canvas = this._get_action_element(selector);
        if (canvas.tagName.toLowerCase() !== "canvas") {
            throw new Error(`canvasNotEmpty is only suitable for canvas elements.`);
        }
        await hoot.waitUntil(() => {
            const context = canvas.getContext("2d");
            const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
            const pixels = new Uint32Array(imageData.data.buffer);
            return pixels.some((pixel) => pixel !== 0); // pixel is on
        });
    },

    /**
     * Get Node for **{@link Selector}**
     * @param {Selector} selector
     * @returns {Node}
     * @default this.anchor
     */
    _get_action_element(selector) {
        if (typeof selector === "string" && selector.length) {
            const nodes = hoot.queryAll(selector);
            return nodes.find(hoot.isVisible) || nodes.at(0);
        } else if (typeof selector === "object" && Boolean(selector?.nodeType)) {
            return selector;
        }
        return this.anchor;
    },

    // Useful for wysiwyg editor.
    _set_range(element, start_or_stop) {
        function _node_length(node) {
            if (node.nodeType === Node.TEXT_NODE) {
                return node.nodeValue.length;
            } else {
                return node.childNodes.length;
            }
        }
        const selection = element.ownerDocument.getSelection();
        selection.removeAllRanges();
        const range = new Range();
        let node = element;
        let length = 0;
        if (start_or_stop === "start") {
            while (node.firstChild) {
                node = node.firstChild;
            }
        } else {
            while (node.lastChild) {
                node = node.lastChild;
            }
            length = _node_length(node);
        }
        range.setStart(node, length);
        range.setEnd(node, length);
        selection.addRange(range);
    },

    queryAll(target, options) {
        return hoot.queryAll(target, options);
    },

    queryFirst(target, options) {
        return hoot.queryFirst(target, options);
    },

    queryOne(target, options) {
        return hoot.queryOne(target, options);
    },

    waitFor(target, options) {
        return hoot.waitFor(target, options);
    },

    waitUntil(predicate, options) {
        return hoot.waitUntil(predicate, options);
    },

    animationFrame(...args) {
        return hoot.animationFrame(...args);
    },
});
