/*
 * 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.shell.plugins.builtin.project;

import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.enterprise.event.Event;
import javax.inject.Inject;

import org.jboss.forge.maven.MavenPluginFacet;
import org.jboss.forge.parser.java.util.Strings;
import org.jboss.forge.project.Facet;
import org.jboss.forge.project.Project;
import org.jboss.forge.project.dependencies.Dependency;
import org.jboss.forge.project.dependencies.DependencyBuilder;
import org.jboss.forge.project.dependencies.DependencyInstaller;
import org.jboss.forge.project.dependencies.DependencyQuery;
import org.jboss.forge.project.dependencies.DependencyQueryBuilder;
import org.jboss.forge.project.dependencies.DependencyRepository;
import org.jboss.forge.project.dependencies.NonSnapshotDependencyFilter;
import org.jboss.forge.project.dependencies.ScopeType;
import org.jboss.forge.project.facets.DependencyFacet;
import org.jboss.forge.project.facets.DependencyFacet.KnownRepository;
import org.jboss.forge.project.facets.FacetNotFoundException;
import org.jboss.forge.project.facets.MetadataFacet;
import org.jboss.forge.project.facets.PackagingFacet;
import org.jboss.forge.project.facets.events.InstallFacets;
import org.jboss.forge.project.facets.events.RemoveFacets;
import org.jboss.forge.project.services.FacetFactory;
import org.jboss.forge.shell.PromptType;
import org.jboss.forge.shell.Shell;
import org.jboss.forge.shell.ShellColor;
import org.jboss.forge.shell.ShellMessages;
import org.jboss.forge.shell.plugins.Alias;
import org.jboss.forge.shell.plugins.Command;
import org.jboss.forge.shell.plugins.DefaultCommand;
import org.jboss.forge.shell.plugins.Help;
import org.jboss.forge.shell.plugins.Option;
import org.jboss.forge.shell.plugins.PipeOut;
import org.jboss.forge.shell.plugins.Plugin;
import org.jboss.forge.shell.plugins.RequiresFacet;
import org.jboss.forge.shell.plugins.RequiresProject;
import org.jboss.forge.shell.plugins.Topic;
import org.jboss.forge.shell.util.ConstraintInspector;

/**
 * @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
 */
@Alias("project")
@Topic("Project")
@RequiresProject
@RequiresFacet({ DependencyFacet.class, MavenPluginFacet.class, PackagingFacet.class })
@Help("Perform actions involving the project status, build system, or dependency management system.")
public class ProjectPlugin implements Plugin
{

   private Project project;
   private Shell shell;
   private FacetFactory factory;
   private Event<InstallFacets> installFacets;
   private Event<RemoveFacets> removeFacets;
   private DependencyInstaller dependencyInstaller;

   public ProjectPlugin()
   {
   }

   @Inject
   public ProjectPlugin(final Project project, final Shell shell, final FacetFactory factory,
            final Event<InstallFacets> installFacets, final Event<RemoveFacets> removeFacets,
            final DependencyInstaller installer)
   {
      this.project = project;
      this.shell = shell;
      this.factory = factory;
      this.installFacets = installFacets;
      this.removeFacets = removeFacets;
      this.dependencyInstaller = installer;
   }

   @DefaultCommand
   public void info(final PipeOut out)
   {
      out.print(ShellColor.BOLD, " name: ");
      out.println(project.getFacet(MetadataFacet.class).getProjectName());
      out.print(ShellColor.BOLD, " groupId:  ");
      out.println(project.getFacet(MetadataFacet.class).getTopLevelPackage());
      out.print(ShellColor.BOLD, " final artifact:  ");
      out.println(project.getFacet(PackagingFacet.class).getFinalArtifact().getName());
      out.print(ShellColor.BOLD, " packaging:  ");
      out.println(project.getFacet(PackagingFacet.class).getPackagingType().getType());
      out.print(ShellColor.BOLD, " dir:  ");
      out.println(project.getProjectRoot().getFullyQualifiedName());
   }

   @Command("install-facet")
   public void installFacet(@Option(required = true,
            completer = AvailableFacetsCompleter.class,
            description = "Name of the facet to install") final String facetName)
   {
      try
      {
         Facet facet = factory.getFacetByName(facetName);
         installFacets.fire(new InstallFacets(facet.getClass()));
      }
      catch (FacetNotFoundException e)
      {
         throw new RuntimeException("Could not find a facet with the name: " + facetName
                  + "; use 'project list-facets' to list all available facets.", e);
      }
   }

