// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.petite;

import jodd.bean.BeanUtil;
import jodd.introspector.Setter;
import jodd.petite.meta.InitMethodInvocationStrategy;
import jodd.petite.scope.Scope;
import jodd.petite.scope.SingletonScope;

import java.util.Collection;

import jodd.typeconverter.Convert;
import jodd.log.Logger;
import jodd.log.LoggerFactory;

/**
 * Petite IOC container.
 */
public class PetiteContainer extends PetiteBeans {

	private static final Logger log = LoggerFactory.getLogger(PetiteContainer.class);

	/**
	 * Petite container reference name.
	 * Used when container itself is added as its bean.
	 * @see #addSelf()
	 * @see #addSelf(String)
	 */
	public static final String PETITE_CONTAINER_REF_NAME = "petiteContainer";

	protected final ScopedProxyManager scopedProxyManager;

	/**
	 * Creates new Petite container using {@link PetiteConfig default configuration}.
	 */
	public PetiteContainer() {
		this(new PetiteConfig());
	}

	/**
	 * Creates new Petite container using {@link PetiteContainer provided configuration}.
	 */
	public PetiteContainer(PetiteConfig config) {
		super(config);

		if (JoddPetite.useProxetta) {
			scopedProxyManager = new ScopedProxyManager();
		} else {
			scopedProxyManager = null;
		}

		if (log.isDebugEnabled()) {
			log.debug("Petite container created");

			if (JoddPetite.useProxetta) {
				log.debug("Petite proxy features enabled");
			} else {
				log.debug("Petite proxy features not available");
			}
		}
	}

	// ---------------------------------------------------------------- core

	/**
	 * Creates new bean instance and performs constructor injection.
	 */
	protected Object newBeanInstance(BeanDefinition def) {
		if (def.ctor == null) {
			def.ctor = petiteResolvers.resolveCtorInjectionPoint(def.type);
		}

		int paramNo = def.ctor.references.length;
		Object[] args = new Object[paramNo];

		// wiring
		if (def.wiringMode != WiringMode.NONE) {
			for (int i = 0; i < paramNo; i++) {
				args[i] = getBean(def.ctor.references[i]);
				if (args[i] == null) {
					if ((def.wiringMode == WiringMode.STRICT)) {
						throw new PetiteException(
								"Wiring constructor failed. References '" + Convert.toString(def.ctor.references[i]) +
								"' not found for constructor: " + def.ctor.constructor);
					}
				}
			}
		}

		// create instance
		Object bean;
		try {
			bean = def.ctor.constructor.newInstance(args);
		} catch (Exception ex) {
			throw new PetiteException("Failed to create new bean instance '" + def.type.getName() + "' using constructor: " + def.ctor.constructor, ex);
		}

		return bean;
	}

	/**
	 * Wires beans.
	 * @param bean target bean
	 * @param def bean definition
	 */
	protected void wireBean(Object bean, BeanDefinition def) {
		if (def.wiringMode == WiringMode.NONE) {
			return;
		}
		wireProperties(bean, def);
		wireMethods(bean, def);
	}

