/*
 * Copyright 2012 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.addon.projects.impl;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;

import javax.enterprise.event.Observes;
import javax.inject.Inject;
import javax.inject.Singleton;

import org.jboss.forge.addon.facets.Facet;
import org.jboss.forge.addon.facets.FacetFactory;
import org.jboss.forge.addon.projects.BuildSystem;
import org.jboss.forge.addon.projects.BuildSystemFacet;
import org.jboss.forge.addon.projects.Project;
import org.jboss.forge.addon.projects.ProjectAssociationProvider;
import org.jboss.forge.addon.projects.ProjectFacet;
import org.jboss.forge.addon.projects.ProjectFactory;
import org.jboss.forge.addon.projects.ProjectListener;
import org.jboss.forge.addon.projects.spi.ProjectCache;
import org.jboss.forge.addon.resource.DirectoryResource;
import org.jboss.forge.addon.resource.FileResource;
import org.jboss.forge.addon.resource.ResourceFactory;
import org.jboss.forge.addon.resource.events.ResourceEvent;
import org.jboss.forge.addon.resource.monitor.ResourceListener;
import org.jboss.forge.addon.resource.monitor.ResourceMonitor;
import org.jboss.forge.furnace.addons.AddonRegistry;
import org.jboss.forge.furnace.container.cdi.events.Local;
import org.jboss.forge.furnace.event.PreShutdown;
import org.jboss.forge.furnace.services.Imported;
import org.jboss.forge.furnace.spi.ListenerRegistration;
import org.jboss.forge.furnace.util.Assert;
import org.jboss.forge.furnace.util.OperatingSystemUtils;
import org.jboss.forge.furnace.util.Predicate;

/**
 * @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
 */
@Singleton
public class ProjectFactoryImpl implements ProjectFactory
{
   private static final Logger log = Logger.getLogger(ProjectFactoryImpl.class.getName());

   @Inject
   private AddonRegistry registry;

   @Inject
   private ResourceFactory resourceFactory;

   @Inject
   private FacetFactory factory;

   @Inject
   private Imported<ProjectListener> builtInListeners;

   @Inject
   private Imported<ProjectCache> caches;

   private final List<ListenerRegistration<ResourceListener>> listeners = new ArrayList<ListenerRegistration<ResourceListener>>();

   void shutdown(@Observes @Local PreShutdown event)
   {
      invalidateCaches();
      for (ListenerRegistration<ResourceListener> registration : listeners)
      {
         registration.removeListener();
      }
   }

   private final Predicate<ProjectFacet> notBuildSystemFilter = new Predicate<ProjectFacet>()
   {
      @Override
      public boolean accept(ProjectFacet type)
      {
         return !(type instanceof BuildSystemFacet);
      }
   };

   private final List<ProjectListener> projectListeners = new ArrayList<ProjectListener>();

   private final Predicate<Project> acceptsAllProjects = new Predicate<Project>()
   {
      @Override
      public boolean accept(Project type)
      {
         return true;
      }
   };

   @Override
   public Project findProject(FileResource<?> target)
   {
      Project result = null;
      Imported<BuildSystem> instances = registry.getServices(BuildSystem.class);
      for (BuildSystem buildSystem : instances)
      {
         try
         {
            result = findProject(target, buildSystem);
         }
         finally
         {
            instances.release(buildSystem);
         }

         if (result != null)
            break;
      }
      return result;
   }

   @Override
   public Project findProject(FileResource<?> target, BuildSystem buildSystem)
   {
      return findProject(target, buildSystem, acceptsAllProjects);
   }

   @Override
   public Project findProject(FileResource<?> target, Predicate<Project> filter)
   {
      if (filter == null)
      {
         filter = acceptsAllProjects;
      }

      Project result = null;
      Imported<BuildSystem> instances = registry.getServices(BuildSystem.class);
      for (BuildSystem buildSystem : instances)
      {
         try
         {
            result = findProject(target, buildSystem, filter);
         }
         finally
         {
            instances.release(buildSystem);
         }

         if (result != null)
            break;
      }

      return result;
   }

   @Override
   public Project findProject(FileResource<?> target, BuildSystem buildSystem, Predicate<Project> filter)
   {
      if (filter == null)
      {
         filter = acceptsAllProjects;
      }

      Project result = null;
      DirectoryResource r = (target instanceof DirectoryResource) ? (DirectoryResource) target : target
               .getParent();
      while (r != null && result == null)
      {
         Iterator<ProjectCache> cacheIterator = caches.iterator();
         while (cacheIterator.hasNext() && result == null)
         {
            ProjectCache cache = cacheIterator.next();
            try
            {
               result = cache.get(r);
               if (result != null && !filter.accept(result))
                  result = null;
               if (result != null)
                  break;
            }
            finally
            {
               caches.release(cache);
            }
         }

         if (result == null && buildSystem.containsProject(r))
         {
            result = buildSystem.createProject(r);
            if (result != null && !filter.accept(result))
               result = null;

            if (result != null)
            {
               registerAvailableFacets(result);
               cacheProject(result);
            }
         }

         r = r.getParent();
      }

      return result;
   }

