// Copyright 2018 Google LLC
//
// 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.cloud.deploymentmanager.autogen;

import static com.google.cloud.deploymentmanager.autogen.proto.GceMetadataItem.ValueSpecCase.VALUE_FROM_DEPLOY_INPUT_FIELD;
import static com.google.cloud.deploymentmanager.autogen.proto.LocalSsdSpec.CountSpecCase.COUNT_FROM_DEPLOY_INPUT_FIELD;
import static com.google.template.soy.data.SoyValueConverter.markAsSoyMap;

import com.google.cloud.deploymentmanager.autogen.proto.BooleanExpression;
import com.google.cloud.deploymentmanager.autogen.proto.BooleanExpression.BooleanDeployInputField;
import com.google.cloud.deploymentmanager.autogen.proto.BooleanExpression.ExternalIpAvailability;
import com.google.cloud.deploymentmanager.autogen.proto.DeployInputField;
import com.google.cloud.deploymentmanager.autogen.proto.DeployInputSection;
import com.google.cloud.deploymentmanager.autogen.proto.DeployInputSpec;
import com.google.cloud.deploymentmanager.autogen.proto.DiskSpec;
import com.google.cloud.deploymentmanager.autogen.proto.DiskSpec.DeviceName.DeviceNameCase;
import com.google.cloud.deploymentmanager.autogen.proto.ExternalIpSpec;
import com.google.cloud.deploymentmanager.autogen.proto.ExternalIpSpec.Type;
import com.google.cloud.deploymentmanager.autogen.proto.GceMetadataItem;
import com.google.cloud.deploymentmanager.autogen.proto.MultiVmDeploymentPackageSpec;
import com.google.cloud.deploymentmanager.autogen.proto.SingleVmDeploymentPackageSpec;
import com.google.cloud.deploymentmanager.autogen.proto.VmTierSpec;
import com.google.cloud.deploymentmanager.autogen.proto.ZoneSpec;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.multibindings.Multibinder;
import com.google.protobuf.Message;
import com.google.template.soy.data.SoyList;
import com.google.template.soy.data.SoyProtoValue;
import com.google.template.soy.data.SoyValue;
import com.google.template.soy.data.SoyValueConverter;
import com.google.template.soy.data.restricted.NullData;
import com.google.template.soy.data.restricted.StringData;
import com.google.template.soy.shared.restricted.Signature;
import com.google.template.soy.shared.restricted.SoyFunction;
import com.google.template.soy.shared.restricted.SoyFunctionSignature;
import com.google.template.soy.shared.restricted.SoyJavaFunction;
import com.google.template.soy.shared.restricted.TypedSoyFunction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Additional soy functions to support our templating.
 */
final class SoyFunctions {

  static final class Module extends AbstractModule {
    @Override
    protected void configure() {
      Multibinder<SoyFunction> soyFunctionSetBinder =
          Multibinder.newSetBinder(binder(), SoyFunction.class);
      soyFunctionSetBinder.addBinding().to(DependentTiers.class);
      soyFunctionSetBinder.addBinding().to(DeployInputFieldIsString.class);
      soyFunctionSetBinder.addBinding().to(DeployInputFieldName.class);
      soyFunctionSetBinder.addBinding().to(FindDeployInputField.class);
      soyFunctionSetBinder.addBinding().to(FindInputsWithTestDefaultValues.class);
      soyFunctionSetBinder.addBinding().to(FindInputTestDefaultValue.class);
      soyFunctionSetBinder.addBinding().to(FindDisplayGroup.class);
      soyFunctionSetBinder.addBinding().to(FindVmTier.class);
      soyFunctionSetBinder.addBinding().to(FieldValueLabelMap.class);
      soyFunctionSetBinder.addBinding().to(TierPrefixed.class);
      soyFunctionSetBinder.addBinding().to(TierTemplateName.class);
      soyFunctionSetBinder.addBinding().to(BooleanExpressionDisplayCondition.class);
      soyFunctionSetBinder.addBinding().to(BooleanExpressionJinjaExpression.class);
      soyFunctionSetBinder.addBinding().to(AdditionalDiskTypePropertyName.class);
      soyFunctionSetBinder.addBinding().to(AdditionalDiskSizePropertyName.class);
      soyFunctionSetBinder.addBinding().to(ListDeployInputFields.class);
      soyFunctionSetBinder.addBinding().to(ExternalIpTypeName.class);
      soyFunctionSetBinder.addBinding().to(SolutionHasGpus.class);
      soyFunctionSetBinder.addBinding().to(GetTestConfigDefaultValues.class);
    }
  }

