/**
 * @aedart/contracts
 * 
 * BSD-3-Clause, Copyright (c) 2023-present Alin Eugen Deac <aedart@gmail.com>.
 */

import { Constructor, Callback, ClassMethodReference, ConstructorLike } from '@aedart/contracts';
import { CallbackWrapper } from '@aedart/contracts/support';
import { Throwable } from '@aedart/contracts/support/exceptions';

/**
 * Contextual Binding Builder
 *
 * Adaptation of Laravel's Contextual Binding Builder.
 *
 * @see https://github.com/laravel/framework/blob/master/src/Illuminate/Contracts/Container/ContextualBindingBuilder.php
 */
interface ContextualBindingBuilder {
    /**
     * Define the target identifier in this context.
     *
     * @param {Identifier} identifier
     *
     * @return {this}
     */
    needs(identifier: Identifier): this;
    /**
     * Define the implementation to be resolved in this context.
     *
     * @param {FactoryCallback | Constructor} implementation
     *
     * @return {void}
     *
     * @throws {TypeError}
     */
    give(implementation: FactoryCallback | Constructor): void;
}

/**
 * Service Container
 *
 * Adaptation of Psr's `ContainerInterface`, and Laravel's service `Container`.
 *
 * @see https://www.php-fig.org/psr/psr-11/#31-psrcontainercontainerinterface
 * @see https://github.com/laravel/framework/blob/master/src/Illuminate/Contracts/Container/Container.php
 */