   @Override
   public Project createProject(DirectoryResource projectDir, BuildSystem buildSystem)
   {
      return createProject(projectDir, buildSystem, null);
   }

   @Override
   public Project createProject(DirectoryResource target, BuildSystem buildSystem,
            Iterable<Class<? extends ProjectFacet>> facetTypes)
   {
      Assert.notNull(target, "Target project directory must not be null.");
      Assert.notNull(buildSystem, "Build system type must not be null.");

      if (facetTypes != null)
         Assert.isTrue(isBuildable(buildSystem, facetTypes),
                  "The provided build system [" + buildSystem.getType()
                           + "] cannot create a project that requires facets of the following types: "
                           + getMissingBuildSystemFacets(buildSystem, getRequiredBuildSystemFacets(facetTypes)));

      Project result = buildSystem.createProject(target);
      if (result != null)
      {
         DirectoryResource parentDir = result.getProjectRoot().getParent().reify(DirectoryResource.class);
         if (parentDir != null)
         {
            Imported<ProjectAssociationProvider> buildSystemInstances = registry
                     .getServices(ProjectAssociationProvider.class);
            for (ProjectAssociationProvider provider : buildSystemInstances)
            {
               if (provider.canAssociate(result, parentDir))
               {
                  provider.associate(result, parentDir);
               }
               buildSystemInstances.release(provider);
            }
         }
      }

      if (result != null && facetTypes != null)
      {
         for (Class<? extends ProjectFacet> facetType : facetTypes)
         {
            try
            {
               if (!BuildSystemFacet.class.isAssignableFrom(facetType))
               {
                  Iterable<? extends ProjectFacet> facets = factory.createFacets(result, facetType);
                  for (ProjectFacet projectFacet : facets)
                  {
                     if (factory.install(result, projectFacet, notBuildSystemFilter))
                     {
                        break;
                     }
                  }
               }
            }
            catch (RuntimeException e)
            {
               throw new IllegalStateException("Could not install " + Facet.class.getSimpleName() + " of type ["
                        + facetType + "] into Project [" + result + "]", e);
            }
         }
      }

      if (result != null)
      {
         registerAvailableFacets(result);
      }

      if (result != null)
      {
         cacheProject(result);
         fireProjectCreated(result);
      }

      return result;
   }

   private Iterable<Class<? extends BuildSystemFacet>> getMissingBuildSystemFacets(BuildSystem buildSystem,
            Iterable<Class<? extends BuildSystemFacet>> requiredFacets)
   {
      Set<Class<? extends BuildSystemFacet>> result = new HashSet<Class<? extends BuildSystemFacet>>();
      Iterable<Class<? extends BuildSystemFacet>> providedFacetTypes = buildSystem.getProvidedFacetTypes();
      if (requiredFacets != null && providedFacetTypes != null)
      {
         for (Class<? extends BuildSystemFacet> required : requiredFacets)
         {
            boolean found = false;
            for (Class<? extends BuildSystemFacet> provided : providedFacetTypes)
            {
               if (provided.isAssignableFrom(required))
                  found = true;
            }
            if (!found)
               result.add(required);
         }
      }
      return result;
   }

   private boolean isBuildable(BuildSystem buildSystem, Iterable<Class<? extends ProjectFacet>> facets)
   {
      boolean result = false;
      Iterable<Class<? extends BuildSystemFacet>> requiredFacets = getRequiredBuildSystemFacets(facets);
      if (requiredFacets == null)
      {
         result = true;
      }
      else
      {
         result = !getMissingBuildSystemFacets(buildSystem, requiredFacets).iterator().hasNext();
      }
      return result;
   }

   @SuppressWarnings("unchecked")
   private Iterable<Class<? extends BuildSystemFacet>> getRequiredBuildSystemFacets(
            Iterable<Class<? extends ProjectFacet>> facets)
   {
      Set<Class<? extends BuildSystemFacet>> result = new HashSet<Class<? extends BuildSystemFacet>>();
      for (Class<? extends ProjectFacet> facetType : facets)
      {
         if (BuildSystemFacet.class.isAssignableFrom(facetType))
         {
            result.add((Class<? extends BuildSystemFacet>) facetType);
         }
      }
      return result;
   }

   private void registerAvailableFacets(Project result)
   {
      for (Class<ProjectFacet> type : registry.getExportedTypes(ProjectFacet.class))
      {
         Iterable<ProjectFacet> facets = factory.createFacets(result, type);
         for (ProjectFacet facet : facets)
         {
            if (facet != null && factory.register(result, facet))
            {
               log.fine("Registered Facet [" + facet + "] into Project [" + result + "]");
            }
         }
      }
   }

