// 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.syntax;

import com.google.common.primitives.Booleans;
import com.google.devtools.build.lib.skylarkinterface.Param;
import com.google.devtools.build.lib.skylarkinterface.SkylarkSignature;
import com.google.devtools.build.lib.syntax.BuiltinFunction.ExtraArgKind;
import com.google.devtools.build.lib.util.Preconditions;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;

/**
 * This class defines utilities to process @SkylarkSignature annotations
 * to configure a given field.
 */
public class SkylarkSignatureProcessor {
  /**
   * Extracts a {@code FunctionSignature.WithValues<Object, SkylarkType>} from an annotation
   * @param name the name of the function
   * @param annotation the annotation
   * @param defaultValues an optional list of default values
   * @param paramDoc an optional list into which to store documentation strings
   * @param enforcedTypesList an optional list into which to store effective types to enforce
   */
  // NB: the two arguments paramDoc and enforcedTypesList are used to "return" extra values via
  // side-effects, and that's ugly
  // TODO(bazel-team): use AutoValue to declare a value type to use as return value?
  public static FunctionSignature.WithValues<Object, SkylarkType> getSignature(
      String name, SkylarkSignature annotation,
      @Nullable Iterable<Object> defaultValues,
      @Nullable List<String> paramDoc, @Nullable List<SkylarkType> enforcedTypesList) {

    Preconditions.checkArgument(name.equals(annotation.name()),
        "%s != %s", name, annotation.name());
    ArrayList<Parameter<Object, SkylarkType>> paramList = new ArrayList<>();
    HashMap<String, SkylarkType> enforcedTypes = enforcedTypesList == null
        ? null : new HashMap<String, SkylarkType>();

    HashMap<String, String> doc = new HashMap<>();
    boolean documented = annotation.documented();
    if (annotation.doc().isEmpty() && documented) {
      throw new RuntimeException(String.format("function %s is undocumented", name));
    }

    Iterator<Object> defaultValuesIterator = defaultValues == null
        ? null : defaultValues.iterator();
    try {
      boolean named = false;
      for (Param param : annotation.parameters()) {
        boolean mandatory = param.defaultValue() != null && param.defaultValue().isEmpty();
        Object defaultValue = mandatory ? null : getDefaultValue(param, defaultValuesIterator);
        if (param.named() && !param.positional() && !named) {
          named = true;
          @Nullable Param starParam = null;
          if (!annotation.extraPositionals().name().isEmpty()) {
            starParam = annotation.extraPositionals();
          }
          paramList.add(getParameter(name, starParam, enforcedTypes, doc, documented,
                /*mandatory=*/false, /*star=*/true, /*starStar=*/false, /*defaultValue=*/null));
        }
        paramList.add(getParameter(name, param, enforcedTypes, doc, documented,
                mandatory, /*star=*/false, /*starStar=*/false, defaultValue));
      }
      if (!annotation.extraPositionals().name().isEmpty() && !named) {
        paramList.add(getParameter(name, annotation.extraPositionals(), enforcedTypes, doc,
            documented, /*mandatory=*/false, /*star=*/true, /*starStar=*/false,
            /*defaultValue=*/null));
      }
      if (!annotation.extraKeywords().name().isEmpty()) {
        paramList.add(
            getParameter(name, annotation.extraKeywords(), enforcedTypes, doc, documented,
                /*mandatory=*/false, /*star=*/false, /*starStar=*/true, /*defaultValue=*/null));
      }
      FunctionSignature.WithValues<Object, SkylarkType> signature =
          FunctionSignature.WithValues.<Object, SkylarkType>of(paramList);
      for (String paramName : signature.getSignature().getNames()) {
        if (enforcedTypesList != null) {
          enforcedTypesList.add(enforcedTypes.get(paramName));
        }
        if (paramDoc != null) {
          paramDoc.add(doc.get(paramName));
        }
      }
      return signature;
    } catch (FunctionSignature.SignatureException e) {
      throw new RuntimeException(String.format(
          "Invalid signature while configuring BuiltinFunction %s", name), e);
    }
  }

