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

import static org.joox.JOOX.$;
import static org.joox.JOOX.attr;

import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang3.StringUtils;
import org.jboss.windup.config.GraphRewrite;
import org.jboss.windup.config.metadata.RuleMetadata;
import org.jboss.windup.config.phase.InitialAnalysisPhase;
import org.jboss.windup.config.query.Query;
import org.jboss.windup.config.ruleprovider.IteratingRuleProvider;
import org.jboss.windup.graph.model.ProjectModel;
import org.jboss.windup.graph.service.GraphService;
import org.jboss.windup.config.projecttraversal.ProjectTraversalCache;
import org.jboss.windup.reporting.model.TechnologyTagLevel;
import org.jboss.windup.reporting.service.TechnologyTagService;
import org.jboss.windup.rules.apps.java.model.JavaClassModel;
import org.jboss.windup.rules.apps.java.service.JavaClassService;
import org.jboss.windup.rules.apps.javaee.model.JNDIResourceModel;
import org.jboss.windup.rules.apps.javaee.model.SpringBeanModel;
import org.jboss.windup.rules.apps.javaee.model.SpringConfigurationFileModel;
import org.jboss.windup.rules.apps.javaee.model.association.JNDIReferenceModel;
import org.jboss.windup.rules.apps.javaee.service.JNDIResourceService;
import org.jboss.windup.rules.apps.javaee.service.SpringBeanService;
import org.jboss.windup.rules.apps.javaee.service.SpringConfigurationFileService;
import org.jboss.windup.rules.apps.xml.model.XmlFileModel;
import org.jboss.windup.rules.apps.xml.service.XmlFileService;
import org.ocpsoft.rewrite.config.ConditionBuilder;
import org.ocpsoft.rewrite.context.EvaluationContext;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * Discovers spring configuration XML files, and places the metadata into the Graph.
 *
 * @author <a href="mailto:jesse.sightler@gmail.com">Jesse Sightler</a>
 * @author <a href="mailto:bradsdavis@gmail.com">Brad Davis</a>
 */
@RuleMetadata(phase = InitialAnalysisPhase.class, perform = "Discover Spring Config Files")
public class DiscoverSpringConfigurationFilesRuleProvider extends IteratingRuleProvider<XmlFileModel> {
    private static final Logger LOG = Logger.getLogger(DiscoverSpringConfigurationFilesRuleProvider.class.getSimpleName());

    private static final String TECH_TAG = "Spring XML";
    private static final TechnologyTagLevel TECH_TAG_LEVEL = TechnologyTagLevel.IMPORTANT;

    @Override
    public ConditionBuilder when() {
        return Query.fromType(XmlFileModel.class).withProperty(XmlFileModel.ROOT_TAG_NAME, "beans");
    }

