import { IActivationContext, ILifetime, ILifetimeContext, ILifetimeManager, ILifetimeSlot } from "./interfaces";
import { ActivationContext } from "./ActivationContext";
import { argumentNotNull, isDestroyable } from "./traits";

const safeCall = (item: () => void) => {
    try {
        item();
    } catch {
        // silence!
    }
};

const _emptySlot = Object.freeze({
    has: () => false,

    initialize: () => void (0),

    get: () => {
        throw new Error("The specified item isn't registered with this lifetime manager");
    },

    store: () => void (0)
});

const _unknonwSlot = Object.freeze({
    has: () => false,

    initialize: () => {
        throw new Error("Can't call initialize on the unknown lifetime object");
    },
    get: () => {
        throw new Error("The lifetime object isn't initialized");
    },
    store: () => {
        throw new Error("Can't store a value in the unknown lifetime object");
    }
});

let nextId = 0;

const singletons: { [K: string]: unknown } = {};


const pendingSlot = <T>(store: (item: T) => void) => ({
    has: () => false,

    get: () => {
        throw new Error("The value in this slot doesn't exist");
    },

    initialize: () => {
        throw new Error("Cyclic reference detected");
    },

    store
});

const noop = () => void(0);

const valueSlot = <T>(value: T, cleanup: (item: T) => void) => ({
    has: () => true,

    get: () => value,

    initialize: () => {
        throw new Error("The slot already has a value");
    },

    store: () => {
        throw new Error("The slot already has a value");
    }
});

export class LifetimeManager implements ILifetimeManager {
    private _destroyed = false;

    private readonly _slots: Record<string, ILifetimeSlot<unknown>> = {};

    slot<T>(cookie: string): ILifetimeSlot<T> {
        if (cookie in this._slots)
            return this._slots[cookie] as ILifetimeSlot<T>;

        const store = (item: T, cleanup?: (item: T) => void) => {
            this._assertNotDestroyed();
            this._slots[cookie] = valueSlot(
                item,
                cleanup ?? isDestroyable(item) ? () => item.destroy() : noop 
            );
        };

        return {
            has: () => false,
            get: () => {
                throw new Error("The value isn't stored in this slot");
            },
            store,
            initialize: () => {
                this._assertNotDestroyed();
                this._slots[cookie] = pendingSlot(store);
            }
        };
    }

    private _assertNotDestroyed() {
        if (this._destroyed)
            throw new Error("The lifetime manager is destroyed");

    }

    destroy() {
        if (!this._destroyed) {
            this._destroyed = true;
            this._cleanup.forEach(safeCall);
            this._cleanup.length = 0;
        }
    }

}

export const emptyLifetime = <T>(): ILifetime<T> => {
    return _emptyLifetime;
};

export const hierarchyLifetime = <T>(): ILifetime<T> => {
    let _lifetime: ILifetime<T> = _unknownLifetime;
    return {
        initialize(context: ILifetimeContext) {
            if (_lifetime !== _unknownLifetime)
                throw new Error("Cyclic reference activation detected");

            _lifetime = context.createContainerLifetime<T>();
        },
        get() {
            return _lifetime.get();
        },
        has() {
            return _lifetime.has();
        },
        store(item: NonNullable<T>, cleanup?: (item: NonNullable<T>) => void) {
            return _lifetime.store(item, cleanup);
        },
        toString() {
            return `[object HierarchyLifetime, has=${String(this.has())}]`;
        }
    };
};

/**
 * Creates a lifetime instance bound to the current activation context. This
 * lifetime will store the service instance per activation context. Every
 * top level service resolution will create a new activation context. This
 * context is propagated to subsequent service resolution thus all services
 * with context lifetime will be shared among their consumers. 
 *  
 * @returns The instance of the lifetime.
 */
export const contextLifetime = <T>(): ILifetime<T> => {
    let _lifetime: ILifetime<T> = _unknownLifetime;
    return {
        initialize(context: ILifetimeContext) {
            if (_lifetime !== _unknownLifetime)
                throw new Error("Cyclic reference detected");
            _lifetime = context.createLifetime();
        },
        get() {
            return _lifetime.get();
        },
        has() {
            return _lifetime.has();
        },
        store(item: NonNullable<T>) {
            _lifetime.store(item);
        },
        toString() {
            return `[object ContextLifetime, has=${String(this.has())}]`;
        }
    };
};

/**
 * Creates the lifetime for the service which will allow existence only one
 * instance with the specified {@linkcode typeId}. If there will be created
 * several lifetime instances with same `typeId` in the runtime, they will
 * share the same service instance.
 * 
 * @param typeId The identified for the global instance, usually this is a
 *   fully qualified class name
 * @returns The lifetime instance 
 */
export const singletonLifetime = <T>(typeId: string): ILifetime<T> => {
    argumentNotNull(typeId, "typeId");
    let pending = false;
    return {
        has() {
            return typeId in singletons;
        },
        get() {
            if (!this.has())
                throw new Error(`The instance ${typeId} doesn't exists`);
            return singletons[typeId] as NonNullable<T>;
        },
        initialize() {
            if (pending)
                throw new Error("Cyclic reference detected");
            pending = true;
        },
        store(item: NonNullable<T>) {
            singletons[typeId] = item;
            pending = false;
        },
        toString() {
            return `[object SingletonLifetime, has=${String(this.has())}, typeId=${typeId}]`;
        }
    };
};

/** Creates a lifetime bound to the specified container. Using this lifetime
 * will create a single service instance per the specified container.
 * 
 * @param container The container which will manage the lifetime for the service
 */
export const containerLifetime = <T>(container: { createLifetime<X>(): ILifetime<X> }) => {
    let _lifetime: ILifetime<T> = _unknownLifetime;
    return {
        initialize() {
            if (_lifetime !== _unknownLifetime)
                throw new Error("Cyclic reference detected");
            _lifetime = container.createLifetime();
        },
        get() {
            return _lifetime.get();
        },
        has() {
            return _lifetime.has();
        },
        store(item: NonNullable<T>, cleanup?: (item: NonNullable<T>) => void) {
            _lifetime.store(item, cleanup);
        },
        toString() {
            return `[object ContainerLifetime, has=${String(_lifetime.has())}]`;
        }
    };
};
