/*
 * Copyright 2013 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Eclipse Public License version 1.0, available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.jboss.forge.arquillian;

import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import org.jboss.arquillian.container.spi.client.container.DeployableContainer;
import org.jboss.arquillian.container.spi.client.container.DeploymentException;
import org.jboss.arquillian.container.spi.client.container.LifecycleException;
import org.jboss.arquillian.container.spi.client.deployment.Deployment;
import org.jboss.arquillian.container.spi.client.protocol.ProtocolDescription;
import org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaData;
import org.jboss.arquillian.core.api.Instance;
import org.jboss.arquillian.core.api.annotation.Inject;
import org.jboss.forge.arquillian.archive.AddonArchiveBase;
import org.jboss.forge.arquillian.archive.AddonDependencyAware;
import org.jboss.forge.arquillian.archive.AddonDeploymentArchive;
import org.jboss.forge.arquillian.archive.RepositoryLocationAware;
import org.jboss.forge.arquillian.impl.ShrinkWrapUtil;
import org.jboss.forge.arquillian.protocol.FurnaceHolder;
import org.jboss.forge.arquillian.protocol.FurnaceProtocolDescription;
import org.jboss.forge.furnace.Furnace;
import org.jboss.forge.furnace.addons.Addon;
import org.jboss.forge.furnace.addons.AddonId;
import org.jboss.forge.furnace.addons.AddonRegistry;
import org.jboss.forge.furnace.impl.FurnaceImpl;
import org.jboss.forge.furnace.impl.util.Files;
import org.jboss.forge.furnace.manager.AddonManager;
import org.jboss.forge.furnace.manager.impl.AddonManagerImpl;
import org.jboss.forge.furnace.manager.maven.MavenContainer;
import org.jboss.forge.furnace.manager.maven.addon.MavenAddonDependencyResolver;
import org.jboss.forge.furnace.manager.spi.AddonDependencyResolver;
import org.jboss.forge.furnace.repositories.AddonDependencyEntry;
import org.jboss.forge.furnace.repositories.AddonRepository;
import org.jboss.forge.furnace.repositories.AddonRepositoryMode;
import org.jboss.forge.furnace.repositories.MutableAddonRepository;
import org.jboss.forge.furnace.spi.ContainerLifecycleListener;
import org.jboss.forge.furnace.spi.ListenerRegistration;
import org.jboss.forge.furnace.util.Addons;
import org.jboss.forge.furnace.util.Callables;
import org.jboss.forge.furnace.util.ClassLoaders;
import org.jboss.forge.furnace.util.OperatingSystemUtils;
import org.jboss.forge.furnace.util.SecurityActions;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.descriptor.api.Descriptor;

/**
 * @author <a href="mailto:[email protected]">Lincoln Baxter, III</a>
 */
public class FurnaceDeployableContainer implements DeployableContainer<FurnaceContainerConfiguration>
{
   @Inject
   private Instance<Deployment> deploymentInstance;

   private final FurnaceHolder furnaceHolder = new FurnaceHolder();
   private ForgeRunnable runnable;
   private File addonDir;

   private MutableAddonRepository repository;
   private final Map<String, MutableAddonRepository> deploymentRepositories = new ConcurrentHashMap<String, MutableAddonRepository>();

   private final Map<Deployment, AddonId> deployedAddons = new HashMap<Deployment, AddonId>();
   private Thread thread;

   private boolean undeploying = false;
   private FurnaceContainerConfiguration configuration;

   private static String originalUserSettings;
   private static String originalLocalRepository;
   private static String originalGlobalSettings;

   static
   {
      originalUserSettings = System.getProperty(MavenContainer.ALT_USER_SETTINGS_XML_LOCATION);
      originalLocalRepository = System.getProperty(MavenContainer.ALT_LOCAL_REPOSITORY_LOCATION);
      originalGlobalSettings = System.getProperty(MavenContainer.ALT_GLOBAL_SETTINGS_XML_LOCATION);
   }

