// 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.analysis.actions;

import com.google.auto.value.AutoValue;
import com.google.common.base.Function;
import com.google.devtools.build.lib.util.Preconditions;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Strings used to express requirements on action execution environments.
 */
public class ExecutionRequirements {

  /** An execution requirement that can be split into a key and a value part using a regex. */
  @AutoValue
  public abstract static class ParseableRequirement {

    /**
     * Thrown when a {@link ParseableRequirement} feels responsible for a tag, but the {@link
     * #validator()} method returns an error.
     */
    public static class ValidationException extends Exception {
      private final String tagValue;

      /**
       * Creates a new {@link ValidationException}.
       *
       * @param tagValue the erroneous value that was parsed from the tag.
       * @param errorMsg an error message that tells the user what's wrong with the value.
       */
      public ValidationException(String tagValue, String errorMsg) {
        super(errorMsg);
        this.tagValue = tagValue;
      }

      /**
       * Returns the erroneous value of the parsed tag.
       *
       * <p>Useful to put in error messages shown to the user.
       */
      public String getTagValue() {
        return tagValue;
      }
    }

    /**
     * Create a new parseable execution requirement definition.
     *
     * <p>If a tag doesn't match the detectionPattern, it will be ignored. If a tag matches the
     * detectionPattern, but not the validationPattern, it is assumed that the value is somehow
     * wrong (e.g. the user put a float or random string where we expected an integer).
     *
     * @param userFriendlyName a human readable name of the tag and its format, e.g. "cpu:<int>"
     * @param detectionPattern a regex that will be used to detect whether a tag matches this
     *     execution requirement. It should have one capture group that grabs the value of the tag.
     *     This should be general enough to permit even wrong value types. Example: "cpu:(.+)".
     * @param validator a Function that will be used to validate the value of the tag. It should
     *     return null if the value is fine to use or a human-friendly error message describing why
     *     the value is not valid.
     */
    static ParseableRequirement create(
        String userFriendlyName, Pattern detectionPattern, Function<String, String> validator) {
      return new AutoValue_ExecutionRequirements_ParseableRequirement(
          userFriendlyName, detectionPattern, validator);
    }

    public abstract String userFriendlyName();

    public abstract Pattern detectionPattern();

    public abstract Function<String, String> validator();

    /**
     * Returns the parsed value from a tag, if this {@link ParseableRequirement} detects that it is
     * responsible for it, otherwise returns {@code null}.
     *
     * @throws ValidationException if the value parsed out of the tag doesn't pass the validator.
     */
    public String parseIfMatches(String tag) throws ValidationException {
      Matcher matcher = detectionPattern().matcher(tag);
      if (!matcher.matches()) {
        return null;
      }
      String tagValue = matcher.group(1);
      String errorMsg = validator().apply(tagValue);
      if (errorMsg != null) {
        throw new ValidationException(tagValue, errorMsg);
      }
      return tagValue;
    }
  }

  /** If an action would not successfully run other than on Darwin. */
  public static final String REQUIRES_DARWIN = "requires-darwin";

  /** How many hardware threads an action requires for execution. */
  public static final ParseableRequirement CPU =
      ParseableRequirement.create(
          "cpu:<int>",
          Pattern.compile("cpu:(.+)"),
          new Function<String, String>() {
            @Override
            public String apply(String s) {
              Preconditions.checkNotNull(s);

              int value;
              try {
                value = Integer.parseInt(s);
              } catch (NumberFormatException e) {
                return "can't be parsed as an integer";
              }

              // De-and-reserialize & compare to only allow canonical integer formats.
              if (!Integer.toString(value).equals(s)) {
                return "must be in canonical format (e.g. '4' instead of '+04')";
              }

              if (value < 1) {
                return "can't be zero or negative";
              }

              return null;
            }
          });
}
