/**
 * The contents of this file is dual-licensed under 2 
 * alternative Open Source/Free licenses: LGPL 2.1 or later and 
 * Apache License 2.0. (starting with JNA version 4.0.0).
 * 
 * You can freely decide which license you want to apply to 
 * the project.
 * 
 * You may obtain a copy of the LGPL License at:
 * 
 * http://www.gnu.org/licenses/licenses.html
 * 
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "LGPL2.1".
 * 
 * You may obtain a copy of the Apache License at:
 * 
 * http://www.apache.org/licenses/
 * 
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "AL2.0".
 */
package com.sun.jna;

import org.reflections.Reflections;
import org.reflections.scanners.ResourcesScanner;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;

import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * Utility class for detecting missing {@link com.sun.jna.Structure#getFieldOrder()} methods.
 *
 * This class could be moved to the unit test tree, but then reusing it in the 'platform' project would require
 * publishing this test tree.
 *
 * @author Dan Rollo
 * Date: 1/17/13
 * Time: 4:08 PM
 */
public final class StructureFieldOrderInspector {

    private StructureFieldOrderInspector(){}

    /**
     * Search for Structure sub types in the source tree of the given class, and validate the getFieldOrder() method,
     * and collects all errors into one exception.
     *
     * @param classDeclaredInSourceTreeToSearch a class who's source tree will be searched for Structure sub types.
     * @param ignoreConstructorError list of classname prefixes for which to ignore construction errors.
     */
    public static void batchCheckStructureGetFieldOrder(final Class<?> classDeclaredInSourceTreeToSearch,
                                                   final List<String> ignoreConstructorError) {
        final Set<Class<? extends Structure>> classes = StructureFieldOrderInspector.findSubTypesOfStructure(classDeclaredInSourceTreeToSearch);

        final List<Throwable> problems = new ArrayList<Throwable>();

        for (final Class<? extends Structure> structureSubType : classes) {
            try {
                StructureFieldOrderInspector.checkMethodGetFieldOrder(structureSubType, ignoreConstructorError);
            } catch (Throwable t) {
                problems.add(t);
            }
        }

        if (problems.size() > 0) {
            String msg = "";
            for (final Throwable t : problems) {
                msg += t.getMessage() + "; \n";
            }

            throw new RuntimeException("Some Structure sub types (" + problems.size() + ") have problems with getFieldOrder(): \n" + msg);
        }
    }

    /**
     * Search for Structure sub types in the source tree of the given class, and validate the getFieldOrder() method.
     *
     * @param classDeclaredInSourceTreeToSearch a class who's source tree will be searched for Structure sub types.
     * @param ignoreConstructorError list of classname prefixes for which to ignore construction errors.
     */
    public static void checkStructureGetFieldOrder(final Class<?> classDeclaredInSourceTreeToSearch,
                                                   final List<String> ignoreConstructorError) {
        final Set<Class<? extends Structure>> classes = StructureFieldOrderInspector.findSubTypesOfStructure(classDeclaredInSourceTreeToSearch);

        for (final Class<? extends Structure> structureSubType : classes) {
            StructureFieldOrderInspector.checkMethodGetFieldOrder(structureSubType, ignoreConstructorError);
        }
    }

    /**
     * Find all classes that extend {@link Structure}.
     */
    public static Set<Class<? extends Structure>> findSubTypesOfStructure(final Class<?> classDeclaredInSourceTreeToSearch) {

        // use: http://code.google.com/p/reflections/

        final Reflections reflections = new Reflections(new ConfigurationBuilder()
                .setScanners(new SubTypesScanner(false /* don't exclude Object.class */), new ResourcesScanner())
                .setUrls(ClasspathHelper.forClass(classDeclaredInSourceTreeToSearch))
        );

        return reflections.getSubTypesOf(Structure.class);
    }


    public static void checkMethodGetFieldOrder(final Class<? extends Structure> structureSubType,
                                                final List<String> ignoreConstructorError) {

        if (Structure.ByValue.class.isAssignableFrom(structureSubType)
                || Structure.ByReference.class.isAssignableFrom(structureSubType)) {

            // ignore tagging interfaces
            return;
        }

        final Method methodGetFieldOrder = getMethodGetFieldOrder(structureSubType);


        if (Modifier.isAbstract(structureSubType.getModifiers())) {
            // do not try to construct abstract Structure sub types
            return;
        }
        final Constructor<? extends Structure> structConstructor;
        try {
            structConstructor = structureSubType.getDeclaredConstructor();
        } catch (NoSuchMethodException e) {
            if (structureSubType == Structure.FFIType.class) {
                // ignore this case
                // @todo Allow user to pass in list of classes for which to skip construction?
                return;
            }
            throw new RuntimeException("Parameterless constructor failed on Structure sub type: " + structureSubType.getName());
        }

        if (!structConstructor.isAccessible()) {
            structConstructor.setAccessible(true);
        }
        final Structure structure;
        try {
            structure= structConstructor.newInstance();
        } catch (InstantiationException e) {
            throw new RuntimeException("Could not instantiate Structure sub type: " + structureSubType.getName(), e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Could not instantiate Structure sub type: " + structureSubType.getName(), e);
        } catch (InvocationTargetException e) {
            // this is triggered by checks in Structure.getFields(), and static loadlibrary() failures
            if (ignoreConstructorError != null) {
                final String structSubtypeName = structureSubType.getName();
                for (final String classPrefix : ignoreConstructorError) {
                    if (structSubtypeName.startsWith(classPrefix)) {
                        return;
                    }
                }
            }
            throw new RuntimeException("Could not instantiate Structure sub type: " + structureSubType.getName(), e);
        }

        if (!methodGetFieldOrder.isAccessible()) {
            methodGetFieldOrder.setAccessible(true);
        }
        final List<?> methodCallFieldList;
        try {
            methodCallFieldList = (List<?>) methodGetFieldOrder.invoke(structure);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Could not invoke getFieldOrder() on Structure sub type: " + structureSubType.getName(), e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException("Could not invoke getFieldOrder() on Structure sub type: " + structureSubType.getName(), e);
        }

        final Field[] actualFields = structureSubType.getFields(); // include fields from super classes
        final List<String> actualFieldNames = new ArrayList<String>(actualFields.length);
        for (final Field field : actualFields) {
            // ignore static fields
            if (!Modifier.isStatic(field.getModifiers())) {
                final String actualFieldName = field.getName();
                if (!methodCallFieldList.contains(actualFieldName)) {
                    throw new IllegalArgumentException(structureSubType.getName() + ".getFieldOrder() [" + methodCallFieldList
                            + "] does not include declared field: " + actualFieldName);
                }
                actualFieldNames.add(actualFieldName);
            }
        }

        for (final Object methodCallField : methodCallFieldList) {
            if (!actualFieldNames.contains(methodCallField)) {
                throw new IllegalArgumentException(structureSubType.getName() + ".getFieldOrder() [" + methodCallFieldList
                        + "] includes undeclared field: " + methodCallField);
            }
        }
    }

    /**
     * Find the getFieldOrder() method in the given class, or any of it's parents.
     * @param structureSubType a structure sub type
     * @return the getFieldOrder() method found in the given class, or any of it's parents.
     */
    private static Method getMethodGetFieldOrder(Class<? extends Structure> structureSubType) {
        final Method methodGetFieldOrder;
        try {
            methodGetFieldOrder = structureSubType.getDeclaredMethod("getFieldOrder", new Class[]{});
        } catch (NoSuchMethodException e) {
            if (structureSubType.getSuperclass() != null) {
                // look for method in parent
                return getMethodGetFieldOrder((Class<? extends Structure>) structureSubType.getSuperclass());
            }
            throw new IllegalArgumentException("The Structure sub type: " + structureSubType.getName()
                    + " must define the method: getFieldOrder()."
                    + " See the javadoc for Structure.getFieldOrder() for details.", e);
        }
        return methodGetFieldOrder;
    }
}
