import { Descriptor, ILifetime, IActivationContext, DescriptorMap, ILifetimeManager } from "./interfaces";
import { argumentNotNull } from "./traits";

export interface ActivationContextInfo {
    name: string;

    service: string;

}

let nextId = 1;

/** This object is created once per `Container.resolve` method call and used to
 * cache dependencies and to track created instances. The activation context
 * tracks services with `context` activation type.
 * 
 * @template S The service map used in the activation context, services from
 *   this map are available to resolution.
 * @template U A set of keys from the service map which can be overridden in
 *   this activation context.
 */
export class ActivationContext<S> implements IActivationContext<S> {
    private readonly _cache: Record<string, unknown>;

    private readonly _services: DescriptorMap<S>;

    private readonly _name: string;

    private readonly _service: Descriptor<S, unknown>;

    private readonly _containerLifetimeManager: ILifetimeManager;

    private readonly _parent: ActivationContext<S> | undefined;

    /** Creates a new activation context with the specified parameters.
     * @param containerLifetimeManager the container which starts the activation process
     * @param services the initial service registrations
     * @param name the name of the service being activated, this parameter is
     *  used for the debug purpose.
     * @param service the service to activate, this parameter is used for the
     *  debug purpose.
     */
    constructor(containerLifetimeManager: ILifetimeManager, services: DescriptorMap<S>, name: string, service: Descriptor<S, unknown>, cache = {}) {
        this._name = name;
        this._service = service;
        this._cache = cache;
        this._services = services;
        this._containerLifetimeManager = containerLifetimeManager;
    }

    /** the name of the current resolving dependency */
    getName() {
        return this._name;
    }

    createContainerLifetime<T>() {
        return this._containerLifetimeManager.create<T>();
    }

    resolve<K extends keyof S>(name: K): NonNullable<S[K]>;
    resolve<K extends keyof S, T>(name: K, def: T): NonNullable<S[K]> | T;
    resolve<K extends keyof S, T>(name: K, def?: T): NonNullable<S[K]> | T | undefined {
        const d = this._services[name];

        if (d !== undefined) {
            return this.activate(d, name.toString());
        } else {
            if (arguments.length > 1)
                return def;
            else
                throw new Error(`Service ${String(name)} not found`);
        }
    }

    /**
     * registers services local to the the activation context
     *
     * @name{string} the name of the service
     * @service{string} the service descriptor to register
     */
    register<K extends keyof S>(name: K, service: DescriptorMap<S>[K]) {
        argumentNotNull(name, "name");

        const d = this._services[name];
        if (d !== undefined && !d.configurable)
            throw new Error(`Service ${String(name)} can't be overridden`);

        this._services[name] = service;
    }

    createLifetime<T>(): ILifetime<T> {
        const id = nextId++;
        return {
            initialize() { },
            has: () => id in this._cache,
            get: () => {
                const v = this._cache[id] as T;
                if (v === undefined || v === null)
                    throw new Error("The value isn't present in the activation context");
                return v;
            },
            store: item => {
                this._cache[id] = item;
            }
        };
    }

    activate<T>(d: Descriptor<S, T>, name: string) {
        // TODO: add logging
        // if (trace.isLogEnabled())
        //    trace.log("enter {0} {1}", name, d);

        const ctx = this.enter(d, name);
        const v = d.activate(ctx);

        // if (trace.isLogEnabled())
        //    trace.log(`leave ${name}`);

        return v;
    }

    getStack(): ActivationContextInfo[] {
        const stack = [{
            name: this._name,
            service: this._service.toString()
        }];

        return this._parent ?
            stack.concat(this._parent.getStack()) :
            stack;
    }

    private enter(service: Descriptor<S, unknown>, name: string) {
        return new ActivationContext(
            this._containerLifetimeManager,
            service.hasOverrides ?
                Object.create(this._services) as typeof this._services :
                this._services,
            name,
            service,
            this._cache
        );
    }
}
