// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
// * with the License.  You may obtain a copy of the License at                                                              *
// *                                                                                                                         *
// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
// *                                                                                                                         *
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
// * specific language governing permissions and limitations under the License.                                              *
// ***************************************************************************************************************************
package org.apache.juneau.cp;

import static org.apache.juneau.collections.JsonMap.*;
import static org.apache.juneau.common.internal.StringUtils.*;
import static org.apache.juneau.internal.CollectionUtils.*;
import static java.util.stream.Collectors.*;

import java.lang.annotation.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.stream.*;

import org.apache.juneau.*;
import org.apache.juneau.collections.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.marshaller.*;
import org.apache.juneau.reflect.*;

/**
 * Java bean store.
 *
 * <p>
 * A simple storage database for beans keyed by type and name.
 * Used to retrieve and instantiate beans using an injection-like API.
 * It's similar in concept to the injection framework of Spring but greatly simplified in function and not intended to implement a full-fledged injection framework.
 *
 * <p>
 * Beans can be stored with or without names.  Named beans are typically resolved using
 * the <ja>@Named</ja> or <ja>@Qualified</ja> annotations on constructor or method parameters.
 *
 * <p>
 * Beans are added through the following methods:
 * <ul class='javatreec'>
 * 	<li class='jm'>{@link #add(Class,Object) add(Class,Object)}
 * 	<li class='jm'>{@link #add(Class,Object,String) add(Class,Object,String)}
 * 	<li class='jm'>{@link #addBean(Class,Object) addBean(Class,Object)}
 * 	<li class='jm'>{@link #addBean(Class,Object,String) addBean(Class,Object,String)}
 * 	<li class='jm'>{@link #addSupplier(Class,Supplier) addSupplier(Class,Supplier)}
 * 	<li class='jm'>{@link #addSupplier(Class,Supplier,String) addSupplier(Class,Supplier,String)}
 * </ul>
 *
 * <p>
 * Beans are retrieved through the following methods:
 * <ul class='javatreec'>
 * 	<li class='jm'>{@link #getBean(Class) getBean(Class)}
 * 	<li class='jm'>{@link #getBean(Class,String) getBean(Class,String)}
 * 	<li class='jm'>{@link #stream(Class) stream(Class)}
 * </ul>
 *
 * <p>
 * Beans are created through the following methods:
 * <ul class='javatreec'>
 * 	<li class='jm'>{@link #createBean(Class) createBean(Class)}
 * 	<li class='jm'>{@link #createMethodFinder(Class) createMethodFinder(Class)}
 * 	<li class='jm'>{@link #createMethodFinder(Class,Class) createMethodFinder(Class,Class)}
 * 	<li class='jm'>{@link #createMethodFinder(Class,Object) createMethodFinder(Class,Object)}
 * </ul>
 *
 * <h5 class='section'>Notes:</h5><ul>
 * 	<li class='note'>Bean stores can be nested using {@link Builder#parent(BeanStore)}.
 * 	<li class='note'>Bean stores can be made read-only using {@link Builder#readOnly()}.
 * 	<li class='note'>Bean stores can be made thread-safe using {@link Builder#threadSafe()}.
 * </ul>
 *
 * <h5 class='section'>See Also:</h5><ul>
 * </ul>
 */
public class BeanStore {

	//-----------------------------------------------------------------------------------------------------------------
	// Static
	//-----------------------------------------------------------------------------------------------------------------

	/**
	 * Non-existent bean store.
	 */
	public static final class Void extends BeanStore {}

	/**
	 * Static read-only reusable instance.
	 */
	public static final BeanStore INSTANCE = create().readOnly().build();

	/**
	 * Static creator.
	 *
	 * @return A new {@link Builder} object.
	 */
	public static Builder create() {
		return new Builder();
	}

	/**
	 * Static creator.
	 *
	 * @param parent Parent bean store.  Can be <jk>null</jk> if this is the root resource.
	 * @return A new {@link BeanStore} object.
	 */
	public static BeanStore of(BeanStore parent) {
		return create().parent(parent).build();
	}

