/*
 * Copyright 2016-2020 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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.springframework.data.rest.webmvc;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceProcessor;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.core.EmbeddedWrapper;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;

/**
 * Component to easily invoke all {@link ResourceProcessor} instances registered for values of type
 * {@link ResourceSupport}.
 *
 * @author Oliver Gierke
 * @author Mark Paluch
 * @since 2.5
 * @deprecated use the same type from Spring HATEOAS, will be removed in 2.7.
 */
@Deprecated
public class ResourceProcessorInvoker {

	private final List<ProcessorWrapper> processors;

	/**
	 * Creates a new {@link ResourceProcessorInvoker} to consider the given {@link ResourceProcessor} to post-process the
	 * controller methods return value to before invoking the delegate.
	 *
	 * @param processors the {@link ResourceProcessor}s to be considered, must not be {@literal null}.
	 */
	public ResourceProcessorInvoker(Collection<ResourceProcessor<?>> processors) {

		Assert.notNull(processors, "ResourceProcessors must not be null!");

		this.processors = new ArrayList<ProcessorWrapper>();

		for (ResourceProcessor<?> processor : processors) {

			ResolvableType processorType = ResolvableType.forClass(ResourceProcessor.class, processor.getClass());
			Class<?> rawType = processorType.getGeneric(0).resolve();

			if (Resource.class.isAssignableFrom(rawType)) {
				this.processors.add(new ResourceProcessorWrapper(processor));
			} else if (Resources.class.isAssignableFrom(rawType)) {
				this.processors.add(new ResourcesProcessorWrapper(processor));
			} else {
				this.processors.add(new DefaultProcessorWrapper(processor));
			}
		}

		Collections.sort(this.processors, AnnotationAwareOrderComparator.INSTANCE);
	}

	/**
	 * Invokes all {@link ResourceProcessor} instances registered for the type of the given value.
	 *
	 * @param value must not be {@literal null}.
	 * @return
	 */
	public <T extends ResourceSupport> T invokeProcessorsFor(T value) {

		Assert.notNull(value, "Value must not be null!");

		return invokeProcessorsFor(value, ResolvableType.forClass(value.getClass()));
	}

	/**
	 * Invokes all {@link ResourceProcessor} instances registered for the type of the given value and reference type.
	 *
	 * @param value must not be {@literal null}.
	 * @param referenceType must not be {@literal null}.
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public <T extends ResourceSupport> T invokeProcessorsFor(T value, ResolvableType referenceType) {

		Assert.notNull(value, "Value must not be null!");
		Assert.notNull(referenceType, "Reference type must not be null!");

		// For Resources implementations, process elements first
		if (ResourceProcessorHandlerMethodReturnValueHandler.RESOURCES_TYPE.isAssignableFrom(referenceType)) {

			Resources<?> resources = (Resources<?>) value;
			ResolvableType elementTargetType = ResolvableType.forClass(Resources.class, referenceType.getRawClass())
					.getGeneric(0);
			List<Object> result = new ArrayList<Object>(resources.getContent().size());

			for (Object element : resources) {

				ResolvableType elementType = ResolvableType.forClass(element.getClass());

				if (!getRawType(elementTargetType).equals(elementType.getRawClass())) {
					elementTargetType = elementType;
				}

				result.add(invokeProcessorsFor(element, elementTargetType));
			}

			ReflectionUtils.setField(ResourceProcessorHandlerMethodReturnValueHandler.CONTENT_FIELD, resources, result);
		}

		return (T) invokeProcessorsFor((Object) value, referenceType);
	}

	/**
	 * Invokes all registered {@link ResourceProcessor}s registered for the given {@link ResolvableType}.
	 *
	 * @param value the object to process
	 * @param type
	 * @return
	 */
	private Object invokeProcessorsFor(Object value, ResolvableType type) {

		Object currentValue = value;

		// Process actual value
		for (ResourceProcessorInvoker.ProcessorWrapper wrapper : this.processors) {
			if (wrapper.supports(type, currentValue)) {
				currentValue = wrapper.invokeProcessor(currentValue);
			}
		}

		return currentValue;
	}

