/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2010-2013 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * http://glassfish.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.jersey.server.impl.modelapi.annotation;

import com.sun.jersey.core.reflection.AnnotatedMethod;
import com.sun.jersey.core.reflection.MethodList;
import com.sun.jersey.core.header.MediaTypes;
import com.sun.jersey.api.model.AbstractField;
import com.sun.jersey.api.model.AbstractResource;
import com.sun.jersey.api.model.AbstractResourceConstructor;
import com.sun.jersey.api.model.AbstractResourceMethod;
import com.sun.jersey.api.model.AbstractSetterMethod;
import com.sun.jersey.api.model.AbstractSubResourceLocator;
import com.sun.jersey.api.model.AbstractSubResourceMethod;
import com.sun.jersey.api.model.Parameter;
import com.sun.jersey.api.model.Parameter.Source;
import com.sun.jersey.api.model.Parameterized;
import com.sun.jersey.api.model.PathValue;
import com.sun.jersey.core.reflection.ReflectionHelper;
import com.sun.jersey.impl.ImplMessages;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.security.AccessController;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.Consumes;
import javax.ws.rs.CookieParam;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.Encoded;
import javax.ws.rs.FormParam;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.MatrixParam;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;

/**
 *
 * @author japod
 */
public class IntrospectionModeller {

    private static final Logger LOGGER = Logger.getLogger(IntrospectionModeller.class.getName());

    public static AbstractResource createResource(Class<?> resourceClass) {
        final Class<?> annotatedResourceClass = getAnnotatedResourceClass(resourceClass);
        final Path rPathAnnotation = annotatedResourceClass.getAnnotation(Path.class);
        final boolean isRootResourceClass = (null != rPathAnnotation);

        final boolean isEncodedAnotOnClass =
                (null != annotatedResourceClass.getAnnotation(Encoded.class));

        AbstractResource resource;

        if (isRootResourceClass) {
            resource = new AbstractResource(resourceClass,
                    new PathValue(rPathAnnotation.value()));
        } else { // just a subresource class
            resource = new AbstractResource(resourceClass);
        }

        workOutConstructorsList(resource, resourceClass.getConstructors(),
                isEncodedAnotOnClass);

        workOutFieldsList(resource, isEncodedAnotOnClass);

        final MethodList methodList = new MethodList(resourceClass);

        workOutSetterMethodsList(resource, methodList, isEncodedAnotOnClass);

        final Consumes classScopeConsumesAnnotation =
                annotatedResourceClass.getAnnotation(Consumes.class);
        final Produces classScopeProducesAnnotation =
                annotatedResourceClass.getAnnotation(Produces.class);
        workOutResourceMethodsList(resource, methodList, isEncodedAnotOnClass,
                classScopeConsumesAnnotation, classScopeProducesAnnotation);
        workOutSubResourceMethodsList(resource, methodList, isEncodedAnotOnClass,
                classScopeConsumesAnnotation, classScopeProducesAnnotation);
        workOutSubResourceLocatorsList(resource, methodList, isEncodedAnotOnClass);

        workOutPostConstructPreDestroy(resource);

        if (LOGGER.isLoggable(Level.FINEST)) {
            LOGGER.finest(ImplMessages.NEW_AR_CREATED_BY_INTROSPECTION_MODELER(
                    resource.toString()));
        }

        return resource;
    }

    private static Class getAnnotatedResourceClass(Class rc) {
        if (rc.isAnnotationPresent(Path.class)) return rc;

        for (Class i : rc.getInterfaces())
            if (i.isAnnotationPresent(Path.class)) return i;

        return rc;
    }

    private static void addConsumes(
            AnnotatedMethod am,
            AbstractResourceMethod resourceMethod,
            Consumes consumeMimeAnnotation) {
        // Override annotation is present in method
        if (am.isAnnotationPresent(Consumes.class))
            consumeMimeAnnotation = am.getAnnotation(Consumes.class);

        resourceMethod.setAreInputTypesDeclared(consumeMimeAnnotation != null);
        resourceMethod.getSupportedInputTypes().addAll(
                MediaTypes.createMediaTypes(consumeMimeAnnotation));
    }

