/*
 * $Id$
 *
 * License Agreement.
 *
 * Rich Faces - Natural Ajax for Java Server Faces (JSF)
 *
 * Copyright (C) 2007 Exadel, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 2.1 as published by the Free Software Foundation.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
 */
package org.richfaces.cdk.apt.processors;

import java.lang.annotation.Annotation;
import java.util.HashSet;
import java.util.Set;

import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;

import org.richfaces.cdk.CdkException;
import org.richfaces.cdk.annotations.Facet;
import org.richfaces.cdk.annotations.JsfComponent;
import org.richfaces.cdk.apt.SourceUtils;
import org.richfaces.cdk.apt.SourceUtils.BeanProperty;
import org.richfaces.cdk.model.ClassName;
import org.richfaces.cdk.model.ComponentLibrary;
import org.richfaces.cdk.model.ComponentModel;
import org.richfaces.cdk.model.EventModel;
import org.richfaces.cdk.model.FacesId;
import org.richfaces.cdk.model.FacetModel;

/**
 * <p class="changed_added_4_0">
 * That class process component-related annotations such as {@link org.richfaces.cdk.annotations.JsfComponent} or
 * {@link javax.faces.component.FacesComponent} and stores information in model.
 * </p>
 *
 * @author asmirnov@exadel.com
 *
 */
public class ComponentProcessor extends ProcessorBase implements CdkAnnotationProcessor {
    public static final String COMPONENT_FAMILY = "COMPONENT_FAMILY";
    public static final String COMPONENT_TYPE = "COMPONENT_TYPE";

    private Set<Name> processedNames = new HashSet<Name>();

    public void process(Element element, ComponentLibrary library) {
        SourceUtils sourceUtils = getSourceUtils();
        if (sourceUtils.isAnnotationPresent(element, JsfComponent.class)) {
            TypeElement componentElement = (TypeElement) element;
            AnnotationMirror annotation = sourceUtils.getAnnotationMirror(componentElement, JsfComponent.class);

            if (wasComponentProcessed(componentElement)) {
                return;
            }

            // Process class-level annotations.
            ComponentModel component = new ComponentModel();

            // Should that component be generated ?
            setClassNames(componentElement, component, annotation);
            setComponentProperties(componentElement, component, annotation);

            library.getComponents().add(component);

            // Process the second level annotations.
            for (AnnotationMirror subcomponent : sourceUtils.getAnnotationValues(annotation, "components",
                    AnnotationMirror.class)) {
                ComponentModel subcomponentModel = new ComponentModel();
                subcomponentModel.setBaseClass(component.getTargetClass());
                if (!sourceUtils.isDefaultValue(subcomponent, "generate")) {
                    subcomponentModel.setTargetClass(sourceUtils.getAnnotationValue(subcomponent, "generate", ClassName.class));
                    subcomponentModel.setGenerate(true);
                }
                setComponentProperties(null, subcomponentModel, subcomponent);
                setDescription(subcomponentModel, annotation, getDocComment(componentElement));
                library.getComponents().add(subcomponentModel);
            }
        }
    }

    private boolean wasComponentProcessed(TypeElement componentElement) {
        boolean wasProcessed = processedNames.contains(componentElement.getQualifiedName());
        processedNames.add(componentElement.getQualifiedName());
        return wasProcessed;
    }

    /**
     * <p class="changed_added_4_0">
     * process annotation and set component model properties.
     * </p>
     *
     * @param componentElement
     * @param component
     * @param annotation
     * @throws CdkException
     */
    void setComponentProperties(TypeElement componentElement, ComponentModel component, AnnotationMirror annotation)
            throws CdkException {
        SourceUtils sourceUtils = getSourceUtils();

        setComponentType(componentElement, component, annotation);
        setComponeneFamily(componentElement, component, annotation);
        setDescription(component, annotation, getDocComment(componentElement));
        if (!sourceUtils.isDefaultValue(annotation, "renderer")) {
            setRendererType(component, sourceUtils.getAnnotationValue(annotation, "renderer", AnnotationMirror.class));
        }
        processFacets(componentElement, component, annotation);
        processAttributes(componentElement, component, annotation);
        processEvents(componentElement, component, annotation);
        for (TypeMirror atributesInterface : sourceUtils.getAnnotationValues(annotation, "interfaces", TypeMirror.class)) {
            if (TypeKind.DECLARED.equals(atributesInterface.getKind())) {
                processFacetsFromType(sourceUtils.asTypeElement(atributesInterface), component, sourceUtils);
            } else {
                // TODO - record warning.
            }
        }

        setTagInfo(annotation, component);

        // TODO - process @Test annotations.
    }

