/*******************************************************************************
 * Copyright 2011 Google Inc. All Rights Reserved.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * 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 com.google.gdt.eclipse.maven.configurators;

import com.google.gdt.eclipse.core.StringUtilities;
import com.google.gdt.eclipse.core.properties.WebAppProjectProperties;
import com.google.gdt.eclipse.maven.Activator;
import com.google.gdt.eclipse.maven.sdk.GWTMavenRuntime;
import com.google.gwt.eclipse.core.nature.GWTNature;

import org.apache.maven.model.Dependency;
import org.apache.maven.model.Plugin;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.m2e.core.lifecyclemapping.model.IPluginExecutionMetadata;
import org.eclipse.m2e.core.project.IMavenProjectFacade;
import org.eclipse.m2e.core.project.MavenProjectChangedEvent;
import org.eclipse.m2e.core.project.configurator.AbstractBuildParticipant;
import org.eclipse.m2e.core.project.configurator.MojoExecutionBuildParticipant;
import org.eclipse.m2e.core.project.configurator.ProjectConfigurationRequest;
import org.osgi.service.prefs.BackingStoreException;

import java.util.List;

/**
 * M2Eclipse project configuration extension that configures a project to get the GWT project nature.
 * <p>
 * NOTE: Do not access this class from outside of the configurators package. All classes in the configurators package
 * have dependencies on plugins that may or may not be present in the user's installation. As long as these classes are
 * only invoked through m2Eclipe's extension points, other parts of this plug-in can be used without requiring the
 * m2Eclipse dependencies.
 */
public class MavenProjectConfigurator extends AbstracMavenProjectConfigurator {

  /**
   * These properties may be pulled from the gwt-maven-plugin configuration of the pom to be used in configuring the GWT
   * Web Application settings for the Eclipse project. If not present, defaults are used.
   */
  private static final String ECLIPSE_LAUNCH_SRC_DIR_PROPERTY_KEY = "eclipseLaunchFromWarDir";
  private static final boolean ECLIPSE_LAUNCH_SRC_DIR_DEFAULT = false;
  private static final String WAR_SRC_DIR_PROPERTY_KEY = "warSourceDirectory";
  private static final String WEB_APP_DIRECTORY = "webappDirectory";
  private static final String HOSTED_WEB_APP_DIRECTORY = "hostedWebapp"; // GWT Maven Plugin 1 only
  private static final String GWT_MAVEN_MODULENAME = "moduleName";
  private static final String GWT_MAVEN_MODULESHORTNAME = "moduleShortName";

  private static final String WAR_SRC_DIR_DEFAULT = "src/main/webapp";

  @Override
  protected void doConfigure(final MavenProject mavenProject, IProject project, ProjectConfigurationRequest unused,
      final IProgressMonitor monitor) throws CoreException {
    Activator.log("MavenProjectConfigurator.doConfigure() invoked");

    // configure the GWT Nature
    boolean hasGwtNature = configureNature(project, mavenProject, GWTNature.NATURE_ID, true, new NatureCallback() {
      @Override
      protected void beforeAddingNature() {
        configureGwtProject(mavenProject, monitor);
      }
    }, monitor);

    // retrieve gwt-maven-plugin configuration if it exists
    Plugin gwtMavenPlugin = getGwtMavenPlugin(mavenProject);
    Xpp3Dom mavenConfig = gwtMavenPlugin == null ? null : (Xpp3Dom) gwtMavenPlugin.getConfiguration();

    // Persist GWT nature settings
    if (!hasGwtNature) {
      Activator.log("MavenProjectConfigurator: Skipping Maven configuration because GWT nature is false. hasGWTNature="
          + hasGwtNature);
      // Exit no maven plugin found
      return;
    }

    try {
      persistGwtNatureSettings(project, mavenProject, mavenConfig);
    } catch (BackingStoreException exception) {
      Activator.logError("MavenProjectConfigurator: Problem configuring maven project.", exception);
    }
  }

