/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 org.apache.uima.fit.maven;

import static org.apache.maven.plugins.annotations.LifecyclePhase.PROCESS_CLASSES;
import static org.apache.maven.plugins.annotations.ResolutionScope.TEST;
import static org.apache.uima.fit.factory.AnalysisEngineFactory.createEngineDescription;
import static org.apache.uima.fit.factory.CollectionReaderFactory.createReaderDescription;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Modifier;
import java.nio.file.Path;
import java.nio.file.Paths;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.uima.analysis_engine.AnalysisEngineDescription;
import org.apache.uima.collection.CollectionReaderDescription;
import org.apache.uima.fit.factory.TypeSystemDescriptionFactory;
import org.apache.uima.fit.maven.util.Util;
import org.apache.uima.resource.ResourceCreationSpecifier;
import org.apache.uima.resource.ResourceInitializationException;
import org.apache.uima.resource.ResourceSpecifier;
import org.apache.uima.resource.metadata.ProcessingResourceMetaData;
import org.apache.uima.resource.metadata.TypeSystemDescription;
import org.codehaus.plexus.util.FileUtils;
import org.sonatype.plexus.build.incremental.BuildContext;
import org.xml.sax.SAXException;

/**
 * Generate descriptor files for uimaFIT-based UIMA components.
 */
@Mojo( //
        name = "generate", //
        defaultPhase = PROCESS_CLASSES, //
        requiresDependencyResolution = TEST, //
        requiresDependencyCollection = TEST, //
        threadSafe = true)
public class GenerateDescriptorsMojo extends AbstractMojo {
  @Parameter(defaultValue = "${project}", readonly = true)
  private MavenProject project;

  @Component
  private BuildContext buildContext;

  private ClassLoader componentLoader;

  /**
   * Path where the generated resources are written.
   */
  @Parameter(defaultValue = "${project.build.directory}/classes", required = true)
  private File outputDirectory;

  /**
   * Do not add the output directory as a resource to the build.
   */
  @Parameter(defaultValue = "true", required = true)
  private boolean addOutputDirectoryAsResourceDirectory;

  /**
   * Skip generation of META-INF/org.apache.uima.fit/components.txt
   */
  @Parameter(defaultValue = "false", required = true)
  private boolean skipComponentsManifest;

  /**
   * Source file encoding.
   */
  @Parameter(defaultValue = "${project.build.sourceEncoding}", required = true)
  private String encoding;

  /**
   * Fail on error.
   */
  @Parameter(defaultValue = "true", required = true)
  private boolean failOnError;

  /**
   * Skip plugin execution.
   */
  @Parameter(property = "uima-generate.skip", defaultValue = "false", required = true)
  private boolean skip;

  /**
   * Skip plugin execution only during incremental builds (e.g. triggered from m2e).
   */
  @Parameter(defaultValue = "false", required = true)
  private boolean skipDuringIncrementalBuilds;

  enum TypeSystemSerialization {
    NONE, EMBEDDED
  }

  /**
   * Mode of adding type systems found on the classpath via the uimaFIT detection mechanism at
   * compile time to the generated descriptor. By default, no type systems are added.
   */
  @Parameter(defaultValue = "NONE")
  private TypeSystemSerialization addTypeSystemDescriptions;

  /**
   * Scope threshold to include. The default is "compile" (which implies compile, provided and
   * system dependencies). Can also be changed to "test" (which implies all dependencies).
   */
  @Parameter(defaultValue = "compile", required = true)
  private String includeScope;

