/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2011, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file 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.ee.component.deployers;

import static org.jboss.as.ee.component.Attachments.COMPONENT_REGISTRY;
import static org.jboss.as.ee.component.Attachments.EE_MODULE_CONFIGURATION;
import static org.jboss.as.ee.logging.EeLogger.ROOT_LOGGER;
import static org.jboss.as.server.deployment.Attachments.MODULE;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.jboss.as.ee.component.Attachments;
import org.jboss.as.ee.component.BasicComponent;
import org.jboss.as.ee.component.BasicComponentCreateService;
import org.jboss.as.ee.component.BindingConfiguration;
import org.jboss.as.ee.component.ClassDescriptionTraversal;
import org.jboss.as.ee.component.Component;
import org.jboss.as.ee.component.ComponentConfiguration;
import org.jboss.as.ee.component.ComponentNamingMode;
import org.jboss.as.ee.component.ComponentRegistry;
import org.jboss.as.ee.component.ComponentStartService;
import org.jboss.as.ee.component.ComponentView;
import org.jboss.as.ee.component.DependencyConfigurator;
import org.jboss.as.ee.component.EEApplicationClasses;
import org.jboss.as.ee.component.EEModuleClassDescription;
import org.jboss.as.ee.component.EEModuleConfiguration;
import org.jboss.as.ee.component.InjectionSource;
import org.jboss.as.ee.component.InterceptorDescription;
import org.jboss.as.ee.component.ViewConfiguration;
import org.jboss.as.ee.component.ViewService;
import org.jboss.as.ee.logging.EeLogger;
import org.jboss.as.ee.metadata.MetadataCompleteMarker;
import org.jboss.as.naming.ManagedReferenceFactory;
import org.jboss.as.naming.ServiceBasedNamingStore;
import org.jboss.as.naming.deployment.ContextNames;
import org.jboss.as.naming.deployment.JndiNamingDependencyProcessor;
import org.jboss.as.naming.service.BinderService;
import org.jboss.as.naming.service.NamingStoreService;
import org.jboss.as.server.CurrentServiceContainer;
import org.jboss.as.server.Services;
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.modules.Module;
import org.jboss.msc.service.CircularDependencyException;
import org.jboss.msc.service.DuplicateServiceException;
import org.jboss.msc.service.Service;
import org.jboss.msc.service.ServiceBuilder;
import org.jboss.msc.service.ServiceController;
import org.jboss.msc.service.ServiceName;
import org.jboss.msc.service.ServiceTarget;

/**
 * @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
 * @author Eduardo Martins
 */
public final class ComponentInstallProcessor implements DeploymentUnitProcessor {

    private static final ServiceName JNDI_BINDINGS_SERVICE = ServiceName.of("JndiBindingsService");

    private static final List<String> SPEC_COMPONENTS = List.of(
            "BeanManager",
            "DefaultContextService",
            "DefaultDataSource",
            "DefaultJMSConnectionFactory",
            "DefaultManagedExecutorService",
            "DefaultManagedScheduledExecutorService",
            "DefaultManagedThreadFactory",
            "EJBContext",
            "HandleDelegate",
            "InAppClientContainer",
            "InstanceName",
            "ModuleName",
            "ORB",
            "TimerService",
            "TransactionSynchronizationRegistry",
            "Validator",
            "ValidatorFactory"
    );

