/**
 * Initialize the touchkey setup.
 *
 * Requires an empty SVG container, and a HTML input tag (recommended: type="hidden") to write the string representation
 * to.  Can additionally be provided with the ID of a HTML form which should be auto-submitted after touchkey entry.
 *
 * Example:
 *
 * <form id="touchkey-form" method="post" action="/login">
 *     <svg id="touchkey-svg"></svg>
 *     <input type="hidden" name="touchkey" id="touchkey-field" />
 * </form>
 * <script>
 *     initTouchkey(false, "touchkey-svg", "touchkey-form", "touchkey-field");
 * </script>
 *
 * @param {boolean} keepPattern: Whether to keep the pattern on screen, or to clear it after the end event.
 * @param {string} svgid: HTML id of the SVG container the touchkey is drawn in.
 * @param {string} formid: HTML id of the form that should be submitted after touchkey entry. null to disable
 *                         auto-submit.
 * @param {string} formfieldid: ID of the input object that should receive the entered touchkey as its value.
 */
initTouchkey = (keepPattern, svgid, formid, formfieldid) => {

    // Define forEach (iteration) and slice (abused for shallow copying) on HTMLCollections (reuse the Array methods)
    HTMLCollection.prototype.forEach = Array.prototype.forEach;
    HTMLCollection.prototype.slice = Array.prototype.slice;

    // Max. distance to a nodes center, before the path snaps to the node.
    // Expressed as inverse ratio of the container width
    const SNAPPING_SENSITIVITY = 12;

    // Get the DOM objects for the SVG, the form and the input field
    let svg = document.getElementById(svgid);
    let form;
    if (formid !== null) {
        form = document.getElementById(formid);
    }
    let formfield = document.getElementById(formfieldid);

    // Reference to the SVG line object that's currently being drawn by the user, or null, if none
    let currentStroke = null;
    // ID generator for the SVG line objects drawn by the user
    let strokeId = 0;
    // Set of touchkey pattern nodes that have already been visited by a users pattern, and may not be reused again
    let doneMap = {};
    // The string representation of the touchkey entered by the user.
    let enteredKey = '';

    /**
     * Helper function to create a new stroke after the user completed one stroke by connecting two pattern nodes.
     *
     * @param {number} fromX: X coordinate of the starting point of this line.
     * @param {number} fromY: Y coordinate of the starting point of this line.
     * @param {number} toX: X coordinate of the ending point of this line.
     * @param {number} toY: Y coordinate of the ending point of this line.
     * @returns {string} The ID of the generated line object.
     */
    let drawLine = (fromX, fromY, toX, toY) => {
        // Create a new SVG line object
        let line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
        // Generate and set an unique ID for the line object
        let id = 'touchkey-svg-stroke-' + (strokeId++);
        line.setAttribute('id', id);
        // Create and set the HTML class attribute
        line.setAttribute('class', 'touchkey-svg-stroke');
        // Create and set the coordinate attributes
        line.setAttribute('x1', fromX.toString());
        line.setAttribute('y1', fromY.toString());
        line.setAttribute('x2', toX.toString());
        line.setAttribute('y2', toY.toString());
        // Create and set the style attribute (grey, circular ends, 5% width)
        line.setAttribute('style', 'stroke: grey; stroke-width: 5%; stroke-linecap: round');
        // Add the line to the SVG
        svg.appendChild(line);
        // Return the previously generated ID to identify the line object by
        return id;
    };

    /**
     * Helper function used to remove the "trailing stroke" (i.e. after the user let go, there is a dangling stroke from
     * the node that was hit last to the mouse pointer/finger).
     */
    let endPath = () => {
        // Remove the current stroke ...
        svg.removeChild(svg.getElementById(currentStroke));
        // ... and set its reference to null
        currentStroke = null;
    };

    /**
     * Helper function used to clear the touchkey pattern drawn by the user.
     */
    let clearTouchkey = () => {
        // Reset the set of visited pattern nodes
        doneMap = {};
        // Reset the touchkey string representation
        enteredKey = '';
        // Remove all line objects. Create a shallow copy of the list first to retain deletion order.
        svg.getElementsByClassName('touchkey-svg-stroke').slice().forEach((line) => {
            svg.removeChild(line);
        });
    };

    /**
     * Helper function to read and convert the event coordinates of a MouseEvent or TouchEvent to coordinates relative
     * to the SVG's origin.
     *
     * @param {(MouseEvent|TouchEvent)} ev: The event to get the X and Y coordinates from.
     * @param {(ClientRect|DOMRect)} svgrect: Bounds rectangle of the SVG container.
     * @returns {Array} The X and Y coordinates relative to the SVG's origin.
     */
    let getEventCoordinates = (ev, svgrect) => {
        // Check for existence of the "touches" property to distinguish between touch and mouse events
        // For a touch event, take the page coordinates of the first touch
        // For a mouse event, take the event coordinates
        // Then subtract the SVG's origin from the page-relative coordinates to obtain the translated coordinates
        const trX = (typeof ev.touches !== 'undefined' ? ev.touches[0].pageX : ev.x) - svgrect.left;
        const trY = (typeof ev.touches !== 'undefined' ? ev.touches[0].pageY : ev.y) - svgrect.top;
        return [trX, trY]
    };

    /**
     * Find the pattern node closest to a coordinate.
     *
     * @param {number} evX: X coordinate of the point to search the closest node for.
     * @param {number} evY: Y coordinate of the point to search the closest node for.
     * @param {(ClientRect|DOMRect)} svgrect: Bounds rectangle of the SVG container.
     * @returns {Array} The node's ID, the distance, the X and Y coordinate of the node's center.
     */
    let getNearestPatternNode = (evX, evY, svgrect) => {
        // Initialize nearest neighbors search variables
        let minId = '';
        let minDist2 = Infinity;  // Squared distance
        let minX = 0;
        let minY = 0;
        // Iterate the pattern nodes for nearest neighbors search
        document.getElementsByClassName('touchkey-svg-node').forEach((node) => {
            // Get the position of a node's center, converted from ratio into absolute pixel count
            let x = parseFloat(node.getAttribute('cx')) / 100.0 * svgrect.width;
            let y = parseFloat(node.getAttribute('cy')) / 100.0 * svgrect.height;
            // Compute the squared distance from the event coordinate to the node's center
            let dist2 = Math.pow(evX - x, 2) + Math.pow(evY - y, 2);
            // Keep the properties of the closest node
            if (dist2 < minDist2) {
                minDist2 = dist2;
                minId = node.dataset.stringRepresentation;
                minX = x;
                minY = y;
            }
        });

        return [minId, Math.sqrt(minDist2), minX, minY];
    };

    /**
     * Event handler for "mouse down" / "touch down" events.
     *
     * Selects an "anchor node", i.e. the node where the pattern path started, and draws a line from there to the event
     * coordinates.
     */
    svg.ontouchstart = svg.onmousedown = (ev) => {
        // Remove any previous strokes that may still be there if "keepPattern" was set to true in the init call
        clearTouchkey();
        // Get the SVG container's rectangle
        const svgrect = svg.getBoundingClientRect();
        // Get the event coordinates relative to the SVG container's origin
        const [trX, trY] = getEventCoordinates(ev, svgrect);
        // Get the closest pattern node
        const [minId, _, minX, minY] = getNearestPatternNode(trX, trY, svgrect);
        // Create the line from the anchor node to the event position
        currentStroke = drawLine(minX, minY, trX, trY);
        // Mark the anchor node as visited
        doneMap[minId] = 1;
        // Add the anchor node's string representation to the touchkey string representation
        enteredKey += minId;
    };

    /**
     * Event handler for "mouse move" / "touch move" events.
     */
    svg.ontouchmove = svg.onmousemove = (ev) => {
        // Only act if the user started is drawing a pattern (only relevant for mouse input)
        if (currentStroke != null) {
            // Get the SVG container's rectangle
            const svgrect = svg.getBoundingClientRect();
            // Get the event coordinates relative to the SVG container's origin
            const [trX, trY] = getEventCoordinates(ev, svgrect);
            // Get the closest pattern node
            const [minId, minDist, minX, minY] = getNearestPatternNode(trX, trY, svgrect);
            // If the closest node is not visited yet, and the event coordinate is less than  from the node's
            // center, snap the current stroke to the node, and create a new stroke starting from this node
            if (minDist < (svgrect.width / SNAPPING_SENSITIVITY) && !(minId in doneMap)) {
                // Snap the current stroke to the node
                let line = svg.getElementById(currentStroke);
                line.setAttribute('x2', minX.toString());
                line.setAttribute('y2', minY.toString());
                // Create a new line object from the closest node to the event position
                currentStroke = drawLine(minX, minY, trX, trY);
                // Mark the closest node as visited
                doneMap[minId] = 1;
                // Append its string representation to the touchkey string representation
                enteredKey += minId;
            } else {
                // If the stroke was not snapped to the closest node, update its end position
                let line = svg.getElementById(currentStroke);
                line.setAttribute('x2', trX);
                line.setAttribute('y2', trY);
            }
        }
    };

    /**
     * Event handler for "mouse up" / "touch end" events.
     *
     * Sets the input object value, and optionally clears the SVG path and submits the form.
     */
    svg.ontouchend = svg.onmouseup = () => {
        // Remove the trailing, unfinished stroke
        endPath();
        // Write the touchkey string representation to the input field
        formfield.value = enteredKey;
        // Erase the touchkey pattern, if requested in the init call
        if (keepPattern !== true) {
            clearTouchkey();
        }
        // Submit the HTML form, if requested in the init call
        if (formid !== null) {
            form.submit();
        }
    };

    /*
     * Create the SVG touchkey nodes
     */

    // Node ID provider
    let touchkey_node_counter = 0;
    // Percentages for centers of quarters of the container's width and height
    ['12.5%', '37.5%', '62.5%', '87.5%'].forEach((y) => {
        ['12.5%', '37.5%', '62.5%', '87.5%'].forEach((x) => {
            // Create a new pattern node
            let node = document.createElementNS('http://www.w3.org/2000/svg', 'circle');

            // Generate a new ID (and the touchkey string representation from it)
            let id = touchkey_node_counter++;
            node.dataset.stringRepresentation = id.toString(16).toLowerCase();

            // Class
            node.setAttribute('class', 'touchkey-svg-node');
            // Center
            node.setAttribute('cx', x);
            node.setAttribute('cy', y);
            // Radius
            node.setAttribute('r', '10%');
            // Center color
            node.setAttribute('fill', 'white');
            // Circle color
            node.setAttribute('stroke', 'grey');
            // Circle width
            node.setAttribute('stroke-width', '2%');

            // Add the node to the SVG container
            svg.appendChild(node);
        });
    });

};