	/**
	 * Wires properties.
	 */
	protected void wireProperties(Object bean, BeanDefinition def) {
		if (def.properties == null) {
			def.properties = petiteResolvers.resolvePropertyInjectionPoint(def.type, def.wiringMode == WiringMode.AUTOWIRE);
		}

		boolean mixing = petiteConfig.wireScopedProxy || petiteConfig.detectMixedScopes;

		for (PropertyInjectionPoint pip : def.properties) {
			String[] refNames = pip.references;

			Object value = null;

			if (mixing) {
				BeanDefinition refBeanDefinition = lookupBeanDefinitions(refNames);

				if (refBeanDefinition != null) {
					value = scopedProxyManager.lookupValue(this, def, refBeanDefinition);
				}
			}

			if (value == null) {
				value = getBean(refNames);
			}

			if (value == null) {
				if ((def.wiringMode == WiringMode.STRICT)) {
					throw new PetiteException("Wiring failed. Beans references: '" +
							Convert.toString(refNames) + "' not found for property: "+ def.type.getName() +
							'#' + pip.propertyDescriptor.getName());
				}
				continue;
			}

			// BeanUtil.setDeclaredProperty(bean, pip.propertyDescriptor.getName(), value);

			Setter setter = pip.propertyDescriptor.getSetter(true);
			try {
				setter.invokeSetter(bean, value);
			}
			catch (Exception ex) {
				throw new PetiteException("Wiring failed", ex);
			}
		}

		// sets
		if (def.sets == null) {
			def.sets = petiteResolvers.resolveSetInjectionPoint(def.type, def.wiringMode == WiringMode.AUTOWIRE);
		}
		for (SetInjectionPoint sip : def.sets) {

			String[] beanNames = resolveBeanNamesForType(sip.targetClass);

			Collection beans = sip.createSet(beanNames.length);

			for (String beanName : beanNames) {
				if (!beanName.equals(def.name)) {
					Object value = getBean(beanName);
					beans.add(value);
				}
			}

			//BeanUtil.setDeclaredProperty(bean, sip.field.getName(), beans);

			Setter setter = sip.propertyDescriptor.getSetter(true);
			try {
				setter.invokeSetter(bean, beans);
			}
			catch (Exception ex) {
				throw new PetiteException("Wiring failed", ex);
			}
		}
	}

	/**
	 * Wires methods.
	 */
	protected void wireMethods(Object bean, BeanDefinition def) {
		if (def.methods == null) {
			def.methods = petiteResolvers.resolveMethodInjectionPoint(def.type);
		}
		for (MethodInjectionPoint methodRef : def.methods) {
			String[][] refNames = methodRef.references;
			Object[] args = new Object[refNames.length];
			for (int i = 0; i < refNames.length; i++) {
				String[] refName = refNames[i];
				Object value = null;

				boolean mixing = petiteConfig.wireScopedProxy || petiteConfig.detectMixedScopes;

				if (mixing) {
					BeanDefinition refBeanDefinition = lookupBeanDefinitions(refName);

					if (refBeanDefinition != null) {
						value = scopedProxyManager.lookupValue(this, def, refBeanDefinition);
					}
				}

				if (value == null) {
					value = getBean(refName);
				}

				args[i] = value;
				if (value == null) {
					if ((def.wiringMode == WiringMode.STRICT)) {
						throw new PetiteException("Wiring failed. Beans references: '" +
								Convert.toString(refName) + "' not found for method: " + def.type.getName() + '#' + methodRef.method.getName());
					}
				}
			}

			try {
				methodRef.method.invoke(bean, args);
			} catch (Exception ex) {
				throw new PetiteException(ex);
			}

		}
	}

	protected void resolveInitAndDestroyMethods(Object bean, BeanDefinition def) {
		if (def.initMethods == null) {
			def.initMethods = petiteResolvers.resolveInitMethodPoint(bean);
		}
		if (def.destroyMethods == null) {
			def.destroyMethods = petiteResolvers.resolveDestroyMethodPoint(bean);
		}
	}

	/**
	 * Invokes all init methods, if they exist. Also resolves destroy methods.
	 */
	protected void invokeInitMethods(Object bean, BeanDefinition def, InitMethodInvocationStrategy invocationStrategy) {
		for (InitMethodPoint initMethod : def.initMethods) {
			if (invocationStrategy != initMethod.invocationStrategy) {
				continue;
			}
			try {
				initMethod.method.invoke(bean);
			} catch (Exception ex) {
				throw new PetiteException("Invalid init method: " + initMethod, ex);
			}
		}
	}

