// Copyright 2016 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.android;

import com.android.builder.core.VariantType;
import com.android.repository.Revision;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.io.CharStreams;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.Nullable;

/**
 * Builder for AAPT command lines, with support for making flags conditional on build tools version
 * and variant type.
 */
public class AaptCommandBuilder {

  private final ImmutableList.Builder<String> flags = new ImmutableList.Builder<>();
  private Revision buildToolsVersion;
  private VariantType variantType;

  public AaptCommandBuilder(Path aapt) {
    flags.add(aapt.toString());
  }

  /** Sets the build tools version to be used for {@link #whenVersionIsAtLeast}. */
  public AaptCommandBuilder forBuildToolsVersion(@Nullable Revision buildToolsVersion) {
    Preconditions.checkState(
        this.buildToolsVersion == null, "A build tools version was already specified.");
    this.buildToolsVersion = buildToolsVersion;
    return this;
  }

  /** Sets the variant type to be used for {@link #whenVariantIs}. */
  public AaptCommandBuilder forVariantType(VariantType variantType) {
    Preconditions.checkNotNull(variantType);
    Preconditions.checkState(this.variantType == null, "A variant type was already specified.");
    this.variantType = variantType;
    return this;
  }

  /** Adds a single flag to the builder. */
  public AaptCommandBuilder add(String flag) {
    flags.add(flag);
    return this;
  }

  /**
   * Adds a flag to the builder, along with a string value. The two will be added as different words
   * in the final command line. If the value is {@code null}, neither the flag nor the value will be
   * added.
   */
  public AaptCommandBuilder add(String flag, @Nullable String value) {
    Preconditions.checkNotNull(flag);
    if (!Strings.isNullOrEmpty(value)) {
      flags.add(flag);
      flags.add(value);
    }
    return this;
  }

  /**
   * Adds a flag to the builder, along with a path value. The path will be converted to a string
   * using {@code toString}, then the flag and the path will be added to the final command line as
   * different words. If the value is {@code null}, neither the flag nor the path will be added.
   *
   * @see #add(String,String)
   */
  public AaptCommandBuilder add(String flag, @Nullable Path path) {
    Preconditions.checkNotNull(flag);
    if (path != null) {
      add(flag, path.toString());
    }
    return this;
  }

  /**
   * Adds a flag to the builder multiple times, once for each value in the given collection. {@code
   * null} values will be skipped. If the collection is empty, nothing will be added. The values
   * will be added in the source collection's iteration order.
   *
   * <p>ex. If {@code flag} is {@code "-0"} and {@code values} contains the values {@code "png"},
   * {@code null}, and {@code "gif"}, then four words will be added to the final command line:
   * {@code "-0", "png", "-0", "gif"}.
   */
  public AaptCommandBuilder addRepeated(String flag, Collection<String> values) {
    Preconditions.checkNotNull(flag);
    for (String value : values) {
      add(flag, value);
    }
    return this;
  }

  /**
   * Adds a flag to the builder multiple times, once for each value in the given collection. {@code
   * null} values will be skipped. If the collection is empty, nothing will be added. The values
   * will be added in the source collection's iteration order. See {@link
   * AaptCommandBuilder#addRepeated(String, Collection)} for more information. If the collection
   * exceed 200 items, the values will be written to a file and passed as &lt;flag&gt @&lt;file&gt;.
   */
  public AaptCommandBuilder addParameterableRepeated(
      final String flag, Collection<String> values, Path workingDirectory) throws IOException {
    Preconditions.checkNotNull(flag);
    Preconditions.checkNotNull(workingDirectory);
    if (values.size() > 200) {
      add(
          flag,
          "@" + Files.write(
              Files.createDirectories(workingDirectory).resolve("params" + flag),
              ImmutableList.of(values.stream().collect(Collectors.joining(" ")))));
    } else {
      addRepeated(flag, values);
    }
    return this;
  }

  /** Adds the next flag to the builder only if the condition is true. */
  public ConditionalAaptCommandBuilder when(boolean condition) {
    if (condition) {
      return new SuccessfulConditionCommandBuilder(this);
    } else {
      return new FailedConditionCommandBuilder(this);
    }
  }

  /** Adds the next flag to the builder only if the variant type is the passed-in type. */
  public ConditionalAaptCommandBuilder whenVariantIs(VariantType variantType) {
    Preconditions.checkNotNull(variantType);
    return when(this.variantType == variantType);
  }