    private static void addProduces(
            AnnotatedMethod am,
            AbstractResourceMethod resourceMethod,
            Produces produceMimeAnnotation) {
        // Override annotation is present in method
        if (am.isAnnotationPresent(Produces.class))
            produceMimeAnnotation = am.getAnnotation(Produces.class);

        resourceMethod.setAreOutputTypesDeclared(produceMimeAnnotation != null);
        resourceMethod.getSupportedOutputTypes().addAll(
                MediaTypes.createQualitySourceMediaTypes(produceMimeAnnotation));
    }

    private static void workOutConstructorsList(
            AbstractResource resource,
            Constructor[] ctorArray,
            boolean isEncoded) {
        if (null != ctorArray) {
            for (Constructor ctor : ctorArray) {
                final AbstractResourceConstructor aCtor =
                        new AbstractResourceConstructor(ctor);
                processParameters(
                        resource.getResourceClass(),
                        ctor.getDeclaringClass(),
                        aCtor,
                        ctor,
                        isEncoded);
                resource.getConstructors().add(aCtor);
            }
        }
    }

    private static void workOutFieldsList(
            AbstractResource resource,
            boolean isEncoded) {
        Class c = resource.getResourceClass();
        if (c.isInterface())
            return;

        while (c != Object.class) {
             for (final Field f : c.getDeclaredFields()) {
                if (f.getDeclaredAnnotations().length > 0) {
                    final AbstractField af = new AbstractField(f);
                    Parameter p = createParameter(
                            resource.getResourceClass(),
                            f.getDeclaringClass(),
                            isEncoded,
                            f.getType(),
                            f.getGenericType(),
                            f.getAnnotations());
                    if (null != p) {
                        af.getParameters().add(p);
                        resource.getFields().add(af);
                    }
                }
             }
             c = c.getSuperclass();
        }
    }

    private static void workOutPostConstructPreDestroy(AbstractResource resource) {
        Class postConstruct = AccessController.doPrivileged(ReflectionHelper.classForNamePA("javax.annotation.PostConstruct"));
        if (postConstruct == null)
            return;

        Class preDestroy = AccessController.doPrivileged(ReflectionHelper.classForNamePA("javax.annotation.PreDestroy"));

        final MethodList methodList = new MethodList(resource.getResourceClass(), true);
        HashSet<String> names = new HashSet<String>();
        for (AnnotatedMethod m : methodList.
                hasAnnotation(postConstruct).
                hasNumParams(0).
                hasReturnType(void.class)) {
            Method method = m.getMethod();
            // only add method if not hidden/overridden
            if (names.add(method.getName())) {
                AccessController.doPrivileged(ReflectionHelper.setAccessibleMethodPA(method));
                resource.getPostConstructMethods().add(0, method);
            }
        }

        names = new HashSet<String>();
        for (AnnotatedMethod m : methodList.
                hasAnnotation(preDestroy).
                hasNumParams(0).
                hasReturnType(void.class)) {
            Method method = m.getMethod();
            // only add method if not hidden/overridden
            if (names.add(method.getName())) {
                AccessController.doPrivileged(ReflectionHelper.setAccessibleMethodPA(method));
                resource.getPreDestroyMethods().add(method);
            }
        }
    }

    private static void workOutSetterMethodsList(
            AbstractResource resource,
            MethodList methodList,
            boolean isEncoded) {
        for (AnnotatedMethod m : methodList.
                hasNotMetaAnnotation(HttpMethod.class).
                hasNotAnnotation(Path.class).
                hasNumParams(1).
                hasReturnType(void.class).
                nameStartsWith("set")) {

            final AbstractSetterMethod asm = new AbstractSetterMethod(resource, m.getMethod(), m.getAnnotations());
            Parameter p = createParameter(
                    resource.getResourceClass(),
                    m.getMethod().getDeclaringClass(),
                    isEncoded,
                    m.getParameterTypes()[0],
                    m.getGenericParameterTypes()[0],
                    m.getAnnotations());
            if (null != p) {
                asm.getParameters().add(p);
                resource.getSetterMethods().add(asm);
            }
        }
    }

