/**
 * Library of caret movement computations.
 * @author Louis-Dominique Dubeau
 * @license MPL 2.0
 * @copyright Mangalam Research Center for Buddhist Languages
 */
define(["require", "exports", "./domtypeguards", "./domutil", "./wed-util"], function (require, exports, domtypeguards_1, domutil_1, wed_util_1) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    function moveInAttributes(node, modeTree) {
        return modeTree.getAttributeHandling(node) === "edit";
    }
    /**
     * @param pos The position form which we start.
     *
     * @param root The root of the DOM tree within which we move.
     *
     * @param after Whether we are to move after the placeholder (``true``) or not
     * (``false``).
     *
     * @returns If called with a position inside a placeholder, return a position
     * outside of the placeholder. Otherwise, return the position unchanged.
     */
    function moveOutOfPlaceholder(pos, root, after) {
        // If we are in a placeholder node, immediately move out of it.
        const closestPh = domutil_1.closestByClass(pos.node, "_placeholder", root);
        if (closestPh !== null) {
            const parent = closestPh.parentNode;
            let index = domutil_1.indexOf(parent.childNodes, closestPh);
            if (after) {
                index++;
            }
            pos = pos.make(parent, index);
        }
        return pos;
    }
    /**
     * Determines what should be used as the "container" for caret movement
     * purposes. The "container" is the element within which caret movements are
     * constrained. (The caret cannot move out of it.)
     *
     * @param docRoot The root element of the document being edited by wed.
     *
     * @returns A container that can be used by the caret movement functions.
     */
    function determineContainer(docRoot) {
        let container = docRoot.firstChild;
        if (!domtypeguards_1.isElement(container)) {
            throw new Error("docRoot does not contain an element");
        }
        // This takes care of the special case where we have an empty document that
        // contains only a placeholder. In such case, setting the container to
        // docRoot.firstChild will have a perverse effect of setting the container to
        // be **inside** the current pos.
        if (container.classList.contains("_placeholder")) {
            container = docRoot;
        }
        return container;
    }
    /**
     * Determine whether a position is within the editable content of an element or
     * outside of it. Modes often decorate elements by adding decorations before and
     * after the content of the element. These are not editable, and should be
     * skipped by caret movement.
     *
     * @param element The element in which the caret is appearing.
     *
     * @param offset The offset into the element at which the caret is positioned.
     *
     * @param modeTree The mode tree from which to get a mode.
     *
     * @returns ``true`` if we are inside editable content, ``false`` otherwise.
     */
    function insideEditableContent(element, offset, modeTree) {
        const mode = modeTree.getMode(element);
        const [before, after] = mode.nodesAroundEditableContents(element);
        // If the element has nodes before editable contents and the caret would
        // be before or among such nodes, then ...
        if (before !== null && domutil_1.indexOf(element.childNodes, before) >= offset) {
            return false;
        }
        // If the element has nodes after editable contents and the caret would be
        // after or among such nodes, then ...
        if (after !== null && domutil_1.indexOf(element.childNodes, after) < offset) {
            return false;
        }
        return true;
    }
    /**
     * @returns ``true`` if ``prev`` and ``next`` are both decorated; ``false``
     * otherwise.
     */
    function bothDecorated(prev, next) {
        if (next === undefined || prev === undefined) {
            return false;
        }
        const nextFirst = next.firstChild;
        const prevLast = prev.lastChild;
        return domtypeguards_1.isElement(nextFirst) &&
            nextFirst.classList.contains("_gui") &&
            !nextFirst.classList.contains("_invisible") &&
            domtypeguards_1.isElement(prevLast) &&
            prevLast.classList.contains("_gui") &&
            !prevLast.classList.contains("_invisible");
    }
    /**
     * Find the first node in a set of nodes which is such that the reference node
     * **precedes** it.
     *
     * @param haystack The nodes to search.
     *
     * @param ref The reference node.
     *
     * @returns The first node in ``haystack`` which does not precede ``ref``.
     */
    function findNext(haystack, ref) {
        const arr = Array.prototype.slice.call(haystack);
        for (const x of arr) {
            // tslint:disable-next-line:no-bitwise
            if ((x.compareDocumentPosition(ref) &
                Node.DOCUMENT_POSITION_PRECEDING) !== 0) {
                return x;
            }
        }
        return undefined;
    }
    const directionToFunction = {
        right: positionRight,
        left: positionLeft,
        up: positionUp,
        down: positionDown,
    };
    function newPosition(pos, direction, docRoot, modeTree) {
        const fn = directionToFunction[direction];
        if (fn === undefined) {
            throw new Error(`cannot resolve direction: ${direction}`);
        }
        return fn(pos, docRoot, modeTree);
    }
    exports.newPosition = newPosition;
    /**
     * Compute the position to the right of a starting position. This function takes
     * into account wed-specific needs. For instance, it knows how start and end
     * labels are structured.
     *
     * @param pos The position at which we start.
     *
     * @param docRoot The element within which caret movement is to be constrained.
     *
     * @param modeTree The mode tree from which to get a mode.
     *
     * @returns The new position, or ``undefined`` if there is no such position.
     */
    // tslint:disable-next-line:cyclomatic-complexity max-func-body-length
    function positionRight(pos, docRoot, modeTree) {
        if (pos == null) {
            return undefined;
        }
        const root = pos.root;
        // If we are in a placeholder node, immediately move out of it.
        pos = moveOutOfPlaceholder(pos, root, true);
        const container = determineContainer(docRoot);
        // tslint:disable-next-line:strict-boolean-expressions no-constant-condition
        while (true) {
            const guiBefore = domutil_1.closestByClass(pos.node, "_gui", root);
            const nextCaret = domutil_1.nextCaretPosition(pos.toArray(), container, false);
            if (nextCaret === null) {
                pos = null;
                break;
            }
            pos = pos.make(nextCaret);
            const { node, offset } = pos;
            const closestGUI = domutil_1.closest(node, "._gui:not(._invisible)", root);
            if (closestGUI !== null) {
                const startLabel = closestGUI.classList.contains("__start_label");
                if (startLabel &&
                    moveInAttributes(domutil_1.closestByClass(closestGUI, "_real", root), modeTree)) {
                    if (domutil_1.closestByClass(node, "_attribute_value", root) !== null) {
                        // We're in an attribute value, stop here.
                        break;
                    }
                    // Already in the element name, or in a previous attribute, move from
                    // attribute to attribute.
                    if (domutil_1.closest(node, "._element_name, ._attribute", root) !== null) {
                        // Search for the next attribute.
                        const nextAttr = findNext(closestGUI.getElementsByClassName("_attribute"), node);
                        if (nextAttr !== undefined) {
                            // There is a next attribute: move to it.
                            const val = wed_util_1.getAttrValueNode(domutil_1.childByClass(nextAttr, "_attribute_value"));
                            pos = pos.make(val, 0);
                            break;
                        }
                    }
                    // else fall through and move to end of gui element.
                }
                if (guiBefore === closestGUI) {
                    // Move to the end of the gui element ...
                    pos = pos.make(closestGUI, closestGUI.childNodes.length);
                    // ... and then out of it.
                    continue;
                }
                pos = pos.make(
                // If in a label, normalize to element name. If in another kind of gui
                // element, normalize to start of the element.
                (startLabel || domutil_1.closestByClass(node, "_label", closestGUI) !== null) ?
                    node.getElementsByClassName("_element_name")[0] :
                    closestGUI, 0);
                // ... stop here.
                break;
            }
            // Can't stop inside a phantom node.
            const closestPhantom = domutil_1.closestByClass(node, "_phantom", root);
            if (closestPhantom !== null) {
                // This ensures the next loop will move after the phantom.
                pos = pos.make(closestPhantom, closestPhantom.childNodes.length);
                continue;
            }
            // Or beyond the first position in a placeholder node.
            const closestPh = domutil_1.closestByClass(node, "_placeholder", root);
            if (closestPh !== null && offset > 0) {
                // This ensures the next loop will move after the placeholder.
                pos = pos.make(closestPh, closestPh.childNodes.length);
                continue;
            }
            // Make sure the position makes sense from an editing standpoint.
            if (domtypeguards_1.isElement(node)) {
                const nextNode = node.childNodes[offset];
                // Always move into text
                if (domtypeguards_1.isText(nextNode)) {
                    continue;
                }
                const prevNode = node.childNodes[offset - 1];
                // Stop between two decorated elements.
                if (bothDecorated(prevNode, nextNode)) {
                    break;
                }
                if (domtypeguards_1.isElement(prevNode) &&
                    // We do not stop in front of element nodes.
                    ((domtypeguards_1.isElement(nextNode) &&
                        !nextNode.classList.contains("_end_wrapper") &&
                        !prevNode.classList.contains("_start_wrapper")) ||
                        prevNode.matches("._wed-validation-error, ._gui.__end_label"))) {
                    // can't stop here
                    continue;
                }
                // If the offset is not inside the editable content of the node, then...
                if (!insideEditableContent(node, offset, modeTree)) {
                    // ... can't stop here.
                    continue;
                }
            }
            // If we get here, the position is good!
            break;
        }
        return pos !== null ? pos : undefined;
    }
    exports.positionRight = positionRight;
    /**
     * Compute the position to the left of a starting position. This function takes
     * into account wed-specific needs. For instance, it knows how start and end
     * labels are structured.
     *
     * @param pos The position at which we start.
     *
     * @param docRoot The element within which caret movement is to be constrained.
     *
     * @param modeTree The mode tree from which to get a mode.
     *
     * @returns The new position, or ``undefined`` if there is no such position.
     */
    // tslint:disable-next-line:cyclomatic-complexity max-func-body-length
    function positionLeft(pos, docRoot, modeTree) {
        if (pos == null) {
            return undefined;
        }
        const root = pos.root;
        // If we are in a placeholder node, immediately move out of it.
        pos = moveOutOfPlaceholder(pos, root, false);
        const container = determineContainer(docRoot);
        // tslint:disable-next-line:strict-boolean-expressions no-constant-condition
        while (true) {
            let elName = domutil_1.closestByClass(pos.node, "_element_name", root);
            const wasInName = (pos.node === elName) && (pos.offset === 0);
            const prevCaret = domutil_1.prevCaretPosition(pos.toArray(), container, false);
            if (prevCaret === null) {
                pos = null;
                break;
            }
            pos = pos.make(prevCaret);
            const node = pos.node;
            let offset = pos.offset;
            const closestGUI = domutil_1.closest(node, "._gui:not(._invisible)", root);
            if (closestGUI !== null) {
                const startLabel = closestGUI.classList.contains("__start_label");
                if (startLabel && !wasInName &&
                    moveInAttributes(domutil_1.closestByClass(closestGUI, "_real", root), modeTree)) {
                    if (domutil_1.closestByClass(node, "_attribute_value", closestGUI) !== null) {
                        // We're in an attribute value, stop here.
                        break;
                    }
                    let attr = domutil_1.closestByClass(node, "_attribute", closestGUI);
                    if (attr === null &&
                        domtypeguards_1.isElement(node) &&
                        node.nextElementSibling !== null &&
                        node.nextElementSibling.classList.contains("_attribute")) {
                        attr = node.nextElementSibling;
                    }
                    if (attr === null) {
                        elName = domutil_1.closestByClass(node, "_element_name", closestGUI);
                        attr = elName !== null ? elName.nextElementSibling : null;
                    }
                    let prevAttr = attr !== null ? attr.previousElementSibling : null;
                    // If we have not yet found anything, then the
                    // previous attribute is the last one.
                    if (prevAttr === null) {
                        const all = closestGUI.getElementsByClassName("_attribute");
                        if (all.length > 0) {
                            prevAttr = all[all.length - 1];
                        }
                    }
                    // Eliminate those elements which are not attributes.
                    if (prevAttr !== null && !prevAttr.classList.contains("_attribute")) {
                        prevAttr = null;
                    }
                    if (prevAttr !== null) {
                        // There is a previous attribute: move to it.
                        let val = domutil_1.childByClass(prevAttr, "_attribute_value");
                        offset = 0;
                        if (val.lastChild !== null) {
                            val = val.lastChild;
                            if (domtypeguards_1.isElement(val) && val.classList.contains("_placeholder")) {
                                offset = 0;
                            }
                            else if (domtypeguards_1.isText(val)) {
                                offset = val.length;
                            }
                            else {
                                throw new Error("unexpected content in attribute value");
                            }
                        }
                        pos = pos.make(val, offset);
                        break;
                    }
                }
                if (!wasInName) {
                    pos = pos.make(
                    // If we are in any label, normalize to the element name, otherwise
                    // normalize to the first position in the gui element.
                    (startLabel ||
                        domutil_1.closestByClass(node, "_label", closestGUI) !== null) ?
                        closestGUI.getElementsByClassName("_element_name")[0]
                        : closestGUI, 0);
                    break;
                }
                // ... move to start of gui element ...
                pos = pos.make(closestGUI, 0);
                // ... and then out of it.
                continue;
            }
            const closestPh = domutil_1.closestByClass(node, "_placeholder", root);
            if (closestPh !== null) {
                // Stopping in a placeholder is fine, but normalize the position to the
                // start of the text.
                pos = pos.make(closestPh.firstChild, 0);
                break;
            }
            // Can't stop inside a phantom node.
            const closestPhantom = domutil_1.closestByClass(node, "_phantom", root);
            if (closestPhantom !== null) {
                // Setting the position to this will ensure that on the next loop we move
                // to the left of the phantom node.
                pos = pos.make(closestPhantom, 0);
                continue;
            }
            // Make sure the position makes sense from an editing standpoint.
            if (domtypeguards_1.isElement(node)) {
                const prevNode = node.childNodes[offset - 1];
                // Always move into text
                if (domtypeguards_1.isText(prevNode)) {
                    continue;
                }
                const nextNode = node.childNodes[offset];
                // Stop between two decorated elements.
                if (bothDecorated(prevNode, nextNode)) {
                    break;
                }
                if (domtypeguards_1.isElement(nextNode) &&
                    // We do not stop just before a start tag button.
                    ((domtypeguards_1.isElement(prevNode) &&
                        !prevNode.classList.contains("_start_wrapper") &&
                        !nextNode.classList.contains("_end_wrapper")) ||
                        // Can't stop right before a validation error.
                        nextNode.matches("._gui.__start_label, .wed-validation-error"))) {
                    continue;
                } // can't stop here
                // If the offset is not inside the editable content of the node, then...
                if (!insideEditableContent(node, offset, modeTree)) {
                    // ... can't stop here.
                    continue;
                }
            }
            // If we get here, the position is good!
            break;
        }
        return pos !== null ? pos : undefined;
    }
    exports.positionLeft = positionLeft;
    /**
     * Compute the position under a starting position. This function takes into
     * account wed-specific needs. For instance, it knows how start and end labels
     * are structured.
     *
     * @param pos The position at which we start.
     *
     * @param docRoot The element within which caret movement is to be constrained.
     *
     * @param modeTree The mode tree from which to get a mode.
     *
     * @returns The new position, or ``undefined`` if there is no such position.
     */
    function positionDown(pos, docRoot, modeTree) {
        if (pos == null) {
            return undefined;
        }
        // Search for the next line.
        const initialCaret = wed_util_1.boundaryXY(pos);
        let next = initialCaret;
        while (initialCaret.bottom > next.top) {
            pos = positionRight(pos, docRoot, modeTree);
            if (pos === undefined) {
                return undefined;
            }
            next = wed_util_1.boundaryXY(pos);
        }
        // pos is now at the start of the next line. We need to find the position that
        // is closest horizontally.
        const nextBottom = next.bottom;
        let minDist = Infinity;
        let minPosition;
        while (pos !== undefined) {
            const dist = Math.abs(next.left - initialCaret.left);
            // We've started moving away from the minimum distance.
            if (dist > minDist) {
                break;
            }
            // We've moved to yet another line. The minimum we have so far is *it*.
            if (nextBottom <= next.top) {
                break;
            }
            minDist = dist;
            minPosition = pos;
            pos = positionRight(pos, docRoot, modeTree);
            if (pos !== undefined) {
                next = wed_util_1.boundaryXY(pos);
            }
        }
        return minPosition;
    }
    exports.positionDown = positionDown;
    /**
     * Compute the position above a starting position. This function takes into
     * account wed-specific needs. For instance, it knows how start and end labels
     * are structured.
     *
     * @param pos The position at which we start.
     *
     * @param docRoot The element within which caret movement is to be constrained.
     *
     * @param modeTree The mode tree from which to get a mode.
     *
     * @returns The new position, or ``undefined`` if there is no such position.
     */
    function positionUp(pos, docRoot, modeTree) {
        if (pos == null) {
            return undefined;
        }
        // Search for the previous line.
        const initialBoundary = wed_util_1.boundaryXY(pos);
        let prev = initialBoundary;
        while (initialBoundary.top < prev.bottom) {
            pos = positionLeft(pos, docRoot, modeTree);
            if (pos === undefined) {
                return undefined;
            }
            prev = wed_util_1.boundaryXY(pos);
        }
        // pos is now at the end of the previous line. We need to find the position
        // that is closest horizontally.
        const prevTop = prev.top;
        let minDist = Infinity;
        let minPosition;
        while (pos !== undefined) {
            const dist = Math.abs(prev.left - initialBoundary.left);
            // We've started moving away from the minimum distance.
            if (dist > minDist) {
                break;
            }
            // We've moved to yet another line. The minimum we have so far is *it*.
            if (prev.bottom <= prevTop) {
                break;
            }
            minDist = dist;
            minPosition = pos;
            pos = positionLeft(pos, docRoot, modeTree);
            if (pos !== undefined) {
                prev = wed_util_1.boundaryXY(pos);
            }
        }
        return minPosition;
    }
    exports.positionUp = positionUp;
});
//  LocalWords:  docRoot firstChild pos
//# sourceMappingURL=caret-movement.js.map