/*
 * Copyright 2016 DiffPlug
 *
 * 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 com.diffplug.common.base;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;

/**
 * Utilities for obtaining the fields and getter methods of an object using reflection.
 * Useful for first-pass debugging of runtime objects.
 */
public class FieldsAndGetters {
	/**
	 * Returns a {@code Stream} of all public fields and their values for the given object.
	 *
	 * @see #fields(Object, Predicate)
	 */
	public static Stream<Map.Entry<Field, Object>> fields(Object obj) {
		return fields(obj, Predicates.alwaysTrue());
	}

	/**
	 * Returns a {@code Stream} of all public fields which match {@code predicate} and their values for the given object.
	 * <p>
	 * This method uses reflection to find all of the public instance fields of the given object,
	 * and if they pass the given predicate, it includes them in a stream of {@code Map.Entry<Field, Object>}
	 * where the entry's value is the value of the field for this object.
	 */
	public static Stream<Map.Entry<Field, Object>> fields(Object obj, Predicate<Field> predicate) {
		Class<?> clazz = obj == null ? ObjectIsNull.class : obj.getClass();
		return Arrays.asList(clazz.getFields()).stream()
				// gotta be public
				.filter(field -> Modifier.isPublic(field.getModifiers()))
				// gotta be an instance field
				.filter(field -> !Modifier.isStatic(field.getModifiers()))
				// gotta pass the predicate 
				.filter(predicate)
				// get its value
				.map(field -> createEntry(field, tryCall(field.getName(), () -> field.get(obj))));
	}

	/**
	 * Returns a {@code Stream} of all public getter methods and their return values for the given object.
	 *
	 * @see #getters(Object, Predicate)
	 */
	public static Stream<Map.Entry<Method, Object>> getters(Object obj) {
		return getters(obj, Predicates.alwaysTrue());
	}

	/**
	 * Returns a {@code Stream} of all public getter methods which match {@code predicate} and their return values for the given object.
	 * <p>
	 * This method uses reflection to find all of the public instance methods which don't take any arguments
	 * and return a value.  If they pass the given predicate, then they are called, and the return value is
	 * included in a stream of {@code Map.Entry<Method, Object>}.
	 * <p>
	 * Note that there are some methods which have the signature of a getter, but actually mutate the object
	 * being inspected, e.g. {@link java.io.InputStream#read()}.  These will be called unless you manually
	 * exclude them using the predicate.
	 */
	public static Stream<Map.Entry<Method, Object>> getters(Object obj, Predicate<Method> predicate) {
		Class<?> clazz = obj == null ? ObjectIsNull.class : obj.getClass();
		return Arrays.asList(clazz.getMethods()).stream()
				// we only want methods that don't take parameters
				.filter(method -> method.getParameterTypes().length == 0)
				// we only want public methods
				.filter(method -> Modifier.isPublic(method.getModifiers()))
				// we only want instance methods
				.filter(method -> !Modifier.isStatic(method.getModifiers()))
				// we only want methods that don't return void
				.filter(method -> !method.getReturnType().equals(Void.TYPE))
				// we only want methods that pass our predicate
				.filter(predicate)
				// turn it into Map<Method, Result>
				.map(method -> createEntry(method, tryCall(method.getName(), () -> method.invoke(obj))));
	}

	/** Sentinel class for null objects. */
	public static class ObjectIsNull {}

	/** Executes the given function, return any exceptions it might throw as wrapped values. */
	private static Object tryCall(String methodName, Throwing.Supplier<Object> supplier) {
		try {
			return supplier.get();
		} catch (Throwable error) {
			return new CallException(methodName, error);
		}
	}

	/** Exception which wraps up a thrown exception - ensures that users don't think an exception was returned. */
	private static class CallException extends Exception {
		private static final long serialVersionUID = 1206955156719866328L;

		private final String methodName;

