// Copyright 2019 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.bazel.rules.ninja.parser;

import com.google.common.base.Ascii;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Interner;
import com.google.devtools.build.lib.bazel.rules.ninja.file.GenericParsingException;
import com.google.devtools.build.lib.collect.ImmutableSortedKeyListMultimap;
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.errorprone.annotations.Immutable;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/** Ninja target (build statement) representation. */
public final class NinjaTarget {

  /**
   * A list of variables that pertain to input and output artifact paths of a Ninja target. For
   * memory savings, these variables should be expanded late, and the expansion results not retained
   * in memory. (Otherwise, these paths are persisted twice!)
   */
  private static final ImmutableSet<String> INPUTS_OUTPUTS_VARIABLES =
      ImmutableSet.of("in", "in_newline", "out");

  /** Builder for {@link NinjaTarget}. */
  public static class Builder {
    private String ruleName;
    private final ImmutableSortedKeyListMultimap.Builder<InputKind, PathFragment> inputsBuilder;
    private final ImmutableSortedKeyListMultimap.Builder<OutputKind, PathFragment> outputsBuilder;
    private final NinjaScope scope;
    private final long offset;

    private final ImmutableSortedMap.Builder<String, String> variablesBuilder;
    private final Interner<String> nameInterner;

    private Builder(NinjaScope scope, long offset, Interner<String> nameInterner) {
      this.scope = scope;
      this.offset = offset;
      inputsBuilder = ImmutableSortedKeyListMultimap.builder();
      outputsBuilder = ImmutableSortedKeyListMultimap.builder();
      variablesBuilder = ImmutableSortedMap.naturalOrder();
      this.nameInterner = nameInterner;
    }

    public Builder setRuleName(String ruleName) {
      this.ruleName = ruleName;
      return this;
    }

    public Builder addInputs(InputKind kind, Collection<PathFragment> inputs) {
      inputsBuilder.putAll(kind, inputs);
      return this;
    }

    public Builder addOutputs(OutputKind kind, Collection<PathFragment> outputs) {
      outputsBuilder.putAll(kind, outputs);
      return this;
    }

    public Builder addVariable(String key, String value) {
      variablesBuilder.put(key, value);
      return this;
    }

    public NinjaTarget build() throws GenericParsingException {
      Preconditions.checkNotNull(ruleName);
      String internedName = nameInterner.intern(ruleName);
      ImmutableSortedMap<String, String> variables = variablesBuilder.build();

      ImmutableSortedMap<NinjaRuleVariable, NinjaVariableValue> ruleVariables;
      if (internedName.equals("phony")) {
        ruleVariables = ImmutableSortedMap.of();
      } else {
        NinjaRule ninjaRule = scope.findRule(offset, ruleName);
        if (ninjaRule == null) {
          throw new GenericParsingException(
              String.format("could not resolve rule '%s'", internedName));
        } else {
          ruleVariables =
              reduceRuleVariables(scope, offset, ninjaRule.getVariables(), variables, nameInterner);
        }
      }
      return new NinjaTarget(
          nameInterner.intern(ruleName),
          inputsBuilder.build(),
          outputsBuilder.build(),
          offset,
          ruleVariables);
    }

