/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.isis.core.commons.reflection;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Stream;

/**
 * 
 * Provides shortcuts for common java.lang.reflect idioms.
 * 
 * @since 2.0.0
 *
 */
public class Reflect {

	public static Object[] emptyObjects = {};
	public static Class<?>[] emptyClasses = {};

	// -- CLASS REFLECTION

	/**
	 * Returns declared methods of this class/interface and all super classes/interfaces.
	 * @param type
	 * @return
	 */
	public static List<Method> getAllDeclaredMethods(Class<?> type) {
		final List<Method> methods = new ArrayList<>();

		Stream.of(type.getDeclaredMethods()).forEach(methods::add);
		visitInterfaces(type,c->Stream.of(c.getDeclaredMethods()).forEach(methods::add));
		visitSuperclassesOf(type,c->Stream.of(c.getDeclaredMethods()).forEach(methods::add));
		return methods;
	}

	/**
	 * Returns declared fields of this class/interface and all super classes/interfaces.
	 * @param type
	 * @return
	 */
	public static List<Field> getAllDeclaredFields(Class<?> type) {
		final List<Field> fields = new ArrayList<>();

		Stream.of(type.getDeclaredFields()).forEach(fields::add);
		visitInterfaces(type,c->Stream.of(c.getDeclaredFields()).forEach(fields::add));
		visitSuperclassesOf(type,c->Stream.of(c.getDeclaredFields()).forEach(fields::add));
		return fields;
	}

	public static void visitSuperclassesOf(final Class<?> clazz, final Consumer<Class<?>> visitor){
		final Class<?> superclass = clazz.getSuperclass();
		if(superclass!=null){
			visitor.accept(superclass);
			visitSuperclassesOf(superclass, visitor);
		}
	}

	public static void visitInterfaces(final Class<?> clazz, final Consumer<Class<?>> visitor){
		if(clazz.isInterface())
			visitor.accept(clazz);

		for(Class<?> interf : clazz.getInterfaces())
			visitor.accept(interf);
	}

	public static Method getGetter(Class<?> cls, String propertyName) throws IntrospectionException {
		final BeanInfo beanInfo = Introspector.getBeanInfo(cls);
		for(PropertyDescriptor pd:beanInfo.getPropertyDescriptors()){
			if(!pd.getName().equals(propertyName))
				continue;
			return pd.getReadMethod();
		}
		return null;	
	}

	public static Method getGetter(Object bean, String propertyName) throws IntrospectionException {
		if(bean==null)
			return null;
		return getGetter(bean, propertyName);	
	}

	// -- METHOD HANDLES

	public static MethodHandle handleOf(Method method) throws IllegalAccessException {
		if(!method.isAccessible()) {
			method.setAccessible(true);
			MethodHandle mh = MethodHandles.publicLookup().unreflect(method);
			method.setAccessible(false);
			return mh;	
		}
		return MethodHandles.publicLookup().unreflect(method);
	}

	public static MethodHandle handleOf(Field field) throws IllegalAccessException {
		if(!field.isAccessible()) {
			field.setAccessible(true);
			MethodHandle mh = MethodHandles.lookup().unreflectGetter(field);
			field.setAccessible(false);
			return mh;	
		}
		return MethodHandles.lookup().unreflectGetter(field);
	}

	// -- PRIMITIVE TYPES

	private static final Set<Class<?>> primitives = new HashSet<>(Arrays.asList(
			boolean.class,
			byte.class,
			char.class,
			double.class, 
			float.class,
			int.class,
			long.class,
			short.class
			//void.class //separated out into its own predicate: isVoid(...)
			));

	private static final Set<Class<?>> primitiveWrappers = new HashSet<>(Arrays.asList(
			Boolean.class,
			Byte.class,
			Character.class,
			Double.class,
			Float.class,
			Integer.class,
			Long.class,
			Short.class
			//Void.class //separated out into its own predicate: isVoid(...)
			));

	// -- TYPE PREDICATES

	public static boolean isVoid(Class<?> c) {
		Objects.requireNonNull(c);
		return c == void.class || c == Void.class;
	}

	public static boolean isPrimitive(Class<?> c) {
		Objects.requireNonNull(c);
		return primitives.contains(c);
	}

	public static boolean isPrimitiveWrapper(Class<?> c) {
		Objects.requireNonNull(c);
		return primitiveWrappers.contains(c);
	}


	// -- METHOD PREDICATES

	public static boolean isNoArg(Method m) {
		Objects.requireNonNull(m);
		return m.getParameterTypes().length==0;
	}

	public static boolean isPublic(Method m) {
		Objects.requireNonNull(m);
		return Modifier.isPublic(m.getModifiers());
	}

	public static boolean isVoid(Method m) {
		Objects.requireNonNull(m);
		return isVoid(m.getReturnType());
	}






}