   private void cacheProject(final Project project)
   {
      for (ProjectCache cache : caches)
      {
         try
         {
            cache.store(project);
         }
         finally
         {
            caches.release(cache);
         }
      }

      final ResourceMonitor monitor = project.getProjectRoot().monitor();
      ListenerRegistration<ResourceListener> registration = monitor.addResourceListener(new ResourceListener()
      {
         @Override
         public void processEvent(ResourceEvent event)
         {
            for (ProjectCache cache : caches)
            {
               try
               {
                  cache.evict(project);
               }
               finally
               {
                  caches.release(cache);
               }
            }
            monitor.cancel();
         }
      });

      this.listeners.add(registration);
   }

   private void fireProjectCreated(Project project)
   {
      for (ProjectListener listener : builtInListeners)
      {
         listener.projectCreated(project);
      }
      for (ProjectListener listener : projectListeners)
      {
         listener.projectCreated(project);
      }
   }

   @Override
   public Project createTempProject() throws IllegalStateException
   {
      return createTempProject(Collections.<Class<? extends ProjectFacet>> emptySet());
   }

   @Override
   public Project createTempProject(Iterable<Class<? extends ProjectFacet>> facetTypes) throws IllegalStateException
   {
      Imported<BuildSystem> buildSystems = registry.getServices(BuildSystem.class);
      if (buildSystems.isAmbiguous())
         throw new IllegalStateException(
                  "Cannot create generic temporary project in environment where multiple build systems are available. "
                           + "A single build system must be selected.");

      BuildSystem buildSystem = buildSystems.get();
      try
      {
         return createTempProject(buildSystem, facetTypes);
      }
      finally
      {
         buildSystems.release(buildSystem);
      }
   }

   @Override
   public Project createTempProject(BuildSystem buildSystem)
   {
      return createTempProject(buildSystem, Collections.<Class<? extends ProjectFacet>> emptySet());
   }

   @Override
   public Project createTempProject(BuildSystem buildSystem, Iterable<Class<? extends ProjectFacet>> facetTypes)
   {
      File rootDirectory = OperatingSystemUtils.createTempDir();
      DirectoryResource addonDir = resourceFactory.create(DirectoryResource.class, rootDirectory);
      DirectoryResource projectDir = addonDir.createTempResource();
      projectDir.deleteOnExit();
      Project project = createProject(projectDir, buildSystem, facetTypes);
      return project;
   }

   @Override
   public ListenerRegistration<ProjectListener> addProjectListener(final ProjectListener listener)
   {
      Assert.notNull(listener, "Project listener must not be null.");
      projectListeners.add(listener);
      return new ListenerRegistration<ProjectListener>()
      {
         @Override
         public ProjectListener removeListener()
         {
            projectListeners.remove(listener);
            return listener;
         }
      };
   }

   @Override
   public boolean containsProject(DirectoryResource bound, FileResource<?> target)
   {
      boolean found = false;
      Imported<BuildSystem> instances = registry.getServices(BuildSystem.class);
      for (BuildSystem buildSystem : instances)
      {
         try
         {
            found = containsProject(bound, target, buildSystem);
            if (found)
               break;
         }
         finally
         {
            instances.release(buildSystem);
         }
      }
      return found;
   }

   @Override
   public boolean containsProject(DirectoryResource bound, FileResource<?> target, BuildSystem buildSystem)
   {
      Assert.notNull(bound, "Boundary should not be null");
      Assert.isTrue(isParent(bound, target), "Target should be a child of bound");
      DirectoryResource dir = (target instanceof DirectoryResource) ? (DirectoryResource) target : target.getParent();
      boolean found = false;
      DirectoryResource r = bound;
      while (r != null && !found)
      {
         found = buildSystem.containsProject(r);
         if (dir.equals(r))
         {
            break;
         }
         r = r.getParent();
      }
      return found;
   }

   private boolean isParent(DirectoryResource dir, FileResource<?> child)
   {
      DirectoryResource childDir = (child instanceof DirectoryResource) ? child.reify(DirectoryResource.class) : child
               .getParent();
      while (childDir != null)
      {
         if (dir.equals(childDir))
         {
            return true;
         }
         childDir = childDir.getParent();
      }
      return false;
   }

   @Override
   public boolean containsProject(FileResource<?> target)
   {
      boolean found = false;
      Imported<BuildSystem> instances = registry.getServices(BuildSystem.class);
      for (BuildSystem buildSystem : instances)
      {
         try
         {
            found = containsProject(target, buildSystem);
            if (found)
               break;
         }
         finally
         {
            instances.release(buildSystem);
         }
      }
      return found;
   }

   @Override
   public boolean containsProject(FileResource<?> target, BuildSystem buildSystem)
   {
      Assert.notNull(target, "Target resource must not be null.");
      Assert.notNull(buildSystem, "Project build system must not be null.");
      boolean found = false;
      DirectoryResource dir = (target instanceof DirectoryResource) ? (DirectoryResource) target : target.getParent();
      DirectoryResource r = dir;
      while (r != null && !found)
      {
         found = buildSystem.containsProject(r);
         r = r.getParent();
      }
      return found;
   }

   @Override
   public void invalidateCaches()
   {
      for (ProjectCache cache : caches)
      {
         try
         {
            cache.invalidate();
         }
         finally
         {
            caches.release(cache);
         }
      }
   }
}
