// Copyright 2024 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.skyframe.config;

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

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.analysis.config.FragmentOptions;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.Label.PackageContext;
import com.google.devtools.build.lib.concurrent.ThreadSafety;
import com.google.devtools.build.lib.skyframe.SkyFunctions;
import com.google.devtools.build.lib.skyframe.serialization.VisibleForSerialization;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
import com.google.devtools.build.skyframe.SkyFunctionName;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import com.google.devtools.common.options.OptionDefinition;
import com.google.devtools.common.options.OptionValueDescription;
import com.google.devtools.common.options.OptionsParsingException;
import com.google.devtools.common.options.OptionsParsingResult;
import java.util.Map;

/** Stores the {@link OptionsParsingResult} from {@link ParsedFlagsFunction}. */
@AutoCodec
public class ParsedFlagsValue implements SkyValue {

  /** Key for {@link ParsedFlagsValue} based on the raw flags. */
  @ThreadSafety.Immutable
  @AutoCodec
  public record Key(
      ImmutableList<String> rawFlags,
      PackageContext packageContext,
      boolean includeDefaultValues,
      ImmutableMap<String, Label> flagAliasMappings)
      implements SkyKey {
    private static final SkyKeyInterner<Key> interner = SkyKey.newInterner();

    /**
     * @deprecated Use {@link #create} instead to ensure interning.
     */
    @Deprecated
    public Key {
      checkNotNull(rawFlags);
      checkNotNull(packageContext);
      checkNotNull(flagAliasMappings);
    }

    /**
     * Returns a new {@link Key} for the given command-line flags, such as {@code
     * --compilation_mode=bdg} or {@code --//custom/starlark:flag=23}.
     */
    public static Key create(
        ImmutableList<String> rawFlags,
        PackageContext packageContext,
        ImmutableMap<String, Label> flagAliasMappings) {
      return create(rawFlags, packageContext, /* includeDefaultValues= */ false, flagAliasMappings);
    }

    /**
     * Returns a new {@link Key} for the given command-line flags, such as {@code
     * --compilation_mode=bdg} or {@code --//custom/starlark:flag=23}.
     */
    @AutoCodec.Instantiator
    public static Key create(
        ImmutableList<String> rawFlags,
        PackageContext packageContext,
        boolean includeDefaultValues,
        ImmutableMap<String, Label> flagAliasMappings) {
      return interner.intern(
          new Key(rawFlags, packageContext, includeDefaultValues, flagAliasMappings));
    }

    @Override
    public SkyFunctionName functionName() {
      return SkyFunctions.PARSED_FLAGS;
    }

    @Override
    public SkyKeyInterner<Key> getSkyKeyInterner() {
      return interner;
    }
  }

  static ParsedFlagsValue parseAndCreate(NativeAndStarlarkFlags flags)
      throws OptionsParsingException {
    return new ParsedFlagsValue(flags, flags.parse());
  }

  @AutoCodec.Instantiator
  @VisibleForSerialization
  static ParsedFlagsValue createForDeserialization(NativeAndStarlarkFlags flags) {
    try {
      return parseAndCreate(flags);
    } catch (OptionsParsingException e) {
      // Should be impossible since it parsed successfully before it was serialized.
      throw new IllegalStateException(e);
    }
  }

  private final NativeAndStarlarkFlags flags;
  private final OptionsParsingResult parsingResult;
  private final LoadingCache<BuildOptions, BuildConfigurationKey> mergeCache =
      Caffeine.newBuilder().weakKeys().build(this::mergeWithImpl);

  private ParsedFlagsValue(NativeAndStarlarkFlags flags, OptionsParsingResult parsingResult) {
    this.parsingResult = checkNotNull(parsingResult);
    this.flags = checkNotNull(flags);
  }

  public OptionsParsingResult parsingResult() {
    return parsingResult;
  }

