package org.apache.turbine.annotation;

/*
 * 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.
 */

import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.commons.configuration2.Configuration;
import org.apache.commons.lang3.StringUtils;
import org.apache.fulcrum.pool.PoolException;
import org.apache.fulcrum.pool.PoolService;
import org.apache.fulcrum.security.model.turbine.TurbineAccessControlList;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.turbine.Turbine;
import org.apache.turbine.modules.Loader;
import org.apache.turbine.services.Service;
import org.apache.turbine.services.ServiceManager;
import org.apache.turbine.services.TurbineServices;
import org.apache.turbine.services.assemblerbroker.AssemblerBrokerService;
import org.apache.turbine.util.TurbineException;

/**
 * AnnotationProcessor contains static helper methods that handle the
 * Turbine annotations for objects
 *
 * @author <a href="mailto:tv@apache.org">Thomas Vandahl</a>
 * @version $Id: TurbineAssemblerBrokerService.java 1521103 2013-09-09 13:38:07Z tv $
 */
public class AnnotationProcessor
{
    /** Logging */
    private static final Logger log = LogManager.getLogger(AnnotationProcessor.class);

    /** Annotation cache */
    private static final ConcurrentMap<String, Annotation[]> annotationCache = new ConcurrentHashMap<>();

    /**
     * Get cached annotations for field, class or method
     *
     * @param object a field, class or method
     *
     * @return the declared annotations for the object
     */
    public static Annotation[] getAnnotations(AccessibleObject object)
    {
        String key = object.getClass() + object.toString();
        Annotation[] annotations = annotationCache.get(key);
        if (annotations == null)
        {
            Annotation[] newAnnotations = object.getDeclaredAnnotations();
            annotations = annotationCache.putIfAbsent(key, newAnnotations);
            if (annotations == null)
            {
                annotations = newAnnotations;
            }
        }
        return annotations;
    }

    public enum ConditionType
    {
        COMPOUND, ANY;
    }

    /**
     * Check if the object given is authorized to be executed based on its annotations
     *
     * The method will return false if one of the annotations denies execution
     *
     * @see #isAuthorized(AccessibleObject, TurbineAccessControlList, ConditionType)
     *
     * @param <A> ACL instance
     * @param object accessible object to test
     * @param acl access control list
     * @return true if the execution is allowed
     */
    public static <A extends TurbineAccessControlList<?>> boolean isAuthorized(AccessibleObject object, A acl)
    {
        return isAuthorized( object, acl, ConditionType.COMPOUND );
    }