   @Override
   public ProtocolMetaData deploy(final Archive<?> archive) throws DeploymentException
   {
      try
      {
         resetMavenSettings();
         Deployment deployment = deploymentInstance.get();
         final AddonId addonToDeploy = getAddonEntry(deployment);

         if (undeploying)
         {
            System.out.println("Cleaning test runtime.");
            undeploying = false;
            cleanup();
         }

         if (archive instanceof AddonDeploymentArchive)
         {
            AddonDeploymentArchive deploymentArchive = (AddonDeploymentArchive) archive;
            AddonDependencyResolver resolver = new MavenAddonDependencyResolver(configuration.getClassifier());
            AddonManager addonManager = new AddonManagerImpl(runnable.furnace, resolver);

            AddonRepository target = selectTargetRepository(deploymentArchive);

            for (DeploymentListener listener : deploymentArchive.getDeploymentListeners())
            {
               listener.preDeploy(furnaceHolder.getFurnace(), archive);
            }

            addonManager.install(deploymentArchive.getAddonId(), target).perform();
            waitForDeploymentCompletion(deployment, addonToDeploy,
                     deploymentArchive.getDeploymentTimeoutQuantity(),
                     deploymentArchive.getDeploymentTimeoutUnit());

            for (DeploymentListener listener : deploymentArchive.getDeploymentListeners())
            {
               listener.postDeploy(furnaceHolder.getFurnace(), archive);
            }
         }
         else if (archive instanceof AddonArchiveBase<?>)
         {
            final MutableAddonRepository target = selectTargetRepository((AddonArchiveBase<?>) archive);

            waitForConfigurationRescan(new Callable<Void>()
            {
               @Override
               public Void call() throws Exception
               {
                  deployToRepository(archive, target, addonToDeploy);
                  return null;
               }
            });

            waitForDeploymentCompletion(deployment, addonToDeploy,
                     ((AddonArchiveBase<?>) archive).getDeploymentTimeoutQuantity(),
                     ((AddonArchiveBase<?>) archive).getDeploymentTimeoutUnit());
         }
         else
         {
            throw new IllegalArgumentException(
                     "Invalid Archive type. Ensure that your @Deployment method returns type 'AddonArchive'.");
         }

         return new ProtocolMetaData().addContext(furnaceHolder);
      }
      catch (Exception e)
      {
         throw new DeploymentException(e.getMessage(), e);
      }
   }

   private void resetMavenSettings()
   {
      if (originalUserSettings != null)
         System.setProperty(MavenContainer.ALT_USER_SETTINGS_XML_LOCATION, originalUserSettings);
      else
         System.clearProperty(MavenContainer.ALT_USER_SETTINGS_XML_LOCATION);

      if (originalLocalRepository != null)
         System.setProperty(MavenContainer.ALT_LOCAL_REPOSITORY_LOCATION, originalLocalRepository);
      else
         System.clearProperty(MavenContainer.ALT_LOCAL_REPOSITORY_LOCATION);

      if (originalGlobalSettings != null)
         System.setProperty(MavenContainer.ALT_GLOBAL_SETTINGS_XML_LOCATION, originalGlobalSettings);
      else
         System.clearProperty(MavenContainer.ALT_GLOBAL_SETTINGS_XML_LOCATION);
   }

   private <T> T waitForConfigurationRescan(Callable<T> action)
   {

      ConfigurationScanListener listener = new ConfigurationScanListener();
      ListenerRegistration<ContainerLifecycleListener> registration = runnable.furnace
               .addContainerLifecycleListener(listener);

      T result = Callables.call(action);

      while (runnable.furnace.getStatus().isStarting() || !listener.isConfigurationScanned())
      {
         try
         {
            Thread.sleep(100);
         }
         catch (InterruptedException e)
         {
            throw new RuntimeException("Sleep interrupted while waiting for configuration rescan.", e);
         }
      }

      registration.removeListener();

      return result;
   }

