/*******************************************************************************
 * Copyright (c) 2005, 2016 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     het@google.com - Bug 441790
 *******************************************************************************/
package org.eclipse.jdt.internal.compiler.apt.model;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.MirroredTypesException;
import javax.lang.model.type.TypeMirror;

import org.eclipse.jdt.internal.compiler.apt.dispatch.BaseProcessingEnvImpl;
import org.eclipse.jdt.internal.compiler.impl.Constant;
import org.eclipse.jdt.internal.compiler.lookup.AnnotationBinding;
import org.eclipse.jdt.internal.compiler.lookup.ArrayBinding;
import org.eclipse.jdt.internal.compiler.lookup.ElementValuePair;
import org.eclipse.jdt.internal.compiler.lookup.FieldBinding;
import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeIds;

public class AnnotationMirrorImpl implements AnnotationMirror, InvocationHandler {

	public final BaseProcessingEnvImpl _env;
	public final AnnotationBinding _binding;

	/* package */ AnnotationMirrorImpl(BaseProcessingEnvImpl env, AnnotationBinding binding) {
		_env = env;
		_binding = binding;
	}

	@Override
	public boolean equals(Object obj) {
		if (obj instanceof AnnotationMirrorImpl) {
			if (this._binding == null) {
				return ((AnnotationMirrorImpl) obj)._binding == null;
			}
			return equals(this._binding, ((AnnotationMirrorImpl) obj)._binding);
		}
		return obj == null ? false : obj.equals(this); // obj could be wrapped by a proxy.
	}

	private static boolean equals(AnnotationBinding annotationBinding, AnnotationBinding annotationBinding2) {
		if (annotationBinding.getAnnotationType() != annotationBinding2.getAnnotationType()) return false; //$IDENTITY-COMPARISON$
		final ElementValuePair[] elementValuePairs = annotationBinding.getElementValuePairs();
		final ElementValuePair[] elementValuePairs2 = annotationBinding2.getElementValuePairs();
		final int length = elementValuePairs.length;
		if (length != elementValuePairs2.length) return false;
		loop: for (int i = 0; i < length; i++) {
			ElementValuePair pair = elementValuePairs[i];
			// loop on the given pair to make sure one will match
			for (int j = 0; j < length; j++) {
				ElementValuePair pair2 = elementValuePairs2[j];
				if (pair.binding == pair2.binding) {
					if (pair.value == null) {
						if (pair2.value == null) {
							continue loop;
						}
						return false;
					} else {
						if (pair2.value == null) return false;
						if (pair2.value instanceof Object[] && pair.value instanceof Object[]) {
							if (!Arrays.equals((Object[]) pair.value, (Object[]) pair2.value)) {
								return false;
							}
						} else if (!pair2.value.equals(pair.value)){
							return false;
						}
					}
					continue loop;
				}
			}
			return false;
		}
		return true;
	}

	@Override
	public DeclaredType getAnnotationType() {
		return (DeclaredType) _env.getFactory().newTypeMirror(_binding.getAnnotationType());
	}

	/**
	 * @return all the members of this annotation mirror that have explicit values.
	 * Default values are not included.
	 */
	@Override
	public Map<? extends ExecutableElement, ? extends AnnotationValue> getElementValues() {
		if (this._binding == null) {
			return Collections.emptyMap();
		}
		ElementValuePair[] pairs = _binding.getElementValuePairs();
		Map<ExecutableElement, AnnotationValue> valueMap =
			new LinkedHashMap<>(pairs.length);
		for (ElementValuePair pair : pairs) {
			MethodBinding method = pair.getMethodBinding();
			if (method == null) {
				// ideally we should be able to create a fake ExecutableElementImpl
				continue;
			}
			ExecutableElement e = new ExecutableElementImpl(_env, method);
			AnnotationValue v = new AnnotationMemberValue(_env, pair.getValue(), method);
			valueMap.put(e, v);
		}
		return Collections.unmodifiableMap(valueMap);
	}

