text: "use strict";
  Object.defineProperty(exports, "__esModule", { value: true });
  const constants_1 = require("@nestjs/common/constants");
  const shared_utils_1 = require("@nestjs/common/utils/shared.utils");
  const runtime_exception_1 = require("../errors/exceptions/runtime.exception");
  const undefined_dependency_exception_1 = require("../errors/exceptions/undefined-dependency.exception");
  const unknown_dependencies_exception_1 = require("../errors/exceptions/unknown-dependencies.exception");
  const constants_2 = require("./constants");
  const inquirer_1 = require("./inquirer");
  const instance_wrapper_1 = require("./instance-wrapper");
  class Injector {
      async loadMiddleware(wrapper, collection, module, contextId = constants_2.STATIC_CONTEXT, inquirer) {
          const { metatype } = wrapper;
          const targetWrapper = collection.get(metatype.name);
          if (!shared_utils_1.isUndefined(targetWrapper.instance)) {
              return;
          }
          const loadInstance = (instances) => {
              targetWrapper.instance = targetWrapper.isDependencyTreeStatic()
                  ? new metatype(...instances)
                  : Object.create(metatype.prototype);
          };
          await this.resolveConstructorParams(wrapper, module, null, loadInstance, contextId, inquirer);
      }
      async loadController(wrapper, module, contextId = constants_2.STATIC_CONTEXT) {
          const controllers = module.controllers;
          await this.loadInstance(wrapper, controllers, module, contextId, wrapper);
          await this.loadEnhancersPerContext(wrapper, contextId, wrapper);
      }
      async loadInjectable(wrapper, module, contextId = constants_2.STATIC_CONTEXT, inquirer) {
          const injectables = module.injectables;
          await this.loadInstance(wrapper, injectables, module, contextId, inquirer);
      }
      async loadProvider(wrapper, module, contextId = constants_2.STATIC_CONTEXT, inquirer) {
          const providers = module.providers;
          await this.loadInstance(wrapper, providers, module, contextId, inquirer);
          await this.loadEnhancersPerContext(wrapper, contextId, wrapper);
      }
      loadPrototype({ name }, collection, contextId = constants_2.STATIC_CONTEXT) {
          if (!collection) {
              return;
          }
          const target = collection.get(name);
          const instance = target.createPrototype(contextId);
          if (instance) {
              const wrapper = new instance_wrapper_1.InstanceWrapper(Object.assign(Object.assign({}, target), { instance }));
              collection.set(name, wrapper);
          }
      }
      applyDoneHook(wrapper) {
          let done;
          wrapper.donePromise = new Promise((resolve, reject) => {
              done = resolve;
          });
          wrapper.isPending = true;
          return done;
      }
      async loadInstance(wrapper, collection, module, contextId = constants_2.STATIC_CONTEXT, inquirer) {
          const inquirerId = this.getInquirerId(inquirer);
          const instanceHost = wrapper.getInstanceByContextId(contextId, inquirerId);
          if (instanceHost.isPending) {
              return instanceHost.donePromise;
          }
          const done = this.applyDoneHook(instanceHost);
          const { name, inject } = wrapper;
          const targetWrapper = collection.get(name);
          if (shared_utils_1.isUndefined(targetWrapper)) {
              throw new runtime_exception_1.RuntimeException();
          }
          if (instanceHost.isResolved) {
              return done();
          }
          const callback = async (instances) => {
              const properties = await this.resolveProperties(wrapper, module, inject, contextId, wrapper, inquirer);
              const instance = await this.instantiateClass(instances, wrapper, targetWrapper, contextId, inquirer);
              this.applyProperties(instance, properties);
              done();
          };
          await this.resolveConstructorParams(wrapper, module, inject, callback, contextId, wrapper, inquirer);
      }
      async resolveConstructorParams(wrapper, module, inject, callback, contextId = constants_2.STATIC_CONTEXT, inquirer, parentInquirer) {
          const inquirerId = this.getInquirerId(inquirer);
          const metadata = wrapper.getCtorMetadata();
          if (metadata && contextId !== constants_2.STATIC_CONTEXT) {
              const deps = await this.loadCtorMetadata(metadata, contextId, inquirer, parentInquirer);
              return callback(deps);
          }
          const dependencies = shared_utils_1.isNil(inject)
              ? this.reflectConstructorParams(wrapper.metatype)
              : inject;
          const optionalDependenciesIds = shared_utils_1.isNil(inject)
              ? this.reflectOptionalParams(wrapper.metatype)
              : [];
          let isResolved = true;
          const resolveParam = async (param, index) => {
              try {
                  if (this.isInquirer(param, parentInquirer)) {
                      return parentInquirer && parentInquirer.instance;
                  }
                  const paramWrapper = await this.resolveSingleParam(wrapper, param, { index, dependencies }, module, contextId, inquirer, index);
                  const instanceHost = paramWrapper.getInstanceByContextId(contextId, inquirerId);
                  if (!instanceHost.isResolved && !paramWrapper.forwardRef) {
                      isResolved = false;
                  }
                  return instanceHost && instanceHost.instance;
              }
              catch (err) {
                  const isOptional = optionalDependenciesIds.includes(index);
                  if (!isOptional) {
                      throw err;
                  }
                  return undefined;
              }
          };
          const instances = await Promise.all(dependencies.map(resolveParam));
          isResolved && (await callback(instances));
      }
      reflectConstructorParams(type) {
          const paramtypes = Reflect.getMetadata(constants_1.PARAMTYPES_METADATA, type) || [];
          const selfParams = this.reflectSelfParams(type);
          selfParams.forEach(({ index, param }) => (paramtypes[index] = param));
          return paramtypes;
      }
      reflectOptionalParams(type) {
          return Reflect.getMetadata(constants_1.OPTIONAL_DEPS_METADATA, type) || [];
      }
      reflectSelfParams(type) {
          return Reflect.getMetadata(constants_1.SELF_DECLARED_DEPS_METADATA, type) || [];
      }
      async resolveSingleParam(wrapper, param, dependencyContext, module, contextId = constants_2.STATIC_CONTEXT, inquirer, keyOrIndex) {
          if (shared_utils_1.isUndefined(param)) {
              throw new undefined_dependency_exception_1.UndefinedDependencyException(wrapper.name, dependencyContext, module);
          }
          const token = this.resolveParamToken(wrapper, param);
          return this.resolveComponentInstance(module, shared_utils_1.isFunction(token) ? token.name : token, dependencyContext, wrapper, contextId, inquirer, keyOrIndex);
      }
      resolveParamToken(wrapper, param) {
          if (!param.forwardRef) {
              return param;
          }
          wrapper.forwardRef = true;
          return param.forwardRef();
      }
      async resolveComponentInstance(module, name, dependencyContext, wrapper, contextId = constants_2.STATIC_CONTEXT, inquirer, keyOrIndex) {
          const providers = module.providers;
          const instanceWrapper = await this.lookupComponent(providers, module, Object.assign(Object.assign({}, dependencyContext), { name }), wrapper, contextId, inquirer);
          shared_utils_1.isString(keyOrIndex)
              ? wrapper.addPropertiesMetadata(keyOrIndex, instanceWrapper)
              : wrapper.addCtorMetadata(keyOrIndex, instanceWrapper);
          return this.resolveComponentHost(module, instanceWrapper, contextId, inquirer);
      }
      async resolveComponentHost(module, instanceWrapper, contextId = constants_2.STATIC_CONTEXT, inquirer) {
          const inquirerId = this.getInquirerId(inquirer);
          const instanceHost = instanceWrapper.getInstanceByContextId(contextId, inquirerId);
          if (!instanceHost.isResolved && !instanceWrapper.forwardRef) {
              await this.loadProvider(instanceWrapper, module, contextId, inquirer);
          }
          else if (!instanceHost.isResolved &&
              instanceWrapper.forwardRef &&
              (contextId !== constants_2.STATIC_CONTEXT || !!inquirerId)) {
              /**
               * When circular dependency has been detected between
               * either request/transient providers, we have to asynchronously
               * resolve instance host for a specific contextId or inquirer, to ensure
               * that eventual lazily created instance will be merged with the prototype
               * instantiated beforehand.
               */
              instanceHost.donePromise &&
                  instanceHost.donePromise.then(() => this.loadProvider(instanceWrapper, module, contextId, inquirer));
          }
          if (instanceWrapper.async) {
              const host = instanceWrapper.getInstanceByContextId(contextId, inquirerId);
              host.instance = await host.instance;
              instanceWrapper.setInstanceByContextId(contextId, host, inquirerId);
          }
          return instanceWrapper;
      }
      async lookupComponent(providers, module, dependencyContext, wrapper, contextId = constants_2.STATIC_CONTEXT, inquirer) {
          const { name } = dependencyContext;
          if (wrapper && wrapper.name === name) {
              throw new unknown_dependencies_exception_1.UnknownDependenciesException(wrapper.name, dependencyContext, module);
          }
          const scanInExports = () => this.lookupComponentInExports(dependencyContext, module, wrapper, contextId, inquirer);
          return providers.has(name) ? providers.get(name) : scanInExports();
      }
      async lookupComponentInExports(dependencyContext, module, wrapper, contextId = constants_2.STATIC_CONTEXT, inquirer) {
          const instanceWrapper = await this.lookupComponentInImports(module, dependencyContext.name, wrapper, [], contextId, inquirer);
          if (shared_utils_1.isNil(instanceWrapper)) {
              throw new unknown_dependencies_exception_1.UnknownDependenciesException(wrapper.name, dependencyContext, module);
          }
          return instanceWrapper;
      }
      async lookupComponentInImports(module, name, wrapper, moduleRegistry = [], contextId = constants_2.STATIC_CONTEXT, inquirer, isTraversing) {
          let instanceWrapperRef = null;
          const imports = module.imports || new Set();
          const identity = (item) => item;
          let children = [...imports.values()].filter(identity);
          if (isTraversing) {
              const contextModuleExports = module.exports;
              children = children.filter(child => contextModuleExports.has(child.metatype && child.metatype.name));
          }
          for (const relatedModule of children) {
              if (moduleRegistry.includes(relatedModule.id)) {
                  continue;
              }
              moduleRegistry.push(relatedModule.id);
              const { providers, exports } = relatedModule;
              if (!exports.has(name) || !providers.has(name)) {
                  const instanceRef = await this.lookupComponentInImports(relatedModule, name, wrapper, moduleRegistry, contextId, inquirer, true);
                  if (instanceRef) {
                      return instanceRef;
                  }
                  continue;
              }
              instanceWrapperRef = providers.get(name);
              const inquirerId = this.getInquirerId(inquirer);
              const instanceHost = instanceWrapperRef.getInstanceByContextId(contextId, inquirerId);
              if (!instanceHost.isResolved && !instanceWrapperRef.forwardRef) {
                  await this.loadProvider(instanceWrapperRef, relatedModule, contextId, wrapper);
                  break;
              }
          }
          return instanceWrapperRef;
      }
      async resolveProperties(wrapper, module, inject, contextId = constants_2.STATIC_CONTEXT, inquirer, parentInquirer) {
          if (!shared_utils_1.isNil(inject)) {
              return [];
          }
          const metadata = wrapper.getPropertiesMetadata();
          if (metadata && contextId !== constants_2.STATIC_CONTEXT) {
              return this.loadPropertiesMetadata(metadata, contextId, inquirer);
          }
          const properties = this.reflectProperties(wrapper.metatype);
          const instances = await Promise.all(properties.map(async (item) => {
              try {
                  const dependencyContext = {
                      key: item.key,
                      name: item.name,
                  };
                  if (this.isInquirer(item.name, parentInquirer)) {
                      return parentInquirer && parentInquirer.instance;
                  }
                  const paramWrapper = await this.resolveSingleParam(wrapper, item.name, dependencyContext, module, contextId, inquirer, item.key);
                  if (!paramWrapper) {
                      return undefined;
                  }
                  const inquirerId = this.getInquirerId(inquirer);
                  const instanceHost = paramWrapper.getInstanceByContextId(contextId, inquirerId);
                  return instanceHost.instance;
              }
              catch (err) {
                  if (!item.isOptional) {
                      throw err;
                  }
                  return undefined;
              }
          }));
          return properties.map((item, index) => (Object.assign(Object.assign({}, item), { instance: instances[index] })));
      }
      reflectProperties(type) {
          const properties = Reflect.getMetadata(constants_1.PROPERTY_DEPS_METADATA, type) || [];
          const optionalKeys = Reflect.getMetadata(constants_1.OPTIONAL_PROPERTY_DEPS_METADATA, type) || [];
          return properties.map((item) => (Object.assign(Object.assign({}, item), { name: item.type, isOptional: optionalKeys.includes(item.key) })));
      }
      applyProperties(instance, properties) {
          if (!shared_utils_1.isObject(instance)) {
              return undefined;
          }
          properties
              .filter(item => !shared_utils_1.isNil(item.instance))
              .forEach(item => (instance[item.key] = item.instance));
      }
      async instantiateClass(instances, wrapper, targetMetatype, contextId = constants_2.STATIC_CONTEXT, inquirer) {
          const { metatype, inject } = wrapper;
          const inquirerId = this.getInquirerId(inquirer);
          const instanceHost = targetMetatype.getInstanceByContextId(contextId, inquirerId);
          const isStatic = wrapper.isStatic(contextId, inquirer);
          const isInRequestScope = wrapper.isInRequestScope(contextId, inquirer);
          const isLazyTransient = wrapper.isLazyTransient(contextId, inquirer);
          const isInContext = isStatic || isInRequestScope || isLazyTransient;
          if (shared_utils_1.isNil(inject) && isInContext) {
              instanceHost.instance = wrapper.forwardRef
                  ? Object.assign(instanceHost.instance, new metatype(...instances))
                  : new metatype(...instances);
          }
          else if (isInContext) {
              const factoryReturnValue = targetMetatype.metatype(...instances);
              instanceHost.instance = await factoryReturnValue;
          }
          instanceHost.isResolved = true;
          return instanceHost.instance;
      }
      async loadPerContext(instance, module, collection, ctx) {
          const wrapper = collection.get(instance.constructor && instance.constructor.name);
          await this.loadInstance(wrapper, collection, module, ctx, wrapper);
          await this.loadEnhancersPerContext(wrapper, ctx, wrapper);
          const host = wrapper.getInstanceByContextId(ctx);
          return host && host.instance;
      }
      async loadEnhancersPerContext(wrapper, ctx, inquirer) {
          const enhancers = wrapper.getEnhancersMetadata() || [];
          const loadEnhancer = (item) => {
              const hostModule = item.host;
              return this.loadInstance(item, hostModule.injectables, hostModule, ctx, inquirer);
          };
          await Promise.all(enhancers.map(loadEnhancer));
      }
      async loadCtorMetadata(metadata, contextId, inquirer, parentInquirer) {
          const hosts = await Promise.all(metadata.map(async (item) => this.resolveScopedComponentHost(item, contextId, inquirer, parentInquirer)));
          const inquirerId = this.getInquirerId(inquirer);
          return hosts.map(item => item.getInstanceByContextId(contextId, inquirerId).instance);
      }
      async loadPropertiesMetadata(metadata, contextId, inquirer) {
          const dependenciesHosts = await Promise.all(metadata.map(async ({ wrapper: item, key }) => ({
              key,
              host: await this.resolveComponentHost(item.host, item, contextId, inquirer),
          })));
          const inquirerId = this.getInquirerId(inquirer);
          return dependenciesHosts.map(({ key, host }) => ({
              key,
              name: key,
              instance: host.getInstanceByContextId(contextId, inquirerId).instance,
          }));
      }
      getInquirerId(inquirer) {
          return inquirer && inquirer.id;
      }
      resolveScopedComponentHost(item, contextId, inquirer, parentInquirer) {
          return this.isInquirerRequest(item, parentInquirer)
              ? parentInquirer
              : this.resolveComponentHost(item.host, item, contextId, inquirer);
      }
      isInquirerRequest(item, parentInquirer) {
          return item.isTransient && item.name === inquirer_1.INQUIRER && parentInquirer;
      }
      isInquirer(param, parentInquirer) {
          return param === inquirer_1.INQUIRER && parentInquirer;
      }
  }
  exports.Injector = Injector;
  