/**
 * Created by Weizehua on 2017/1/13.
 */

import "reflect-metadata"

interface Newable<T> extends Function {
    new(...args): T;
}

@Injectable()
export class Injector {
    // private static _instance = new Injector();
    private static isInjectable = Symbol('Injector:is-injectable');
    private static isSingleton = Symbol('Injector:is-singleton');
    private static singletonInstance = Symbol('Injector:singleton-instance');
    public static checkDependencyLoop = true;
    public static strictDecorationCheck = true;

    private constructor() {
    }

    static get<T>(ctor: Newable<T>): T {
        // A class without @Injectable() decorated can be instantiate too
        // **But that is not recommended**
        if (Injector.strictDecorationCheck && !ctor[Injector.isInjectable]) {
            throw new Error("To get a instance of " + ctor.name + ", you should decorate it with @Injectable().")
        }

        // There is a different instantiation for Singleton class.
        if (ctor[Injector.isSingleton])
            return ctor[Injector.singletonInstance] = ctor[Injector.singletonInstance] || Injector.create(ctor);

        return Injector.create(ctor);
    }

    private static create<T>(ctor: Newable<T>, follwer?: Function[]): T {
        let argtypes = Reflect.getMetadata("design:paramtypes", ctor) || [];

        // If it depends on nothing, just new it.
        if (argtypes.length === 0) {
            return new ctor();
        }

        // If it depends on something else, check if there is a dependency loop
        if (Injector.checkDependencyLoop && follwer
            && follwer.find(val => {
                    return val === ctor;
                })) {
            throw new Error("Found dependency loop while trying to instantiate " + ctor.name + ". Please check your code.");
        }

        // Prepare for next dependency loop check
        let newFollower = (<Function[]>[ctor]).concat(follwer);

        // Instantiate the arguments it needs
        let args = [];
        for (let i = 0; i < argtypes.length; i++) {
            args.push(Injector.get(argtypes[i]), newFollower);
        }
        return new ctor(...args);
    }

    static injectableDecorator<T>(ctor: Newable<T>): Newable<T> {
        ctor[Injector.isInjectable] = true;
        return ctor;
    }

    static singletonDecorator<T>(ctor: Newable<T>): Newable<T> {
        ctor[Injector.isInjectable] = true;
        ctor[Injector.isSingleton] = true;
        return ctor;
    }

}

/* @Injectable() marks a class injectable
 * Use Injector.get() to get a instance of that class
 * Calling Injector.get() multiple times on a @Injectable() decorated class
 * will produce different instances.
 */
export function Injectable(): ClassDecorator {
    return Injector.injectableDecorator;
}
/* @Singleton() marks a singleton class.
 * Use Injector.get() to get the instance of singleton class
 */
export function Singleton(): ClassDecorator {
    return Injector.singletonDecorator;
}

