/**
 * @license
 * Copyright Google Inc. All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */
/**
 * @fileoverview
 * @suppress {globalThis}
 */

import * as webSocketPatch from './websocket';

export function propertyDescriptorLegacyPatch(api: _ZonePrivate, _global: any) {
  const {isNode, isMix} = api.getGlobalObjects() !;
  if (isNode && !isMix) {
    return;
  }

  if (!canPatchViaPropertyDescriptor(api, _global)) {
    const supportsWebSocket = typeof WebSocket !== 'undefined';
    // Safari, Android browsers (Jelly Bean)
    patchViaCapturingAllTheEvents(api);
    api.patchClass('XMLHttpRequest');
    if (supportsWebSocket) {
      webSocketPatch.apply(api, _global);
    }
    (Zone as any)[api.symbol('patchEvents')] = true;
  }
}

function canPatchViaPropertyDescriptor(api: _ZonePrivate, _global: any) {
  const {isBrowser, isMix} = api.getGlobalObjects() !;
  if ((isBrowser || isMix) &&
      !api.ObjectGetOwnPropertyDescriptor(HTMLElement.prototype, 'onclick') &&
      typeof Element !== 'undefined') {
    // WebKit https://bugs.webkit.org/show_bug.cgi?id=134364
    // IDL interface attributes are not configurable
    const desc = api.ObjectGetOwnPropertyDescriptor(Element.prototype, 'onclick');
    if (desc && !desc.configurable) return false;
    // try to use onclick to detect whether we can patch via propertyDescriptor
    // because XMLHttpRequest is not available in service worker
    if (desc) {
      api.ObjectDefineProperty(
          Element.prototype, 'onclick',
          {enumerable: true, configurable: true, get: function() { return true; }});
      const div = document.createElement('div');
      const result = !!div.onclick;
      api.ObjectDefineProperty(Element.prototype, 'onclick', desc);
      return result;
    }
  }

  const XMLHttpRequest = _global['XMLHttpRequest'];
  if (!XMLHttpRequest) {
    // XMLHttpRequest is not available in service worker
    return false;
  }
  const ON_READY_STATE_CHANGE = 'onreadystatechange';
  const XMLHttpRequestPrototype = XMLHttpRequest.prototype;

  const xhrDesc =
      api.ObjectGetOwnPropertyDescriptor(XMLHttpRequestPrototype, ON_READY_STATE_CHANGE);

  // add enumerable and configurable here because in opera
  // by default XMLHttpRequest.prototype.onreadystatechange is undefined
  // without adding enumerable and configurable will cause onreadystatechange
  // non-configurable
  // and if XMLHttpRequest.prototype.onreadystatechange is undefined,
  // we should set a real desc instead a fake one
  if (xhrDesc) {
    api.ObjectDefineProperty(
        XMLHttpRequestPrototype, ON_READY_STATE_CHANGE,
        {enumerable: true, configurable: true, get: function() { return true; }});
    const req = new XMLHttpRequest();
    const result = !!req.onreadystatechange;
    // restore original desc
    api.ObjectDefineProperty(XMLHttpRequestPrototype, ON_READY_STATE_CHANGE, xhrDesc || {});
    return result;
  } else {
    const SYMBOL_FAKE_ONREADYSTATECHANGE = api.symbol('fake');
    api.ObjectDefineProperty(XMLHttpRequestPrototype, ON_READY_STATE_CHANGE, {
      enumerable: true,
      configurable: true,
      get: function() { return this[SYMBOL_FAKE_ONREADYSTATECHANGE]; },
      set: function(value) { this[SYMBOL_FAKE_ONREADYSTATECHANGE] = value; }
    });
    const req = new XMLHttpRequest();
    const detectFunc = () => {};
    req.onreadystatechange = detectFunc;
    const result = (req as any)[SYMBOL_FAKE_ONREADYSTATECHANGE] === detectFunc;
    req.onreadystatechange = null as any;
    return result;
  }
}

// Whenever any eventListener fires, we check the eventListener target and all parents
// for `onwhatever` properties and replace them with zone-bound functions
// - Chrome (for now)
function patchViaCapturingAllTheEvents(api: _ZonePrivate) {
  const {eventNames} = api.getGlobalObjects() !;
  const unboundKey = api.symbol('unbound');
  for (let i = 0; i < eventNames.length; i++) {
    const property = eventNames[i];
    const onproperty = 'on' + property;
    self.addEventListener(property, function(event) {
      let elt: any = <Node>event.target, bound, source;
      if (elt) {
        source = elt.constructor['name'] + '.' + onproperty;
      } else {
        source = 'unknown.' + onproperty;
      }
      while (elt) {
        if (elt[onproperty] && !elt[onproperty][unboundKey]) {
          bound = api.wrapWithCurrentZone(elt[onproperty], source);
          bound[unboundKey] = elt[onproperty];
          elt[onproperty] = bound;
        }
        elt = elt.parentElement;
      }
    }, true);
  }
}