/*
 * Copyright (C) 2017 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.
 */
package com.android.dx;

import com.android.dx.dex.file.ClassDefItem;
import com.android.dx.rop.annotation.Annotation;
import com.android.dx.rop.annotation.AnnotationVisibility;
import com.android.dx.rop.annotation.Annotations;
import com.android.dx.rop.annotation.NameValuePair;
import com.android.dx.rop.cst.*;

import java.lang.annotation.ElementType;
import java.util.HashMap;

/**
 * Identifies an annotation on a program element, see {@link ElementType}.
 *
 * Currently it is only targeting Class, Method, Field and Parameter because those are supported by
 * {@link com.android.dx.dex.file.AnnotationsDirectoryItem} so far.
 *
 * <p><strong>NOTE:</strong>
 * So far it only supports adding method annotation. The annotation of class, field and parameter
 * will be implemented later.
 *
 * <p><strong>WARNING:</strong>
 * The declared element of an annotation type should either have a default value or be set a value via
 * {@code AnnotationId.set(Element)}. Otherwise it will incur
 * {@link java.lang.annotation.IncompleteAnnotationException} when accessing the annotation element
 * through reflection. The example is as follows:
 * <pre>
 *     {@code @Retention(RetentionPolicy.RUNTIME)}
 *     {@code @Target({ElementType.METHOD})}
 *     {@code @interface MethodAnnotation {
 *                boolean elementBoolean();
 *                // boolean elementBoolean() default false;
 *            }
 *
 *            TypeId<?> GENERATED = TypeId.get("LGenerated;");
 *            MethodId<?, Void> methodId = GENERATED.getMethod(VOID, "call");
 *            Code code = dexMaker.declare(methodId, PUBLIC);
 *            code.returnVoid();
 *
 *            TypeId<MethodAnnotation> annotationTypeId = TypeId.get(MethodAnnotation.class);
 *            AnnotationId<?, MethodAnnotation> annotationId = AnnotationId.get(GENERATED,
 *              annotationTypeId, ElementType.METHOD);
 *
 *            AnnotationId.Element element = new AnnotationId.Element("elementBoolean", true);
 *            annotationId.set(element);
 *            annotationId.addToMethod(dexMaker, methodId);
 *     }
 * </pre>
 *
 * @param <D> the type that declares the program element.
 * @param <V> the annotation type. It should be a known type before compile.
 */
public final class AnnotationId<D, V> {
    private final TypeId<D> declaringType;
    private final TypeId<V> type;
    /** The type of program element to be annotated */
    private final ElementType annotatedElement;
    /** The elements this annotation holds */
    private final HashMap<String, NameValuePair> elements;

    private AnnotationId(TypeId<D> declaringType, TypeId<V> type, ElementType annotatedElement) {
        this.declaringType = declaringType;
        this.type = type;
        this.annotatedElement = annotatedElement;
        this.elements = new HashMap<>();
    }

    /**
     *  Construct an instance. It initially contains no elements.
     *
     * @param declaringType the type declaring the program element.
     * @param type the annotation type.
     * @param annotatedElement the program element type to be annotated.
     * @return an annotation {@code AnnotationId<D,V>} instance.
     */
    public static <D, V> AnnotationId<D, V> get(TypeId<D> declaringType, TypeId<V> type,
                                                ElementType annotatedElement) {
        if (annotatedElement != ElementType.TYPE &&
                annotatedElement != ElementType.METHOD &&
                annotatedElement != ElementType.FIELD &&
                annotatedElement != ElementType.PARAMETER) {
            throw new IllegalArgumentException("element type is not supported to annotate yet.");
        }

        return new AnnotationId<>(declaringType, type, annotatedElement);
    }

    /**
     * Set an annotation element of this instance.
     * If there is a preexisting element with the same name, it will be
     * replaced by this method.
     *
     * @param element {@code non-null;} the annotation element to be set.
     */
    public void set(Element element) {
        if (element == null) {
            throw new NullPointerException("element == null");
        }

        CstString pairName = new CstString(element.getName());
        Constant pairValue = Element.toConstant(element.getValue());
        NameValuePair nameValuePair = new NameValuePair(pairName, pairValue);
        elements.put(element.getName(), nameValuePair);
    }

