"use strict";
/*
 * @adonisjs/fold
 *
 * (c) Harminder Virk <virk@adonisjs.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.Ioc = void 0;
const utils_1 = require("@poppinss/utils");
const helpers_1 = require("@poppinss/utils/build/helpers");
const Fakes_1 = require("./Fakes");
const Bindings_1 = require("./Bindings");
const Injector_1 = require("./Injector");
const Resolver_1 = require("../Resolver");
const ImportAliases_1 = require("./ImportAliases");
const helpers_2 = require("../helpers");
const IocProxy_1 = require("./IocProxy");
const IocLookupException_1 = require("../Exceptions/IocLookupException");
class Ioc {
    constructor() {
        this.fakes = new Fakes_1.Fakes(this);
        this.bindings = new Bindings_1.Bindings(this);
        this.injector = new Injector_1.Injector(this);
        this.aliases = new ImportAliases_1.ImportAliases(this);
        /**
         * The current state of using proxies
         */
        this.usingProxies = false;
        /**
         * Define the module type for resolving auto import aliases. Defaults
         * to `cjs`
         */
        this.module = 'cjs';
    }
    /**
     * Registered aliases. The key is the alias and value is the
     * absolute directory path
     */
    get importAliases() {
        return this.aliases.list;
    }
    /**
     * Detect if the module export value is an esm module
     */
    isEsm(value) {
        return this.module === 'esm' ? true : (0, helpers_2.isEsm)(value);
    }
    /**
     * Wraps object and class to a proxy to enable the fakes
     * API
     */
    wrapAsProxy(namespace, value) {
        /**
         * Wrap objects inside proxy
         */
        if (helpers_1.types.isObject(value)) {
            return new IocProxy_1.IocProxyObject(namespace, value, this.fakes);
        }
        /**
         * Wrap class inside proxy
         */
        if (helpers_1.types.isClass(value)) {
            return (0, IocProxy_1.IocProxyClass)(namespace, value, this.fakes);
        }
        return value;
    }
    /**
     * Wrap value inside proxy by also inspecting for esm
     * default exports
     */
    wrapEsmModuleAsProxy(namespace, value) {
        /**
         * Wrap the default export of esm modules inside in a proxy and
         * not the entire module
         */
        if (this.isEsm(value)) {
            if (value.default) {
                /**
                 * We should never mutate the actual ESM module object and always clone it first
                 * for abvious reasons that objects are shared by reference
                 */
                const clonedModule = Object.getOwnPropertyNames(value).reduce((result, key) => {
                    result[key] = value[key];
                    return result;
                }, {});
                clonedModule.default = this.wrapAsProxy(namespace, clonedModule.default);
                return clonedModule;
            }
            /**
             * We don't proxy named exports as we don't have a good story on what to proxy
             *
             * - Should we proxy the whole module?
             * - Or should be expose api to allow proxying a selected set of modules
             */
            return value;
        }
        return this.wrapAsProxy(namespace, value);
    }
    /**
     * Makes an instance of a class by injecting dependencies
     */
    makeRaw(value, args) {
        return this.injector.make(value, args || []);
    }
    /**
     * Makes an instance of a class asynchronously by injecting dependencies
     */
    async makeRawAsync(value, args) {
        return this.injector.makeAsync(value, args || []);
    }
    /**
     * Enable/disable proxies. Proxies are mainly required for fakes to
     * work
     */
    useProxies(enable = true) {
        this.usingProxies = !!enable;
        return this;
    }
    /**
     * Register a binding with a callback. The callback return value will be
     * used when binding is resolved
     */
    bind(binding, callback) {
        (0, helpers_2.ensureIsFunction)(callback, '"ioc.bind" expect 2nd argument to be a function');
        this.bindings.register(binding, callback, false);
        return this;
    }
    /**
     * Same as the [[bind]] method, but registers a singleton only. Singleton's callback
     * is invoked only for the first time and then the cached value is used
     */
    singleton(binding, callback) {
        (0, helpers_2.ensureIsFunction)(callback, '"ioc.singleton" expect 2nd argument to be a function');
        this.bindings.register(binding, callback, true);
        return this;
    }
    /**
     * Define an import alias
     */
    alias(absolutePath, alias) {
        this.aliases.register(absolutePath, alias);
        return this;
    }
    /**
     * Register a fake for a namespace. Fakes works both for "bindings" and "import aliases".
     * Fakes only work when proxies are enabled using "useProxies".
     */
    fake(namespace, callback) {
        (0, helpers_2.ensureIsFunction)(callback, '"ioc.fake" expect 2nd argument to be a function');
        this.fakes.register(namespace, callback);
        return this;
    }
    /**
     * Clear selected or all the fakes. Calling the method with no arguments
     * will clear all the fakes
     */
    restore(namespace) {
        namespace ? this.fakes.delete(namespace) : this.fakes.clear();
        return this;
    }
    /**
     * Find if a fake has been registered for a given namespace
     */
    hasFake(namespace) {
        return this.fakes.has(namespace);
    }
    /**
     * Find if a binding exists for a given namespace
     */
    hasBinding(namespace) {
        return this.bindings.has(namespace);
    }
    /**
     * Find if a namespace is part of the auto import aliases. Returns false, when namespace
     * is an alias path but has an explicit binding too
     */
    isAliasPath(namespace) {
        if (this.bindings.has(namespace)) {
            return false;
        }
        return this.aliases.has(namespace);
    }
    /**
     * Lookup a namespace. The output contains the complete namespace,
     * along with its type. The type is an "alias" or a "binding".
     *
     * Null is returned when unable to lookup the namespace inside the container
     *
     * Note: This method just checks if a namespace is registered or binding
     *      or can be it resolved from auto import aliases or not. However,
     *      it doesn't check for the module existence on the disk.
     *
     * Optionally you can define a prefix namespace
     * to be used to build the complete namespace. For example:
     *
     * - namespace: UsersController
     * - prefixNamespace: App/Controllers/Http
     * - Output: App/Controllers/Http/UsersController
     *
     * Prefix namespace is ignored for absolute namespaces. For example:
     *
     * - namespace: /App/UsersController
     * - prefixNamespace: App/Controllers/Http
     * - Output: App/UsersController
     */
    lookup(namespace, prefixNamespace) {
        if (typeof namespace !== 'string' && namespace['namespace'] && namespace['type']) {
            return namespace;
        }
        /**
         * Ensure namespace is defined as a string only
         */
        if (typeof namespace !== 'string') {
            throw IocLookupException_1.IocLookupException.invalidNamespace();
        }
        /**
         * Build complete namespace
         */
        if (namespace.startsWith('/')) {
            namespace = namespace.substr(1);
        }
        else if (prefixNamespace) {
            namespace = `${prefixNamespace.replace(/\/$/, '')}/${namespace}`;
        }
        /**
         * Namespace is a binding
         */
        if (this.hasBinding(namespace)) {
            return {
                type: 'binding',
                namespace: namespace,
            };
        }
        /**
         * Namespace is an alias
         */
        if (this.isAliasPath(namespace)) {
            return {
                type: 'alias',
                namespace: namespace,
            };
        }
        return null;
    }
    /**
     * Same as [[lookup]]. But raises exception instead of returning null
     */
    lookupOrFail(namespace, prefixNamespace) {
        const lookupNode = this.lookup(namespace, prefixNamespace);
        if (!lookupNode) {
            throw IocLookupException_1.IocLookupException.lookupFailed(namespace);
        }
        return lookupNode;
    }
    /**
     * Resolve a binding by invoking the binding factory function. An exception
     * is raised, if the binding namespace is unregistered.
     */
    resolveBinding(binding) {
        if (this.trapCallback) {
            return this.trapCallback(binding);
        }
        const value = this.bindings.resolve(binding);
        if (this.usingProxies) {
            return this.wrapAsProxy(binding, value);
        }
        return value;
    }
    /**
     * Import namespace from the auto import aliases. This method assumes you are
     * using native ES modules
     */
    async import(namespace) {
        if (this.trapCallback) {
            return this.trapCallback(namespace);
        }
        const value = await this.aliases.resolveAsync(namespace);
        if (this.usingProxies) {
            return this.wrapEsmModuleAsProxy(namespace, value);
        }
        return value;
    }
    /**
     * Same as the "import" method, but uses CJS for requiring the module from its
     * path
     */
    require(namespace) {
        if (this.trapCallback) {
            return this.trapCallback(namespace);
        }
        const value = this.aliases.resolve(namespace);
        if (this.usingProxies) {
            return this.wrapEsmModuleAsProxy(namespace, value);
        }
        return value;
    }
    /**
     * The use method looks up a namespace inside both the bindings and the
     * auto import aliases
     */
    use(namespace) {
        if (this.trapCallback) {
            return this.trapCallback(typeof namespace === 'string' ? namespace : namespace['namespace']);
        }
        const lookupNode = this.lookupOrFail(namespace);
        if (lookupNode.type === 'alias') {
            return this.require(lookupNode.namespace);
        }
        return this.resolveBinding(lookupNode.namespace);
    }
    /**
     * Same as the [[use]] method, but instead uses ES modules for resolving
     * the auto import aliases
     */
    async useAsync(namespace) {
        if (this.trapCallback) {
            return this.trapCallback(typeof namespace === 'string' ? namespace : namespace['namespace']);
        }
        const lookupNode = this.lookupOrFail(namespace);
        if (lookupNode.type === 'alias') {
            return this.import(lookupNode.namespace);
        }
        return this.resolveBinding(lookupNode.namespace);
    }
    /**
     * Makes an instance of the class by first resolving it.
     */
    make(namespace, args) {
        const isContainerNamespace = typeof namespace === 'string' || (namespace['namespace'] && namespace['type']);
        /**
         * Value is not a container namespace or a lookup
         * node
         */
        if (!isContainerNamespace) {
            return this.makeRaw(namespace, args);
        }
        /**
         * Invoke trap callback (if registered)
         */
        if (this.trapCallback) {
            return this.trapCallback(typeof namespace === 'string' ? namespace : namespace['namespace']);
        }
        const lookupNode = this.lookupOrFail(namespace);
        /**
         * We do not touch bindings at all. The factory function
         * return value is used as it is
         */
        if (lookupNode.type === 'binding') {
            return this.resolveBinding(lookupNode.namespace);
        }
        const value = this.require(lookupNode.namespace);
        /**
         * We attempt to make an instance of only the export
         * default of a ES module
         */
        if (this.isEsm(value) && value.default) {
            return this.makeRaw(value.default, args || []);
        }
        return this.makeRaw(value, args);
    }
    /**
     * Same as the [[make]] method, but instead uses ES modules for resolving
     * the auto import aliases
     */
    async makeAsync(namespace, args) {
        const isContainerNamespace = typeof namespace === 'string' || (namespace['namespace'] && namespace['type']);
        /**
         * Value is not a container namespace or a lookup
         * node
         */
        if (!isContainerNamespace) {
            return this.makeRawAsync(namespace, args);
        }
        /**
         * Invoke trap callback (if registered)
         */
        if (this.trapCallback) {
            return this.trapCallback(typeof namespace === 'string' ? namespace : namespace['namespace']);
        }
        const lookupNode = this.lookupOrFail(namespace);
        /**
         * We do not touch bindings at all. The factory function
         * return value is used as it is
         */
        if (lookupNode.type === 'binding') {
            return this.resolveBinding(lookupNode.namespace);
        }
        const value = await this.import(lookupNode.namespace);
        /**
         * We attempt to make an instance of only the export
         * default of a ES module
         */
        if (this.isEsm(value) && value.default) {
            return this.makeRawAsync(value.default, args || []);
        }
        return this.makeRawAsync(value, args);
    }
    /**
     * Define a callback to be called when all of the container
     * bindings are available.
     *
     * Note: This method is exclusive for bindings and doesn't resolve
     * auto import aliases
     */
    withBindings(namespaces, cb) {
        if (namespaces.every((namespace) => this.hasBinding(namespace))) {
            /**
             * The callback accepts a tuple, whereas map returns an array. So we
             * need to cast the value to any by hand
             */
            cb(...namespaces.map((namespace) => this.resolveBinding(namespace)));
        }
    }
    /**
     * @deprecated: Use "withBindings" instead
     */
    with(namespaces, cb) {
        process.emitWarning('DeprecationWarning', 'container.with() is deprecated. Use container.withBindings() instead');
        return this.withBindings(namespaces, cb);
    }
    /**
     * Call method on an object and automatically resolve its depdencies
     */
    call(target, method, args) {
        if (typeof target[method] !== 'function') {
            throw new utils_1.Exception(`Missing method "${method}" on "${target.constructor.name}"`);
        }
        return this.injector.call(target, method, args || []);
    }
    /**
     * Same as [[call]], but uses ES modules for resolving the auto
     * import aliases
     */
    async callAsync(target, method, args) {
        if (typeof target[method] !== 'function') {
            throw new utils_1.Exception(`Missing method "${method}" on "${target.constructor.name}"`);
        }
        return this.injector.callAsync(target, method, args || []);
    }
    /**
     * Trap container lookup calls. It includes
     *
     * - Ioc.use
     * - Ioc.useAsync
     * - Ioc.make
     * - Ioc.makeAsync
     * - Ioc.require
     * - Ioc.import
     * - Ioc.resolveBinding
     */
    trap(callback) {
        this.trapCallback = callback;
        return this;
    }
    /**
     * Returns the resolver instance to resolve Ioc container bindings with
     * little ease. Since, the IocResolver uses an in-memory cache to
     * improve the lookup speed, we suggest keeping a reference to
     * the output of this method to leverage caching
     */
    getResolver(fallbackMethod, rcNamespaceKey, fallbackNamespace) {
        return new Resolver_1.IocResolver(this, fallbackMethod, rcNamespaceKey, fallbackNamespace);
    }
}
exports.Ioc = Ioc;