  @Override
  public void execute() throws MojoExecutionException {
    if (isSkipped()) {
      return;
    }

    // add the generated sources to the build
    if (!outputDirectory.exists()) {
      outputDirectory.mkdirs();
      buildContext.refresh(outputDirectory);
    }

    // Get the compiled classes from this project
    String[] files = FileUtils.getFilesFromExtension(project.getBuild().getOutputDirectory(),
            new String[] { "class" });

    componentLoader = Util.getClassloader(project, includeScope);

    // List of components that is later written to META-INF/org.apache.uima.fit/components.txt
    StringBuilder componentsManifest = new StringBuilder();

    int countGenerated = 0;
    for (String file : files) {
      String base = file.substring(0, file.length() - 6);
      String clazzPath = base.substring(project.getBuild().getOutputDirectory().length() + 1);
      String clazzName = clazzPath.replace(File.separator, ".");

      ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
      try {
        Class clazz = componentLoader.loadClass(clazzName);

        // Make the componentLoader available to uimaFIT e.g. to resolve imports
        Thread.currentThread().setContextClassLoader(componentLoader);

        // Do not generate descriptors for abstract classes, they cannot be instantiated.
        if (Modifier.isAbstract(clazz.getModifiers())) {
          continue;
        }

        ResourceCreationSpecifier desc = null;
        ProcessingResourceMetaData metadata = null;
        switch (Util.getType(componentLoader, clazz)) {
          case ANALYSIS_ENGINE:
            AnalysisEngineDescription aeDesc = createEngineDescription(clazz);
            metadata = aeDesc.getAnalysisEngineMetaData();
            desc = aeDesc;
            break;
          case COLLECTION_READER:
            CollectionReaderDescription crDesc = createReaderDescription(clazz);
            metadata = crDesc.getCollectionReaderMetaData();
            desc = crDesc;
          default:
            // Do nothing
        }

        if (desc != null) {
          switch (addTypeSystemDescriptions) {
            case EMBEDDED:
              embedTypeSystems(metadata);
              break;
            case NONE: // fall-through
            default:
              // Do nothing
          }

          File out = new File(outputDirectory, clazzPath + ".xml");
          out.getParentFile().mkdirs();
          toXML(desc, out.getPath());
          countGenerated++;

          // Remember component
          componentsManifest.append("classpath*:").append(clazzPath + ".xml").append('\n');
        }
      } catch (SAXException e) {
        handleError("Cannot serialize descriptor for [" + clazzName + "]", e);
      } catch (IOException e) {
        handleError("Cannot write descriptor for [" + clazzName + "]", e);
      } catch (ClassNotFoundException e) {
        handleError("Cannot analyze class [" + clazzName + "]", e);
      } catch (ResourceInitializationException e) {
        handleError("Cannot generate descriptor for [" + clazzName + "]", e);
      } finally {
        Thread.currentThread().setContextClassLoader(classLoader);
      }
    }

    getLog().info(
            "Generated " + countGenerated + " descriptor" + (countGenerated != 1 ? "s." : "."));

    // Write META-INF/org.apache.uima.fit/components.txt unless skipped and unless there are no
    // components
    if (!skipComponentsManifest && componentsManifest.length() > 0) {
      File path = new File(outputDirectory, "META-INF/org.apache.uima.fit/components.txt");
      FileUtils.mkdir(path.getParent());
      try {
        FileUtils.fileWrite(path.getPath(), encoding, componentsManifest.toString());
      } catch (IOException e) {
        handleError("Cannot write components manifest to [" + path + "]"
                + ExceptionUtils.getRootCauseMessage(e), e);
      }
    }

    if (addOutputDirectoryAsResourceDirectory && countGenerated > 0) {
      Path absoluteDescriptorOutputPath = outputDirectory.toPath().toAbsolutePath();
      Path absoluteBuildOutputDirectory = Paths.get(project.getBuild().getOutputDirectory())
              .toAbsolutePath();
      Path absoluteBuildTestOutputDirectory = Paths.get(project.getBuild().getTestOutputDirectory())
              .toAbsolutePath();

      // Add the output folder as a new resource folder if any descriptors were generated and
      // only
      // if the descriptors were generated directly into the build output folder. The latter
      // can
      // be the case if the mojo is executed in a late build phase where the resources plugin
      // doesn't run anymore.
      if (!absoluteBuildOutputDirectory.equals(absoluteDescriptorOutputPath)
              && !absoluteBuildTestOutputDirectory.equals(absoluteDescriptorOutputPath)) {
        Resource resource = new Resource();
        resource.setDirectory(outputDirectory.getPath());
        resource.setFiltering(false);
        resource.addInclude("**/*.xml");
        project.addResource(resource);
      }
    }
  }

  private boolean isSkipped() {
    if (skipDuringIncrementalBuilds && buildContext.isIncremental()) {
      getLog().info("Generation of UIMA component descriptors skipped in incremental build.");
      return true;
    }

    if (skip) {
      getLog().info("Generation of UIMA component descriptors skipped.");
      return true;
    }

    return false;
  }

  private void handleError(String message, Exception e) throws MojoExecutionException {
    if (failOnError) {
      throw new MojoExecutionException(message, e);
    }

    getLog().error(message, e);
  }

  private void embedTypeSystems(ProcessingResourceMetaData metadata)
          throws ResourceInitializationException {
    TypeSystemDescriptionFactory.forceTypeDescriptorsScan();
    TypeSystemDescription tsDesc = TypeSystemDescriptionFactory.createTypeSystemDescription();
    metadata.setTypeSystem(tsDesc);
  }

  /**
   * Save descriptor XML to file system.
   */
  private void toXML(ResourceSpecifier aDesc, String aFilename) throws SAXException, IOException {
    File out = new File(aFilename);
    getLog().debug("Writing descriptor to: " + out);
    try (OutputStream os = new FileOutputStream(out);) {
      aDesc.toXML(os);
    }
  }
}