    private static void workOutResourceMethodsList(
            AbstractResource resource,
            MethodList methodList,
            boolean isEncoded,
            Consumes classScopeConsumesAnnotation,
            Produces classScopeProducesAnnotation) {
        for (AnnotatedMethod m : methodList.hasMetaAnnotation(HttpMethod.class).
                hasNotAnnotation(Path.class)) {

            final ReflectionHelper.ClassTypePair ct = getGenericReturnType(resource.getResourceClass(), m.getMethod());
            final AbstractResourceMethod resourceMethod = new AbstractResourceMethod(
                    resource,
                    m.getMethod(),
                    ct.c, ct.t,
                    m.getMetaMethodAnnotations(HttpMethod.class).get(0).value(),
                    m.getAnnotations());

            addConsumes(m, resourceMethod, classScopeConsumesAnnotation);
            addProduces(m, resourceMethod, classScopeProducesAnnotation);
            processParameters(
                    resourceMethod.getResource().getResourceClass(),
                    resourceMethod.getMethod().getDeclaringClass(),
                    resourceMethod, m, isEncoded);

            resource.getResourceMethods().add(resourceMethod);
        }
    }

    private static ReflectionHelper.ClassTypePair getGenericReturnType(
            Class concreteClass,
            Method m) {
        return getGenericType(concreteClass, m.getDeclaringClass(), m.getReturnType(), m.getGenericReturnType());
    }

    private static void workOutSubResourceMethodsList(
            AbstractResource resource,
            MethodList methodList,
            boolean isEncoded,
            Consumes classScopeConsumesAnnotation,
            Produces classScopeProducesAnnotation) {

        for (AnnotatedMethod m : methodList.hasMetaAnnotation(HttpMethod.class).hasAnnotation(Path.class)) {

            final Path mPathAnnotation = m.getAnnotation(Path.class);
            final PathValue pv = new PathValue(mPathAnnotation.value());

            final boolean emptySegmentCase =  "/".equals(pv.getValue()) || "".equals(pv.getValue());

            if (!emptySegmentCase) {
                final ReflectionHelper.ClassTypePair ct = getGenericReturnType(resource.getResourceClass(), m.getMethod());
                final AbstractSubResourceMethod abstractSubResourceMethod = new AbstractSubResourceMethod(
                        resource,
                        m.getMethod(),
                        ct.c, ct.t,
                        pv,
                        m.getMetaMethodAnnotations(HttpMethod.class).get(0).value(),
                        m.getAnnotations());

                addConsumes(m, abstractSubResourceMethod, classScopeConsumesAnnotation);
                addProduces(m, abstractSubResourceMethod, classScopeProducesAnnotation);
                processParameters(
                        abstractSubResourceMethod.getResource().getResourceClass(),
                        abstractSubResourceMethod.getMethod().getDeclaringClass(),
                        abstractSubResourceMethod, m, isEncoded);

                resource.getSubResourceMethods().add(abstractSubResourceMethod);

            } else { // treat the sub-resource method as a resource method
                final ReflectionHelper.ClassTypePair ct = getGenericReturnType(resource.getResourceClass(), m.getMethod());
                final AbstractResourceMethod abstractResourceMethod = new AbstractResourceMethod(
                        resource,
                        m.getMethod(),
                        ct.c, ct.t,
                        m.getMetaMethodAnnotations(HttpMethod.class).get(0).value(),
                        m.getAnnotations());

                addConsumes(m, abstractResourceMethod, classScopeConsumesAnnotation);
                addProduces(m, abstractResourceMethod, classScopeProducesAnnotation);
                processParameters(
                        abstractResourceMethod.getResource().getResourceClass(),
                        abstractResourceMethod.getMethod().getDeclaringClass(),
                        abstractResourceMethod, m, isEncoded);

                resource.getResourceMethods().add(abstractResourceMethod);
            }
        }
    }

    private static void workOutSubResourceLocatorsList(
            AbstractResource resource,
            MethodList methodList,
            boolean isEncoded) {

        for (AnnotatedMethod m : methodList.hasNotMetaAnnotation(HttpMethod.class).
                hasAnnotation(Path.class)) {
            final Path mPathAnnotation = m.getAnnotation(Path.class);
            final AbstractSubResourceLocator subResourceLocator = new AbstractSubResourceLocator(
                    resource,
                    m.getMethod(),
                    new PathValue(
                        mPathAnnotation.value()),
                    m.getAnnotations());

            processParameters(
                    subResourceLocator.getResource().getResourceClass(),
                    subResourceLocator.getMethod().getDeclaringClass(),
                    subResourceLocator, m, isEncoded);

            resource.getSubResourceLocators().add(subResourceLocator);
        }
    }

