/*
 * JBoss, Home of Professional Open Source
 * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors
 * as indicated by the @authors tag. All rights reserved.
 * See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jboss.arquillian.container.impl.client.container;

import java.util.List;
import java.util.concurrent.Callable;
import org.jboss.arquillian.container.spi.Container;
import org.jboss.arquillian.container.spi.Container.State;
import org.jboss.arquillian.container.spi.ContainerRegistry;
import org.jboss.arquillian.container.spi.client.container.DeployableContainer;
import org.jboss.arquillian.container.spi.client.deployment.Deployment;
import org.jboss.arquillian.container.spi.client.deployment.DeploymentDescription;
import org.jboss.arquillian.container.spi.client.deployment.DeploymentScenario;
import org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaData;
import org.jboss.arquillian.container.spi.context.annotation.DeploymentScoped;
import org.jboss.arquillian.container.spi.event.DeployDeployment;
import org.jboss.arquillian.container.spi.event.DeployManagedDeployments;
import org.jboss.arquillian.container.spi.event.DeploymentEvent;
import org.jboss.arquillian.container.spi.event.UnDeployDeployment;
import org.jboss.arquillian.container.spi.event.UnDeployManagedDeployments;
import org.jboss.arquillian.container.spi.event.container.AfterDeploy;
import org.jboss.arquillian.container.spi.event.container.AfterUnDeploy;
import org.jboss.arquillian.container.spi.event.container.BeforeDeploy;
import org.jboss.arquillian.container.spi.event.container.BeforeUnDeploy;
import org.jboss.arquillian.container.spi.event.container.DeployerEvent;
import org.jboss.arquillian.core.api.Event;
import org.jboss.arquillian.core.api.Injector;
import org.jboss.arquillian.core.api.Instance;
import org.jboss.arquillian.core.api.InstanceProducer;
import org.jboss.arquillian.core.api.annotation.Inject;
import org.jboss.arquillian.core.api.annotation.Observes;

/**
 * Controller for handling all Deployment related operations. <br/>
 * <br/>
 * <p>
 * Fires DeployDeployment events for each deployment that should be deployed during startup. This so the Cores exception
 * handling
 * will be triggered if Deployment fails inside the context of the deployment and container. This lets extensions listen
 * for Exceptions types
 * and handle them inside the same context.
 *
 * @author <a href="mailto:aslak@redhat.com">Aslak Knutsen</a>
 */
public class ContainerDeployController {
    @Inject
    private Instance<ContainerRegistry> containerRegistry;

    @Inject
    private Instance<DeploymentScenario> deploymentScenario;

    @Inject
    private Instance<Injector> injector;

    /**
     * Deploy all deployments marked as managed = true.
     *
     * @throws Exception
     */
    public void deployManaged(@Observes DeployManagedDeployments event) throws Exception {
        forEachManagedDeployment(new Operation<Container, Deployment>() {
            @Inject
            private Event<DeploymentEvent> event;

            @Override
            public void perform(Container container, Deployment deployment) throws Exception {
                //when a container is manually controlled, the deployment is deployed automatically
                //once the container is manually started, not now
                if (!"manual".equals(container.getContainerConfiguration().getMode())) {
                    if (container.getState() != State.STARTED) {
                        throw new IllegalStateException("Trying to deploy a managed deployment "
                            + deployment.getDescription().getName()
                            + " to a non started managed container "
                            + container.getName());
                    }
                    event.fire(new DeployDeployment(container, deployment));
                }
            }
        });
    }

    /**
     * Undeploy all deployments marked as managed, and all manually deployed.
     *
     * @throws Exception
     */
    public void undeployManaged(@Observes UnDeployManagedDeployments event) throws Exception {
        forEachDeployedDeployment(new Operation<Container, Deployment>() {
            @Inject
            private Event<DeploymentEvent> event;

            @Override
            public void perform(Container container, Deployment deployment) throws Exception {
                if (container.getState().equals(Container.State.STARTED) && deployment.isDeployed()) {
                    event.fire(new UnDeployDeployment(container, deployment));
                }
            }
        });
    }