	private static boolean isRawTypeAssignable(ResolvableType left, Class<?> right) {
		return getRawType(left).isAssignableFrom(right);
	}

	private static Class<?> getRawType(ResolvableType type) {

		Class<?> rawType = type.getRawClass();
		return rawType == null ? Object.class : rawType;
	}

	/**
	 * Interface to unify interaction with {@link ResourceProcessor}s. The {@link Ordered} rank should be determined by
	 * the underlying processor.
	 *
	 * @author Oliver Gierke
	 */
	private interface ProcessorWrapper extends Ordered {

		/**
		 * Returns whether the underlying processor supports the given {@link ResolvableType}. It might also additionally
		 * inspect the object that would eventually be handed to the processor.
		 *
		 * @param type the type of object to be post processed, will never be {@literal null}.
		 * @param value the object that would be passed into the processor eventually, can be {@literal null}.
		 * @return
		 */
		boolean supports(ResolvableType type, Object value);

		/**
		 * Performs the actual invocation of the processor. Implementations can be sure
		 * {@link #supports(ResolvableType, Object)} has been called before and returned {@literal true}.
		 *
		 * @param object
		 */
		Object invokeProcessor(Object object);
	}

	/**
	 * Default implementation of {@link ProcessorWrapper} to generically deal with {@link ResourceSupport} types.
	 *
	 * @author Oliver Gierke
	 */
	private static class DefaultProcessorWrapper implements ResourceProcessorInvoker.ProcessorWrapper {

		private final ResourceProcessor<?> processor;
		private final ResolvableType targetType;

		/**
		 * Creates a new {@link DefaultProcessorWrapper} with the given {@link ResourceProcessor}.
		 *
		 * @param processor must not be {@literal null}.
		 */
		public DefaultProcessorWrapper(ResourceProcessor<?> processor) {

			Assert.notNull(processor, "ResourceProcessor must not be null!");

			this.processor = processor;
			this.targetType = ResolvableType.forClass(ResourceProcessor.class, processor.getClass()).getGeneric(0);
		}

		/*
		 * (non-Javadoc)
		 * @see org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler.ProcessorWrapper#supports(org.springframework.core.ResolvableType, java.lang.Object)
		 */
		@Override
		public boolean supports(ResolvableType type, Object value) {
			return isRawTypeAssignable(targetType, getRawType(type));
		}

		/*
		 * (non-Javadoc)
		 * @see org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler.PostProcessorWrapper#invokeProcessor(java.lang.Object)
		 */
		@Override
		@SuppressWarnings("unchecked")
		public Object invokeProcessor(Object object) {
			return ((ResourceProcessor<ResourceSupport>) processor).process((ResourceSupport) object);
		}

		/*
		 * (non-Javadoc)
		 * @see org.springframework.core.Ordered#getOrder()
		 */
		@Override
		public int getOrder() {
			return CustomOrderAwareComparator.INSTANCE.getOrder(processor);
		}

		/**
		 * Returns the target type the underlying {@link ResourceProcessor} wants to get invoked for.
		 *
		 * @return the targetType
		 */
		public ResolvableType getTargetType() {
			return targetType;
		}
	}

	/**
	 * {@link ProcessorWrapper} to deal with {@link ResourceProcessor}s for {@link Resource}s. Will fall back to peeking
	 * into the {@link Resource}'s content for type resolution.
	 *
	 * @author Oliver Gierke
	 */
	private static class ResourceProcessorWrapper extends ResourceProcessorInvoker.DefaultProcessorWrapper {

		/**
		 * Creates a new {@link ResourceProcessorWrapper} for the given {@link ResourceProcessor}.
		 *
		 * @param processor must not be {@literal null}.
		 */
		public ResourceProcessorWrapper(ResourceProcessor<?> processor) {
			super(processor);
		}

		/*
		 * (non-Javadoc)
		 * @see org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler.DefaultProcessorWrapper#supports(org.springframework.core.ResolvableType, java.lang.Object)
		 */
		@Override
		public boolean supports(ResolvableType type, Object value) {

			if (!ResourceProcessorHandlerMethodReturnValueHandler.RESOURCE_TYPE.isAssignableFrom(type)) {
				return false;
			}

			return super.supports(type, value) && isValueTypeMatch((Resource<?>) value, getTargetType());
		}