	/**
	 * @see javax.lang.model.util.Elements#getElementValuesWithDefaults(AnnotationMirror)
	 * @return all the members of this annotation mirror that have explicit or default
	 * values.
	 */
	public Map<? extends ExecutableElement, ? extends AnnotationValue> getElementValuesWithDefaults() {
		if (this._binding == null) {
			return Collections.emptyMap();
		}
		ElementValuePair[] pairs = _binding.getElementValuePairs();
		ReferenceBinding annoType = _binding.getAnnotationType();
		Map<ExecutableElement, AnnotationValue> valueMap =
			new LinkedHashMap<>();
		for (MethodBinding method : annoType.methods()) {
			// if binding is in ElementValuePair list, then get value from there
			boolean foundExplicitValue = false;
			for (int i = 0; i < pairs.length; ++i) {
				MethodBinding explicitBinding = pairs[i].getMethodBinding();
				if (method == explicitBinding) {
					ExecutableElement e = new ExecutableElementImpl(_env, explicitBinding);
					AnnotationValue v = new AnnotationMemberValue(_env, pairs[i].getValue(), explicitBinding);
					valueMap.put(e, v);
					foundExplicitValue = true;
					break;
				}
			}
			// else get default value if one exists
			if (!foundExplicitValue) {
				Object defaultVal = method.getDefaultValue();
				if (null != defaultVal) {
					ExecutableElement e = new ExecutableElementImpl(_env, method);
					AnnotationValue v = new AnnotationMemberValue(_env, defaultVal, method);
					valueMap.put(e, v);
				}
			}
		}
		return Collections.unmodifiableMap(valueMap);
	}

	@Override
	public int hashCode() {
		if (this._binding == null) return this._env.hashCode();
		return this._binding.hashCode();
	}

	/*
	 * Used by getAnnotation(), which returns a reflective proxy of the annotation class.  When processors then
	 * invoke methods such as value() on the annotation proxy, this method is called.
	 * <p>
	 * A challenge here is that the processor was not necessarily compiled against the same annotation
	 * definition that the compiler is looking at right now, not to mention that the annotation itself
	 * may be defective in source.  So the actual type of the value may be quite different than the
	 * type expected by the caller, which will result in a ClassCastException, which is ugly for the
	 * processor to try to catch.  So we try to catch and correct this type mismatch where possible.
	 * <p>
	 * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[])
	 */
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
	{
		if (this._binding == null) return null;
        final String methodName = method.getName();
        if ( args == null || args.length == 0 ) {
            if( methodName.equals("hashCode") ) { //$NON-NLS-1$
                return Integer.valueOf(hashCode());
            }
            else if( methodName.equals("toString") ) { //$NON-NLS-1$
                return toString();
            }
            else if( methodName.equals("annotationType")) { //$NON-NLS-1$
            	return proxy.getClass().getInterfaces()[0];
            }
        }
        else if ( args.length == 1 && methodName.equals("equals") ) { //$NON-NLS-1$
            return Boolean.valueOf(equals(args[0]));
        }

        // If it's not one of the above methods, it must be an annotation member, so it cannot take any arguments
        if ( args != null && args.length != 0 ) {
            throw new NoSuchMethodException("method " + method.getName() + formatArgs(args) + " does not exist on annotation " + toString()); //$NON-NLS-1$ //$NON-NLS-2$
        }
        final MethodBinding methodBinding = getMethodBinding(methodName);
        if ( methodBinding == null ) {
            throw new NoSuchMethodException("method " + method.getName() + "() does not exist on annotation" + toString()); //$NON-NLS-1$ //$NON-NLS-2$
        }

        Object actualValue = null;
        boolean foundMethod = false;
        ElementValuePair[] pairs = _binding.getElementValuePairs();
		for (ElementValuePair pair : pairs) {
			if (methodName.equals(new String(pair.getName()))) {
				actualValue = pair.getValue();
				foundMethod = true;
				break;
			}
		}
		if (!foundMethod) {
			// couldn't find explicit value; see if there's a default
			actualValue = methodBinding.getDefaultValue();
		}
		Class<?> expectedType = method.getReturnType();
		TypeBinding actualType = methodBinding.returnType;
        return getReflectionValue(actualValue, actualType, expectedType);
	}

	@Override
    public String toString() {
    	TypeMirror decl = getAnnotationType();
    	StringBuilder sb = new StringBuilder();
    	sb.append('@');
    	sb.append(decl.toString());
    	Map<? extends ExecutableElement, ? extends AnnotationValue> values = getElementValues();
		if (!values.isEmpty()) {
			sb.append('(');
			boolean first = true;
			for (Entry<? extends ExecutableElement, ? extends AnnotationValue> e : values.entrySet()) {
				if (!first) {
					sb.append(", "); //$NON-NLS-1$
				}
				first = false;
				sb.append(e.getKey().getSimpleName());
				sb.append(" = "); //$NON-NLS-1$
				sb.append(e.getValue().toString());
			}
			sb.append(')');
		}
		return sb.toString();
    }