    private static void processParameters(
            Class concreteClass,
            Class declaringClass,
            Parameterized parametrized,
            Constructor ctor,
            boolean isEncoded) {
        Class[] parameterTypes = ctor.getParameterTypes();
        Type[] genericParameterTypes = ctor.getGenericParameterTypes();
        // Workaround bug http://bugs.sun.com/view_bug.do?bug_id=5087240
        if (parameterTypes.length != genericParameterTypes.length) {
            Type[] _genericParameterTypes = new Type[parameterTypes.length];
            _genericParameterTypes[0] = parameterTypes[0];
            System.arraycopy(genericParameterTypes, 0, _genericParameterTypes, 1, genericParameterTypes.length);
            genericParameterTypes = _genericParameterTypes;
        }

        processParameters(
                concreteClass, declaringClass,
                parametrized,
                ((null != ctor.getAnnotation(Encoded.class)) || isEncoded),
                parameterTypes,
                genericParameterTypes,
                ctor.getParameterAnnotations());
    }

    private static void processParameters(
            Class concreteClass,
            Class declaringClass,
            Parameterized parametrized,
            AnnotatedMethod method,
            boolean isEncoded) {
        processParameters(
                concreteClass, declaringClass,
                parametrized,
                ((null != method.getAnnotation(Encoded.class)) || isEncoded),
                method.getParameterTypes(),
                method.getGenericParameterTypes(),
                method.getParameterAnnotations());
    }

    private static void processParameters(
            Class concreteClass,
            Class declaringClass,
            Parameterized parametrized,
            boolean isEncoded,
            Class[] parameterTypes,
            Type[] genericParameterTypes,
            Annotation[][] parameterAnnotations) {

        for (int i = 0; i < parameterTypes.length; i++) {
            Parameter parameter = createParameter(
                    concreteClass, declaringClass,
                    isEncoded, parameterTypes[i],
                    genericParameterTypes[i],
                    parameterAnnotations[i]);
            if (null != parameter) {
                parametrized.getParameters().add(parameter);
            } else {
                // clean up the parameters
                parametrized.getParameters().removeAll(parametrized.getParameters());
                break;
            }
        }
    }

    private static interface ParamAnnotationHelper<T extends Annotation> {

        public String getValueOf(T a);

        public Parameter.Source getSource();
    }

    private static Map<Class, ParamAnnotationHelper> createParamAnotHelperMap() {
        Map<Class, ParamAnnotationHelper> m = new WeakHashMap<Class, ParamAnnotationHelper>();
        m.put(Context.class, new ParamAnnotationHelper<Context>() {

            @Override
            public String getValueOf(Context a) {
                return null;
            }

            @Override
            public Parameter.Source getSource() {
                return Parameter.Source.CONTEXT;
            }
        });
        m.put(HeaderParam.class, new ParamAnnotationHelper<HeaderParam>() {

            @Override
            public String getValueOf(HeaderParam a) {
                return a.value();
            }

            @Override
            public Parameter.Source getSource() {
                return Parameter.Source.HEADER;
            }
        });
        m.put(CookieParam.class, new ParamAnnotationHelper<CookieParam>() {

            @Override
            public String getValueOf(CookieParam a) {
                return a.value();
            }

            @Override
            public Parameter.Source getSource() {
                return Parameter.Source.COOKIE;
            }
        });
        m.put(MatrixParam.class, new ParamAnnotationHelper<MatrixParam>() {

            @Override
            public String getValueOf(MatrixParam a) {
                return a.value();
            }

            @Override
            public Parameter.Source getSource() {
                return Parameter.Source.MATRIX;
            }
        });
        m.put(QueryParam.class, new ParamAnnotationHelper<QueryParam>() {

            @Override
            public String getValueOf(QueryParam a) {
                return a.value();
            }

            @Override
            public Parameter.Source getSource() {
                return Parameter.Source.QUERY;
            }
        });
        m.put(PathParam.class, new ParamAnnotationHelper<PathParam>() {

            @Override
            public String getValueOf(PathParam a) {
                return a.value();
            }

            @Override
            public Parameter.Source getSource() {
                return Parameter.Source.PATH;
            }
        });
        m.put(FormParam.class, new ParamAnnotationHelper<FormParam>() {

            @Override
            public String getValueOf(FormParam a) {
                return a.value();
            }

            @Override
            public Parameter.Source getSource() {
                return Parameter.Source.FORM;
            }
        });
        return Collections.unmodifiableMap(m);
    }
    private final static Map<Class, ParamAnnotationHelper> ANOT_HELPER_MAP =
            createParamAnotHelperMap();

