/*
 * Copyright The OpenTelemetry Authors
 * SPDX-License-Identifier: Apache-2.0
 */

package io.opentelemetry.javaagent.tooling.muzzle.references;

import static java.util.Arrays.asList;
import static java.util.Collections.emptySet;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
import org.objectweb.asm.Type;

/**
 * The builder of {@link ClassRef}.
 *
 * <p>This class is used in the auto-generated {@code InstrumentationModule#getMuzzleReferences()}
 * method, it is not meant to be used directly by agent extension developers.
 */
public final class ClassRefBuilder {

  // this could be exposed as a system property if needed, but for now it's just helpful to be able
  // to change manually here when reviewing/optimizing the generated getMuzzleReferences() method
  static final boolean COLLECT_SOURCES = true;

  private final Set<Source> sources = new LinkedHashSet<>();
  private final Set<Flag> flags = new LinkedHashSet<>();
  private final String className;
  private final Set<String> interfaceNames = new LinkedHashSet<>();
  private final List<FieldRef> fields = new ArrayList<>();
  private final List<MethodRef> methods = new ArrayList<>();

  @Nullable private String superClassName = null;

  ClassRefBuilder(String className) {
    this.className = className;
  }

  @CanIgnoreReturnValue
  public ClassRefBuilder setSuperClassName(String superName) {
    this.superClassName = superName;
    return this;
  }

  @CanIgnoreReturnValue
  public ClassRefBuilder addInterfaceNames(Collection<String> interfaceNames) {
    this.interfaceNames.addAll(interfaceNames);
    return this;
  }

  @CanIgnoreReturnValue
  public ClassRefBuilder addInterfaceName(String interfaceName) {
    interfaceNames.add(interfaceName);
    return this;
  }

  @CanIgnoreReturnValue
  public ClassRefBuilder addSource(String sourceName) {
    return addSource(sourceName, 0);
  }

  @CanIgnoreReturnValue
  public ClassRefBuilder addSource(String sourceName, int line) {
    if (COLLECT_SOURCES) {
      sources.add(new Source(sourceName, line));
    }
    return this;
  }

  @CanIgnoreReturnValue
  public ClassRefBuilder addFlag(Flag flag) {
    flags.add(flag);
    return this;
  }

  @CanIgnoreReturnValue
  public ClassRefBuilder addField(
      Source[] fieldSources,
      Flag[] fieldFlags,
      String fieldName,
      Type fieldType,
      boolean isFieldDeclared) {
    FieldRef field =
        new FieldRef(
            COLLECT_SOURCES ? new LinkedHashSet<>(asList(fieldSources)) : emptySet(),
            new LinkedHashSet<>(asList(fieldFlags)),
            fieldName,
            fieldType.getDescriptor(),
            isFieldDeclared);

    int existingIndex = fields.indexOf(field);
    if (existingIndex == -1) {
      fields.add(field);
    } else {
      fields.set(existingIndex, field.merge(fields.get(existingIndex)));
    }
    return this;
  }

  @CanIgnoreReturnValue
  public ClassRefBuilder addMethod(
      Source[] methodSources,
      Flag[] methodFlags,
      String methodName,
      Type methodReturnType,
      Type... methodArgumentTypes) {
    MethodRef method =
        new MethodRef(
            COLLECT_SOURCES ? new LinkedHashSet<>(asList(methodSources)) : emptySet(),
            new LinkedHashSet<>(asList(methodFlags)),
            methodName,
            Type.getMethodDescriptor(methodReturnType, methodArgumentTypes));

    int existingIndex = methods.indexOf(method);
    if (existingIndex == -1) {
      methods.add(method);
    } else {
      methods.set(existingIndex, method.merge(methods.get(existingIndex)));
    }
    return this;
  }

  public ClassRef build() {
    return new ClassRef(
        sources,
        flags,
        className,
        superClassName,
        interfaceNames,
        new LinkedHashSet<>(fields),
        new LinkedHashSet<>(methods));
  }
}