    public void deploy(final DeploymentPhaseContext phaseContext) throws DeploymentUnitProcessingException {
        final DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit();
        final Module module = deploymentUnit.getAttachment(MODULE);
        final EEModuleConfiguration moduleConfiguration = deploymentUnit.getAttachment(EE_MODULE_CONFIGURATION);
        if (module == null || moduleConfiguration == null) {
            return;
        }
        ComponentRegistry componentRegistry = deploymentUnit.getAttachment(COMPONENT_REGISTRY);

        final List<ServiceName> dependencies = deploymentUnit.getAttachmentList(org.jboss.as.server.deployment.Attachments.JNDI_DEPENDENCIES);

        final ServiceName bindingDependencyService = JndiNamingDependencyProcessor.serviceName(deploymentUnit.getServiceName());

        // Iterate through each component, installing it into the container
        for (final ComponentConfiguration configuration : moduleConfiguration.getComponentConfigurations()) {
            try {
                ROOT_LOGGER.tracef("Installing component %s", configuration.getComponentClass().getName());
                deployComponent(phaseContext, configuration, dependencies, bindingDependencyService);
                componentRegistry.addComponent(configuration);

                //we need to make sure that the web deployment has a dependency on all components it the app, so web components are started
                //when the web subsystem is starting
                //we only add a dependency on components in the same sub deployment, otherwise we get circular dependencies when initialize-in-order is used
                deploymentUnit.addToAttachmentList(org.jboss.as.server.deployment.Attachments.WEB_DEPENDENCIES, configuration.getComponentDescription().getStartServiceName());
            } catch (Exception e) {
                throw EeLogger.ROOT_LOGGER.failedToInstallComponent(e, configuration.getComponentName());
            }
        }
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected void deployComponent(final DeploymentPhaseContext phaseContext, final ComponentConfiguration configuration, final List<ServiceName> jndiDependencies, final ServiceName bindingDependencyService) throws DeploymentUnitProcessingException {

        final DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit();
        final ServiceTarget serviceTarget = phaseContext.getServiceTarget();

        final String applicationName = configuration.getApplicationName();
        final String moduleName = configuration.getModuleName();
        final String componentName = configuration.getComponentName();
        final EEApplicationClasses applicationClasses = deploymentUnit.getAttachment(Attachments.EE_APPLICATION_CLASSES_DESCRIPTION);
        final Module module = deploymentUnit.getAttachment(org.jboss.as.server.deployment.Attachments.MODULE);

        //create additional injectors

        final ServiceName createServiceName = configuration.getComponentDescription().getCreateServiceName();
        final ServiceName startServiceName = configuration.getComponentDescription().getStartServiceName();
        final BasicComponentCreateService createService = configuration.getComponentCreateServiceFactory().constructService(configuration);
        final ServiceBuilder<Component> createBuilder = serviceTarget.addService(createServiceName, createService);

        final ComponentStartService startService = new ComponentStartService();
        final ServiceBuilder<Component> startBuilder = serviceTarget.addService(startServiceName, startService);

        deploymentUnit.addToAttachmentList(org.jboss.as.server.deployment.Attachments.DEPLOYMENT_COMPLETE_SERVICES, startServiceName);

        //WFLY-1402 we don't add the bindings to the jndi dependencies list directly, instead
        //the bindings depend on the this artificial service
        ServiceName jndiDepServiceName = configuration.getComponentDescription().getServiceName().append(JNDI_BINDINGS_SERVICE);
        final ServiceBuilder<Void> jndiDepServiceBuilder = serviceTarget.addService(jndiDepServiceName, Service.NULL);
        jndiDependencies.add(jndiDepServiceName);

        // Add all service dependencies
        for (DependencyConfigurator configurator : configuration.getCreateDependencies()) {
            configurator.configureDependency(createBuilder, createService);
        }
        for (DependencyConfigurator configurator : configuration.getStartDependencies()) {
            configurator.configureDependency(startBuilder, startService);
        }

        // START depends on CREATE
        startBuilder.addDependency(createServiceName, BasicComponent.class, startService.getComponentInjector());
        Services.addServerExecutorDependency(startBuilder, startService.getExecutorInjector());

        //don't start components until all bindings are up
        startBuilder.requires(bindingDependencyService);
        final ServiceName contextServiceName;
        //set up the naming context if necessary
        if (configuration.getComponentDescription().getNamingMode() == ComponentNamingMode.CREATE) {
            final NamingStoreService contextService = new NamingStoreService(true);
            serviceTarget.addService(configuration.getComponentDescription().getContextServiceName(), contextService).install();
        }

        final InjectionSource.ResolutionContext resolutionContext = new InjectionSource.ResolutionContext(
                configuration.getComponentDescription().getNamingMode() == ComponentNamingMode.USE_MODULE,
                configuration.getComponentName(),
                configuration.getModuleName(),
                configuration.getApplicationName()
        );

        // Iterate through each view, creating the services for each
        for (ViewConfiguration viewConfiguration : configuration.getViews()) {
            final ServiceName serviceName = viewConfiguration.getViewServiceName();
            final ViewService viewService = new ViewService(viewConfiguration);
            final ServiceBuilder<ComponentView> componentViewServiceBuilder = serviceTarget.addService(serviceName, viewService);
            componentViewServiceBuilder
                    .addDependency(createServiceName, Component.class, viewService.getComponentInjector());
            for(final DependencyConfigurator<ViewService> depConfig : viewConfiguration.getDependencies()) {
                depConfig.configureDependency(componentViewServiceBuilder, viewService);
            }
            componentViewServiceBuilder.install();
            startBuilder.requires(serviceName);
            // The bindings for the view
            for (BindingConfiguration bindingConfiguration : viewConfiguration.getBindingConfigurations()) {
                final String bindingName = bindingConfiguration.getName();
                final ContextNames.BindInfo bindInfo = ContextNames.bindInfoFor(applicationName, moduleName, componentName, bindingName);
                final BinderService service = new BinderService(bindInfo.getBindName(), bindingConfiguration.getSource());

                //these bindings should never be merged, if a view binding is duplicated it is an error
                jndiDepServiceBuilder.requires(bindInfo.getBinderServiceName());

                ServiceBuilder<ManagedReferenceFactory> serviceBuilder = serviceTarget.addService(bindInfo.getBinderServiceName(), service);
                bindingConfiguration.getSource().getResourceValue(resolutionContext, serviceBuilder, phaseContext, service.getManagedObjectInjector());
                serviceBuilder.addDependency(bindInfo.getParentContextServiceName(), ServiceBasedNamingStore.class, service.getNamingStoreInjector());
                try {
                    serviceBuilder.install();
                } catch (DuplicateServiceException e) {
                    handleDuplicateService(configuration, bindInfo.getAbsoluteJndiName());
                    throw e;
                }

            }
        }

        if (configuration.getComponentDescription().getNamingMode() == ComponentNamingMode.CREATE) {
            // The bindings for the component
            final Set<ServiceName> bound = new HashSet<ServiceName>();
            processBindings(phaseContext, configuration, serviceTarget, resolutionContext, configuration.getComponentDescription().getBindingConfigurations(), jndiDepServiceBuilder, bound);

            //class level bindings should be ignored if the deployment is metadata complete
            if (!MetadataCompleteMarker.isMetadataComplete(phaseContext.getDeploymentUnit())) {

                // The bindings for the component class
                new ClassDescriptionTraversal(configuration.getComponentClass(), applicationClasses) {
                    @Override
                    protected void handle(final Class<?> clazz, final EEModuleClassDescription classDescription) throws DeploymentUnitProcessingException {
                        if (classDescription != null) {
                            processBindings(phaseContext, configuration, serviceTarget, resolutionContext, classDescription.getBindingConfigurations(), jndiDepServiceBuilder, bound);
                        }
                    }
                }.run();


                for (InterceptorDescription interceptor : configuration.getComponentDescription().getAllInterceptors()) {
                    final Class<?> interceptorClass;
                    try {
                        interceptorClass = module.getClassLoader().loadClass(interceptor.getInterceptorClassName());
                    } catch (ClassNotFoundException e) {
                        throw EeLogger.ROOT_LOGGER.cannotLoadInterceptor(e, interceptor.getInterceptorClassName(), configuration.getComponentClass());
                    }
                    if (interceptorClass != null) {
                        new ClassDescriptionTraversal(interceptorClass, applicationClasses) {
                            @Override
                            protected void handle(final Class<?> clazz, final EEModuleClassDescription classDescription) throws DeploymentUnitProcessingException {
                                if (classDescription != null) {
                                    processBindings(phaseContext, configuration, serviceTarget, resolutionContext, classDescription.getBindingConfigurations(), jndiDepServiceBuilder, bound);
                                }
                            }
                        }.run();
                    }
                }
            }
        }

        createBuilder.install();
        startBuilder.install();
        jndiDepServiceBuilder.install();
    }

    @SuppressWarnings("unchecked")
    private void processBindings(DeploymentPhaseContext phaseContext, ComponentConfiguration configuration, ServiceTarget serviceTarget, InjectionSource.ResolutionContext resolutionContext, List<BindingConfiguration> bindings, final ServiceBuilder<?> jndiDepServiceBuilder, final Set<ServiceName> bound) throws DeploymentUnitProcessingException {

        //we only handle java:comp bindings for components that have their own namespace here, the rest are processed by ModuleJndiBindingProcessor
        for (BindingConfiguration bindingConfiguration : bindings) {
            if (bindingConfiguration.getName().startsWith("java:comp") || !bindingConfiguration.getName().startsWith("java:")) {
                final String bindingName = bindingConfiguration.getName().startsWith("java:comp") ? bindingConfiguration.getName() : "java:comp/env/" + bindingConfiguration.getName();
                final ContextNames.BindInfo bindInfo = ContextNames.bindInfoForEnvEntry(configuration.getApplicationName(), configuration.getModuleName(), configuration.getComponentName(), configuration.getComponentDescription().getNamingMode() == ComponentNamingMode.CREATE, bindingName);
                if (bound.contains(bindInfo.getBinderServiceName())) {
                    continue;
                }
                bound.add(bindInfo.getBinderServiceName());
                try {
                    final BinderService service = new BinderService(bindInfo.getBindName(), bindingConfiguration.getSource());
                    jndiDepServiceBuilder.requires(bindInfo.getBinderServiceName());
                    ServiceBuilder<ManagedReferenceFactory> serviceBuilder = serviceTarget.addService(bindInfo.getBinderServiceName(), service);
                    bindingConfiguration.getSource().getResourceValue(resolutionContext, serviceBuilder, phaseContext, service.getManagedObjectInjector());
                    serviceBuilder.addDependency(bindInfo.getParentContextServiceName(), ServiceBasedNamingStore.class, service.getNamingStoreInjector());
                    serviceBuilder.install();
                } catch (DuplicateServiceException e) {
                    ServiceController<ManagedReferenceFactory> registered = (ServiceController<ManagedReferenceFactory>) CurrentServiceContainer.getServiceContainer().getService(bindInfo.getBinderServiceName());
                    if (registered == null)
                        throw e;

                    BinderService service = (BinderService) registered.getService();
                    if (!service.getSource().equals(bindingConfiguration.getSource()))
                        throw EeLogger.ROOT_LOGGER.conflictingBinding(bindingName, bindingConfiguration.getSource());
                } catch (CircularDependencyException e) {
                    throw EeLogger.ROOT_LOGGER.circularDependency(bindingName);
                }
            }
        }
    }

    private void handleDuplicateService(ComponentConfiguration configuration, String bindingName) {
        String name = configuration.getComponentName();
        if (SPEC_COMPONENTS.contains(name)) {
            Class conflict = configuration.getComponentClass();

            ROOT_LOGGER.duplicateJndiBindingFound(name, bindingName, conflict);
        }
    }
}
