/*
 * Copyright 2016-2025 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.json;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.springframework.core.MethodParameter;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.webmvc.mapping.Associations;
import org.springframework.data.rest.webmvc.support.DomainClassResolver;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.NativeWebRequest;

import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * Translator for {@link Sort} arguments that is aware of Jackson-Mapping on domain classes. Jackson field names are
 * translated to {@link PersistentProperty} names. Domain class is looked up by resolving request URLs to mapped
 * repositories. {@link Sort} translation is skipped if a domain class cannot be resolved.
 *
 * @author Mark Paluch
 * @author Oliver Gierke
 * @since 2.6
 */
public class JacksonMappingAwareSortTranslator {

	private final Repositories repositories;
	private final DomainClassResolver domainClassResolver;
	private final SortTranslator sortTranslator;

	/**
	 * Creates a new {@link JacksonMappingAwareSortTranslator} for the given {@link ObjectMapper}, {@link Repositories},
	 * {@link DomainClassResolver} and {@link PersistentEntities}.
	 *
	 * @param objectMapper must not be {@literal null}.
	 * @param repositories must not be {@literal null}.
	 * @param domainClassResolver must not be {@literal null}.
	 * @param persistentEntities must not be {@literal null}.
	 * @param associations must not be {@literal null}.
	 */
	public JacksonMappingAwareSortTranslator(ObjectMapper objectMapper, Repositories repositories,
			DomainClassResolver domainClassResolver, PersistentEntities persistentEntities, Associations associations) {

		Assert.notNull(repositories, "Repositories must not be null");
		Assert.notNull(domainClassResolver, "DomainClassResolver must not be null");
		Assert.notNull(associations, "Associations must not be null");

		this.repositories = repositories;
		this.domainClassResolver = domainClassResolver;
		this.sortTranslator = new SortTranslator(persistentEntities, objectMapper, associations);
	}

	/**
	 * Translates Jackson field names within a {@link Sort} to {@link PersistentProperty} property names.
	 *
	 * @param input must not be {@literal null}.
	 * @param parameter must not be {@literal null}.
	 * @param webRequest must not be {@literal null}.
	 * @return a {@link Sort} containing translated property names or {@literal null} the resulting {@link Sort} contains
	 *         no properties.
	 */
	protected Sort translateSort(Sort input, MethodParameter parameter, NativeWebRequest webRequest) {

		Assert.notNull(input, "Sort must not be null");
		Assert.notNull(parameter, "MethodParameter must not be null");
		Assert.notNull(webRequest, "NativeWebRequest must not be null");

		Class<?> domainClass = domainClassResolver.resolve(parameter.getMethod(), webRequest);

		if (domainClass == null) {
			return input;
		}

		PersistentEntity<?, ?> persistentEntity = repositories.getPersistentEntity(domainClass);

		return sortTranslator.translateSort(input, persistentEntity);
	}

	/**
	 * Translates {@link Sort} orders from Jackson-mapped field names to {@link PersistentProperty} names.
	 *
	 * @author Mark Paluch
	 * @author Oliver Gierke
	 * @since 2.6
	 */
	public static class SortTranslator {

		private static final String DELIMITERS = "_\\.";
		private static final String ALL_UPPERCASE = "[A-Z0-9._$]+";

		private static final Pattern SPLITTER = Pattern.compile("(?:[%s]?([%s]*?[^%s]+))".replaceAll("%s", DELIMITERS));

		private final PersistentEntities entities;
		private final ObjectMapper objectMapper;
		private final Associations associations;

		public SortTranslator(PersistentEntities entities, ObjectMapper objectMapper, Associations associations) {

			Assert.notNull(entities, "PersistentEntities must not be null");
			Assert.notNull(objectMapper, "ObjectMapper must not be null");
			Assert.notNull(associations, "Associations must not be null");

			this.entities = entities;
			this.objectMapper = objectMapper;
			this.associations = associations;
		}

		/**
		 * Translates {@link Sort} orders from Jackson-mapped field names to {@link PersistentProperty} names. Properties
		 * that cannot be resolved are dropped.
		 *
		 * @param input must not be {@literal null}.
		 * @param rootEntity must not be {@literal null}.
		 * @return {@link Sort} with translated field names or {@literal null} if translation dropped all sort fields.
		 */
		public Sort translateSort(Sort input, PersistentEntity<?, ?> rootEntity) {

			Assert.notNull(input, "Sort must not be null");
			Assert.notNull(rootEntity, "PersistentEntity must not be null");

			List<Order> filteredOrders = new ArrayList<Order>();

			for (Order order : input) {

				List<String> iteratorSource = new ArrayList<String>();
				Matcher matcher = SPLITTER.matcher("_" + order.getProperty());

				while (matcher.find()) {
					iteratorSource.add(matcher.group(1));
				}

				String mappedPropertyPath = getMappedPropertyPath(rootEntity, iteratorSource);

				if (mappedPropertyPath != null) {
					filteredOrders.add(order.withProperty(mappedPropertyPath));
				}
			}

			return filteredOrders.isEmpty() ? Sort.unsorted() : Sort.by(filteredOrders);
		}