    @SuppressWarnings("unchecked")
    private static Parameter createParameter(
            Class concreteClass,
            Class declaringClass,
            boolean isEncoded,
            Class<?> paramClass,
            Type paramType,
            Annotation[] annotations) {

        if (null == annotations) {
            return null;
        }

        Annotation paramAnnotation = null;
        Parameter.Source paramSource = null;
        String paramName = null;
        boolean paramEncoded = isEncoded;
        String paramDefault = null;

        /**
         * Create a parameter from the list of annotations.
         * Unknown annotated parameters are also supported, and in such a
         * cases the last unrecognized annotation is taken to be that
         * associated with the parameter.
         */
        for (Annotation annotation : annotations) {
            if (ANOT_HELPER_MAP.containsKey(annotation.annotationType())) {
                ParamAnnotationHelper helper = ANOT_HELPER_MAP.get(annotation.annotationType());
                paramAnnotation = annotation;
                paramSource = helper.getSource();
                paramName = helper.getValueOf(annotation);
            } else if (Encoded.class == annotation.annotationType()) {
                paramEncoded = true;
            } else if (DefaultValue.class == annotation.annotationType()) {
                paramDefault = ((DefaultValue) annotation).value();
            } else {
                // lets only clear things down if we've not found a ANOT_HELPER_MAP annotation already
                if (paramAnnotation == null) {
                    paramAnnotation = annotation;
                    paramSource = Source.UNKNOWN;
                    paramName = getValue(annotation);
                }
            }
        }

        if (paramAnnotation == null) {
            paramSource = Parameter.Source.ENTITY;
        }

        ReflectionHelper.ClassTypePair ct = getGenericType(concreteClass, declaringClass, paramClass, paramType);
        paramType = ct.t;
        paramClass = ct.c;

        return new Parameter(
                annotations, paramAnnotation,
                paramSource,
                paramName, paramType, paramClass,
                paramEncoded, paramDefault);
    }

    private static String getValue(Annotation a) {
        try {
            Method m = a.annotationType().getMethod("value");
            if (m.getReturnType() != String.class)
                return null;
            return (String)m.invoke(a);
        } catch (Exception ex) {
        }
        return null;
    }

    private static ReflectionHelper.ClassTypePair getGenericType(
            final Class concreteClass,
            final Class declaringClass,
            final Class c,
            final Type t) {
        if (t instanceof TypeVariable) {
            ReflectionHelper.ClassTypePair ct = ReflectionHelper.resolveTypeVariable(
                    concreteClass,
                    declaringClass,
                    (TypeVariable)t);

            if (ct != null) {
                return ct;
            }
        } else if (t instanceof ParameterizedType) {
            final ParameterizedType pt = (ParameterizedType)t;
            final Type[] ptts = pt.getActualTypeArguments();
            boolean modified =  false;
            for (int i = 0; i < ptts.length; i++) {
                ReflectionHelper.ClassTypePair ct =
                        getGenericType(concreteClass, declaringClass, (Class)pt.getRawType(), ptts[i]);
                if (ct.t != ptts[i]) {
                    ptts[i] = ct.t;
                    modified = true;
                }
            }
            if (modified) {
                ParameterizedType rpt = new ParameterizedType() {
                    @Override
                    public Type[] getActualTypeArguments() {
                        return ptts.clone();
                    }

                    @Override
                    public Type getRawType() {
                        return pt.getRawType();
                    }

                    @Override
                    public Type getOwnerType() {
                        return pt.getOwnerType();
                    }
                };
                return new ReflectionHelper.ClassTypePair((Class)pt.getRawType(), rpt);
            }
        } else if (t instanceof GenericArrayType) {
            GenericArrayType gat = (GenericArrayType)t;
            final ReflectionHelper.ClassTypePair ct =
                    getGenericType(concreteClass, declaringClass, null, gat.getGenericComponentType());
            if (gat.getGenericComponentType() != ct.t) {
                try {
                    Class ac = ReflectionHelper.getArrayClass(ct.c);
                    return new ReflectionHelper.ClassTypePair(ac, ac);
                } catch (Exception e) {
                }
            }
        }

        return new ReflectionHelper.ClassTypePair(c, t);
    }
}
