/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* 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 { enqueue, isClass } from './helpers.js';
import { ContainerResolver } from './resolver.js';
import { ContextBindingsBuilder } from './contextual_bindings_builder.js';
/**
 * The container class exposes the API to register bindings, values
 * and resolve them.
 *
 * Known bindings types can be defined at the time of the constructing
 * the container.
 *
 * ```ts
 * new Container<{ 'route': Route, encryption: Encryption }>()
 * ```
 *
 * You can resolve bindings and construct classes as follows
 *
 * ```ts
 * await container.make(BINDING_NAME)
 * await container.make(CLASS_CONSTRUCTOR)
 * ```
 */
export class Container {
    /**
     * A set of defined aliases for the bindings
     */
    #aliases = new Map();
    /**
     * Contextual bindings are same as binding, but instead defined
     * for a parent class constructor.
     *
     * The contextual bindings can only be registered for class constructors, because
     * that is what gets injected to the class.
     */
    #contextualBindings = new Map();
    /**
     * A collection of bindings with registered swapped implementations. Swaps can only
     * be define for a class, because the goal is swap the dependency tree defined
     * using the Inject decorator and inject decorator does not take anything
     * other than a class.
     */
    #swaps = new Map();
    /**
     * Registered bindings. Singleton and normal bindings, both are
     * registered inside the bindings map
     */
    #bindings = new Map();
    /**
     * Registered bindings as values. The values are preferred over the bindings.
     */
    #bindingValues = new Map();
    /**
     * Registered hooks.
     */
    #hooks = new Map();
    /**
     * Container options
     */
    #options;
    constructor(options) {
        this.#options = options || {};
    }
    /**
     * Define an emitter instance to use
     */
    useEmitter(emitter) {
        this.#options.emitter = emitter;
        return this;
    }
    /**
     * Create a container resolver to resolve bindings, or make classes.
     *
     * ```ts
     * const resolver = container.createResolver()
     * await resolver.make(CLASS_CONSTRUCTOR)
     * ```
     *
     * Bind values with the resolver. Resolver values are isolated from the
     * container.
     *
     * ```ts
     * resolver.bindValue(HttpContext, new HttpContext())
     * await resolver.make(UsersController)
     * ```
     */
    createResolver() {
        return new ContainerResolver({
            bindings: this.#bindings,
            bindingValues: this.#bindingValues,
            swaps: this.#swaps,
            hooks: this.#hooks,
            aliases: this.#aliases,
            contextualBindings: this.#contextualBindings,
        }, this.#options);
    }
    hasBinding(binding) {
        return this.#aliases.has(binding) || this.#bindingValues.has(binding) || this.#bindings.has(binding);
    }
    hasAllBindings(bindings) {
        return bindings.every((binding) => this.hasBinding(binding));
    }
    make(binding, runtimeValues) {
        return this.createResolver().make(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 container.call(await container.make(UsersController), 'index')
     * ```
     */
    call(value, method, runtimeValues) {
        return this.createResolver().call(value, method, runtimeValues);
    }
    /**
     * Register an alias for a binding. The value can be a reference
     * to an existing binding or to a class constructor that will
     * instantiate to the same value as the alias.
     */
    alias(
    /**
     * An alias must always be defined as a string or a symbol. Classes cannot be
     * aliases
     */
    alias, 
    /**
     * The value should either be the constructor point to the alias value
     * or reference to binding that has the same value as the alias
     */
    value) {
        if (typeof alias !== 'string' && typeof alias !== 'symbol') {
            throw new Error('The container alias key must be of type "string" or "symbol"');
        }
        this.#aliases.set(alias, value);
    }
    bind(binding, resolver) {
        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 binding to container "%O"', binding)
        this.#bindings.set(binding, { resolver, isSingleton: false });
    }
    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 container %O', binding)
        this.#bindingValues.set(binding, value);
    }
    singleton(binding, resolver) {
        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 singleton to container %O', binding)
        this.#bindings.set(binding, { resolver: enqueue(resolver), isSingleton: true });
    }
    /**
     * Define a fake implementation for a binding or a class constructor.
     * Fakes have the highest priority when resolving dependencies
     * from the container.
     */
    swap(binding, resolver) {
        if (!isClass(binding)) {
            throw new Error(`Cannot call swap on value "${binding}". Only classes can be swapped`);
        }
        // debug('defining swap for %O', binding)
        this.#swaps.set(binding, resolver);
    }
    /**
     * Restore binding by removing its swap
     */
    restore(binding) {
        // debug('removing swap for %s', binding)
        this.#swaps.delete(binding);
    }
    /**
     * Restore mentioned or all bindings by removing
     * their swaps
     */
    restoreAll(bindings) {
        if (!bindings) {
            // debug('removing all swaps')
            this.#swaps.clear();
            return;
        }
        for (const binding of bindings) {
            this.restore(binding);
        }
    }
    resolving(binding, callback) {
        binding = this.#aliases.get(binding) || binding;
        if (!this.#hooks.has(binding)) {
            this.#hooks.set(binding, new Set());
        }
        const callbacks = this.#hooks.get(binding);
        callbacks.add(callback);
    }
    /**
     * Create a contextual builder to define contextual bindings
     */
    when(parent) {
        return new ContextBindingsBuilder(parent, this);
    }
    /**
     * Add a contextual binding for a given class constructor. A
     * contextual takes a parent, parent's dependency and a callback
     * to self resolve the dependency.
     *
     * For example:
     * - When "UsersController"
     * - Asks for "Hash class"
     * - Provide "Argon2" implementation
     */
    contextualBinding(parent, binding, resolver) {
        if (!isClass(binding)) {
            throw new Error(`The binding value for contextual binding should be class`);
        }
        if (!isClass(parent)) {
            throw new Error(`The parent value for contextual binding should be class`);
        }
        // debug('adding contextual binding %O to %O', binding, parent)
        /**
         * Create map for the parent if doesn't already exists
         */
        if (!this.#contextualBindings.has(parent)) {
            this.#contextualBindings.set(parent, new Map());
        }
        const parentBindings = this.#contextualBindings.get(parent);
        parentBindings.set(binding, { resolver });
    }
}
//# sourceMappingURL=container.js.map