   @Command("remove-facet")
   public void removeFacet(@Option(required = true,
            completer = InstalledFacetsCompleter.class,
            description = "Name of the facet to install") final String facetName)
   {
      try
      {
         Facet facet = factory.getFacetByName(facetName);
         removeFacets.fire(new RemoveFacets(facet.getClass()));
      }
      catch (FacetNotFoundException e)
      {
         throw new RuntimeException("Could not find a facet with the name: " + facetName
                  + "; use 'project list-facets' to list all available facets.", e);
      }
   }

   /*
    * Dependency manipulation
    */
   @Command(value = "add-dependency", help = "Add a dependency to this project.")
   public void addDep(
            @Option(required = true,
                     type = PromptType.DEPENDENCY_ID,
                     description = "[ groupId:artifactId {:version :scope :packaging} ]",
                     help = "dependency identifier, ex: \"org.jboss.forge:forge-api:1.0.0\"") Dependency gav,
            @Option(type = PromptType.DEPENDENCY_ID,
                     name = "exclude",
                     description = "[ groupId:artifactId ]",
                     help = "exclusion identifier, ex: \"org.jboss.forge:forge-api\"") final Dependency exclusion,
            final PipeOut out
            )
   {
      final DependencyFacet deps = project.getFacet(DependencyFacet.class);
      final boolean hasEffectiveManagedDependency = deps.hasEffectiveManagedDependency(gav);
      Dependency gavCopy = DependencyBuilder.create(gav);

      if (hasEffectiveManagedDependency)
      {
         Dependency existingDep = deps.getEffectiveManagedDependency(gav);
         if (!shell.promptBoolean(String.format("Dependency is managed [%s:%s:%s], reference the managed dependency?",
                  existingDep.getGroupId(), existingDep.getArtifactId(), existingDep.getVersion()), true))
         {
            if (Strings.isNullOrEmpty(gavCopy.getVersion()))
            {
               DependencyQuery query = DependencyQueryBuilder.create(gavCopy).setFilter(
                        new NonSnapshotDependencyFilter());
               gavCopy = shell.promptChoiceTyped("Add which version?", deps.resolveAvailableVersions(query));
            }
         }
      }
      this.dependencyInstaller.install(project, gavCopy);
   }

   @Command(value = "find-dependency", help = "Search for dependencies in all configured project repositories.")
   public void searchDep(
            @Option(required = true,
                     help = "dependency identifier, ex: \"org.jboss.forge:forge-api:1.0.0\"",
                     description = "[ groupId:artifactId {:version:scope:packaging} ]",
                     type = PromptType.DEPENDENCY_ID
            ) Dependency gav,
            final PipeOut out
            )
   {
      DependencyFacet deps = project.getFacet(DependencyFacet.class);
      if ((gav.getVersion() == null) || gav.getVersion().trim().isEmpty())
      {
         gav = DependencyBuilder.create(gav).setVersion("[0,)");
      }
      List<Dependency> versions = deps.resolveAvailableVersions(gav);

      for (Dependency dep : versions)
      {
         out.println(DependencyBuilder.toString(dep));
      }

      if (versions.isEmpty())
      {
         out.println("No artifacts found for the query [" + gav + "]");
      }
   }

   @Command(value = "remove-dependency", help = "Remove a dependency from this project")
   public void removeDep(
            @Option(required = true,
                     type = PromptType.DEPENDENCY_ID,
                     description = "[ groupId :artifactId {:version :scope :packaging} ]",
                     help = "dependency identifier, ex: \"org.jboss.forge:forge-api:1.0.0\"") final Dependency gav,
            final PipeOut out
            )
   {
      DependencyFacet deps = project.getFacet(DependencyFacet.class);
      if (deps.hasEffectiveDependency(gav))
      {
         deps.removeDependency(gav);
         out.println("Removed dependency [" + gav + "]");
      }
      else
      {
         out.println("Dependency [" + gav + "] not found in project... ");
      }
   }

   @Command(value = "list-dependencies", help = "List all dependencies this project includes")
   public void listDeps(final PipeOut out)
   {
      DependencyFacet deps = project.getFacet(DependencyFacet.class);

      for (Dependency dep : deps.getDependencies())
      {
         printDep(out, dep);
      }
   }

