/*
 * JBoss, Home of Professional Open Source
 * Copyright 2012, Red Hat Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.as.jsf.deployment;

import org.jboss.as.ee.component.ComponentDescription;
import org.jboss.as.ee.component.EEApplicationClasses;
import org.jboss.as.ee.component.EEModuleDescription;
import org.jboss.as.ee.structure.DeploymentType;
import org.jboss.as.ee.structure.DeploymentTypeMarker;
import org.jboss.as.jsf.logging.JSFLogger;
import org.jboss.as.server.deployment.Attachments;
import org.jboss.as.server.deployment.DeploymentPhaseContext;
import org.jboss.as.server.deployment.DeploymentUnit;
import org.jboss.as.server.deployment.DeploymentUnitProcessingException;
import org.jboss.as.server.deployment.DeploymentUnitProcessor;
import org.jboss.as.server.deployment.DeploymentUtils;
import org.jboss.as.server.deployment.annotation.CompositeIndex;
import org.jboss.as.server.deployment.module.ResourceRoot;
import org.jboss.as.web.common.WarMetaData;
import org.jboss.as.web.common.WebComponentDescription;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.metadata.javaee.spec.ParamValueMetaData;
import org.jboss.metadata.parser.util.NoopXMLResolver;
import org.jboss.metadata.web.spec.WebMetaData;
import org.jboss.modules.Module;
import org.jboss.vfs.VirtualFile;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Sets up JSF managed beans as components using information in the annotations and
 *
 * @author Stuart Douglas
 */
public class JSFManagedBeanProcessor implements DeploymentUnitProcessor {

    public static final DotName MANAGED_BEAN_ANNOTATION = DotName.createSimple("javax.faces.bean.ManagedBean");

    private static final String WEB_INF_FACES_CONFIG = "WEB-INF/faces-config.xml";

    private static final String MANAGED_BEAN = "managed-bean";
    private static final String MANAGED_BEAN_CLASS = "managed-bean-class";

    private static final String CONFIG_FILES = "javax.faces.CONFIG_FILES";

    @Override
    public void deploy(final DeploymentPhaseContext phaseContext) throws DeploymentUnitProcessingException {
        final DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit();
        final CompositeIndex index = deploymentUnit.getAttachment(Attachments.COMPOSITE_ANNOTATION_INDEX);
        final EEModuleDescription moduleDescription = deploymentUnit.getAttachment(org.jboss.as.ee.component.Attachments.EE_MODULE_DESCRIPTION);
        final EEApplicationClasses applicationClassesDescription = deploymentUnit.getAttachment(org.jboss.as.ee.component.Attachments.EE_APPLICATION_CLASSES_DESCRIPTION);
        final Module module = deploymentUnit.getAttachment(Attachments.MODULE);
        if (index == null) {
            return;
        }
        if (module == null) {
            return;
        }
        if (!DeploymentTypeMarker.isType(DeploymentType.WAR, deploymentUnit)) {
            return;
        }
        final Set<String> managedBeanClasses = new HashSet<String>();
        handleAnnotations(index, managedBeanClasses);
        processXmlManagedBeans(deploymentUnit, managedBeanClasses);
        for (String managedBean : managedBeanClasses) {
            //try and load the class, and skip the class if it cannot be loaded
            //this is not ideal, but we are not allowed to let the deployment
            //fail due to missing managed beans
            try {
                final Class<?> componentClass = module.getClassLoader().loadClass(managedBean);
                componentClass.getConstructor();
            } catch (ClassNotFoundException e) {
                JSFLogger.ROOT_LOGGER.managedBeanLoadFail(managedBean);
                continue;
            } catch (NoSuchMethodException e) {
                JSFLogger.ROOT_LOGGER.managedBeanNoDefaultConstructor(managedBean);
                continue;
            }
            installManagedBeanComponent(managedBean, moduleDescription, deploymentUnit, applicationClassesDescription);
        }

    }