    /**
     * Check if the object given is authorized to be executed based on its annotations
     * The method's return value depends on the conditonType, refer to the ConditionType
     *
     * @param <A> ACL instance
     * @param object the object
     * @param acl access control list
     * @param conditonType either {@link ConditionType#COMPOUND}: The method will return false if one of the annotations denies execution
     *                     or {@link ConditionType#ANY} : The method will return true if one of the annotations allows execution
     * @return true if the execution is allowed
     */
    public static <A extends TurbineAccessControlList<?>> boolean isAuthorized(AccessibleObject object, A acl, ConditionType conditonType)
    {
        Annotation[] annotations = getAnnotations(object);

        for (Annotation annotation : annotations)
        {
            if (annotation instanceof TurbineRequiredRole)
            {
                TurbineRequiredRole trr = (TurbineRequiredRole) annotation;
                String[] roleNames = trr.value();
                String group = trr.group();

                if (StringUtils.isEmpty(group)) // global group
                {
                    for (String roleName : roleNames)
                    {
                        switch ( conditonType ) {
                            case COMPOUND: default:
                                if (!acl.hasRole(roleName))
                                {
                                    return false;
                                }
                                break;
                            case ANY:
                                if (acl.hasRole(roleName))
                                {
                                    return true;
                                }
                                break;
                        }
                    }
                    if (conditonType == ConditionType.ANY) { // nothing matched
                        return false;
                    }
                }
                else
                {
                    for (String roleName : roleNames)
                    {
                        switch ( conditonType ) {
                            case COMPOUND: default:
                                if (!acl.hasRole(roleName, group))
                                {
                                    return false;
                                }
                                break;
                            case ANY:
                                if (acl.hasRole(roleName, group))
                                {
                                    return true;
                                }
                                break;
                        }
                    }
                }
            }
            else if (annotation instanceof TurbineRequiredPermission)
            {
                TurbineRequiredPermission trp = (TurbineRequiredPermission) annotation;
                String[] permissionNames = trp.value();
                String group = trp.group();

                if (StringUtils.isEmpty(group)) // global group
                {
                    for (String permissionName : permissionNames)
                    {
                        switch ( conditonType ) {
                            case COMPOUND: default:
                                if (!acl.hasPermission(permissionName))
                                {
                                    return false;
                                }
                                break;
                            case ANY:
                                if (acl.hasPermission(permissionName))
                                {
                                    return true;
                                }
                                break;
                        }
                    }
                }
                else
                {
                    for (String permissionName : permissionNames)
                    {
                        switch ( conditonType ) {
                            case COMPOUND: default:
                                if (!acl.hasPermission(permissionName, group))
                                {
                                    return false;
                                }
                                break;
                            case ANY:
                                if (acl.hasPermission(permissionName, group))
                                {
                                    return true;
                                }
                                break;
                        }

                    }
                }
            }
        }

        return true;
    }

    /**
     * Search for annotated fields of the object and inject the appropriate
     * objects
     *
     * @param object the object
     * @throws TurbineException if the objects could not be injected
     */
    public static void process(Object object) throws TurbineException
    {
        process(object, false);
    }

    /**
     * Search for annotated fields and optionally of method fields of the object and inject the appropriate
     * objects
     *
     * @param object the object
     * @param hasTurbineServicesInMethodFields set <code>true </code>, if methods should be parsed
     * @throws TurbineException if the objects could not be injected
     */
    public static void process(Object object, Boolean hasTurbineServicesInMethodFields) throws TurbineException
    {
        ServiceManager manager = null;
        Configuration config = null;
        AssemblerBrokerService assembler = null;
        PoolService pool= null;
        Class<?> clazz = object.getClass();
        
        boolean isTurbineService = false;
        if ( clazz.isAnnotationPresent(TurbineService.class)) {
            TurbineService service = clazz.getAnnotation(TurbineService.class);
            log.debug("retrieved class annotation: "+ service);
            isTurbineService = true;
        } 

        while (clazz != null)
        {
            Field[] fields = clazz.getDeclaredFields();

            for (Field field : fields)
            {
                Annotation[] annotations = getAnnotations(field);

                for (Annotation a : annotations)
                {
                    if (a instanceof TurbineService)
                    {
                        if (manager == null)
                        {
                            manager = TurbineServices.getInstance();
                        }
                        injectTurbineService(object, manager, field, (TurbineService) a);
                    }
                    else if (a instanceof TurbineConfiguration)
                    {
                        if (config == null)
                        {
                            config = Turbine.getConfiguration();
                        }
                        injectTurbineConfiguration(object, config, field, (TurbineConfiguration) a);
                    }
                    else if (a instanceof TurbineLoader)
                    {
                        if (assembler == null)
                        {
                            assembler = (AssemblerBrokerService) TurbineServices.getInstance().
                                getService(AssemblerBrokerService.SERVICE_NAME);
                        }
                        injectTurbineLoader(object, assembler, field, (TurbineLoader) a);
                    }
                    else if (a instanceof TurbineTool)
                    {
                        if (pool == null)
                        {
                            pool = (PoolService)TurbineServices.getInstance()
                                    .getService(PoolService.ROLE);
                        }
                        injectTurbineTool(object, pool, field, (TurbineTool) a);
                    }
                }
                if (isTurbineService)
                {
                    if (field.getType().isAnnotationPresent(TurbineService.class)) {
                        TurbineService service = field.getType().getAnnotation(TurbineService.class);
                        log.debug("retrieved implicit class annotation: "+ service);
                        if (manager == null)
                        {
                            manager = TurbineServices.getInstance();
                        }
                        injectTurbineService(object, manager, field, service);
                    }    
                }
            }

            if (hasTurbineServicesInMethodFields) {
                manager = processMethods(object, manager, clazz, isTurbineService);
            }

            clazz = clazz.getSuperclass();
        }
    }