  /**
   * Returns all a tiers that the tier specified in the first argument depends upon.
   */
  @VisibleForTesting
  @Singleton
  @SoyFunctionSignature(
      name = "dependentTiers",
      value = {
        @Signature(
            parameterTypes = {
              "cloud.deploymentmanager.autogen.VmTierSpec",
              "cloud.deploymentmanager.autogen.MultiVmDeploymentPackageSpec"
            },
            returnType = "list<cloud.deploymentmanager.autogen.VmTierSpec>")
      })
  static final class DependentTiers extends TypedSoyFunction implements SoyJavaFunction {
    @Inject
    DependentTiers() {}

    @Override
    public SoyValue computeForJava(List<SoyValue> args) {
      VmTierSpec tier = (VmTierSpec) ((SoyProtoValue) args.get(0)).getProto();
      List<VmTierSpec> tierList;
      try {
        tierList = extractTierList(args.get(1));
      } catch (IllegalArgumentException e) {
        throw new IllegalArgumentException("Unexpected 2nd arg type for dependentTiers", e);
      }

      Set<Integer> dependentIndices = new HashSet<>();
      for (GceMetadataItem item : tier.getGceMetadataItemsList()) {
        if (item.hasTierVmNames()) {
          dependentIndices.add(findTier(item.getTierVmNames().getTier(), tierList));
        }
      }

      // List dependents in the order they appear in the tier list.
      List<VmTierSpec> dependents = new ArrayList<>(dependentIndices.size());
      for (int i = 0; i < tierList.size(); i++) {
        if (dependentIndices.contains(i)) {
          dependents.add(tierList.get(i));
        }
      }
      return SoyValueConverter.INSTANCE.convert(dependents).resolve();
    }

    private int findTier(String name, List<VmTierSpec> tierList) {
      for (int i = 0; i < tierList.size(); i++) {
        if (name.equals(tierList.get(i).getName())) {
          return i;
        }
      }
      throw new IllegalArgumentException("Unable to find tier with name: " + name);
    }
  }

  @Singleton
  @SoyFunctionSignature(
      name = "deployInputFieldIsString",
      value = {
        @Signature(
            parameterTypes = {"cloud.deploymentmanager.autogen.DeployInputField"},
            returnType = "bool")
      })
  private static final class DeployInputFieldIsString extends TypedSoyFunction
      implements SoyJavaFunction {
    @Inject
    DeployInputFieldIsString() {}

    @Override
    public SoyValue computeForJava(List<SoyValue> args) {
      DeployInputField field = (DeployInputField) ((SoyProtoValue) args.get(0)).getProto();
      switch (field.getTypeCase()) {
        case STRING_BOX:
        case EMAIL_BOX:
        case STRING_DROPDOWN:
        case ZONE_DROPDOWN:
          return SoyValueConverter.INSTANCE.convert(true).resolve();
        default:
          return SoyValueConverter.INSTANCE.convert(false).resolve();
      }
    }
  }

  /**
   * Derives the name for a deploy input field.
   *
   * <p>While we can take a string instead, forcing a proper field definition prevents the mistake
   * of entering an invalid field name. {@link FindDeployInputField} can be used to look up the
   * field definition from a field name, which will ensure that any invalid field name will fail.
   */
  @VisibleForTesting
  @Singleton
  @SoyFunctionSignature(
      name = "deployInputFieldName",
      value = {
        @Signature(
            parameterTypes = {"cloud.deploymentmanager.autogen.DeployInputField"},
            returnType = "string")
      })
  static final class DeployInputFieldName extends TypedSoyFunction implements SoyJavaFunction {
    @Inject
    DeployInputFieldName() {}

    @Override
    public SoyValue computeForJava(List<SoyValue> args) {
      DeployInputField field = (DeployInputField) ((SoyProtoValue) args.get(0)).getProto();
      return StringData.forValue(formatFieldName(field.getName()));
    }

    public static String formatFieldName(String fieldSpecName) {
      return String.format("input_%s", fieldSpecName);
    }
  }