   /*
    * Managed Dependency manipulation
    */
   @Command(value = "add-managed-dependency", help = "Add a managed dependency to this project.")
   public void addManDep(
            @Option(required = true,
                     type = PromptType.DEPENDENCY_ID,
                     description = "[ groupId :artifactId {:version :scope :packaging} ]",
                     help = "managed dependency identifier, ex: \"org.jboss.forge:forge-api:1.0.0\"") Dependency gav,
            final PipeOut out
            )
   {
      DependencyFacet manDeps = project.getFacet(DependencyFacet.class);

      if (!manDeps.hasEffectiveManagedDependency(gav)
               || shell.promptBoolean(
                        "Managed dependency already exists [" + gav.getGroupId() + ":" + gav.getArtifactId()
                                 + "], continue?", true))
      {
         DependencyBuilder search = DependencyBuilder.create(gav).setVersion("[0,)");
         List<Dependency> availableVersions = manDeps.resolveAvailableVersions(search);

         if (availableVersions.isEmpty())
         {
            throw new RuntimeException("No available versions resolved for managed dependency [" + gav + "]");
         }

         if (!availableVersions.contains(gav))
         {
            ShellMessages.info(out, "No artifact found for managed dependency [" + gav + "]");
            if (availableVersions.size() > 1)
            {
               gav = shell.promptChoiceTyped("Add which version?", availableVersions);
            }
            else if (shell.promptBoolean("Use [" + availableVersions.get(0) + "] instead?", true))
            {
               gav = availableVersions.get(0);
            }
            else
            {
               throw new RuntimeException("Could not add managed dependency [" + gav + "]");
            }
         }

         if (manDeps.hasEffectiveManagedDependency(gav))
         {
            Dependency managedDependency = manDeps.getManagedDependency(gav);
            manDeps.removeManagedDependency(managedDependency);
         }
         manDeps.addManagedDependency(gav);
         out.println("Added managed dependency [" + gav + "]");
      }
      else
      {
         ShellMessages.info(out, "Aborted.");
      }
   }

   @Command(value = "find-managed-dependency", help = "Search for managed dependencies in all configured project repositories.")
   public void searchManDep(
            @Option(required = true,
                     help = "managed dependency identifier, ex: \"org.jboss.forge:forge-api:1.0.0\"",
                     description = "[ groupId:artifactId {:version:scope:packaging} ]",
                     type = PromptType.DEPENDENCY_ID
            ) Dependency gav,
            @Option(required = false,
                     flagOnly = true,
                     help = "Perform a search only within the locally configured repository",
                     name = "offlineSearch"
            ) final boolean offline,
            final PipeOut out
            )
   {
      DependencyFacet manDeps = project.getFacet(DependencyFacet.class);
      if ((gav.getVersion() == null) || gav.getVersion().trim().isEmpty())
      {
         gav = DependencyBuilder.create(gav).setVersion("[0,)");
      }
      List<Dependency> versions = manDeps.resolveAvailableVersions(gav);

      for (Dependency manDep : versions)
      {
         out.println(DependencyBuilder.toString(manDep));
      }

      if (versions.isEmpty())
      {
         out.println("No artifacts found for the query [" + gav + "]");
      }
   }

   @Command(value = "remove-managed-dependency", help = "Remove a managed dependency from this project")
   public void removeManDep(
            @Option(required = true,
                     type = PromptType.DEPENDENCY_ID,
                     description = "[ groupId :artifactId {:version :scope :packaging} ]",
                     help = "managed dependency identifier, ex: \"org.jboss.forge:forge-api:1.0.0\"") final Dependency gav,
            final PipeOut out
            )
   {
      DependencyFacet manDeps = project.getFacet(DependencyFacet.class);
      if (manDeps.hasEffectiveManagedDependency(gav))
      {
         manDeps.removeManagedDependency(gav);
         out.println("Removed managed dependency [" + gav + "]");
      }
      else
      {
         out.println("Managed dependency [" + gav + "] not found in project... ");
      }
   }

   @Command(value = "list-managed-dependencies", help = "List all managed dependencies this project includes")
   public void listManDeps(final PipeOut out)
   {
      DependencyFacet manDeps = project.getFacet(DependencyFacet.class);

      for (Dependency manDep : manDeps.getManagedDependencies())
      {
         printDep(out, manDep);
      }
   }

