/*
 * Copyright 2002-2018 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
 *
 *      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.springframework.expression.spel.ast;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.asm.Label;
import org.springframework.asm.MethodVisitor;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.CodeFlow;
import org.springframework.expression.spel.CompilablePropertyAccessor;
import org.springframework.expression.spel.ExpressionState;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelMessage;
import org.springframework.expression.spel.support.ReflectivePropertyAccessor;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;

/**
 * Represents a simple property or field reference.
 *
 * @author Andy Clement
 * @author Juergen Hoeller
 * @author Clark Duplichien
 * @since 3.0
 */
public class PropertyOrFieldReference extends SpelNodeImpl {

	private final boolean nullSafe;

	private final String name;

	@Nullable
	private String originalPrimitiveExitTypeDescriptor;

	@Nullable
	private volatile PropertyAccessor cachedReadAccessor;

	@Nullable
	private volatile PropertyAccessor cachedWriteAccessor;


	public PropertyOrFieldReference(boolean nullSafe, String propertyOrFieldName, int pos) {
		super(pos);
		this.nullSafe = nullSafe;
		this.name = propertyOrFieldName;
	}


	public boolean isNullSafe() {
		return this.nullSafe;
	}

	public String getName() {
		return this.name;
	}


	@Override
	public ValueRef getValueRef(ExpressionState state) throws EvaluationException {
		return new AccessorLValue(this, state.getActiveContextObject(), state.getEvaluationContext(),
				state.getConfiguration().isAutoGrowNullReferences());
	}

