/*
 * Copyright 2017-2019 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.cloud.gcp.data.datastore.core.mapping;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.expression.BeanFactoryAccessor;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;

/**
 * Metadata class for entities stored in Datastore.
 * @param <T> the type of the persistent entity
 *
 * @author Chengyuan Zhao
 * @since 1.1
 */
public class DatastorePersistentEntityImpl<T>
		extends BasicPersistentEntity<T, DatastorePersistentProperty>
		implements DatastorePersistentEntity<T> {

	private static final ExpressionParser PARSER = new SpelExpressionParser();

	private final Expression kindNameExpression;

	private final String classBasedKindName;

	private final Entity kind;

	private final DiscriminatorField discriminatorField;

	private final DiscriminatorValue discriminatorValue;

	private final DatastoreMappingContext datastoreMappingContext;

	private StandardEvaluationContext context;

	/**
	 * Constructor.
	 * @param information type information about the underlying entity type.
	 * @param datastoreMappingContext a mapping context used to get metadata for related
	 *     persistent entities.
	 */
	public DatastorePersistentEntityImpl(TypeInformation<T> information,
			DatastoreMappingContext datastoreMappingContext) {
		super(information);

		Class<?> rawType = information.getType();

		this.datastoreMappingContext = datastoreMappingContext;
		this.context = new StandardEvaluationContext();
		this.kind = findAnnotation(Entity.class);
		this.discriminatorField = findAnnotation(DiscriminatorField.class);
		this.discriminatorValue = findAnnotation(DiscriminatorValue.class);
		this.classBasedKindName = this.hasTableName() ? this.kind.name()
				: StringUtils.uncapitalize(rawType.getSimpleName());
		this.kindNameExpression = detectExpression();
	}

	protected boolean hasTableName() {
		return this.kind != null && StringUtils.hasText(this.kind.name());
	}

	@Nullable
	private Expression detectExpression() {
		if (!hasTableName()) {
			return null;
		}

		Expression expression = PARSER.parseExpression(this.kind.name(),
				ParserContext.TEMPLATE_EXPRESSION);

		return (expression instanceof LiteralExpression) ? null : expression;
	}

	@Override
	public String kindName() {
		if (this.discriminatorValue != null && this.discriminatorField == null) {
			throw new DatastoreDataException(
					"This class expects a discrimination field but none are designated: " + getType());
		}
		else if (this.discriminatorValue != null && getType().getSuperclass() != Object.class) {
			return ((DatastorePersistentEntityImpl) (this.datastoreMappingContext
					.getPersistentEntity(getType().getSuperclass()))).getDiscriminationSuperclassPersistentEntity()
							.kindName();
		}
		return (this.kindNameExpression == null) ? this.classBasedKindName
				: this.kindNameExpression.getValue(this.context, String.class);
	}

	@Override
	public DatastorePersistentProperty getIdPropertyOrFail() {
		if (!hasIdProperty()) {
			throw new DatastoreDataException(
					"An ID property was required but does not exist for the type: "
							+ getType());
		}
		return getIdProperty();
	}

	@Override
	public void verify() {
		super.verify();
		initializeSubclassEntities();
		addEntityToDiscriminationFamily();
		checkDiscriminationValues();
	}

	private void checkDiscriminationValues() {
		Set<Class> otherMembers = DatastoreMappingContext.getDiscriminationFamily(getType());
		if (otherMembers != null) {
			for (Class other : otherMembers) {
				DatastorePersistentEntity persistentEntity = this.datastoreMappingContext.getPersistentEntity(other);
				if (getDiscriminatorValue() != null
						&& getDiscriminatorValue().equals(persistentEntity.getDiscriminatorValue())) {
					throw new DatastoreDataException(
							"More than one class in an inheritance hierarchy has the same DiscriminatorValue: "
									+ getType() + " and " + other);
				}
			}
		}

	}

	private void addEntityToDiscriminationFamily() {
		Class parentClass = getType().getSuperclass();
		DatastorePersistentEntity parentEntity = parentClass != Object.class
				? this.datastoreMappingContext.getPersistentEntity(parentClass)
				: null;
		if (parentEntity != null && parentEntity.getDiscriminationFieldName() != null) {
			if (!parentEntity.getDiscriminationFieldName().equals(getDiscriminationFieldName())) {
				throw new DatastoreDataException(
						"This class and its super class both have discrimination fields but they are different fields: "
								+ getType() + " and " + parentClass);
			}
			DatastoreMappingContext.addDiscriminationClassConnection(parentClass, getType());
		}
	}

	@Override
	public String getDiscriminationFieldName() {
		return this.discriminatorField == null ? null : this.discriminatorField.field();
	}

	@Override
	public List<String> getCompatibleDiscriminationValues() {
		if (this.discriminatorValue == null) {
			return Collections.emptyList();
		}
		else {
			List<String> compatibleValues = new LinkedList<>();
			compatibleValues.add(this.discriminatorValue.value());
			DatastorePersistentEntity<?> persistentEntity = this.datastoreMappingContext
					.getPersistentEntity(getType().getSuperclass());
			if (persistentEntity != null) {
				List<String> compatibleDiscriminationValues = persistentEntity.getCompatibleDiscriminationValues();
				compatibleValues.addAll(compatibleDiscriminationValues);
			}
			return compatibleValues;
		}
	}

	@Override
	public String getDiscriminatorValue() {
		return this.discriminatorValue == null ? null : this.discriminatorValue.value();
	}

	@Override
	public void doWithColumnBackedProperties(
			PropertyHandler<DatastorePersistentProperty> handler) {
		doWithProperties(
				(PropertyHandler<DatastorePersistentProperty>) (datastorePersistentProperty) -> {
					if (datastorePersistentProperty.isColumnBacked()) {
						handler.doWithPersistentProperty(datastorePersistentProperty);
					}
				});
	}

	@Override
	public void doWithDescendantProperties(
			PropertyHandler<DatastorePersistentProperty> handler) {
		doWithProperties(
				(PropertyHandler<DatastorePersistentProperty>) (datastorePersistentProperty) -> {
					if (datastorePersistentProperty.isDescendants()) {
						handler.doWithPersistentProperty(datastorePersistentProperty);
					}
				});
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext)
			throws BeansException {
		this.context.addPropertyAccessor(new BeanFactoryAccessor());
		this.context.setBeanResolver(new BeanFactoryResolver(applicationContext));
		this.context.setRootObject(applicationContext);
	}

	/* This method is used by subclass persistent entities to get the superclass Kind name. */
	private DatastorePersistentEntity getDiscriminationSuperclassPersistentEntity() {
		if (this.discriminatorField != null) {
			return this;
		}
		return ((DatastorePersistentEntityImpl) (this.datastoreMappingContext
				.getPersistentEntity(getType().getSuperclass()))).getDiscriminationSuperclassPersistentEntity();
	}

	private void initializeSubclassEntities() {
		ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
		provider.addIncludeFilter(new AssignableTypeFilter(getType()));
		for (BeanDefinition component : provider.findCandidateComponents(getType().getPackage().getName())) {
			try {
				this.datastoreMappingContext.getPersistentEntity(Class.forName(component.getBeanClassName()));
			}
			catch (ClassNotFoundException ex) {
				throw new DatastoreDataException("Could not find expected subclass for this entity: " + getType(), ex);
			}
		}
	}
}