// Copyright 2014 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.python;

import static com.google.common.collect.ImmutableSet.toImmutableSet;

import com.google.common.base.Ascii;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.analysis.config.BuildOptionsView;
import com.google.devtools.build.lib.analysis.config.SymlinkDefinition;
import com.google.devtools.build.lib.analysis.config.transitions.TransitionFactory;
import com.google.devtools.build.lib.buildtool.BuildRequestOptions;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.packages.Attribute.AllowedValueSet;
import com.google.devtools.build.lib.packages.AttributeMap;
import com.google.devtools.build.lib.packages.RawAttributeMapper;
import com.google.devtools.build.lib.packages.RuleTransitionData;
import com.google.devtools.build.lib.packages.Type;
import com.google.devtools.build.lib.util.FileType;
import com.google.devtools.build.lib.vfs.Path;
import java.util.Set;
import java.util.function.Function;

/** Rule definitions for Python rules. */
public class PyRuleClasses {

  public static final FileType PYTHON_SOURCE = FileType.of(".py", ".py3");

  /**
   * A value set of the target and sentinel values that doesn't mention the sentinel in error
   * messages.
   */
  public static final AllowedValueSet TARGET_PYTHON_ATTR_VALUE_SET =
      new AllowedValueSet(PythonVersion.TARGET_AND_SENTINEL_STRINGS) {
        @Override
        public String getErrorReason(Object value) {
          return String.format("has to be one of 'PY2' or 'PY3' instead of '%s'", value);
        }
      };

  /**
   * Returns a rule transition factory for Python binary rules and other rules that may change the
   * Python version.
   *
   * <p>The factory reads the version specified by the target's {@code python_version} attribute (if
   * given), which must exist on the rule class. If a value was read successfully, the factory
   * returns a transition that sets the version to that value. Otherwise, the factory returns {@code
   * defaultTransition} instead.
   *
   * <p>If the attribute has an unparseable value, then the factory returns {@code
   * defaultTransition} and it is up to the rule's analysis phase ({@link
   * PyCommon#validatePythonVersionAttr}) to report an attribute error to the user. This case should
   * be prevented by attribute validation if the rule class is defined correctly.
   */
  public static TransitionFactory<RuleTransitionData> makeVersionTransition(
      PythonVersionTransition defaultTransition) {
    return (ruleData) -> {
      AttributeMap attrs = RawAttributeMapper.of(ruleData.rule());
      // Fail fast if we're used on an ill-defined rule class.
      Preconditions.checkArgument(
          attrs.has(PyCommon.PYTHON_VERSION_ATTRIBUTE, Type.STRING),
          "python version transitions require that the RuleClass define a "
              + "'python_version' attribute");
      // Attribute validation should enforce that the attribute string value is either a target
      // value ("PY2" or "PY3") or the sentinel value ("_INTERNAL_SENTINEL"). But just in case,
      // we'll, treat an invalid value as the default value rather than propagate an unchecked
      // exception in this context. That way the user can at least get a clean error message
      // instead of a crash.
      PythonVersionTransition transition;
      try {
        PythonVersion versionFromAttribute = PyCommon.readPythonVersionFromAttribute(attrs);
        if (versionFromAttribute == null) {
          transition = defaultTransition;
        } else {
          transition = PythonVersionTransition.toConstant(versionFromAttribute);
        }
      } catch (IllegalArgumentException ex) {
        transition = defaultTransition;
      }
      return transition;
    };
  }

  /**
   * A Python version transition that sets the version as specified by the target's attributes, with
   * a default determined by {@link PythonOptions#getDefaultPythonVersion}.
   */
  public static final TransitionFactory<RuleTransitionData> VERSION_TRANSITION =
      makeVersionTransition(PythonVersionTransition.toDefault());

  /** The py2 and py3 symlinks. */
  public enum PySymlink implements SymlinkDefinition {
    PY2(PythonVersion.PY2),
    PY3(PythonVersion.PY3);

    private final String versionString;
    private final PythonVersionTransition transition;

    PySymlink(PythonVersion version) {
      this.versionString = Ascii.toLowerCase(version.toString());
      this.transition = PythonVersionTransition.toConstant(version);
    }

    @Override
    public String getLinkName(String symlinkPrefix, String productName, String workspaceBaseName) {
      return symlinkPrefix + versionString;
    }

    @Override
    public ImmutableSet<Path> getLinkPaths(
        BuildRequestOptions buildRequestOptions,
        Set<BuildConfigurationValue> targetConfigs,
        Function<BuildOptions, BuildConfigurationValue> configGetter,
        RepositoryName repositoryName,
        Path outputPath,
        Path execRoot) {
      if (!buildRequestOptions.experimentalCreatePySymlinks) {
        return ImmutableSet.of();
      }
      EventHandler e =
          event -> {
            throw new UnsupportedOperationException(
                "This transition shouldn't do anything that could fail.\n"
                    + "TODO(bazel-team): refactor this to not call patch(). Blaze code should"
                    + " not apply transitions unless it absolutely has to, since that requires"
                    + " sequencing (like supporting Starlark flags and handling exceptions)"
                    + " that's easy to get wrong.");
          };
      return targetConfigs.stream()
          .map(
              config ->
                  configGetter.apply(
                      transition.patch(
                          new BuildOptionsView(
                              config.getOptions(), transition.requiresOptionFragments()),
                          e)))
          .map(config -> config.getOutputDirectory(repositoryName).getRoot().asPath())
          .distinct()
          .collect(toImmutableSet());
    }
  }
}