    @Override
    public void perform(GraphRewrite event, EvaluationContext context, XmlFileModel payload) {
        JavaClassService javaClassService = new JavaClassService(event.getGraphContext());
        XmlFileService xmlFileService = new XmlFileService(event.getGraphContext());
        TechnologyTagService technologyTagService = new TechnologyTagService(event.getGraphContext());
        SpringConfigurationFileService springConfigurationFileService = new SpringConfigurationFileService(
                event.getGraphContext());
        SpringBeanService springBeanService = new SpringBeanService(event.getGraphContext());
        JNDIResourceService jndiResourceService = new JNDIResourceService(event.getGraphContext());

        Document doc = xmlFileService.loadDocumentQuiet(event, context, payload);
        if (doc == null) {
            // skip if the xml failed to load
            return;
        }
        List<Element> beansElements = $(doc).namespace("s", "http://www.springframework.org/schema/beans")
                .xpath("/s:beans").get();

        if (beansElements.isEmpty()) {
            LOG.log(Level.WARNING, "Found [beans] XML without namespace at: " + payload.getFilePath() + ".");
            return;
        }

        technologyTagService.addTagToFileModel(payload, TECH_TAG, TECH_TAG_LEVEL);

        Element element = beansElements.get(0);
        SpringConfigurationFileModel springConfigurationModel = springConfigurationFileService
                .addTypeToModel(payload);

        Set<ProjectModel> applications = ProjectTraversalCache.getApplicationsForProject(event.getGraphContext(), payload.getProjectModel());

        // create bean models
        List<Element> beans = $(element).children("bean").get();
        for (Element bean : beans) {
            String clz = $(bean).attr("class");
            String id = $(bean).attr("id");
            String name = $(bean).attr("name");

            if (StringUtils.isBlank(id) && StringUtils.isNotBlank(name)) {
                id = name;
            }
            if (StringUtils.isBlank(clz)) {
                LOG.log(Level.WARNING, "Spring Bean did not include class:" + $(bean).toString());
                continue;
            }

            SpringBeanModel springBeanRef = springBeanService.create();
            springBeanRef.setApplications(applications);

            if (StringUtils.isNotBlank(id)) {
                springBeanRef.setSpringBeanName(id);
            }

            JavaClassModel classReference = javaClassService.getOrCreatePhantom(clz);
            springBeanRef.setJavaClass(classReference);

            springConfigurationModel.addSpringBeanReference(springBeanRef);

            // find out if the class is a JndiObjectFactoryBean, extract the JNDI & type the JNDI resource, when expectedType property is provided.
            /*
             * <bean id="beanDataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"
             * value="jdbc/ExampleSpringBeanDataSource"/> <property name="expectedType" value="javax.sql.DataSource"/> </bean>
             */
            if (StringUtils.isNotBlank(clz) && StringUtils.equals("org.springframework.jndi.JndiObjectFactoryBean", clz)) {
                String expectedType = $(bean).children("property").filter(attr("name", "expectedType")).first().attr("value");
                String jndiName = $(bean).children("property").filter(attr("name", "jndiName")).first().attr("value");

                LOG.info("Found JNDI in Bean Spring: " + jndiName);
                if (StringUtils.isNotBlank(jndiName)) {
                    JNDIResourceModel jndiResource = jndiResourceService.createUnique(applications, jndiName);
                    if (StringUtils.isNotBlank(expectedType)) {
                        LOG.info(" -- Type: " + expectedType);
                        jndiResourceService.associateTypeJndiResource(jndiResource, expectedType);
                    }

                    JNDIReferenceModel reference = GraphService.addTypeToModel(event.getGraphContext(), springBeanRef, JNDIReferenceModel.class);
                    reference.setJndiReference(jndiResource);
                }
            }
        }

        // extract map elements
        /*
         * <util:map id="exMap">
         * <entry key="ex" value="val"/>
         * </util:map>
         */
        for (Element map : $(element).children("map").get()) {
            String id = $(map).attr("id");

            SpringBeanModel springBeanRef = springBeanService.create();
            springBeanRef.setSpringBeanName(id);
            springBeanRef.setApplications(applications);
            springConfigurationModel.addSpringBeanReference(springBeanRef);

            JavaClassModel classReference = javaClassService.getOrCreatePhantom("java.util.Map");
            springBeanRef.setJavaClass(classReference);
        }

        // extract map elements
        /*
         * <util:set id="exSet">
         * <value>ex</value>
         * </util:set>
         */
        for (Element map : $(element).children("set").get()) {
            String id = $(map).attr("id");

            SpringBeanModel springBeanRef = springBeanService.create();
            springBeanRef.setSpringBeanName(id);
            springBeanRef.setApplications(applications);
            springConfigurationModel.addSpringBeanReference(springBeanRef);

            JavaClassModel classReference = javaClassService.getOrCreatePhantom("java.util.Set");
            springBeanRef.setJavaClass(classReference);
        }

        // extract JNDI references & type the JNDI resource, when expected-type is provided.
        /*
         * <jee:jndi-lookup id="jeeDataSource" jndi-name="jdbc/ExampleSpringJNDIDataSource" expected-type="javax.sql.DataSource" />
         */
        List<Element> jndis = $(element).children("jndi-lookup").get();
        if (jndis.size() > 0) {
            for (Element jndi : jndis) {
                String id = $(jndi).attr("id");
                String jndiName = $(jndi).attr("jndi-name");
                String expectedType = $(jndi).attr("expected-type");

                LOG.info("Found JNDI in JEE Spring: " + jndiName);

                SpringBeanModel springBeanRef = springBeanService.create();
                springBeanRef.setSpringBeanName(id);
                springBeanRef.setApplications(applications);

                JNDIResourceModel jndiResource = null;
                if (StringUtils.isNotBlank(jndiName)) {
                    jndiResource = jndiResourceService.createUnique(applications, jndiName);
                    if (StringUtils.isNotBlank(expectedType)) {
                        LOG.info(" -- Type: " + expectedType);
                        jndiResourceService.associateTypeJndiResource(jndiResource, expectedType);
                    }


                    //this will make it so that we have an association from the SpringBean to the JNDI Resource, layered on top of the typical SpringBean.
                    //this in turn will make this both a SpringBeanModel and a JNDIReferenceModel in the graph, so we can specifically look up Spring Bean JNDI References
                    JNDIReferenceModel reference = GraphService.addTypeToModel(event.getGraphContext(), springBeanRef, JNDIReferenceModel.class);
                    reference.setJndiReference(jndiResource);
                }

                //jndi-lookup is shorthand for this class.  register it to allow location of the bean by name to configuration file
                final String clz = "org.springframework.jndi.JndiObjectFactoryBean";
                JavaClassModel classReference = javaClassService.getOrCreatePhantom(clz);
                springBeanRef.setJavaClass(classReference);


                springConfigurationModel.addSpringBeanReference(springBeanRef);
            }
        }
    }
}
