/*
 * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

import java.lang.classfile.*;
import java.lang.classfile.AnnotationValue;

import java.lang.annotation.RetentionPolicy;
import java.util.*;
import java.util.stream.Collectors;

public class TestAnnotationInfo {
    public final String annotationName;
    public final RetentionPolicy policy;
    public final boolean isContainer;
    public final List<Pair> elementValues;

    public TestAnnotationInfo(String typeIndexName, RetentionPolicy policy, Pair... values) {
        this(typeIndexName, policy, false, values);
    }

    public TestAnnotationInfo(String typeIndexName, RetentionPolicy policy, boolean isRepeatable, Pair... values) {
        this.annotationName = typeIndexName;
        this.policy = policy;
        this.isContainer = isRepeatable;
        elementValues = Arrays.asList(values);
    }

    public void testAnnotation(TestResult testResult, ClassModel classFile, Annotation annotation) {
        testResult.checkEquals(annotation.classSymbol().descriptorString(),
                String.format("L%s;", annotationName), "Testing annotation name : " + annotationName);
        testResult.checkEquals(annotation.elements().size(),
                elementValues.size(), "Number of element values");
        if (!testResult.checkEquals(annotation.elements().size(), elementValues.size(),
                "Number of element value pairs")) {
            return;
        }
        for (int i = 0; i < annotation.elements().size(); ++i) {
            AnnotationElement pair = annotation.elements().get(i);
            testResult.checkEquals(pair.name().stringValue(),
                    elementValues.get(i).elementName, "element_name_index : " + elementValues.get(i).elementName);
            elementValues.get(i).elementValue.testElementValue(testResult, classFile, pair.value());
        }
    }

    @Override
    public String toString() {
        return String.format("@%s(%s)", annotationName,
                elementValues.stream()
                        .map(Pair::toString)
                        .filter(s -> !s.isEmpty())
                        .collect(Collectors.joining(", ")));
    }

    public static class Pair {
        public final String elementName;
        public final TestElementValue elementValue;

        public Pair(String elementName, TestElementValue elementValue) {
            this.elementName = elementName;
            this.elementValue = elementValue;
        }

        @Override
        public String toString() {
            return elementName + "=" + elementValue;
        }
    }

    public static abstract class TestElementValue {
        private final int tag;

        public TestElementValue(int tag) {
            this.tag = tag;
        }

        public void testTag(TestResult testCase, int actualTag) {
            testCase.checkEquals(actualTag, tag, "tag " + (char) tag);
        }

        public abstract void testElementValue(TestResult testResult,
                                              ClassModel classFile,
                                              AnnotationValue element_value);
    }

    public static class TestIntegerElementValue extends TestElementValue {
        private final int value;

        public TestIntegerElementValue(int tag, int value) {
            super(tag);
            this.value = value;
        }

        @Override
        public void testElementValue(TestResult testResult,
                                     ClassModel classFile,
                                     AnnotationValue element_value) {
            testTag(testResult, element_value.tag());
            switch (element_value.tag()) {
                case 'B':
                    testResult.checkEquals((int)((AnnotationValue.OfByte) element_value).byteValue(), value, "const_value_index : " + value);
                    break;
                case 'S':
                    testResult.checkEquals((int)((AnnotationValue.OfShort) element_value).shortValue(), value, "const_value_index : " + value);
                    break;
                default:
                    testResult.checkEquals(((AnnotationValue.OfInt) element_value).intValue(), value, "const_value_index : " + value);
            }
        }

        @Override
        public String toString() {
            return String.valueOf(value);
        }
    }

    public static class TestBooleanElementValue extends TestElementValue {
        private final boolean value;

        public TestBooleanElementValue(boolean value) {
            super('Z');
            this.value = value;
        }

        @Override
        public void testElementValue(TestResult testResult,
                                     ClassModel classFile,
                                     AnnotationValue element_value) {
            testTag(testResult, element_value.tag());
            AnnotationValue.OfBoolean ev = (AnnotationValue.OfBoolean) element_value;
            testResult.checkEquals(ev.booleanValue(), value, "const_value_index : " + value);
        }

        @Override
        public String toString() {
            return String.valueOf(value);
        }
    }

    public static class TestCharElementValue extends TestElementValue {
        private final char value;

        public TestCharElementValue(char value) {
            super('C');
            this.value = value;
        }

        @Override
        public void testElementValue(TestResult testResult,
                                     ClassModel classFile,
                                     AnnotationValue element_value) {
            testTag(testResult, element_value.tag());
            AnnotationValue.OfChar ev =
                    (AnnotationValue.OfChar) element_value;
            testResult.checkEquals(ev.charValue(), value, "const_value_index : " + value);
        }

        @Override
        public String toString() {
            return String.format("\'%c\'", value);
        }
    }

    public static class TestLongElementValue extends TestElementValue {
        private final long value;

        public TestLongElementValue(long value) {
            super('J');
            this.value = value;
        }

        @Override
        public void testElementValue(TestResult testResult,
                                     ClassModel classFile,
                                     AnnotationValue element_value) {
            testTag(testResult, element_value.tag());
            AnnotationValue.OfLong ev =
                    (AnnotationValue.OfLong) element_value;
            testResult.checkEquals(ev.longValue(), value, "const_value_index");
        }

        @Override
        public String toString() {
            return String.valueOf(value);
        }
    }

    public static class TestFloatElementValue extends TestElementValue {
        private final float value;

        public TestFloatElementValue(float value) {
            super('F');
            this.value = value;
        }

        @Override
        public void testElementValue(TestResult testResult,
                                     ClassModel classFile,
                                     AnnotationValue element_value) {
            testTag(testResult, element_value.tag());
            AnnotationValue.OfFloat ev =
                    (AnnotationValue.OfFloat) element_value;
            testResult.checkEquals(ev.floatValue(), value, "const_value_index");
        }

        @Override
        public String toString() {
            return String.valueOf(value) + "f";
        }
    }

    public static class TestDoubleElementValue extends TestElementValue {
        private final double value;

        public TestDoubleElementValue(double value) {
            super('D');
            this.value = value;
        }

        @Override
        public void testElementValue(TestResult testResult,
                                     ClassModel classFile,
                                     AnnotationValue element_value) {
            testTag(testResult, element_value.tag());
            AnnotationValue.OfDouble ev =
                    (AnnotationValue.OfDouble) element_value;
            testResult.checkEquals(ev.doubleValue(), value, "const_value_index");
        }

        @Override
        public String toString() {
            return String.valueOf(value);
        }
    }

    public static class TestStringElementValue extends TestElementValue {
        private final String value;

        public TestStringElementValue(String value) {
            super('s');
            this.value = value;
        }

        @Override
        public void testElementValue(TestResult testResult,
                                     ClassModel classFile,
                                     AnnotationValue element_value) {
            testTag(testResult, element_value.tag());
            AnnotationValue.OfString ev =
                    (AnnotationValue.OfString) element_value;
            testResult.checkEquals(ev.stringValue(), value, "const_value_index");
        }

        @Override
        public String toString() {
            return String.format("\"%s\"", value);
        }
    }

    public static class TestEnumElementValue extends TestElementValue {
        private final String typeName;
        private final String constName;

        public TestEnumElementValue(String typeName, String constName) {
            super('e');
            this.typeName = typeName;
            this.constName = constName;
        }

        @Override
        public void testElementValue(
                TestResult testResult,
                ClassModel classFile,
                AnnotationValue element_value) {
            testTag(testResult, element_value.tag());
            AnnotationValue.OfEnum ev = (AnnotationValue.OfEnum) element_value;
            testResult.checkEquals(ev.classSymbol().descriptorString(),
                    String.format("L%s;", typeName), "type_name_index");
            testResult.checkEquals(ev.constantName().stringValue(),
                    constName, "const_name_index");
        }

        @Override
        public String toString() {
            return typeName + "." + constName;
        }
    }

    public static class TestClassElementValue extends TestElementValue {
        private final String className;

        private final static Map<String, String> mappedClassName;

        static {
            mappedClassName = new HashMap<>();
            mappedClassName.put("void", "V");
            mappedClassName.put("char", "C");
            mappedClassName.put("byte", "B");
            mappedClassName.put("short", "S");
            mappedClassName.put("int", "I");
            mappedClassName.put("long", "J");
            mappedClassName.put("float", "F");
            mappedClassName.put("double", "D");
        }

        public TestClassElementValue(String className) {
            super('c');
            this.className = className;
        }

        @Override
        public void testElementValue(
                TestResult testResult,
                ClassModel classFile,
                AnnotationValue element_value) {
            testTag(testResult, element_value.tag());
            AnnotationValue.OfClass ev = (AnnotationValue.OfClass) element_value;
            String expectedClassName = className.replace(".class", "");
            expectedClassName = mappedClassName.getOrDefault(expectedClassName,
                    String.format("Ljava/lang/%s;", expectedClassName));
            testResult.checkEquals(ev.classSymbol().descriptorString(),
                    expectedClassName, "class_info_index : " + expectedClassName);
        }

        @Override
        public String toString() {
            return className;
        }
    }

    public static class TestArrayElementValue extends TestElementValue {
        public final List<TestElementValue> values;

        public TestArrayElementValue(TestElementValue...values) {
            super('[');
            this.values = new ArrayList<>(Arrays.asList(values));
        }

        @Override
        public void testElementValue(
                TestResult testResult,
                ClassModel classFile,
                AnnotationValue element_value) {
            testTag(testResult, element_value.tag());
            AnnotationValue.OfArray ev = (AnnotationValue.OfArray) element_value;

            for (int i = 0; i < values.size(); ++i) {
                values.get(i).testElementValue(testResult, classFile, ev.values().get(i));
            }
        }

        @Override
        public String toString() {
            return values.stream()
                    .map(TestElementValue::toString)
                    .collect(Collectors.joining(", ", "{", "}"));
        }
    }

    public static class TestAnnotationElementValue extends TestElementValue {
        private final String annotationName;
        private final TestAnnotationInfo annotation;

        public TestAnnotationElementValue(String className, TestAnnotationInfo annotation) {
            super('@');
            this.annotationName = className;
            this.annotation = annotation;
        }

        @Override
        public void testElementValue(
                TestResult testResult,
                ClassModel classFile,
                AnnotationValue element_value) {
            testTag(testResult, element_value.tag());
            Annotation ev = ((AnnotationValue.OfAnnotation) element_value).annotation();
            testResult.checkEquals(
                    ev.classSymbol().descriptorString(),
                    String.format("L%s;", annotationName),
                    "type_index");
            for (int i = 0; i < ev.elements().size(); ++i) {
                AnnotationElement pair = ev.elements().get(i);
                Pair expectedPair = annotation.elementValues.get(i);
                expectedPair.elementValue.testElementValue(testResult, classFile, pair.value());
                testResult.checkEquals(
                        pair.name().stringValue(),
                        expectedPair.elementName,
                        "element_name_index");
            }
        }

        @Override
        public String toString() {
            return annotation.toString();
        }
    }
}