	/**
	 * Injects all parameters.
	 */
	protected void injectParams(Object bean, BeanDefinition def) {
		if (def.name == null) {
			return;
		}

		if (def.params == null) {
			def.params = resolveBeanParams(def.name, petiteConfig.getResolveReferenceParameters());
		}
		int len = def.name.length() + 1;
		for (String param : def.params) {
			Object value = getParameter(param);
			String destination = param.substring(len);
			try {
				BeanUtil.declared.setProperty(bean, destination, value);
			} catch (Exception ex) {
				throw new PetiteException("Unable to set parameter: '" + param + "' to bean: " + def.name, ex);
			}
		}
	}


	// ---------------------------------------------------------------- get beans

	/**
	 * Returns Petite bean instance. Bean name will be resolved from provided type.
	 */
	@SuppressWarnings({"unchecked"})
	public <T> T getBean(Class<T> type) {
		String name = resolveBeanName(type);
		return (T) getBean(name);
	}

	/**
	 * Returns Petite bean instance named as one of the provided names.
	 */
	protected Object getBean(String[] names) {
		for (String name : names) {
			if (name == null) {
				continue;
			}
			Object bean = getBean(name);
			if (bean != null) {
				return bean;
			}
		}
		return null;
	}

	/**
	 * Returns Petite bean instance.
	 * Petite container will find the bean in corresponding scope and all its dependencies,
	 * either by constructor or property injection. When using constructor injection, cyclic dependencies
	 * can not be prevented, but at least they are detected.
	 *
	 * @see PetiteContainer#createBean(Class)
	 */
	public <T> T getBean(String name) {

		// Lookup for registered bean definition.
		BeanDefinition def = lookupBeanDefinition(name);

		if (def == null) {

			// try provider
			ProviderDefinition providerDefinition = providers.get(name);

			if (providerDefinition != null) {
				return (T) invokeProvider(providerDefinition);
			}
			return null;
		}

		// Find the bean in its scope
		Object bean = def.scopeLookup();

		if (bean == null) {
			// Create new bean in the scope
			bean = newBeanInstance(def);
			registerBeanAndWireAndInjectParamsAndInvokeInitMethods(def, bean);
		}

		return (T) bean;
	}

	/**
	 * Wires bean, injects parameters and invokes init methods.
	 * Such a loooong name :)
	 */
	protected void registerBeanAndWireAndInjectParamsAndInvokeInitMethods(BeanDefinition def, Object bean) {
		resolveInitAndDestroyMethods(bean, def);
		def.scopeRegister(bean);
		invokeInitMethods(bean, def, InitMethodInvocationStrategy.POST_CONSTRUCT);
		wireBean(bean, def);
		invokeInitMethods(bean, def, InitMethodInvocationStrategy.POST_DEFINE);
		injectParams(bean, def);
		invokeInitMethods(bean, def, InitMethodInvocationStrategy.POST_INITIALIZE);
	}

	// ---------------------------------------------------------------- wire

	/**
	 * Wires provided bean with the container using default wiring mode.
	 * Bean is <b>not</b> registered withing container.
	 */
	public void wire(Object bean) {
		wire(bean, null);
	}

	/**
	 * Wires provided bean with the container and optionally invokes init methods.
	 * Bean is <b>not</b> registered withing container.
	 */
	public void wire(Object bean, WiringMode wiringMode) {
		wiringMode = petiteConfig.resolveWiringMode(wiringMode);
		BeanDefinition def = new BeanDefinition(null, bean.getClass(), null, wiringMode);
		registerBeanAndWireAndInjectParamsAndInvokeInitMethods(def, bean);
	}

	// ---------------------------------------------------------------- create

	/**
	 * Creates and wires a bean within the container using default wiring mode and default init methods flag.
	 * Bean is <b>not</b> registered.
	 */
	public <E> E createBean(Class<E> type) {
		return createBean(type, null);
	}

