package de.metas.ui.web.process.view;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.adempiere.util.lang.MutableInt;
import org.compiere.Adempiere;
import org.reflections.ReflectionUtils;
import org.slf4j.Logger;

import de.metas.cache.CCache;
import de.metas.i18n.IMsgBL;
import de.metas.logging.LogManager;
import de.metas.ui.web.process.ProcessInstanceResult;
import de.metas.ui.web.process.view.ViewAction.AlwaysAllowPrecondition;
import de.metas.ui.web.process.view.ViewActionDescriptor.ViewActionDescriptorBuilder;
import de.metas.ui.web.process.view.ViewActionDescriptor.ViewActionMethodArgumentExtractor;
import de.metas.ui.web.process.view.ViewActionDescriptor.ViewActionMethodReturnTypeConverter;
import de.metas.ui.web.view.IView;
import de.metas.ui.web.window.datatypes.DocumentIdsSelection;
import de.metas.util.Services;

import lombok.NonNull;

/*
 * #%L
 * metasfresh-webui-api
 * %%
 * Copyright (C) 2017 metas GmbH
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program. If not, see
 * <http://www.gnu.org/licenses/gpl-2.0.html>.
 * #L%
 */

/**
 * Factory method used extract {@link ViewActionDescriptorsList} from different sources.
 *
 * @author metas-dev <[email protected]>
 *
 */
public class ViewActionDescriptorsFactory
{
	public static final transient ViewActionDescriptorsFactory instance = new ViewActionDescriptorsFactory();

	private static final transient Logger logger = LogManager.getLogger(ViewActionDescriptorsFactory.class);

	private final CCache<String, ViewActionDescriptorsList> viewActionsDescriptorByViewClassname = CCache.newCache("viewActionsDescriptorByViewClassname", 50, 0);

	private ViewActionDescriptorsFactory()
	{
	}

	/**
	 * Gets the view action descriptors from given class.
	 *
	 * @param clazz an utility class or a view class
	 * @return action descriptors
	 */
	public ViewActionDescriptorsList getFromClass(@NonNull final Class<?> clazz)
	{
		return viewActionsDescriptorByViewClassname.getOrLoad(clazz.getName(), () -> createFromClass(clazz));
	}

	private static final ViewActionDescriptorsList createFromClass(@NonNull final Class<?> clazz)
	{
		final ActionIdGenerator actionIdGenerator = new ActionIdGenerator();

		@SuppressWarnings("unchecked")
		final Set<Method> viewActionMethods = ReflectionUtils.getAllMethods(clazz, ReflectionUtils.withAnnotation(ViewAction.class));
		final List<ViewActionDescriptor> viewActions = viewActionMethods.stream()
				.map(viewActionMethod -> {
					try
					{
						return createViewActionDescriptor(actionIdGenerator.getActionId(viewActionMethod), viewActionMethod);
					}
					catch (final Throwable ex)
					{
						logger.warn("Failed creating view action descriptor for {}. Ignored", viewActionMethod, ex);
						return null;
					}
				})
				.filter(actionDescriptor -> actionDescriptor != null)
				.collect(Collectors.toList());

		return ViewActionDescriptorsList.of(viewActions);
	}