    /**
     * Add this annotation to a method.
     *
     * @param dexMaker DexMaker instance.
     * @param method Method to be added to.
     */
    public void addToMethod(DexMaker dexMaker, MethodId<?, ?> method) {
        if (annotatedElement != ElementType.METHOD) {
            throw new IllegalStateException("This annotation is not for method");
        }

        if (!method.declaringType.equals(declaringType)) {
            throw new IllegalArgumentException("Method" + method + "'s declaring type is inconsistent with" + this);
        }

        ClassDefItem classDefItem = dexMaker.getTypeDeclaration(declaringType).toClassDefItem();

        if (classDefItem == null) {
            throw new NullPointerException("No class defined item is found");
        } else {
            CstMethodRef cstMethodRef = method.constant;

            if (cstMethodRef == null) {
                throw new NullPointerException("Method reference is NULL");
            } else {
                // Generate CstType
                CstType cstType = CstType.intern(type.ropType);

                // Generate Annotation
                Annotation annotation = new Annotation(cstType, AnnotationVisibility.RUNTIME);

                // Add generated annotation
                Annotations annotations = new Annotations();
                for (NameValuePair nvp : elements.values()) {
                    annotation.add(nvp);
                }
                annotations.add(annotation);
                classDefItem.addMethodAnnotations(cstMethodRef, annotations, dexMaker.getDexFile());
            }
        }
    }

    /**
     *  A wrapper of <code>NameValuePair</code> represents a (name, value) pair used as the contents
     *  of an annotation.
     *
     *  An {@code Element} instance is stored in {@code AnnotationId.elements} by calling {@code
     *  AnnotationId.set(Element)}.
     *
     *  <p><strong>WARNING: </strong></p>
     *  the name should be exact same as the annotation element declared in the annotation type
     *  which is referred by field {@code AnnotationId.type},otherwise the annotation will fail
     *  to add and {@code java.lang.reflect.Method.getAnnotations()} will return nothing.
     *
     */
    public static final class Element {
        /** {@code non-null;} the name */
        private final String name;
        /** {@code non-null;} the value */
        private final Object value;

        /**
         * Construct an instance.
         *
         * @param name {@code non-null;} the name
         * @param value {@code non-null;} the value
         */
        public Element(String name, Object value) {
            if (name == null) {
                throw new NullPointerException("name == null");
            }

            if (value == null) {
                throw new NullPointerException("value == null");
            }
            this.name = name;
            this.value = value;
        }

        public String getName() {
            return name;
        }

        public Object getValue() {
            return value;
        }

        /** {@inheritDoc} */
        @Override
        public String toString() {
            return "[" + name + ", " + value + "]";
        }

        /** {@inheritDoc} */
        @Override
        public int hashCode() {
            return name.hashCode() * 31 + value.hashCode();
        }

        /** {@inheritDoc} */
        @Override
        public boolean equals(Object other) {
            if (! (other instanceof Element)) {
                return false;
            }

            Element otherElement = (Element) other;

            return name.equals(otherElement.name)
                    && value.equals(otherElement.value);
        }

        /**
         *  Convert a value of an element to a {@code Constant}.
         *  <p><strong>Warning:</strong> Array or TypeId value is not supported yet.
         *
         * @param value an annotation element value.
         * @return a Constant
         */
        static Constant toConstant(Object value) {
            Class clazz = value.getClass();
            if (clazz.isEnum()) {
                CstString descriptor = new CstString(TypeId.get(clazz).getName());
                CstString name = new CstString(((Enum)value).name());
                CstNat cstNat = new CstNat(name, descriptor);
                return new CstEnumRef(cstNat);
            } else if (clazz.isArray()) {
                throw new UnsupportedOperationException("Array is not supported yet");
            } else if (value instanceof TypeId) {
                throw new UnsupportedOperationException("TypeId is not supported yet");
            } else {
                return  Constants.getConstant(value);
            }
        }
    }
}
