// Copyright 2018 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.rules.config;

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
import com.google.devtools.build.lib.cmdline.Label;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;

/** Marker interface for detecting feature flags in the Starlark setting map. */
public interface FeatureFlagValue {
  /** Returns the value of this flag, or null if it's set default. */
  @Nullable
  String getValue();

  /** A feature flag value for a flag known to be set to a particular value. */
  @AutoValue
  abstract class SetValue implements FeatureFlagValue {
    static SetValue of(String value) {
      return new AutoValue_FeatureFlagValue_SetValue(value);
    }

    @Override
    public abstract String getValue();

    @Override
    public final String toString() {
      return String.format("FeatureFlagValue.SetValue{%s}", getValue());
    }
  }

  /** A feature flag value for a flag known to be set to its default value. */
  enum DefaultValue implements FeatureFlagValue {
    INSTANCE;

    @Nullable
    @Override
    public String getValue() {
      return null;
    }

    @Override
    public String toString() {
      return "FeatureFlagValue.DefaultValue{}";
    }
  }

  /** A feature flag value for a flag which was requested but which value was already trimmed. */
  enum UnknownValue implements FeatureFlagValue {
    INSTANCE;

    @Override
    public String getValue() {
      throw new IllegalStateException();
    }

    @Override
    public String toString() {
      return "FeatureFlagValue.UnknownValue{}";
    }
  }

  /** Returns a new BuildOptions with a new map of feature flag values. */
  static BuildOptions replaceFlagValues(BuildOptions original, Map<Label, String> newValues) {
    BuildOptions.Builder result = original.toBuilder();
    for (Map.Entry<Label, Object> entry : original.getStarlarkOptions().entrySet()) {
      if (entry.getValue() instanceof FeatureFlagValue) {
        result.removeStarlarkOption(entry.getKey());
      }
    }
    ImmutableMap.Builder<Label, Object> newValueObjects = new ImmutableMap.Builder<>();
    for (Map.Entry<Label, String> entry : newValues.entrySet()) {
      newValueObjects.put(entry.getKey(), SetValue.of(entry.getValue()));
    }
    result.addStarlarkOptions(newValueObjects.buildOrThrow());
    BuildOptions builtResult = result.build();
    if (builtResult.contains(ConfigFeatureFlagOptions.class)) {
      builtResult.get(ConfigFeatureFlagOptions.class).allFeatureFlagValuesArePresent = true;
    }
    return builtResult;
  }

  /** Returns a new BuildOptions with the feature flag values trimmed down to the given flags. */
  static BuildOptions trimFlagValues(BuildOptions original, Set<Label> availableFlags) {
    // An important performance property of this method is that we don't create a new BuildOptions
    // instance unless we really need one. This particularly saves the expensive cost of
    // BuildOptions.hashCode(). Since this method is called unconditionally over every configured
    // target, this has real observable effect on build analysis time.
    Set<Label> seenFlags = new LinkedHashSet<>();
    Set<Label> flagsToTrim = new LinkedHashSet<>();
    Map<Label, Object> unknownFlagsToAdd = new LinkedHashMap<>();
    boolean changeAllValuesPresentOption = false;
    if (original.contains(ConfigFeatureFlagOptions.class)) {
      changeAllValuesPresentOption =
          original.get(ConfigFeatureFlagOptions.class).allFeatureFlagValuesArePresent;
    }

    // What do we need to change?
    original.getStarlarkOptions().entrySet().stream()
        .filter(entry -> entry.getValue() instanceof FeatureFlagValue)
        .forEach(featureFlagEntry -> seenFlags.add(featureFlagEntry.getKey()));
    flagsToTrim.addAll(Sets.difference(seenFlags, availableFlags));
    FeatureFlagValue unknownFlagValue =
        changeAllValuesPresentOption ? DefaultValue.INSTANCE : UnknownValue.INSTANCE;
    for (Label unknownFlag : Sets.difference(availableFlags, seenFlags)) {
      unknownFlagsToAdd.put(unknownFlag, unknownFlagValue);
    }

    // Nothing changed? Return the original BuildOptions.
    if (flagsToTrim.isEmpty() && unknownFlagsToAdd.isEmpty() && !changeAllValuesPresentOption) {
      return original;
    }

    // Else construct a new one. This should not be the common case.
    BuildOptions.Builder result = original.toBuilder();
    flagsToTrim.forEach(trimmedFlag -> result.removeStarlarkOption(trimmedFlag));
    unknownFlagsToAdd.forEach((flag, value) -> result.addStarlarkOption(flag, value));
    BuildOptions builtResult = result.build();
    if (builtResult.contains(ConfigFeatureFlagOptions.class)) {
      builtResult.get(ConfigFeatureFlagOptions.class).allFeatureFlagValuesArePresent = false;
    }
    return builtResult;
  }

  /**
   * Returns the map of known non-default flag values. Throws UnknownValueException when a flag is
   * set to UNKNOWN_VALUE (due to an earlier trimming gone wrong).
   */
  static ImmutableSortedMap<Label, String> getFlagValues(BuildOptions options)
      throws UnknownValueException {
    ImmutableSortedMap.Builder<Label, String> knownValues = ImmutableSortedMap.naturalOrder();
    ImmutableList.Builder<Label> unknownFlagsBuilder = new ImmutableList.Builder<>();
    for (Map.Entry<Label, Object> entry : options.getStarlarkOptions().entrySet()) {
      if (entry.getValue().equals(UnknownValue.INSTANCE)) {
        unknownFlagsBuilder.add(entry.getKey());
      } else if (entry.getValue() instanceof FeatureFlagValue) {
        String value = ((FeatureFlagValue) entry.getValue()).getValue();
        if (value != null) {
          knownValues.put(entry.getKey(), value);
        }
      }
    }
    ImmutableList<Label> unknownFlags = unknownFlagsBuilder.build();
    if (!unknownFlags.isEmpty()) {
      throw new UnknownValueException(unknownFlags);
    }
    return knownValues.buildOrThrow();
  }

  /** Exception class for when getFlagValues runs into UNKNOWN_VALUE. */
  static final class UnknownValueException extends InvalidConfigurationException {
    private static final String ERROR_TEMPLATE =
        "Feature flag %1$s was accessed in a configuration it is not present in. All "
            + "targets which depend on %1$s directly or indirectly must name it in their "
            + "transitive_configs attribute.";
    private final ImmutableList<Label> unknownFlags;

    UnknownValueException(ImmutableList<Label> unknownFlags) {
      super(
          "Some feature flags were incorrectly specified:\n"
              + unknownFlags.stream()
                  .map((missingLabel) -> String.format(ERROR_TEMPLATE, missingLabel))
                  .collect(Collectors.joining("\n")));
      this.unknownFlags = unknownFlags;
    }

    ImmutableList<Label> getUnknownFlags() {
      return unknownFlags;
    }
  }
}
