import { IDestroyable, MapOf } from "../interfaces";
import { argumentNotNull, isDestroyable, argumentNotEmptyString, isRemovable } from "../safe";
import { ILifetime, ServiceContainer } from "./interfaces";
import { ActivationContext } from "./ActivationContext";

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

const emptyLifetime: ILifetime = 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: ILifetime = 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: any = {};

export class LifetimeManager implements IDestroyable {
    private _cleanup: (() => void)[] = [];
    private _cache: MapOf<any> = {};
    private _destroyed = false;

    private _pending: MapOf<boolean> = {};

    create(): ILifetime {
        const self = this;
        const id = ++nextId;
        return {
            has() {
                return (id in self._cache);
            },

            get() {
                const t = self._cache[id];
                if (t === undefined)
                    throw new Error(`The item with with the key ${id} isn't found`);
                return t;
            },

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

            store(item: any, cleanup?: (item: any) => void) {
                argumentNotNull(id, "id");
                argumentNotNull(item, "item");

                if (this.has())
                    throw new Error(`The item with with the key ${id} already registered with this lifetime manager`);
                delete self._pending[id];

                self._cache[id] = item;

                if (self._destroyed)
                    throw new Error("Lifetime manager is destroyed");
                if (cleanup) {
                    self._cleanup.push(() => cleanup(item));
                } else if (isDestroyable(item)) {
                    self._cleanup.push(() => item.destroy());
                }
            }
        };
    }

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

    static empty(): ILifetime {
        return emptyLifetime;
    }

    static hierarchyLifetime() {
        let _lifetime = unknownLifetime;
        return {
            initialize(context: ActivationContext<any>) {
                if (_lifetime !== unknownLifetime)
                    throw new Error("Cyclic reference activation detected");

                _lifetime = context.getContainer().getLifetimeManager().create();
            },
            get() {
                return _lifetime.get();
            },
            has() {
                return _lifetime.has();
            },
            store(item: any, cleanup?: (item: any) => void) {
                return _lifetime.store(item, cleanup);
            },
            toString() {
                return `[object HierarchyLifetime, has=${this.has()}]`;
            }
        };
    }

    static contextLifetime() {
        let _lifetime = unknownLifetime;
        return {
            initialize(context: ActivationContext<any>) {
                if (_lifetime !== unknownLifetime)
                    throw new Error("Cyclic reference detected");
                _lifetime = context.createLifetime();
            },
            get() {
                return _lifetime.get();
            },
            has() {
                return _lifetime.has();
            },
            store(item: any) {
                _lifetime.store(item);
            },
            toString() {
                return `[object ContextLifetime, has=${this.has()}]`;
            }
        };
    }

    static singletonLifetime(typeId: string) {
        argumentNotEmptyString(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];
            },
            initialize() {
                if (pending)
                    throw new Error("Cyclic reference detected");
                pending = true;
            },
            store(item: any) {
                singletons[typeId] = item;
                pending = false;
            },
            toString() {
                return `[object SingletonLifetime, has=${this.has()}, typeId=${typeId}]`;
            }
        };
    }

    static containerLifetime(container: ServiceContainer<any>) {
        let _lifetime = unknownLifetime;
        return {
            initialize(context: ActivationContext<any>) {
                if (_lifetime !== unknownLifetime)
                    throw new Error("Cyclic reference detected");
                _lifetime = container.getLifetimeManager().create();
            },
            get() {
                return _lifetime.get();
            },
            has() {
                return _lifetime.has();
            },
            store(item: any) {
                _lifetime.store(item);
            },
            toString() {
                return `[object ContainerLifetime, has=${_lifetime.has()}]`
            }
        };
    }
}
