/*
 * 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.dev.mvn;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;

import javax.inject.Inject;

import org.apache.maven.model.Model;
import org.apache.maven.model.PluginExecution;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.codehaus.plexus.util.xml.Xpp3DomBuilder;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.jboss.forge.maven.MavenCoreFacet;
import org.jboss.forge.parser.xml.Node;
import org.jboss.forge.parser.xml.XMLParser;
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.facets.JavaSourceFacet;
import org.jboss.forge.shell.PromptType;
import org.jboss.forge.shell.Shell;
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.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;

/**
 * @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
 */

@Alias("shade")
@Topic("Project")
@RequiresProject
@RequiresFacet({ MavenCoreFacet.class, JavaSourceFacet.class })
@Help("Provides quick configurations for the maven-shade-plugin. " +
         "Find more information at the project homepage ( " +
         "http://maven.apache.org/plugins/maven-shade-plugin/ )")
public class MavenShadePlugin implements Plugin
{
   private final Shell shell;
   private final Project project;
   private final Dependency dep = DependencyBuilder.create("org.apache.maven.plugins:maven-shade-plugin:1.4");

   @Inject
   public MavenShadePlugin(final Shell shell, final Project project)
   {
      this.shell = shell;
      this.project = project;
   }

   @Command(help = "Setup the basic shade configuration (includes all dependencies)")
   public void setup(final PipeOut out) throws XmlPullParserException, IOException
   {
      if (!isInstalled())
      {
         install();
      }

      if (isInstalled())
      {
         ShellMessages.success(out, "Shade plugin is installed.");
      }
   }

   @Command(help = "Add a dependency to the uber-jar")
   public void include(@Option(completer = InstalledDependencyCompleter.class,
            description = "the dependency pattern to include [e.g: com.example.*:*]") final Dependency dep)
   {
      modifyConfiguration(new ModifyNode()
      {
         @Override
         public void modify(final Node configuration)
         {
            Node excludes = configuration
                     .getOrCreate("artifactSet")
                     .getOrCreate("includes");

            for (Node n : excludes.get("include"))
            {
               if (DependencyBuilder.areEquivalent(DependencyBuilder.create(n.getText()), dep))
               {
                  return;
               }
            }

            excludes.createChild("include").text(dep.getGroupId() + ":" + dep.getArtifactId());
         }
      });
   }

   @Command(help = "Exclude a dependency from the uber-jar")
   public void exclude(@Option(completer = InstalledDependencyCompleter.class) final Dependency dep)
   {
      modifyConfiguration(new ModifyNode()
      {
         @Override
         public void modify(final Node configuration)
         {
            Node excludes = configuration
                     .getOrCreate("artifactSet")
                     .getOrCreate("excludes");

            for (Node n : excludes.get("exclude"))
            {
               if (DependencyBuilder.areEquivalent(DependencyBuilder.create(n.getText()), dep))
               {
                  return;
               }
            }

            excludes.createChild("exclude").text(dep.getGroupId() + ":" + dep.getArtifactId());
         }
      });
   }

   @Command(help = "Reset the current shade configuration (includes all dependencies).")
   public void reset() throws XmlPullParserException, IOException
   {
      assertInstalled();
      if (shell.promptBoolean("Really reset configuration?"))
      {
         removeShadePlugin();
         install();
      }
   }

   @Command(help = "Remove all shade configurations from the POM")
   public void remove() throws XmlPullParserException, IOException
   {
      assertInstalled();
      if (shell.promptBoolean("Really remove all shade configuration?"))
      {
         removeShadePlugin();
         ShellMessages.info(shell, "Removed all shade configuration from POM.");
      }
      else if (!isInstalled())
         ShellMessages.success(shell, "Shade is not configured.");
      else
         ShellMessages.info(shell, "Aborted.");
   }