		private String getMappedPropertyPath(PersistentEntity<?, ?> rootEntity, List<String> iteratorSource) {

			List<String> persistentPropertyPath = mapPropertyPath(rootEntity, iteratorSource);

			if (persistentPropertyPath.isEmpty()) {
				return null;
			}

			return StringUtils.collectionToDelimitedString(persistentPropertyPath, ".");
		}

		private List<String> mapPropertyPath(PersistentEntity<?, ?> rootEntity, List<String> iteratorSource) {

			List<String> persistentPropertyPath = new ArrayList<String>(iteratorSource.size());

			TypedSegment typedSegment = TypedSegment.create(entities, objectMapper, rootEntity);

			for (String field : iteratorSource) {

				String fieldName = field.matches(ALL_UPPERCASE) ? field : StringUtils.uncapitalize(field);

				if (!typedSegment.hasPersistentPropertyForField(fieldName)) {
					return Collections.emptyList();
				}

				List<? extends PersistentProperty<?>> persistentProperties = typedSegment.getPersistentProperties(fieldName);

				for (PersistentProperty<?> persistentProperty : persistentProperties) {

					if (associations.isLinkableAssociation(persistentProperty)) {
						return Collections.emptyList();
					}

					persistentPropertyPath.add(persistentProperty.getName());
				}

				typedSegment = typedSegment.next(persistentProperties.get(persistentProperties.size() - 1));
			}

			return persistentPropertyPath;
		}
	}

	/**
	 * A typed segment inside a Jackson property path. {@link TypedSegment} represents a segment in JSON field path to
	 * {@link PersistentProperty} mapping.
	 *
	 * @author Mark Paluch
	 */
	static class TypedSegment {

		private final PersistentEntities persistentEntities;
		private final ObjectMapper objectMapper;
		private final Optional<PersistentEntity<?, ? extends PersistentProperty<?>>> currentType;
		private final MappedProperties currentProperties;
		private final WrappedProperties currentWrappedProperties;

		private TypedSegment(TypedSegment previous,
				Optional<PersistentEntity<?, ? extends PersistentProperty<?>>> persistentEntity) {
			this(previous.persistentEntities, previous.objectMapper, persistentEntity);
		}

		private TypedSegment(PersistentEntities persistentEntities, ObjectMapper objectMapper,
				Optional<PersistentEntity<?, ? extends PersistentProperty<?>>> persistentEntity) {

			this.persistentEntities = persistentEntities;
			this.objectMapper = objectMapper;
			this.currentType = persistentEntity;
			this.currentProperties = persistentEntity//
					.map(it -> MappedProperties.forSerialization(it, objectMapper))//
					.orElseGet(() -> MappedProperties.none());
			this.currentWrappedProperties = persistentEntity//
					.map(it -> WrappedProperties.fromJacksonProperties(persistentEntities, it, objectMapper))//
					.orElseGet(() -> WrappedProperties.none());

		}

		/**
		 * Creates the initial {@link TypedSegment} given {@link PersistentEntities}, {@link ObjectMapper} and
		 * {@link PersistentEntity}.
		 *
		 * @param persistentEntities must not be {@literal null}.
		 * @param objectMapper must not be {@literal null}.
		 * @param rootEntity the initial entity to start mapping from, must not be {@literal null}.
		 * @return
		 */
		public static TypedSegment create(PersistentEntities persistentEntities, ObjectMapper objectMapper,
				PersistentEntity<?, ?> rootEntity) {

			Assert.notNull(persistentEntities, "PersistentEntities must not be null");
			Assert.notNull(objectMapper, "ObjectMapper must not be null");
			Assert.notNull(rootEntity, "PersistentEntity must not be null");

			return new TypedSegment(persistentEntities, objectMapper, Optional.of(rootEntity));
		}

		/**
		 * Continue mapping by providing the next {@link PersistentProperty}.
		 *
		 * @param persistentProperty must not be {@literal null}.
		 * @return
		 */
		public TypedSegment next(PersistentProperty<?> persistentProperty) {

			Assert.notNull(persistentProperty, "PersistentProperty must not be null");

			return new TypedSegment(this, persistentEntities.getPersistentEntity(persistentProperty.getType()));
		}

		private boolean hasPersistentPropertyForField(String fieldName) {

			return currentType != null && (currentProperties.hasPersistentPropertyForField(fieldName)
					|| currentWrappedProperties.hasPersistentPropertiesForField(fieldName));
		}

		private List<? extends PersistentProperty<?>> getPersistentProperties(String fieldName) {

			if (currentWrappedProperties.hasPersistentPropertiesForField(fieldName)) {
				return currentWrappedProperties.getPersistentProperties(fieldName);
			}

			return Collections.singletonList(currentProperties.getPersistentProperty(fieldName));
		}
	}
}
