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

import HTMLOperation from "../HTMLOperation.mjs";
import Sortable from "sortablejs";
import Utils from "../../core/Utils.mjs";


/**
 * Waiter to handle events related to the recipe.
 */
class RecipeWaiter {

    /**
     * RecipeWaiter 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.removeIntent = false;
    }


    /**
     * Sets up the drag and drop capability for operations in the operations and recipe areas.
     */
    initialiseOperationDragNDrop() {
        const recList = document.getElementById("rec-list");

        // Recipe list
        Sortable.create(recList, {
            group: "recipe",
            sort: true,
            animation: 0,
            delay: 0,
            filter: ".arg",
            preventOnFilter: false,
            setData: function(dataTransfer, dragEl) {
                dataTransfer.setData("Text", dragEl.querySelector(".op-title").textContent);
            },
            onEnd: function(evt) {
                if (this.removeIntent) {
                    evt.item.remove();
                    evt.target.dispatchEvent(this.manager.operationremove);
                }
            }.bind(this),
            onSort: function(evt) {
                if (evt.from.id === "rec-list") {
                    document.dispatchEvent(this.manager.statechange);
                }
            }.bind(this)
        });

        Sortable.utils.on(recList, "dragover", function() {
            this.removeIntent = false;
        }.bind(this));

        Sortable.utils.on(recList, "dragleave", function() {
            this.removeIntent = true;
            this.app.progress = 0;
        }.bind(this));

        Sortable.utils.on(recList, "touchend", function(e) {
            const loc = e.changedTouches[0];
            const target = document.elementFromPoint(loc.clientX, loc.clientY);

            this.removeIntent = !recList.contains(target);
        }.bind(this));

        // Favourites category
        document.querySelector("#categories a").addEventListener("dragover", this.favDragover.bind(this));
        document.querySelector("#categories a").addEventListener("dragleave", this.favDragleave.bind(this));
        document.querySelector("#categories a").addEventListener("drop", this.favDrop.bind(this));
    }


    /**
     * Creates a drag-n-droppable seed list of operations.
     *
     * @param {element} listEl - The list to initialise
     */
    createSortableSeedList(listEl) {
        Sortable.create(listEl, {
            group: {
                name: "recipe",
                pull: "clone",
                put: false,
            },
            sort: false,
            setData: function(dataTransfer, dragEl) {
                dataTransfer.setData("Text", dragEl.textContent);
            },
            onStart: function(evt) {
                // Removes popover element and event bindings from the dragged operation but not the
                // event bindings from the one left in the operations list. Without manually removing
                // these bindings, we cannot re-initialise the popover on the stub operation.
                $(evt.item)
                    .popover("dispose")
                    .removeData("bs.popover")
                    .off("mouseenter")
                    .off("mouseleave")
                    .attr("data-toggle", "popover-disabled");
                $(evt.clone)
                    .off(".popover")
                    .removeData("bs.popover");
            },
            onEnd: this.opSortEnd.bind(this)
        });
    }


    /**
     * Handler for operation sort end events.
     * Removes the operation from the list if it has been dropped outside. If not, adds it to the list
     * at the appropriate place and initialises it.
     *
     * @fires Manager#operationadd
     * @param {event} evt
     */
    opSortEnd(evt) {
        if (this.removeIntent && evt.item.parentNode.id === "rec-list") {
            evt.item.remove();
            return;
        }

        // Reinitialise the popover on the original element in the ops list because for some reason it
        // gets destroyed and recreated. If the clone isn't in the ops list, we use the original item instead.
        let enableOpsElement;
        if (evt.clone.parentNode && evt.clone.parentNode.classList.contains("op-list")) {
            enableOpsElement = evt.clone;
        } else {
            enableOpsElement = evt.item;
            $(evt.item).attr("data-toggle", "popover");
        }
        this.manager.ops.enableOpsListPopovers(enableOpsElement);

        if (evt.item.parentNode.id !== "rec-list") {
            return;
        }

        this.buildRecipeOperation(evt.item);
        evt.item.dispatchEvent(this.manager.operationadd);
    }