   @Command(help = "Relocates bundled dependency classes to a new package")
   public void relocate(
            @Option(name = "pattern",
                     help = "the original package",
                     type = PromptType.JAVA_PACKAGE,
                     required = true) final String pattern,
            @Option(name = "shadedPattern",
                     help = "the renamed \"shaded\" package",
                     type = PromptType.JAVA_PACKAGE,
                     required = true) final String shadedPattern,
            @Option(name = "excludes",
                     help = "packages to exclude from shading",
                     type = PromptType.JAVA_PACKAGE) final String... excludes)
            throws XmlPullParserException, IOException
   {

      modifyConfiguration(new ModifyNode()
      {
         @Override
         public void modify(final Node configuration)
         {
            Node relocationNode = configuration.getOrCreate("relocations").createChild("relocation");
            relocationNode.createChild("pattern").text(pattern);
            relocationNode.createChild("shadedPattern").text(shadedPattern);

            String excludeMsg = "";
            if ((excludes != null) && (excludes.length > 0))
            {
               Node excludesNode = relocationNode.createChild("excludes");
               for (String e : excludes)
               {
                  excludesNode.createChild("exclude").text(e);
               }
               excludeMsg = ", excluding " + Arrays.asList(excludes);
            }
            ShellMessages.success(shell, "Relocating [" + pattern + "] to [" + shadedPattern + "]" + excludeMsg);
         }

      });
   }

   @Command(value = "make-executable", help = "Make the resulting jar executable by specifying the target Main class")
   public void makeExecutable(
            @Option(name = "mainClass",
                     description = "the fully qualified main class [e.g: com.example.Main]",
                     type = PromptType.JAVA_CLASS,
                     required = true) final String mainClass) throws XmlPullParserException, IOException
   {
      modifyConfiguration(new ModifyNode()
      {
         @Override
         public void modify(final Node configuration)
         {
            Node relocationNode = configuration
                     .getOrCreate("transformers")
                     .getOrCreate(
                              "transformer@implementation=org.apache.maven.plugins.shade.resource.ManifestResourceTransformer");
            relocationNode.getOrCreate("mainClass").text(mainClass);
         }
      });

   }

   /*
    * Helpers
    */
   private void modifyConfiguration(final ModifyNode command)
   {
      try
      {
         assertInstalled();

         MavenCoreFacet mvn = project.getFacet(MavenCoreFacet.class);
         Model pom = mvn.getPOM();

         org.apache.maven.model.Plugin plugin = getPlugin(pom);
         PluginExecution execution = plugin.getExecutions().get(0);
         Node configuration = XMLParser.parse(((Xpp3Dom) execution.getConfiguration()).toUnescapedString());

         command.modify(configuration);

         execution.setConfiguration(Xpp3DomBuilder.build(XMLParser.toXMLInputStream(configuration), "UTF-8"));
         mvn.setPOM(pom);
      }
      catch (Exception e)
      {
         throw new RuntimeException("Error updating configuration", e);
      }
   }

   private void removeShadePlugin()
   {
      MavenCoreFacet mvn = project.getFacet(MavenCoreFacet.class);
      Model pom = mvn.getPOM();

      pom.getBuild().removePlugin(getPlugin(pom));
      mvn.setPOM(pom);
   }

   private void assertInstalled()
   {
      if (!isInstalled())
      {
         throw new RuntimeException("Shade plugin not installed. Run 'shade setup' to continue.");
      }
   }

   private void install() throws XmlPullParserException, IOException
   {
      MavenCoreFacet mvn = project.getFacet(MavenCoreFacet.class);
      Model pom = mvn.getPOM();

      org.apache.maven.model.Plugin plugin = new org.apache.maven.model.Plugin();

      plugin.setArtifactId(dep.getArtifactId());
      plugin.setGroupId(dep.getGroupId());
      plugin.setVersion(dep.getVersion());

      Xpp3Dom dom = Xpp3DomBuilder.build(new ByteArrayInputStream("<configuration></configuration>".getBytes()),
               "UTF-8");

      List<PluginExecution> executions = plugin.getExecutions();
      PluginExecution execution = new PluginExecution();
      execution.setPhase("package");
      execution.addGoal("shade");
      execution.setConfiguration(dom);
      executions.add(execution);

      pom.getBuild().getPlugins().add(plugin);
      mvn.setPOM(pom);
   }

   private boolean isInstalled()
   {
      MavenCoreFacet mvn = project.getFacet(MavenCoreFacet.class);
      Model pom = mvn.getPOM();
      return getPlugin(pom) != null;
   }

   private org.apache.maven.model.Plugin getPlugin(final Model pom)
   {
      for (org.apache.maven.model.Plugin p : pom.getBuild().getPlugins())
      {
         if (dep.getGroupId().equals(p.getGroupId()) && dep.getArtifactId().equals(p.getArtifactId()))
         {
            return p;
         }
      }
      return null;
   }

   private interface ModifyNode
   {
      void modify(Node node);
   }
}