  /**
   * Adds the next flag to the builder only if the build tools version is unspecified or is greater
   * than or equal to the given version.
   */
  public ConditionalAaptCommandBuilder whenVersionIsAtLeast(Revision requiredVersion) {
    Preconditions.checkNotNull(requiredVersion);
    return when(buildToolsVersion == null || buildToolsVersion.compareTo(requiredVersion) >= 0);
  }

  /** Assembles the full command line as a list. */
  public List<String> build() {
    return flags.build();
  }

  /** Wrapper for potentially adding flags to an AaptCommandBuilder based on a conditional. */
  public interface ConditionalAaptCommandBuilder {
    /**
     * Adds a single flag to the builder if the condition was true.
     *
     * @see AaptCommandBuilder#add(String)
     */
    AaptCommandBuilder thenAdd(String flag);

    /**
     * Adds a single flag and associated string value to the builder if the value is non-null and
     * the condition was true.
     *
     * @see AaptCommandBuilder#add(String,String)
     */
    AaptCommandBuilder thenAdd(String flag, @Nullable String value);

    /**
     * Adds a single flag and associated path value to the builder if the value is non-null and the
     * condition was true.
     *
     * @see AaptCommandBuilder#add(String,Path)
     */
    AaptCommandBuilder thenAdd(String flag, @Nullable Path value);

    /**
     * Adds the values in the collection to the builder, each preceded by the given flag, if the
     * collection was non-empty and the condition was true.
     *
     * @see AaptCommandBuilder#addRepeated(String,Collection<String>)
     */
    AaptCommandBuilder thenAddRepeated(String flag, Collection<String> values);
  }

  /**
   * Forwarding implementation of ConditionalAaptCommandBuilder returned when a condition is true.
   */
  private static class SuccessfulConditionCommandBuilder implements ConditionalAaptCommandBuilder {
    private final AaptCommandBuilder originalCommandBuilder;

    public SuccessfulConditionCommandBuilder(AaptCommandBuilder originalCommandBuilder) {
      this.originalCommandBuilder = originalCommandBuilder;
    }

    @Override
    public AaptCommandBuilder thenAdd(String flag) {
      return originalCommandBuilder.add(flag);
    }

    @Override
    public AaptCommandBuilder thenAdd(String flag, @Nullable String value) {
      return originalCommandBuilder.add(flag, value);
    }

    @Override
    public AaptCommandBuilder thenAdd(String flag, @Nullable Path value) {
      return originalCommandBuilder.add(flag, value);
    }

    @Override
    public AaptCommandBuilder thenAddRepeated(String flag, Collection<String> values) {
      return originalCommandBuilder.addRepeated(flag, values);
    }
  }

  /** Null implementation of ConditionalAaptCommandBuilder returned when a condition is false. */
  private static class FailedConditionCommandBuilder implements ConditionalAaptCommandBuilder {
    private final AaptCommandBuilder originalCommandBuilder;

    public FailedConditionCommandBuilder(AaptCommandBuilder originalCommandBuilder) {
      this.originalCommandBuilder = originalCommandBuilder;
    }

    @Override
    public AaptCommandBuilder thenAdd(String flag) {
      Preconditions.checkNotNull(flag);
      return originalCommandBuilder;
    }

    @Override
    public AaptCommandBuilder thenAdd(String flag, @Nullable String value) {
      Preconditions.checkNotNull(flag);
      return originalCommandBuilder;
    }

    @Override
    public AaptCommandBuilder thenAdd(String flag, @Nullable Path value) {
      Preconditions.checkNotNull(flag);
      return originalCommandBuilder;
    }

    @Override
    public AaptCommandBuilder thenAddRepeated(String flag, Collection<String> values) {
      Preconditions.checkNotNull(flag);
      Preconditions.checkNotNull(values);
      return originalCommandBuilder;
    }
  }

  /**
   * Executes command and returns log.
   *
   * @throws IOException when the process cannot execute.
   */
  public String execute(String action) throws IOException {
    final StringBuilder processLog = new StringBuilder();
    List<String> command = build();

    final Process process = new ProcessBuilder().command(command).redirectErrorStream(true).start();
    processLog.append("Command: ");
    Joiner.on("\n\t").appendTo(processLog, command);
    processLog.append("\n");
    final InputStreamReader stdout =
        new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8);
    while (process.isAlive()) {
      processLog.append(CharStreams.toString(stdout));
    }
    if (process.exitValue() != 0) {
      throw new RuntimeException(String.format("Error during %s:", action) + "\n" + processLog);
    }
    return processLog.toString();
  }
}