  /** Finds the input field definition from the field name. */
  @VisibleForTesting
  @Singleton
  @SoyFunctionSignature(
      name = "findDeployInputField",
      value = {
        @Signature(
            parameterTypes = {
              "string",
              "cloud.deploymentmanager.autogen.DeployInputSpec|null|undefined"
            },
            returnType = "cloud.deploymentmanager.autogen.DeployInputField")
      })
  static final class FindDeployInputField extends TypedSoyFunction implements SoyJavaFunction {
    @Inject
    FindDeployInputField() {}

    @Override
    public SoyValue computeForJava(List<SoyValue> list) {
      String name = list.get(0).stringValue();
      DeployInputSpec deployInputSpec = (DeployInputSpec) ((SoyProtoValue) list.get(1)).getProto();
      for (DeployInputSection section : deployInputSpec.getSectionsList()) {
        for (DeployInputField field : section.getFieldsList()) {
          if (name.equals(field.getName())) {
            return SoyValueConverter.INSTANCE.convert(field).resolve();
          }
        }
      }
      throw new IllegalArgumentException("Unable to find deploy input field named: " + name);
    }
  }

  /**
   * Finds the collection of required input fields that have no defaultValue.
   */
  @VisibleForTesting
  @Singleton
  @SoyFunctionSignature(
      name = "findInputsWithTestDefaultValues",
      value = {
        @Signature(
            parameterTypes = {"cloud.deploymentmanager.autogen.DeployInputSpec"},
            returnType = "list<cloud.deploymentmanager.autogen.DeployInputField>")
      })
  static final class FindInputsWithTestDefaultValues extends TypedSoyFunction
      implements SoyJavaFunction {
    @Inject
    FindInputsWithTestDefaultValues() {}

    @Override
    public SoyValue computeForJava(List<SoyValue> args) {
      DeployInputSpec deployInputSpec = (DeployInputSpec) ((SoyProtoValue) args.get(0)).getProto();
      List<DeployInputField> filteredFields = new ArrayList<>();
      for (DeployInputSection section : deployInputSpec.getSectionsList()) {
        for (DeployInputField field : section.getFieldsList()) {
          if (hasTestDefaultValue(field)) {
            filteredFields.add(field);
          }
        }
      }
      return SoyValueConverter.INSTANCE.convert(filteredFields).resolve();
    }

    private boolean hasTestDefaultValue(DeployInputField field) {
      switch (field.getTypeCase()) {
        case STRING_BOX:
          return !Strings.isNullOrEmpty(field.getStringBox().getTestDefaultValue());
        case EMAIL_BOX:
          return !field.getEmailBox().getTestDefaultValue().isEmpty();
        case INTEGER_BOX:
          return field.getIntegerBox().hasTestDefaultValue();
        default:
          return false;
      }
    }
  }

  /**
   * Finds the default test value for given input field.
   */
  @VisibleForTesting
  @Singleton
  @SoyFunctionSignature(
      name = "findInputTestDefaultValue",
      value = {
        @Signature(
            parameterTypes = {"cloud.deploymentmanager.autogen.DeployInputField"},
            returnType = "string|int")
      })
  static final class FindInputTestDefaultValue extends TypedSoyFunction implements SoyJavaFunction {
    @Inject
    FindInputTestDefaultValue() {}

    @Override
    public SoyValue computeForJava(List<SoyValue> args) {
      DeployInputField field = (DeployInputField) ((SoyProtoValue) args.get(0)).getProto();
      Object value;
      switch (field.getTypeCase()) {
        case STRING_BOX:
          value = field.getStringBox().getTestDefaultValue();
          break;
        case EMAIL_BOX:
          value = field.getEmailBox().getTestDefaultValue();
          break;
        case INTEGER_BOX:
          value = field.getIntegerBox().getTestDefaultValue().getValue();
          break;
        default:
          throw new IllegalArgumentException(
              String.format(
                  "Unexpected field type '%s' for field '%s')",
                  field.getTypeCase(),
                  field.getName()));
      }
      return SoyValueConverter.INSTANCE.convert(value).resolve();
    }
  }

