import { DOMParser } from '@xmldom/xmldom';
import { atob, btoa } from 'abab';
import { NativeModules, Platform } from 'react-native';
import BackgroundTimer from 'react-native-background-timer';
import { TextDecoder, TextEncoder } from 'text-encoding';

import 'promise.withresolvers/auto'; // Promise.withResolvers.
import 'react-native-url-polyfill/auto'; // Complete URL polyfill.

import Storage from './Storage';

const { AppInfo } = NativeModules;

/**
 * Implements an absolute minimum of the common logic of
 * {@code Document.querySelector} and {@code Element.querySelector}. Implements
 * the most simple of selectors necessary to satisfy the call sites at the time
 * of this writing (i.e. Select by tagName).
 *
 * @param {Node} node - The Node which is the root of the tree to query.
 * @param {string} selectors - The group of CSS selectors to match on.
 * @returns {Element} - The first Element which is a descendant of the specified
 * node and matches the specified group of selectors.
 */
function _querySelector(node, selectors) {
    let element = null;

    node && _visitNode(node, n => {
        if (n.nodeType === 1 /* ELEMENT_NODE */
                && n.nodeName === selectors) {
            element = n;

            return true;
        }

        return false;
    });

    return element;
}

/**
 * Visits each Node in the tree of a specific root Node (using depth-first
 * traversal) and invokes a specific callback until the callback returns true.
 *
 * @param {Node} node - The root Node which represents the tree of Nodes to
 * visit.
 * @param {Function} callback - The callback to invoke with each visited Node.
 * @returns {boolean} - True if the specified callback returned true for a Node
 * (at which point the visiting stopped); otherwise, false.
 */
function _visitNode(node, callback) {
    if (callback(node)) {
        return true;
    }

    /* eslint-disable no-param-reassign, no-extra-parens */

    if ((node = node.firstChild)) {
        do {
            if (_visitNode(node, callback)) {
                return true;
            }
        } while ((node = node.nextSibling));
    }

    /* eslint-enable no-param-reassign, no-extra-parens */

    return false;
}

