package org.jboss.windup.rules.apps.javaee.rules;


import org.apache.commons.lang3.StringUtils;
import org.jboss.forge.furnace.util.Strings;
import org.jboss.windup.ast.java.data.TypeReferenceLocation;
import org.jboss.windup.config.AbstractRuleProvider;
import org.jboss.windup.config.GraphRewrite;
import org.jboss.windup.config.loader.RuleLoaderContext;
import org.jboss.windup.config.metadata.RuleMetadata;
import org.jboss.windup.config.operation.iteration.AbstractIterationOperation;
import org.jboss.windup.config.phase.InitialAnalysisPhase;
import org.jboss.windup.graph.model.ProjectModel;
import org.jboss.windup.graph.service.GraphService;
import org.jboss.windup.graph.service.Service;
import org.jboss.windup.config.projecttraversal.ProjectTraversalCache;
import org.jboss.windup.rules.apps.java.condition.JavaClass;
import org.jboss.windup.rules.apps.java.model.AbstractJavaSourceModel;
import org.jboss.windup.rules.apps.java.model.JavaClassModel;
import org.jboss.windup.rules.apps.java.scan.ast.JavaTypeReferenceModel;
import org.jboss.windup.rules.apps.java.scan.ast.annotations.JavaAnnotationListTypeValueModel;
import org.jboss.windup.rules.apps.java.scan.ast.annotations.JavaAnnotationLiteralTypeValueModel;
import org.jboss.windup.rules.apps.java.scan.ast.annotations.JavaAnnotationTypeReferenceModel;
import org.jboss.windup.rules.apps.java.scan.ast.annotations.JavaAnnotationTypeValueModel;
import org.jboss.windup.rules.apps.java.scan.ast.AnalyzeJavaFilesRuleProvider;
import org.jboss.windup.rules.apps.javaee.model.EjbMessageDrivenModel;
import org.jboss.windup.rules.apps.javaee.model.EjbSessionBeanModel;
import org.jboss.windup.rules.apps.javaee.service.JmsDestinationService;
import org.ocpsoft.rewrite.config.Configuration;
import org.ocpsoft.rewrite.config.ConfigurationBuilder;
import org.ocpsoft.rewrite.context.EvaluationContext;

import java.util.Set;

/**
 * Scans for classes with EJB related annotations, and adds EJB related metadata for these.
 */
@RuleMetadata(phase = InitialAnalysisPhase.class, after = AnalyzeJavaFilesRuleProvider.class)
public class DiscoverEjbAnnotationsRuleProvider extends AbstractRuleProvider
{
    @Override
    public Configuration getConfiguration(RuleLoaderContext ruleLoaderContext)
    {

        String ruleIDPrefix = getClass().getSimpleName();
        return ConfigurationBuilder.begin()
                    .addRule()
                    .when(JavaClass.references("javax.ejb.{annotationType}").at(TypeReferenceLocation.ANNOTATION))
                    .perform(new AbstractIterationOperation<JavaTypeReferenceModel>()
                    {
                        public void perform(GraphRewrite event, EvaluationContext context, JavaTypeReferenceModel payload)
                        {
                            extractEJBMetadata(event, payload);
                        }
                    })
                    .where("annotationType").matches("Stateless|Stateful")
                    .withId(ruleIDPrefix + "_StatelessAndStatefulRule")
                    .addRule()
                    .when(JavaClass.references("javax.ejb.MessageDriven").at(TypeReferenceLocation.ANNOTATION))
                    .perform(new AbstractIterationOperation<JavaTypeReferenceModel>()
                    {
                        @Override
                        public void perform(GraphRewrite event, EvaluationContext context, JavaTypeReferenceModel payload)
                        {
                            extractMessageDrivenMetadata(event, payload);
                        }
                    })
                    .withId(ruleIDPrefix + "_MessageDrivenRule");
    }

    private String getAnnotationLiteralValue(JavaAnnotationTypeReferenceModel model, String name)
    {
        JavaAnnotationTypeValueModel valueModel = model.getAnnotationValues().get(name);

        if (valueModel instanceof JavaAnnotationLiteralTypeValueModel)
        {
            JavaAnnotationLiteralTypeValueModel literalTypeValue = (JavaAnnotationLiteralTypeValueModel) valueModel;
            return literalTypeValue.getLiteralValue();
        }
        else
        {
            return null;
        }
    }

    private void extractEJBMetadata(GraphRewrite event, JavaTypeReferenceModel javaTypeReference)
    {
        javaTypeReference.getFile().setGenerateSourceReport(true);
        JavaAnnotationTypeReferenceModel annotationTypeReference = (JavaAnnotationTypeReferenceModel) javaTypeReference;

        JavaClassModel ejbClass = getJavaClass(javaTypeReference);

        String ejbName = getAnnotationLiteralValue(annotationTypeReference, "name");
        if (Strings.isNullOrEmpty(ejbName))
        {
            ejbName = ejbClass.getClassName();
        }

        String sessionType = javaTypeReference.getResolvedSourceSnippit()
                    .substring(javaTypeReference.getResolvedSourceSnippit().lastIndexOf(".") + 1);

        Service<EjbSessionBeanModel> sessionBeanService = new GraphService<>(event.getGraphContext(), EjbSessionBeanModel.class);
        EjbSessionBeanModel sessionBean = sessionBeanService.create();

        Set<ProjectModel> applications = ProjectTraversalCache.getApplicationsForProject(event.getGraphContext(), javaTypeReference.getFile().getProjectModel());
        sessionBean.setApplications(applications);
        sessionBean.setBeanName(ejbName);
        sessionBean.setEjbClass(ejbClass);
        sessionBean.setSessionType(sessionType);
    }