	/**
	 * Used for constructing exception message text.
	 * @return a string like "(a, b, c)".
	 */
    private String formatArgs(final Object[] args)
    {
        // estimate that each class name (plus the separators) is 10 characters long plus 2 for "()".
        final StringBuilder builder = new StringBuilder(args.length * 8 + 2 );
        builder.append('(');
        for( int i=0; i<args.length; i++ )
        {
            if( i > 0 )
            	builder.append(", "); //$NON-NLS-1$
            builder.append(args[i].getClass().getName());
        }
        builder.append(')');
        return builder.toString();
    }

	/**
	 * Find a particular annotation member by name.
	 * @return a compiler method binding, or null if no member was found.
	 */
	private MethodBinding getMethodBinding(String name) {
		ReferenceBinding annoType = _binding.getAnnotationType();
		MethodBinding[] methods = annoType.getMethods(name.toCharArray());
		for (MethodBinding method : methods) {
			// annotation members have no parameters
			if (method.parameters.length == 0) {
				return method;
			}
		}
		return null;
	}

	/**
	 * Convert an annotation member value from JDT into Reflection, and from whatever its actual type
	 * is into whatever type the reflective invoker of a method is expecting.
	 * <p>
	 * Only certain types are permitted as member values.  Specifically, a member must be a constant,
	 * and must be either a primitive type, String, Class, an enum constant, an annotation, or an
	 * array of any of those.  Multidimensional arrays are not permitted.
	 *
	 * @param actualValue the value as represented by {@link ElementValuePair#getValue()}
	 * @param actualType the return type of the corresponding {@link MethodBinding}
	 * @param expectedType the type that the reflective method invoker is expecting
	 * @return an object of the expected type representing the annotation member value,
	 * or an appropriate dummy value (such as null) if no value is available
	 */
	private Object getReflectionValue(Object actualValue, TypeBinding actualType, Class<?> expectedType)
	{
		if (null == expectedType) {
			// With no expected type, we can't even guess at a conversion
			return null;
		}
		if (null == actualValue) {
			// Return a type-appropriate equivalent of null
			return Factory.getMatchingDummyValue(expectedType);
		}
		if (expectedType.isArray()) {
			if (Class.class.equals(expectedType.getComponentType())) {
				// package Class[]-valued return as a MirroredTypesException
				if (actualType.isArrayType() && ((ArrayBinding)actualType).leafComponentType.erasure().id == TypeIds.T_JavaLangClass) {

					Object[] bindings;
					if(actualValue instanceof Object[]) {
						bindings = (Object[]) actualValue;
					} else if(actualValue instanceof TypeBinding) {
						// when a single class element is passed to array: @AnnotationType(Something.class)
						bindings = new Object[] {actualValue};
					} else {
						bindings = null;
					}

					if(bindings != null) {
						List<TypeMirror> mirrors = new ArrayList<>(bindings.length);
						for (int i = 0; i < bindings.length; ++i) {
							if (bindings[i] instanceof TypeBinding) {
								mirrors.add(_env.getFactory().newTypeMirror((TypeBinding)bindings[i]));
							}
						}
						throw new MirroredTypesException(mirrors);
					}
				}
				// TODO: actual value is not a TypeBinding[].  Should we return a TypeMirror[] around an ErrorType?
				return null;
			}
			// Handle arrays of types other than Class, e.g., int[], MyEnum[], ...
			return convertJDTArrayToReflectionArray(actualValue, actualType, expectedType);
		}
		else if (Class.class.equals(expectedType)) {
			// package the Class-valued return as a MirroredTypeException
			if (actualValue instanceof TypeBinding) {
				TypeMirror mirror = _env.getFactory().newTypeMirror((TypeBinding)actualValue);
				throw new MirroredTypeException(mirror);
			}
			else {
				// TODO: actual value is not a TypeBinding.  Should we return a TypeMirror around an ErrorType?
				return null;
			}
		}
		else {
			// Handle unitary values of type other than Class, e.g., int, MyEnum, ...
			return convertJDTValueToReflectionType(actualValue, actualType, expectedType);
		}
	}