    /**
     * Handler for favourite dragover events.
     * If the element being dragged is an operation, displays a visual cue so that the user knows it can
     * be dropped here.
     *
     * @param {event} e
     */
    favDragover(e) {
        if (e.dataTransfer.effectAllowed !== "move")
            return false;

        e.stopPropagation();
        e.preventDefault();
        if (e.target.className && e.target.className.indexOf("category-title") > -1) {
            // Hovering over the a
            e.target.classList.add("favourites-hover");
        } else if (e.target.parentNode.className && e.target.parentNode.className.indexOf("category-title") > -1) {
            // Hovering over the Edit button
            e.target.parentNode.classList.add("favourites-hover");
        } else if (e.target.parentNode.parentNode.className && e.target.parentNode.parentNode.className.indexOf("category-title") > -1) {
            // Hovering over the image on the Edit button
            e.target.parentNode.parentNode.classList.add("favourites-hover");
        }
    }


    /**
     * Handler for favourite dragleave events.
     * Removes the visual cue.
     *
     * @param {event} e
     */
    favDragleave(e) {
        e.stopPropagation();
        e.preventDefault();
        document.querySelector("#categories a").classList.remove("favourites-hover");
    }


    /**
     * Handler for favourite drop events.
     * Adds the dragged operation to the favourites list.
     *
     * @param {event} e
     */
    favDrop(e) {
        e.stopPropagation();
        e.preventDefault();
        e.target.classList.remove("favourites-hover");

        const opName = e.dataTransfer.getData("Text");
        this.app.addFavourite(opName);
    }


    /**
     * Handler for ingredient change events.
     *
     * @fires Manager#statechange
     */
    ingChange(e) {
        if (e && e.target && e.target.classList.contains("no-state-change")) return;
        window.dispatchEvent(this.manager.statechange);
    }


    /**
     * Handler for disable click events.
     * Updates the icon status.
     *
     * @fires Manager#statechange
     * @param {event} e
     */
    disableClick(e) {
        const icon = e.target;

        if (icon.getAttribute("disabled") === "false") {
            icon.setAttribute("disabled", "true");
            icon.classList.add("disable-icon-selected");
            icon.parentNode.parentNode.classList.add("disabled");
        } else {
            icon.setAttribute("disabled", "false");
            icon.classList.remove("disable-icon-selected");
            icon.parentNode.parentNode.classList.remove("disabled");
        }

        this.app.progress = 0;
        window.dispatchEvent(this.manager.statechange);
    }


    /**
     * Handler for breakpoint click events.
     * Updates the icon status.
     *
     * @fires Manager#statechange
     * @param {event} e
     */
    breakpointClick(e) {
        const bp = e.target;

        if (bp.getAttribute("break") === "false") {
            bp.setAttribute("break", "true");
            bp.classList.add("breakpoint-selected");
        } else {
            bp.setAttribute("break", "false");
            bp.classList.remove("breakpoint-selected");
        }

        window.dispatchEvent(this.manager.statechange);
    }


    /**
     * Handler for operation doubleclick events.
     * Removes the operation from the recipe and auto bakes.
     *
     * @fires Manager#statechange
     * @param {event} e
     */
    operationDblclick(e) {
        e.target.remove();
        this.opRemove(e);
    }


    /**
     * Handler for operation child doubleclick events.
     * Removes the operation from the recipe.
     *
     * @fires Manager#statechange
     * @param {event} e
     */
    operationChildDblclick(e) {
        e.target.parentNode.remove();
        this.opRemove(e);
    }


    /**
     * Generates a configuration object to represent the current recipe.
     *
     * @returns {recipeConfig}
     */
    getConfig() {
        const config = [];
        let ingredients, ingList, disabled, bp, item;
        const operations = document.querySelectorAll("#rec-list li.operation");

        for (let i = 0; i < operations.length; i++) {
            ingredients = [];
            disabled = operations[i].querySelector(".disable-icon");
            bp = operations[i].querySelector(".breakpoint");
            ingList = operations[i].querySelectorAll(".arg");

            for (let j = 0; j < ingList.length; j++) {
                if (ingList[j].getAttribute("type") === "checkbox") {
                    // checkbox
                    ingredients[j] = ingList[j].checked;
                } else if (ingList[j].classList.contains("toggle-string")) {
                    // toggleString
                    ingredients[j] = {
                        option: ingList[j].parentNode.parentNode.querySelector("button").textContent,
                        string: ingList[j].value
                    };
                } else if (ingList[j].getAttribute("type") === "number") {
                    // number
                    ingredients[j] = parseFloat(ingList[j].value);
                } else {
                    // all others
                    ingredients[j] = ingList[j].value;
                }
            }

            item = {
                op: operations[i].querySelector(".op-title").textContent,
                args: ingredients
            };

            if (disabled && disabled.getAttribute("disabled") === "true") {
                item.disabled = true;
            }

            if (bp && bp.getAttribute("break") === "true") {
                item.breakpoint = true;
            }

            config.push(item);
        }

        return config;
    }


