/**
 * @author n1474335 [n1474335@gmail.com]
 * @copyright Crown Copyright 2016
 * @license Apache-2.0
 */

import {EditorSelection} from "@codemirror/state";
import {chrEncWidth} from "../../core/lib/ChrEnc.mjs";

/**
 * Waiter to handle events related to highlighting in CyberChef.
 */
class HighlighterWaiter {

    /**
     * HighlighterWaiter constructor.
     *
     * @param {App} app - The main view object for CyberChef.
     * @param {Manager} manager - The CyberChef event manager.
     */
    constructor(app, manager) {
        this.app = app;
        this.manager = manager;

        this.currentSelectionRanges = [];
    }

    /**
     * Handler for selection change events in the input and output
     *
     * Highlights the given offsets in the input or output.
     * We will only highlight if:
     *     - input hasn't changed since last bake
     *     - last bake was a full bake
     *     - all operations in the recipe support highlighting
     *
     * @param {string} io
     * @param {ViewUpdate} e
     */
    selectionChange(io, e) {
        // Confirm we are not currently baking
        if (!this.app.autoBake_ || this.app.baking) return false;

        // Confirm this was a user-generated event to prevent looping
        // from setting the selection in this class
        if (!e.transactions[0].isUserEvent("select")) return false;

        this.currentSelectionRanges = [];

        // Confirm some non-empty ranges are set
        const selectionRanges = e.state.selection.ranges;

        // Adjust offsets based on the width of the character set
        const inputCharacterWidth = chrEncWidth(this.manager.input.getChrEnc());
        const outputCharacterWidth = chrEncWidth(this.manager.output.getChrEnc());
        let ratio = 1;
        if (inputCharacterWidth !== outputCharacterWidth &&
            inputCharacterWidth !== 0 && outputCharacterWidth !== 0) {
            ratio = io === "input" ?
                inputCharacterWidth / outputCharacterWidth :
                outputCharacterWidth / inputCharacterWidth;
        }

        // Loop through ranges and send request for output offsets for each one
        const direction = io === "input" ? "forward" : "reverse";
        for (const range of selectionRanges) {
            const pos = [{
                start: Math.floor(range.from * ratio),
                end: Math.floor(range.to * ratio)
            }];
            this.manager.worker.highlight(this.app.getRecipeConfig(), direction, pos);
        }
    }


    /**
     * Displays highlight offsets sent back from the Chef.
     *
     * @param {Object[]} pos - The position object for the highlight.
     * @param {number} pos.start - The start offset.
     * @param {number} pos.end - The end offset.
     * @param {string} direction
     */
    displayHighlights(pos, direction) {
        if (!pos) return;
        if (this.manager.tabs.getActiveTab("input") !== this.manager.tabs.getActiveTab("output")) return;

        const io = direction === "forward" ? "output" : "input";
        this.highlight(io, pos);
    }


    /**
     * Sends selection updates to the relevant EditorView.
     *
     * @param {string} io - The input or output
     * @param {Object[]} ranges - An array of position objects to highlight
     * @param {number} ranges.start - The start offset
     * @param {number} ranges.end - The end offset
     */
    async highlight(io, ranges) {
        if (!this.app.options.showHighlighter) return false;
        if (!this.app.options.attemptHighlight) return false;
        if (!ranges || !ranges.length) return false;

        const view = io === "input" ?
            this.manager.input.inputEditorView :
            this.manager.output.outputEditorView;

        // Add new SelectionRanges to existing ones
        for (const range of ranges) {
            if (typeof range.start !== "number" ||
                typeof range.end !== "number")
                continue;
            const selection = range.end <= range.start ?
                EditorSelection.cursor(range.start) :
                EditorSelection.range(range.start, range.end);

            this.currentSelectionRanges.push(selection);
        }

        // Set selection
        if (this.currentSelectionRanges.length) {
            try {
                view.dispatch({
                    selection: EditorSelection.create(this.currentSelectionRanges),
                    scrollIntoView: true
                });
            } catch (err) {
                // Ignore Range Errors
                if (!err.toString().startsWith("RangeError")) {
                    console.error(err);
                }

            }
        }
    }

}

export default HighlighterWaiter;