interface Container {
    /**
     * Register a binding
     *
     * @param {Identifier} identifier
     * @param {FactoryCallback | Constructor} [concrete]
     * @param {boolean} [shared=false]
     *
     * @returns {this}
     *
     * @throws {TypeError}
     */
    bind(identifier: Identifier, concrete?: FactoryCallback | Constructor, shared?: boolean): this;
    /**
     * Register a binding, if none already exists for given identifier
     *
     * @param {Identifier} identifier
     * @param {FactoryCallback | Constructor} [concrete]
     * @param {boolean} [shared=false]
     *
     * @returns {this}
     *
     * @throws {TypeError}
     */
    bindIf(identifier: Identifier, concrete?: FactoryCallback | Constructor, shared?: boolean): this;
    /**
     * Register a shared binding
     *
     * @param {Identifier} identifier
     * @param {FactoryCallback | Constructor} [concrete]
     *
     * @returns {this}
     *
     * @throws {TypeError}
     */
    singleton(identifier: Identifier, concrete?: FactoryCallback | Constructor): this;
    /**
     * Register a shared binding, if none already exists for given identifier
     *
     * @param {Identifier} identifier
     * @param {FactoryCallback | Constructor} [concrete]
     *
     * @returns {this}
     *
     * @throws {TypeError}
     */
    singletonIf(identifier: Identifier, concrete?: FactoryCallback | Constructor): this;
    /**
     * Register existing object instance as a shared binding
     *
     * @template T = object
     *
     * @param {Identifier} identifier
     * @param {T} instance
     *
     * @returns {T}
     *
     * @throws {TypeError}
     */
    instance<T = object>(identifier: Identifier, instance: T): T;
    /**
     * Add a contextual binding in this container
     *
     * @param {Constructor} concrete
     * @param {Identifier} identifier
     * @param {FactoryCallback | Constructor} implementation
     *
     * @return {this}
     *
     * @throws {TypeError}
     */
    addContextualBinding(concrete: Constructor, identifier: Identifier, implementation: FactoryCallback | Constructor): this;
    /**
     * Define a contextual binding
     *
     * @param {...Constructor[]} concrete
     *
     * @return {ContextualBindingBuilder}
     *
     * @throws {TypeError}
     */
    when(...concrete: Constructor[]): ContextualBindingBuilder;
    /**
     * Resolves binding value that matches identifier and returns it
     *
     * @template T = any
     *
     * @param {Identifier} identifier
     *
     * @returns {T}
     *
     * @throws {NotFoundException}
     * @throws {ContainerException}
     */
    get<T = any>(identifier: Identifier): T;
    /**
     * Determine if an entry is registered for given identifier.
     *
     * @param {Identifier} identifier
     *
     * @returns {boolean}
     */
    has(identifier: Identifier): boolean;
    /**
     * Alias for {@link has}
     *
     * @param {Identifier} identifier
     *
     * @returns {boolean}
     */
    bound(identifier: Identifier): boolean;
    /**
     * Alias identifier as a different identifier
     *
     * @param {Identifier} identifier
     * @param {Alias} alias
     *
     * @returns {this}
     *
     * @throws {TypeError}
     */
    alias(identifier: Identifier, alias: Alias): this;
    /**
     * Determine if identifier is an alias
     *
     * @param {Identifier} identifier
     *
     * @return {boolean}
     */
    isAlias(identifier: Identifier): boolean;
    /**
     * Determine if identifier is registered as a "shared" binding
     *
     * @param {Identifier} identifier
     *
     * @returns {boolean}
     */
    isShared(identifier: Identifier): boolean;
    /**
     * Resolves binding value that matches identifier and returns it
     *
     * @template T = any
     *
     * @param {Identifier} identifier
     * @param {any[]} [args] Eventual arguments to pass on to {@link FactoryCallback} or {@link Constructor}
     *
     * @returns {T}
     *
     * @throws {NotFoundException}
     * @throws {ContainerException}
     */
    make<T = any>(identifier: Identifier, args?: any[]): T;
    /**
     * Resolves values if a binding exists for identifier, or returns a default value
     *
     * @template T = any
     * @template D = undefined
     *
     * @param {Identifier} identifier
     * @param {any[]} [args] Eventual arguments to pass on to {@link FactoryCallback} or {@link Constructor}
     * @param {D} [defaultValue]
     *
     * @returns {T}
     *
     * @throws {ContainerException}
     */
    makeOrDefault<T = any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ D = undefined>(identifier: Identifier, args?: any[], defaultValue?: D): T | D;
    /**
     * Instantiate a new instance of given concrete
     *
     * @template T = object
     *
     * @param {Constructor<T> | Binding<T>} concrete
     * @param {any[]} [args] Eventual arguments to pass on the concrete instance's constructor.
     *
     * @returns {T}
     *
     * @throws {ContainerException}
     */
    build<T = object>(concrete: Constructor<T> | Binding<T>, args?: any[]): T;
    /**
     * Call given method and inject dependencies if needed
     *
     * @param {Callback | CallbackWrapper | ClassMethodReference} method
     * @param {any[]} [args]
     *
     * @return {any}
     *
     * @throws {ContainerException}
     */
    call(method: Callback | CallbackWrapper | ClassMethodReference, args?: any[]): any;
    /**
     * Extend the registered binding
     *
     * @param {Identifier} identifier
     * @param {ExtendCallback} callback
     *
     * @return {this}
     *
     * @throws {TypeError}
     * @throws {ContainerException}
     */
    extend(identifier: Identifier, callback: ExtendCallback): this;
    /**
     * Register a callback to be invoked whenever identifier is "rebound"
     *
     * @param {Identifier} identifier
     * @param {ReboundCallback} callback
     *
     * @return {any | void}
     *
     * @throws {ContainerException}
     */
    rebinding(identifier: Identifier, callback: ReboundCallback): any | void;
    /**
     * Forget binding and resolved instance for given identifier
     *
     * @param {Identifier} identifier
     *
     * @returns {boolean}
     */
    forget(identifier: Identifier): boolean;
    /**
     * Forget resolved instance for given identifier
     *
     * @param {Identifier} identifier
     *
     * @return {boolean}
     */
    forgetInstance(identifier: Identifier): boolean;
    /**
     * Flush container of all bindings and resolved instances
     *
     * @returns {void}
     */
    flush(): void;
    /**
     * Determine if identifier has been resolved
     *
     * @param {Identifier} identifier
     *
     * @return {boolean}
     */
    isResolved(identifier: Identifier): boolean;
    /**
     * Register a callback to be invoked before a binding is resolved
     *
     * @param {Identifier} identifier
     * @param {BeforeResolvedCallback} callback
     *
     * @return {this}
     */
    before(identifier: Identifier, callback: BeforeResolvedCallback): this;
    /**
     * Register a callback to be invoked after a binding has been resolved
     *
     * @param {Identifier} identifier
     * @param {AfterResolvedCallback} callback
     *
     * @return {this}
     */
    after(identifier: Identifier, callback: AfterResolvedCallback): this;
}