    /**
     * Moves or removes the breakpoint indicator in the recipe based on the position.
     *
     * @param {number|boolean} position - If boolean, turn off all indicators
     */
    updateBreakpointIndicator(position) {
        const operations = document.querySelectorAll("#rec-list li.operation");
        if (typeof position === "boolean") position = operations.length;
        for (let i = 0; i < operations.length; i++) {
            if (i === position) {
                operations[i].classList.add("break");
            } else {
                operations[i].classList.remove("break");
            }
        }
    }


    /**
     * Given an operation stub element, this function converts it into a full recipe element with
     * arguments.
     *
     * @param {element} el - The operation stub element from the operations pane
     */
    buildRecipeOperation(el) {
        const opName = el.textContent;
        const op = new HTMLOperation(opName, this.app.operations[opName], this.app, this.manager);
        el.innerHTML = op.toFullHtml();

        if (this.app.operations[opName].flowControl) {
            el.classList.add("flow-control-op");
        }

        // Disable auto-bake if this is a manual op
        if (op.manualBake && this.app.autoBake_) {
            this.manager.controls.setAutoBake(false);
            this.app.alert("Auto-Bake is disabled by default when using this operation.", 5000);
        }
    }


    /**
     * Adds the specified operation to the recipe.
     *
     * @fires Manager#operationadd
     * @param {string} name - The name of the operation to add
     * @returns {element}
     */
    addOperation(name) {
        const item = document.createElement("li");

        item.classList.add("operation");
        item.innerHTML = name;
        this.buildRecipeOperation(item);
        document.getElementById("rec-list").appendChild(item);

        $(item).find("[data-toggle='tooltip']").tooltip();

        item.dispatchEvent(this.manager.operationadd);
        return item;
    }


    /**
     * Removes all operations from the recipe.
     *
     * @fires Manager#operationremove
     */
    clearRecipe() {
        const recList = document.getElementById("rec-list");
        while (recList.firstChild) {
            recList.removeChild(recList.firstChild);
        }
        recList.dispatchEvent(this.manager.operationremove);
    }


    /**
     * Handler for operation dropdown events from toggleString arguments.
     * Sets the selected option as the name of the button.
     *
     * @param {event} e
     */
    dropdownToggleClick(e) {
        e.stopPropagation();
        e.preventDefault();

        const el = e.target;
        const button = el.parentNode.parentNode.querySelector("button");

        button.innerHTML = el.textContent;
        this.ingChange();
    }


    /**
     * Triggers various change events for operation arguments that have just been initialised.
     *
     * @param {HTMLElement} op
     */
    triggerArgEvents(op) {
        // Trigger populateOption and argSelector events
        const triggerableOptions = op.querySelectorAll(".populate-option, .arg-selector");
        const evt = new Event("change", {bubbles: true});
        if (triggerableOptions.length) {
            for (const el of triggerableOptions) {
                el.dispatchEvent(evt);
            }
        }
    }


    /**
     * Handler for operationadd events.
     *
     * @listens Manager#operationadd
     * @fires Manager#statechange
     * @param {event} e
     */
    opAdd(e) {
        log.debug(`'${e.target.querySelector(".op-title").textContent}' added to recipe`);

        this.triggerArgEvents(e.target);
        window.dispatchEvent(this.manager.statechange);
    }


    /**
     * Handler for operationremove events.
     *
     * @listens Manager#operationremove
     * @fires Manager#statechange
     * @param {event} e
     */
    opRemove(e) {
        log.debug("Operation removed from recipe");
        window.dispatchEvent(this.manager.statechange);
    }


    /**
     * Handler for text argument dragover events.
     * Gives the user a visual cue to show that items can be dropped here.
     *
     * @param {event} e
     */
    textArgDragover (e) {
        // This will be set if we're dragging an operation
        if (e.dataTransfer.effectAllowed === "move")
            return false;

        e.stopPropagation();
        e.preventDefault();
        e.target.closest("textarea.arg").classList.add("dropping-file");
    }