	/**
	 * Convert an array of JDT types as obtained from ElementValuePair.getValue()
	 * (e.g., an Object[] containing IntConstant elements) to the type expected by
	 * a reflective method invocation (e.g., int[]).
	 * <p>
	 * This does not handle arrays of Class, but it does handle primitives, enum constants,
	 * and types such as String.
	 * @param jdtValue the actual value returned by ElementValuePair.getValue() or MethodBinding.getDefault()
	 * @param jdtType the return type of the annotation method binding
	 * @param expectedType the type that the invoker of the method is expecting; must be an array type
	 * @return an Object which is, e.g., an int[]; or null, if an array cannot be created.
	 */
	private Object convertJDTArrayToReflectionArray(Object jdtValue, TypeBinding jdtType, Class<?> expectedType)
	{
		assert null != expectedType && expectedType.isArray();
		if (!jdtType.isArrayType()) {
			// the compiler says that the type binding isn't an array type; this probably means
			// that there's some sort of syntax error.
			return null;
		}
		Object[] jdtArray;
		// See bug 261969: it's legal to pass a solo element for an array-typed value
		if (jdtValue != null && !(jdtValue instanceof Object[])) {
			// Create an array of the expected type
			jdtArray = (Object[]) Array.newInstance(jdtValue.getClass(), 1);
			jdtArray[0] = jdtValue;
		} else {
			jdtArray = (Object[])jdtValue;
		}
		TypeBinding jdtLeafType = jdtType.leafComponentType();
		Class<?> expectedLeafType = expectedType.getComponentType();
        final int length = jdtArray.length;
        final Object returnArray = Array.newInstance(expectedLeafType, length);
        for (int i = 0; i < length; ++i) {
        	Object jdtElementValue = jdtArray[i];
    		if (expectedLeafType.isPrimitive() || String.class.equals(expectedLeafType)) {
    			if (jdtElementValue instanceof Constant) {
    				if (boolean.class.equals(expectedLeafType)) {
    					Array.setBoolean(returnArray, i, ((Constant)jdtElementValue).booleanValue());
    				}
    				else if (byte.class.equals(expectedLeafType)) {
    					Array.setByte(returnArray, i, ((Constant)jdtElementValue).byteValue());
    				}
    				else if (char.class.equals(expectedLeafType)) {
    					Array.setChar(returnArray, i, ((Constant)jdtElementValue).charValue());
    				}
    				else if (double.class.equals(expectedLeafType)) {
    					Array.setDouble(returnArray, i, ((Constant)jdtElementValue).doubleValue());
    				}
    				else if (float.class.equals(expectedLeafType)) {
    					Array.setFloat(returnArray, i, ((Constant)jdtElementValue).floatValue());
    				}
    				else if (int.class.equals(expectedLeafType)) {
    					Array.setInt(returnArray, i, ((Constant)jdtElementValue).intValue());
    				}
    				else if (long.class.equals(expectedLeafType)) {
    					Array.setLong(returnArray, i, ((Constant)jdtElementValue).longValue());
    				}
    				else if (short.class.equals(expectedLeafType)) {
    					Array.setShort(returnArray, i, ((Constant)jdtElementValue).shortValue());
    				}
    				else if (String.class.equals(expectedLeafType)) {
    					Array.set(returnArray, i, ((Constant)jdtElementValue).stringValue());
    				}
    			}
    			else {
	    			// Primitive or string is expected, but our actual value cannot be coerced into one.
	    			// TODO: if the actual value is an array of primitives, should we unpack the first one?
	    			Factory.setArrayMatchingDummyValue(returnArray, i, expectedLeafType);
    			}
    		}
    		else if (expectedLeafType.isEnum()) {
    			Object returnVal = null;
    	        if (jdtLeafType != null && jdtLeafType.isEnum() && jdtElementValue instanceof FieldBinding) {
    	        	FieldBinding binding = (FieldBinding)jdtElementValue;
    	        	try {
    	        		Field returnedField = null;
    	        		returnedField = expectedLeafType.getField( new String(binding.name) );
    	        		if (null != returnedField) {
    	        			returnVal = returnedField.get(null);
    	        		}
    	        	}
    	        	catch (NoSuchFieldException nsfe) {
    	        		// return null
    	        	}
    	        	catch (IllegalAccessException iae) {
    	        		// return null
    	        	}
    	        }
    	        Array.set(returnArray, i, returnVal);
    		}
    		else if (expectedLeafType.isAnnotation()) {
    			// member value is expected to be an annotation type.  Wrap it in an Annotation proxy.
    			Object returnVal = null;
    			if (jdtLeafType.isAnnotationType() && jdtElementValue instanceof AnnotationBinding) {
    				AnnotationMirrorImpl annoMirror =
    					(AnnotationMirrorImpl)_env.getFactory().newAnnotationMirror((AnnotationBinding)jdtElementValue);
    				returnVal = Proxy.newProxyInstance(expectedLeafType.getClassLoader(),
    						new Class[]{ expectedLeafType }, annoMirror );
    			}
    	        Array.set(returnArray, i, returnVal);
    		}
    		else {
    			Array.set(returnArray, i, null);
    		}
        }
		return returnArray;
	}