  @Override
  public AbstractBuildParticipant getBuildParticipant(final IMavenProjectFacade projectFacade, MojoExecution execution,
      final IPluginExecutionMetadata executionMetadata) {
    Activator.log("MavenProjectConfigurator.getBuildParticipant for Maven invoked");

    // Run the execution generate-module for maven2 plugin
    // Don't run on war for gwt maven plugin1
    MojoExecutionBuildParticipant build = null;
    MavenProject mavenProject = projectFacade.getMavenProject();
    if (mavenProject != null && isGwtMavenPlugin2(mavenProject)) {
      Activator.log("MavenProjectConfigurator.getBuildParticipant adding build participant for generate-module.");
      build = new MojoExecutionBuildParticipant(execution, true, true);
    }

    return build;
  }

  @Override
  public void mavenProjectChanged(MavenProjectChangedEvent event, IProgressMonitor monitor) throws CoreException {
    super.mavenProjectChanged(event, monitor);
  }

  @Override
  public void configure(ProjectConfigurationRequest request, IProgressMonitor monitor) throws CoreException {
    Activator.log("MavenProjectConfigurator.configure invoked");
    super.configure(request, monitor);
  }

  /**
   * Save the settings for the GWT nature in the application GWT preferences.
   *
   * @param project
   * @param mavenProject
   * @param mavenConfig
   * @throws BackingStoreException
   */
  private void persistGwtNatureSettings(IProject project, MavenProject mavenProject, Xpp3Dom mavenConfig)
      throws BackingStoreException {
    IPath warOutDir = getWarOutDir(project, mavenProject);

    WebAppProjectProperties.setWarSrcDir(project, getWarSrcDir(mavenProject, mavenConfig)); // src/main/webapp
    WebAppProjectProperties.setWarSrcDirIsOutput(project, getLaunchFromHere(mavenConfig)); // false

    // TODO the extension should be used, from WarArgProcessor
    WebAppProjectProperties.setLastUsedWarOutLocation(project, warOutDir);

    WebAppProjectProperties.setGwtMavenModuleName(project, getGwtModuleName(mavenProject));
    WebAppProjectProperties.setGwtMavenModuleShortName(project, getGwtModuleShortName(mavenProject));

    String message = "MavenProjectConfiguratior Maven: Success with setting up GWT Nature\n";
    message += "\tartifactId=" + mavenProject.getArtifactId() + "\n";
    message += "\tversion=" + mavenProject.getVersion() + "\n";
    message += "\twarOutDir=" + warOutDir;
    Activator.log(message);
  }

  /**
   * Get the GWT Maven plugin 2 <moduleName/>.
   *
   * @param mavenProject
   * @return the moduleName from configuration
   */
  private String getGwtModuleName(MavenProject mavenProject) {
    if (!isGwtMavenPlugin2(mavenProject)) {
      return null;
    }

    Plugin gwtPlugin2 = getGwtMavenPlugin2(mavenProject);
    if (gwtPlugin2 == null) {
      return null;
    }

    Xpp3Dom gwtPluginConfig = (Xpp3Dom) gwtPlugin2.getConfiguration();
    if (gwtPluginConfig == null) {
      return null;
    }

    String moduleName = null;
    for (Xpp3Dom child : gwtPluginConfig.getChildren()) {
      if (child != null && GWT_MAVEN_MODULENAME.equals(child.getName())) {
        moduleName = child.getValue().trim();
      }
    }
    return moduleName;
  }

  /**
   * Get the GWT Maven plugin 2 <moduleShort=Name/>.
   *
   * @param mavenProject
   * @return the moduleName from configuration
   */
  private String getGwtModuleShortName(MavenProject mavenProject) {
    if (!isGwtMavenPlugin2(mavenProject)) {
      return null;
    }

    Plugin gwtPlugin2 = getGwtMavenPlugin2(mavenProject);
    if (gwtPlugin2 == null) {
      return null;
    }

    Xpp3Dom gwtPluginConfig = (Xpp3Dom) gwtPlugin2.getConfiguration();
    if (gwtPluginConfig == null) {
      return null;
    }

    String moduleName = null;
    for (Xpp3Dom child : gwtPluginConfig.getChildren()) {
      if (child != null && GWT_MAVEN_MODULESHORTNAME.equals(child.getName())) {
        moduleName = child.getValue().trim();
      }
    }
    return moduleName;
  }

