// Copyright 2016 The Bazel Authors. All rights reserved.
//
// Licensed 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 com.google.devtools.build.lib.bazel.rules.android;

import com.google.auto.value.AutoValue;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Ordering;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import javax.annotation.Nullable;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * A collection of .pom contents that reference versioned archive files with dependencies.
 */
final class SdkMavenRepository {
  private enum PackagingType {
    AAR {
      @Override
      ImmutableList<String> createRule(
          String name, String archiveLabel, ImmutableList<String> dependencyLabels) {
        ImmutableList.Builder<String> ruleLines = new ImmutableList.Builder<>();
        ruleLines.add("aar_import(");
        ruleLines.add("    name = '" + name + "',");
        ruleLines.add("    aar = '" + archiveLabel + "',");
        ruleLines.add("    exports = [");
        for (String dependencyLabel : dependencyLabels) {
          ruleLines.add("        '" + dependencyLabel + "',");
        }
        ruleLines.add("    ],");
        ruleLines.add(")");
        ruleLines.add("");
        return ruleLines.build();
      }
    },
    JAR {
      @Override
      ImmutableList<String> createRule(
          String name, String archiveLabel, ImmutableList<String> dependencyLabels) {
        ImmutableList.Builder<String> ruleLines = new ImmutableList.Builder<>();
        ruleLines.add("java_import(");
        ruleLines.add("    name = '" + name + "',");
        ruleLines.add("    jars = ['" + archiveLabel + "'],");
        ruleLines.add("    exports = [");
        for (String dependencyLabel : dependencyLabels) {
          ruleLines.add("        '" + dependencyLabel + "',");
        }
        ruleLines.add("    ],");
        ruleLines.add(")");
        ruleLines.add("");
        return ruleLines.build();
      }
    },
    UNKNOWN {
      @Override
      ImmutableList<String> createRule(String name, String archiveLabel,
          ImmutableList<String> dependencyLabels) {
        ImmutableList.Builder<String> ruleLines = new ImmutableList.Builder<>();
        ruleLines.add("genrule(");
        ruleLines.add("    name = '" + name + "',");
        ruleLines.add("    outs = ['ignored_" + name + "'],");
        ruleLines.add("    cmd = 'echo Bazel does not recognize the Maven packaging type for: \""
            + archiveLabel + "\"; exit 1',");
        ruleLines.add(")");
        ruleLines.add("");
        return ruleLines.build();
      }
    };

    static PackagingType getPackagingType(String name) {
      for (PackagingType packagingType : PackagingType.values()) {
        if (packagingType.name().equalsIgnoreCase(name)) {
          return packagingType;
        }
      }
      return UNKNOWN;
    }

    abstract ImmutableList<String> createRule(
        String name, String archiveLabel, ImmutableList<String> dependencyLabels);
  }

  private final ImmutableSortedSet<Pom> poms;
  private final ImmutableSet<MavenCoordinate> allKnownCoordinates;

  private SdkMavenRepository(ImmutableSortedSet<Pom> poms) {
    this.poms = poms;
    ImmutableSet.Builder<MavenCoordinate> coordinates = new ImmutableSet.Builder<>();
    for (Pom pom : poms) {
      if (!PackagingType.getPackagingType(pom.packaging()).equals(PackagingType.UNKNOWN)) {
        coordinates.add(pom.mavenCoordinate());
      }
    }
    allKnownCoordinates = coordinates.build();
  }

  /**
   * Parses a set of maven repository directory trees looking for and parsing .pom files.
   */
  static SdkMavenRepository create(Iterable<Path> mavenRepositories) throws IOException {
    Collection<Path> pomPaths = new ArrayList<>();
    for (Path mavenRepository : mavenRepositories) {
      pomPaths.addAll(FileSystemUtils.traverseTree(mavenRepository, new Predicate<Path>() {
        @Override
        public boolean apply(@Nullable Path path) {
          return path.toString().endsWith(".pom");
        }
      }));
    }

    ImmutableSortedSet.Builder<Pom> poms =
        new ImmutableSortedSet.Builder<>(Ordering.usingToString());
    for (Path pomPath : pomPaths) {
      try {
        Pom pom = Pom.parse(pomPath);
        if (pom != null) {
          poms.add(pom);
        }
      } catch (ParserConfigurationException | SAXException e) {
        throw new IOException(e);
      }
    }
    return new SdkMavenRepository(poms.build());
  }

