/**
 * @license
 * Copyright Google LLC 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
 */

import {findEventTasks} from '../common/events';
import {patchQueueMicrotask} from '../common/queue-microtask';
import {patchTimer} from '../common/timers';
import {ArraySlice, isMix, patchMacroTask, patchMicroTask} from '../common/utils';
import {ZoneType} from '../zone-impl';

import {patchEvents} from './events';
import {patchFs} from './fs';
import {patchNodeUtil} from './node_util';

const set = 'set';
const clear = 'clear';

export function patchNode(Zone: ZoneType): void {
  patchNodeUtil(Zone);
  patchEvents(Zone);
  patchFs(Zone);

  Zone.__load_patch('node_timers', (global: any, Zone: ZoneType) => {
    // Timers
    let globalUseTimeoutFromTimer = false;
    try {
      const timers = require('timers');
      let globalEqualTimersTimeout = global.setTimeout === timers.setTimeout;
      if (!globalEqualTimersTimeout && !isMix) {
        // 1. if isMix, then we are in mix environment such as Electron
        // we should only patch timers.setTimeout because global.setTimeout
        // have been patched
        // 2. if global.setTimeout not equal timers.setTimeout, check
        // whether global.setTimeout use timers.setTimeout or not
        const originSetTimeout = timers.setTimeout;
        timers.setTimeout = function () {
          globalUseTimeoutFromTimer = true;
          return originSetTimeout.apply(this, arguments);
        };
        const detectTimeout = global.setTimeout(() => {}, 100);
        clearTimeout(detectTimeout);
        timers.setTimeout = originSetTimeout;
      }
      patchTimer(timers, set, clear, 'Timeout');
      patchTimer(timers, set, clear, 'Interval');
      patchTimer(timers, set, clear, 'Immediate');
    } catch (error) {
      // timers module not exists, for example, when we using nativeScript
      // timers is not available
    }
    if (isMix) {
      // if we are in mix environment, such as Electron,
      // the global.setTimeout has already been patched,
      // so we just patch timers.setTimeout
      return;
    }
    if (!globalUseTimeoutFromTimer) {
      // 1. global setTimeout equals timers setTimeout
      // 2. or global don't use timers setTimeout(maybe some other library patch setTimeout)
      // 3. or load timers module error happens, we should patch global setTimeout
      patchTimer(global, set, clear, 'Timeout');
      patchTimer(global, set, clear, 'Interval');
      patchTimer(global, set, clear, 'Immediate');
    } else {
      // global use timers setTimeout, but not equals
      // this happens when use nodejs v0.10.x, global setTimeout will
      // use a lazy load version of timers setTimeout
      // we should not double patch timer's setTimeout
      // so we only store the __symbol__ for consistency
      global[Zone.__symbol__('setTimeout')] = global.setTimeout;
      global[Zone.__symbol__('setInterval')] = global.setInterval;
      global[Zone.__symbol__('setImmediate')] = global.setImmediate;
    }
  });

  // patch process related methods
  Zone.__load_patch('nextTick', () => {
    // patch nextTick as microTask
    patchMicroTask(process, 'nextTick', (self: any, args: any[]) => {
      return {
        name: 'process.nextTick',
        args: args,
        cbIdx: args.length > 0 && typeof args[0] === 'function' ? 0 : -1,
        target: process,
      };
    });
  });

  Zone.__load_patch(
    'handleUnhandledPromiseRejection',
    (global: any, Zone: ZoneType, api: _ZonePrivate) => {
      (Zone as any)[api.symbol('unhandledPromiseRejectionHandler')] =
        findProcessPromiseRejectionHandler('unhandledRejection');

      (Zone as any)[api.symbol('rejectionHandledHandler')] =
        findProcessPromiseRejectionHandler('rejectionHandled');

      // handle unhandled promise rejection
      function findProcessPromiseRejectionHandler(evtName: string) {
        return function (e: any) {
          const eventTasks = findEventTasks(process, evtName);
          eventTasks.forEach((eventTask) => {
            // process has added unhandledrejection event listener
            // trigger the event listener
            if (evtName === 'unhandledRejection') {
              eventTask.invoke(e.rejection, e.promise);
            } else if (evtName === 'rejectionHandled') {
              eventTask.invoke(e.promise);
            }
          });
        };
      }
    },
  );

  // Crypto
  Zone.__load_patch('crypto', () => {
    let crypto: any;
    try {
      crypto = require('crypto');
    } catch (err) {}

    // use the generic patchMacroTask to patch crypto
    if (crypto) {
      const methodNames = ['randomBytes', 'pbkdf2'];
      methodNames.forEach((name) => {
        patchMacroTask(crypto, name, (self: any, args: any[]) => {
          return {
            name: 'crypto.' + name,
            args: args,
            cbIdx:
              args.length > 0 && typeof args[args.length - 1] === 'function' ? args.length - 1 : -1,
            target: crypto,
          };
        });
      });
    }
  });

  Zone.__load_patch('console', (global: any, Zone: ZoneType) => {
    const consoleMethods = [
      'dir',
      'log',
      'info',
      'error',
      'warn',
      'assert',
      'debug',
      'timeEnd',
      'trace',
    ];
    consoleMethods.forEach((m: string) => {
      const originalMethod = ((console as any)[Zone.__symbol__(m)] = (console as any)[m]);
      if (originalMethod) {
        (console as any)[m] = function () {
          const args = ArraySlice.call(arguments);
          if (Zone.current === Zone.root) {
            return originalMethod.apply(this, args);
          } else {
            return Zone.root.run(originalMethod, this, args);
          }
        };
      }
    });
  });

  Zone.__load_patch('queueMicrotask', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
    patchQueueMicrotask(global, api);
  });
}