   private MutableAddonRepository selectTargetRepository(RepositoryLocationAware<?> archive)
   {
      MutableAddonRepository target = repository;
      if (archive instanceof RepositoryLocationAware<?>
               && ((RepositoryLocationAware<?>) archive).getAddonRepository() != null)
      {
         final String repositoryName = ((RepositoryLocationAware<?>) archive).getAddonRepository();
         if (deploymentRepositories.get(repositoryName) == null)
         {
            target = waitForConfigurationRescan(new Callable<MutableAddonRepository>()
            {
               @Override
               public MutableAddonRepository call() throws Exception
               {
                  return (MutableAddonRepository) runnable.furnace.addRepository(AddonRepositoryMode.MUTABLE,
                           new File(addonDir, OperatingSystemUtils.getSafeFilename(repositoryName)));
               }
            });
            deploymentRepositories.put(repositoryName, target);
         }
         else
            target = deploymentRepositories.get(repositoryName);
      }
      return target;
   }

   private void waitForDeploymentCompletion(Deployment deployment, final AddonId addonToDeploy, int quantity,
            TimeUnit unit)
            throws DeploymentException
   {
      AddonRegistry registry = runnable.getForge().getAddonRegistry();
      Addon addon = registry.getAddon(addonToDeploy);
      try
      {
         Future<Void> future = addon.getFuture();
         if (!future.isDone())
         {
            future.get();
         }
         Addons.waitUntilStartedOrMissing(addon, quantity, unit);
      }
      catch (Exception e)
      {
         deployment.deployedWithError(e);
         throw new DeploymentException("AddonDependency " + addonToDeploy + " failed to deploy.", e);
      }
      if (addon.getStatus().isFailed())
      {
         DeploymentException e = new DeploymentException("AddonDependency " + addonToDeploy + " failed to deploy.");
         deployment.deployedWithError(e);
         throw new DeploymentException("AddonDependency " + addonToDeploy + " failed to deploy.", e);
      }
   }

   private void deployToRepository(Archive<?> archive, MutableAddonRepository repository, final AddonId addonToDeploy)
   {
      File destDir = repository.getAddonBaseDir(addonToDeploy);
      destDir.mkdirs();
      ShrinkWrapUtil.toFile(new File(destDir.getAbsolutePath(), archive.getName()), archive);
      ShrinkWrapUtil.unzip(destDir, archive);
      System.out.println("Furnace test harness is deploying [" + addonToDeploy + "] to repository [" + repository
               + "] ...");

      if (archive instanceof AddonDependencyAware)
      {
         repository.deploy(addonToDeploy,
                  ((AddonDependencyAware<?>) archive).getAddonDependencies(),
                  Collections.<File> emptyList());
      }
      else
      {
         repository.deploy(addonToDeploy,
                  Collections.<AddonDependencyEntry> emptyList(),
                  Collections.<File> emptyList());
      }

      repository.enable(addonToDeploy);
   }

   private void cleanup()
   {
      try
      {
         stop();
         start();
      }
      catch (LifecycleException e)
      {
         throw new RuntimeException("Failed to clean up after test case.", e);
      }
   }

   @Override
   public void deploy(Descriptor descriptor) throws DeploymentException
   {
      throw new UnsupportedOperationException("Descriptors not supported by Furnace");
   }

   private AddonId getAddonEntry(Deployment deployment)
   {
      if (!deployedAddons.containsKey(deployment))
      {
         String[] coordinates = deployment.getDescription().getName().split(",");
         AddonId entry;
         if (coordinates.length == 3)
            entry = AddonId.from(coordinates[0], coordinates[1], coordinates[2]);
         else if (coordinates.length == 2)
            entry = AddonId.from(coordinates[0], coordinates[1]);
         else if (coordinates.length == 1)
            entry = AddonId.from(coordinates[0], UUID.randomUUID().toString());
         else
            entry = AddonId.from(UUID.randomUUID().toString(), UUID.randomUUID().toString());

         deployedAddons.put(deployment, entry);
      }
      return deployedAddons.get(deployment);
   }

   @Override
   public Class<FurnaceContainerConfiguration> getConfigurationClass()
   {
      return FurnaceContainerConfiguration.class;
   }

   @Override
   public ProtocolDescription getDefaultProtocol()
   {
      return new FurnaceProtocolDescription();
   }

   @Override
   public void setup(FurnaceContainerConfiguration configuration)
   {
      this.configuration = configuration;
   }