		/**
		 * Returns whether the given {@link Resource} matches the given target {@link ResolvableType}. We inspect the
		 * {@link Resource}'s value to determine the match.
		 *
		 * @param resource
		 * @param target must not be {@literal null}.
		 * @return whether the given {@link Resource} can be assigned to the given target {@link ResolvableType}
		 */
		private static boolean isValueTypeMatch(Resource<?> resource, ResolvableType target) {

			if (resource == null || !isRawTypeAssignable(target, resource.getClass())) {
				return false;
			}

			Object content = resource.getContent();

			if (content == null) {
				return false;
			}

			ResolvableType type = findGenericType(target, Resource.class);
			return type != null && type.getGeneric(0).isAssignableFrom(ResolvableType.forClass(content.getClass()));
		}

		private static ResolvableType findGenericType(ResolvableType source, Class<?> type) {

			Class<?> rawType = getRawType(source);

			if (Object.class.equals(rawType)) {
				return null;
			}

			if (rawType.equals(type)) {
				return source;
			}

			return findGenericType(source.getSuperType(), type);
		}
	}

	/**
	 * {@link ProcessorWrapper} for {@link ResourceProcessor}s targeting {@link Resources}. Will peek into the content of
	 * the {@link Resources} for type matching decisions if needed.
	 *
	 * @author Oliver Gierke
	 */
	static class ResourcesProcessorWrapper extends ResourceProcessorInvoker.DefaultProcessorWrapper {

		/**
		 * Creates a new {@link ResourcesProcessorWrapper} for the given {@link ResourceProcessor}.
		 *
		 * @param processor must not be {@literal null}.
		 */
		public ResourcesProcessorWrapper(ResourceProcessor<?> processor) {
			super(processor);
		}

		/*
		 * (non-Javadoc)
		 * @see org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler.DefaultProcessorWrapper#supports(org.springframework.core.ResolvableType, java.lang.Object)
		 */
		@Override
		public boolean supports(ResolvableType type, Object value) {

			if (!ResourceProcessorHandlerMethodReturnValueHandler.RESOURCES_TYPE.isAssignableFrom(type)) {
				return false;
			}

			return super.supports(type, value) && isValueTypeMatch((Resources<?>) value, getTargetType());
		}

		/**
		 * Returns whether the given {@link Resources} instance matches the given {@link ResolvableType}. We predict this by
		 * inspecting the first element of the content of the {@link Resources}.
		 *
		 * @param resources the {@link Resources} to inspect.
		 * @param target that target {@link ResolvableType}.
		 * @return
		 */
		static boolean isValueTypeMatch(Resources<?> resources, ResolvableType target) {

			if (resources == null) {
				return false;
			}

			Collection<?> content = resources.getContent();

			if (content.isEmpty()) {
				return false;
			}

			ResolvableType superType = null;

			for (Class<?> resourcesType : Arrays.<Class<?>> asList(resources.getClass(), Resources.class)) {

				superType = ResolvableType.forClass(resourcesType, getRawType(target));

				if (superType != null) {
					break;
				}
			}

			if (superType == null) {
				return false;
			}

			Object element = content.iterator().next();
			ResolvableType resourceType = superType.getGeneric(0);

			if (element instanceof Resource) {
				return ResourceProcessorWrapper.isValueTypeMatch((Resource<?>) element, resourceType);
			} else if (element instanceof EmbeddedWrapper) {
				return isRawTypeAssignable(resourceType, ((EmbeddedWrapper) element).getRelTargetType());
			}

			return false;
		}
	}

	/**
	 * Helper extension of {@link AnnotationAwareOrderComparator} to make {@link #getOrder(Object)} public to allow it
	 * being used in a standalone fashion.
	 *
	 * @author Oliver Gierke
	 */
	private static class CustomOrderAwareComparator extends AnnotationAwareOrderComparator {

		public static ResourceProcessorInvoker.CustomOrderAwareComparator INSTANCE = new CustomOrderAwareComparator();

		@Override
		protected int getOrder(Object obj) {
			return super.getOrder(obj);
		}
	}
}
