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

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

const _emptyLifetime = Object.freeze({
    has() {
        return false;
    },

    initialize() {
    },

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

    store() {
        // does nothing
    },

    toString() {
        return `[object EmptyLifetime]`;
    }

});

const _unknownLifetime = Object.freeze({
    has() {
        return 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");
    },

    toString() {
        return `[object UnknownLifetime]`;
    }
});

let nextId = 0;

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

export class LifetimeManager implements ILifetimeManager {
    private _cleanup: (() => void)[] = [];
    private readonly _cache: { [K: string]: unknown } = {};
    private _destroyed = false;

    private readonly _pending: { [K: string]: unknown } = {};

    create<T>(): ILifetime<T> & { remove(): void; } {
        const id = ++nextId;
        return {
            has: () => id in this._cache,

            get: () => {
                const t = this._cache[id] as T;
                if (t === undefined || t === null)
                    throw new Error(`The item with with the key ${id} isn't found`);
                return t;
            },

            initialize: () => {
                if (this._pending[id])
                    throw Error(`Cyclic reference detected: the item with the key ${id} is already activating.`);
                this._pending[id] = true;
            },

            store: (item: NonNullable<T>, cleanup?: (item: NonNullable<T>) => void) => {
                if (id in this._cache)
                    throw new Error(`The item with with the key ${id} already registered with this lifetime manager`);
                if (this._destroyed)
                    throw new Error("Lifetime manager is destroyed");

                delete this._pending[id];

                this._cache[id] = item;

                if (cleanup) {
                    this._cleanup.push(() => cleanup(item));
                } else if (isDestroyable(item)) {
                    this._cleanup.push(() => item.destroy());
                }
            },

            remove: () => {
                if (this._pending[id])
                    throw new Error(`The item '${id}' can't be removed before it has been stored`);
                delete this._cache[id];
            }
        };
    }

    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: IActivationContext<object>) {
            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())}]`;
        }
    };
};

export const contextLifetime = <T>(): ILifetime<T> => {
    let _lifetime: ILifetime<T> = _unknownLifetime;
    return {
        initialize(context: ActivationContext<object>) {
            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())}]`;
        }
    };
};

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}]`;
        }
    };
};

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>) {
            _lifetime.store(item);
        },
        toString() {
            return `[object ContainerLifetime, has=${String(_lifetime.has())}]`;
        }
    };
};
