/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.annotationProcessors.utils;

import org.mozilla.gecko.annotationProcessors.AnnotationInfo;
import org.mozilla.gecko.annotationProcessors.classloader.AnnotatableEntity;
import org.mozilla.gecko.annotationProcessors.classloader.ClassWithOptions;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;

/**
 * Iterator over the methods in a given method list which have the WrappedJNIMethod
 * annotation. Returns an object containing both the annotation (Which may contain interesting
 * parameters) and the argument.
 */
public class GeneratableElementIterator implements Iterator<AnnotatableEntity> {
    private final ClassWithOptions mClass;
    private final Member[] mObjects;
    private AnnotatableEntity mNextReturnValue;
    private int mElementIndex;
    private AnnotationInfo mClassInfo;

    private boolean mIterateEveryEntry;

    public GeneratableElementIterator(ClassWithOptions annotatedClass) {
        mClass = annotatedClass;

        final Class<?> aClass = annotatedClass.wrappedClass;
        // Get all the elements of this class as AccessibleObjects.
        Member[] aMethods = aClass.getDeclaredMethods();
        Member[] aFields = aClass.getDeclaredFields();
        Member[] aCtors = aClass.getDeclaredConstructors();

        // Shove them all into one buffer.
        Member[] objs = new Member[aMethods.length + aFields.length + aCtors.length];

        int offset = 0;
        System.arraycopy(aMethods, 0, objs, 0, aMethods.length);
        offset += aMethods.length;
        System.arraycopy(aFields, 0, objs, offset, aFields.length);
        offset += aFields.length;
        System.arraycopy(aCtors, 0, objs, offset, aCtors.length);

        // Sort the elements to ensure determinism.
        Arrays.sort(objs, new AlphabeticAnnotatableEntityComparator<Member>());
        mObjects = objs;

        // Check for "Wrap ALL the things" flag.
        for (Annotation annotation : aClass.getDeclaredAnnotations()) {
            mClassInfo = buildAnnotationInfo(aClass, annotation);
            if (mClassInfo != null) {
                mIterateEveryEntry = true;
                break;
            }
        }

        findNextValue();
    }

    private Class<?>[] getFilteredInnerClasses() {
        // Go through all inner classes and see which ones we want to generate.
        final Class<?>[] candidates = mClass.wrappedClass.getDeclaredClasses();
        int count = 0;

        for (int i = 0; i < candidates.length; ++i) {
            final GeneratableElementIterator testIterator
                    = new GeneratableElementIterator(new ClassWithOptions(candidates[i], null));
            if (testIterator.hasNext()
                    || testIterator.getFilteredInnerClasses() != null) {
                count++;
                continue;
            }
            // Clear out ones that don't match.
            candidates[i] = null;
        }
        return count > 0 ? candidates : null;
    }

    public ClassWithOptions[] getInnerClasses() {
        final Class<?>[] candidates = getFilteredInnerClasses();
        if (candidates == null) {
            return new ClassWithOptions[0];
        }

        int count = 0;
        for (Class<?> candidate : candidates) {
            if (candidate != null) {
                count++;
            }
        }

        final ClassWithOptions[] ret = new ClassWithOptions[count];
        count = 0;
        for (Class<?> candidate : candidates) {
            if (candidate != null) {
                ret[count++] = new ClassWithOptions(
                        candidate, mClass.generatedName + "::" + candidate.getSimpleName());
            }
        }
        assert ret.length == count;

        Arrays.sort(ret, new Comparator<ClassWithOptions>() {
            @Override public int compare(ClassWithOptions lhs, ClassWithOptions rhs) {
                return lhs.generatedName.compareTo(rhs.generatedName);
            }
        });
        return ret;
    }