  /**
   * Get the war output directory.
   *
   * @param project
   * @param mavenProject
   * @return returns the war output path
   */
  private IPath getWarOutDir(IProject project, MavenProject mavenProject) {
    String artifactId = mavenProject.getArtifactId();
    String version = mavenProject.getVersion();
    IPath locationOfProject = (project.getRawLocation() != null ? project.getRawLocation() : project.getLocation());

    IPath warOut = null;

    // Default directory target/artifact-version
    if (locationOfProject != null && artifactId != null && version != null) {
      warOut = locationOfProject.append("target").append(artifactId + "-" + version);
    }

    // Get the GWT Maven plugin 1 <hostedWebapp/> directory
    if (isGwtMavenPlugin1(mavenProject) && getGwtMavenPluginHostedWebAppDirectory(mavenProject) != null) {
      warOut = getGwtMavenPluginHostedWebAppDirectory(mavenProject);
    }

    // Get the Gwt Maven plugin 1 <webappDirectory/>
    if (isGwtMavenPlugin2(mavenProject) && getGwtPlugin2WebAppDirectory(mavenProject) != null) {
      warOut = getGwtPlugin2WebAppDirectory(mavenProject);
    }

    // Get the maven war plugin <webappDirectory/>
    if (getMavenWarPluginWebAppDirectory(mavenProject) != null) {
      warOut = getMavenWarPluginWebAppDirectory(mavenProject);
    }

    // make the directory if it doesn't exist
    if (warOut != null) {
      warOut.toFile().mkdirs();
    }

    return warOut;
  }

  /**
   * Get the GWT Maven <hostedWebapp/> directory.
   *
   * @param mavenProject
   * @return the webapp directory
   */
  private IPath getGwtMavenPluginHostedWebAppDirectory(MavenProject mavenProject) {
    Plugin warPlugin = getGwtMavenPlugin(mavenProject);
    if (warPlugin == null) {
      return null;
    }

    Xpp3Dom warPluginConfig = (Xpp3Dom) warPlugin.getConfiguration();
    if (warPluginConfig == null) {
      return null;
    }

    IPath warOut = null;
    for (Xpp3Dom child : warPluginConfig.getChildren()) {
      if (child != null && HOSTED_WEB_APP_DIRECTORY.equals(child.getName())) {
        String path = child.getValue().trim();
        if (!path.isEmpty()) {
          warOut = new Path(path);
        }
      }
    }

    return warOut;
  }

  /**
   * Return the war out directory via the Maven war plugin <webappDirectory/>.
   *
   * @param mavenProject
   * @return path to web app direcotry
   */
  protected IPath getMavenWarPluginWebAppDirectory(MavenProject mavenProject) {
    Plugin warPlugin = getWarPlugin(mavenProject);
    if (warPlugin == null) {
      return null;
    }

    Xpp3Dom warPluginConfig = (Xpp3Dom) warPlugin.getConfiguration();
    if (warPluginConfig == null) {
      return null;
    }

    IPath warOut = null;
    for (Xpp3Dom child : warPluginConfig.getChildren()) {
      if (child != null && WEB_APP_DIRECTORY.equals(child.getName())) {
        String path = child.getValue().trim();
        if (!path.isEmpty()) {
          warOut = new Path(path);
        }
      }
    }

    return warOut;
  }

  /**
   * Returns the Gwt Maven Plugin 2 web app directory <webappDirectory/>.
   *
   * @param mavenProject
   * @return path to web app direcotry
   */
  protected IPath getGwtPlugin2WebAppDirectory(MavenProject mavenProject) {
    Plugin warPlugin = getGwtMavenPlugin2(mavenProject);
    if (warPlugin == null) {
      return null;
    }

    Xpp3Dom warPluginConfig = (Xpp3Dom) warPlugin.getConfiguration();
    if (warPluginConfig == null) {
      return null;
    }

    IPath warOut = null;
    for (Xpp3Dom child : warPluginConfig.getChildren()) {
      if (child != null && WEB_APP_DIRECTORY.equals(child.getName())) {
        String path = child.getValue().trim();
        if (!path.isEmpty()) {
          warOut = new Path(path);
        }
      }
    }

    return warOut;
  }