    private static ServiceManager processMethods(Object object, ServiceManager manager, Class<?> clazz, boolean isTurbineService) throws TurbineException {
        Method[] methods = clazz.getMethods();

        for (Method method : methods)
        {
            Annotation[] annotations = getAnnotations(method);
            for (Annotation a : annotations)
            {
                if (a instanceof TurbineService)
                {

                    if (manager == null)
                    {
                        manager = TurbineServices.getInstance();
                    }
                    injectTurbineService(object, manager, method, (TurbineService) a);
                }
            }
            if (isTurbineService)
            {
                if (manager == null)
                {
                    manager = TurbineServices.getInstance();
                }
                Class<?>[] classes = method.getParameterTypes();
                for (Class<?> c : classes)
                {
                    if ( c.isAnnotationPresent(TurbineService.class)) {
                        TurbineService service = c.getAnnotation(TurbineService.class);
                        log.debug("retrieved implicit service in Turbien service: "+ service);
                        injectTurbineService(object, manager, method, service);
                    } 
                    
                }
            }
        }
        return manager;
    }

    /**
     * Inject Turbine loader into field of object
     *
     * @param object the object to process
     * @param assembler AssemblerBrokerService, provides the loader
     * @param field the field
     * @param annotation the value of the annotation
     *
     * @throws TurbineException if loader cannot be set
     */
    private static void injectTurbineLoader(Object object, AssemblerBrokerService assembler, Field field, TurbineLoader annotation) throws TurbineException
    {
        Loader<?> loader = assembler.getLoader(annotation.value());
        field.setAccessible(true);

        try
        {
            log.debug("Injection of {} into object {}", loader, object);

            field.set(object, loader);
        }
        catch (IllegalArgumentException | IllegalAccessException e)
        {
            throw new TurbineException("Could not inject loader "
                    + loader + " into object " + object, e);
        }
    }
    
    /**
     * Inject Turbine tool into field of object and 
     * injects annotations provided in the tool.
     *
     * @param object the object to process
     * @param pool PoolService, provides the pool
     * @param field the field
     * @param annotation the value of the annotation
     *
     * @throws TurbineException if loader cannot be set
     */
    private static void injectTurbineTool(Object object, PoolService pool, Field field, TurbineTool annotation) throws TurbineException
    {
        Object tool = null;
        try
        {
            tool = pool.getInstance(annotation.value());
            // inject annotations in tool
            process(tool);

            field.setAccessible(true);
            log.debug("Injection of {} into object {}", tool, object);

            field.set(object, tool);
        }
        catch (PoolException | IllegalArgumentException | IllegalAccessException e)
        {
            throw new TurbineException("Could not inject tool "
                    + tool + " into object " + object, e);
        } 
    }

