// 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.runtime;

import static com.google.common.base.Preconditions.checkState;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.CompletionContext;
import com.google.devtools.build.lib.actions.CompletionContext.ArtifactReceiver;
import com.google.devtools.build.lib.actions.FileArtifactValue;
import com.google.devtools.build.lib.actions.FilesetOutputSymlink;
import com.google.devtools.build.lib.analysis.TargetCompleteEvent;
import com.google.devtools.build.lib.buildeventstream.ArtifactGroupNamer;
import com.google.devtools.build.lib.buildeventstream.BuildEvent;
import com.google.devtools.build.lib.buildeventstream.BuildEvent.LocalFile.LocalFileType;
import com.google.devtools.build.lib.buildeventstream.BuildEventContext;
import com.google.devtools.build.lib.buildeventstream.BuildEventIdUtil;
import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId;
import com.google.devtools.build.lib.buildeventstream.GenericBuildEvent;
import com.google.devtools.build.lib.buildeventstream.PathConverter;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import java.util.Collection;

/**
 * A {@link BuildEvent} introducing a set of artifacts to be referred to later by its name. Those
 * events are generated by the {@link BuildEventStreamer} upon seeing an {@link
 * com.google.devtools.build.lib.actions.EventReportingArtifacts}, if necessary.
 */
class NamedArtifactGroup implements BuildEvent {
  private final String name;
  private final CompletionContext completionContext;
  private final NestedSet<?> set; // of Artifact or ExpandedArtifact

  /**
   * Create a {@link NamedArtifactGroup}. Although the set may contain a mixture of Artifacts and
   * ExpandedArtifacts, all its leaf successors ("direct elements") are ExpandedArtifacts.
   */
  NamedArtifactGroup(String name, CompletionContext completionContext, NestedSet<?> set) {
    this.name = name;
    this.completionContext = completionContext;
    this.set = set;
  }

  @Override
  public BuildEventId getEventId() {
    return BuildEventIdUtil.fromArtifactGroupName(name);
  }

  @Override
  public Collection<BuildEventId> getChildrenEvents() {
    return ImmutableSet.of();
  }

  @Override
  public Collection<LocalFile> referencedLocalFiles() {
    ImmutableList.Builder<LocalFile> artifacts = ImmutableList.builder();
    for (Object elem : set.getLeaves()) {
      switch ((ExpandedArtifact) elem) {
        case NormalExpandedArtifact(Artifact artifact, FileArtifactValue metadata) -> {
          artifacts.add(
              new LocalFile(
                  completionContext.pathResolver().toPath(artifact),
                  LocalFileType.forArtifact(artifact, metadata),
                  metadata));
        }
        case FilesetExpandedArtifact(Artifact fileset, FilesetOutputSymlink link) -> {
          artifacts.add(
              new LocalFile(
                  completionContext.pathResolver().toPath(link.target()),
                  LocalFileType.forArtifact(link.target(), link.metadata()),
                  link.metadata()));
        }
      }
    }
    return artifacts.build();
  }

  @Override
  public BuildEventStreamProtos.BuildEvent asStreamProto(BuildEventContext converters) {
    PathConverter pathConverter = converters.pathConverter();
    ArtifactGroupNamer namer = converters.artifactGroupNamer();

    BuildEventStreamProtos.NamedSetOfFiles.Builder builder =
        BuildEventStreamProtos.NamedSetOfFiles.newBuilder();
    for (Object elem : set.getLeaves()) {
      BuildEventStreamProtos.File file =
          switch ((ExpandedArtifact) elem) {
            case NormalExpandedArtifact(Artifact artifact, FileArtifactValue metadata) -> {
              String uri = pathConverter.apply(completionContext.pathResolver().toPath(artifact));
              yield TargetCompleteEvent.newFile(artifact, metadata, uri);
            }
            case FilesetExpandedArtifact(Artifact fileset, FilesetOutputSymlink link) -> {
              String uri =
                  pathConverter.apply(completionContext.pathResolver().toPath(link.target()));
              yield TargetCompleteEvent.newFile(
                  fileset.getRoot(),
                  fileset.getRootRelativePath().getRelative(link.name()),
                  link.metadata(),
                  uri);
            }
          };
      // Omit files with unknown contents (e.g. if uploading failed).
      if (file.getFileCase() != BuildEventStreamProtos.File.FileCase.FILE_NOT_SET) {
        builder.addFiles(file);
      }
    }

    for (NestedSet<?> succ : set.getNonLeaves()) {
      builder.addFileSets(namer.apply(succ.toNode()));
    }
    return GenericBuildEvent.protoChaining(this).setNamedSetOfFiles(builder.build()).build();
  }

  /**
   * Given a set whose leaf successors are {@link Artifact} and {@link ExpandedArtifact}, returns a
   * new NestedSet whose leaf successors are all ExpandedArtifact. Non-leaf successors are
   * unaltered.
   */
  static NestedSet<?> expandSet(CompletionContext ctx, NestedSet<?> artifacts) {
    NestedSetBuilder<Object> res = NestedSetBuilder.newBuilder(Order.STABLE_ORDER);
    for (Object elem : artifacts.getLeaves()) {
      switch (elem) {
        case ExpandedArtifact expandedArtifact -> {
          res.add(expandedArtifact);
        }
        case Artifact artifact -> {
          ctx.visitArtifacts(
              ImmutableList.of(artifact),
              new ArtifactReceiver() {
                @Override
                public void accept(Artifact artifact, FileArtifactValue metadata) {
                  res.add(new NormalExpandedArtifact(artifact, metadata));
                }

                @Override
                public void acceptFilesetMapping(Artifact fileset, FilesetOutputSymlink link) {
                  res.add(new FilesetExpandedArtifact(fileset, link));
                }
              });
        }
        default -> {
          throw new IllegalStateException("Unexpected type in artifact set: %s".formatted(elem));
        }
      }
    }
    boolean noDirects = res.isEmpty();

    ImmutableList<? extends NestedSet<?>> nonLeaves = artifacts.getNonLeaves();
    for (NestedSet<?> succ : nonLeaves) {
      res.addTransitive(succ);
    }

    NestedSet<?> result = res.build();

    // If we have executed one call to res.addTransitive(x)
    // and none to res.add, then res.build() simply returns x
    // ("inlining"), which may violate our postcondition on elements.
    // (This can happen if 'artifacts' contains one non-leaf successor
    // and one or more leaf successors for which visitArtifacts is a no-op.)
    //
    // In that case we need to recursively apply the expansion. Ugh.
    if (noDirects && nonLeaves.size() == 1) {
      return expandSet(ctx, result);
    }

    return result;
  }

  private sealed interface ExpandedArtifact
      permits NormalExpandedArtifact, FilesetExpandedArtifact {}

  private record NormalExpandedArtifact(Artifact artifact, FileArtifactValue metadata)
      implements ExpandedArtifact {
    private NormalExpandedArtifact {
      checkState(!artifact.isDirectory(), "artifact must be expanded: %s", artifact);
    }
  }

  private record FilesetExpandedArtifact(Artifact fileset, FilesetOutputSymlink link)
      implements ExpandedArtifact {
    private FilesetExpandedArtifact {
      checkState(fileset.isFileset(), "artifact must be a fileset: %s", fileset);
    }
  }
}