  /**
   * Returns a new {@link BuildConfigurationKey} with options containing all flags from the given
   * {@link BuildOptions} with {@link #parsingResult()} merged in.
   *
   * <p>Returns a {@link BuildConfigurationKey} instead of just {@link BuildOptions} so that caching
   * of mappings also saves the CPU cost of interning {@link BuildConfigurationKey} (which is what
   * callers typically need).
   *
   * <p>The merging logic is as follows:
   *
   * <ul>
   *   <li>For native flags, only the fragments in the original {@link BuildOptions} are kept.
   *   <li>Any native flags in this instance, for fragments that are kept, are set to the value from
   *       this instance.
   *   <li>All Starlark flags from the original {@link BuildOptions} are kept, then all Starlark
   *       options from this instance are added, along with the scope from their parsed {@code
   *       scope} attribute when known.
   *   <li>Any Starlark flags which are present in both, the value from this instance is kept.
   *   <li>Any Starlark flags set back to their default value are removed, along with their scope.
   * </ul>
   *
   * <p>To preserve fragment trimming, this method will not expand the set of included native
   * fragments from the original {@link BuildOptions}. If the parsing result contains native options
   * whose owning fragment is not part of the original {@link BuildOptions} they will be ignored
   * (i.e. not set on the resulting options). Starlark options are not affected by this restriction.
   *
   * @param source the base options to modify
   * @return a {@link BuildConfigurationKey} to request the new configuration after applying this
   *     parsed flags value to the original options
   */
  public BuildConfigurationKey mergeWith(BuildOptions source) {
    return mergeCache.get(source);
  }

  private BuildConfigurationKey mergeWithImpl(BuildOptions source) {
    BuildOptions.Builder builder = source.toBuilder();

    // Handle native options.
    for (OptionValueDescription optionValue : parsingResult.allOptionValues()) {
      OptionDefinition optionDefinition = optionValue.getOptionDefinition();
      // All options obtained from an options parser are guaranteed to have been defined in an
      // FragmentOptions class.
      Class<? extends FragmentOptions> fragmentOptionClass =
          optionDefinition.getDeclaringClass(FragmentOptions.class);

      FragmentOptions fragment = builder.getFragmentOptions(fragmentOptionClass);
      if (fragment == null) {
        // Preserve trimming by ignoring fragments not present in the original options.
        continue;
      }
      updateOptionValue(fragment, optionDefinition, optionValue);
    }

    // Merge Starlark options. The scope info from the source options is already in the builder,
    // copied by source.toBuilder(). Add the scope info from this instance's parsed scope
    // attributes on top of it, then merge the values: flags reset to their default value are
    // removed by removeStarlarkOption, which also deletes the scope info just added.
    builder.addScopeTypeMap(
        BuildOptions.convertScopesAttributes(
            parsingResult.getScopesAttributes(), parsingResult.getStarlarkOptions()));
    for (Map.Entry<String, Object> starlarkOption : parsingResult.getStarlarkOptions().entrySet()) {
      updateStarlarkFlag(builder, starlarkOption.getKey(), starlarkOption.getValue());
    }

    return BuildConfigurationKey.create(builder.build());
  }

  private static void updateOptionValue(
      FragmentOptions fragment,
      OptionDefinition optionDefinition,
      OptionValueDescription optionValue) {
    // TODO: https://github.com/bazelbuild/bazel/issues/22453 - This will completely overwrite
    //  accumulating flags, which is almost certainly not what users want. Instead this should
    //  intelligently merge options.
    Object value = optionValue.getValue();
    optionDefinition.setValue(fragment, value);
  }

  private void updateStarlarkFlag(
      BuildOptions.Builder builder, String rawFlagName, Object rawFlagValue) {
    Label flagName = Label.parseCanonicalUnchecked(rawFlagName);
    // If the known default value is the same as the new value, unset it.
    if (isStarlarkFlagSetToDefault(rawFlagName, rawFlagValue)) {
      builder.removeStarlarkOption(flagName);
    } else {
      builder.addStarlarkOption(flagName, rawFlagValue);
    }
  }

  private boolean isStarlarkFlagSetToDefault(String rawFlagName, Object rawFlagValue) {
    var defaultVal = flags.starlarkFlagDefaults().get(rawFlagName);
    return defaultVal != null && defaultVal.equals(rawFlagValue);
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (!(obj instanceof ParsedFlagsValue that)) {
      return false;
    }
    return flags.equals(that.flags);
  }

  @Override
  public int hashCode() {
    return flags.hashCode();
  }

  @Override
  public String toString() {
    return MoreObjects.toStringHelper(this)
        .add("flags", flags)
        .add("parsingResult", parsingResult)
        .toString();
  }
}
