/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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 org.apache.cxf.jaxrs.utils;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.logging.Logger;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.MessageBodyWorkers;

import org.apache.cxf.common.i18n.BundleUtils;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.PrimitiveUtils;
import org.apache.cxf.jaxrs.impl.tl.ThreadLocalContextResolver;
import org.apache.cxf.jaxrs.impl.tl.ThreadLocalHttpHeaders;
import org.apache.cxf.jaxrs.impl.tl.ThreadLocalHttpServletRequest;
import org.apache.cxf.jaxrs.impl.tl.ThreadLocalMessageBodyWorkers;
import org.apache.cxf.jaxrs.impl.tl.ThreadLocalProxy;
import org.apache.cxf.jaxrs.impl.tl.ThreadLocalRequest;
import org.apache.cxf.jaxrs.impl.tl.ThreadLocalSecurityContext;
import org.apache.cxf.jaxrs.impl.tl.ThreadLocalServletContext;
import org.apache.cxf.jaxrs.impl.tl.ThreadLocalUriInfo;
import org.apache.cxf.jaxrs.model.AbstractResourceInfo;
import org.apache.cxf.message.Message;

public final class InjectionUtils {
    
    private static final Logger LOG = LogUtils.getL7dLogger(InjectionUtils.class);
    private static final ResourceBundle BUNDLE = BundleUtils.getBundle(InjectionUtils.class);
    
    private InjectionUtils() {
        
    }

    public static Method checkProxy(Method methodToInvoke, Object resourceObject) {
        if (Proxy.class.isInstance(resourceObject)) {
            
            for (Class<?> c : resourceObject.getClass().getInterfaces()) {
                try {
                    Method m = c.getMethod(
                        methodToInvoke.getName(), methodToInvoke.getParameterTypes());
                    if (m != null) {
                        return m;
                    }
                } catch (NoSuchMethodException ex) {
                    //ignore
                }
            }
            
        }
        return methodToInvoke; 
    }
 