    private AnnotationInfo buildAnnotationInfo(AnnotatedElement element, Annotation annotation) {
        Class<? extends Annotation> annotationType = annotation.annotationType();
        final String annotationTypeName = annotationType.getName();
        if (!annotationTypeName.equals("org.mozilla.gecko.annotation.WrapForJNI")) {
            return null;
        }

        String stubName = null;
        boolean isMultithreadedStub = false;
        boolean noThrow = false;
        boolean narrowChars = false;
        boolean catchException = false;
        try {
            // Determine the explicitly-given name of the stub to generate, if any.
            final Method stubNameMethod = annotationType.getDeclaredMethod("stubName");
            stubNameMethod.setAccessible(true);
            stubName = (String) stubNameMethod.invoke(annotation);

            if (element instanceof Class<?>) {
                // Make @WrapForJNI always allow multithread by default, individual methods can then
                // override with their own annotation
                isMultithreadedStub = true;
            } else {
                // Determine if the generated stub is to allow calls from multiple threads.
                final Method multithreadedStubMethod = annotationType.getDeclaredMethod("allowMultithread");
                multithreadedStubMethod.setAccessible(true);
                isMultithreadedStub = (Boolean) multithreadedStubMethod.invoke(annotation);
            }

            // Determine if ignoring exceptions
            final Method noThrowMethod = annotationType.getDeclaredMethod("noThrow");
            noThrowMethod.setAccessible(true);
            noThrow = (Boolean) noThrowMethod.invoke(annotation);

            // Determine if strings should be wide or narrow
            final Method narrowCharsMethod = annotationType.getDeclaredMethod("narrowChars");
            narrowCharsMethod.setAccessible(true);
            narrowChars = (Boolean) narrowCharsMethod.invoke(annotation);

            // Determine if we should catch exceptions
            final Method catchExceptionMethod = annotationType.getDeclaredMethod("catchException");
            catchExceptionMethod.setAccessible(true);
            catchException = (Boolean) catchExceptionMethod.invoke(annotation);

        } catch (NoSuchMethodException e) {
            System.err.println("Unable to find expected field on WrapForJNI annotation. Did the signature change?");
            e.printStackTrace(System.err);
            System.exit(3);
        } catch (IllegalAccessException e) {
            System.err.println("IllegalAccessException reading fields on WrapForJNI annotation. Seems the semantics of Reflection have changed...");
            e.printStackTrace(System.err);
            System.exit(4);
        } catch (InvocationTargetException e) {
            System.err.println("InvocationTargetException reading fields on WrapForJNI annotation. This really shouldn't happen.");
            e.printStackTrace(System.err);
            System.exit(5);
        }

        // If the method name was not explicitly given in the annotation generate one...
        if (stubName.isEmpty()) {
            stubName = Utils.getNativeName(element);
        }

        return new AnnotationInfo(
            stubName, isMultithreadedStub, noThrow, narrowChars, catchException);
    }

    /**
     * Find and cache the next appropriately annotated method, plus the annotation parameter, if
     * one exists. Otherwise cache null, so hasNext returns false.
     */
    private void findNextValue() {
        while (mElementIndex < mObjects.length) {
            Member candidateElement = mObjects[mElementIndex];
            mElementIndex++;
            for (Annotation annotation : ((AnnotatedElement) candidateElement).getDeclaredAnnotations()) {
                AnnotationInfo info = buildAnnotationInfo((AnnotatedElement)candidateElement, annotation);
                if (info != null) {
                    mNextReturnValue = new AnnotatableEntity(candidateElement, info);
                    return;
                }
            }

            // If no annotation found, we might be expected to generate anyway
            // using default arguments, thanks to the "Generate everything" annotation.
            if (mIterateEveryEntry) {
                AnnotationInfo annotationInfo = new AnnotationInfo(
                    Utils.getNativeName(candidateElement),
                    mClassInfo.isMultithreaded,
                    mClassInfo.noThrow,
                    mClassInfo.narrowChars,
                    mClassInfo.catchException);
                mNextReturnValue = new AnnotatableEntity(candidateElement, annotationInfo);
                return;
            }
        }
        mNextReturnValue = null;
    }

    @Override
    public boolean hasNext() {
        return mNextReturnValue != null;
    }

    @Override
    public AnnotatableEntity next() {
        AnnotatableEntity ret = mNextReturnValue;
        findNextValue();
        return ret;
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException("Removal of methods from GeneratableElementIterator not supported.");
    }
}