    /**
     * Parse the faces config files looking for managed bean classes. The parser is quite
     * simplistic as the only information we need is the managed-bean-class element
     */
    private void processXmlManagedBeans(final DeploymentUnit deploymentUnit, final Set<String> managedBeanClasses) {
        for (final VirtualFile facesConfig : getConfigurationFiles(deploymentUnit)) {
            InputStream is = null;
            try {
                is = facesConfig.openStream();
                final XMLInputFactory inputFactory = XMLInputFactory.newInstance();
                inputFactory.setXMLResolver(NoopXMLResolver.create());
                XMLStreamReader parser = inputFactory.createXMLStreamReader(is);
                StringBuilder className = null;
                int indent = 0;
                boolean managedBean = false;
                boolean managedBeanClass = false;
                while (true) {
                    int event = parser.next();
                    if (event == XMLStreamConstants.END_DOCUMENT) {
                        parser.close();
                        break;
                    }
                    if (event == XMLStreamConstants.START_ELEMENT) {
                        indent++;
                        if (indent == 2) {
                            if (parser.getLocalName().equals(MANAGED_BEAN)) {
                                managedBean = true;
                            }
                        } else if (indent == 3 && managedBean) {
                            if (parser.getLocalName().equals(MANAGED_BEAN_CLASS)) {
                                managedBeanClass = true;
                                className = new StringBuilder();
                            }
                        }

                    } else if (event == XMLStreamConstants.END_ELEMENT) {
                        indent--;
                        managedBeanClass = false;
                        if (indent == 1) {
                            managedBean = false;
                        }
                        if (className != null) {
                            managedBeanClasses.add(className.toString().trim());
                            className = null;
                        }
                    } else if (managedBeanClass && event == XMLStreamConstants.CHARACTERS) {
                        className.append(parser.getText());
                    }
                }
            } catch (Exception e) {
                JSFLogger.ROOT_LOGGER.managedBeansConfigParseFailed(facesConfig);
            } finally {
                try {
                    if (is != null) {
                        is.close();
                    }
                } catch (IOException e) {
                    // Ignore
                }
            }
        }
    }

    public Set<VirtualFile> getConfigurationFiles(DeploymentUnit deploymentUnit) {
        final Set<VirtualFile> ret = new HashSet<VirtualFile>();
        final List<ResourceRoot> resourceRoots = DeploymentUtils.allResourceRoots(deploymentUnit);
        for (final ResourceRoot resourceRoot : resourceRoots) {
            final VirtualFile webInfFacesConfig = resourceRoot.getRoot().getChild(WEB_INF_FACES_CONFIG);
            if (webInfFacesConfig.exists()) {
                ret.add(webInfFacesConfig);
            }
            //look for files that end in .faces-config.xml
            final VirtualFile metaInf = resourceRoot.getRoot().getChild("META-INF");
            if (metaInf.exists() && metaInf.isDirectory()) {
                for (final VirtualFile file : metaInf.getChildren()) {
                    if (file.getName().equals("faces-config.xml") || file.getName().endsWith(".faces-config.xml")) {
                        ret.add(file);
                    }
                }
            }
        }
        String configFiles = null;
        //now look for files in the javax.faces.CONFIG_FILES context param
        final WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY);
        if (warMetaData != null) {
            final WebMetaData webMetaData = warMetaData.getWebMetaData();
            if (webMetaData != null) {
                final List<ParamValueMetaData> contextParams = webMetaData.getContextParams();
                if (contextParams != null) {
                    for (final ParamValueMetaData param : contextParams) {
                        if (param.getParamName().equals(CONFIG_FILES)) {
                            configFiles = param.getParamValue();
                            break;
                        }
                    }
                }
            }
        }
        if (configFiles != null) {
            final String[] files = configFiles.split(",");
            final ResourceRoot deploymentRoot = deploymentUnit.getAttachment(Attachments.DEPLOYMENT_ROOT);
            if (deploymentRoot != null) {
                for (final String file : files) {
                    final VirtualFile configFile = deploymentRoot.getRoot().getChild(file);
                    if (configFile.exists()) {
                        ret.add(configFile);
                    }
                }
            }
        }
        return ret;
    }

    private void handleAnnotations(final CompositeIndex index, final Set<String> managedBeanClasses) throws DeploymentUnitProcessingException {
        final List<AnnotationInstance> annotations = index.getAnnotations(MANAGED_BEAN_ANNOTATION);
        if (annotations != null) {
            for (final AnnotationInstance annotation : annotations) {

                final AnnotationTarget target = annotation.target();
                if (target instanceof ClassInfo) {
                    final String className = ((ClassInfo) target).name().toString();
                    managedBeanClasses.add(className);
                } else {
                    throw new DeploymentUnitProcessingException(JSFLogger.ROOT_LOGGER.invalidManagedBeanAnnotation(target));
                }
            }
        }
    }

    private void installManagedBeanComponent(String className, final EEModuleDescription moduleDescription, final DeploymentUnit deploymentUnit, final EEApplicationClasses applicationClassesDescription) {
        final ComponentDescription componentDescription = new WebComponentDescription(MANAGED_BEAN.toString() + "." + className, className, moduleDescription, deploymentUnit.getServiceName(), applicationClassesDescription);
        moduleDescription.addComponent(componentDescription);
        deploymentUnit.addToAttachmentList(WebComponentDescription.WEB_COMPONENTS, componentDescription.getStartServiceName());
    }

    @Override
    public void undeploy(final DeploymentUnit context) {

    }
}