(global => {
    // DOMParser
    //
    // Required by:
    // - lib-jitsi-meet requires this if using WebSockets
    global.DOMParser = DOMParser;

    // addEventListener
    //
    // Required by:
    // - jQuery
    if (typeof global.addEventListener === 'undefined') {
        // eslint-disable-next-line no-empty-function
        global.addEventListener = () => {};
    }

    // removeEventListener
    //
    // Required by:
    // - features/base/conference/middleware
    if (typeof global.removeEventListener === 'undefined') {
        // eslint-disable-next-line no-empty-function
        global.removeEventListener = () => {};
    }

    // document
    //
    // Required by:
    // - jQuery
    // - Strophe
    if (typeof global.document === 'undefined') {
        const document
            = new DOMParser().parseFromString(
                '<html><head></head><body></body></html>',
                'text/xml');

        // document.addEventListener
        //
        // Required by:
        // - jQuery
        if (typeof document.addEventListener === 'undefined') {
            // eslint-disable-next-line no-empty-function
            document.addEventListener = () => {};
        }

        // document.cookie
        //
        // Required by:
        // - herment
        if (typeof document.cookie === 'undefined') {
            document.cookie = '';
        }

        // document.implementation.createHTMLDocument
        //
        // Required by:
        // - jQuery
        if (typeof document.implementation.createHTMLDocument === 'undefined') {
            document.implementation.createHTMLDocument = function(title = '') {
                const htmlDocument
                    = new DOMParser().parseFromString(
                        `<html>
                            <head><title>${title}</title></head>
                            <body></body>
                        </html>`,
                        'text/xml');

                Object.defineProperty(htmlDocument, 'body', {
                    get() {
                        return htmlDocument.getElementsByTagName('body')[0];
                    }
                });

                return htmlDocument;
            };
        }

        // Element.querySelector
        //
        // Required by:
        // - lib-jitsi-meet/modules/xmpp
        const elementPrototype
            = Object.getPrototypeOf(document.documentElement);

        if (elementPrototype) {
            if (typeof elementPrototype.querySelector === 'undefined') {
                elementPrototype.querySelector = function(selectors) {
                    return _querySelector(this, selectors);
                };
            }

            // Element.remove
            //
            // Required by:
            // - lib-jitsi-meet ChatRoom#onPresence parsing
            if (typeof elementPrototype.remove === 'undefined') {
                elementPrototype.remove = function() {
                    if (this.parentNode !== null) {
                        this.parentNode.removeChild(this);
                    }
                };
            }

            // Element.innerHTML
            //
            // Required by:
            // - jQuery's .append method
            if (!elementPrototype.hasOwnProperty('innerHTML')) {
                Object.defineProperty(elementPrototype, 'innerHTML', {
                    get() {
                        return this.childNodes.toString();
                    },

                    set(innerHTML) {
                        // MDN says: removes all of element's children, parses
                        // the content string and assigns the resulting nodes as
                        // children of the element.

                        // Remove all of element's children.
                        this.textContent = '';

                        // Parse the content string.
                        const d
                            = new DOMParser().parseFromString(
                                `<div>${innerHTML}</div>`,
                                'text/xml');

                        // Assign the resulting nodes as children of the
                        // element.
                        const documentElement = d.documentElement;
                        let child;

                        // eslint-disable-next-line no-cond-assign
                        while (child = documentElement.firstChild) {
                            this.appendChild(child);
                        }
                    }
                });
            }

            // Element.children
            //
            // Required by:
            // - lib-jitsi-meet ChatRoom#onPresence parsing
            if (!elementPrototype.hasOwnProperty('children')) {
                Object.defineProperty(elementPrototype, 'children', {
                    get() {
                        const nodes = this.childNodes;
                        const children = [];
                        let i = 0;
                        let node = nodes[i];

                        while (node) {
                            if (node.nodeType === 1) {
                                children.push(node);
                            }
                            i += 1;
                            node = nodes[i];
                        }

                        return children;
                    }
                });
            }
        }

        global.document = document;
    }

    // location
    if (typeof global.location === 'undefined') {
        global.location = {
            href: '',

            // Required by:
            // - lib-jitsi-meet/modules/xmpp/xmpp.js
            search: ''
        };
    }

    const { navigator } = global;

    if (navigator) {
        // userAgent
        //
        // Required by:
        // - lib-jitsi-meet/modules/browser/BrowserDetection.js

        // React Native version
        const { reactNativeVersion } = Platform.constants;
        const rnVersion
            = `react-native/${reactNativeVersion.major}.${reactNativeVersion.minor}.${reactNativeVersion.patch}`;

        // (OS version)
        const os = `${Platform.OS.toLowerCase()}/${Platform.Version}`;

        // SDK
        const liteTxt = AppInfo.isLiteSDK ? '-lite' : '';
        const sdkVersion = `JitsiMeetSDK/${AppInfo.sdkVersion}${liteTxt}`;

        const parts = [
            navigator.userAgent ?? '',
            sdkVersion,
            os,
            rnVersion
        ];

        navigator.userAgent = parts.filter(Boolean).join(' ');
    }

    // WebRTC
    require('./webrtc');

    // Performance API

    // RN only provides the now() method, since the polyfill refers the global
    // performance object itself we extract it here to avoid infinite recursion.
    const performanceNow = global.performance.now;

    const perf = require('react-native-performance');

    global.performance = perf.default;
    global.performance.now = performanceNow;
    global.PerformanceObserver = perf.PerformanceObserver;

    // Timers
    //
    // React Native's timers won't run while the app is in the background, this
    // is a known limitation. Replace them with a background-friendly alternative.
    if (Platform.OS === 'android') {
        global.clearTimeout = BackgroundTimer.clearTimeout.bind(BackgroundTimer);
        global.clearInterval = BackgroundTimer.clearInterval.bind(BackgroundTimer);
        global.setInterval = BackgroundTimer.setInterval.bind(BackgroundTimer);
        global.setTimeout = (fn, ms = 0) => BackgroundTimer.setTimeout(fn, ms);
    }

    // localStorage
    if (typeof global.localStorage === 'undefined') {
        global.localStorage = new Storage('@jitsi-meet/');
    }

    // sessionStorage
    //
    // Required by:
    // - herment
    // - Strophe
    if (typeof global.sessionStorage === 'undefined') {
        global.sessionStorage = new Storage();
    }

    global.TextDecoder = TextDecoder;
    global.TextEncoder = TextEncoder;

    // atob
    //
    // Required by:
    // - Strophe
    if (typeof global.atob === 'undefined') {
        global.atob = atob;
    }

    // btoa
    //
    // Required by:
    // - Strophe
    if (typeof global.btoa === 'undefined') {
        global.btoa = btoa;
    }

})(global || window || this); // eslint-disable-line no-invalid-this
