/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * 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.
 */

import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Constructor;
import java.lang.reflect.Parameter;

public class Main {
    // A simple parameter annotation
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AnnotationA {}

    // A parameter annotation with additional state
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AnnotationB {
        String value() default "default-value";
    }

    // An inner class whose constructors with have an implicit
    // argument for the enclosing instance.
    public class Inner {
        private final int number;
        private final String text;
        boolean flag;

        Inner(@AnnotationA int number, String text) {
            this.number = number;
            this.text = text;
            this.flag = false;
        }

        Inner(@AnnotationA int number, String text, @AnnotationB("x") boolean flag) {
            this.number = number;
            this.text = text;
            this.flag = flag;
        }
    }

    // An inner class whose constructors with have no implicit
    // arguments for the enclosing instance.
    public static class StaticInner {
        private final int number;
        private final String text;
        boolean flag;

        StaticInner(@AnnotationA int number, String text) {
            this.number = number;
            this.text = text;
            this.flag = false;
        }

        StaticInner(@AnnotationB("foo") int number, String text, @AnnotationA boolean flag) {
            this.number = number;
            this.text = text;
            this.flag = flag;
        }
    }

    public enum ImportantNumber {
        ONE(1.0),
        TWO(2.0),
        MANY(3.0, true);

        private double doubleValue;
        private boolean isLarge;

        ImportantNumber(@AnnotationA double doubleValue) {
            this.doubleValue = doubleValue;
            this.isLarge = false;
        }

        ImportantNumber(@AnnotationB("x") double doubleValue, @AnnotationB("y") boolean isLarge) {
            this.doubleValue = doubleValue;
            this.isLarge = isLarge;
        }
    }

    public enum BinaryNumber {
        ZERO,
        ONE;
    }

    private abstract static class AnonymousBase {
        public AnonymousBase(@AnnotationA String s) {}
    }

    private static String annotationToNormalizedString(Annotation annotation) {
        // String.replace() to accomodate different representation across VMs.
        return annotation.toString().replace("\"", "");
    }

    private static void DumpConstructorParameterAnnotations(Class<?> cls) throws Throwable {
        System.out.println(cls.getName());
        for (Constructor c : cls.getDeclaredConstructors()) {
            System.out.println(" " + c);
            Annotation[][] annotations = c.getParameterAnnotations();
            Parameter[] parameters = c.getParameters();
            for (int i = 0; i < annotations.length; ++i) {
                // Exercise java.lang.reflect.Executable.getParameterAnnotationsNative()
                // which retrieves all annotations for the parameters.
                System.out.print("  Parameter [" + i + "]:");
                for (Annotation annotation : parameters[i].getAnnotations()) {
                    System.out.println("    Indexed : " + annotationToNormalizedString(annotation));
                }
                for (Annotation annotation : annotations[i]) {
                    System.out.println("    Array : " + annotationToNormalizedString(annotation));
                }

                // Exercise Parameter.getAnnotationNative() with
                // retrieves a single parameter annotation according to type.
                Object[] opaqueClasses = new Object[] {AnnotationA.class, AnnotationB.class};
                for (Object opaqueClass : opaqueClasses) {
                    @SuppressWarnings("unchecked")
                    Class<? extends Annotation> annotationClass =
                            (Class<? extends Annotation>) opaqueClass;
                    Annotation annotation = parameters[i].getDeclaredAnnotation(annotationClass);
                    String hasAnnotation = (annotation != null ? "Yes" : "No");
                    System.out.println("    " + annotationClass.getName() + " " + hasAnnotation);

                    Annotation[] parameterAnnotations = parameters[i].getDeclaredAnnotationsByType(annotationClass);
                    for (Annotation parameterAnnotation : parameterAnnotations) {
                        System.out.println("    " + annotationToNormalizedString(parameterAnnotation));
                    }
                }
            }
        }
    }

    private Class<?> getLocalClassWithEnclosingInstanceCapture() {
        class LocalClass {
            private final int integerValue;

            LocalClass(@AnnotationA int integerValue) {
                this.integerValue = integerValue;
            }
        }
        return LocalClass.class;
    }

    private Class<?> getLocalClassWithEnclosingInstanceAndLocalCapture() {
        final long CAPTURED_VALUE = System.currentTimeMillis();
        class LocalClassWithCapture {
            private final String value;
            private final long capturedValue;

            LocalClassWithCapture(@AnnotationA String p1) {
                this.value = p1;
                this.capturedValue = CAPTURED_VALUE;
            }
        }
        return LocalClassWithCapture.class;
    }

    public static void main(String[] args) throws Throwable {
        // A local class declared in a static context (0 implicit parameters).
        class LocalClassStaticContext {
            private final int value;

            LocalClassStaticContext(@AnnotationA int p0) {
                this.value = p0;
            }
        }

        final long CAPTURED_VALUE = System.currentTimeMillis();
        // A local class declared in a static context with a capture (1 implicit parameters).
        class LocalClassStaticContextWithCapture {
            private final long capturedValue;
            private final String argumentValue;

            LocalClassStaticContextWithCapture(@AnnotationA String p1) {
                this.capturedValue = CAPTURED_VALUE;
                this.argumentValue = p1;
            }
        }

        // Another local class declared in a static context with a capture (1 implicit parameters).
        class LocalClassStaticContextWithCaptureAlternateOrdering {
            private final String argumentValue;
            private final long capturedValue;

            LocalClassStaticContextWithCaptureAlternateOrdering(@AnnotationA String p1) {
                this.argumentValue = p1;
                this.capturedValue = CAPTURED_VALUE;
            }
        }

        DumpConstructorParameterAnnotations(Main.class);
        DumpConstructorParameterAnnotations(LocalClassStaticContext.class);
        DumpConstructorParameterAnnotations(LocalClassStaticContextWithCapture.class);
        DumpConstructorParameterAnnotations(LocalClassStaticContextWithCaptureAlternateOrdering.class);
        Main m = new Main();
        DumpConstructorParameterAnnotations(m.getLocalClassWithEnclosingInstanceCapture());
        DumpConstructorParameterAnnotations(m.getLocalClassWithEnclosingInstanceAndLocalCapture());
        DumpConstructorParameterAnnotations(Inner.class);
        DumpConstructorParameterAnnotations(StaticInner.class);
        DumpConstructorParameterAnnotations(ImportantNumber.class);
        DumpConstructorParameterAnnotations(BinaryNumber.class);
        DumpConstructorParameterAnnotations(new AnonymousBase("") {}.getClass());
    }
}