/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable max-len */
/* eslint-disable @typescript-eslint/no-explicit-any */
/*
 * @adonisjs/fold
 *
 * (c) AdonisJS
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
import { isClass } from './helpers.js';
import { containerProvider } from './provider.js';
/**
 * Container resolver exposes the APIs to resolve bindings. You can think
 * of resolver as an isolated container instance, with only the APIs
 * to resolve bindings.
 *
 * ```ts
 * const container = new Container()
 * const resolver = container.createResolver()
 *
 * await resolver.make(BINDING_NAME)
 * await resolver.make(CLASS_CONSTRUCTOR)
 * ```
 */
export class ContainerResolver {
    /**
     * Reference to the container aliases. They are shared between the container
     * and resolver.
     *
     * We do not mutate this property within the resolver
     */
    #containerAliases;
    /**
     * Pre-registered contextual bindings. They are shared between the container
     * and resolver.
     *
     * We do not mutate this property within the resolver
     */
    #containerContextualBindings;
    /**
     * Pre-registered bindings. They are shared between the container
     * and resolver.
     *
     * We do not mutate this property within the resolver
     */
    #containerBindings;
    /**
     * Pre-registered bindings. They are shared between the container
     * and resolver.
     *
     * We mutate this property within the resolver to set singleton
     * cached values
     */
    #containerBindingValues;
    /**
     * Pre-registered swaps for bindings. They are shared between
     * the container and resolver.
     *
     * We do not mutate this property within the resolver
     */
    #containerSwaps;
    /**
     * Reference to the container hooks
     */
    #containerHooks;
    /**
     * Binding values local to the resolver
     */
    #bindingValues = new Map();
    /**
     * Container options
     */
    #options;
    constructor(container, options) {
        this.#containerBindings = container.bindings;
        this.#containerBindingValues = container.bindingValues;
        this.#containerSwaps = container.swaps;
        this.#containerHooks = container.hooks;
        this.#containerAliases = container.aliases;
        this.#containerContextualBindings = container.contextualBindings;
        this.#options = options;
    }
    /**
     * Constructs exception for invalid binding value
     */
    #invalidBindingException(parent, binding) {
        if (parent) {
            const error = new Error(`Cannot inject "${binding}" in "[class ${parent.name}]"`);
            // error.help = 'The value is not a valid class'
            return error;
        }
        return new Error(`Cannot construct value "${binding}" using container`);
    }
    /**
     * Constructs exception for binding with missing dependencies
     */
    #missingDependenciesException(parent, binding) {
        if (parent) {
            const error = new Error(`Cannot inject "[class ${binding.name}]" in "[class ${parent.name}]"`);
            // error.help = `Container is not able to resolve "${parent.name}" class dependencies. Did you forget to use @inject() decorator?`
            return error;
        }
        return new Error(`Cannot construct "[class ${binding.name}]" class. Container is not able to resolve its dependencies. Did you forget to use @inject() decorator?`);
    }
    /**
     * Returns the provider for the class constructor
     */
    #getBindingProvider(binding) {
        return binding.containerProvider;
    }
    /**
     * Returns the binding resolver for a parent and a binding. Returns
     * undefined when no contextual binding exists
     */
    #getBindingResolver(parent, binding) {
        const parentBindings = this.#containerContextualBindings.get(parent);
        if (!parentBindings) {
            return;
        }
        const bindingResolver = parentBindings.get(binding);
        if (!bindingResolver) {
            return;
        }
        return bindingResolver.resolver;
    }
    /**
     * Notify emitter
     */
    #emit(binding, value) {
        if (!this.#options.emitter) {
            return;
        }
        this.#options.emitter.emit('container_binding:resolved', { binding, value });
    }
    /**
     * Execute hooks for a given binding
     */
    async #execHooks(binding, value) {
        const callbacks = this.#containerHooks.get(binding);
        if (!callbacks || callbacks.size === 0) {
            return;
        }
        for (const callback of callbacks) {
            await callback(value, this);
        }
    }
    hasBinding(binding) {
        return (this.#containerAliases.has(binding) ||
            this.#bindingValues.has(binding) ||
            this.#containerBindingValues.has(binding) ||
            this.#containerBindings.has(binding));
    }
    hasAllBindings(bindings) {
        return bindings.every((binding) => this.hasBinding(binding));
    }
    /**
     * Resolves binding in context of a parent. The method is same as
     * the "make" method, but instead takes a parent class
     * constructor.
     */
    async resolveFor(parent, binding, runtimeValues) {
        const isAClass = isClass(binding);
        /**
         * Raise exception when the binding is not a string, a class constructor
         * or a symbol.
         */
        if (typeof binding !== 'string' && typeof binding !== 'symbol' && !isAClass) {
            throw this.#invalidBindingException(parent, binding);
        }
        /**
         * Entertain swaps with highest priority. The swaps can only exists for
         * class constructors.
         */
        if (isAClass && this.#containerSwaps.has(binding)) {
            const resolver = this.#containerSwaps.get(binding);
            const value = await resolver(this, runtimeValues);
            // if (debug.enabled) {
            //   debug('resolved swap for binding %O, resolved value :%O', binding, value)
            // }
            /**
             * Executing hooks and emitting events for the swaps is
             * debatable for now
             */
            await this.#execHooks(binding, value);
            this.#emit(binding, value);
            return value;
        }
        /**
         * Resolving contextual binding. Contextual bindings can only exists for
         * class constructors
         */
        const contextualResolver = isAClass && this.#getBindingResolver(parent, binding);
        if (contextualResolver) {
            const value = await contextualResolver(this, runtimeValues);
            // if (debug.enabled) {
            //   debug('resolved using contextual resolver binding %O, resolved value :%O', binding, value)
            // }
            await this.#execHooks(binding, value);
            this.#emit(binding, value);
            return value;
        }
        /**
         * First priority is given to the RESOLVER binding values
         */
        if (this.#bindingValues.has(binding)) {
            const value = this.#bindingValues.get(binding);
            // if (debug.enabled) {
            //   debug('resolved from resolver values %O, resolved value :%O', binding, value)
            // }
            this.#emit(binding, value);
            return value;
        }
        /**
         * Next priority is given to the CONTAINER binding values
         */
        if (this.#containerBindingValues.has(binding)) {
            const value = this.#containerBindingValues.get(binding);
            // if (debug.enabled) {
            //   debug('resolved from container values %O, resolved value :%O', binding, value)
            // }
            this.#emit(binding, value);
            return value;
        }
        /**
         * Followed by the CONTAINER bindings
         */
        if (this.#containerBindings.has(binding)) {
            const { resolver, isSingleton } = this.#containerBindings.get(binding);
            let value;
            let executeHooks = isSingleton ? false : true;
            /**
             * Invoke binding resolver to get the value. In case of singleton,
             * the "enqueue" method returns an object with the value and a
             * boolean telling if a cached value is resolved.
             */
            if (isSingleton) {
                const result = await resolver(this, runtimeValues);
                value = result.value;
                executeHooks = !result.cached;
            }
            else {
                value = await resolver(this, runtimeValues);
            }
            // if (debug.enabled) {
            //   debug('resolved binding %O, resolved value :%O', binding, value)
            // }
            if (executeHooks) {
                await this.#execHooks(binding, value);
            }
            this.#emit(binding, value);
            return value;
        }
        /**
         * Create an instance of the class with its constructor
         * dependencies.
         */
        if (isAClass) {
            let dependencies = [];
            const classConstructor = binding;
            const bindingProvider = this.#getBindingProvider(classConstructor);
            if (bindingProvider) {
                dependencies = await bindingProvider(classConstructor, '_constructor', this, containerProvider, runtimeValues);
            }
            else {
                dependencies = await containerProvider(classConstructor, '_constructor', this, runtimeValues);
            }
            /**
             * Class has dependencies for which we do not have runtime values and neither
             * we have typehints. Therefore we throw an exception
             */
            if (dependencies.length < classConstructor.length) {
                throw this.#missingDependenciesException(parent, binding);
            }
            const value = new binding(...dependencies);
            // if (debug.enabled) {
            //   debug('constructed class %O, resolved value :%O', binding, value)
            // }
            await this.#execHooks(binding, value);
            this.#emit(binding, value);
            return value;
        }
        throw new Error(`Cannot resolve binding "${String(binding)}" from the container`);
    }
    async make(binding, runtimeValues) {
        /**
         * Make alias
         */
        if (this.#containerAliases.has(binding)) {
            return this.resolveFor(null, this.#containerAliases.get(binding), runtimeValues);
        }
        return this.resolveFor(null, binding, runtimeValues);
    }
    /**
     * Call a method on an object by injecting its dependencies. The method
     * dependencies are resolved in the same manner as a class constructor
     * dependencies.
     *
     * ```ts
     * await resolver.call(await resolver.make(UsersController), 'index')
     * ```
     */
    async call(value, method, runtimeValues) {
        if (typeof value[method] !== 'function') {
            throw new Error(`Missing method "${String(method)}" on "${value}"`);
        }
        // if (debug.enabled) {
        //   debug('calling method %s, on value :%O', method, value)
        // }
        let dependencies = [];
        const binding = value.constructor;
        const bindingProvider = this.#getBindingProvider(binding);
        if (bindingProvider) {
            dependencies = await bindingProvider(binding, method, this, containerProvider, runtimeValues);
        }
        else {
            dependencies = await containerProvider(binding, method, this, runtimeValues);
        }
        /**
         * Method has dependencies for which we do not have runtime values and neither
         * we have typehints. Therefore we throw an exception
         */
        if (dependencies.length < value[method].length) {
            throw new Error(`Cannot call "${binding.name}.${String(method)}" method. Container is not able to resolve its dependencies. Did you forget to use @inject() decorator?`);
        }
        return value[method](...dependencies);
    }
    bindValue(binding, value) {
        if (typeof binding !== 'string' && typeof binding !== 'symbol' && !isClass(binding)) {
            throw new Error('The container binding key must be of type "string", "symbol", or a "class constructor"');
        }
        // debug('adding value to resolver "%O"', binding)
        this.#bindingValues.set(binding, value);
    }
}
//# sourceMappingURL=resolver.js.map