// Copyright 2017 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.analysis.platform;

import static com.google.common.collect.ImmutableList.toImmutableList;

import com.google.common.base.VerifyException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
import com.google.devtools.build.lib.analysis.platform.ConstraintCollection.DuplicateConstraintException;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.packages.BuiltinProvider;
import com.google.devtools.build.lib.packages.NativeInfo;
import com.google.devtools.build.lib.starlarkbuildapi.platform.PlatformInfoApi;
import com.google.devtools.build.lib.util.Fingerprint;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.List;
import java.util.Objects;
import javax.annotation.Nullable;
import net.starlark.java.eval.Printer;
import net.starlark.java.eval.StarlarkSemantics;

/** Provider for a platform, which is a group of constraints and values. */
@Immutable
public class PlatformInfo extends NativeInfo
    implements PlatformInfoApi<ConstraintSettingInfo, ConstraintValueInfo> {

  /** Name used in Starlark for accessing this provider. */
  public static final String STARLARK_NAME = "PlatformInfo";

  /**
   * Empty {@link PlatformInfo} instance for a invalid or empty (e.g. builtin) actions. See also
   * src/main/starlark/builtins_bzl/platforms/BUILD#empty
   */
  public static final PlatformInfo EMPTY_PLATFORM_INFO;

  static {
    try {
      EMPTY_PLATFORM_INFO =
          PlatformInfo.builder().setLabel(PlatformConstants.INTERNAL_PLATFORM).build();
    } catch (DuplicateConstraintException | ExecPropertiesException e) {
      // This can never happen since we're not passing any values to the builder.
      throw new VerifyException(e);
    }
  }

  /** Provider singleton constant. */
  public static final BuiltinProvider<PlatformInfo> PROVIDER =
      new BuiltinProvider<PlatformInfo>(STARLARK_NAME, PlatformInfo.class) {};

  private final Label label;
  private final ConstraintCollection constraints;

  private final PlatformProperties execProperties;

  private final ImmutableList<String> flags;

  private final ImmutableList<ConfigMatchingProvider> requiredSettings;

  private final boolean checkToolchainTypes;
  private final ImmutableList<Label> allowedToolchainTypes;

  @Nullable private final String missingToolchainErrorMessage;

  private PlatformInfo(
      Label label,
      ConstraintCollection constraints,
      PlatformProperties execProperties,
      ImmutableList<String> flags,
      ImmutableList<ConfigMatchingProvider> requiredSettings,
      boolean checkToolchainTypes,
      ImmutableList<Label> allowedToolchainTypes,
      String missingToolchainErrorMessage) {
    this.label = label;
    this.constraints = constraints;
    this.execProperties = execProperties;
    this.flags = flags;
    this.requiredSettings = requiredSettings;
    this.checkToolchainTypes = checkToolchainTypes;
    this.allowedToolchainTypes = allowedToolchainTypes;
    this.missingToolchainErrorMessage = missingToolchainErrorMessage;
  }

  @Override
  public BuiltinProvider<PlatformInfo> getProvider() {
    return PROVIDER;
  }

  @Override
  public Label label() {
    return label;
  }

  @Override
  public ConstraintCollection constraints() {
    return constraints;
  }

  public ImmutableMap<String, String> execProperties() {
    return execProperties.properties();
  }

  public ImmutableList<String> flags() {
    return flags;
  }

  public ImmutableList<ConfigMatchingProvider> requiredSettings() {
    return requiredSettings;
  }

  public boolean checkToolchainTypes() {
    return checkToolchainTypes;
  }

  public ImmutableList<Label> allowedToolchainTypes() {
    return allowedToolchainTypes;
  }

  @Nullable
  public String getMissingToolchainErrorMessage() {
    return missingToolchainErrorMessage;
  }

  @Override
  public void repr(Printer printer, StarlarkSemantics semantics) {
    printer.append(String.format("PlatformInfo(%s, constraints=%s)", label, constraints));
  }

  /** Add this platform to the given fingerprint. */
  public void addTo(Fingerprint fp) {
    fp.addString(label.toString());
    constraints.addToFingerprint(fp);
    fp.addStringMap(execProperties.properties());
    fp.addStrings(flags);
    fp.addStrings(
        requiredSettings.stream()
            .map(ConfigMatchingProvider::label)
            .map(Label::toString)
            .collect(toImmutableList()));
    fp.addStrings(allowedToolchainTypes.stream().map(Label::toString).collect(toImmutableList()));
    fp.addBoolean(checkToolchainTypes);
    fp.addNullableString(missingToolchainErrorMessage);
  }

  @Override
  public boolean equals(Object o) {
    if (!(o instanceof PlatformInfo that)) {
      return false;
    }
    return Objects.equals(label, that.label)
        && Objects.equals(constraints, that.constraints)
        && Objects.equals(execProperties, that.execProperties)
        && Objects.equals(flags, that.flags)
        && Objects.equals(requiredSettings, that.requiredSettings)
        && (checkToolchainTypes == that.checkToolchainTypes)
        && Objects.equals(allowedToolchainTypes, that.allowedToolchainTypes)
        && Objects.equals(missingToolchainErrorMessage, that.missingToolchainErrorMessage);
  }

  @Override
  public int hashCode() {
    return Objects.hash(
        label,
        constraints,
        execProperties,
        flags,
        requiredSettings,
        checkToolchainTypes,
        allowedToolchainTypes,
        missingToolchainErrorMessage);
  }

  /** Returns a new {@link Builder} for creating a fresh {@link PlatformInfo} instance. */
  public static Builder builder() {
    return new Builder();
  }

  /** Builder class to facilitate creating valid {@link PlatformInfo} instances. */
  public static class Builder {

    @Nullable private PlatformInfo parent = null;
    private Label label;
    private final ConstraintCollection.Builder constraints = ConstraintCollection.builder();
    private final PlatformProperties.Builder execPropertiesBuilder = PlatformProperties.builder();
    private final ImmutableList.Builder<String> flags = new ImmutableList.Builder<>();
    private final ImmutableList.Builder<ConfigMatchingProvider> requiredSettings =
        new ImmutableList.Builder<>();
    private boolean checkToolchainTypes = false;
    private final ImmutableList.Builder<Label> allowedToolchainTypes =
        new ImmutableList.Builder<>();
    @Nullable private String missingToolchainErrorMessage = null;

    /**
     * Sets the parent {@link PlatformInfo} that this platform inherits from. Constraint values set
     * directly on this instance will be kept, but any other constraint settings will be found from
     * the parent, if set.
     *
     * @param parent the platform that is the parent of this platform
     * @return the {@link Builder} instance for method chaining
     */
    @CanIgnoreReturnValue
    public Builder setParent(@Nullable PlatformInfo parent) {
      this.parent = parent;
      if (parent == null) {
        this.constraints.parent(null);
        this.execPropertiesBuilder.setParent(null);
      } else {
        this.constraints.parent(parent.constraints);
        this.execPropertiesBuilder.setParent(parent.execProperties);
      }
      return this;
    }

    /**
     * Sets the {@link Label} for this {@link PlatformInfo}.
     *
     * @param label the label identifying this platform
     * @return the {@link Builder} instance for method chaining
     */
    @CanIgnoreReturnValue
    public Builder setLabel(Label label) {
      this.label = label;
      return this;
    }

    /**
     * Adds the given constraint value to the constraints that define this {@link PlatformInfo}.
     *
     * @param constraint the constraint to add
     * @return the {@link Builder} instance for method chaining
     */
    @CanIgnoreReturnValue
    public Builder addConstraint(ConstraintValueInfo constraint) {
      this.constraints.addConstraints(constraint);
      return this;
    }

    /**
     * Adds the given constraint values to the constraints that define this {@link PlatformInfo}.
     *
     * @param constraints the constraints to add
     * @return the {@link Builder} instance for method chaining
     */
    @CanIgnoreReturnValue
    public Builder addConstraints(Iterable<ConstraintValueInfo> constraints) {
      this.constraints.addConstraints(constraints);
      return this;
    }

    /**
     * Sets the execution properties.
     *
     * <p>If there is a parent {@link PlatformInfo} set, then all parent's properties will be
     * inherited. Any properties included in both will use the child's value. Use the value of empty
     * string to unset a property.
     */
    @CanIgnoreReturnValue
    public Builder setExecProperties(@Nullable ImmutableMap<String, String> properties) {
      this.execPropertiesBuilder.setProperties(properties);
      return this;
    }

    /** Add the given flags to this {@link PlatformInfo}. */
    @CanIgnoreReturnValue
    public Builder addFlags(Iterable<String> flags) {
      this.flags.addAll(flags);
      return this;
    }

    /** Add the given settings to this {@link PlatformInfo}. */
    @CanIgnoreReturnValue
    public Builder addRequiredSettings(List<ConfigMatchingProvider> requiredSettings) {
      this.requiredSettings.addAll(requiredSettings);
      return this;
    }

    @CanIgnoreReturnValue
    public Builder checkToolchainTypes(boolean checkToolchainTypes) {
      this.checkToolchainTypes = checkToolchainTypes;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder addAllowedToolchainTypes(List<Label> allowedToolchainTypes) {
      this.allowedToolchainTypes.addAll(allowedToolchainTypes);
      return this;
    }

    /**
     * Sets an error message to display when a required toolchain cannot be resolved for this
     * platform.
     */
    @CanIgnoreReturnValue
    public Builder setMissingToolchainErrorMessage(@Nullable String message) {
      if (message == null || message.isEmpty()) {
        this.missingToolchainErrorMessage = null;
      } else {
        this.missingToolchainErrorMessage = message;
      }
      return this;
    }

    /**
     * Returns the new {@link PlatformInfo} instance.
     *
     * @throws DuplicateConstraintException if more than one constraint value exists for the same
     *     constraint setting
     */
    public PlatformInfo build() throws DuplicateConstraintException, ExecPropertiesException {
      // Merge parent flags and this builder's flags. Parent flags always come first so that flags
      // from this builder will override or combine, depending on the flag type.
      ImmutableList.Builder<String> flagBuilder = new ImmutableList.Builder<>();
      if (this.parent != null) {
        flagBuilder.addAll(this.parent.flags);
      }
      flagBuilder.addAll(this.flags.build());

      // Required settings are explicitly **not** inherited from the parent, so do not merge.
      ImmutableList<ConfigMatchingProvider> settings = requiredSettings.build();

      return new PlatformInfo(
          label,
          constraints.build(),
          execPropertiesBuilder.build(),
          flagBuilder.build(),
          settings,
          checkToolchainTypes,
          allowedToolchainTypes.build(),
          missingToolchainErrorMessage);
    }
  }

  /** Exception that indicates something is wrong in exec_properties configuration. */
  public static class ExecPropertiesException extends Exception {
    ExecPropertiesException(String message) {
      super(message);
    }
  }
}
