/*
 * 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.scrplugin.processing;

import java.lang.reflect.Array;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.AutoDetect;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Modified;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.References;
import org.apache.felix.scr.annotations.Service;
import org.apache.felix.scr.annotations.Services;
import org.apache.felix.scrplugin.SCRDescriptorException;
import org.apache.felix.scrplugin.SCRDescriptorFailureException;
import org.apache.felix.scrplugin.SpecVersion;
import org.apache.felix.scrplugin.annotations.AnnotationProcessor;
import org.apache.felix.scrplugin.annotations.ClassAnnotation;
import org.apache.felix.scrplugin.annotations.FieldAnnotation;
import org.apache.felix.scrplugin.annotations.MethodAnnotation;
import org.apache.felix.scrplugin.annotations.ScannedAnnotation;
import org.apache.felix.scrplugin.annotations.ScannedClass;
import org.apache.felix.scrplugin.description.ClassDescription;
import org.apache.felix.scrplugin.description.ComponentConfigurationPolicy;
import org.apache.felix.scrplugin.description.ComponentDescription;
import org.apache.felix.scrplugin.description.PropertyDescription;
import org.apache.felix.scrplugin.description.PropertyType;
import org.apache.felix.scrplugin.description.PropertyUnbounded;
import org.apache.felix.scrplugin.description.ReferenceCardinality;
import org.apache.felix.scrplugin.description.ReferenceDescription;
import org.apache.felix.scrplugin.description.ReferencePolicy;
import org.apache.felix.scrplugin.description.ReferencePolicyOption;
import org.apache.felix.scrplugin.description.ReferenceStrategy;
import org.apache.felix.scrplugin.description.ServiceDescription;

/**
 * This is the processor for the Apache Felix SCR annotations.
 */
public class SCRAnnotationProcessor implements AnnotationProcessor {

    /**
     * @see org.apache.felix.scrplugin.annotations.AnnotationProcessor#getName()
     */
    public String getName() {
        return "Apache Felix SCR Annotation Processor";
    }

    /**
     * @throws SCRDescriptorException
     * @throws SCRDescriptorFailureException
     * @see org.apache.felix.scrplugin.annotations.AnnotationProcessor#process(org.apache.felix.scrplugin.annotations.ScannedClass, org.apache.felix.scrplugin.description.ClassDescription)
     */
    public void process(final ScannedClass scannedClass, final ClassDescription describedClass)
                    throws SCRDescriptorFailureException, SCRDescriptorException {

        final List<ClassAnnotation> componentTags = scannedClass.getClassAnnotations(Component.class.getName());
        scannedClass.processed(componentTags);

        for (final ClassAnnotation cad : componentTags) {
            describedClass.add(createComponent(cad, scannedClass));
        }

        // search for the component descriptions and use the first one
        final List<ComponentDescription> componentDescs = describedClass.getDescriptions(ComponentDescription.class);
        ComponentDescription found = null;
        if (!componentDescs.isEmpty()) {
            found = componentDescs.get(0);
        }

        if (found != null) {
            final ComponentDescription cd = found;

            // search for methods
            final List<MethodAnnotation> methodTags = scannedClass.getMethodAnnotations(null);
            for (final MethodAnnotation m : methodTags) {
                if (m.getName().equals(Activate.class.getName())) {
                    cd.setActivate(m.getAnnotatedMethod().getName());
                    scannedClass.processed(m);
                } else if (m.getName().equals(Deactivate.class.getName())) {
                    cd.setDeactivate(m.getAnnotatedMethod().getName());
                    scannedClass.processed(m);
                } else if (m.getName().equals(Modified.class.getName())) {
                    cd.setModified(m.getAnnotatedMethod().getName());
                    scannedClass.processed(m);
                }
            }

        }

        // service tags
        final List<ClassAnnotation> allServiceTags = new ArrayList<ClassAnnotation>();
        final List<ClassAnnotation> serviceTags = scannedClass.getClassAnnotations(Service.class.getName());
        if (serviceTags.size() > 0) {
            scannedClass.processed(serviceTags);
            allServiceTags.addAll(serviceTags);
        }
        // services tags - class level
        final List<ClassAnnotation> servicesTags = scannedClass.getClassAnnotations(Services.class.getName());
        if (servicesTags.size() > 0) {
            scannedClass.processed(servicesTags);
            for (final ClassAnnotation cad : servicesTags) {
                final ClassAnnotation[] values = (ClassAnnotation[]) cad.getValue("value");
                if (values != null) {
                    allServiceTags.addAll(Arrays.asList(values));
                }
            }
        }
        if (allServiceTags.size() > 0) {
            describedClass.add(createService(allServiceTags, scannedClass));
        }

        // references - class level
        final List<ClassAnnotation> referencesClassTags = scannedClass.getClassAnnotations(References.class.getName());
        scannedClass.processed(referencesClassTags);
        for (final ClassAnnotation cad : referencesClassTags) {
            final ClassAnnotation[] values = (ClassAnnotation[]) cad.getValue("value");
            if (values != null) {
                createReferences(Arrays.asList(values), describedClass);
            }
        }

        // reference - class level
        final List<ClassAnnotation> refClassTags = scannedClass.getClassAnnotations(Reference.class.getName());
        scannedClass.processed(refClassTags);
        createReferences(refClassTags, describedClass);

        // reference - field level
        final List<FieldAnnotation> refFieldTags = scannedClass.getFieldAnnotations(Reference.class.getName());
        scannedClass.processed(refFieldTags);
        createReferences(refFieldTags, describedClass);

        // properties - class level
        final List<ClassAnnotation> propsClassTags = scannedClass.getClassAnnotations(Properties.class.getName());
        scannedClass.processed(propsClassTags);
        for (final ClassAnnotation cad : propsClassTags) {
            final ClassAnnotation[] values = (ClassAnnotation[]) cad.getValue("value");
            if (values != null) {
                createProperties(Arrays.asList(values), describedClass);
            }
        }

        // property - class level
        final List<ClassAnnotation> propClassTags = scannedClass.getClassAnnotations(Property.class.getName());
        scannedClass.processed(propClassTags);
        createProperties(propClassTags, describedClass);

        // property - field level
        final List<FieldAnnotation> propFieldTags = scannedClass.getFieldAnnotations(Property.class.getName());
        scannedClass.processed(propFieldTags);
        createProperties(propFieldTags, describedClass);
    }