	/**
	 * Convert a JDT annotation value as obtained from ElementValuePair.getValue()
	 * (e.g., IntConstant, FieldBinding, etc.) to the type expected by a reflective
	 * method invocation (e.g., int, an enum constant, etc.).
	 * @return a value of type {@code expectedType}, or a dummy value of that type if
	 * the actual value cannot be converted.
	 */
	private Object convertJDTValueToReflectionType(Object jdtValue, TypeBinding actualType, Class<?> expectedType) {
		if (expectedType.isPrimitive() || String.class.equals(expectedType)) {
			if (jdtValue instanceof Constant) {
				if (boolean.class.equals(expectedType)) {
					return ((Constant)jdtValue).booleanValue();
				}
				else if (byte.class.equals(expectedType)) {
					return ((Constant)jdtValue).byteValue();
				}
				else if (char.class.equals(expectedType)) {
					return ((Constant)jdtValue).charValue();
				}
				else if (double.class.equals(expectedType)) {
					return ((Constant)jdtValue).doubleValue();
				}
				else if (float.class.equals(expectedType)) {
					return ((Constant)jdtValue).floatValue();
				}
				else if (int.class.equals(expectedType)) {
					return ((Constant)jdtValue).intValue();
				}
				else if (long.class.equals(expectedType)) {
					return ((Constant)jdtValue).longValue();
				}
				else if (short.class.equals(expectedType)) {
					return ((Constant)jdtValue).shortValue();
				}
				else if (String.class.equals(expectedType)) {
					return ((Constant)jdtValue).stringValue();
				}
			}
			// Primitive or string is expected, but our actual value cannot be coerced into one.
			// TODO: if the actual value is an array of primitives, should we unpack the first one?
			return Factory.getMatchingDummyValue(expectedType);
		}
		else if (expectedType.isEnum()) {
			Object returnVal = null;
	        if (actualType != null && actualType.isEnum() && jdtValue instanceof FieldBinding) {

	        	FieldBinding binding = (FieldBinding)jdtValue;
	        	try {
	        		Field returnedField = null;
	        		returnedField = expectedType.getField( new String(binding.name) );
	        		if (null != returnedField) {
	        			returnVal = returnedField.get(null);
	        		}
	        	}
	        	catch (NoSuchFieldException nsfe) {
	        		// return null
	        	}
	        	catch (IllegalAccessException iae) {
	        		// return null
	        	}
	        }
	        return null == returnVal ? Factory.getMatchingDummyValue(expectedType) : returnVal;
		}
		else if (expectedType.isAnnotation()) {
			// member value is expected to be an annotation type.  Wrap it in an Annotation proxy.
			if (actualType.isAnnotationType() && jdtValue instanceof AnnotationBinding) {
				AnnotationMirrorImpl annoMirror =
					(AnnotationMirrorImpl)_env.getFactory().newAnnotationMirror((AnnotationBinding)jdtValue);
				return Proxy.newProxyInstance(expectedType.getClassLoader(),
						new Class[]{ expectedType }, annoMirror );
			}
			else {
				// No way to cast a non-annotation value to an annotation type; return null to caller
				return null;
			}
		}
		else {
			return Factory.getMatchingDummyValue(expectedType);
		}
	}

}
