import "reflect-metadata";
import { Constructable } from "../types/constructable";
import { METADATA_KEY } from "../constants/metadata.keys";
import { InstanceContainer } from "../container/instance-container";
import { ActionHandler } from "../decorators/action/action";
import { Provider } from "../decorators/provider/provider";

export namespace Resolver {
	/**
	 * Resolve provided target class (either @Provider or @ReduxAction)
	 *
	 * @param target the target to resolve
	 * @param container the container to keep the instance in
	 * @param parentModuleName name of the module the target is provided in (used i.e. for error messages)
	 */
	export function resolve<T>(target: Constructable<T>, container: InstanceContainer, parentModuleName: string): T {
		const isProvider = Reflect.hasOwnMetadata(METADATA_KEY.PROVIDER_PARAM_TYPES, target);
		if (isProvider) {
			return resolveProvider<T>(target, container, parentModuleName);
		}

		const isReduxAction = Reflect.hasOwnMetadata(METADATA_KEY.ACTION_HANDLER_PARAM_TYPES, target);
		if (isReduxAction) {
			return resolveAction<T>(target, container, parentModuleName);
		}

		const allowedDecorators = [Provider.name, ActionHandler.name];
		throw new Error("Target must be one of " + allowedDecorators + "!");
	}

	function resolveProvider<T>(target: Constructable<T>, container: InstanceContainer, parentModuleName: string): T {
		if (!Reflect.hasOwnMetadata(METADATA_KEY.PROVIDER_PARAM_TYPES, target)) {
			throw new Error("Attempting to resolve target with name: " + target.name + ". " + target.name + " is not a provider in module: " + parentModuleName + "!");
		}

		if (containerHasElement(container, target.name)) {
			return container.get(target.name);
		}

		let designParamTypes = Reflect.getMetadata(METADATA_KEY.PROVIDER_PARAM_TYPES, target) || [];
		let injections = designParamTypes.map((token: Constructable<T>) => {
			return resolveProvider<T>(token, container, parentModuleName);
		});

		let newInstance = new target(...injections);
		container.add(target.name, newInstance);
		return newInstance;
	}

	function resolveAction<T>(target: Constructable<T>, container: InstanceContainer, parentModuleName: string): T {
		if (containerHasElement(container, target.name)) {
			return container.get(target.name);
		}

		let reducActionDesignParamTypes = Reflect.getMetadata(METADATA_KEY.ACTION_HANDLER_PARAM_TYPES, target) || [];
		let injections = reducActionDesignParamTypes.map((token: Constructable<T>) => {
			return resolve<T>(token, container, parentModuleName);
		});

		let newInstance = new target(...injections);
		container.add(target.name, newInstance);
		return newInstance;
	}

	function containerHasElement(container: InstanceContainer, instanceIdentifier: string) {
		try {
			container.get(instanceIdentifier);
			return true;
		} catch (e) {
			return false;
		}
	}
}
