/*
 * Copyright 2020 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.desugar.retarget;

import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.auto.value.AutoValue;
import com.google.auto.value.extension.memoized.Memoized;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.Resources;
import com.google.devtools.build.android.desugar.langmodel.MethodInvocationSite;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.protobuf.ExtensionRegistry;
import com.google.protobuf.TextFormat;
import java.io.IOError;
import java.io.IOException;
import java.net.URL;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * The configuration that specifies the class members subject to replacement with a new target class
 * member.
 */
@AutoValue
public abstract class ClassMemberRetargetConfig {

  public static final URL DEFAULT_PROTO_URL =
      ClassMemberRetargetConfig.class.getResource("retarget_config.textproto");

  /** The configuration file that defines class member retargeting. */
  abstract ImmutableList<URL> invocationReplacementConfigUrls();

  abstract ImmutableList<MethodInvocationReplacement> inProcessReplacements();

  /**
   * The enabled configuration flags for invocation replacements. A replacement takes effect if and
   * only if the set intersection of this flag and the {@code range} field of {@code
   * MethodInvocations#replacements} is non-empty.
   */
  abstract ImmutableSet<ReplacementRange> enabledInvocationReplacementRanges();

  public static ClassMemberRetargetConfig.Builder builder() {
    return new AutoValue_ClassMemberRetargetConfig.Builder();
  }

  @Memoized
  MethodInvocations invocationReplacementConfigProto() {
    MethodInvocations.Builder invocationReplacementsBuilder = MethodInvocations.newBuilder();
    try {
      for (URL url : invocationReplacementConfigUrls()) {
        String protoText = Resources.toString(url, UTF_8);
        MethodInvocations methodInvocations =
            TextFormat.parse(
                protoText, ExtensionRegistry.getEmptyRegistry(), MethodInvocations.class);
        invocationReplacementsBuilder.mergeFrom(methodInvocations);
      }
      invocationReplacementsBuilder.addAllReplacements(inProcessReplacements());
      return invocationReplacementsBuilder.build();
    } catch (IOException e) {
      throw new IOError(e);
    }
  }

  /**
   * The parsed invocation replacement configuration from {@link #invocationReplacementConfigUrls}.
   */
  @Memoized
  public ImmutableMap<MethodInvocationSite, MethodInvocationSite> invocationReplacements() {
    ImmutableMap.Builder<MethodInvocationSite, MethodInvocationSite> replacementsBuilder =
        ImmutableMap.builder();
    for (MethodInvocationReplacement replacement :
        invocationReplacementConfigProto().getReplacementsList()) {
      MethodInvocationSite invocationSite = MethodInvocationSite.fromProto(replacement.getSource());
      Set<ReplacementRange> replacementRanges = new LinkedHashSet<>(replacement.getRangeList());
      if (replacementRanges.contains(ReplacementRange.ALL)
          || replacementRanges.stream().anyMatch(enabledInvocationReplacementRanges()::contains)) {
        replacementsBuilder.put(
            invocationSite, MethodInvocationSite.fromProto(replacement.getDestination()));
      }
    }
    return replacementsBuilder.buildOrThrow();
  }

  @Nullable
  public final MethodInvocationSite findReplacementSite(
      MethodInvocationSite verbatimInvocationSite) {
    return invocationReplacements().get(verbatimInvocationSite);
  }

  /** The builder for {@link ClassMemberRetargetConfig}. */
  @AutoValue.Builder
  public abstract static class Builder {

    public abstract ImmutableList.Builder<URL> invocationReplacementConfigUrlsBuilder();

    public abstract ImmutableList.Builder<MethodInvocationReplacement>
        inProcessReplacementsBuilder();

    public abstract ImmutableSet.Builder<ReplacementRange>
        enabledInvocationReplacementRangesBuilder();

    @CanIgnoreReturnValue
    public Builder addInvocationReplacementConfigUrl(URL value) {
      invocationReplacementConfigUrlsBuilder().add(value);
      return this;
    }

    @CanIgnoreReturnValue
    public Builder addInProcessReplacement(MethodInvocationReplacement value) {
      inProcessReplacementsBuilder().add(value);
      return this;
    }

    @CanIgnoreReturnValue
    public Builder addEnabledInvocationReplacementRange(ReplacementRange value) {
      enabledInvocationReplacementRangesBuilder().add(value);
      return this;
    }

    @CanIgnoreReturnValue
    public Builder addAllEnabledInvocationReplacementRange(Collection<ReplacementRange> value) {
      enabledInvocationReplacementRangesBuilder().addAll(value);
      return this;
    }

    public abstract ClassMemberRetargetConfig build();
  }
}