    /**
     * @see org.apache.felix.scrplugin.annotations.AnnotationProcessor#getRanking()
     */
    public int getRanking() {
        return 1000;
    }

    /**
     * Create a component description.
     *
     * @param cad
     *            The component annotation for the class.
     * @param scannedClass
     *            The scanned class.
     */
    private ComponentDescription createComponent(final ClassAnnotation cad, final ScannedClass scannedClass) {
        final ComponentDescription component = new ComponentDescription(cad);
        final boolean classIsAbstract = Modifier.isAbstract(scannedClass.getScannedClass().getModifiers());
        component.setAbstract(cad.getBooleanValue("componentAbstract", classIsAbstract));

        component.setCreatePid(cad.getBooleanValue("createPid", true));

        component.setName(cad.getStringValue("name", scannedClass.getScannedClass().getName()));

        component.setLabel(cad.getStringValue("label", null));
        component.setDescription(cad.getStringValue("description", null));

        component.setCreateDs(cad.getBooleanValue("ds", true));

        component.setCreateMetatype(cad.getBooleanValue("metatype", false));

        if (cad.getValue("enabled") != null) {
            component.setEnabled(cad.getBooleanValue("enabled", true));
        }
        if (cad.getValue("specVersion") != null) {
            component.setSpecVersion(SpecVersion.fromName(cad.getValue("specVersion").toString()));
        }
        component.setFactory(cad.getStringValue("factory", null));
        // FELIX-593: immediate attribute does not default to true all the
        // times hence we only set it if declared in the tag
        if (cad.getValue("immediate") != null) {
            component.setImmediate(cad.getBooleanValue("immediate", false));
        }
        component.setInherit(cad.getBooleanValue("inherit", true));
        component.setConfigurationPolicy(ComponentConfigurationPolicy.valueOf(cad.getEnumValue("policy",
                        ComponentConfigurationPolicy.OPTIONAL.name())));
        component.setSetMetatypeFactoryPid(cad.getBooleanValue("configurationFactory", false));

        // Version 1.2
        component.setConfigurationPid(cad.getStringValue("configurationPid", null));

        return component;
    }