    @SuppressWarnings("unchecked")
    public static void injectFieldValue(final Field f, 
                                        final Object o, 
                                        final Object v) {
        AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                f.setAccessible(true);
                try {
                    f.set(o, v);
                } catch (IllegalAccessException ex) {
                    LOG.warning(new org.apache.cxf.common.i18n.Message("FIELD_INJECTION_FAILURE", 
                                                                       BUNDLE, 
                                                                       f.getName()).toString());
                }
                return null;
            }
        });
        
    }
    
    public static Class<?> getActualType(Type genericType) {
        if (genericType == null 
            || !ParameterizedType.class.isAssignableFrom(genericType.getClass())) {
            return null;
        }
        ParameterizedType paramType = (ParameterizedType)genericType;
        return (Class<?>)paramType.getActualTypeArguments()[0];
    }
    
    public static void injectThroughMethod(Object requestObject,
                                           Method method,
                                           Object parameterValue) {
        try {
            Method methodToInvoke = checkProxy(method, requestObject);
            methodToInvoke.invoke(requestObject, new Object[]{parameterValue});
        } catch (Exception ex) {
            LOG.warning(new org.apache.cxf.common.i18n.Message("METHOD_INJECTION_FAILURE", 
                                                               BUNDLE, 
                                                               method.getName()).toString());
        }
    }
    
    public static Object handleParameter(String value, Class<?> pClass) {
        if (pClass.isPrimitive()) {
            return PrimitiveUtils.read(value, pClass);
        }
        // check constructors accepting a single String value
        try {
            Constructor<?> c = pClass.getConstructor(new Class<?>[]{String.class});
            if (c !=  null) {
                return c.newInstance(new Object[]{value});
            }
        } catch (Exception ex) {
            // try valueOf
        }
        // check for valueOf(String) static methods
        try {
            Method m = pClass.getMethod("valueOf", new Class<?>[]{String.class});
            if (m != null && Modifier.isStatic(m.getModifiers())) {
                return m.invoke(null, new Object[]{value});
            }
        } catch (Exception ex) {
            // no luck
        }
        return null;
    }
    
    public static Object handleBean(Class<?> paramType, MultivaluedMap<String, String> values) {
        Object bean = null;
        try {
            bean = paramType.newInstance();
            for (Map.Entry<String, List<String>> entry : values.entrySet()) {
                boolean injected = false;
                for (Method m : paramType.getMethods()) {
                    if (m.getName().equalsIgnoreCase("set" + entry.getKey())
                        && m.getParameterTypes().length == 1) {
                        Object paramValue = handleParameter(entry.getValue().get(0), 
                                                            m.getParameterTypes()[0]);
                        if (paramValue != null) {
                            injectThroughMethod(bean, m, paramValue);
                            injected = true;
                            break;
                        }
                    }
                }
                if (injected) {
                    continue;
                }
                for (Field f : paramType.getFields()) {
                    if (f.getName().equalsIgnoreCase(entry.getKey())) {
                        Object paramValue = handleParameter(entry.getValue().get(0), 
                                                            f.getType());
                        if (paramValue != null) {
                            injectFieldValue(f, bean, paramValue);
                            break;
                        }
                    }
                }
            }
        } catch (Exception ex) {
            LOG.warning(new org.apache.cxf.common.i18n.Message("CLASS_INSTANCIATION_FAILURE", 
                                                               BUNDLE, 
                                                               paramType.getName()).toString());    
        }
        return bean;
    }
    
    @SuppressWarnings("unchecked")
    public static Object injectIntoList(Type genericType, List<String> values,
                                        boolean decoded) {
        Class<?> realType = InjectionUtils.getActualType(genericType);
        List theValues = new ArrayList();
        for (String r : values) {
            if (decoded) {
                r = JAXRSUtils.uriDecode(r);
            }
            Object o = InjectionUtils.handleParameter(r, realType);
            if (o != null) {
                theValues.add(o);
            }
        }
        return theValues;
    }
    
    
    
    @SuppressWarnings("unchecked")
    public static Object injectIntoSet(Type genericType, List<String> values, 
                                       boolean sorted, boolean decoded) {
        Class<?> realType = InjectionUtils.getActualType(genericType);
        Set theValues = sorted ? new TreeSet() : new HashSet();
        for (String r : values) {
            if (decoded) {
                r = JAXRSUtils.uriDecode(r);
            }
            Object o = InjectionUtils.handleParameter(r, realType);
            if (o != null) {
                theValues.add(o);
            }
        }
        return theValues;
    }
    
    public static Object createParameterObject(List<String> paramValues,
                                               Class<?> paramType,
                                               Type genericType,
                                               String defaultValue,
                                               boolean isLast,
                                               boolean decoded) {
        
        if (paramValues == null) {
            if (defaultValue != null) {
                paramValues = Collections.singletonList(defaultValue);
            } else {
                if (paramType.isPrimitive()) {
                    paramValues = Collections.singletonList(
                        boolean.class == paramType ? "false" : "0");
                } else {
                    return null;
                }
            }
        }
        
        if (List.class.isAssignableFrom(paramType)) {
            return InjectionUtils.injectIntoList(genericType, paramValues, decoded);
        } else if (Set.class.isAssignableFrom(paramType)) {
            return InjectionUtils.injectIntoSet(genericType, paramValues, false, decoded);
        } else if (SortedSet.class.isAssignableFrom(paramType)) {
            return InjectionUtils.injectIntoSet(genericType, paramValues, true, decoded);
        } else {
            String result = null;
            if (paramValues.size() > 0) {
                result = isLast ? paramValues.get(paramValues.size() - 1)
                                : paramValues.get(0);
            }
            if (result != null) {
                if (decoded) {
                    result = JAXRSUtils.uriDecode(result);
                }
                return InjectionUtils.handleParameter(result, paramType);
            } else {
                return null;
            }
        }
    }
    
    public static ThreadLocalProxy createThreadLocalProxy(Class<?> type) {
        ThreadLocalProxy proxy = null;
        if (UriInfo.class.isAssignableFrom(type)) {
            proxy = new ThreadLocalUriInfo();
        } else if (HttpHeaders.class.isAssignableFrom(type)) {
            proxy = new ThreadLocalHttpHeaders();
        } else if (SecurityContext.class.isAssignableFrom(type)) {
            proxy = new ThreadLocalSecurityContext();
        } else if (ContextResolver.class.isAssignableFrom(type)) {
            proxy = new ThreadLocalContextResolver();
        } else if (Request.class.isAssignableFrom(type)) {
            proxy = new ThreadLocalRequest();
        }  else if (MessageBodyWorkers.class.isAssignableFrom(type)) {
            proxy = new ThreadLocalMessageBodyWorkers();
        } else if (HttpServletRequest.class.isAssignableFrom(type)) {
            proxy = new ThreadLocalHttpServletRequest();
        } else if (ServletContext.class.isAssignableFrom(type)) {
            proxy = new ThreadLocalServletContext();
        }
        return proxy;
    }
    
    public static void injectContextProxies(AbstractResourceInfo cri, Object instance) {
        if (!cri.isSingleton()) {
            return;
        }
        
        for (Map.Entry<Class<?>, Method> entry : cri.getContextMethods().entrySet()) {
            ThreadLocalProxy proxy = cri.getContextSetterProxy(entry.getValue());
            InjectionUtils.injectThroughMethod(instance, entry.getValue(), proxy);
        }
        
        for (Field f : cri.getContextFields()) {
            ThreadLocalProxy proxy = cri.getContextFieldProxy(f);
            InjectionUtils.injectFieldValue(f, instance, proxy);
        }
    }
    
    @SuppressWarnings("unchecked")
    public static void injectContextField(AbstractResourceInfo cri, 
                                          Field f, Object o, Object value,
                                          boolean resource) {
        if (!cri.isSingleton()) {
            InjectionUtils.injectFieldValue(f, o, value);
        } else {
            ThreadLocalProxy proxy = resource ? cri.getResourceFieldProxy(f)
                                              : cri.getContextFieldProxy(f);
            if (proxy != null) {
                proxy.set(value);
            }
        }
    }
    
    @SuppressWarnings("unchecked")
    public static void injectContextMethods(Object requestObject,
                                            AbstractResourceInfo cri,
                                            Message message) {
        for (Map.Entry<Class<?>, Method> entry : cri.getContextMethods().entrySet()) {
            Object o = JAXRSUtils.createContextValue(message, 
                                              entry.getValue().getGenericParameterTypes()[0],
                                              entry.getKey());
            
            if (o != null) {
                if (!cri.isSingleton()) {
                    InjectionUtils.injectThroughMethod(requestObject, entry.getValue(), o);
                } else {
                    ThreadLocalProxy proxy = cri.getContextSetterProxy(entry.getValue());
                    if (proxy != null) {
                        proxy.set(o);
                    }
                }
                
            }
        }
    }
    
    // TODO : should we have context and resource fields be treated as context fields ?
    
    public static void injectContextFields(Object o,
                                           AbstractResourceInfo cri,
                                           Message m) {
        
        for (Field f : cri.getContextFields()) {
            Object value = JAXRSUtils.createContextValue(m, f.getGenericType(), f.getType());
            InjectionUtils.injectContextField(cri, f, o, value, false);
        }
    }
    
    public static void injectResourceFields(Object o,
                                            AbstractResourceInfo cri,
                                            Message m) {
        
        for (Field f : cri.getResourceFields()) {
            Object value = JAXRSUtils.createResourceValue(m, f.getType());
            InjectionUtils.injectContextField(cri, f, o, value, true);
        }
    }
}