	/**
	 * Static creator.
	 *
	 * @param parent Parent bean store.  Can be <jk>null</jk> if this is the root resource.
	 * @param outer The outer bean used when instantiating inner classes.  Can be <jk>null</jk>.
	 * @return A new {@link BeanStore} object.
	 */
	public static BeanStore of(BeanStore parent, Object outer) {
		return create().parent(parent).outer(outer).build();
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Builder
	//-----------------------------------------------------------------------------------------------------------------

	/**
	 * Builder class.
	 */
	@FluentSetters
	public static class Builder {

		BeanStore parent;
		boolean readOnly, threadSafe;
		Object outer;
		Class<? extends BeanStore> type;
		BeanStore impl;

		/**
		 * Constructor.
		 */
		protected Builder() {}

		/**
		 * Instantiates this bean store.
		 *
		 * @return A new bean store.
		 */
		public BeanStore build() {
			if (impl != null)
				return impl;
			if (type == null || type == BeanStore.class)
				return new BeanStore(this);

			ClassInfo c = ClassInfo.of(type);

			MethodInfo m = c.getDeclaredMethod(
				x -> x.isPublic()
				&& x.hasNoParams()
				&& x.isStatic()
				&& x.hasName("getInstance")
			);
			if (m != null)
				return m.invoke(null);

			ConstructorInfo ci = c.getPublicConstructor(x -> x.canAccept(this));
			if (ci != null)
				return ci.invoke(this);

			ci = c.getDeclaredConstructor(x -> x.isProtected() && x.canAccept(this));
			if (ci != null)
				return ci.accessible().invoke(this);

			throw new BasicRuntimeException("Could not find a way to instantiate class {0}", type);
		}

		//-------------------------------------------------------------------------------------------------------------
		// Properties
		//-------------------------------------------------------------------------------------------------------------

		/**
		 * Specifies the parent bean store.
		 *
		 * <p>
		 * Bean searches are performed recursively up this parent chain.
		 *
		 * @param value The setting value.
		 * @return  This object.
		 */
		@FluentSetter
		public Builder parent(BeanStore value) {
			parent = value;
			return this;
		}

		/**
		 * Specifies that the bean store is read-only.
		 *
		 * <p>
		 * This means methods such as {@link BeanStore#addBean(Class, Object)} cannot be used.
		 *
		 * @return  This object.
		 */
		@FluentSetter
		public Builder readOnly() {
			readOnly = true;
			return this;
		}

		/**
		 * Specifies that the bean store being created should be thread-safe.
		 *
		 * @return  This object.
		 */
		@FluentSetter
		public Builder threadSafe() {
			threadSafe = true;
			return this;
		}

		/**
		 * Specifies the outer bean context.
		 *
		 * <p>
		 * The outer context bean to use when calling constructors on inner classes.
		 *
		 * @param value The outer bean context.  Can be <jk>null</jk>.
		 * @return  This object.
		 */
		@FluentSetter
		public Builder outer(Object value) {
			this.outer = value;
			return this;
		}

		/**
		 * Overrides the bean to return from the {@link #build()} method.
		 *
		 * @param value The bean to return from the {@link #build()} method.
		 * @return This object.
		 */
		@FluentSetter
		public Builder impl(BeanStore value) {
			this.impl = value;
			return this;
		}

		/**
		 * Overrides the bean store type.
		 *
		 * <p>
		 * The specified type must have one of the following:
		 * <ul>
		 * 	<li>A static <c>getInstance()</c> method.
		 * 	<li>A public constructor that takes in this builder.
		 * 	<li>A protected constructor that takes in this builder.
		 * </ul>
		 *
		 * @param value The bean store type.
		 * @return This object.
		 */
		@FluentSetter
		public Builder type(Class<? extends BeanStore> value) {
			this.type = value;
			return this;
		}
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Instance
	//-----------------------------------------------------------------------------------------------------------------

	private final Deque<BeanStoreEntry<?>> entries;
	private final Map<Class<?>,BeanStoreEntry<?>> unnamedEntries;

	final Optional<BeanStore> parent;
	final Optional<Object> outer;
	final boolean readOnly, threadSafe;
	final SimpleReadWriteLock lock;

	BeanStore() {
		this(create());
	}

	/**
	 * Constructor.
	 *
	 * @param builder The builder containing the settings for this bean.
	 */
	protected BeanStore(Builder builder) {
		parent = optional(builder.parent);
		outer = optional(builder.outer);
		readOnly = builder.readOnly;
		threadSafe = builder.threadSafe;
		lock = threadSafe ? new SimpleReadWriteLock() : SimpleReadWriteLock.NO_OP;
		entries = threadSafe ? new ConcurrentLinkedDeque<>() : linkedList();
		unnamedEntries = threadSafe ? new ConcurrentHashMap<>() : map();
	}

	/**
	 * Adds an unnamed bean of the specified type to this factory.
	 *
	 * @param <T> The class to associate this bean with.
	 * @param beanType The class to associate this bean with.
	 * @param bean The bean.  Can be <jk>null</jk>.
	 * @return This object.
	 */
	public <T> BeanStore addBean(Class<T> beanType, T bean) {
		return addBean(beanType, bean, null);
	}

	/**
	 * Adds a named bean of the specified type to this factory.
	 *
	 * @param <T> The class to associate this bean with.
	 * @param beanType The class to associate this bean with.
	 * @param bean The bean.  Can be <jk>null</jk>.
	 * @param name The bean name if this is a named bean.  Can be <jk>null</jk>.
	 * @return This object.
	 */
	public <T> BeanStore addBean(Class<T> beanType, T bean, String name) {
		return addSupplier(beanType, ()->bean, name);
	}

	/**
	 * Adds a supplier for an unnamed bean of the specified type to this factory.
	 *
	 * @param <T> The class to associate this bean with.
	 * @param beanType The class to associate this bean with.
	 * @param bean The bean supplier.
	 * @return This object.
	 */
	public <T> BeanStore addSupplier(Class<T> beanType, Supplier<T> bean) {
		return addSupplier(beanType, bean, null);
	}

	/**
	 * Adds a supplier for a named bean of the specified type to this factory.
	 *
	 * @param <T> The class to associate this bean with.
	 * @param beanType The class to associate this bean with.
	 * @param bean The bean supplier.
	 * @param name The bean name if this is a named bean.  Can be <jk>null</jk>.
	 * @return This object.
	 */
	public <T> BeanStore addSupplier(Class<T> beanType, Supplier<T> bean, String name) {
		assertCanWrite();
		BeanStoreEntry<T> e = createEntry(beanType, bean, name);
		try (SimpleLock x = lock.write()) {
			entries.addFirst(e);
			if (isEmpty(name))
				unnamedEntries.put(beanType, e);
		}
		return this;
	}

	/**
	 * Same as {@link #addBean(Class,Object)} but returns the bean instead of this object for fluent calls.
	 *
	 * @param <T> The class to associate this bean with.
	 * @param beanType The class to associate this bean with.
	 * @param bean The bean.  Can be <jk>null</jk>.
	 * @return The bean.
	 */
	public <T> T add(Class<T> beanType, T bean) {
		add(beanType, bean, null);
		return bean;
	}

	/**
	 * Same as {@link #addBean(Class,Object,String)} but returns the bean instead of this object for fluent calls.
	 *
	 * @param <T> The class to associate this bean with.
	 * @param beanType The class to associate this bean with.
	 * @param bean The bean.  Can be <jk>null</jk>.
	 * @param name The bean name if this is a named bean.  Can be <jk>null</jk>.
	 * @return The bean.
	 */
	public <T> T add(Class<T> beanType, T bean, String name) {
		addBean(beanType, bean, name);
		return bean;
	}

	/**
	 * Clears out all bean in this bean store.
	 *
	 * <p>
	 * Does not affect the parent bean store.
	 *
	 * @return This object.
	 */
	public BeanStore clear() {
		assertCanWrite();
		try (SimpleLock x = lock.write()) {
			unnamedEntries.clear();
			entries.clear();
		}
		return this;
	}

	/**
	 * Returns the unnamed bean of the specified type.
	 *
	 * @param <T> The type of bean to return.
	 * @param beanType The type of bean to return.
	 * @return The bean.
	 */
	@SuppressWarnings("unchecked")
	public <T> Optional<T> getBean(Class<T> beanType) {
		try (SimpleLock x = lock.read()) {
			BeanStoreEntry<T> e = (BeanStoreEntry<T>) unnamedEntries.get(beanType);
			if (e != null)
				return optional(e.get());
			if (parent.isPresent())
				return parent.get().getBean(beanType);
			return empty();
		}
	}

	/**
	 * Returns the named bean of the specified type.
	 *
	 * @param <T> The type of bean to return.
	 * @param beanType The type of bean to return.
	 * @param name The bean name.  Can be <jk>null</jk>.
	 * @return The bean.
	 */
	@SuppressWarnings("unchecked")
	public <T> Optional<T> getBean(Class<T> beanType, String name)  {
		try (SimpleLock x = lock.read()) {
			BeanStoreEntry<T> e = (BeanStoreEntry<T>)entries.stream().filter(x2 -> x2.matches(beanType, name)).findFirst().orElse(null);
			if (e != null)
				return optional(e.get());
			if (parent.isPresent())
				return parent.get().getBean(beanType, name);
			return empty();
		}
	}

	/**
	 * Returns all the beans in this store of the specified type.
	 *
	 * <p>
	 * Returns both named and unnamed beans.
	 *
	 * <p>
	 * The results from the parent bean store are appended to the list of beans from this beans store.
	 *
	 * @param <T> The bean type to return.
	 * @param beanType The bean type to return.
	 * @return The bean entries.  Never <jk>null</jk>.
	 */
	public <T> Stream<BeanStoreEntry<T>> stream(Class<T> beanType)  {
		@SuppressWarnings("unchecked")
		Stream<BeanStoreEntry<T>> s = entries.stream().filter(x -> x.matches(beanType)).map(x -> (BeanStoreEntry<T>)x);
		if (parent.isPresent())
			s = Stream.concat(s, parent.get().stream(beanType));
		return s;
	}

	/**
	 * Removes an unnamed bean from this store.
	 *
	 * @param beanType The bean type being removed.
	 * @return This object.
	 */
	public BeanStore removeBean(Class<?> beanType) {
		return removeBean(beanType, null);
	}

	/**
	 * Removes a named bean from this store.
	 *
	 * @param beanType The bean type being removed.
	 * @param name The bean name to remove.
	 * @return This object.
	 */
	public BeanStore removeBean(Class<?> beanType, String name) {
		assertCanWrite();
		try (SimpleLock x = lock.write()) {
			if (name == null)
				unnamedEntries.remove(beanType);
			entries.removeIf(y -> y.matches(beanType, name));
		}
		return this;
	}

	/**
	 * Returns <jk>true</jk> if this store contains the specified unnamed bean type.
	 *
	 * @param beanType The bean type to check.
	 * @return <jk>true</jk> if this store contains the specified unnamed bean type.
	 */
	public boolean hasBean(Class<?> beanType) {
		return unnamedEntries.containsKey(beanType) || parent.map(x -> x.hasBean(beanType)).orElse(false);
	}

	/**
	 * Returns <jk>true</jk> if this store contains the specified named bean type.
	 *
	 * @param beanType The bean type to check.
	 * @param name The bean name.
	 * @return <jk>true</jk> if this store contains the specified named bean type.
	 */
	public boolean hasBean(Class<?> beanType, String name) {
		return entries.stream().anyMatch(x -> x.matches(beanType, name)) || parent.map(x -> x.hasBean(beanType, name)).orElse(false);
	}

	/**
	 * Instantiates a bean creator.
	 *
	 * <h5 class='section'>See Also:</h5><ul>
	 * 	<li class='jc'>{@link BeanCreator} for usage.
	 * </ul>
	 *
	 * @param <T> The bean type to create.
	 * @param beanType The bean type to create.
	 * @return A new bean creator.
	 */
	public <T> BeanCreator<T> createBean(Class<T> beanType) {
		return new BeanCreator<>(beanType, this);
	}

	/**
	 * Create a method finder for finding bean creation methods.
	 *
	 * <h5 class='section'>See Also:</h5><ul>
	 * 	<li class='jc'>{@link BeanCreateMethodFinder} for usage.
	 * </ul>
	 *
	 * @param <T> The bean type to create.
	 * @param beanType The bean type to create.
	 * @param resource The class containing the bean creator method.
	 * @return The method finder.  Never <jk>null</jk>.
	 */
	public <T> BeanCreateMethodFinder<T> createMethodFinder(Class<T> beanType, Object resource) {
		return new BeanCreateMethodFinder<>(beanType, resource, this);
	}

	/**
	 * Create a method finder for finding bean creation methods.
	 *
	 * <p>
	 * Same as {@link #createMethodFinder(Class,Class)} but looks for only static methods on the specified resource class
	 * and not also instance methods within the context of a bean.
	 *
	 * <h5 class='section'>See Also:</h5><ul>
	 * 	<li class='jc'>{@link BeanCreateMethodFinder} for usage.
	 * </ul>
	 *
	 * @param <T> The bean type to create.
	 * @param beanType The bean type to create.
	 * @param resourceClass The class containing the bean creator method.
	 * @return The method finder.  Never <jk>null</jk>.
	 */
	public <T> BeanCreateMethodFinder<T> createMethodFinder(Class<T> beanType, Class<?> resourceClass) {
		return new BeanCreateMethodFinder<>(beanType, resourceClass , this);
	}

	/**
	 * Create a method finder for finding bean creation methods.
	 *
	 * <p>
	 * Same as {@link #createMethodFinder(Class,Object)} but uses {@link Builder#outer(Object)} as the resource bean.
	 *
	 * <h5 class='section'>See Also:</h5><ul>
	 * 	<li class='jc'>{@link BeanCreateMethodFinder} for usage.
	 * </ul>
	 *
	 * @param <T> The bean type to create.
	 * @param beanType The bean type to create.
	 * @return The method finder.  Never <jk>null</jk>.
	 */
	public <T> BeanCreateMethodFinder<T> createMethodFinder(Class<T> beanType) {
		return new BeanCreateMethodFinder<>(beanType, outer.orElseThrow(() -> new IllegalArgumentException("Method cannot be used without outer bean definition.")), this);
	}

	/**
	 * Given an executable, returns a list of types that are missing from this factory.
	 *
	 * @param executable The constructor or method to get the params for.
	 * @return A comma-delimited list of types that are missing from this factory, or <jk>null</jk> if none are missing.
	 */
	public String getMissingParams(ExecutableInfo executable) {
		List<ParamInfo> params = executable.getParams();
		List<String> l = list();
		loop: for (int i = 0; i < params.size(); i++) {
			ParamInfo pi = params.get(i);
			ClassInfo pt = pi.getParameterType();
			if (i == 0 && outer.isPresent() && pt.isInstance(outer.get()))
				continue loop;
			if (pt.is(Optional.class) || pt.is(BeanStore.class))
				continue loop;
			String beanName = findBeanName(pi);
			Class<?> ptc = pt.inner();
			if (beanName == null && !hasBean(ptc))
				l.add(pt.getSimpleName());
			if (beanName != null && !hasBean(ptc, beanName))
				l.add(pt.getSimpleName() + '@' + beanName);
		}
		return l.isEmpty() ? null : l.stream().sorted().collect(joining(","));
	}

	/**
	 * Given the list of param types, returns <jk>true</jk> if this factory has all the parameters for the specified executable.
	 *
	 * @param executable The constructor or method to get the params for.
	 * @return A comma-delimited list of types that are missing from this factory.
	 */
	public boolean hasAllParams(ExecutableInfo executable) {
		loop: for (int i = 0; i < executable.getParamCount(); i++) {
			ParamInfo pi = executable.getParam(i);
			ClassInfo pt = pi.getParameterType();
			if (i == 0 && outer.isPresent() && pt.isInstance(outer.get()))
				continue loop;
			if (pt.is(Optional.class) || pt.is(BeanStore.class))
				continue loop;
			String beanName = findBeanName(pi);
			Class<?> ptc = pt.inner();
			if ((beanName == null && !hasBean(ptc)) || (beanName != null && !hasBean(ptc, beanName)))
				return false;
		}
		return true;
	}


	/**
	 * Returns the corresponding beans in this factory for the specified param types.
	 *
	 * @param executable The constructor or method to get the params for.
	 * @return The corresponding beans in this factory for the specified param types.
	 */
	public Object[] getParams(ExecutableInfo executable) {
		Object[] o = new Object[executable.getParamCount()];
		for (int i = 0; i < executable.getParamCount(); i++) {
			ParamInfo pi = executable.getParam(i);
			ClassInfo pt = pi.getParameterType();
			if (i == 0 && outer.isPresent() && pt.isInstance(outer.get())) {
				o[i] = outer.get();
			} else if (pt.is(BeanStore.class)) {
				o[i] = this;
			} else {
				String beanName = findBeanName(pi);
				Class<?> ptc = pt.unwrap(Optional.class).inner();
				Optional<?> o2 = beanName == null ? getBean(ptc) : getBean(ptc, beanName);
				o[i] = pt.is(Optional.class) ? o2 : o2.orElse(null);
			}
		}
		return o;
	}

	@Override /* Object */
	public String toString() {
		return Json5.of(properties());
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Extension methods
	//-----------------------------------------------------------------------------------------------------------------

	/**
	 * Creates an entry in this store for the specified bean.
	 *
	 * <p>
	 * Subclasses can override this method to create their own entry subtypes.
	 *
	 * @param <T> The class type to associate with the bean.
	 * @param type The class type to associate with the bean.
	 * @param bean The bean supplier.
	 * @param name Optional name to associate with the bean.  Can be <jk>null</jk>.
	 * @return A new bean store entry.
	 */
	protected <T> BeanStoreEntry<T> createEntry(Class<T> type, Supplier<T> bean, String name) {
		return BeanStoreEntry.create(type, bean, name);
	}

	// <FluentSetters>

		// </FluentSetters>

	//-----------------------------------------------------------------------------------------------------------------
	// Helper methods
	//-----------------------------------------------------------------------------------------------------------------
	private String findBeanName(ParamInfo pi) {
		Annotation n = pi.getAnnotation(Annotation.class, x -> x.annotationType().getSimpleName().equals("Named"));
		if (n != null)
			return AnnotationInfo.of((ClassInfo)null, n).getValue(String.class, "value", NOT_EMPTY).orElse(null);
		return null;
	}

	private void assertCanWrite() {
		if (readOnly)
			throw new IllegalStateException("Method cannot be used because BeanStore is read-only.");
	}

	private JsonMap properties() {
		Predicate<Boolean> nf = ObjectUtils::isTrue;
		return filteredMap()
			.append("identity", ObjectUtils.identity(this))
			.append("entries", entries.stream().map(BeanStoreEntry::properties).collect(toList()))
			.append("outer", ObjectUtils.identity(outer.orElse(null)))
			.append("parent", parent.map(BeanStore::properties).orElse(null))
			.appendIf(nf, "readOnly", readOnly)
			.appendIf(nf, "threadSafe", threadSafe)
		;
	}
}