    /**
     * Handler for text argument dragleave events.
     * Removes the visual cue.
     *
     * @param {event} e
     */
    textArgDragLeave (e) {
        e.stopPropagation();
        e.preventDefault();
        e.target.classList.remove("dropping-file");
    }


    /**
     * Handler for text argument drop events.
     * Loads the dragged data into the argument textarea.
     *
     * @param {event} e
     */
    textArgDrop(e) {
        // This will be set if we're dragging an operation
        if (e.dataTransfer.effectAllowed === "move")
            return false;

        e.stopPropagation();
        e.preventDefault();
        const targ = e.target;
        const file = e.dataTransfer.files[0];
        const text = e.dataTransfer.getData("Text");

        targ.classList.remove("dropping-file");

        if (text) {
            targ.value = text;
            return;
        }

        if (file) {
            const reader = new FileReader();
            const self = this;
            reader.onload = function (e) {
                targ.value = e.target.result;
                // Trigger floating label move
                const changeEvent = new Event("change");
                targ.dispatchEvent(changeEvent);
                window.dispatchEvent(self.manager.statechange);
            };
            reader.readAsText(file);
        }
    }


    /**
     * Sets register values.
     *
     * @param {number} opIndex
     * @param {number} numPrevRegisters
     * @param {string[]} registers
     */
    setRegisters(opIndex, numPrevRegisters, registers) {
        const op = document.querySelector(`#rec-list .operation:nth-child(${opIndex + 1})`),
            prevRegList = op.querySelector(".register-list");

        // Remove previous div
        if (prevRegList) prevRegList.remove();

        const registerList = [];
        for (let i = 0; i < registers.length; i++) {
            registerList.push(`$R${numPrevRegisters + i} = ${Utils.escapeHtml(Utils.truncate(Utils.printable(registers[i]), 100))}`);
        }
        const registerListEl = `<div class="register-list">
                ${registerList.join("<br>")}
            </div>`;

        op.insertAdjacentHTML("beforeend", registerListEl);
    }


    /**
     * Adjusts the number of ingredient columns as the width of the recipe changes.
     */
    adjustWidth() {
        const recList = document.getElementById("rec-list");

        if (!this.ingredientRuleID) {
            this.ingredientRuleID = null;
            this.ingredientChildRuleID = null;

            // Find relevant rules in the stylesheet
            // try/catch for chrome 64+ CORS error on cssRules.
            try {
                for (const i in document.styleSheets[0].cssRules) {
                    if (document.styleSheets[0].cssRules[i].selectorText === ".ingredients") {
                        this.ingredientRuleID = i;
                    }
                    if (document.styleSheets[0].cssRules[i].selectorText === ".ingredients > div") {
                        this.ingredientChildRuleID = i;
                    }
                }
            } catch (e) {
                // Do nothing.
            }
        }

        if (!this.ingredientRuleID || !this.ingredientChildRuleID) return;

        const ingredientRule = document.styleSheets[0].cssRules[this.ingredientRuleID];
        const ingredientChildRule = document.styleSheets[0].cssRules[this.ingredientChildRuleID];

        if (recList.clientWidth < 450) {
            ingredientRule.style.gridTemplateColumns = "auto auto";
            ingredientChildRule.style.gridColumn = "1 / span 2";
        } else if (recList.clientWidth < 620) {
            ingredientRule.style.gridTemplateColumns = "auto auto auto";
            ingredientChildRule.style.gridColumn = "1 / span 3";
        } else {
            ingredientRule.style.gridTemplateColumns = "auto auto auto auto";
            ingredientChildRule.style.gridColumn = "1 / span 4";
        }

        // Hide Chef icon on Bake button if the page is compressed
        const bakeIcon = document.querySelector("#bake img");

        if (recList.clientWidth < 370) {
            // Hide Chef icon on Bake button
            bakeIcon.style.display = "none";
        } else {
            bakeIcon.style.display = "inline-block";
        }

        // Scale controls to fit pane width
        const controls = document.getElementById("controls");
        const controlsContent = document.getElementById("controls-content");
        const scale = (controls.clientWidth - 1) / controlsContent.scrollWidth;

        controlsContent.style.transform = `translate(-50%, -50%) scale(${scale})`;
    }

}

export default RecipeWaiter;
