/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
// dmccann - December 10/2009 - 2.0.1 - Initial implementation
package org.eclipse.persistence.jaxb.javamodel;

import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.persistence.internal.helper.ConversionManager;

/**
 * <p>
 * <b>Purpose:</b>The purpose of this class is to act as a dynamic proxy that
 * allows JDK Annotation method calls to be made on a non Annotation object.
 *
 * <p>
 * <b>Responsibilities:</b>
 * <ul>
 * <li>Create and return a dynamic proxy instance based on an Annotation class
 * and a <code>Map</code> of components (method name to value pairs)</li>
 * <li>Allow JDK Annotation method calls to be invoked on the proxy object</li>
 * </ul>
 * <p>
 * This class provides a means to invoke JDK Annotation method calls on a non
 * Annotation instance.
 *
 * @see java.lang.reflect.Proxy
 */
public class AnnotationProxy implements InvocationHandler {
    private Map<String, Object> components;
    private ConversionManager conversionMgr;
    private static final String ANNOTATION_TYPE_METHOD_NAME = "annotationType";

    /**
     * This constructor sets the <code>Map</code> of components (method name to
     * value pairs)and the ConversionManager to be used when converting values
     * in the <code>Map</code> based on the return type of the associated
     * <code>Method</code> Note that the preferred method of obtaining an
     * instance of this proxy class is via
     * <code>getProxy(Map, Class<A>, ClassLoader, ConversionManager)</code>
     *
     * @param components
     *            <code>Map</code> of method name to value pairs
     * @param conversionMgr
     *            <code>ConversionManager</code> instance for converting to the
     *            correct return type in the <code>invoke</code> method
     */
    private AnnotationProxy(Map<String, Object> components, ConversionManager conversionMgr) {
        this.components = components;
        this.conversionMgr = conversionMgr;
    }

    /**
     * This is the preferred way to obtain an instance of a dynamic proxy.
     *
     * The method takes a <code>ClassLoader</code> (which is used to load the
     * target <code>Annotation</code>), a <code>Class</code> (which indicates
     * the target <code>Annotation</code>, i.e.
     * <code>javax.xml.bind.annotation.XmlElement.class)</code>, and a
     * <code>Map</code> of method name to value pairs, which represent the
     * method names on the <code>Annotation</code> and the values that are to be
     * returned from each method call. For example, if this proxy is to be used
     * for an <code>@XmlElement</code>, the <code>Map</code> should contain the
     * following keys:
     *
     * <ul>
     * <li>defaultValue</li> <li>name</li> <li>namespace</li> <li>nillable</li>
     * <li>required</li> <li> type</li>
     * </ul>
     *
     * Following are example key/value pairs :
     *
     * <ul>
     * <li>"defaultValue", "##default"</li> <li>"name", "employee"</li> <li>
     * "namespace", "www.example.org"</li> <li>"nillable", false</li> <li>
     * "required", false</li> <li>"type",
     * javax.xml.bind.annotation.XmlElement.DEFAULT.class</li>
     * </ul>
     *
     * @param components
     *            <code>Map</code> of method name/value pairs for this proxy
     *            instance
     * @param annoClass
     *            The interface for the proxy class to implement
     * @param cl
     *            The <code>ClassLoader</code> to define the proxy class
     * @param conversionMgr
     *            <code>ConversionManager</code> instance for converting to the
     *            correct return type in the <code>invoke</code> method
     * @return A dynamic proxy instance based on a Java model JavaAnnotation
     */
    public static <A extends Annotation> A getProxy(Map<String, Object> components, Class<A> annoClass, ClassLoader cl, ConversionManager conversionMgr) {
        // add the 'annotationType' method name/value pair to the components map
        if (components == null) {
            components = new HashMap<String, Object>();
        }
        components.put(ANNOTATION_TYPE_METHOD_NAME, annoClass.getName());

        // Pass the classloader to the ConversionManager as well
        // conversionMgr.setLoader(cl);

        return (A) Proxy.newProxyInstance(cl, new Class[] { annoClass }, new AnnotationProxy(components, conversionMgr));
    }

    /**
     * Return the <code>Map</code> of method name/value pairs for this proxy
     * instance.
     *
     * @return <code>Map</code> of method name/value pairs for this proxy
     *         instance
     */
    public Map<String, Object> getComponents() {
        return this.components;
    }

    /**
     * Invoke a given <code>Method</code> on this proxy. The component
     * <code>Map</code> will be accessed using the given <code>Method</code>'s
     * name, and if an entry exists, the associated value is returned.
     *
     * @param proxy
     *            Satisfy the <code>InvocationHandler</code> interface - not
     *            used
     * @param method
     *            The <code>Method</code> instance corresponding to the
     *            interface method invoked on the proxy instance
     * @param args
     *            Satisfy the <code>InvocationHandler</code> interface - not
     *            used
     * @return The value from the method invocation on the proxy instance
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (components == null) {
            return null;
        }

        Class returnType = method.getReturnType();
        Object value = getComponents().get(method.getName());

        if (value == null && returnType == boolean.class) {
            return false;
        }
        if (value == null && returnType == Boolean.class) {
            return Boolean.FALSE;
        }

        if (returnType.isArray()) {
            return handleArrayData(returnType, value);
        }

        // use the ConversionManager to ensure that the correct type is returned
        return conversionMgr.convertObject(value, returnType);
    }

    private Object handleArrayData(Class returnType, Object value) {
        if (value == null) {
            return null;
        }

        Object[] data = (Object[]) value;
        Class componentType = returnType.getComponentType();
        Object[] convertedArray = (Object[]) Array.newInstance(componentType, data.length);

        for (int i = 0; i < data.length; i++) {
            convertedArray[i] = conversionMgr.convertObject(data[i], componentType);
        }

        return convertedArray;
    }

}