		private CallException(String methodName, Throwable cause) {
			super(cause);
			this.methodName = methodName;
		}

		@Override
		public String toString() {
			return "When calling " + methodName + ": " + getCause().getMessage();
		}
	}

	/**
	 * Returns a {@code Stream} of all public fields and getter methods and their values for the given object.
	 *
	 * @see #getters(Object, Predicate)
	 */
	public static Stream<Map.Entry<String, Object>> fieldsAndGetters(Object obj) {
		return fieldsAndGetters(obj, Predicates.alwaysTrue());
	}

	/**
	 * Returns a {@code Stream} of all public fields and getter methods which match {@code predicate} and their values for the given object.
	 * <p>
	 * This method combines the results of {@link #fields(Object, Predicate)} and {@link #getters(Object, Predicate)}. The {@code Predicate<String>}
	 * will be passed the field names and the getter names (which are postfixed by {@code ()} to mark them as methods).
	 * 
	 * @see #fields(Object, Predicate)
	 * @see #getters(Object, Predicate)
	 */
	public static Stream<Map.Entry<String, Object>> fieldsAndGetters(Object obj, Predicate<String> predicate) {
		Stream<Map.Entry<String, Object>> fields = fields(obj, field -> predicate.test(field.getName()))
				.map(entry -> createEntry(entry.getKey().getName(), entry.getValue()));
		Function<Method, String> methodName = method -> method.getName() + "()";
		Stream<Map.Entry<String, Object>> getters = getters(obj, field -> predicate.test(methodName.apply(field)))
				.map(entry -> createEntry(methodName.apply(entry.getKey()), entry.getValue()));
		return Stream.concat(fields, getters);
	}

	/**
	 * Passes each field and getter of {@code obj} to {@code evalPredicate}, grabs its value if it passes, and if the value passes {@code dumpPredicate} then it is dumped to {@code printer}.
	 * @see #fieldsAndGetters(Object, Predicate)
	 */
	public static void dumpIf(String name, Object obj, Predicate<String> evalPredicate, Predicate<Map.Entry<String, Object>> dumpPredicate, StringPrinter printer) {
		printer.println(name + ": " + obj.getClass().getName());
		fieldsAndGetters(obj, evalPredicate).filter(dumpPredicate).forEach(entry -> {
			printer.println("\t" + entry.getKey() + " = " + entry.getValue());
		});
	}

	/**
	 * Dumps all fields and getters of {@code obj} to {@code System.out}.
	 * @see #dumpIf
	 */
	public static void dumpAll(String name, Object obj) {
		dumpAll(name, obj, StringPrinter.systemOut());
	}

	/**
	 * Dumps all non-null fields and getters of {@code obj} to {@code System.out}.
	 * @see #dumpIf
	 */
	public static void dumpNonNull(String name, Object obj) {
		dumpNonNull(name, obj, StringPrinter.systemOut());
	}

	/**
	 * Dumps all fields and getters of {@code obj} to {@code printer}.
	 * @see #dumpIf
	 */
	public static void dumpAll(String name, Object obj, StringPrinter printer) {
		dumpIf(name, obj, Predicates.alwaysTrue(), Predicates.alwaysTrue(), printer);
	}

	/**
	 * Dumps all non-null fields and getters of {@code obj} to {@code printer}.
	 * @see #dumpIf
	 */
	public static void dumpNonNull(String name, Object obj, StringPrinter printer) {
		dumpIf(name, obj, Predicates.alwaysTrue(), entry -> entry.getValue() != null, printer);
	}

	/** Creates an immutable Map.Entry. */
	private static <K, V> Map.Entry<K, V> createEntry(K key, V value) {
		return new Map.Entry<K, V>() {
			@Override
			public K getKey() {
				return key;
			}

			@Override
			public V getValue() {
				return value;
			}

			@Override
			public V setValue(V value) {
				throw new UnsupportedOperationException();
			}
		};
	}
}