  @VisibleForTesting
  @Singleton
  @SoyFunctionSignature(
      name = "getTestConfigDefaultValues",
      value = {
        @Signature(
            parameterTypes = {
              "cloud.deploymentmanager.autogen.MultiVmDeploymentPackageSpec|cloud.deploymentmanager.autogen.SingleVmDeploymentPackageSpec"
            },
            returnType = "map<string, string>")
      })
  static final class GetTestConfigDefaultValues extends TypedSoyFunction
      implements SoyJavaFunction {
    @Inject
    GetTestConfigDefaultValues() {}

    private static final String DEFAULT_ZONE_PROP_NAME = "zone";
    private static final String DEFAULT_ZONE = "us-central1-f";

    private static Map<String, String> getZoneDefaultValue(ZoneSpec zoneSpec) {
      ImmutableMap.Builder<String, String> result = ImmutableMap.builder();
      if (zoneSpec.getDefaultZone().isEmpty()) {
        result.put(DEFAULT_ZONE_PROP_NAME, DEFAULT_ZONE);
      }
      return result.buildOrThrow();
    }

    @Override
    public SoyValue computeForJava(List<SoyValue> args) {
      Message message = ((SoyProtoValue) args.get(0)).getProto();
      if (message instanceof SingleVmDeploymentPackageSpec) {
        return SoyValueConverter.INSTANCE
            .convert(
                markAsSoyMap(
                    getZoneDefaultValue(((SingleVmDeploymentPackageSpec) message).getZone())))
            .resolve();
      }

      return SoyValueConverter.INSTANCE
          .convert(
              markAsSoyMap(getZoneDefaultValue(((MultiVmDeploymentPackageSpec) message).getZone())))
          .resolve();
    }
  }

  /** Finds the display group for a grouped boolean checkbox field. */
  @VisibleForTesting
  @Singleton
  @SoyFunctionSignature(
      name = "findDisplayGroup",
      value = {
        @Signature(
            parameterTypes = {
              "cloud.deploymentmanager.autogen.DeployInputField",
              "cloud.deploymentmanager.autogen.DeployInputSection"
            },
            returnType =
                "cloud.deploymentmanager.autogen.DeployInputField.GroupedBooleanCheckbox.DisplayGroup")
      })
  static final class FindDisplayGroup extends TypedSoyFunction implements SoyJavaFunction {
    @Inject
    FindDisplayGroup() {}

    @Override
    public SoyValue computeForJava(List<SoyValue> args) {
      DeployInputField targetField = (DeployInputField) ((SoyProtoValue) args.get(0)).getProto();
      Preconditions.checkArgument(targetField.hasGroupedBooleanCheckbox());
      DeployInputSection section = (DeployInputSection) ((SoyProtoValue) args.get(1)).getProto();
      boolean foundField = false;
      for (DeployInputField field : Lists.reverse(section.getFieldsList())) {
        if (!foundField) {
          if (targetField.getName().equals(field.getName())) {
            foundField = true;
          }
        }
        if (foundField) {
          if (!field.hasGroupedBooleanCheckbox()) {
            throw new RuntimeException(
                "No preceding grouped boolean checkbox field with a display group");
          }
          if (field.getGroupedBooleanCheckbox().hasDisplayGroup()) {
            return SoyValueConverter.INSTANCE
                .convert(field.getGroupedBooleanCheckbox().getDisplayGroup())
                .resolve();
          }
        }
      }
      throw new RuntimeException(
          "Unable to locate display group for field " + targetField.getName());
    }
  }

  /** Finds the corresponding {@code VmTierSpec} given its name. */
  @VisibleForTesting
  @Singleton
  @SoyFunctionSignature(
      name = "findVmTier",
      value = {
        @Signature(
            parameterTypes = {
              "string",
              "list<cloud.deploymentmanager.autogen.VmTierSpec>|null|undefined"
            },
            returnType = "cloud.deploymentmanager.autogen.VmTierSpec")
      })
  static final class FindVmTier extends TypedSoyFunction implements SoyJavaFunction {
    @Inject
    FindVmTier() {}

    @Override
    public SoyValue computeForJava(List<SoyValue> args) {
      String tierName = args.get(0).stringValue();
      List<VmTierSpec> tierList;
      try {
        tierList = extractTierList(args.get(1));
      } catch (IllegalArgumentException e) {
        throw new IllegalArgumentException("Unexpected 2nd arg type for findVmTier function", e);
      }
      for (VmTierSpec tier : tierList) {
        if (tierName.equals(tier.getName())) {
          return SoyValueConverter.INSTANCE.convert(tier).resolve();
        }
      }
      throw new RuntimeException("Unable to locate tier with name " + tierName);
    }
  }