   /*
    * Property manipulation
    */
   @Command("set-property")
   public void addProp(
            @Option(required = true,
                     name = "name",
                     completer = DependencyPropertyCompleter.class) final String name,
            @Option(required = true,
                     name = "value") final String value,
            final PipeOut out)
   {
      DependencyFacet deps = project.getFacet(DependencyFacet.class);

      if (deps.getProperties().containsKey(name) &&
               shell.promptBoolean("Update property [" + name + "=" + deps.getProperty(name) + "] to new value ["
                        + value + "]", true))
      {
         deps.setProperty(name, value);
         out.println("Updated...");
      }
      else
      {
         deps.setProperty(name, value);
         out.println("Set property [" + name + "=" + value + "]");
      }
   }

   @Command("remove-property")
   public void removeProp(
            @Option(required = true, description = "propname",
                     completer = DependencyPropertyCompleter.class) final String name,
            final PipeOut out)
   {
      DependencyFacet deps = project.getFacet(DependencyFacet.class);
      if (deps.getProperties().containsKey(name))
      {
         String value = deps.removeProperty(name);
         out.println("Removed property [" + name + "=" + value + "]");
      }
      else
      {
         out.println("No such property [" + name + "]");
      }
   }

   @Command("list-properties")
   public void listProps(final PipeOut out)
   {
      DependencyFacet deps = project.getFacet(DependencyFacet.class);
      Map<String, String> properties = deps.getProperties();

      for (Entry<String, String> entry : properties.entrySet())
      {
         out.print(entry.getKey() + "=");
         out.println(ShellColor.BLUE, entry.getValue());
      }
   }

   @Command("list-facets")
   public void listFacets(PipeOut out)
   {
      Set<Class<? extends Facet>> facets = factory.getFacetTypes();

      out.println(ShellColor.BOLD, "NOT INSTALLED");
      for (Class<? extends Facet> type : facets)
      {
         String name = ConstraintInspector.getName(type);
         if (!project.hasFacet(type))
         {
            out.println("- " + name + "\t[" + type.getName() + "]");
         }
      }

      out.println();

      out.println(ShellColor.BOLD, "INSTALLED");
      for (Class<? extends Facet> type : facets)
      {
         String name = ConstraintInspector.getName(type);
         if (project.hasFacet(type) && !project.getFacet(type).isInstalled())
         {
            out.println(ShellColor.RED, "? " + name + "\t[" + type.getName()
                     + " - WARN: facet is no longer available]");
         }
         else if (project.hasFacet(type))
         {
            out.println(ShellColor.GREEN, "+ " + name + "\t[" + type.getName() + "]");
         }
      }
   }

   /*
    * Repositories
    */
   @Command("add-known-repository")
   public void repoAdd(
            @Option(description = "type...", required = true) final KnownRepository repo,
            final PipeOut out)
   {
      DependencyFacet deps = project.getFacet(DependencyFacet.class);

      if (deps.hasRepository(repo))
      {
         out.println("Repository exists [" + repo.name() + "->" + repo.getUrl() + "]");
      }
      else
      {
         deps.addRepository(repo);
         out.println("Added repository [" + repo.name() + "->" + repo.getUrl() + "]");
      }
   }

   @Command("add-repository")
   public void repoAdd(
            @Option(description = "repository name...", required = true) final String name,
            @Option(description = "repository URL...", required = true) final String url,
            final PipeOut out)
   {
      DependencyFacet deps = project.getFacet(DependencyFacet.class);

      if (deps.hasRepository(url))
      {
         out.println("Repository exists [" + url + "]");
      }
      else
      {
         deps.addRepository(name, url);
         out.println("Added repository [" + name + "->" + url + "]");
      }
   }

   @Command("remove-repository")
   public void repoRemove(
            @Option(required = true, description = "repo url...",
                     completer = RepositoryCompleter.class) final String url,
            final PipeOut out)
   {
      DependencyFacet deps = project.getFacet(DependencyFacet.class);

      DependencyRepository rep;
      if ((rep = deps.removeRepository(url)) != null)
      {
         out.println("Removed repository [" + rep.getId() + "->" + rep.getUrl() + "]");
      }
      else
      {
         out.println("No repository with url [" + url + "]");
      }
   }

