"use strict";
/**
 * Copyright (c) Haiku 2016-2018. All rights reserved.
 */
var __extends = (this && this.__extends) || (function () {
    var extendStatics = Object.setPrototypeOf ||
        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var __assign = (this && this.__assign) || Object.assign || function(t) {
    for (var s, i = 1, n = arguments.length; i < n; i++) {
        s = arguments[i];
        for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
            t[p] = s[p];
    }
    return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
var allSvgElementNames_1 = require("../../helpers/allSvgElementNames");
var HaikuBase_1 = require("./../../HaikuBase");
var applyLayout_1 = require("./applyLayout");
var assignAttributes_1 = require("./assignAttributes");
var cloneVirtualElement_1 = require("./cloneVirtualElement");
var createRightClickMenu_1 = require("./createRightClickMenu");
var createSvgElement_1 = require("./createSvgElement");
var createTextNode_1 = require("./createTextNode");
var getElementSize_1 = require("./getElementSize");
var getFlexId_1 = require("./getFlexId");
var getLocalDomEventPosition_1 = require("./getLocalDomEventPosition");
var getTypeAsString_1 = require("./getTypeAsString");
var isBlankString_1 = require("./isBlankString");
var isTextNode_1 = require("./isTextNode");
var mixpanelInit_1 = require("./mixpanelInit");
var normalizeName_1 = require("./normalizeName");
var removeElement_1 = require("./removeElement");
var replaceElementWithText_1 = require("./replaceElementWithText");
var shouldElementBeReplaced_1 = require("./shouldElementBeReplaced");
var connectTarget = function (virtualNode, domElement) {
    if (virtualNode && typeof virtualNode === 'object') {
        // A virtual node can have multiple targets in the DOM due to an implementation
        // detail in the Haiku editing environment; FIXME
        if (!virtualNode.__targets) {
            virtualNode.__targets = [];
        }
        if (virtualNode.__targets.indexOf(domElement) === -1) {
            virtualNode.__targets.push(domElement);
        }
    }
};
var shouldListenerReceiveEvent = function (name, event, match) {
    // Since we subscribe to events from the root element, we rewrite these as 'mouseover' and 'mouseout'
    // so we can adequately capture "bubbled" enters/leaves to meet the 99% expectation of users. Then
    // we mimic the bubbling logic here so the semantics align with normal DOM semantics
    if (name === 'mouseenter' || name === 'mouseleave') {
        return (match === event.target &&
            ( // Don't fire if the user's mouse went from inside the child into the target
            !match.contains ||
                !match.contains(event.relatedTarget)));
    }
    return (match === event.target ||
        (match.contains && match.contains(event.target)));
};
// tslint:disable:variable-name
var HaikuDOMRenderer = /** @class */ (function (_super) {
    __extends(HaikuDOMRenderer, _super);
    function HaikuDOMRenderer(mount, config) {
        var _this = _super.call(this) || this;
        _this.mount = mount;
        _this.user = {
            mouse: {
                x: 0,
                y: 0,
                down: 0,
                buttons: [0, 0, 0],
            },
            keys: {},
            touches: [],
            mouches: [],
        };
        _this.config = config;
        _this.shouldCreateContainer = true;
        // Ensure we don't conflict with existing content in the mount
        _this.clear();
        return _this;
    }
    HaikuDOMRenderer.prototype.clear = function () {
        if (this.mount) {
            while (this.mount.firstChild) {
                this.mount.removeChild(this.mount.firstChild);
            }
        }
    };
    HaikuDOMRenderer.prototype.render = function (virtualContainer, virtualTree, component) {
        return HaikuDOMRenderer.renderTree(this.mount, virtualContainer, [virtualTree], component, false, // isPatchOperation
        false);
    };
    HaikuDOMRenderer.prototype.patch = function (component, patches) {
        // The component upstream may use an empty value to indicate a no-op
        if (!patches || Object.keys(patches).length < 1) {
            return;
        }
        for (var flexId in patches) {
            var virtualElement = patches[flexId];
            if (virtualElement.__targets) {
                for (var i = 0; i < virtualElement.__targets.length; i++) {
                    var target = virtualElement.__targets[i];
                    if (target.parentNode) {
                        HaikuDOMRenderer.updateElement(target, virtualElement, target.parentNode, virtualElement.__parent, component, true);
                    }
                }
            }
        }
    };
    HaikuDOMRenderer.prototype.menuize = function (component) {
        return createRightClickMenu_1.default(this.mount, component);
    };
    HaikuDOMRenderer.prototype.mixpanel = function (mixpanelToken, component) {
        return mixpanelInit_1.default(mixpanelToken, component);
    };
    HaikuDOMRenderer.prototype.hasSizing = function () {
        return this.config && this.config.sizing && this.config.sizing !== 'normal';
    };
    HaikuDOMRenderer.prototype.getZoom = function () {
        return ((this.config && this.config.zoom) || 1.0);
    };
    HaikuDOMRenderer.prototype.getPan = function () {
        return {
            x: (this.config && this.config.pan && this.config.pan.x) || 0,
            y: (this.config && this.config.pan && this.config.pan.y) || 0,
        };
    };
    HaikuDOMRenderer.prototype.createContainer = function (out) {
        if (out === void 0) { out = {}; }
        var size;
        if (this.mount) {
            size = getElementSize_1.default(this.mount);
        }
        else {
            console.warn('[haiku dom renderer] mount empty; using fallback size');
            size = { x: 1, y: 1 };
        }
        out.layout = {
            computed: {
                size: size,
            },
        };
        if (!this.hasSizing() || !this.config.alwaysComputeSizing) {
            this.shouldCreateContainer = false;
        }
        return out;
    };
    HaikuDOMRenderer.prototype.initialize = function () {
        var _this = this;
        var user = this.user;
        var setMouse = function (mouseEvent) {
            // Since the mount itself might be subject to sizing styles that we cannot
            // use to calculate appropriately, use the top element i.e. the 'artboard'
            var topElement = _this.mount.childNodes[0];
            if (topElement) {
                var pos = getLocalDomEventPosition_1.default(mouseEvent, topElement);
                user.mouse.x = pos.x;
                user.mouse.y = pos.y;
            }
        };
        var setTouches = function (touchEvent) {
            user.touches.splice(0);
            // Since the mount itself might be subject to sizing styles that we cannot
            // use to calculate appropriately, use the top element i.e. the 'artboard'
            var topElement = _this.mount.childNodes[0];
            if (topElement) {
                for (var i = 0; i < touchEvent.touches.length; i++) {
                    var touch = touchEvent.touches[i];
                    var pos = getLocalDomEventPosition_1.default(touch, topElement);
                    user.touches.push(pos);
                }
            }
        };
        var setMouches = function () {
            user.mouches.splice(0);
            // Only treat a mouse like a touch if it is down.
            if (user.mouse.down) {
                user.mouches.push(user.mouse);
            }
            user.mouches.push.apply(user.mouches, user.touches);
        };
        var clearKey = function () {
            for (var which in user.keys) {
                user.keys[which] = 0;
            }
        };
        var clearMouse = function () {
            user.mouse.down = 0;
            user.touches.splice(0);
            for (var i = 0; i < user.mouse.buttons.length; i++) {
                user.mouse.buttons[i] = 0;
            }
        };
        var clearMouch = function () {
            user.mouches.splice(0);
        };
        var clearTouch = function () {
            user.touches.splice(0);
        };
        var doc = this.mount.ownerDocument;
        var win = doc.defaultView || doc.parentWindow;
        this.mount.addEventListener('mousedown', function (mouseEvent) {
            ++user.mouse.down;
            ++user.mouse.buttons[mouseEvent.button];
            setMouse(mouseEvent);
            setMouches();
        });
        this.mount.addEventListener('mouseup', function (mouseEvent) {
            clearMouse();
            clearMouch();
            setMouches();
        });
        // NOTE: if there are perf or interop issues that arise from
        //       attaching event listeners directly to host window,
        //       could expose a haikuOption for reverting to "attach to mount" behavior
        win.addEventListener('mousemove', function (mouseEvent) {
            setMouse(mouseEvent);
            setMouches();
        });
        win.addEventListener('mouseenter', function (mouseEvent) {
            clearMouse();
            clearMouch();
        });
        win.addEventListener('mouseleave', function (mouseEvent) {
            clearMouse();
            clearMouch();
        });
        this.mount.addEventListener('wheel', function (mouseEvent) {
            setMouse(mouseEvent);
            setMouches();
        }, {
            passive: true,
        });
        doc.addEventListener('keydown', function (keyEvent) {
            if (user.keys[keyEvent.which] === undefined) {
                user.keys[keyEvent.which] = 0;
            }
            ++user.keys[keyEvent.which];
        });
        doc.addEventListener('keyup', function (keyEvent) {
            if (user.keys[keyEvent.which] === undefined) {
                user.keys[keyEvent.which] = 0;
            }
            // Known Mac "feature" where keyup never fires while meta key (91) is down
            // When right-click menu is toggled we don't get all mouseup events
            if (keyEvent.which === 91 || keyEvent.which === 17) {
                clearKey();
            }
            user.keys[keyEvent.which] = 0;
        });
        // If there's any sizing mode that requires computation of container size
        // and alwaysComputeSizing is *disabled*, make an "overriding" assumption
        // that we probably want to recompute the container when media queries change.
        if (this.hasSizing() && !this.config.alwaysComputeSizing) {
            win.addEventListener('resize', function () {
                _this.shouldCreateContainer = true;
            });
            win.addEventListener('orientationchange', function () {
                _this.shouldCreateContainer = true;
            });
        }
        win.addEventListener('blur', function (blurEvent) {
            clearKey();
            clearMouse();
            clearTouch();
            clearMouch();
        });
        win.addEventListener('focus', function (blurEvent) {
            clearKey();
            clearMouse();
            clearTouch();
            clearMouch();
        });
        win.addEventListener('touchstart', function (touchEvent) {
            setTouches(touchEvent);
            setMouches();
        }, {
            passive: true,
        });
        win.addEventListener('touchend', function (touchEvent) {
            clearTouch();
            clearMouch();
        });
        win.addEventListener('touchmove', function (touchEvent) {
            setTouches(touchEvent);
            setMouches();
        }, {
            passive: true,
        });
        win.addEventListener('touchenter', function (touchEvent) {
            clearTouch();
            clearMouch();
        });
        win.addEventListener('touchleave', function (touchEvent) {
            clearTouch();
            clearMouch();
        });
    };
    HaikuDOMRenderer.prototype.mountEventListener = function (selector, name, listener) {
        var _this = this;
        var rewritten = name;
        if (name === 'mouseenter') {
            rewritten = 'mouseover';
        }
        if (name === 'mouseleave') {
            rewritten = 'mouseout';
        }
        this.mount.addEventListener(rewritten, function (domEvent) {
            // If no explicit selector/target for the event, just fire the listener
            if (!selector || selector === HaikuBase_1.GLOBAL_LISTENER_KEY ||
                !domEvent || !domEvent.target) {
                listener(null, domEvent);
                return;
            }
            var query = selector;
            // Convert haiku:* selectors into proper attribute selectors
            if (selector.slice(0, 6) === 'haiku:') {
                query = "[haiku-id=\"" + selector.slice(6) + "\"]";
            }
            // If the event originated from the element or its descendants
            var match = _this.mount.querySelector(query);
            if (match) {
                if (shouldListenerReceiveEvent(name, domEvent, match)) {
                    listener(domEvent.target, domEvent);
                    return;
                }
            }
        });
    };
    HaikuDOMRenderer.prototype.getUser = function () {
        var zoom = this.getZoom();
        var pan = this.getPan();
        return {
            zoom: zoom,
            pan: pan,
            mouse: {
                x: (this.user.mouse.x) / zoom,
                y: (this.user.mouse.y) / zoom,
                down: this.user.mouse.down,
                buttons: this.user.mouse.buttons.slice(),
            },
            keys: __assign({}, this.user.keys),
            touches: this.user.touches.slice(),
            mouches: this.user.mouches.slice(),
        };
    };
    HaikuDOMRenderer.createTagNode = function (domElement, virtualElement, parentVirtualElement, component) {
        var tagName = normalizeName_1.default(getTypeAsString_1.default(virtualElement));
        var flexId = getFlexId_1.default(virtualElement);
        var newDomElement;
        if (allSvgElementNames_1.default[tagName]) {
            // SVG
            newDomElement = createSvgElement_1.default(domElement, tagName);
        }
        else {
            // Normal DOM
            newDomElement = domElement.ownerDocument.createElement(tagName);
        }
        // This didn't happen in renderTree because the element didn't exist yet.
        if (!newDomElement.haiku) {
            newDomElement.haiku = {
                // This is used to detect whether the element's host component has changed.
                // Don't remove this without understanding the effect on Haiku.app.
                component: component,
            };
        }
        component.subcacheEnsure(flexId);
        var incomingKey = virtualElement.key ||
            (virtualElement.attributes && virtualElement.attributes.key);
        if (incomingKey !== undefined && incomingKey !== null) {
            newDomElement.haiku.key = incomingKey;
        }
        // updateElement recurses down into setAttributes, etc.
        HaikuDOMRenderer.updateElement(newDomElement, virtualElement, domElement, parentVirtualElement, component, null);
        return newDomElement;
    };
    HaikuDOMRenderer.appendChild = function (alreadyChildElement, virtualElement, parentDomElement, parentVirtualElement, component) {
        var domElementToInsert = isTextNode_1.default(virtualElement)
            ? createTextNode_1.default(parentDomElement, virtualElement)
            : HaikuDOMRenderer.createTagNode(parentDomElement, virtualElement, parentVirtualElement, component);
        applyLayout_1.default(domElementToInsert, virtualElement, parentDomElement, parentVirtualElement, component, null);
        parentDomElement.appendChild(domElementToInsert);
        return domElementToInsert;
    };
    HaikuDOMRenderer.replaceElement = function (domElement, virtualElement, parentDomNode, parentVirtualElement, component) {
        var flexId = getFlexId_1.default(virtualElement);
        component.subcacheClear(flexId);
        var newElement = isTextNode_1.default(virtualElement)
            ? createTextNode_1.default(domElement, virtualElement)
            : HaikuDOMRenderer.createTagNode(domElement, virtualElement, parentVirtualElement, component);
        applyLayout_1.default(newElement, virtualElement, parentDomNode, parentVirtualElement, component, null);
        parentDomNode.replaceChild(newElement, domElement);
        return newElement;
    };
    HaikuDOMRenderer.updateElement = function (domElement, virtualElement, parentNode, parentVirtualElement, component, isPatchOperation) {
        var flexId = getFlexId_1.default(virtualElement);
        // If a text node, go straight to 'replace' since we don't know the tag name
        if (isTextNode_1.default(virtualElement)) {
            replaceElementWithText_1.default(domElement, virtualElement, component);
            return virtualElement;
        }
        if (!domElement.haiku) {
            domElement.haiku = {
                // This is used to detect whether the element's host component has changed.
                // Don't remove this without understanding the effect on Haiku.app.
                component: component,
            };
        }
        component.subcacheEnsure(flexId);
        if (!domElement.haiku.element) {
            // Must clone so we get a correct picture of differences in attributes between runs, e.g. for detecting attribute
            // removals
            domElement.haiku.element = cloneVirtualElement_1.default(virtualElement);
        }
        var domTagName = domElement.tagName.toLowerCase().trim();
        var elName = normalizeName_1.default(getTypeAsString_1.default(virtualElement));
        var virtualElementTagName = elName.toLowerCase().trim();
        var incomingKey = virtualElement.key || (virtualElement.attributes && virtualElement.attributes.key);
        var existingKey = domElement.haiku && domElement.haiku.key;
        var isKeyDifferent = incomingKey !== null && incomingKey !== undefined && incomingKey !== existingKey;
        // For so-called 'horizon' elements, we assume that we've ceded control to another renderer,
        // so the most we want to do is update the attributes and layout properties, but leave the rest alone
        if (!component.isHorizonElement(virtualElement)) {
            if (domTagName !== virtualElementTagName) {
                return HaikuDOMRenderer.replaceElement(domElement, virtualElement, parentNode, parentVirtualElement, component);
            }
            if (isKeyDifferent) {
                return HaikuDOMRenderer.replaceElement(domElement, virtualElement, parentNode, parentVirtualElement, component);
            }
        }
        if (virtualElement.attributes && typeof virtualElement.attributes === 'object') {
            assignAttributes_1.default(domElement, virtualElement, component, isPatchOperation);
        }
        applyLayout_1.default(domElement, virtualElement, parentNode, parentVirtualElement, component, isPatchOperation);
        if (incomingKey !== undefined && incomingKey !== null) {
            domElement.haiku.key = incomingKey;
        }
        var instance = (virtualElement && virtualElement.__instance) || component;
        if (Array.isArray(virtualElement.children)) {
            // For performance, we don't render children during a patch operation, except in the case
            // that we have some text content, which we (hack) need to always assume needs an update.
            // TODO: Fix this hack and make smarter
            var doSkipChildren = isPatchOperation && (typeof virtualElement.children[0] !== 'string');
            HaikuDOMRenderer.renderTree(domElement, virtualElement, virtualElement.children, instance, isPatchOperation, doSkipChildren);
        }
        else if (!virtualElement.children) {
            // In case of falsy virtual children, we still need to remove elements that were already there
            HaikuDOMRenderer.renderTree(domElement, virtualElement, [], instance, isPatchOperation, null);
        }
        return domElement;
    };
    HaikuDOMRenderer.renderTree = function (domElement, virtualElement, virtualChildren, component, isPatchOperation, doSkipChildren) {
        // E.g. I might want to inspect the dom node, grab the haiku source data, etc.
        connectTarget(virtualElement, domElement);
        var flexId = getFlexId_1.default(virtualElement);
        if (!domElement.haiku) {
            domElement.haiku = {
                // This is used to detect whether the element's host component has changed.
                // Don't remove this without understanding the effect on Haiku.app.
                component: component,
            };
        }
        domElement.haiku.virtual = virtualElement;
        // Must clone so we get a correct picture of differences in attributes
        // between runs, e.g. for detecting attribute removals
        domElement.haiku.element = cloneVirtualElement_1.default(virtualElement);
        component.subcacheEnsure(flexId);
        if (!Array.isArray(virtualChildren)) {
            return domElement;
        }
        // For so-called 'horizon' elements, we assume that we've ceded control to another renderer,
        // so the most we want to do is update the attributes and layout properties, but leave the rest alone
        if (component.isHorizonElement(virtualElement)) {
            return domElement;
        }
        // During patch renders we don't want to drill down and update children as
        // we're just going to end up doing a lot of unnecessary DOM writes
        if (doSkipChildren) {
            return domElement;
        }
        while (virtualChildren.length > 0 && isBlankString_1.default(virtualChildren[0])) {
            virtualChildren.shift();
        }
        // Store a copy of the array here, otherwise we can hit a race where as we remove
        // elements from the DOM, the childNodes array gets shifted and the indices get offset, leading
        // to removals not occurring properly
        var domChildNodes = [];
        for (var k = 0; k < domElement.childNodes.length; k++) {
            domChildNodes[k] = domElement.childNodes[k];
        }
        var max = virtualChildren.length;
        if (max < domChildNodes.length) {
            max = domChildNodes.length;
        }
        for (var i = 0; i < max; i++) {
            var virtualChild = virtualChildren[i];
            var domChild = domChildNodes[i];
            if (!virtualChild && !domChild) {
                // empty
            }
            else if (!virtualChild && domChild) {
                removeElement_1.default(domChild, flexId, component);
            }
            else if (virtualChild) {
                if (!domChild) {
                    var insertedElement = HaikuDOMRenderer.appendChild(null, virtualChild, domElement, virtualElement, component);
                    connectTarget(virtualChild, insertedElement);
                }
                else {
                    // Circumstances in which we want to completely *replace* the element:
                    // - We see that our cached target element is not the one at this location
                    // - We see that the DOM id doesn't match the incoming one
                    // - we see that the haiku-id doesn't match the incoming one.
                    // If we now have an element that is different, we need to trigger a full re-render
                    // of itself and all of its children, because e.g. url(#...) references will retain pointers to
                    // old elements and this is the only way to clear the DOM to get a correct render.
                    if (shouldElementBeReplaced_1.default(domChild, virtualChild, component)) {
                        var newElement = HaikuDOMRenderer.replaceElement(domChild, virtualChild, domElement, virtualElement, component);
                        connectTarget(virtualChild, newElement);
                    }
                    else {
                        HaikuDOMRenderer.updateElement(domChild, virtualChild, domElement, virtualElement, component, isPatchOperation);
                        connectTarget(virtualChild, domChild);
                    }
                }
            }
        }
        return domElement;
    };
    HaikuDOMRenderer.__name__ = 'HaikuDOMRenderer';
    return HaikuDOMRenderer;
}(HaikuBase_1.default));
exports.default = HaikuDOMRenderer;
//# sourceMappingURL=HaikuDOMRenderer.js.map