  /**
   * Creates BUILD files at {@code @<android sdk>/<group id>/BUILD} containing aar_import and
   * java_import rules with dependencies as {@code exports}. The targets are named
   * {@code @<android sdk>//<group id>:<artifact id>-<version id>}.
   */
  void writeBuildFiles(Path outputDirectory) throws IOException {
    for (Pom pom : poms) {
      Path buildFilePath = outputDirectory.getRelative(pom.mavenCoordinate().groupId() + "/BUILD");

      if (!buildFilePath.getParentDirectory().exists()) {
        buildFilePath.getParentDirectory().createDirectory();
      }

      if (!buildFilePath.exists()) {
        FileSystemUtils.writeContentAsLatin1(
            buildFilePath, "package(default_visibility = [\"//visibility:public\"])\n\n");
      }

      ImmutableList.Builder<String> dependencyLabels = new ImmutableList.Builder<>();
      for (MavenCoordinate dependencyCoordinate : pom.dependencyCoordinates()) {
        // Filter out dependencies that are not present in the Maven repository or have unknown
        // packaging types.
        if (allKnownCoordinates.contains(dependencyCoordinate)) {
          dependencyLabels.add(dependencyCoordinate.targetLabel());
        }
      }

      ImmutableList<String> ruleLines = PackagingType.getPackagingType(pom.packaging())
          .createRule(
              pom.mavenCoordinate().targetName(),
              pom.archiveLabel(outputDirectory),
              dependencyLabels.build());
      FileSystemUtils.appendLinesAs(buildFilePath, StandardCharsets.ISO_8859_1, ruleLines);
    }
  }

  /**
   * Creates the contents of the exports_files rule list containing all of the archives specified by
   * the pom files in the Maven repositories.
   */
  String getExportsFiles(Path outputDirectory) {
    StringBuilder exportedFiles = new StringBuilder();
    for (Pom pom : poms) {
      exportedFiles.append(String.format(
          "    '%s',\n", pom.archivePath().relativeTo(outputDirectory).getPathString()));
    }
    return exportedFiles.toString();
  }

  /**
   * The relevant contents of a .pom file needed to populate BUILD files for aars and jars in the
   * Android SDK extras maven repositories.
   */
  @AutoValue
  abstract static class Pom {
    private static final String DEFAULT_PACKAGING = "jar";

    static Pom parse(Path path) throws IOException, ParserConfigurationException, SAXException {
      Document pomDocument =
          DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(path.getInputStream());
      Node packagingNode = pomDocument.getElementsByTagName("packaging").item(0);
      String packaging = packagingNode == null ? DEFAULT_PACKAGING : packagingNode.getTextContent();
      MavenCoordinate coordinate = MavenCoordinate.create(
          pomDocument.getElementsByTagName("groupId").item(0).getTextContent(),
          pomDocument.getElementsByTagName("artifactId").item(0).getTextContent(),
          pomDocument.getElementsByTagName("version").item(0).getTextContent());

      ImmutableSortedSet.Builder<MavenCoordinate> dependencyCoordinates =
          new ImmutableSortedSet.Builder<>(Ordering.usingToString());
      NodeList dependencies = pomDocument.getElementsByTagName("dependency");
      for (int i = 0; i < dependencies.getLength(); i++) {
        if (dependencies.item(i) instanceof Element) {
          Element dependency = (Element) dependencies.item(i);
          dependencyCoordinates.add(MavenCoordinate.create(
              dependency.getElementsByTagName("groupId").item(0).getTextContent(),
              dependency.getElementsByTagName("artifactId").item(0).getTextContent(),
              dependency.getElementsByTagName("version").item(0).getTextContent()));
        }
      }

      return new AutoValue_SdkMavenRepository_Pom(
          path, packaging, coordinate, dependencyCoordinates.build());
    }

    abstract Path path();

    abstract String packaging();

    abstract MavenCoordinate mavenCoordinate();

    abstract ImmutableSortedSet<MavenCoordinate> dependencyCoordinates();

    String name() {
      String pomFilename = path().getBaseName();
      return pomFilename.substring(0, pomFilename.lastIndexOf(".pom"));
    }

    Path archivePath() {
      return path().getParentDirectory().getRelative(name() + "." + packaging());
    }

    /** The label for the .aar or .jar file in the repository. */
    String archiveLabel(Path outputDirectory) {
      return "//:" + archivePath().relativeTo(outputDirectory);
    }
  }

  /**
   * A 3-tuple of group id, artifact id and version used to identify Maven targets.
   */
  @AutoValue
  abstract static class MavenCoordinate {
    static MavenCoordinate create(String groupId, String artifactId, String version) {
      return new AutoValue_SdkMavenRepository_MavenCoordinate(groupId, artifactId, version);
    }

    abstract String groupId();

    abstract String artifactId();

    abstract String version();

    /** The target name for the java_import or aar_import for the Maven coordinate. */
    String targetName() {
      return artifactId() + "-" + version();
    }

    /** The target label for the java_import or aar_import for the Maven coordinate. */
    String targetLabel() {
      return "//" + groupId() + ":" + targetName();
    }
  }
}
