var dialogPolyfill = (function() {

    var supportCustomEvent = window.CustomEvent;
    if (!supportCustomEvent || typeof supportCustomEvent == "object") {
        supportCustomEvent = function CustomEvent(event, x) {
            x = x || {};
            var ev = document.createEvent('CustomEvent');
            ev.initCustomEvent(event, !!x.bubbles, !!x.cancelable, x.detail || null);
            return ev;
        };
        supportCustomEvent.prototype = window.Event.prototype;
    }

    /**
     * Finds the nearest <dialog> from the passed element.
     *
     * @param {Element} el to search from
     * @param {HTMLDialogElement} dialog found
     */
    function findNearestDialog(el) {
        while (el) {
            if (el.nodeName == 'DIALOG') {
                return el;
            }
            el = el.parentElement;
        }
        return null;
    }

    var dialogPolyfill = {};

    dialogPolyfill.reposition = function(element) {
        var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
        var topValue = scrollTop + (window.innerHeight - element.offsetHeight) / 2;
        element.style.top = Math.max(0, topValue) + 'px';
        element.dialogPolyfillInfo.isTopOverridden = true;
    };

    dialogPolyfill.inNodeList = function(nodeList, node) {
        for (var i = 0; i < nodeList.length; ++i) {
            if (nodeList[i] == node)
                return true;
        }
        return false;
    };

    dialogPolyfill.isInlinePositionSetByStylesheet = function(element) {
        for (var i = 0; i < document.styleSheets.length; ++i) {
            var styleSheet = document.styleSheets[i];
            var cssRules = null;
            // Some browsers throw on cssRules.
            try {
                cssRules = styleSheet.cssRules;
            } catch (e) {}
            if (!cssRules)
                continue;
            for (var j = 0; j < cssRules.length; ++j) {
                var rule = cssRules[j];
                var selectedNodes = null;
                // Ignore errors on invalid selector texts.
                try {
                    selectedNodes = document.querySelectorAll(rule.selectorText);
                } catch(e) {}
                if (!selectedNodes || !dialogPolyfill.inNodeList(selectedNodes, element))
                    continue;
                var cssTop = rule.style.getPropertyValue('top');
                var cssBottom = rule.style.getPropertyValue('bottom');
                if ((cssTop && cssTop != 'auto') || (cssBottom && cssBottom != 'auto'))
                    return true;
            }
        }
        return false;
    };

    dialogPolyfill.needsCentering = function(dialog) {
        var computedStyle = window.getComputedStyle(dialog);
        if (computedStyle.position != 'absolute') {
            return false;
        }

        // We must determine whether the top/bottom specified value is non-auto.  In
        // WebKit/Blink, checking computedStyle.top == 'auto' is sufficient, but
        // Firefox returns the used value. So we do this crazy thing instead: check
        // the inline style and then go through CSS rules.
        if ((dialog.style.top != 'auto' && dialog.style.top != '') ||
            (dialog.style.bottom != 'auto' && dialog.style.bottom != ''))
            return false;
        return !dialogPolyfill.isInlinePositionSetByStylesheet(dialog);
    };

    dialogPolyfill.showDialog = function(isModal) {
        if (this.open) {
            throw 'InvalidStateError: showDialog called on open dialog';
        }
        this.open = true;  // TODO: should be a getter mapped to attribute
        this.setAttribute('open', 'open');

        if (isModal) {
            // Find element with `autofocus` attribute or first form control
            var first_form_ctrl = null;
            var autofocus = null;
            var findElementToFocus = function(root) {
                if (!root.children) {
                    return;
                }
                for (var i = 0; i < root.children.length; i++) {
                    var elem = root.children[i];
                    if (first_form_ctrl === null && !elem.disabled && (
                        elem.nodeName == 'BUTTON' ||
                        elem.nodeName == 'INPUT'  ||
                        elem.nodeName == 'KEYGEN' ||
                        elem.nodeName == 'SELECT' ||
                        elem.nodeName == 'TEXTAREA')) {
                        first_form_ctrl = elem;
                    }
                    if (elem.autofocus) {
                        autofocus = elem;
                        return;
                    }
                    findElementToFocus(elem);
                    if (autofocus !== null) return;
                }
            };

            findElementToFocus(this);

            if (autofocus !== null) {
                autofocus.focus();
            } else if (first_form_ctrl !== null) {
                first_form_ctrl.focus();
            }
        }

        if (dialogPolyfill.needsCentering(this))
            dialogPolyfill.reposition(this);
        if (isModal) {
            this.dialogPolyfillInfo.modal = true;
            dialogPolyfill.dm.pushDialog(this);
        }

        // IE sometimes complains when calling .focus() that it
        // "Can't move focus to the control because it is invisible, not enabled, or of a type that does not accept the focus."
        try {
            if (autofocus !== null) {
                autofocus.focus();
            } else if (first_form_ctrl !== null) {
                first_form_ctrl.focus();
            }
        } catch(e) {}
        this.style.zoom = 1;
    };

    dialogPolyfill.close = function(retval) {
        if (!this.open && !window.HTMLDialogElement) {
            // Native implementations will set .open to false, so ignore this error.
            throw 'InvalidStateError: close called on closed dialog';
        }
        this.open = false;
        this.removeAttribute('open');

        // Leave returnValue untouched in case it was set directly on the element
        if (typeof retval != 'undefined') {
            this.returnValue = retval;
        }

        // This won't match the native <dialog> exactly because if the user sets top
        // on a centered polyfill dialog, that top gets thrown away when the dialog is
        // closed. Not sure it's possible to polyfill this perfectly.
        if (this.dialogPolyfillInfo.isTopOverridden) {
            this.style.top = 'auto';
        }

        if (this.dialogPolyfillInfo.modal) {
            dialogPolyfill.dm.removeDialog(this);
        }

        // Triggering "close" event for any attached listeners on the <dialog>
        var event;
        if (document.createEvent) {
            event = document.createEvent('HTMLEvents');
            event.initEvent('close', true, true);
        } else {
            event = new Event('close');
        }
        this.dispatchEvent(event);

        return this.returnValue;
    };

    dialogPolyfill.registerDialog = function(element) {
        if (element.show) {
            // console.warn("This browser already supports <dialog>, the polyfill " +
            //     "may not work correctly.");
        }
        element.show = dialogPolyfill.showDialog.bind(element, false);
        element.showModal = dialogPolyfill.showDialog.bind(element, true);
        element.close = dialogPolyfill.close.bind(element);
        element.dialogPolyfillInfo = {};
        element.open = false;
    };

    // The overlay is used to simulate how a modal dialog blocks the document. The
    // blocking dialog is positioned on top of the overlay, and the rest of the
    // dialogs on the pending dialog stack are positioned below it. In the actual
    // implementation, the modal dialog stacking is controlled by the top layer,
    // where z-index has no effect.
    var TOP_LAYER_ZINDEX = 100000;
    var MAX_PENDING_DIALOGS = 100000;

    dialogPolyfill.DialogManager = function() {
        this.pendingDialogStack = [];
        this.overlay = document.createElement('div');
        this.overlay.style.width = '100%';
        this.overlay.style.height = '100%';
        this.overlay.style.position = 'fixed';
        this.overlay.style.left = '0px';
        this.overlay.style.top = '0px';
        this.overlay.style.backgroundColor = 'rgba(0,0,0,0.0)';

        this.focusPageLast = this.createFocusable();
        this.overlay.appendChild(this.focusPageLast);

        this.overlay.addEventListener('click', function(e) {
            var redirectedEvent = document.createEvent('MouseEvents');
            redirectedEvent.initMouseEvent(e.type, e.bubbles, e.cancelable, window,
                e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey,
                e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget);
            document.body.dispatchEvent(redirectedEvent);
        });

        // TODO: Only install when any dialogs are open.
        document.addEventListener('submit', function(ev) {
            var method = ev.target.getAttribute('method');
            method = method ? method.toLowerCase() : '';
            if (method != 'dialog') { return; }
            ev.preventDefault();

            var dialog = findNearestDialog(ev.target);
            if (!dialog) { return; }

            // FIXME: The original event doesn't contain the INPUT element used to
            // submit the form (if any). Look in some possible places.
            var returnValue;
            var cands = [document.activeElement, ev.explicitOriginalTarget];
            cands.some(function(cand) {
                if (cand && cand.nodeName == 'INPUT' && cand.form == ev.target) {
                    returnValue = cand.value;
                    return true;
                }
            });
            dialog.close(returnValue);
        }, true);
    };

    dialogPolyfill.DialogManager.prototype.createFocusable = function(tabIndex) {
        var span = document.createElement('span');
        span.tabIndex = tabIndex || 0;
        span.style.opacity = 0;
        span.style.position = 'static';
        return span;
    };

    dialogPolyfill.DialogManager.prototype.blockDocument = function() {
        if (!document.body.contains(this.overlay)) {
            document.body.appendChild(this.overlay);

            // On Safari/Mac (and possibly other browsers), the documentElement is
            // not focusable. This is required for modal dialogs as it is the first
            // element to be hit by a tab event, and further tabs are redirected to
            // the most visible dialog.
            if (this.needsDocumentElementFocus === undefined) {
                document.documentElement.focus();
                this.needsDocumentElementFocus =
                    (document.activeElement != document.documentElement);
            }
            if (this.needsDocumentElementFocus) {
                document.documentElement.tabIndex = 1;
            }
        }
    };

    dialogPolyfill.DialogManager.prototype.unblockDocument = function() {
        document.body.removeChild(this.overlay);
        if (this.needsDocumentElementFocus) {
            // TODO: Restore the previous tabIndex, rather than clearing it.
            document.documentElement.tabIndex = '';
        }
    };

    dialogPolyfill.DialogManager.prototype.updateStacking = function() {
        if (this.pendingDialogStack.length == 0) {
            this.unblockDocument();
            return;
        }
        this.blockDocument();

        var zIndex = TOP_LAYER_ZINDEX;
        for (var i = 0; i < this.pendingDialogStack.length; i++) {
            if (i == this.pendingDialogStack.length - 1)
                this.overlay.style.zIndex = zIndex++;
            var dialog = this.pendingDialogStack[i];
            dialog.dialogPolyfillInfo.backdrop.style.zIndex = zIndex++;
            dialog.style.zIndex = zIndex++;
        }
    };

    dialogPolyfill.DialogManager.prototype.handleKey = function(event) {
        var dialogCount = this.pendingDialogStack.length;
        if (dialogCount == 0) {
            return;
        }
        var dialog = this.pendingDialogStack[dialogCount - 1];
        var pfi = dialog.dialogPolyfillInfo;

        switch (event.keyCode) {
            case 9: /* tab */
                var activeElement = document.activeElement;
                var forward = !event.shiftKey;
                if (forward) {
                    // Tab forward, so look for document or fake last focus element.
                    if (activeElement == document.documentElement ||
                        activeElement == document.body ||
                        activeElement == pfi.backdrop) {
                        pfi.focusFirst.focus();
                    } else if (activeElement == pfi.focusLast) {
                        // TODO: Instead of wrapping to focusFirst, escape to browser chrome.
                        pfi.focusFirst.focus();
                    }
                } else {
                    // Tab backwards, so look for fake first focus element.
                    if (activeElement == pfi.focusFirst) {
                        // TODO: Instead of wrapping to focusLast, escape to browser chrome.
                        pfi.focusLast.focus();
                    } else if (activeElement == this.focusPageLast) {
                        // The focus element is at the end of the page (e.g., shift-tab from
                        // the window chrome): move current focus to the last element in the
                        // dialog instead.
                        pfi.focusLast.focus();
                    }
                }
                break;

            case 27: /* esc */
                event.preventDefault();
                event.stopPropagation();
                var cancelEvent = new supportCustomEvent('cancel', {
                    bubbles: false,
                    cancelable: true
                });
                if (dialog.dispatchEvent(cancelEvent)) {
                    dialog.close();
                }
                break;

        }
    };

    dialogPolyfill.DialogManager.prototype.pushDialog = function(dialog) {
        if (this.pendingDialogStack.length >= MAX_PENDING_DIALOGS) {
            throw "Too many modal dialogs";
        }

        var backdrop = document.createElement('div');
        backdrop.className = 'backdrop';
        var clickEventListener = function(e) {
            var redirectedEvent = document.createEvent('MouseEvents');
            redirectedEvent.initMouseEvent(e.type, e.bubbles, e.cancelable, window,
                e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey,
                e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget);
            dialog.dispatchEvent(redirectedEvent);
        };
        backdrop.addEventListener('click', clickEventListener);
        dialog.parentNode.insertBefore(backdrop, dialog.nextSibling);
        dialog.dialogPolyfillInfo.backdrop = backdrop;
        dialog.dialogPolyfillInfo.clickEventListener = clickEventListener;
        this.pendingDialogStack.push(dialog);
        this.updateStacking();

        dialog.dialogPolyfillInfo.focusFirst = this.createFocusable();
        dialog.dialogPolyfillInfo.focusLast = this.createFocusable();
        dialog.appendChild(dialog.dialogPolyfillInfo.focusLast);
        dialog.insertBefore(
            dialog.dialogPolyfillInfo.focusFirst, dialog.firstChild);
    };

    dialogPolyfill.DialogManager.prototype.removeDialog = function(dialog) {
        var index = this.pendingDialogStack.indexOf(dialog);
        if (index == -1) {
            return;
        }
        this.pendingDialogStack.splice(index, 1);
        var backdrop = dialog.dialogPolyfillInfo.backdrop;
        var clickEventListener = dialog.dialogPolyfillInfo.clickEventListener;
        backdrop.removeEventListener('click', clickEventListener);
        backdrop.parentNode.removeChild(backdrop);
        dialog.dialogPolyfillInfo.backdrop = null;
        dialog.dialogPolyfillInfo.clickEventListener = null;
        this.updateStacking();

        dialog.removeChild(dialog.dialogPolyfillInfo.focusFirst);
        dialog.removeChild(dialog.dialogPolyfillInfo.focusLast);
        dialog.dialogPolyfillInfo.focusFirst = null;
        dialog.dialogPolyfillInfo.focusLast = null;
    };

    dialogPolyfill.dm = new dialogPolyfill.DialogManager();

    document.addEventListener('keydown',
        dialogPolyfill.dm.handleKey.bind(dialogPolyfill.dm));

    return dialogPolyfill;
})();
