// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.

// Based on https://github.com/nodejs/node/blob/889ad35d3d41e376870f785b0c1b669cb732013d/lib/internal/per_context/primordials.js
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
// This file subclasses and stores the JS builtins that come from the VM
// so that Node.js's builtin modules do not need to later look these up from
// the global proxy, which can be mutated by users.

// Use of primordials have sometimes a dramatic impact on performance, please
// benchmark all changes made in performance-sensitive areas of the codebase.
// See: https://github.com/nodejs/node/pull/38248

"use strict";

(() => {
  const primordials = {};

  const {
    defineProperty: ReflectDefineProperty,
    getOwnPropertyDescriptor: ReflectGetOwnPropertyDescriptor,
    ownKeys: ReflectOwnKeys,
  } = Reflect;

  // `uncurryThis` is equivalent to `func => Function.prototype.call.bind(func)`.
  // It is using `bind.bind(call)` to avoid using `Function.prototype.bind`
  // and `Function.prototype.call` after it may have been mutated by users.
  const { apply, bind, call } = Function.prototype;
  const uncurryThis = bind.bind(call);
  primordials.uncurryThis = uncurryThis;

  // `applyBind` is equivalent to `func => Function.prototype.apply.bind(func)`.
  // It is using `bind.bind(apply)` to avoid using `Function.prototype.bind`
  // and `Function.prototype.apply` after it may have been mutated by users.
  const applyBind = bind.bind(apply);
  primordials.applyBind = applyBind;

  // Methods that accept a variable number of arguments, and thus it's useful to
  // also create `${prefix}${key}Apply`, which uses `Function.prototype.apply`,
  // instead of `Function.prototype.call`, and thus doesn't require iterator
  // destructuring.
  const varargsMethods = [
    // 'ArrayPrototypeConcat' is omitted, because it performs the spread
    // on its own for arrays and array-likes with a truthy
    // @@isConcatSpreadable symbol property.
    "ArrayOf",
    "ArrayPrototypePush",
    "ArrayPrototypeUnshift",
    // 'FunctionPrototypeCall' is omitted, since there's 'ReflectApply'
    // and 'FunctionPrototypeApply'.
    "MathHypot",
    "MathMax",
    "MathMin",
    "StringPrototypeConcat",
    "TypedArrayOf",
  ];

  function getNewKey(key) {
    return typeof key === "symbol"
      ? `Symbol${key.description[7].toUpperCase()}${key.description.slice(8)}`
      : `${key[0].toUpperCase()}${key.slice(1)}`;
  }

  function copyAccessor(dest, prefix, key, { enumerable, get, set }) {
    ReflectDefineProperty(dest, `${prefix}Get${key}`, {
      value: uncurryThis(get),
      enumerable,
    });
    if (set !== undefined) {
      ReflectDefineProperty(dest, `${prefix}Set${key}`, {
        value: uncurryThis(set),
        enumerable,
      });
    }
  }

  function copyPropsRenamed(src, dest, prefix) {
    for (const key of ReflectOwnKeys(src)) {
      const newKey = getNewKey(key);
      const desc = ReflectGetOwnPropertyDescriptor(src, key);
      if ("get" in desc) {
        copyAccessor(dest, prefix, newKey, desc);
      } else {
        const name = `${prefix}${newKey}`;
        ReflectDefineProperty(dest, name, desc);
        if (varargsMethods.includes(name)) {
          ReflectDefineProperty(dest, `${name}Apply`, {
            // `src` is bound as the `this` so that the static `this` points
            // to the object it was defined on,
            // e.g.: `ArrayOfApply` gets a `this` of `Array`:
            value: applyBind(desc.value, src),
          });
        }
      }
    }
  }

  function copyPropsRenamedBound(src, dest, prefix) {
    for (const key of ReflectOwnKeys(src)) {
      const newKey = getNewKey(key);
      const desc = ReflectGetOwnPropertyDescriptor(src, key);
      if ("get" in desc) {
        copyAccessor(dest, prefix, newKey, desc);
      } else {
        const { value } = desc;
        if (typeof value === "function") {
          desc.value = value.bind(src);
        }

        const name = `${prefix}${newKey}`;
        ReflectDefineProperty(dest, name, desc);
        if (varargsMethods.includes(name)) {
          ReflectDefineProperty(dest, `${name}Apply`, {
            value: applyBind(value, src),
          });
        }
      }
    }
  }

  function copyPrototype(src, dest, prefix) {
    for (const key of ReflectOwnKeys(src)) {
      const newKey = getNewKey(key);
      const desc = ReflectGetOwnPropertyDescriptor(src, key);
      if ("get" in desc) {
        copyAccessor(dest, prefix, newKey, desc);
      } else {
        const { value } = desc;
        if (typeof value === "function") {
          desc.value = uncurryThis(value);
        }

        const name = `${prefix}${newKey}`;
        ReflectDefineProperty(dest, name, desc);
        if (varargsMethods.includes(name)) {
          ReflectDefineProperty(dest, `${name}Apply`, {
            value: applyBind(value),
          });
        }
      }
    }
  }

  // Create copies of configurable value properties of the global object
  [
    "Proxy",
    "globalThis",
  ].forEach((name) => {
    primordials[name] = globalThis[name];
  });

  // Create copy of isNaN
  primordials[isNaN.name] = isNaN;

  // Create copy of queueMicrotask
  primordials["queueMicrotask"] = queueMicrotask;

  // Create copies of URI handling functions
  [
    decodeURI,
    decodeURIComponent,
    encodeURI,
    encodeURIComponent,
  ].forEach((fn) => {
    primordials[fn.name] = fn;
  });

  // Create copies of the namespace objects
  [
    "JSON",
    "Math",
    "Proxy",
    "Reflect",
  ].forEach((name) => {
    copyPropsRenamed(globalThis[name], primordials, name);
  });

  // Create copies of intrinsic objects
  [
    "AggregateError",
    "Array",
    "ArrayBuffer",
    "BigInt",
    "BigInt64Array",
    "BigUint64Array",
    "Boolean",
    "DataView",
    "Date",
    "Error",
    "EvalError",
    "FinalizationRegistry",
    "Float32Array",
    "Float64Array",
    "Function",
    "Int16Array",
    "Int32Array",
    "Int8Array",
    "Map",
    "Number",
    "Object",
    "RangeError",
    "ReferenceError",
    "RegExp",
    "Set",
    "String",
    "Symbol",
    "SyntaxError",
    "TypeError",
    "URIError",
    "Uint16Array",
    "Uint32Array",
    "Uint8Array",
    "Uint8ClampedArray",
    "WeakMap",
    "WeakRef",
    "WeakSet",
  ].forEach((name) => {
    const original = globalThis[name];
    primordials[name] = original;
    copyPropsRenamed(original, primordials, name);
    copyPrototype(original.prototype, primordials, `${name}Prototype`);
  });

  // Create copies of intrinsic objects that require a valid `this` to call
  // static methods.
  // Refs: https://www.ecma-international.org/ecma-262/#sec-promise.all
  [
    "Promise",
  ].forEach((name) => {
    const original = globalThis[name];
    primordials[name] = original;
    copyPropsRenamedBound(original, primordials, name);
    copyPrototype(original.prototype, primordials, `${name}Prototype`);
  });

  // Create copies of abstract intrinsic objects that are not directly exposed
  // on the global object.
  // Refs: https://tc39.es/ecma262/#sec-%typedarray%-intrinsic-object
  [
    { name: "TypedArray", original: Reflect.getPrototypeOf(Uint8Array) },
    {
      name: "ArrayIterator",
      original: {
        prototype: Reflect.getPrototypeOf(Array.prototype[Symbol.iterator]()),
      },
    },
    {
      name: "StringIterator",
      original: {
        prototype: Reflect.getPrototypeOf(String.prototype[Symbol.iterator]()),
      },
    },
  ].forEach(({ name, original }) => {
    primordials[name] = original;
    // The static %TypedArray% methods require a valid `this`, but can't be bound,
    // as they need a subclass constructor as the receiver:
    copyPrototype(original, primordials, name);
    copyPrototype(original.prototype, primordials, `${name}Prototype`);
  });

  const {
    ArrayPrototypeForEach,
    FunctionPrototypeCall,
    Map,
    ObjectFreeze,
    ObjectSetPrototypeOf,
    Promise,
    PromisePrototypeThen,
    Set,
    SymbolIterator,
    WeakMap,
    WeakSet,
  } = primordials;

  // Because these functions are used by `makeSafe`, which is exposed
  // on the `primordials` object, it's important to use const references
  // to the primordials that they use:
  const createSafeIterator = (factory, next) => {
    class SafeIterator {
      constructor(iterable) {
        this._iterator = factory(iterable);
      }
      next() {
        return next(this._iterator);
      }
      [SymbolIterator]() {
        return this;
      }
    }
    ObjectSetPrototypeOf(SafeIterator.prototype, null);
    ObjectFreeze(SafeIterator.prototype);
    ObjectFreeze(SafeIterator);
    return SafeIterator;
  };

  primordials.SafeArrayIterator = createSafeIterator(
    primordials.ArrayPrototypeSymbolIterator,
    primordials.ArrayIteratorPrototypeNext,
  );
  primordials.SafeStringIterator = createSafeIterator(
    primordials.StringPrototypeSymbolIterator,
    primordials.StringIteratorPrototypeNext,
  );

  const copyProps = (src, dest) => {
    ArrayPrototypeForEach(ReflectOwnKeys(src), (key) => {
      if (!ReflectGetOwnPropertyDescriptor(dest, key)) {
        ReflectDefineProperty(
          dest,
          key,
          ReflectGetOwnPropertyDescriptor(src, key),
        );
      }
    });
  };

  /**
 * @type {typeof primordials.makeSafe}
 */
  const makeSafe = (unsafe, safe) => {
    if (SymbolIterator in unsafe.prototype) {
      const dummy = new unsafe();
      let next; // We can reuse the same `next` method.

      ArrayPrototypeForEach(ReflectOwnKeys(unsafe.prototype), (key) => {
        if (!ReflectGetOwnPropertyDescriptor(safe.prototype, key)) {
          const desc = ReflectGetOwnPropertyDescriptor(unsafe.prototype, key);
          if (
            typeof desc.value === "function" &&
            desc.value.length === 0 &&
            SymbolIterator in (FunctionPrototypeCall(desc.value, dummy) ?? {})
          ) {
            const createIterator = uncurryThis(desc.value);
            next ??= uncurryThis(createIterator(dummy).next);
            const SafeIterator = createSafeIterator(createIterator, next);
            desc.value = function () {
              return new SafeIterator(this);
            };
          }
          ReflectDefineProperty(safe.prototype, key, desc);
        }
      });
    } else {
      copyProps(unsafe.prototype, safe.prototype);
    }
    copyProps(unsafe, safe);

    ObjectSetPrototypeOf(safe.prototype, null);
    ObjectFreeze(safe.prototype);
    ObjectFreeze(safe);
    return safe;
  };
  primordials.makeSafe = makeSafe;

  // Subclass the constructors because we need to use their prototype
  // methods later.
  // Defining the `constructor` is necessary here to avoid the default
  // constructor which uses the user-mutable `%ArrayIteratorPrototype%.next`.
  primordials.SafeMap = makeSafe(
    Map,
    class SafeMap extends Map {
      constructor(i) {
        super(i);
      }
    },
  );
  primordials.SafeWeakMap = makeSafe(
    WeakMap,
    class SafeWeakMap extends WeakMap {
      constructor(i) {
        super(i);
      }
    },
  );

  primordials.SafeSet = makeSafe(
    Set,
    class SafeSet extends Set {
      constructor(i) {
        super(i);
      }
    },
  );
  primordials.SafeWeakSet = makeSafe(
    WeakSet,
    class SafeWeakSet extends WeakSet {
      constructor(i) {
        super(i);
      }
    },
  );

  primordials.SafeFinalizationRegistry = makeSafe(
    FinalizationRegistry,
    class SafeFinalizationRegistry extends FinalizationRegistry {
      constructor(cleanupCallback) {
        super(cleanupCallback);
      }
    },
  );

  primordials.SafeWeakRef = makeSafe(
    WeakRef,
    class SafeWeakRef extends WeakRef {
      constructor(target) {
        super(target);
      }
    },
  );

  const SafePromise = makeSafe(
    Promise,
    class SafePromise extends Promise {
      constructor(executor) {
        super(executor);
      }
    },
  );

  primordials.PromisePrototypeCatch = (thisPromise, onRejected) =>
    PromisePrototypeThen(thisPromise, undefined, onRejected);

  /**
   * Attaches a callback that is invoked when the Promise is settled (fulfilled or
   * rejected). The resolved value cannot be modified from the callback.
   * Prefer using async functions when possible.
   * @param {Promise<any>} thisPromise
   * @param {() => void) | undefined | null} onFinally The callback to execute
   *        when the Promise is settled (fulfilled or rejected).
   * @returns A Promise for the completion of the callback.
   */
  primordials.SafePromisePrototypeFinally = (thisPromise, onFinally) =>
    // Wrapping on a new Promise is necessary to not expose the SafePromise
    // prototype to user-land.
    new Promise((a, b) =>
      new SafePromise((a, b) => PromisePrototypeThen(thisPromise, a, b))
        .finally(onFinally)
        .then(a, b)
    );

  ObjectSetPrototypeOf(primordials, null);
  ObjectFreeze(primordials);

  // Provide bootstrap namespace
  globalThis.__bootstrap = { primordials };
})();