/**
 * Binding Identifier
 *
 * A unique identifier used for associating "concrete" items or values in
 * a service container.
 */
type Identifier = string | symbol | number | NonNullable<object> | ConstructorLike | Callback;
/**
 * Binding Alias
 *
 * @see Identifier
 */
type Alias = Identifier;
/**
 * Factory Callback
 *
 * The callback is responsible for resolving a value, when a binding
 * is resolved in a service container.
 */
type FactoryCallback<Value = any> = (container: Container, ...args: any[]) => Value;
/**
 * Extend Callback
 *
 * Callback can be used to "extend", decorate or modify a resolved value
 * from the service container.
 */
type ExtendCallback<Value = any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ ExtendedValue extends Value = any> = (resolved: Value, container: Container) => ExtendedValue;
/**
 * Rebound Callback
 *
 * Callback to be invoked when a binding is "rebound".
 */
type ReboundCallback<Value = any> = (resolved: Value, container: Container) => void;
/**
 * Before Resolved Callback
 *
 * Callback to be invoked before a binding is resolved.
 */
type BeforeResolvedCallback = (identifier: Identifier, args: any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ container: Container) => void;
/**
 * After Resolved Callback
 *
 * Callback to be invoked after a binding has been resolved.
 */
type AfterResolvedCallback<Value = any> = (identifier: Identifier, resolved: Value, container: Container) => void;

/**
 * Binding Entry
 *
 * @template T = any
 */
interface Binding<T = any> {
    /**
     * This binding's identifier
     *
     * @type {Identifier}
     *
     * @readonly
     */
    readonly identifier: Identifier;
    /**
     * The bound value to be resolved by a service container
     *
     * @template T = any
     *
     * @type {FactoryCallback<T> | Constructor<T>}
     *
     * @readonly
     */
    readonly value: FactoryCallback<T> | Constructor<T>;
    /**
     * Shared state of resolved value
     *
     * @type {boolean} If `true`, then service container must register resolved
     *                 value as a singleton.
     *
     * @readonly
     */
    readonly shared: boolean;
    /**
     * Determine if bound value is a {@link FactoryCallback}
     *
     * @returns {boolean}
     */
    isFactoryCallback(): boolean;
    /**
     * Determine if bound value is a {@link Constructor}
     *
     * @returns {boolean}
     */
    isConstructor(): boolean;
}

/**
 * Container Exception
 *
 * General exception to be thrown when something went wrong inside
 * a service container. Inspired by Psr's `ContainerExceptionInterface`.
 *
 * @see https://www.php-fig.org/psr/psr-11/#32-psrcontainercontainerexceptioninterface
 */
interface ContainerException extends Throwable {
}

/**
 * Circular Dependency Exception
 *
 * To be thrown in situations when a binding has a circular dependency.
 */
interface CircularDependencyException extends ContainerException {
}

/**
 * Not Found Exception
 *
 * To be thrown when no entry was found for a given binding identifier.
 * Inspired by Psr's `NotFoundExceptionInterface`.
 *
 * @see https://www.php-fig.org/psr/psr-11/#33-psrcontainernotfoundexceptioninterface
 */
interface NotFoundException extends ContainerException {
}

/**
 * Container identifier
 *
 * @type {Symbol}
 */
declare const CONTAINER: unique symbol;
/**
 * Dependencies identifier
 *
 * Symbol is intended to be used as an identifier for when associating binding identifiers
 * or "concrete" dependencies with an element, e.g. a class, class method, function,...etc.
 *
 * @type {symbol}
 */
declare const DEPENDENCIES: unique symbol;

export { type AfterResolvedCallback, type Alias, type BeforeResolvedCallback, type Binding, CONTAINER, type CircularDependencyException, type Container, type ContainerException, type ContextualBindingBuilder, DEPENDENCIES, type ExtendCallback, type FactoryCallback, type Identifier, type NotFoundException, type ReboundCallback };