   @Override
   public void start() throws LifecycleException
   {
      try
      {
         this.addonDir = OperatingSystemUtils.createTempDir();
      }
      catch (IllegalStateException e)
      {
         throw new LifecycleException("Failed to create temporary addon directory", e);
      }

      try
      {
         System.out.println("Furnace test harness is initializing with addon dir [" + addonDir + "]");
         initContainer();
         startContainer();
      }
      catch (Exception e)
      {
         throw new LifecycleException("Could not start Furnace runnable.", e);
      }
   }

   private void startContainer()
   {
      waitForConfigurationRescan(new Callable<Void>()
      {
         @Override
         public Void call() throws Exception
         {
            thread.start();
            return null;
         }
      });
   }

   private void initContainer()
   {
      runnable = new ForgeRunnable(ClassLoader.getSystemClassLoader());
      furnaceHolder.setFurnace(runnable.furnace);
      thread = new Thread(runnable, "Arquillian Furnace Runtime");
      repository = (MutableAddonRepository) runnable.furnace.addRepository(AddonRepositoryMode.MUTABLE, addonDir);
   }

   @Override
   public void stop() throws LifecycleException
   {
      stopContainer();
      Files.delete(addonDir, true);
   }

   private void stopContainer()
   {
      this.repository = null;
      this.deployedAddons.clear();
      this.deploymentRepositories.clear();
      this.runnable.stop();
      this.thread = null;
   }

   @Override
   public void undeploy(Archive<?> archive) throws DeploymentException
   {
      undeploying = true;
      AddonId addonToUndeploy = getAddonEntry(deploymentInstance.get());
      AddonRegistry registry = runnable.getForge().getAddonRegistry();
      System.out.println("Furnace test harness is undeploying [" + addonToUndeploy + "] ... ");

      try
      {
         if (archive instanceof AddonDeploymentArchive)
         {
            for (DeploymentListener listener : ((AddonDeploymentArchive) archive).getDeploymentListeners())
            {
               listener.preUndeploy(furnaceHolder.getFurnace(), archive);
            }
         }

         Addon addonToStop = registry.getAddon(addonToUndeploy);
         if (addonToStop.getStatus().isLoaded())
            ((MutableAddonRepository) addonToStop.getRepository()).disable(addonToUndeploy);
         Addons.waitUntilStopped(addonToStop);

         if (archive instanceof AddonDeploymentArchive)
         {
            for (DeploymentListener listener : ((AddonDeploymentArchive) archive).getDeploymentListeners())
            {
               listener.postUndeploy(furnaceHolder.getFurnace(), archive);
            }
         }
      }
      catch (Exception e)
      {
         throw new DeploymentException("Failed to undeploy " + addonToUndeploy, e);
      }
      finally
      {
         repository.undeploy(addonToUndeploy);
      }
   }

   @Override
   public void undeploy(Descriptor descriptor) throws DeploymentException
   {
      throw new UnsupportedOperationException("Descriptors not supported by Furnace");
   }

   private class ForgeRunnable implements Runnable
   {
      private final Furnace furnace;
      private final ClassLoader loader;

      public ForgeRunnable(ClassLoader loader)
      {
         this.furnace = new FurnaceImpl();
         this.loader = loader;
      }

      public Furnace getForge()
      {
         return furnace;
      }

      @Override
      public void run()
      {
         try
         {
            ClassLoaders.executeIn(loader, new Callable<Object>()
            {
               @Override
               public Object call() throws Exception
               {
                  System.setProperty(FurnaceImpl.TEST_MODE_PROPERTY, "true");

                  furnace.setServerMode(true);
                  furnace.start(loader);

                  SecurityActions.cleanupThreadLocals(thread);
                  return null;
               }
            });
         }
         catch (Exception e)
         {
            throw new RuntimeException("Failed to start Furnace container.", e);
         }
      }

      public void stop()
      {
         furnace.stop();
      }
   }

   @Override
   public String toString()
   {
      String result = "Furnace: " + runnable.furnace.hashCode() + "\nStatus: " + runnable.furnace.getStatus() + "\n\n";
      for (AddonRepository repository : runnable.furnace.getRepositories())
      {
         result += repository + "\n";
      }
      result += "\n" + runnable.furnace;
      return result;
   }
}