  /** Extracts the map of value labels for a deploy input field if any. */
  @Singleton
  @SoyFunctionSignature(
      name = "fieldValueLabelMap",
      value = {
        @Signature(
            parameterTypes = {"cloud.deploymentmanager.autogen.DeployInputField"},
            returnType = "map<string, string>")
      })
  private static final class FieldValueLabelMap extends TypedSoyFunction
      implements SoyJavaFunction {
    @Inject
    FieldValueLabelMap() {}

    @Override
    public SoyValue computeForJava(List<SoyValue> args) {
      DeployInputField field = (DeployInputField) ((SoyProtoValue) args.get(0)).getProto();
      if (field.hasIntegerDropdown()) {
        if (!field.getIntegerDropdown().getValueLabelsMap().isEmpty()) {
          return SoyValueConverter.INSTANCE
              .convert(toStringMap(field.getIntegerDropdown().getValueLabelsMap()))
              .resolve();
        }
      } else if (field.hasStringDropdown()) {
        if (!field.getStringDropdown().getValueLabelsMap().isEmpty()) {
          return SoyValueConverter.INSTANCE
              .convert(field.getStringDropdown().getValueLabelsMap())
              .resolve();
        }
      }
      return NullData.INSTANCE;
    }

    private <K, V> ImmutableMap<String, String> toStringMap(Map<K, V> from) {
      ImmutableMap.Builder<String, String> result = ImmutableMap.builder();
      for (Map.Entry<K, V> entry : from.entrySet()) {
        result.put(entry.getKey().toString(), entry.getValue().toString());
      }
      return result.buildOrThrow();
    }
  }

  /** Prefixes a string with a tier name. */
  @VisibleForTesting
  @Singleton
  @SoyFunctionSignature(
      name = "tierPrefixed",
      value = {
        @Signature(
            parameterTypes = {
              "string",
              "cloud.deploymentmanager.autogen.VmTierSpec|null|undefined"
            },
            returnType = "string"),
        @Signature(
            parameterTypes = {
              "string",
              "cloud.deploymentmanager.autogen.VmTierSpec|null|undefined",
              "string"
            },
            returnType = "string")
      })
  static final class TierPrefixed extends TypedSoyFunction implements SoyJavaFunction {
    @Inject
    TierPrefixed(SoyDirectives.TierPrefixed directive) {
      this.directive = directive;
    }

    SoyDirectives.TierPrefixed directive;

    @Override
    public SoyValue computeForJava(List<SoyValue> args) {
      return directive.applyForJava(args.get(0), args.subList(1, args.size()));
    }
  }

  @Singleton
  @SoyFunctionSignature(
      name = "tierTemplateName",
      value = {
        @Signature(
            parameterTypes = {"cloud.deploymentmanager.autogen.VmTierSpec"},
            returnType = "string")
      })
  static final class TierTemplateName extends TypedSoyFunction implements SoyJavaFunction {
    @Inject
    TierTemplateName() {}

    @Override
    public SoyValue computeForJava(List<SoyValue> args) {
      VmTierSpec spec = (VmTierSpec) ((SoyProtoValue) args.get(0)).getProto();
      return StringData.forValue(apply(spec));
    }

    public static String apply(VmTierSpec spec) {
      return String.format("%s_tier.jinja", spec.getName());
    }
  }