    /**
     * We expand the rule's variables with the following assumptions: Rule variables can refer to
     * target's variables (and file variables). Interdependence between rule variables can happen
     * only for 'command' variable, for now we ignore other possible dependencies between rule
     * variables (seems the only other variable which can meaningfully depend on sibling variables
     * is description, and currently we are ignoring it).
     *
     * <p>Also, for resolving rule's variables we are using scope+offset of target, according to
     * specification (https://ninja-build.org/manual.html#_variable_expansion).
     *
     * <p>See {@link NinjaRuleVariable} for the list.
     */
    private static ImmutableSortedMap<NinjaRuleVariable, NinjaVariableValue> reduceRuleVariables(
        NinjaScope targetScope,
        long targetOffset,
        Map<NinjaRuleVariable, NinjaVariableValue> ruleVariables,
        ImmutableSortedMap<String, String> targetVariables,
        Interner<String> interner) {
      ImmutableSortedMap.Builder<String, List<Pair<Long, String>>> variablesBuilder =
          ImmutableSortedMap.naturalOrder();
      targetVariables.forEach(
          (key, value) -> variablesBuilder.put(key, ImmutableList.of(Pair.of(0L, value))));
      NinjaScope scopeWithVariables =
          targetScope.createScopeFromExpandedValues(variablesBuilder.build());

      ImmutableSortedMap.Builder<NinjaRuleVariable, NinjaVariableValue> builder =
          ImmutableSortedMap.naturalOrder();

      // Description is taken from the "build" statement (instead of the referenced rule)
      // if it's available.
      boolean targetHasDescription = false;
      String targetVariable = targetVariables.get("description");
      if (targetVariable != null) {
        builder.put(
            NinjaRuleVariable.DESCRIPTION, NinjaVariableValue.createPlainText(targetVariable));
        targetHasDescription = true;
      }

      // symlink_outputs are taken from the "build" statement (instead of the referenced rule)
      // if it's available.
      boolean targetHasSymlinkOutputs = false;
      String symlinkOutputs = targetVariables.get("symlink_outputs");
      if (symlinkOutputs != null) {
        builder.put(
            NinjaRuleVariable.SYMLINK_OUTPUTS, NinjaVariableValue.createPlainText(symlinkOutputs));
        targetHasSymlinkOutputs = true;
      }

      for (Map.Entry<NinjaRuleVariable, NinjaVariableValue> entry : ruleVariables.entrySet()) {
        NinjaRuleVariable type = entry.getKey();

        // Don't use the rule variable if the same variable is defined in the build statement.
        if (type == NinjaRuleVariable.DESCRIPTION && targetHasDescription) {
          continue;
        }
        if (type == NinjaRuleVariable.SYMLINK_OUTPUTS && targetHasSymlinkOutputs) {
          continue;
        }
        NinjaVariableValue reducedValue =
            scopeWithVariables.getReducedValue(
                targetOffset, entry.getValue(), INPUTS_OUTPUTS_VARIABLES, interner);
        builder.put(type, reducedValue);
      }
      return builder.build();
    }
  }

  /** Enum with possible kinds of inputs. */
  @Immutable
  public enum InputKind implements InputOutputKind {
    EXPLICIT,
    IMPLICIT,
    ORDER_ONLY,
    VALIDATION,
  }

  /** Enum with possible kinds of outputs. */
  @Immutable
  public enum OutputKind implements InputOutputKind {
    EXPLICIT,
    IMPLICIT
  }

  /**
   * Marker interface, so that it is possible to address {@link InputKind} and {@link OutputKind}
   * together in one map.
   */
  @Immutable
  public interface InputOutputKind {}

  private final String ruleName;
  private final ImmutableSortedKeyListMultimap<InputKind, PathFragment> inputs;
  private final ImmutableSortedKeyListMultimap<OutputKind, PathFragment> outputs;
  private final long offset;

  /**
   * A "reduced" set of ninja rule variables. All variables are expanded except for those in {@link
   * #INPUTS_OUTPUTS_VARIABLES}, as this saves memory.
   */
  private final ImmutableSortedMap<NinjaRuleVariable, NinjaVariableValue> ruleVariables;

  private NinjaTarget(
      String ruleName,
      ImmutableSortedKeyListMultimap<InputKind, PathFragment> inputs,
      ImmutableSortedKeyListMultimap<OutputKind, PathFragment> outputs,
      long offset,
      ImmutableSortedMap<NinjaRuleVariable, NinjaVariableValue> ruleVariables) {
    this.ruleName = ruleName;
    this.inputs = inputs;
    this.outputs = outputs;
    this.offset = offset;
    this.ruleVariables = ruleVariables;
  }

  public String getRuleName() {
    return ruleName;
  }

  public List<PathFragment> getOutputs() {
    return outputs.get(OutputKind.EXPLICIT);
  }

  public List<PathFragment> getImplicitOutputs() {
    return outputs.get(OutputKind.IMPLICIT);
  }

  public Collection<PathFragment> getAllOutputs() {
    return outputs.values();
  }

