// Copyright 2022 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.rules.cpp;

import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.starlark.StarlarkRuleContext;
import com.google.devtools.build.lib.collect.nestedset.Depset;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException;
import java.util.HashMap;
import java.util.Objects;
import javax.annotation.Nullable;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Mutability;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkCallable;
import net.starlark.java.eval.StarlarkSemantics;
import net.starlark.java.eval.StarlarkThread;
import net.starlark.java.eval.StarlarkValue;
import net.starlark.java.eval.Structure;
import net.starlark.java.eval.Tuple;

/**
 * An implementation of ExtraLinkTimeLibrary that uses functions and data passed in from Starlark.
 */
public class StarlarkDefinedLinkTimeLibrary implements ExtraLinkTimeLibrary, Structure {

  StarlarkDefinedLinkTimeLibrary(
      StarlarkCallable buildLibraryFunction, ImmutableMap<String, Object> objectMap) {
    this.buildLibraryFunction = buildLibraryFunction;
    this.objectMap = objectMap;
    this.key = Key.createKey(buildLibraryFunction, objectMap);
  }

  // Starlark function to create the output library.
  private final StarlarkCallable buildLibraryFunction;

  // Map of parameter names to values. Depsets should be combined when merging libraries.
  private final ImmutableMap<String, Object> objectMap;

  // Key object used to determine the "class" of the library implementation.
  // The equals method is used to determine equality.
  private final Key key;

  @Override
  public Key getKey() {
    return key;
  }

  @Override
  public ExtraLinkTimeLibrary.Builder getBuilder() {
    return new Builder();
  }

  @Override
  public BuildLibraryOutput buildLibraries(
      RuleContext ruleContext, boolean staticMode, boolean forDynamicLibrary)
      throws RuleErrorException, InterruptedException {
    ruleContext.initStarlarkRuleContext();
    StarlarkRuleContext starlarkContext = ruleContext.getStarlarkRuleContext();
    StarlarkSemantics semantics = starlarkContext.getStarlarkSemantics();

    Object response = null;
    try (Mutability mu = Mutability.create("extra_link_time_library_build_libraries_function")) {
      StarlarkThread thread = new StarlarkThread(mu, semantics);
      response =
          Starlark.call(
              thread,
              buildLibraryFunction,
              ImmutableList.of(starlarkContext, staticMode, forDynamicLibrary),
              objectMap);
    } catch (EvalException e) {
      throw new RuleErrorException(e);
    }
    String errorMsg =
        buildLibraryFunction.getName()
            + " in "
            + buildLibraryFunction.getLocation()
            + " should return (depset[CcLinkingContext], depset[File])";
    if (!(response instanceof Tuple)) {
      throw new RuleErrorException(errorMsg);
    }
    Tuple responseTuple = (Tuple) response;
    if (responseTuple.size() != 2) {
      throw new RuleErrorException(errorMsg);
    }
    if (!(responseTuple.get(0) instanceof Depset) || !(responseTuple.get(1) instanceof Depset)) {
      throw new RuleErrorException(errorMsg);
    }
    try {
      return new BuildLibraryOutput(
          ((Depset) responseTuple.get(0)).getSet(CcLinkingContext.LinkerInput.class),
          ((Depset) responseTuple.get(1)).getSet(Artifact.class));
    } catch (Depset.TypeException e) {
      throw new RuleErrorException(e);
    }
  }

  @Nullable
  @Override
  public StarlarkValue getValue(String key) throws EvalException {
    return (StarlarkValue) objectMap.get(key);
  }

  @Override
  public ImmutableCollection<String> getFieldNames() {
    return objectMap.keySet();
  }

  @Override
  public void setField(String field, Object value) throws EvalException {
    throw Starlark.errorf("ExtraLinkLibrary does not support field assignment");
  }

  @Override
  public String getErrorMessageForUnknownField(String field) {
    return String.format("No argument '%s' was passed to this ExtraLinkLibrary", field);
  }

  /**
   * Class to identify the "class" of a StarlarkDefinedLinkTimeLibrary. Uses the build function and
   * the split between depset and non-depset parameters to determine equality.
   */
  private static class Key {

    private final Object builderFunction;
    private final ImmutableList<String> constantFields;
    private final ImmutableList<String> depsetFields;

    private Key(
        Object builderFunction,
        ImmutableList<String> constantFields,
        ImmutableList<String> depsetFields) {
      this.builderFunction = builderFunction;
      this.constantFields = constantFields;
      this.depsetFields = depsetFields;
    }

    public static Key createKey(Object builderFunction, ImmutableMap<String, Object> objectMap) {
      ImmutableList.Builder<String> depsetFields = ImmutableList.builder();
      ImmutableList.Builder<String> constantFields = ImmutableList.builder();
      for (String key : objectMap.keySet()) {
        if (objectMap.get(key) instanceof Depset) {
          depsetFields.add(key);
        } else {
          constantFields.add(key);
        }
      }
      return new Key(builderFunction, constantFields.build(), depsetFields.build());
    }

    @Override
    public boolean equals(Object other) {
      if (!(other instanceof Key)) {
        return false;
      }
      Key key = (Key) other;
      return builderFunction.equals(key.builderFunction)
          && constantFields.equals(key.constantFields)
          && depsetFields.equals(key.depsetFields);
    }

    @Override
    public int hashCode() {
      return Objects.hash(builderFunction, constantFields, depsetFields);
    }
  }

  private static class Builder implements ExtraLinkTimeLibrary.Builder {

    private StarlarkCallable buildLibraryFunction;

    private final HashMap<String, ImmutableList.Builder<Depset>> depsetMapBuilder = new HashMap<>();

    private final HashMap<String, Object> constantsMap = new HashMap<>();

    private Builder() {}

    @Override
    public ExtraLinkTimeLibrary build() {
      ImmutableMap.Builder<String, Object> builder = new ImmutableMap.Builder<>();
      for (String key : depsetMapBuilder.keySet()) {
        try {
          builder.put(
              key,
              Depset.fromDirectAndTransitive(
                  Order.LINK_ORDER,
                  ImmutableList.of(),
                  depsetMapBuilder.get(key).build(),
                  /* strict= */ true));
        } catch (EvalException e) {
          // should never happen; exception comes from bad order argument.
          throw new IllegalStateException(e);
        }
      }
      builder.putAll(constantsMap);
      return new StarlarkDefinedLinkTimeLibrary(buildLibraryFunction, builder.buildOrThrow());
    }

    @Override
    public void addTransitive(ExtraLinkTimeLibrary dep) {
      StarlarkDefinedLinkTimeLibrary library = (StarlarkDefinedLinkTimeLibrary) dep;
      if (buildLibraryFunction == null) {
        buildLibraryFunction = library.buildLibraryFunction;
      }
      for (String key : library.objectMap.keySet()) {
        Object value = library.objectMap.get(key);
        if (value instanceof Depset) {
          depsetMapBuilder.computeIfAbsent(key, k -> ImmutableList.builder()).add((Depset) value);
        } else {
          constantsMap.put(key, value);
        }
      }
    }
  }
}