	private static final ViewActionDescriptor createViewActionDescriptor(@NonNull final String actionId, @NonNull final Method viewActionMethod)
	{
		if (!viewActionMethod.isAccessible())
		{
			viewActionMethod.setAccessible(true);
		}

		final ViewActionDescriptorBuilder actionBuilder = ViewActionDescriptor.builder()
				.actionId(actionId)
				.viewActionMethod(viewActionMethod);

		final ViewAction viewActionAnn = viewActionMethod.getAnnotation(ViewAction.class);
		actionBuilder
				.caption(Services.get(IMsgBL.class).getTranslatableMsgText(viewActionAnn.caption()))
				.description(Services.get(IMsgBL.class).getTranslatableMsgText(viewActionAnn.description()))
				.defaultAction(viewActionAnn.defaultAction())
				.layoutType(viewActionAnn.layoutType());

		//
		// Preconditions
		final Class<? extends ViewAction.Precondition> preconditionClass = viewActionAnn.precondition();
		ViewAction.Precondition preconditionSharedInstance;
		if (AlwaysAllowPrecondition.class.equals(preconditionClass))
		{
			preconditionSharedInstance = AlwaysAllowPrecondition.instance;
		}
		else
		{
			preconditionSharedInstance = null;
		}
		actionBuilder
				.preconditionClass(preconditionClass)
				.preconditionSharedInstance(preconditionSharedInstance);

		//
		// View action method's return type
		actionBuilder.viewActionReturnTypeConverter(createReturnTypeConverter(viewActionMethod));

		//
		// View action method's parameters
		{
			final Class<?>[] methodParameterTypes = viewActionMethod.getParameterTypes();
			final Annotation[][] methodParameterAnnotations = viewActionMethod.getParameterAnnotations();
			for (int parameterIndex = 0; parameterIndex < methodParameterTypes.length; parameterIndex++)
			{
				final String parameterName = String.valueOf(parameterIndex);
				final Class<?> methodParameterType = methodParameterTypes[parameterIndex];
				final ViewActionParam methodParameterAnnotation = Stream.of(methodParameterAnnotations[parameterIndex])
						.filter(ann -> ann instanceof ViewActionParam)
						.map(ann -> (ViewActionParam)ann)
						.findFirst().orElse(null);

				final ViewActionParamDescriptor paramDescriptor = ViewActionParamDescriptor.builder()
						.parameterName(parameterName)
						.parameterValueClass(methodParameterType)
						.parameterAnnotation(methodParameterAnnotation)
						.methodArgumentExtractor(createViewActionMethodArgumentExtractor(parameterName, methodParameterType, methodParameterAnnotation))
						.build();
				actionBuilder.viewActionParamDescriptor(paramDescriptor);
			}
		}

		return actionBuilder.build();
	}

	private static final ViewActionMethodReturnTypeConverter createReturnTypeConverter(final Method method)
	{
		final Class<?> viewActionReturnType = method.getReturnType();
		if (Void.class.equals(viewActionReturnType) || void.class.equals(viewActionReturnType))
		{
			return returnValue -> null;
		}
		else if (ProcessInstanceResult.ResultAction.class.isAssignableFrom(viewActionReturnType))
		{
			return returnType -> (ProcessInstanceResult.ResultAction)returnType;
		}
		else
		{
			throw new IllegalArgumentException("Action method's return type is not supported: " + method);
		}

	}

	private static final ViewActionMethodArgumentExtractor createViewActionMethodArgumentExtractor(final String parameterName, final Class<?> parameterType, final ViewActionParam annotation)
	{
		if (annotation != null)
		{
			return (view, processParameters, selectedDocumentIds) -> processParameters.getFieldView(parameterName).getValueAs(parameterType);
		}
		//
		// selectedDocumentIds internal parameter
		else if (DocumentIdsSelection.class.isAssignableFrom(parameterType))
		{
			return (view, processParameters, selectedDocumentIds) -> selectedDocumentIds;
		}
		//
		// View parameter
		else if (IView.class.isAssignableFrom(parameterType))
		{
			return (view, processParameters, selectedDocumentIds) -> view;
		}

		//
		// Primitive type => not supported
		else if (parameterType.isPrimitive())
		{
			throw new IllegalArgumentException("Action method's primitive parameter " + parameterType + " is not supported for parameterName: " + parameterName);
		}
		//
		// Try getting the bean from spring context
		else
		{
			return (view, processParameters, selectedDocumentIds) -> Adempiere.getBean(parameterType);
		}
	}

	/** Helper class used to generate unique actionIds based on annotated method name */
	private static final class ActionIdGenerator
	{
		private final Map<String, MutableInt> methodName2counter = new HashMap<>();

		public String getActionId(final Method actionMethod)
		{
			final String methodName = actionMethod.getName();
			final MutableInt counter = methodName2counter.computeIfAbsent(methodName, k -> new MutableInt(0));
			final int methodNameSuffix = counter.incrementAndGet();

			if (methodNameSuffix == 1)
			{
				return methodName;
			}
			else if (methodNameSuffix > 1)
			{
				return methodName + methodNameSuffix;
			}
			else
			{
				// shall NEVER happen
				throw new IllegalStateException("internal error: methodNameSuffix <= 0");
			}
		}
	}
}