	/**
	 * Creates and wires a bean within the container and optionally invokes init methods. However, bean is
	 * <b>not</b> registered.
	 */
	@SuppressWarnings({"unchecked"})
	public <E> E createBean(Class<E> type, WiringMode wiringMode) {
		wiringMode = petiteConfig.resolveWiringMode(wiringMode);
		BeanDefinition def = new BeanDefinition(null, type, null, wiringMode);
		Object bean = newBeanInstance(def);
		registerBeanAndWireAndInjectParamsAndInvokeInitMethods(def, bean);
		return (E) bean;
	}

	// ---------------------------------------------------------------- providers

	/**
	 * Invokes provider to get a bean.
	 */
	protected Object invokeProvider(ProviderDefinition provider) {
		if (provider.method != null) {

			Object bean;
			if (provider.beanName != null) {
				// instance factory method
				bean = getBean(provider.beanName);
			} else {
				// static factory method
				bean = null;
			}
			try {
				return provider.method.invoke(bean);
			} catch (Exception ex) {
				throw new PetiteException("Invalid provider method: " + provider.method.getName(), ex);
			}
		}

		throw new PetiteException("Invalid provider");
	}



	// ---------------------------------------------------------------- add

	/**
	 * Adds object instance to the container as singleton bean using default
	 * wiring mode and default init method flag.
	 */
	public void addBean(String name, Object bean) {
		addBean(name, bean, null);
	}

	/**
	 * Adds object instance to the container as singleton bean.
	 */
	public void addBean(String name, Object bean, WiringMode wiringMode) {
		wiringMode = petiteConfig.resolveWiringMode(wiringMode);
		registerPetiteBean(bean.getClass(), name, SingletonScope.class, wiringMode, false);
		BeanDefinition def = lookupExistingBeanDefinition(name);
		registerBeanAndWireAndInjectParamsAndInvokeInitMethods(def, bean);
	}

	/**
	 * Adds self instance to the container so internal beans may fetch
	 * container for further usage. No wiring is used and no init methods are invoked.
	 */
	public void addSelf(String name) {
		addBean(name, this, WiringMode.NONE);
	}

	/**
	 * Adds self instance to the container so internal beans may fetch
	 * container for further usage. No wiring is used and no init methods are invoked.
	 */
	public void addSelf() {
		addBean(PETITE_CONTAINER_REF_NAME, this, WiringMode.NONE);
	}

	// ---------------------------------------------------------------- property

	/**
	 * Sets petite bean property.
	 */
	public void setBeanProperty(String name, Object value) {
		Object bean = null;
		int ndx = name.length();

		while (true) {
			ndx = name.lastIndexOf('.', ndx);
			if (ndx == -1) {
				break;
			}

			String beanName = name.substring(0, ndx);
			bean = getBean(beanName);
			if (bean != null) {
				break;
			}
			ndx--;
		}

		if (bean == null) {
			throw new PetiteException("Invalid bean property: " + name);
		}

		try {
			BeanUtil.declared.setProperty(bean, name.substring(ndx + 1), value);
		} catch (Exception ex) {
			throw new PetiteException("Invalid bean property: " + name, ex);
		}
	}

	/**
	 * Returns petite bean property value.
	 */
	public Object getBeanProperty(String name) {
		int ndx = name.indexOf('.');
		if (ndx == -1) {
			throw new PetiteException("Only bean name is specified, missing property name: " + name);
		}
		String beanName = name.substring(0, ndx);
		Object bean = getBean(beanName);
		if (bean == null) {
			throw new PetiteException("Bean doesn't exist: " + name);
		}
		try {
			return BeanUtil.declared.getProperty(bean, name.substring(ndx + 1));
		} catch (Exception ex) {
			throw new PetiteException("Invalid bean property: " + name, ex);
		}
	}

	// ---------------------------------------------------------------- shutdown

	/**
	 * Shutdowns container. After container is down, it can't be used anymore.
	 */
	public void shutdown() {
		for (Scope scope : scopes.values()) {
			scope.shutdown();
		}

		beans.clear();
		beansAlt.clear();
		scopes.clear();
		providers.clear();
		beanCollections.clear();

	}

}