/*
 * 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.felix.scr.impl.inject;

import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.felix.scr.impl.helper.Coercions;
import org.osgi.framework.Bundle;
import org.osgi.service.component.ComponentException;

public class Annotations
{
    /** Capture all methods defined by the annotation interface */
    private static final Set<Method> ANNOTATION_METHODS = new HashSet<>();
    static
    {
        for(final Method m : Annotation.class.getMethods())
        {
            ANNOTATION_METHODS.add(m);
        }
    }

    /** Constant for the single element method */
    private static final String VALUE_METHOD = "value";

    /** Constant for the prefix constant. */
    private static final String PREFIX_CONSTANT = "PREFIX_";

    /**
     * Check whether the provided type is a single element annotation.
     * A single element annotation has a method named "value" and all
     * other annotation methods must have a default value.
     * @param clazz The provided type
     * @return {@code true} if the type is a single element annotation.
     */
    static public boolean isSingleElementAnnotation(final Class<?> clazz)
    {
        boolean result = false;
        if ( clazz.isAnnotation() )
        {
            result = true;
            boolean hasValue = false;
            for ( final Method method: clazz.getMethods() )
            {
                // filter out methods from Annotation
                boolean isFromAnnotation = false;
                for(final Method objMethod : ANNOTATION_METHODS)
                {
                    if ( objMethod.getName().equals(method.getName())
                      && Arrays.equals(objMethod.getParameterTypes(), method.getParameterTypes()) )
                    {
                        isFromAnnotation = true;
                        break;
                    }
                }
                if ( isFromAnnotation )
                {
                    continue;
                }
                if ( VALUE_METHOD.equals(method.getName()) )
                {
                    hasValue = true;
                    continue;
                }
                if ( method.getDefaultValue() == null )
                {
                    result = false;
                    break;
                }

            }
            if ( result )
            {
                result = hasValue;
            }

        }
        return result;
    }

    static public String getPrefix(Class<?> clazz)
    {
        try
        {
            final Field f = clazz.getField(PREFIX_CONSTANT);
            if ( Modifier.isStatic(f.getModifiers())
                 && Modifier.isPublic(f.getModifiers())
                 && Modifier.isFinal(f.getModifiers())
                 && String.class.isAssignableFrom(f.getType()))
            {
                final Object value = f.get(null);
                if ( value != null )
                {
                    return value.toString();
                }
            }
        }
        catch ( final Exception ignore)
        {
            // ignore
        }
        return null;
    }

    @SuppressWarnings("unchecked")
	static public <T> T toObject(Class<T> clazz, Map<String, Object> props, Bundle b, boolean supportsInterfaces )
    {
        final boolean isSingleElementAnn = isSingleElementAnnotation(clazz);
        final String prefix = getPrefix(clazz);
        final Map<String, Object> m = new HashMap<>();

        final Map<String, Method> complexFields = new HashMap<>();
        for ( final Method method: clazz.getMethods() )
        {
            final String name = method.getName();
            final String mapped;
            if ( isSingleElementAnn && name.equals(VALUE_METHOD) )
            {
                mapped = mapTypeNameToKey(clazz.getSimpleName());
            }
            else
            {
                mapped = mapIdentifierToKey(name);
            }
            final String key = (prefix == null ? mapped : prefix.concat(mapped));

            Object raw = props.get(key);
            Class<?> returnType = method.getReturnType();
            Object cooked;
            if ( returnType.isInterface() || returnType.isAnnotation())
            {
                complexFields.put(key, method);
                continue;
            }

            try
            {
                if (returnType.isArray())
                {
                    Class<?> componentType = returnType.getComponentType();
                    if (componentType.isInterface() || componentType.isAnnotation())
                    {
                        complexFields.put(key, method);
                        continue;
                    }
                    cooked = coerceToArray(componentType, raw, b);
                }
                else
                {
                    cooked = Coercions.coerce(returnType, raw, b);
                }
            }
            catch (ComponentException e)
            {
                cooked = new Invalid(e);
            }
            m.put( name, cooked );
        }
        if (!complexFields.isEmpty())
        {
            if (supportsInterfaces )
            {
                Map<String, List<Map<String, Object>>> nested = extractSubMaps(complexFields.keySet(), props);
                for (Map.Entry<String, Method> entry: complexFields.entrySet())
                {
                    List<Map<String, Object>> proplist = nested.get(entry.getKey());
                    if (proplist == null)
                    {
                    	proplist = Collections.emptyList();
                    }
                    Method method = entry.getValue();
                    Class<?> returnType  = method.getReturnType();
                    if (returnType.isArray())
                    {
                        Class<?> componentType = returnType.getComponentType();
                        Object result = Array.newInstance(componentType, proplist.size());
                        for (int i = 0; i < proplist.size(); i++)
                        {
                            Map<String, Object> rawElement = proplist.get(i);
                            Object cooked = toObject(componentType, rawElement, b, supportsInterfaces);
                            Array.set(result, i, cooked);
                        }
                        m.put(method.getName(), result);
                    }
                    else
                    {
                        if (!proplist.isEmpty())
                        {
                            Object cooked = toObject(returnType, proplist.get(0), b, supportsInterfaces);
                            m.put(method.getName(), cooked);
                        }
                    }
                }
            }
            else
            {
                for (Method method: complexFields.values())
                {
                    m.put(method.getName(), new Invalid("Invalid annotation member type" + method.getReturnType().getName() + " for member: " + method.getName()));
                }
            }
        }

        final InvocationHandler h = new Handler(m, clazz);
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class<?>[] { clazz }, h);
    }

    private static Map<String, List<Map<String, Object>>> extractSubMaps(Collection<String> keys, Map<String, Object> map)
    {
        Map<String, List<Map<String, Object>>> result = new HashMap<>();
        //Form a regexp to recognize all the keys as prefixes in the map keys.
        StringBuilder b = new StringBuilder("(");
        for (String key: keys)
        {
            b.append(key).append("|");
        }
        b.deleteCharAt(b.length() -1);
        b.append(")\\.([0-9]*)\\.(.*)");
        Pattern p = Pattern.compile(b.toString());
        for (Map.Entry<String, Object> entry: map.entrySet())
        {
            String longKey = entry.getKey();
            Matcher m = p.matcher(longKey);
            if (m.matches())
            {
                String key = m.group(1);
                int index = Integer.parseInt(m.group(2));
                String subkey = m.group(3);
                List<Map<String, Object>> subMapsForKey = result.get(key);
                if (subMapsForKey == null)
                {
                    subMapsForKey = new ArrayList<>();
                    result.put(key, subMapsForKey);
                }
                //make sure there is room for the possible new submap
                for (int i = subMapsForKey.size(); i <= index; i++)
                {
                    subMapsForKey.add(new HashMap<String, Object>());
                }
                Map<String, Object> subMap = subMapsForKey.get(index);
                subMap.put(subkey, entry.getValue());
            }
        }
        return result;
    }

    private static Object coerceToArray(Class<?> componentType, Object raw, Bundle bundle)
    {
        if (raw == null)
        {
            return Array.newInstance(componentType, 0);
        }
        if (raw.getClass().isArray())
        {
            int size = Array.getLength(raw);
            Object result = Array.newInstance(componentType, size);
            for (int i = 0; i < size; i++)
            {
                Object rawElement = Array.get(raw, i);
                Object cooked = Coercions.coerce(componentType, rawElement, bundle);
                Array.set(result, i, cooked);
            }
            return result;
        }
        if (raw instanceof Collection)
        {
            @SuppressWarnings("rawtypes")
			Collection raws = (Collection) raw;
            int size = raws.size();
            Object result = Array.newInstance(componentType, size);
            int i = 0;
            for (Object rawElement: raws)
            {
                Object cooked = Coercions.coerce(componentType, rawElement, bundle);
                Array.set(result, i++, cooked);
            }
            return result;

        }
        Object cooked = Coercions.coerce(componentType, raw, bundle);
        Object result = Array.newInstance(componentType, 1);
        Array.set(result, 0, cooked);
        return result;

    }

    private static final Pattern p = Pattern.compile("(\\$_\\$)|(\\$\\$)|(\\$)|(__)|(_)");

    static String mapIdentifierToKey(String name)
    {
        Matcher m = p.matcher(name);
        StringBuffer b = new StringBuffer();
        while (m.find())
        {
            String replacement = "";
            if (m.group(1) != null) replacement = "-";
            if (m.group(2) != null) replacement = "\\$";
            if (m.group(3) != null) replacement = "";
            if (m.group(4) != null) replacement = "_";
            if (m.group(5) != null) replacement = ".";

            m.appendReplacement(b, replacement);
        }
        m.appendTail(b);
        return b.toString();
    }

    static String mapTypeNameToKey(String name)
    {
        final StringBuilder sb = new StringBuilder();
        boolean lastLow = false;
        for(final char c : name.toCharArray())
        {
            if ( lastLow && (Character.isLetter(c) || Character.isDigit(c)) && Character.isUpperCase(c) )
            {
                sb.append('.');
            }
            lastLow = false;
            if ( (Character.isLetter(c) || Character.isDigit(c)) && Character.isLowerCase(c))
            {
                lastLow = true;
            }
            sb.append(Character.toLowerCase(c));
        }
        return sb.toString();
    }

    private final static class Handler implements InvocationHandler
    {
        private final Map<String, Object> values;

        private final Class<?> type;

        public Handler(final Map<String, Object> values, final Class<?> type)
        {
            this.values = values;
            this.type = type;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
        {
            Object value = values.get(method.getName());
            if (value instanceof Invalid)
            {
                throw new ComponentException(((Invalid)value).getMessage());
            }
            if ( value == null )
            {
                // check for methods of the Annotations class like hashCode, toString, equals etc.
                if (method.getName().equals("hashCode") &&
                    method.getParameterTypes().length == 0 )
                {
                    int hashCode = 0;
                    for (final Map.Entry<String, Object> entry : values.entrySet()) {
                        if (value instanceof Invalid) {
                            continue;
                        }
                        hashCode += (127 * entry.getKey().hashCode()) ^ entry.getValue().hashCode();
                    }
                    value = hashCode;
                }
                else if (method.getName().equals("equals")
                         && method.getParameterTypes().length == 1)
                {
                    final Object other = args[0];
                    if (proxy == other)
                    {
                        value = true;
                    }
                    else
                    {
                        value = false;
                        if (type.isInstance(other) && Proxy.isProxyClass(other.getClass()))
                        {
                            final InvocationHandler ih = Proxy.getInvocationHandler(other);
                            if (ih instanceof Handler) {
                                value = ((Handler)ih).values.equals(values);
                            }
                        }
                    }
                }
                else if (method.getName().equals("toString")
                        && method.getParameterTypes().length == 0 )
                {
                    value = type.getName() + " : " + values;
                }
                else if (method.getName().equals("annotationType")
                         && method.getParameterTypes().length == 0 )
                {
                    value = type;
                }
            }
            return value;
        }
    }

    private final static class Invalid
    {
        private final String message;

        public Invalid(ComponentException e)
        {
            this.message = e.getMessage();
        }

        public Invalid(String message)
        {
            this.message = message;
        }

        public String getMessage()
        {
            return message;
        }
    }

}