    private void setRendererType(ComponentModel component, AnnotationMirror annotation) {
        SourceUtils sourceUtils = getSourceUtils();
        if (!sourceUtils.isDefaultValue(annotation, "type")) {
            component.setRendererType(sourceUtils.getAnnotationValue(annotation, "type", FacesId.class));
        }
        if (!sourceUtils.isDefaultValue(annotation, "template")) {
            component.setRendererTemplate(sourceUtils.getAnnotationValue(annotation, "template", String.class));
        }
    }

    void setComponentType(TypeElement componentElement, ComponentModel component, AnnotationMirror annotation) {
        component.setId(FacesId.parseId(getAnnotationPropertyOrConstant(componentElement, annotation, "type", COMPONENT_TYPE)));
    }

    final void processFacets(TypeElement componentElement, ComponentModel component, AnnotationMirror annotation) {
        SourceUtils sourceUtils = getSourceUtils();
        processFacetsFromType(componentElement, component, sourceUtils);
        for (AnnotationMirror facet : sourceUtils.getAnnotationValues(annotation, "facets", AnnotationMirror.class)) {
            if (!sourceUtils.isDefaultValue(facet, "name")) {
                String name = sourceUtils.getAnnotationValue(facet, "name", String.class);
                FacetModel facetModel = component.getOrCreateFacet(name);
                processFacet(facet, facetModel, null);
            } else {
                throw new CdkException("Facet name should be set");
            }
        }
    }

    private void processFacetsFromType(TypeElement componentElement, ComponentModel component, SourceUtils sourceUtils) {
        if (null != componentElement) {
            Set<BeanProperty> properties = sourceUtils.getBeanPropertiesAnnotatedWith(Facet.class, componentElement);

            // TODO - encapsulate attribute builder into utility class.
            for (BeanProperty beanProperty : properties) {
                AnnotationMirror facet = beanProperty.getAnnotationMirror(Facet.class);
                FacetModel facetModel = component.getOrCreateFacet(beanProperty.getName());
                facetModel.setDescription(beanProperty.getDocComment());
                processFacet(facet, facetModel, beanProperty.getDocComment());
                if (!beanProperty.isExists()) {
                    facetModel.setGenerate(true);
                }
            }
        }
    }

    final void processFacet(AnnotationMirror facet, FacetModel facetModel, String docComment) {
        SourceUtils sourceUtils = getSourceUtils();
        if (!sourceUtils.isDefaultValue(facet, "description")) {
            setDescription(facetModel, facet, docComment);
        }
        if (!sourceUtils.isDefaultValue(facet, "generate")) {
            facetModel.setGenerate(sourceUtils.getAnnotationValue(facet, "generate", Boolean.class));
        }
    }

    final void setComponeneFamily(TypeElement componentElement, ComponentModel component, AnnotationMirror annotation) {
        if (null != componentElement) {
            component.setFamily(FacesId.parseId(getAnnotationPropertyOrConstant(componentElement, annotation, "family",
                    COMPONENT_FAMILY)));
        }
    }

    final void processEvents(TypeElement componentElement, ComponentModel component, AnnotationMirror annotation) {
        SourceUtils sourceUtils = getSourceUtils();
        for (AnnotationMirror event : sourceUtils.getAnnotationValues(annotation, "fires", AnnotationMirror.class)) {
            EventModel model = new EventModel();
            sourceUtils.setModelProperty(model, event, "type", "value");
            sourceUtils.setModelProperty(model, event, "listenerInterface", "listener");
            sourceUtils.setModelProperty(model, event, "listenerMethod");
            sourceUtils.setModelProperty(model, event, "sourceInterface", "source");
            component.getEvents().add(model);
        }
    }

    @Override
    public final Class<? extends Annotation> getProcessedAnnotation() {
        return JsfComponent.class;
    }
}