  /**
   * Configures the parameter of this Skylark function using the annotation.
   */
  // TODO(bazel-team): Maybe have the annotation be a string representing the
  // python-style calling convention including default values, and have the regular Parser
  // process it? (builtin function call not allowed when evaluating values, but more complex
  // values are possible by referencing variables in some definition environment).
  // Then the only per-parameter information needed is a documentation string.
  private static Parameter<Object, SkylarkType> getParameter(
      String name, Param param, Map<String, SkylarkType> enforcedTypes,
      Map<String, String> paramDoc, boolean documented,
      boolean mandatory, boolean star, boolean starStar, @Nullable Object defaultValue)
      throws FunctionSignature.SignatureException {

    @Nullable SkylarkType officialType = null;
    @Nullable SkylarkType enforcedType = null;
    if (star && param == null) { // pseudo-parameter to separate positional from named-only
      return new Parameter.Star<>(null);
    }
    if (param.type() != Object.class) {
      if (param.generic1() != Object.class) {
        // Enforce the proper parametric type for Skylark list and set objects
        officialType = SkylarkType.of(param.type(), param.generic1());
        enforcedType = officialType;
      } else {
        officialType = SkylarkType.of(param.type());
        enforcedType = officialType;
      }
      if (param.callbackEnabled()) {
        officialType = SkylarkType.Union.of(
            officialType, SkylarkType.SkylarkFunctionType.of(name, officialType));
        enforcedType = SkylarkType.Union.of(
            enforcedType, SkylarkType.SkylarkFunctionType.of(name, enforcedType));
      }
      if (param.noneable()) {
        officialType = SkylarkType.Union.of(officialType, SkylarkType.NONE);
        enforcedType = SkylarkType.Union.of(enforcedType, SkylarkType.NONE);
      }
    }
    if (enforcedTypes != null) {
      enforcedTypes.put(param.name(), enforcedType);
    }
    if (param.doc().isEmpty() && documented) {
      throw new RuntimeException(
          String.format("parameter %s on method %s is undocumented", param.name(), name));
    }
    if (paramDoc != null) {
      paramDoc.put(param.name(), param.doc());
    }
    if (starStar) {
      return new Parameter.StarStar<>(param.name(), officialType);
    } else if (star) {
      return new Parameter.Star<>(param.name(), officialType);
    } else if (mandatory) {
      return new Parameter.Mandatory<>(param.name(), officialType);
    } else if (defaultValue != null && enforcedType != null) {
      Preconditions.checkArgument(enforcedType.contains(defaultValue),
          "In function '%s', parameter '%s' has default value %s that isn't of enforced type %s",
          name, param.name(), Printer.repr(defaultValue), enforcedType);
    }
    return new Parameter.Optional<>(param.name(), officialType, defaultValue);
  }

  static Object getDefaultValue(Param param, Iterator<Object> iterator) {
    if (iterator != null) {
      return iterator.next();
    } else if (param.defaultValue().isEmpty()) {
      return Runtime.NONE;
    } else {
      try (Mutability mutability = Mutability.create("initialization")) {
        Environment env =
            Environment.builder(mutability)
                .setGlobals(Environment.CONSTANTS_ONLY)
                .setEventHandler(Environment.FAIL_FAST_HANDLER)
                .build()
                .update("unbound", Runtime.UNBOUND);
        return BuildFileAST.eval(env, param.defaultValue());
      } catch (Exception e) {
        throw new RuntimeException(String.format(
            "Exception while processing @SkylarkSignature.Param %s, default value %s",
            param.name(), param.defaultValue()), e);
      }
    }
  }

  /** Extract additional signature information for BuiltinFunction-s */
  public static ExtraArgKind[] getExtraArgs(SkylarkSignature annotation) {
    final int numExtraArgs =
        Booleans.countTrue(
            annotation.useLocation(), annotation.useAst(), annotation.useEnvironment());
    if (numExtraArgs == 0) {
      return null;
    }
    final ExtraArgKind[] extraArgs = new ExtraArgKind[numExtraArgs];
    int i = 0;
    if (annotation.useLocation()) {
      extraArgs[i++] = ExtraArgKind.LOCATION;
    }
    if (annotation.useAst()) {
      extraArgs[i++] = ExtraArgKind.SYNTAX_TREE;
    }
    if (annotation.useEnvironment()) {
      extraArgs[i++] = ExtraArgKind.ENVIRONMENT;
    }
    return extraArgs;
  }

  /**
   * Configure all BaseFunction-s in a class from their @SkylarkSignature annotations
   * @param type a class containing BuiltinFunction fields that need be configured.
   * This function is typically called in a static block to initialize a class,
   * i.e. a class {@code Foo} containing @SkylarkSignature annotations would end with
   * {@code static { SkylarkSignatureProcessor.configureSkylarkFunctions(Foo.class); }}
   */
  public static void configureSkylarkFunctions(Class<?> type) {
    for (Field field : type.getDeclaredFields()) {
      if (field.isAnnotationPresent(SkylarkSignature.class)) {
        // The annotated fields are often private, but we need access them.
        field.setAccessible(true);
        SkylarkSignature annotation = field.getAnnotation(SkylarkSignature.class);
        Object value = null;
        try {
          value = Preconditions.checkNotNull(field.get(null),
              String.format(
                  "Error while trying to configure %s.%s: its value is null", type, field));
          if (BaseFunction.class.isAssignableFrom(field.getType())) {
            BaseFunction function = (BaseFunction) value;
            if (!function.isConfigured()) {
              function.configure(annotation);
            }
            Class<?> nameSpace = function.getObjectType();
            if (nameSpace != null) {
              Preconditions.checkState(!(function instanceof BuiltinFunction.Factory));
              nameSpace = Runtime.getCanonicalRepresentation(nameSpace);
              Runtime.registerFunction(nameSpace, function);
            }
          }
        } catch (IllegalAccessException e) {
          throw new RuntimeException(String.format(
              "Error while trying to configure %s.%s (value %s)", type, field, value), e);
        }
      }
    }
  }
}