    /**
     * Inject Turbine configuration into field of object
     *
     * @param object the object to process
     * @param conf the configuration to use
     * @param field the field
     * @param annotation the value of the annotation
     *
     * @throws TurbineException if configuration cannot be set
     */
    @SuppressWarnings("boxing")
    private static void injectTurbineConfiguration(Object object, Configuration conf, Field field, TurbineConfiguration annotation) throws TurbineException
    {
        Class<?> type = field.getType();
        String key = annotation.value();

        try
        {
            if (Configuration.class.isAssignableFrom(type))
            {
                final Configuration injectConfiguration;
                // Check for annotation value
                if (StringUtils.isNotEmpty(key))
                {
                    injectConfiguration = conf.subset(key);
                }
                else
                {
                    injectConfiguration = conf;
                }

                log.debug("Injection of {} into object {}", injectConfiguration, object);

                field.setAccessible(true);
                field.set(object, injectConfiguration);
            }
            else if (conf.containsKey(key))
            {
                if ( String.class.isAssignableFrom( type ) )
                {
                    String value = conf.getString(key);
                    log.debug("Injection of key {} into object {}", value, object);

                    field.setAccessible(true);
                    field.set(object, value);
                }
                else if ( Boolean.TYPE.isAssignableFrom( type ) )
                {
                    boolean value = conf.getBoolean(key);
                    log.debug("Injection of key {} into object {}", value, object);

                    field.setAccessible(true);
                    field.setBoolean(object, value);
                }
                else if ( Integer.TYPE.isAssignableFrom( type ) )
                {
                    int value = conf.getInt(key);
                    log.debug("Injection of key {} into object {}", value, object);

                    field.setAccessible(true);
                    field.setInt(object, value);
                }
                else if ( Long.TYPE.isAssignableFrom( type ) )
                {
                    long value = conf.getLong(key);
                    log.debug("Injection of key {} into object {}", value, object);

                    field.setAccessible(true);
                    field.setLong(object, value);
                }
                else if ( Short.TYPE.isAssignableFrom( type ) )
                {
                    short value = conf.getShort(key);
                    log.debug("Injection of key {} into object {}", value, object);

                    field.setAccessible(true);
                    field.setShort(object, value);
                }
                else if ( Long.TYPE.isAssignableFrom( type ) )
                {
                    long value = conf.getLong(key);
                    log.debug("Injection of key {} into object {}", value, object);

                    field.setAccessible(true);
                    field.setLong(object, value);
                }
                else if ( Float.TYPE.isAssignableFrom( type ) )
                {
                    float value = conf.getFloat(key);
                    log.debug("Injection of key {} into object {}", value, object);

                    field.setAccessible(true);
                    field.setFloat(object, value);
                }
                else if ( Double.TYPE.isAssignableFrom( type ) )
                {
                    double value = conf.getDouble(key);
                    log.debug("Injection of key {} into object {}", value, object);

                    field.setAccessible(true);
                    field.setDouble(object, value);
                }
                else if ( Byte.TYPE.isAssignableFrom( type ) )
                {
                    byte value = conf.getByte(key);
                    log.debug("Injection of key {} into object {}", value, object);

                    field.setAccessible(true);
                    field.setByte(object, value);
                }
                else if ( List.class.isAssignableFrom( type ) )
                {
                    List<Object> values = conf.getList(key);
                    log.debug("Injection of key {} into object {}", values, object);

                    field.setAccessible(true);
                    field.set(object, values);
                } else {
                    throw new TurbineException("Could not inject type " + 
                      type + " into object " + object + ". Type "+ type + " not assignable in configuration "
                      + conf + " (allowed: String, Boolean, List, Number Types, "+ Configuration.class.getName() + ").");
                }
            } else {
                field.setAccessible(true);
                Object defaultValue = field.get(object);
                // this should not throw an error as it might be set later from container  e. g. session.timeout 
                // we might check field.get<Type> to show the default value of the field, but this is only a guess, it might be set even later..
                log.info("No key {} of type {} injected into object {}. Field {} is set to default {}.", key, type, object, field.getName(), defaultValue);
            }
        }
        catch (IllegalArgumentException | IllegalAccessException e)
        {
            throw new TurbineException("Could not inject configuration "
                    + conf + " into object " + object, e);
        }
    }