  @Singleton
  @SoyFunctionSignature(
      name = "booleanExpressionDisplayCondition",
      value = {
        @Signature(
            parameterTypes = {"cloud.deploymentmanager.autogen.BooleanExpression"},
            returnType = "string"),
        @Signature(
            parameterTypes = {
              "cloud.deploymentmanager.autogen.BooleanExpression",
              "list<cloud.deploymentmanager.autogen.VmTierSpec>|null|undefined"
            },
            returnType = "string")
      })
  static final class BooleanExpressionDisplayCondition extends TypedSoyFunction
      implements SoyJavaFunction {
    private final SoyFunctions.TierPrefixed tierPrefixedFunction;
    private final SoyFunctions.FindVmTier findVmTierFunction;
    private static final String AND = "and";

    @Inject
    BooleanExpressionDisplayCondition(SoyFunctions.TierPrefixed tierPrefixedFunction,
        SoyFunctions.FindVmTier findVmTierFunction) {
      this.tierPrefixedFunction = tierPrefixedFunction;
      this.findVmTierFunction = findVmTierFunction;
    }

    @Override
    public SoyValue computeForJava(List<SoyValue> args) {
      BooleanExpression spec = (BooleanExpression) ((SoyProtoValue) args.get(0)).getProto();
      SoyValue tiersList = NullData.INSTANCE;
      if (args.size() > 1) {
        tiersList = args.get(1);
      }
      String expression = apply(spec, tiersList, tierPrefixedFunction, findVmTierFunction);
      return StringData.forValue(expression);
    }

    public static String apply(BooleanExpression spec, SoyValue tiersList,
      SoyFunctions.TierPrefixed tierPrefixedFunction,
      SoyFunctions.FindVmTier findVmTierFunction) {

      List<String> result = new ArrayList<>();

      if (spec.hasBooleanDeployInputField()) {
        BooleanDeployInputField field = spec.getBooleanDeployInputField();
        String fieldName = DeployInputFieldName.formatFieldName(field.getName());
        String propertyExpression = String.format("properties().%s", fieldName);
        if (field.getNegated()) {
          propertyExpression = "!" + propertyExpression;
        }
        result.add(propertyExpression);
      }

      if (spec.hasHasExternalIp()) {
        ExternalIpAvailability externalIp = spec.getHasExternalIp();
        String noneType = Type.NONE.name();
        SoyValue tierName = StringData.forValue(externalIp.getTier());
        SoyValue tierSpec = NullData.INSTANCE;
        if (!externalIp.getTier().isEmpty()) {
          tierSpec = findVmTierFunction.computeForJava(ImmutableList.of(tierName, tiersList));
        }
        String externalIpProperty = tierPrefixedFunction
            .computeForJava(ImmutableList.of(StringData.forValue("externalIP"), tierSpec))
            .stringValue();
        if (externalIp.getNegated()) {
          result.add(String.format("properties().%s == \"%s\"", externalIpProperty, noneType));
        } else {
          result.add(String.format("properties().%s != \"%s\"", externalIpProperty, noneType));
        }
      }

      if (result.isEmpty()) {
        throw new IllegalArgumentException("No property or hasExternalIP was set.");
      }
      return String.join(String.format(" %s ", AND), result);
    }
  }

  @Singleton
  @SoyFunctionSignature(
      name = "booleanExpressionJinjaExpression",
      value = {
        @Signature(
            parameterTypes = {"cloud.deploymentmanager.autogen.BooleanExpression"},
            returnType = "string")
      })
  static final class BooleanExpressionJinjaExpression extends TypedSoyFunction
      implements SoyJavaFunction {
    @Inject
    BooleanExpressionJinjaExpression() {}

    @Override
    public SoyValue computeForJava(List<SoyValue> args) {
      BooleanExpression spec = (BooleanExpression) ((SoyProtoValue) args.get(0)).getProto();
      return StringData.forValue(apply(spec));
    }

    public static String apply(BooleanExpression spec) {
      if (!spec.hasBooleanDeployInputField()) {
        return "";
      }

      BooleanDeployInputField field = spec.getBooleanDeployInputField();
      String fieldName = DeployInputFieldName.formatFieldName(field.getName());
      String propertyExpression = String.format("properties[\"%s\"]", fieldName);
      if (field.getNegated()) {
        propertyExpression = "!" + propertyExpression;
      }
      return propertyExpression;
    }
  }

  /**
   * Examines a (MultiVm|SingleVm)DeploymentPackageSpec and tells whether it contains an {@code
   * AcceleratorSpec}
   */
  @Singleton
  @SoyFunctionSignature(
      name = "solutionHasGpus",
      value = {
        @Signature(
            parameterTypes = {
              "cloud.deploymentmanager.autogen.MultiVmDeploymentPackageSpec|cloud.deploymentmanager.autogen.SingleVmDeploymentPackageSpec"
            },
            returnType = "bool")
      })
  static final class SolutionHasGpus extends TypedSoyFunction implements SoyJavaFunction {
    @Inject
    SolutionHasGpus() {}

