/*
 * Copyright (c) 2007 Mockito contributors
 * This program is made available under the terms of the MIT License.
 */
package org.mockito.internal.util.reflection;

import org.mockito.internal.util.Checks;
import org.mockito.internal.util.collections.ListUtil;
import org.mockito.internal.util.collections.ListUtil.Filter;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

/**
 * Small fluent reflection tools to work with fields.
 *
 * Code is very new and might need rework.
 */
public abstract class Fields {

    /**
     * Instance fields declared in the class and superclasses of the given instance.
     *
     * @param instance Instance from which declared fields will be retrieved.
     * @return InstanceFields of this object instance.
     */
    public static InstanceFields allDeclaredFieldsOf(Object instance) {
        List<InstanceField> instanceFields = new ArrayList<InstanceField>();
        for (Class<?> clazz = instance.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) {
            instanceFields.addAll(instanceFieldsIn(instance, clazz.getDeclaredFields()));
        }
        return new InstanceFields(instance, instanceFields);
    }

    /**
     * Instance fields declared in the class of the given instance.
     *
     * @param instance Instance from which declared fields will be retrieved.
     * @return InstanceFields of this object instance.
     */
    public static InstanceFields declaredFieldsOf(Object instance) {
        List<InstanceField> instanceFields = new ArrayList<InstanceField>();
        instanceFields.addAll(instanceFieldsIn(instance, instance.getClass().getDeclaredFields()));
        return new InstanceFields(instance, instanceFields);
    }

    private static List<InstanceField> instanceFieldsIn(Object instance, Field[] fields) {
        List<InstanceField> instanceDeclaredFields = new ArrayList<InstanceField>();
        for (Field field : fields) {
            InstanceField instanceField = new InstanceField(field, instance);
            instanceDeclaredFields.add(instanceField);
        }
        return instanceDeclaredFields;
    }

    /**
     * Accept fields annotated by the given annotations.
     *
     * @param annotations Annotation types to check.
     * @return The filter.
     */
    public static Filter<InstanceField> annotatedBy(final Class<? extends Annotation>... annotations) {
        return new Filter<InstanceField>() {
            public boolean isOut(InstanceField instanceField) {
                Checks.checkNotNull(annotations, "Provide at least one annotation class");

                for (Class<? extends Annotation> annotation : annotations) {
                    if(instanceField.isAnnotatedBy(annotation)) {
                        return false;
                    }
                }
                return true;
            }
        };
    }

    /**
     * Accept fields with non null value.
     *
     * @return The filter.
     */
    private static Filter<InstanceField> nullField() {
        return new Filter<InstanceField>() {
            public boolean isOut(InstanceField instanceField) {
                return instanceField.isNull();
            }
        };
    }

    public static class InstanceFields {
        private final Object instance;

        private final List<InstanceField> instanceFields;

        public InstanceFields(Object instance, List<InstanceField> instanceFields) {
            this.instance = instance;
            this.instanceFields = instanceFields;
        }

        public InstanceFields filter(Filter<InstanceField> withFilter) {
            return new InstanceFields(instance, ListUtil.filter(instanceFields, withFilter));
        }

        public InstanceFields notNull() {
            return filter(nullField());
        }

        public List<InstanceField> instanceFields() {
            return new ArrayList<InstanceField>(instanceFields);
        }

        public List<Object> assignedValues() {
            List<Object> values = new ArrayList<Object>(instanceFields.size());
            for (InstanceField instanceField : instanceFields) {
                values.add(instanceField.read());
            }
            return values;
        }

        public List<String> names() {
            List<String> fieldNames = new ArrayList<String>(instanceFields.size());
            for (InstanceField instanceField : instanceFields) {
                fieldNames.add(instanceField.name());
            }
            return fieldNames;
        }
    }
}