    public void deploy(@Observes final DeployDeployment event) throws Exception {
        executeOperation(new Callable<Void>() {
            @Inject
            private Event<DeployerEvent> deployEvent;

            @Inject
            @DeploymentScoped
            private InstanceProducer<DeploymentDescription> deploymentDescriptionProducer;

            @Inject
            @DeploymentScoped
            private InstanceProducer<Deployment> deploymentProducer;

            @Inject
            @DeploymentScoped
            private InstanceProducer<ProtocolMetaData> protocolMetadata;

            @Override
            public Void call() throws Exception {
                DeployableContainer<?> deployableContainer = event.getDeployableContainer();
                Deployment deployment = event.getDeployment();
                DeploymentDescription deploymentDescription = deployment.getDescription();

            /*
             * TODO: should the DeploymentDescription producer some how be automatically registered ?
             * Or should we just 'know' who is the first one to create the context
             */
                deploymentDescriptionProducer.set(deploymentDescription);
                deploymentProducer.set(deployment);

                deployEvent.fire(new BeforeDeploy(deployableContainer, deploymentDescription));

                try {
                    if (deploymentDescription.isArchiveDeployment()) {
                        protocolMetadata.set(deployableContainer.deploy(
                            deploymentDescription.getTestableArchive() != null
                                ? deploymentDescription.getTestableArchive() : deploymentDescription.getArchive()));
                    } else {
                        deployableContainer.deploy(deploymentDescription.getDescriptor());
                    }
                    deployment.deployed();
                } catch (Exception e) {
                    deployment.deployedWithError(e);
                    throw e;
                }

                deployEvent.fire(new AfterDeploy(deployableContainer, deploymentDescription));
                return null;
            }
        });
    }

    public void undeploy(@Observes final UnDeployDeployment event) throws Exception {
        executeOperation(new Callable<Void>() {
            @Inject
            private Event<DeployerEvent> deployEvent;

            @Override
            public Void call() throws Exception {
                DeployableContainer<?> deployableContainer = event.getDeployableContainer();
                Deployment deployment = event.getDeployment();
                DeploymentDescription description = deployment.getDescription();

                deployEvent.fire(new BeforeUnDeploy(deployableContainer, description));

                try {

                    if (deployment.getDescription().isArchiveDeployment()) {
                        try {
                            deployableContainer.undeploy(
                                description.getTestableArchive() != null ? description.getTestableArchive()
                                    : description.getArchive());
                        } catch (Exception e) {
                            if (!deployment.hasDeploymentError()) {
                                throw e;
                            }
                        }
                    } else {
                        deployableContainer.undeploy(description.getDescriptor());
                    }
                } finally {
                    deployment.undeployed();
                }

                deployEvent.fire(new AfterUnDeploy(deployableContainer, description));
                return null;
            }
        });
    }

    private void forEachManagedDeployment(Operation<Container, Deployment> operation) throws Exception {
        DeploymentScenario scenario = this.deploymentScenario.get();
        if (scenario == null) {
            return;
        }
        forEachDeployment(scenario.managedDeploymentsInDeployOrder(), operation);
    }

    private void forEachDeployedDeployment(Operation<Container, Deployment> operation) throws Exception {
        DeploymentScenario scenario = this.deploymentScenario.get();
        if (scenario == null) {
            return;
        }
        forEachDeployment(scenario.deployedDeploymentsInUnDeployOrder(), operation);
    }

    private void forEachDeployment(List<Deployment> deployments, Operation<Container, Deployment> operation)
        throws Exception {
        injector.get().inject(operation);
        ContainerRegistry containerRegistry = this.containerRegistry.get();
        if (containerRegistry == null) {
            return;
        }
        for (Deployment deployment : deployments) {
            Container container = containerRegistry.getContainer(deployment.getDescription().getTarget());
            operation.perform(container, deployment);
        }
    }

    private void executeOperation(Callable<Void> operation)
        throws Exception {
        injector.get().inject(operation);
        operation.call();
    }

    public interface Operation<T, X> {
        void perform(T container, X deployment) throws Exception;
    }
}
