import { Descriptor, ILifetime, DepsMap, IActivationContext, DescriptorMap } from "./interfaces";
import { each, key } from "./traits";

export interface DescriptorImplArgs<S, T> {
    lifetime: ILifetime<T>;

    factory: (refs: Record<key, unknown>) => NonNullable<T>;

    cleanup?: (item: NonNullable<T>) => void;

    overrides?: DescriptorMap<S>;

    dependencies?: DepsMap<S>;
}


export class DescriptorImpl<S, T> implements Descriptor<S, T> {

    private readonly _overrides?: DescriptorMap<S>;

    private readonly _lifetime: ILifetime<T>;

    private readonly _factory: (refs: Record<key, unknown>) => NonNullable<T>;

    private readonly _cleanup?: (item: NonNullable<T>) => void;

    private readonly _deps?: DepsMap<S>;

    readonly hasOverrides: boolean;

    constructor({ lifetime, factory, cleanup, overrides, dependencies }: DescriptorImplArgs<S, T>) {
        this._lifetime = lifetime;
        this._factory = factory;
        if (cleanup)
            this._cleanup = cleanup;
        if (overrides)
            this._overrides = overrides;
        if (dependencies)
            this._deps = dependencies;

        this.hasOverrides = !!overrides;
    }

    activate(context: IActivationContext<S>): NonNullable<T> {

        if (this._lifetime.has())
            return this._lifetime.get();

        this._lifetime.initialize(context);

        if (this._overrides)
            each(this._overrides, (v, k) => context.register(k, v));

        const resolve = <K extends keyof S>({ name, lazy, ...opts }: { name: K; lazy?: boolean; default?: S[K] | null; }) => {
            if (lazy) {
                return () => "default" in opts ? context.resolve(name, opts.default) : context.resolve(name);
            } else {
                return "default" in opts ? context.resolve(name, opts.default) : context.resolve(name);
            }
        };

        const makeRefs = (deps: typeof this._deps) => deps ?
            Object.keys(deps)
                .map(k => {
                    const ref = deps[k];
                    return typeof ref !== "object" ?
                        { [k]: resolve({ name: ref }) } :
                        { [k]: resolve(ref) };
                })
                .reduce((a, p) => ({ ...a, ...p }), {} ):
            {};

        const instance = this._factory.call(undefined, makeRefs(this._deps));

        this._lifetime.store(instance, this._cleanup);

        return instance;
    }


    toString() {
        return `[object DescriptorImpl, lifetime=${String(this._lifetime)}]`;
    }
}