    /**
     * Argument can be either a {@code SingleVmDeploymentPackageSpec} or a {@code
     * MultiVmDeploymentPackageSpec} instance
     */
    @Override
    public SoyValue computeForJava(List<SoyValue> args) {
      Message value = ((SoyProtoValue) args.get(0)).getProto();
      if (value instanceof SingleVmDeploymentPackageSpec) {
        return SoyValueConverter.INSTANCE
            .convert(!((SingleVmDeploymentPackageSpec) value).getAcceleratorsList().isEmpty())
            .resolve();
      }

      List<VmTierSpec> tierList;
      try {
        tierList = extractTierList(args.get(0));
      } catch (IllegalArgumentException e) {
        throw new IllegalArgumentException("Unexpected 2nd arg type for dependentTiers", e);
      }

      for (VmTierSpec tier : tierList) {
        if (!tier.getAcceleratorsList().isEmpty()) {
          return SoyValueConverter.INSTANCE.convert(true).resolve();
        }
      }

      return SoyValueConverter.INSTANCE.convert(false).resolve();
    }
  }

  /**
   * Extracts as a flattened list of fields from {@code DeployInputSpec}.
   * This function can take an additional argument of type {@code VmTierSpec}, in which case it
   * will return only fields that are used by the specified tier.
   */
  @Singleton
  @SoyFunctionSignature(
      name = "listDeployInputFields",
      value = {
        @Signature(
            parameterTypes = {"cloud.deploymentmanager.autogen.DeployInputSpec"},
            returnType = "list<cloud.deploymentmanager.autogen.DeployInputField>"),
        @Signature(
            parameterTypes = {
              "cloud.deploymentmanager.autogen.DeployInputSpec",
              "cloud.deploymentmanager.autogen.VmTierSpec"
            },
            returnType = "list<cloud.deploymentmanager.autogen.DeployInputField>")
      })
  static final class ListDeployInputFields extends TypedSoyFunction implements SoyJavaFunction {
    @Inject
    ListDeployInputFields() {}

    @Override
    public SoyValue computeForJava(List<SoyValue> args) {
      DeployInputSpec inputSpec = (DeployInputSpec) ((SoyProtoValue) args.get(0)).getProto();
      if (args.size() == 1) {
        return SoyValueConverter.INSTANCE.convert(apply(inputSpec)).resolve();
      } else {
        VmTierSpec tierSpec = (VmTierSpec) ((SoyProtoValue) args.get(1)).getProto();
        return SoyValueConverter.INSTANCE.convert(apply(inputSpec, tierSpec)).resolve();
      }
    }

    @VisibleForTesting
    static List<DeployInputField> apply(DeployInputSpec inputSpec) {
      List<DeployInputField> fields = new ArrayList<>();
      for (DeployInputSection section : inputSpec.getSectionsList()) {
        fields.addAll(section.getFieldsList());
      }
      return fields;
    }

    @VisibleForTesting
    static List<DeployInputField> apply(DeployInputSpec inputSpec, VmTierSpec tierSpec) {
      Set<String> referencedFields = buildReferencedFieldsSet(tierSpec);
      List<DeployInputField> fields = new ArrayList<>();
      for (DeployInputSection section : inputSpec.getSectionsList()) {
        for (DeployInputField field : section.getFieldsList()) {
          if (referencedFields.contains(field.getName())) {
            fields.add(field);
          }
        }
      }
      return fields;
    }

    private static Set<String> buildReferencedFieldsSet(VmTierSpec tierSpec) {
      List<GceMetadataItem> metadataItems = tierSpec.getGceMetadataItemsList();
      Set<String> fields = new HashSet<>();
      for (GceMetadataItem metadataItem : metadataItems) {
        if (metadataItem.getValueSpecCase() == VALUE_FROM_DEPLOY_INPUT_FIELD) {
          fields.add(metadataItem.getValueFromDeployInputField());
        }
      }
      if (tierSpec.hasLocalSsds()
          && tierSpec.getLocalSsds().getCountSpecCase() == COUNT_FROM_DEPLOY_INPUT_FIELD) {
        fields.add(tierSpec.getLocalSsds().getCountFromDeployInputField());
      }
      for (DiskSpec disk : tierSpec.getAdditionalDisksList()) {
        if (disk.getDeviceNameSuffix().getDeviceNameCase()
            == DeviceNameCase.NAME_FROM_DEPLOY_INPUT_FIELD) {
          fields.add(disk.getDeviceNameSuffix().getNameFromDeployInputField());
        }
      }
      return fields;
    }
  }