    private void extractMessageDrivenMetadata(GraphRewrite event, JavaTypeReferenceModel javaTypeReference)
    {
        javaTypeReference.getFile().setGenerateSourceReport(true);
        JavaAnnotationTypeReferenceModel annotationTypeReference = (JavaAnnotationTypeReferenceModel) javaTypeReference;

        JavaClassModel ejbClass = getJavaClass(javaTypeReference);

        String ejbName = getAnnotationLiteralValue(annotationTypeReference, "name");
        if (Strings.isNullOrEmpty(ejbName))
        {
            ejbName = ejbClass.getClassName();
        }

        JavaAnnotationTypeValueModel activationConfigAnnotation = annotationTypeReference.getAnnotationValues().get("activationConfig");

        String destination = getAnnotationLiteralValue(annotationTypeReference, "mappedName");
        if (StringUtils.isBlank(destination))
        {
            destination = getDestinationFromActivationConfig(activationConfigAnnotation);
        }

        Service<EjbMessageDrivenModel> messageDrivenService = new GraphService<>(event.getGraphContext(), EjbMessageDrivenModel.class);
        EjbMessageDrivenModel messageDrivenBean = messageDrivenService.create();
        Set<ProjectModel> applications = ProjectTraversalCache.getApplicationsForProject(event.getGraphContext(), javaTypeReference.getFile().getProjectModel());
        messageDrivenBean.setApplications(applications);
        messageDrivenBean.setBeanName(ejbName);
        messageDrivenBean.setEjbClass(ejbClass);

        if (StringUtils.isNotBlank(destination))
        {
            String destinationType = getPropertyFromActivationConfig(activationConfigAnnotation, "destinationType");

            JmsDestinationService jmsDestinationService = new JmsDestinationService(event.getGraphContext());
            messageDrivenBean.setDestination(jmsDestinationService.createUnique(applications, destination, destinationType));
        }
    }

    private String getDestinationFromActivationConfig(JavaAnnotationTypeValueModel annotationTypeReferenceModel)
    {
        return this.getPropertyFromActivationConfig(annotationTypeReferenceModel, "destination");
    }

    private String getPropertyFromActivationConfig(JavaAnnotationTypeValueModel annotationTypeReferenceModel, String property)
    {
        if (property == null)
        {
            throw new IllegalArgumentException("property cannot be null");
        }

        if (annotationTypeReferenceModel == null)
        {
            return null;
        }

        if (annotationTypeReferenceModel instanceof JavaAnnotationListTypeValueModel)
        {
            for (JavaAnnotationTypeValueModel activationConfig : (JavaAnnotationListTypeValueModel) annotationTypeReferenceModel)
            {
                if (!(activationConfig instanceof JavaAnnotationTypeReferenceModel))
                {
                    continue;
                }

                JavaAnnotationTypeReferenceModel javaAnnotationTypeReferenceModel = (JavaAnnotationTypeReferenceModel) activationConfig;
                String propertyValue = getPropertyFromActivationConfig(javaAnnotationTypeReferenceModel, property);
                if (propertyValue != null)
                {
                    return propertyValue;
                }
            }
            return null;
        }
        else if (annotationTypeReferenceModel instanceof JavaAnnotationTypeReferenceModel)
        {
            JavaAnnotationTypeReferenceModel javaAnnotationTypeReferenceModel = (JavaAnnotationTypeReferenceModel) annotationTypeReferenceModel;
            JavaAnnotationTypeValueModel propertyNameModel = javaAnnotationTypeReferenceModel.getAnnotationValues().get("propertyName");
            JavaAnnotationTypeValueModel propertyValueModel = javaAnnotationTypeReferenceModel.getAnnotationValues().get("propertyValue");

            if (propertyNameModel instanceof JavaAnnotationLiteralTypeValueModel
                        && propertyValueModel instanceof JavaAnnotationLiteralTypeValueModel)
            {
                String propertyName = ((JavaAnnotationLiteralTypeValueModel) propertyNameModel).getLiteralValue();
                String propertyValue = ((JavaAnnotationLiteralTypeValueModel) propertyValueModel).getLiteralValue();
                if (property.equals(propertyName))
                {
                    return propertyValue;
                }
            }
            return null;
        }
        else
        {
            return null;
        }
    }

    private JavaClassModel getJavaClass(JavaTypeReferenceModel javaTypeReference)
    {
        JavaClassModel result = null;
        AbstractJavaSourceModel javaSource = javaTypeReference.getFile();
        for (JavaClassModel javaClassModel : javaSource.getJavaClasses())
        {
            // there can be only one public one, and the annotated class should be public
            if (javaClassModel.isPublic() != null && javaClassModel.isPublic())
            {
                result = javaClassModel;
                break;
            }
        }

        if (result == null)
        {
            // no public classes found, so try to find any class (even non-public ones)
            result = javaSource.getJavaClasses().iterator().next();
        }
        return result;
    }

    @Override
    public String toString()
    {
        return "DiscoverEJBAnnotatedClasses";
    }
}