   @Command("list-repositories")
   public void repoList(final PipeOut out)
   {
      DependencyFacet deps = project.getFacet(DependencyFacet.class);
      List<DependencyRepository> repos = deps.getRepositories();

      for (DependencyRepository repo : repos)
      {
         out.print(repo.getId() + "->");
         out.println(ShellColor.BLUE, repo.getUrl());
      }
   }

   @Command("list-plugin-repositories")
   public void pluginRepoList(final PipeOut out)
   {
      MavenPluginFacet deps = project.getFacet(MavenPluginFacet.class);
      List<DependencyRepository> repos = deps.getPluginRepositories();

      for (DependencyRepository repo : repos)
      {
         out.print(repo.getId() + "->");
         out.println(ShellColor.BLUE, repo.getUrl());
      }
   }

   @Command("add-known-plugin-repository")
   public void pluginRepoAdd(
            @Option(description = "type...", required = true) final MavenPluginFacet.KnownRepository repo,
            final PipeOut out)
   {
      MavenPluginFacet deps = project.getFacet(MavenPluginFacet.class);

      if (deps.hasPluginRepository(repo))
      {
         out.println("Plugin repository exists [" + repo.name() + "->" + repo.getUrl() + "]");
      }
      else
      {
         deps.addPluginRepository(repo);
         out.println("Added plugin repository [" + repo.name() + "->" + repo.getUrl() + "]");
      }
   }

   @Command("add-plugin-repository")
   public void pluginRepoAdd(
            @Option(description = "repository name...", required = true) final String name,
            @Option(description = "repository URL...", required = true) final String url,
            final PipeOut out)
   {
      MavenPluginFacet deps = project.getFacet(MavenPluginFacet.class);

      if (deps.hasPluginRepository(url))
      {
         out.println("Plugin repository exists [" + url + "]");
      }
      else
      {
         deps.addPluginRepository(name, url);
         out.println("Added plugin repository [" + name + "->" + url + "]");
      }
   }

   @Command("remove-plugin-repository")
   public void pluginRepoRemove(
            @Option(required = true, description = "repo url...",
                     completer = PluginRepositoryCompleter.class) final String url,
            final PipeOut out)
   {
      MavenPluginFacet deps = project.getFacet(MavenPluginFacet.class);

      DependencyRepository rep;
      if ((rep = deps.removePluginRepository(url)) != null)
      {
         out.println("Removed plugin repository [" + rep.getId() + "->" + rep.getUrl() + "]");
      }
      else
      {
         out.println("No plugin repository with url [" + url + "]");
      }
   }

   /*
    * Utils
    */
   private void printDep(final PipeOut out, final Dependency dep)
   {
      out.println(
               out.renderColor(ShellColor.BLUE, dep.getGroupId())
                        +
                        out.renderColor(ShellColor.BOLD, " : ")
                        +
                        out.renderColor(ShellColor.BLUE, dep.getArtifactId())
                        +
                        out.renderColor(ShellColor.BOLD, " : ")
                        +
                        out.renderColor(ShellColor.NONE, dep.getVersion() == null ? "" : dep.getVersion())
                        +
                        out.renderColor(ShellColor.BOLD, " : ")
                        +
                        out.renderColor(ShellColor.NONE, dep.getPackagingType() == null ? "" : dep
                                 .getPackagingType().toLowerCase())
                        +
                        out.renderColor(ShellColor.BOLD, " : ")
                        +
                        out.renderColor(determineDependencyShellColor(dep.getScopeTypeEnum()),
                                 dep.getScopeType() == null ? "compile" : dep.getScopeType()
                                          .toLowerCase()));
   }

   private ShellColor determineDependencyShellColor(final ScopeType type)
   {
      if (type == null)
      {
         return ShellColor.YELLOW;
      }
      switch (type)
      {
      case PROVIDED:
         return ShellColor.GREEN;
      case COMPILE:
         return ShellColor.YELLOW;
      case RUNTIME:
         return ShellColor.MAGENTA;
      case OTHER:
         return ShellColor.BLACK;
      case SYSTEM:
         return ShellColor.BLACK;
      case TEST:
         return ShellColor.BLUE;
      default:
         break;
      }
      return ShellColor.NONE;
   }

}