  private static List<VmTierSpec> extractTierList(SoyValue tiersArg) {
    if (tiersArg instanceof SoyList) {
      List<? extends SoyValue> list = ((SoyList) tiersArg).asResolvedJavaList();
      return list.stream()
          .map(soyValue -> (VmTierSpec) ((SoyProtoValue) soyValue).getProto())
          .collect(Collectors.toList());
    } else if (tiersArg instanceof SoyProtoValue) {
      return ((MultiVmDeploymentPackageSpec) ((SoyProtoValue) tiersArg).getProto()).getTiersList();
    } else {
      throw new IllegalArgumentException("Unable to extract tier list from argument");
    }
  }

  abstract static class AbstractDiskPropertyName extends TypedSoyFunction
      implements SoyJavaFunction {
    private final SoyFunctions.TierPrefixed tierPrefixedFunction;

    AbstractDiskPropertyName(SoyFunctions.TierPrefixed tierPrefixedFunction) {
      this.tierPrefixedFunction = tierPrefixedFunction;
    }

    @Override
    public SoyValue computeForJava(List<SoyValue> args) {
      // Index is starting with 0 for the first element, we want to start with 1
      int diskPosition = args.get(0).integerValue() + 1;
      SoyValue baseName = StringData.forValue(getPropertyBaseName(diskPosition));
      SoyValue tierSpec = NullData.INSTANCE;
      if (args.size() > 1) {
        tierSpec = args.get(1);
      }
      return tierPrefixedFunction.computeForJava(Arrays.asList(baseName, tierSpec));
    }

    protected abstract String getPropertyBaseName(int diskPosition);
  }

  @Singleton
  @SoyFunctionSignature(
      name = "diskTypePropertyName",
      value = {
        @Signature(
            parameterTypes = {"int"},
            returnType = "string"),
        @Signature(
            parameterTypes = {"int", "cloud.deploymentmanager.autogen.VmTierSpec|null|undefined"},
            returnType = "string")
      })
  static final class AdditionalDiskTypePropertyName extends AbstractDiskPropertyName {
    @Inject
    AdditionalDiskTypePropertyName(TierPrefixed tierPrefixedFunction) {
      super(tierPrefixedFunction);
    }

    @Override
    protected String getPropertyBaseName(int diskPosition) {
      return String.format("%s_type", additionalDiskPropertyName(diskPosition));
    }
  }

  @Singleton
  @SoyFunctionSignature(
      name = "diskSizePropertyName",
      value = {
        @Signature(
            parameterTypes = {"int"},
            returnType = "string"),
        @Signature(
            parameterTypes = {"int", "cloud.deploymentmanager.autogen.VmTierSpec|null|undefined"},
            returnType = "string")
      })
  static final class AdditionalDiskSizePropertyName extends AbstractDiskPropertyName {
    @Inject
    AdditionalDiskSizePropertyName(TierPrefixed tierPrefixedFunction) {
      super(tierPrefixedFunction);
    }

    @Override
    protected String getPropertyBaseName(int diskPosition) {
      return String.format("%s_sizeGb", additionalDiskPropertyName(diskPosition));
    }
  }

  @Singleton
  @SoyFunctionSignature(
      name = "externalIpTypeName",
      value = {
        @Signature(
            parameterTypes = {"cloud.deploymentmanager.autogen.ExternalIpSpec.Type"},
            returnType = "string")
      })
  static final class ExternalIpTypeName extends TypedSoyFunction implements SoyJavaFunction {
    @Inject
    ExternalIpTypeName() {}

    @Override
    public SoyValue computeForJava(List<SoyValue> args) {
      ExternalIpSpec.Type type = ExternalIpSpec.Type.forNumber(args.get(0).integerValue());
      return StringData.forValue(type.name());
    }
  }

  private static String additionalDiskPropertyName(int diskPosition) {
    return String.format("disk%d", diskPosition);
  }

  private SoyFunctions() {}
}