  /**
   * Configure the GWT Nature.
   *
   * @param mavenProject
   * @param monitor
   */
  protected void configureGwtProject(MavenProject mavenProject, IProgressMonitor monitor) {
    // Get the GWT version from the project pom
    String gwtVersion = null;
    List<Dependency> dependencies = mavenProject.getDependencies();
    for (Dependency dependency : dependencies) {
      boolean hasGwtGroupId = GWTMavenRuntime.MAVEN_GWT_GROUP_ID.equals(dependency.getGroupId());
      boolean hasGwtUserArtivactId = GWTMavenRuntime.MAVEN_GWT_USER_ARTIFACT_ID.equals(dependency.getArtifactId());
      boolean hasGwtUserServletId = GWTMavenRuntime.MAVEN_GWT_SERVLET_ARTIFACT_ID.equals(dependency.getArtifactId());
      if (hasGwtGroupId && (hasGwtUserArtivactId || hasGwtUserServletId)) {
        gwtVersion = dependency.getVersion();
        Activator.log("MavenProjectConfigurator, Maven: Setting up Gwt Project. hasGwtGroupId=" + hasGwtGroupId
            + " hasGwtUser=" + hasGwtUserArtivactId + " hasGwtUserServletId=" + hasGwtUserServletId);
        break;
      }
    }

    Activator.log("MavenProjectConfigurator, Maven: gwtVersion=" + gwtVersion);

    resolveGwtDevJar(mavenProject, monitor, gwtVersion);
  }

  private void resolveGwtDevJar(MavenProject mavenProject, IProgressMonitor monitor, String gwtVersion) {
    // Check that the pom.xml has GWT dependencies
    if (gwtVersion != null && !StringUtilities.isEmpty(gwtVersion)) {
      try {
        // Download and install the gwt-dev.jar into the local repository.
        maven.resolve(GWTMavenRuntime.MAVEN_GWT_GROUP_ID, GWTMavenRuntime.MAVEN_GWT_DEV_JAR_ARTIFACT_ID, gwtVersion,
            "jar", null, mavenProject.getRemoteArtifactRepositories(), monitor);
      } catch (CoreException exception) {
        Activator.logError(
            "MavenProjectConfigurator: Problem configuring the maven project because it could not download the "
                + "gwt-dev.jar which is used to determine the version.",
            exception);
      }
    }
  }

  /**
   * Returns the war source directory such as src/main/webapp
   *
   * @param mavenProject
   *
   * @param config
   *          gwt-maven-maven config DOM
   * @return the {@link #WAR_SRC_DIR_PROPERTY_KEY} value from the config if it exists, {@link #WAR_SRC_DIR_DEFAULT}
   *         otherwise or if config is null
   */
  private static final IPath getWarSrcDir(MavenProject mavenProject, Xpp3Dom config) {
    String spath = WAR_SRC_DIR_DEFAULT;

    if (config != null) {
      for (Xpp3Dom child : config.getChildren()) {
        if (child != null && WAR_SRC_DIR_PROPERTY_KEY.equals(child.getName())) {
          spath = child.getValue() == null ? WAR_SRC_DIR_DEFAULT : child.getValue().trim();
        }
      }
    }

    IPath path = null;
    if (spath != null) {
      path = new Path(spath);

      String basePath = mavenProject.getBasedir().toPath().toAbsolutePath().toString();
      String fullPath = basePath + "/" + spath;
      java.io.File fullPathFile = new java.io.File(fullPath);
      if (!fullPathFile.exists()) {
        path = null;
      }
    }

    return path;
  }

  /**
   * Get the project launch locaiton
   *
   * @param config
   *          gwt-maven-maven config DOM
   * @return the {@link #ECLIPSE_LAUNCH_SRC_DIR_PROPERTY_KEY} value from the config if it exists,
   *         {@link #ECLIPSE_LAUNCH_SRC_DIR_DEFAULT} otherwise or if config is null
   */
  private final boolean getLaunchFromHere(Xpp3Dom config) {
    if (config == null) {
      return ECLIPSE_LAUNCH_SRC_DIR_DEFAULT;
    }
    for (Xpp3Dom child : config.getChildren()) {
      if (child != null && ECLIPSE_LAUNCH_SRC_DIR_PROPERTY_KEY.equals(child.getName())) {
        return child.getValue() == null ? ECLIPSE_LAUNCH_SRC_DIR_DEFAULT
            : Boolean.parseBoolean(child.getValue().trim());
      }
    }
    return ECLIPSE_LAUNCH_SRC_DIR_DEFAULT;
  }

}