    /**
     * Inject Turbine service into field of object
     *
     * @param object the object to process
     * @param manager the service manager
     * @param field the field
     * @param annotation the value of the annotation
     *
     * @throws TurbineException if service is not available
     */
    private static void injectTurbineService(Object object, ServiceManager manager, Field field, TurbineService annotation) throws TurbineException
    {
        String serviceName = null;
        // Check for annotation value
        if (annotation != null && StringUtils.isNotEmpty(annotation.value()))
        {
            serviceName = annotation.value();
        }
        // Check for fields SERVICE_NAME and ROLE
        else
        { 
            // check field level annotation
            Field[] typeFields = field.getType().getFields();
            serviceName = checkServiceOrRoleInField(serviceName, typeFields);
            // if it is the default Service, we check class level annotation
            if ( (serviceName == null || serviceName.equals(Service.SERVICE_NAME)) &&
                    field.getType().isAnnotationPresent(TurbineService.class)) {
                TurbineService service = field.getType().getAnnotation(TurbineService.class);
                log.debug("retrieved class annotation: "+ service);
                serviceName = service.value();
            } 
        }

        if (StringUtils.isEmpty(serviceName))
        {
            // Try interface class name (e.g. used by Fulcrum)
            serviceName = field.getType().getName();
        }

        log.debug("Looking up service for injection: {} for object {}", serviceName, object);

        Object service = manager.getService(serviceName); // throws Exception on unknown service
        field.setAccessible(true);

        try
        {
            log.debug("Injection of {} into object {}", serviceName, object);

            field.set(object, service);
        }
        catch (IllegalArgumentException | IllegalAccessException e)
        {
            throw new TurbineException("Could not inject service "
                    + serviceName + " into object " + object, e);
        }
    }

    /**
     * Injects Turbine service into method fields 
     * 
     * @param object the object to process
     * @param manager the service manager
     * @param method The method
     * @param annotation the value of the annotation
     * @throws TurbineException - If service could not be injected.
     */
    private static void injectTurbineService(Object object, ServiceManager manager, Method method, TurbineService annotation) throws TurbineException
    {
        String serviceName = null;
        // Check for annotation value
        if (annotation != null && StringUtils.isNotEmpty(annotation.value()))
        {
            serviceName = annotation.value();
        }
        else
        {
            Class<?>[] classes = method.getParameterTypes();
            for (Class<?> c : classes)
            {
                Field[] fields = c.getFields();
                // Check for fields SERVICE_NAME and ROLE
                serviceName = checkServiceOrRoleInField(serviceName, fields);
                
                if ( (serviceName == null || serviceName.equals(Service.SERVICE_NAME)) &&
                        c.isAnnotationPresent(TurbineService.class)) {
                    TurbineService service = c.getAnnotation(TurbineService.class);
                    log.debug("retrieved class annotation: "+ service);
                    serviceName = service.value();
                } 
                
            }
        }

        log.debug("Looking up service for injection: {} for object {}", serviceName, object);
        if (StringUtils.isEmpty(serviceName))
        {
            // Try interface class name
            serviceName = method.getName();
        }

        Object service = manager.getService(serviceName); // throws Exception on unknown service
        method.setAccessible(true);

        try
        {
            log.debug("Injection of {} into object {}", serviceName, object);

            Object[] paramValues = new Object[1];
            paramValues[0] = service;
            method.invoke(object, paramValues);
        }
        catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e)
        {
            throw new TurbineException("Could not inject service "
                    + serviceName + " into object " + object, e);
        }
    }

    private static String checkServiceOrRoleInField(String serviceName, Field[] fields) {
        for (Field f : fields)
            if (TurbineService.SERVICE_NAME.equals(f.getName()))
            {
                try
                {
                    serviceName = (String)f.get(null);
                }
                catch (IllegalArgumentException | IllegalAccessException e)
                {
                    continue;
                }
                break;
            }
            else if (TurbineService.ROLE.equals(f.getName()))
            {
                try
                {
                    serviceName = (String)f.get(null);
                }
                catch (IllegalArgumentException | IllegalAccessException e)
                {
                    continue;
                }
                break;
            }
        return serviceName;
    }
}
