/*
 * Copyright 2014-present Facebook, Inc.
 *
 * 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.facebook.buck.rules.coercer;

import com.facebook.buck.model.UnflavoredBuildTarget;
import com.facebook.buck.rules.coercer.BuildConfigFields.Field;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.immutables.BuckStyleImmutable;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;

import org.immutables.value.Value;

import java.util.Iterator;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * List of fields to add to a generated {@code BuildConfig.java} file. Each field knows its Java
 * type, variable name, and value.
 */
@Value.Enclosing
@Value.Immutable
@BuckStyleImmutable
abstract class AbstractBuildConfigFields implements Iterable<Field> {

  /** An individual field in a {@link BuildConfigFields}. */
  @Value.Immutable
  abstract static class AbstractField {

    @Value.Parameter
    public abstract String getType();

    @Value.Parameter
    public abstract String getName();

    @Value.Parameter
    public abstract String getValue();

    /**
     * @return a string that could be passed to
     *     {@link BuildConfigFields#fromFieldDeclarations(Iterable)} such that it could be parsed
     *     to return a {@link Field} equal to this object.
     */
    @Override
    public String toString() {
      return String.format("%s %s = %s", getType(), getName(), getValue());
    }

  }

  private static final Pattern VARIABLE_DEFINITION_PATTERN = Pattern.compile(
      "(?<type>[a-zA-Z_$][a-zA-Z0-9_.<>]+)" +
      "\\s+" +
      "(?<name>[a-zA-Z_$][a-zA-Z0-9_$]+)" +
      "\\s*=\\s*" +
      "(?<value>.+)");

  private static final ImmutableSet<String> PRIMITIVE_NUMERIC_TYPE_NAMES = ImmutableSet.of(
      "byte",
      "char",
      "double",
      "float",
      "int",
      "long",
      "short");

  private static final Function<String, Field> TRANSFORM = new Function<String, Field>() {
    @Override
    public Field apply(String input) {
      Matcher matcher = VARIABLE_DEFINITION_PATTERN.matcher(input);
      if (matcher.matches()) {
        return BuildConfigFields.Field.builder()
            .setType(matcher.group("type"))
            .setName(matcher.group("name"))
            .setValue(matcher.group("value"))
            .build();
      } else {
        throw new HumanReadableException("Not a valid BuildConfig variable declaration: %s", input);
      }
    }
  };

  private static final BuildConfigFields EMPTY = BuildConfigFields.of(
      ImmutableMap.<String, Field>of());

  @Value.Parameter
  protected abstract Map<String, Field> getNameToField();

  public static BuildConfigFields fromFieldDeclarations(Iterable<String> declarations) {
    return fromFields(FluentIterable.from(declarations).transform(TRANSFORM));
  }

  /** @return a {@link BuildConfigFields} with no fields */
  public static BuildConfigFields empty() {
    return EMPTY;
  }

  /** @return a {@link BuildConfigFields} that contains the specified fields in iteration order. */
  public static BuildConfigFields fromFields(Iterable<Field> fields) {
    ImmutableMap<String, Field> entries = FluentIterable
        .from(fields)
        .uniqueIndex(new Function<Field, String>() {
          @Override
          public String apply(Field field) {
            return field.getName();
          }
        });
    return BuildConfigFields.builder()
        .putAllNameToField(entries)
        .build();
  }

  /**
   * @return A new {@link BuildConfigFields} with all of the fields from this object, combined with
   *     all of the fields from the specified {@code fields}. If both objects have fields with the
   *     same name, the entry from the {@code fields} parameter wins.
   */
  public BuildConfigFields putAll(BuildConfigFields fields) {

    ImmutableMap.Builder<String, Field> nameToFieldBuilder = ImmutableMap.builder();
    nameToFieldBuilder.putAll(fields.getNameToField());
    for (Field field : this.getNameToField().values()) {
      if (!fields.getNameToField().containsKey(field.getName())) {
        nameToFieldBuilder.put(field.getName(), field);
      }
    }
    return BuildConfigFields.of(nameToFieldBuilder.build());
  }

  /**
   * Creates the Java code for a {@code BuildConfig.java} file in the specified {@code javaPackage}.
   * @param source The build target of the rule that is responsible for generating this
   *     BuildConfig.java file.
   * @param javaPackage The Java package for the generated file.
   * @param useConstantExpressions Whether the value of each field in the generated Java code should
   *     be the literal value from the {@link Field} (i.e., a constant expression) or a
   *     non-constant-expression that is guaranteed to evaluate to the literal value.
   */
  public String generateBuildConfigDotJava(
      UnflavoredBuildTarget source,
      String javaPackage,
      boolean useConstantExpressions) {

    StringBuilder builder = new StringBuilder();
    // By design, we drop the flavor from the BuildTarget (if present), so this debug text makes
    // more sense to users.
    builder.append(String.format("// Generated by %s. DO NOT MODIFY.\n", source));
    builder.append("package ").append(javaPackage).append(";\n");
    builder.append("public class BuildConfig {\n");
    builder.append("  private BuildConfig() {}\n");

    final String prefix = "  public static final ";
    for (Field field : getNameToField().values()) {
      String type = field.getType();
      if ("boolean".equals(type)) {
        // type is a non-numeric primitive.
        boolean isTrue = "true".equals(field.getValue());
        if (!(isTrue || "false".equals(field.getValue()))) {
          throw new HumanReadableException(
              "expected boolean literal but was: %s",
              field.getValue());
        }
        String value;
        if (useConstantExpressions) {
          value = String.valueOf(isTrue);
        } else {
          value = "Boolean.parseBoolean(null)";
          if (isTrue) {
            value = "!" + value;
          }
        }
        builder.append(prefix + "boolean " + field.getName() + " = " + value + ";\n");
      } else {
        String typeSafeZero = PRIMITIVE_NUMERIC_TYPE_NAMES.contains(type) ? "0" : "null";
        String defaultValue = field.getValue();
        if (!useConstantExpressions) {
          defaultValue = "!Boolean.parseBoolean(null) ? " + defaultValue + " : " + typeSafeZero;
        }
        builder.append(prefix + type + " " + field.getName() + " = " + defaultValue + ";\n");
      }
    }

    builder.append("}\n");
    return builder.toString();
  }

  /**
   * @return iterator that enumerates the fields used to construct this {@link BuildConfigFields}.
   *     The {@link Iterator#remove()} method of the return value is not supported.
   */
  @Override
  public Iterator<Field> iterator() {
    return getNameToField().values().iterator();
  }

  /**
   * @return value that represents the data stored in this object such that it can be used to
   *     represent this object in a {@link com.facebook.buck.rules.RuleKey}.
   */
  @Override
  public String toString() {
    return Joiner.on(';').join(getNameToField().values());
  }

}

@@ -2287,16 +2287,46 @@
 -9_.%3C%3E%5D+
+(%22 + Pattern.quote(%22%5B%5D%22) + %22)?
 )%22 +%0A   

Add support for arrays in AbstractBuildConfigFields