    /**
     * Create a service description
     *
     * @param descs
     *            The service annotations.
     * @param scannedClass
     *            The scanned class
     */
    private ServiceDescription createService(final List<ClassAnnotation> descs, final ScannedClass scannedClass) {
        final ServiceDescription service = new ServiceDescription(descs.get(0));

        final List<String> listedInterfaces = new ArrayList<String>();
        for (final ClassAnnotation d : descs) {
            if (d.getBooleanValue("serviceFactory", false)) {
                service.setServiceFactory(true);
            }
            if (d.getValue("value") != null) {
                final String[] interfaces = (String[]) d.getValue("value");
                for (String t : interfaces) {
                    listedInterfaces.add(t);
                }
            }
        }

        if (listedInterfaces.size() > 0 && !listedInterfaces.contains(AutoDetect.class.getName())) {
            for (final String i : listedInterfaces) {
                service.addInterface(i);
            }
        } else {
            // auto detection
            addInterfaces(service, scannedClass.getScannedClass());
        }

        return service;
    }

    /**
     * Recursively add interfaces to the service.
     */
    private void addInterfaces(final ServiceDescription service, final Class<?> javaClass) {
        if (javaClass != null) {
            final Class<?>[] interfaces = javaClass.getInterfaces();
            for (final Class<?> i : interfaces) {
                service.addInterface(i.getName());
                // recursivly add interfaces implemented by this interface
                this.addInterfaces(service, i);
            }

            // try super class
            this.addInterfaces(service, javaClass.getSuperclass());
        }
    }

    /**
     * Create reference descriptions
     *
     * @param descs
     *            List of reference annotations.s
     * @param describedClass
     *            The described class.
     */
    private void createReferences(final List<? extends ScannedAnnotation> descs, final ClassDescription describedClass) {
        for (final ScannedAnnotation ad : descs) {
            final ReferenceDescription ref = new ReferenceDescription(ad);

            // check for field annotation
            final FieldAnnotation fieldAnnotation;
            if (ad instanceof FieldAnnotation) {
                fieldAnnotation = (FieldAnnotation) ad;
                ref.setField(fieldAnnotation.getAnnotatedField());
            } else {
                fieldAnnotation = null;
            }

            ref.setName(ad.getStringValue("name",
                            (fieldAnnotation != null ? fieldAnnotation.getAnnotatedField().getName() : null)));
            String defaultInterfaceName = null;
            if ( fieldAnnotation != null ) {
                if ( fieldAnnotation.getAnnotatedField().getType().isArray() ) {
                    defaultInterfaceName = fieldAnnotation.getAnnotatedField().getType().getComponentType().getName();
                } else {
                    defaultInterfaceName = fieldAnnotation.getAnnotatedField().getType().getName();
                }
            }
            ref.setInterfaceName(ad.getStringValue("referenceInterface", defaultInterfaceName));
            ref.setTarget(ad.getStringValue("target", null));
            ref.setCardinality(ReferenceCardinality.valueOf(ad.getEnumValue("cardinality",
                            ReferenceCardinality.MANDATORY_UNARY.name())));
            ref.setPolicy(ReferencePolicy.valueOf(ad.getEnumValue("policy", ReferencePolicy.STATIC.name())));
            ref.setPolicyOption(ReferencePolicyOption.valueOf(ad.getEnumValue("policyOption", ReferencePolicyOption.RELUCTANT.name())));
            ref.setStrategy(ReferenceStrategy.valueOf(ad.getEnumValue("strategy", ReferenceStrategy.EVENT.name())));

            ref.setBind(ad.getStringValue("bind", null));
            ref.setUnbind(ad.getStringValue("unbind", null));
            ref.setUpdated(ad.getStringValue("updated", null));

            describedClass.add(ref);
        }
    }

    private static final String[] PROPERTY_VALUE_PROCESSING = new String[] { "String", "value", "String", "classValue", "Long",
            "longValue", "Double", "doubleValue", "Float", "floatValue", "Integer", "intValue", "Byte", "byteValue", "Char",
            "charValue", "Boolean", "boolValue", "Short", "shortValue", "Password", "passwordValue" };