	@Override
	public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
		TypedValue tv = getValueInternal(state.getActiveContextObject(), state.getEvaluationContext(),
				state.getConfiguration().isAutoGrowNullReferences());
		PropertyAccessor accessorToUse = this.cachedReadAccessor;
		if (accessorToUse instanceof CompilablePropertyAccessor) {
			CompilablePropertyAccessor accessor = (CompilablePropertyAccessor) accessorToUse;
			setExitTypeDescriptor(CodeFlow.toDescriptor(accessor.getPropertyType()));
		}
		return tv;
	}

	private TypedValue getValueInternal(TypedValue contextObject, EvaluationContext evalContext,
			boolean isAutoGrowNullReferences) throws EvaluationException {

		TypedValue result = readProperty(contextObject, evalContext, this.name);

		// Dynamically create the objects if the user has requested that optional behavior
		if (result.getValue() == null && isAutoGrowNullReferences &&
				nextChildIs(Indexer.class, PropertyOrFieldReference.class)) {
			TypeDescriptor resultDescriptor = result.getTypeDescriptor();
			Assert.state(resultDescriptor != null, "No result type");
			// Create a new collection or map ready for the indexer
			if (List.class == resultDescriptor.getType()) {
				if (isWritableProperty(this.name, contextObject, evalContext)) {
					List<?> newList = new ArrayList<>();
					writeProperty(contextObject, evalContext, this.name, newList);
					result = readProperty(contextObject, evalContext, this.name);
				}
			}
			else if (Map.class == resultDescriptor.getType()) {
				if (isWritableProperty(this.name,contextObject, evalContext)) {
					Map<?,?> newMap = new HashMap<>();
					writeProperty(contextObject, evalContext, this.name, newMap);
					result = readProperty(contextObject, evalContext, this.name);
				}
			}
			else {
				// 'simple' object
				try {
					if (isWritableProperty(this.name,contextObject, evalContext)) {
						Class<?> clazz = result.getTypeDescriptor().getType();
						Object newObject = ReflectionUtils.accessibleConstructor(clazz).newInstance();
						writeProperty(contextObject, evalContext, this.name, newObject);
						result = readProperty(contextObject, evalContext, this.name);
					}
				}
				catch (InvocationTargetException ex) {
					throw new SpelEvaluationException(getStartPosition(), ex.getTargetException(),
							SpelMessage.UNABLE_TO_DYNAMICALLY_CREATE_OBJECT, result.getTypeDescriptor().getType());
				}
				catch (Throwable ex) {
					throw new SpelEvaluationException(getStartPosition(), ex,
							SpelMessage.UNABLE_TO_DYNAMICALLY_CREATE_OBJECT, result.getTypeDescriptor().getType());
				}
			}
		}
		return result;
	}

	@Override
	public void setValue(ExpressionState state, @Nullable Object newValue) throws EvaluationException {
		writeProperty(state.getActiveContextObject(), state.getEvaluationContext(), this.name, newValue);
	}

	@Override
	public boolean isWritable(ExpressionState state) throws EvaluationException {
		return isWritableProperty(this.name, state.getActiveContextObject(), state.getEvaluationContext());
	}

	@Override
	public String toStringAST() {
		return this.name;
	}

	/**
	 * Attempt to read the named property from the current context object.
	 * @return the value of the property
	 * @throws EvaluationException if any problem accessing the property or it cannot be found
	 */
	private TypedValue readProperty(TypedValue contextObject, EvaluationContext evalContext, String name)
			throws EvaluationException {

		Object targetObject = contextObject.getValue();
		if (targetObject == null && this.nullSafe) {
			return TypedValue.NULL;
		}

		PropertyAccessor accessorToUse = this.cachedReadAccessor;
		if (accessorToUse != null) {
			if (evalContext.getPropertyAccessors().contains(accessorToUse)) {
				try {
					return accessorToUse.read(evalContext, contextObject.getValue(), name);
				}
				catch (Exception ex) {
					// This is OK - it may have gone stale due to a class change,
					// let's try to get a new one and call it before giving up...
				}
			}
			this.cachedReadAccessor = null;
		}

		List<PropertyAccessor> accessorsToTry =
				getPropertyAccessorsToTry(contextObject.getValue(), evalContext.getPropertyAccessors());
		// Go through the accessors that may be able to resolve it. If they are a cacheable accessor then
		// get the accessor and use it. If they are not cacheable but report they can read the property
		// then ask them to read it
		try {
			for (PropertyAccessor accessor : accessorsToTry) {
				if (accessor.canRead(evalContext, contextObject.getValue(), name)) {
					if (accessor instanceof ReflectivePropertyAccessor) {
						accessor = ((ReflectivePropertyAccessor) accessor).createOptimalAccessor(
								evalContext, contextObject.getValue(), name);
					}
					this.cachedReadAccessor = accessor;
					return accessor.read(evalContext, contextObject.getValue(), name);
				}
			}
		}
		catch (Exception ex) {
			throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_DURING_PROPERTY_READ, name, ex.getMessage());
		}

		if (contextObject.getValue() == null) {
			throw new SpelEvaluationException(SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE_ON_NULL, name);
		}
		else {
			throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE, name,
					FormatHelper.formatClassNameForMessage(getObjectClass(contextObject.getValue())));
		}
	}

	private void writeProperty(
			TypedValue contextObject, EvaluationContext evalContext, String name, @Nullable Object newValue)
			throws EvaluationException {

		if (contextObject.getValue() == null && this.nullSafe) {
			return;
		}
		if (contextObject.getValue() == null) {
			throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE_ON_NULL, name);
		}

		PropertyAccessor accessorToUse = this.cachedWriteAccessor;
		if (accessorToUse != null) {
			if (evalContext.getPropertyAccessors().contains(accessorToUse)) {
				try {
					accessorToUse.write(evalContext, contextObject.getValue(), name, newValue);
					return;
				}
				catch (Exception ex) {
					// This is OK - it may have gone stale due to a class change,
					// let's try to get a new one and call it before giving up...
				}
			}
			this.cachedWriteAccessor = null;
		}

		List<PropertyAccessor> accessorsToTry =
				getPropertyAccessorsToTry(contextObject.getValue(), evalContext.getPropertyAccessors());
		try {
			for (PropertyAccessor accessor : accessorsToTry) {
				if (accessor.canWrite(evalContext, contextObject.getValue(), name)) {
					this.cachedWriteAccessor = accessor;
					accessor.write(evalContext, contextObject.getValue(), name, newValue);
					return;
				}
			}
		}
		catch (AccessException ex) {
			throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE,
					name, ex.getMessage());
		}

		throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE, name,
				FormatHelper.formatClassNameForMessage(getObjectClass(contextObject.getValue())));
	}

	public boolean isWritableProperty(String name, TypedValue contextObject, EvaluationContext evalContext)
			throws EvaluationException {

		Object value = contextObject.getValue();
		if (value != null) {
			List<PropertyAccessor> accessorsToTry =
					getPropertyAccessorsToTry(contextObject.getValue(), evalContext.getPropertyAccessors());
			for (PropertyAccessor accessor : accessorsToTry) {
				try {
					if (accessor.canWrite(evalContext, value, name)) {
						return true;
					}
				}
				catch (AccessException ex) {
					// let others try
				}
			}
		}
		return false;
	}

	/**
	 * Determines the set of property resolvers that should be used to try and access a property
	 * on the specified target type. The resolvers are considered to be in an ordered list,
	 * however in the returned list any that are exact matches for the input target type (as
	 * opposed to 'general' resolvers that could work for any type) are placed at the start of the
	 * list. In addition, there are specific resolvers that exactly name the class in question
	 * and resolvers that name a specific class but it is a supertype of the class we have.
	 * These are put at the end of the specific resolvers set and will be tried after exactly
	 * matching accessors but before generic accessors.
	 * @param contextObject the object upon which property access is being attempted
	 * @return a list of resolvers that should be tried in order to access the property
	 */
	private List<PropertyAccessor> getPropertyAccessorsToTry(
			@Nullable Object contextObject, List<PropertyAccessor> propertyAccessors) {

		Class<?> targetType = (contextObject != null ? contextObject.getClass() : null);

		List<PropertyAccessor> specificAccessors = new ArrayList<>();
		List<PropertyAccessor> generalAccessors = new ArrayList<>();
		for (PropertyAccessor resolver : propertyAccessors) {
			Class<?>[] targets = resolver.getSpecificTargetClasses();
			if (targets == null) {
				// generic resolver that says it can be used for any type
				generalAccessors.add(resolver);
			}
			else if (targetType != null) {
				for (Class<?> clazz : targets) {
					if (clazz == targetType) {
						specificAccessors.add(resolver);
						break;
					}
					else if (clazz.isAssignableFrom(targetType)) {
						generalAccessors.add(resolver);
					}
				}
			}
		}
		List<PropertyAccessor> resolvers = new ArrayList<>(specificAccessors);
		generalAccessors.removeAll(specificAccessors);
		resolvers.addAll(generalAccessors);
		return resolvers;
	}

	@Override
	public boolean isCompilable() {
		PropertyAccessor accessorToUse = this.cachedReadAccessor;
		return (accessorToUse instanceof CompilablePropertyAccessor &&
				((CompilablePropertyAccessor) accessorToUse).isCompilable());
	}

	@Override
	public void generateCode(MethodVisitor mv, CodeFlow cf) {
		PropertyAccessor accessorToUse = this.cachedReadAccessor;
		if (!(accessorToUse instanceof CompilablePropertyAccessor)) {
			throw new IllegalStateException("Property accessor is not compilable: " + accessorToUse);
		}

		Label skipIfNull = null;
		if (this.nullSafe) {
			mv.visitInsn(DUP);
			skipIfNull = new Label();
			Label continueLabel = new Label();
			mv.visitJumpInsn(IFNONNULL, continueLabel);
			CodeFlow.insertCheckCast(mv, this.exitTypeDescriptor);
			mv.visitJumpInsn(GOTO, skipIfNull);
			mv.visitLabel(continueLabel);
		}

		((CompilablePropertyAccessor) accessorToUse).generateCode(this.name, mv, cf);
		cf.pushDescriptor(this.exitTypeDescriptor);

		if (this.originalPrimitiveExitTypeDescriptor != null) {
			// The output of the accessor is a primitive but from the block above it might be null,
			// so to have a common stack element type at skipIfNull target it is necessary
			// to box the primitive
			CodeFlow.insertBoxIfNecessary(mv, this.originalPrimitiveExitTypeDescriptor);
		}
		if (skipIfNull != null) {
			mv.visitLabel(skipIfNull);
		}
	}

	void setExitTypeDescriptor(String descriptor) {
		// If this property or field access would return a primitive - and yet
		// it is also marked null safe - then the exit type descriptor must be
		// promoted to the box type to allow a null value to be passed on
		if (this.nullSafe && CodeFlow.isPrimitive(descriptor)) {
			this.originalPrimitiveExitTypeDescriptor = descriptor;
			this.exitTypeDescriptor = CodeFlow.toBoxedDescriptor(descriptor);
		}
		else {
			this.exitTypeDescriptor = descriptor;
		}
	}


	private static class AccessorLValue implements ValueRef {

		private final PropertyOrFieldReference ref;

		private final TypedValue contextObject;

		private final EvaluationContext evalContext;

		private final boolean autoGrowNullReferences;

		public AccessorLValue(PropertyOrFieldReference propertyOrFieldReference, TypedValue activeContextObject,
				EvaluationContext evalContext, boolean autoGrowNullReferences) {

			this.ref = propertyOrFieldReference;
			this.contextObject = activeContextObject;
			this.evalContext = evalContext;
			this.autoGrowNullReferences = autoGrowNullReferences;
		}

		@Override
		public TypedValue getValue() {
			TypedValue value =
					this.ref.getValueInternal(this.contextObject, this.evalContext, this.autoGrowNullReferences);
			PropertyAccessor accessorToUse = this.ref.cachedReadAccessor;
			if (accessorToUse instanceof CompilablePropertyAccessor) {
				this.ref.setExitTypeDescriptor(CodeFlow.toDescriptor(((CompilablePropertyAccessor) accessorToUse).getPropertyType()));
			}
			return value;
		}

		@Override
		public void setValue(@Nullable Object newValue) {
			this.ref.writeProperty(this.contextObject, this.evalContext, this.ref.name, newValue);
		}

		@Override
		public boolean isWritable() {
			return this.ref.isWritableProperty(this.ref.name, this.contextObject, this.evalContext);
		}
	}

}