  public Collection<PathFragment> getAllSymlinkOutputs() {
    String symlinkOutputs = computeRuleVariables().get(NinjaRuleVariable.SYMLINK_OUTPUTS);
    if (symlinkOutputs == null) {
      return ImmutableSet.of();
    } else {
      return Arrays.stream(symlinkOutputs.split(" "))
          .map(PathFragment::create)
          .collect(Collectors.toSet());
    }
  }

  public Collection<PathFragment> getAllInputs() {
    return inputs.values();
  }

  public Collection<Map.Entry<InputKind, PathFragment>> getAllInputsAndKind() {
    return inputs.entries();
  }

  public Collection<PathFragment> getExplicitInputs() {
    return inputs.get(InputKind.EXPLICIT);
  }

  public Collection<PathFragment> getImplicitInputs() {
    return inputs.get(InputKind.IMPLICIT);
  }

  public Collection<PathFragment> getOrderOnlyInputs() {
    return inputs.get(InputKind.ORDER_ONLY);
  }

  public Collection<PathFragment> getValidationInputs() {
    return inputs.get(InputKind.VALIDATION);
  }

  public long getOffset() {
    return offset;
  }

  public static Builder builder(NinjaScope scope, long offset, Interner<String> nameInterner) {
    return new Builder(scope, offset, nameInterner);
  }

  public String prettyPrint() {
    return "build "
        + prettyPrintPaths("\n", getOutputs())
        + prettyPrintPaths("\n| ", getImplicitOutputs())
        + "\n: "
        + this.ruleName
        + prettyPrintPaths("\n", getExplicitInputs())
        + prettyPrintPaths("\n| ", getImplicitInputs())
        + prettyPrintPaths("\n|| ", getOrderOnlyInputs());
  }

  @Override
  public int hashCode() {
    return Long.hashCode(offset);
  }

  /**
   * Returns a map from rule variable to fully-expanded value, for all rule variables defined in
   * this target.
   */
  public ImmutableSortedMap<NinjaRuleVariable, String> computeRuleVariables() {
    ImmutableSortedMap<String, String> lateExpansionVariables = computeInputOutputVariables();
    ImmutableSortedMap.Builder<String, String> fullExpansionVariablesBuilder =
        ImmutableSortedMap.<String, String>naturalOrder().putAll(lateExpansionVariables);

    ImmutableSortedMap.Builder<NinjaRuleVariable, String> builder =
        ImmutableSortedMap.naturalOrder();
    for (Map.Entry<NinjaRuleVariable, NinjaVariableValue> entry : ruleVariables.entrySet()) {
      NinjaRuleVariable type = entry.getKey();
      // Skip command for now. It may need to expand other rule variables.
      if (NinjaRuleVariable.COMMAND.equals(type)) {
        continue;
      }

      String expandedValue = entry.getValue().expandValue(lateExpansionVariables);
      builder.put(type, expandedValue);
      fullExpansionVariablesBuilder.put(Ascii.toLowerCase(type.name()), expandedValue);
    }

    // TODO(cparsons): Ensure parsing exception is thrown early if the rule has no command defined.
    // Otherwise, this throws NPE.
    String expandedCommand =
        ruleVariables
            .get(NinjaRuleVariable.COMMAND)
            .expandValue(fullExpansionVariablesBuilder.build());
    builder.put(NinjaRuleVariable.COMMAND, expandedCommand);
    return builder.build();
  }

  private ImmutableSortedMap<String, String> computeInputOutputVariables() {
    ImmutableSortedMap.Builder<String, String> builder = ImmutableSortedMap.naturalOrder();
    String inNewline =
        inputs.get(InputKind.EXPLICIT).stream()
            .map(PathFragment::getPathString)
            .collect(Collectors.joining("\n"));
    String out =
        outputs.get(OutputKind.EXPLICIT).stream()
            .map(PathFragment::getPathString)
            .collect(Collectors.joining(" "));
    builder.put("in", inNewline.replace('\n', ' '));
    builder.put("in_newline", inNewline);
    builder.put("out", out);
    return builder.build();
  }

  private static String prettyPrintPaths(String startDelimiter, Collection<PathFragment> paths) {
    if (paths.isEmpty()) {
      return "";
    }
    return startDelimiter
        + paths.stream().map(PathFragment::getPathString).collect(Collectors.joining("$\n"));
  }
}