    /**
     * Create properties descriptions
     *
     * @throws SCRDescriptorException
     * @throws SCRDescriptorFailureException
     */
    private void createProperties(final List<? extends ScannedAnnotation> descs, final ClassDescription describedClass)
                    throws SCRDescriptorFailureException, SCRDescriptorException {
        for (final ScannedAnnotation ad : descs) {
            final PropertyDescription prop = new PropertyDescription(ad);

            // check for field annotation
            final FieldAnnotation fieldAnnotation;
            if (ad instanceof FieldAnnotation) {
                fieldAnnotation = (FieldAnnotation) ad;
            } else {
                fieldAnnotation = null;
            }

            // Detect values from annotation
            String type = null;
            String[] values = null;
            int index = 0;
            while (type == null && index < PROPERTY_VALUE_PROCESSING.length) {
                final String propType = PROPERTY_VALUE_PROCESSING[index];
                final String propName = PROPERTY_VALUE_PROCESSING[index + 1];
                final Object propValue = ad.getValue(propName);
                if (propValue != null && propValue.getClass().isArray()) {
                    type = propType;
                    values = new String[Array.getLength(propValue)];
                    for (int i = 0; i < values.length; i++) {
                        values[i] = Array.get(propValue, i).toString();
                    }
                }
                index += 2;
            }

            String name = ad.getStringValue("name", null);

            if (values != null) {
                prop.setType(PropertyType.valueOf(type));
                if (values.length == 1) {
                    prop.setValue(values[0]);
                } else {
                    prop.setMultiValue(values);
                }
                if ( name == null ) {
                    final Object value = fieldAnnotation.getAnnotatedFieldValue();
                    if (value != null) {
                        name = value.toString();
                    }
                }

            } else if (fieldAnnotation != null) {
                // Detect values from field
                if ( name != null ) {
                    final Object value = fieldAnnotation.getAnnotatedFieldValue();
                    if (value != null) {
                        if (value.getClass().isArray()) {
                            final String[] newValues = new String[Array.getLength(value)];
                            for (int i = 0; i < newValues.length; i++) {
                                newValues[i] = Array.get(value, i).toString();
                            }
                            prop.setMultiValue(newValues);
                            prop.setType(PropertyType.from(fieldAnnotation.getAnnotatedField().getType().getComponentType()));
                        } else {
                            prop.setType(PropertyType.from(value.getClass()));
                            prop.setValue(value.toString());
                        }
                    }
                } else {
                    final Object value = fieldAnnotation.getAnnotatedFieldValue();
                    if (value != null) {
                        name = value.toString();
                    }
                }
            }

            prop.setName(name);
            prop.setLabel(ad.getStringValue("label", null));
            prop.setDescription(ad.getStringValue("description", null));

            // check type
            if ( prop.getType() == null ) {
                prop.setType(PropertyType.String);
            }

            // private
            if ( ad.getValue("propertyPrivate") != null ) {
                prop.setPrivate(ad.getBooleanValue("propertyPrivate", false));
            }

            // cardinality handling
            final PropertyUnbounded pu = PropertyUnbounded
                            .valueOf(ad.getEnumValue("unbounded", PropertyUnbounded.DEFAULT.name()));
            prop.setUnbounded(pu);

            if (pu == PropertyUnbounded.DEFAULT) {
                prop.setCardinality(ad.getIntegerValue("cardinality", 0));
                if (prop.getMultiValue() != null && prop.getCardinality() == 0) {
                    prop.setUnbounded(PropertyUnbounded.ARRAY);
                }
            } else {
                prop.setCardinality(0);
            }

            if ( prop.getValue() != null ) {
                if ( prop.getUnbounded() == PropertyUnbounded.ARRAY || prop.getUnbounded() == PropertyUnbounded.VECTOR ) {
                    prop.setMultiValue(new String[] {prop.getValue()});
                } else if ( prop.getCardinality() < -1 || prop.getCardinality() > 1 ) {
                    prop.setMultiValue(new String[] {prop.getValue()});
                }
            }
            // options
            final ScannedAnnotation[] options = (ScannedAnnotation[])ad.getValue("options");
            if (options != null) {
                final List<String> propertyOptions = new ArrayList<String>();
                for(final ScannedAnnotation po : options) {
                    propertyOptions.add(po.getStringValue("name", ""));
                    propertyOptions.add(po.getStringValue("value", ""));
                }
                prop.setOptions(propertyOptions.toArray(new String[propertyOptions.size()]));
            }

            describedClass.add(prop);
        }
    }
}
