/*
 * 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.sling.models.impl.model;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Type;

import javax.inject.Named;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.sling.models.annotations.Default;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Optional;
import org.apache.sling.models.annotations.Required;
import org.apache.sling.models.annotations.Source;
import org.apache.sling.models.annotations.Via;
import org.apache.sling.models.annotations.ViaProviderType;
import org.apache.sling.models.annotations.via.BeanProperty;
import org.apache.sling.models.impl.ModelAdapterFactory;
import org.apache.sling.models.impl.ReflectionUtil;
import org.apache.sling.models.spi.injectorspecific.InjectAnnotationProcessor;
import org.apache.sling.models.spi.injectorspecific.InjectAnnotationProcessor2;
import org.apache.sling.models.spi.injectorspecific.StaticInjectAnnotationProcessorFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@SuppressWarnings("deprecation")
abstract class AbstractInjectableElement implements InjectableElement {

    private final AnnotatedElement element;
    private final Type type;
    private final String name;
    private final String source;
    private final ViaSpec via;
    private final boolean hasDefaultValue;
    private final Object defaultValue;
    private final boolean isOptional;
    private final boolean isRequired;
    private final DefaultInjectionStrategy injectionStrategy;
    private final DefaultInjectionStrategy defaultInjectionStrategy;

    private static final Logger log = LoggerFactory.getLogger(ModelAdapterFactory.class);

    public AbstractInjectableElement(AnnotatedElement element, Type type, String defaultName,
            StaticInjectAnnotationProcessorFactory[] processorFactories, DefaultInjectionStrategy defaultInjectionStrategy) {
        this.element = element;
        this.type = type;
        InjectAnnotationProcessor2 annotationProcessor = getAnnotationProcessor(element, processorFactories);
        this.name = getName(element, defaultName, annotationProcessor);
        this.source = getSource(element);
        this.via = getVia(element, annotationProcessor);
        this.hasDefaultValue = getHasDefaultValue(element, annotationProcessor);
        this.defaultValue = getDefaultValue(element, type, annotationProcessor);
        this.isOptional = getOptional(element, annotationProcessor);
        this.isRequired = getRequired(element, annotationProcessor);
        this.injectionStrategy = getInjectionStrategy(element, annotationProcessor, defaultInjectionStrategy);
        this.defaultInjectionStrategy = defaultInjectionStrategy;
    }

    private static InjectAnnotationProcessor2 getAnnotationProcessor(AnnotatedElement element, StaticInjectAnnotationProcessorFactory[] processorFactories) {
        for (StaticInjectAnnotationProcessorFactory processorFactory : processorFactories) {
            InjectAnnotationProcessor2 annotationProcessor = processorFactory.createAnnotationProcessor(element);
            if (annotationProcessor != null) {
                return annotationProcessor;
            }
        }
        return null;
    }

    private static String getName(AnnotatedElement element, String defaultName, InjectAnnotationProcessor2 annotationProcessor) {
        String name = null;
        if (annotationProcessor != null) {
            name = annotationProcessor.getName();
        }
        if (name == null) {
            Named namedAnnotation = element.getAnnotation(Named.class);
            if (namedAnnotation != null) {
                name = namedAnnotation.value();
            }
            else {
                name = defaultName;
            }
        }
        return name;
    }

    private static String getSource(AnnotatedElement element) {
        Source source = ReflectionUtil.getAnnotation(element, Source.class);
        if (source != null) {
            return source.value();
        }
        return null;
    }

    private static ViaSpec getVia(AnnotatedElement element, InjectAnnotationProcessor2 annotationProcessor) {
        ViaSpec spec = new ViaSpec();
        if (annotationProcessor != null) {
            spec.via = annotationProcessor.getVia();
        }
        if (spec.via == null) {
            Via viaAnnotation = element.getAnnotation(Via.class);
            if (viaAnnotation != null) {
                spec.via = viaAnnotation.value();
                spec.type = viaAnnotation.type();
            }
        } else {
            // use default type
            spec.type = BeanProperty.class;
        }
        return spec;
    }

    private static boolean getHasDefaultValue(AnnotatedElement element, InjectAnnotationProcessor2 annotationProcessor) {
        if (annotationProcessor != null) {
            return annotationProcessor.hasDefault();
        }
        return element.isAnnotationPresent(Default.class);
    }

    private static Object getDefaultValue(AnnotatedElement element, Type type, InjectAnnotationProcessor2 annotationProcessor) {
        if (annotationProcessor != null && annotationProcessor.hasDefault()) {
            return annotationProcessor.getDefault();
        }

        Default defaultAnnotation = element.getAnnotation(Default.class);
        if (defaultAnnotation == null) {
            return null;
        }

        Object value = null;

        if (type instanceof Class) {
            Class<?> injectedClass = (Class<?>) type;
            if (injectedClass.isArray()) {
                Class<?> componentType = injectedClass.getComponentType();
                if (componentType == String.class) {
                    value = defaultAnnotation.values();
                } else if (componentType == Integer.TYPE) {
                    value = defaultAnnotation.intValues();
                } else if (componentType == Integer.class) {
                    value = ArrayUtils.toObject(defaultAnnotation.intValues());
                } else if (componentType == Long.TYPE) {
                    value = defaultAnnotation.longValues();
                } else if (componentType == Long.class) {
                    value = ArrayUtils.toObject(defaultAnnotation.longValues());
                } else if (componentType == Boolean.TYPE) {
                    value = defaultAnnotation.booleanValues();
                } else if (componentType == Boolean.class) {
                    value = ArrayUtils.toObject(defaultAnnotation.booleanValues());
                } else if (componentType == Short.TYPE) {
                    value = defaultAnnotation.shortValues();
                } else if (componentType == Short.class) {
                    value = ArrayUtils.toObject(defaultAnnotation.shortValues());
                } else if (componentType == Float.TYPE) {
                    value = defaultAnnotation.floatValues();
                } else if (componentType == Float.class) {
                    value = ArrayUtils.toObject(defaultAnnotation.floatValues());
                } else if (componentType == Double.TYPE) {
                    value = defaultAnnotation.doubleValues();
                } else if (componentType == Double.class) {
                    value = ArrayUtils.toObject(defaultAnnotation.doubleValues());
                } else {
                    log.warn("Default values for {} are not supported", componentType);
                }
            } else {
                if (injectedClass == String.class) {
                    value = defaultAnnotation.values().length == 0 ? "" : defaultAnnotation.values()[0];
                } else if (injectedClass == Integer.class) {
                    value = defaultAnnotation.intValues().length == 0 ? 0 : defaultAnnotation.intValues()[0];
                } else if (injectedClass == Long.class) {
                    value = defaultAnnotation.longValues().length == 0 ? 0l : defaultAnnotation.longValues()[0];
                } else if (injectedClass == Boolean.class) {
                    value = defaultAnnotation.booleanValues().length == 0 ? false : defaultAnnotation.booleanValues()[0];
                } else if (injectedClass == Short.class) {
                    value = defaultAnnotation.shortValues().length == 0 ? ((short) 0) : defaultAnnotation.shortValues()[0];
                } else if (injectedClass == Float.class) {
                    value = defaultAnnotation.floatValues().length == 0 ? 0f : defaultAnnotation.floatValues()[0];
                } else if (injectedClass == Double.class) {
                    value = defaultAnnotation.doubleValues().length == 0 ? 0d : defaultAnnotation.doubleValues()[0];
                } else {
                    log.warn("Default values for {} are not supported", injectedClass);
                }
            }
        } else {
            log.warn("Cannot provide default for {}", type);
        }
        return value;
    }

    private static boolean getOptional(AnnotatedElement element, InjectAnnotationProcessor annotationProcessor) {
        if (element.isAnnotationPresent(Optional.class)) {
            return true;
        }
        if (annotationProcessor != null) {
            Boolean optional = annotationProcessor.isOptional();
            if (optional != null) {
                return optional.booleanValue();
            }
        }
        return false;
    }

    private static boolean getRequired(AnnotatedElement element, InjectAnnotationProcessor annotationProcessor) {
        // do not evaluate the injector-specific annotation (those are only considered for optional)
        // even setting optional=false will not make an attribute mandatory
        return element.isAnnotationPresent(Required.class);
    }

    private static DefaultInjectionStrategy getInjectionStrategy(AnnotatedElement element, InjectAnnotationProcessor annotationProcessor, DefaultInjectionStrategy defaultInjectionStrategy) {
        if (annotationProcessor != null) {
            if (annotationProcessor instanceof InjectAnnotationProcessor2) {
                switch (((InjectAnnotationProcessor2)annotationProcessor).getInjectionStrategy()) {
                    case OPTIONAL:
                        return DefaultInjectionStrategy.OPTIONAL;
                    case REQUIRED:
                        return DefaultInjectionStrategy.REQUIRED;
                    case DEFAULT:
                        break;
                }
            }
        }
        return defaultInjectionStrategy;
    }

    @Override
    public final AnnotatedElement getAnnotatedElement() {
        return this.element;
    }

    @Override
    public final Type getType() {
        return type;
    }

    @Override
    public final String getName() {
        return this.name;
    }

    @Override
    public String getSource() {
        return this.source;
    }

    @Override
    public String getVia() {
        return this.via.via;
    }

    @Override
    public Class<? extends ViaProviderType> getViaProviderType() { return this.via.type; }

    @Override
    public boolean hasDefaultValue() {
        return this.hasDefaultValue;
    }

    @Override
    public Object getDefaultValue() {
        return this.defaultValue;
    }

    @Override
    public boolean isOptional(InjectAnnotationProcessor annotationProcessor) {
        DefaultInjectionStrategy injectionStrategy = this.injectionStrategy;
        boolean isOptional = this.isOptional;
        boolean isRequired = this.isRequired;

        // evaluate annotationProcessor (which depends on the adapter)
        if (annotationProcessor != null) {
            isOptional = getOptional(getAnnotatedElement(), annotationProcessor);
            isRequired = getRequired(getAnnotatedElement(), annotationProcessor);
            injectionStrategy = getInjectionStrategy(element, annotationProcessor, defaultInjectionStrategy);
        }
        if (injectionStrategy == DefaultInjectionStrategy.REQUIRED) {
            return isOptional;
        } else {
            return !isRequired;
        }
    }

    private static class ViaSpec {
        String via;
        Class<? extends ViaProviderType> type;
    }

}