/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * Sets up global variables typical in most JavaScript environments.
 *
 * 1. Global timers (via `setTimeout` etc).
 * 2. Global console object.
 * 3. Hooks for printing stack traces with source maps.
 *
 * Leaves enough room in the environment for implementing your own:
 * 1. Require system.
 * 2. Bridged modules.
 *
 * @providesModule InitializeJavaScriptAppEngine
 */

/* eslint strict: 0 */
/* globals GLOBAL: true, window: true */

require('regenerator/runtime');

if (typeof GLOBAL === 'undefined') {
  global.GLOBAL = this;
}

if (typeof window === 'undefined') {
  global.window = GLOBAL;
}

function setUpConsole() {
  // ExceptionsManager transitively requires Promise so we install it after
  var ExceptionsManager = require('ExceptionsManager');
  ExceptionsManager.installConsoleErrorReporter();
}

/**
 * Assigns a new global property, replacing the existing one if there is one.
 *
 * Existing properties are preserved as `originalPropertyName`. Both properties
 * will maintain the same enumerability & configurability.
 *
 * This allows you to undo the more aggressive polyfills, should you need to.
 * For example, if you want to route network requests through DevTools (to trace
 * them):
 *
 *     global.XMLHttpRequest = global.originalXMLHttpRequest;
 *
 * For more info on that particular case, see:
 * https://github.com/facebook/react-native/issues/934
 */
function polyfillGlobal(name, newValue, scope = GLOBAL) {
  var descriptor = Object.getOwnPropertyDescriptor(scope, name) || {
    // jest for some bad reasons runs the polyfill code multiple times. In jest
    // environment, XmlHttpRequest doesn't exist so getOwnPropertyDescriptor
    // returns undefined and defineProperty default for writable is false.
    // Therefore, the second time it runs, defineProperty will fatal :(
    writable: true,
  };

  if (scope[name] !== undefined) {
    var backupName = `original${name[0].toUpperCase()}${name.substr(1)}`;
    Object.defineProperty(scope, backupName, {...descriptor, value: scope[name]});
  }

  Object.defineProperty(scope, name, {...descriptor, value: newValue});
}

/**
 * Polyfill a module if it is not already defined in `scope`.
 */
function polyfillIfNeeded(name, polyfill, scope = GLOBAL, descriptor = {}) {
  if (scope[name] === undefined) {
    Object.defineProperty(scope, name, {...descriptor, value: polyfill});
  }
}

function setUpErrorHandler() {
  if (global.__fbDisableExceptionsManager) {
    return;
  }

  function handleError(e, isFatal) {
    try {
      require('ExceptionsManager').handleException(e, isFatal);
    } catch (ee) {
      console.log('Failed to print error: ', ee.message);
    }
  }

  var ErrorUtils = require('ErrorUtils');
  ErrorUtils.setGlobalHandler(handleError);
}

/**
 * Sets up a set of window environment wrappers that ensure that the
 * BatchedBridge is flushed after each tick. In both the case of the
 * `UIWebView` based `RCTJavaScriptCaller` and `RCTContextCaller`, we
 * implement our own custom timing bridge that should be immune to
 * unexplainably dropped timing signals.
 */
function setUpTimers() {
  var JSTimers = require('JSTimers');
  GLOBAL.setTimeout = JSTimers.setTimeout;
  GLOBAL.setInterval = JSTimers.setInterval;
  GLOBAL.setImmediate = JSTimers.setImmediate;
  GLOBAL.clearTimeout = JSTimers.clearTimeout;
  GLOBAL.clearInterval = JSTimers.clearInterval;
  GLOBAL.clearImmediate = JSTimers.clearImmediate;
  GLOBAL.cancelAnimationFrame = JSTimers.clearInterval;
  GLOBAL.requestAnimationFrame = function(cb) {
    /*requestAnimationFrame() { [native code] };*/  // Trick scroller library
    return JSTimers.requestAnimationFrame(cb);      // into thinking it's native
  };
}

function setUpAlert() {
  if (!GLOBAL.alert) {
    GLOBAL.alert = function(text) {
      // Require Alert on demand. Requiring it too early can lead to issues
      // with things like Platform not being fully initialized.
      require('Alert').alert('Alert', '' + text);
    };
  }
}

function setUpPromise() {
  // The native Promise implementation throws the following error:
  // ERROR: Event loop not supported.
  GLOBAL.Promise = require('Promise');
}

function setUpXHR() {
  // The native XMLHttpRequest in Chrome dev tools is CORS aware and won't
  // let you fetch anything from the internet
  polyfillGlobal('XMLHttpRequest', require('XMLHttpRequest'));
  polyfillGlobal('FormData', require('FormData'));

  var fetchPolyfill = require('fetch');
  polyfillGlobal('fetch', fetchPolyfill.fetch);
  polyfillGlobal('Headers', fetchPolyfill.Headers);
  polyfillGlobal('Request', fetchPolyfill.Request);
  polyfillGlobal('Response', fetchPolyfill.Response);
}

function setUpGeolocation() {
  polyfillIfNeeded('navigator', {}, GLOBAL, {
    writable: true,
    enumerable: true,
    configurable: true,
  });
  polyfillGlobal('geolocation', require('Geolocation'), GLOBAL.navigator);
}

function setUpMapAndSet() {
  polyfillGlobal('Map', require('Map'));
  polyfillGlobal('Set', require('Set'));
}

function setUpProduct() {
  Object.defineProperty(GLOBAL.navigator, 'product', {value: 'ReactNative'});
}

function setUpWebSockets() {
  polyfillGlobal('WebSocket', require('WebSocket'));
}

function setUpProfile() {
  if (__DEV__) {
    var Systrace = require('Systrace');
    Systrace.swizzleReactPerf();
  }
}

function setUpProcessEnv() {
  GLOBAL.process = GLOBAL.process || {};
  GLOBAL.process.env = GLOBAL.process.env || {};
  if (!GLOBAL.process.env.NODE_ENV) {
    GLOBAL.process.env.NODE_ENV = __DEV__ ? 'development' : 'production';
  }
}

function setUpNumber() {
  polyfillIfNeeded('EPSILON', Math.pow(2, -52), Number);
  polyfillIfNeeded('MAX_SAFE_INTEGER', Math.pow(2, 53) - 1, Number);
  polyfillIfNeeded('MIN_SAFE_INTEGER', -(Math.pow(2, 53) - 1), Number);
}

function setUpDevTools() {
  // not when debugging in chrome
  if (__DEV__) { // TODO(9123099) Strip `__DEV__ &&`
    if (!window.document && require('Platform').OS === 'ios') {
      var setupDevtools = require('setupDevtools');
      setupDevtools();
    }
  }
}

setUpProcessEnv();
setUpConsole();
setUpTimers();
setUpAlert();
setUpPromise();
setUpErrorHandler();
setUpXHR();
setUpGeolocation();
setUpMapAndSet();
setUpProduct();
setUpWebSockets();
setUpProfile();
setUpNumber();
setUpDevTools();

// Just to make sure the JS gets packaged up. Wait until the JS environment has
// been initialized before requiring them.
if (__DEV__) {
  require('RCTDebugComponentOwnership');
}
require('RCTDeviceEventEmitter');
require('PerformanceLogger');

if (__DEV__) {
  // include this transform and it's dependencies on the bundle on dev mode
  require('react-transform-hmr');
}
