content: /*
   * 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 io.airlift.bytecode;
  
  import com.google.common.collect.ImmutableList;
  import com.google.common.collect.ImmutableSet;
  import org.objectweb.asm.ClassVisitor;
  
  import javax.annotation.concurrent.NotThreadSafe;
  
  import java.util.ArrayList;
  import java.util.EnumSet;
  import java.util.List;
  import java.util.Set;
  import java.util.stream.Collectors;
  import java.util.stream.Stream;
  
  import static io.airlift.bytecode.Access.INTERFACE;
  import static io.airlift.bytecode.Access.STATIC;
  import static io.airlift.bytecode.Access.a;
  import static io.airlift.bytecode.Access.toAccessModifier;
  import static java.util.Objects.requireNonNull;
  import static org.objectweb.asm.Opcodes.ACC_SUPER;
  import static org.objectweb.asm.Opcodes.V17;
  
  @NotThreadSafe
  public class ClassDefinition
  {
      private final EnumSet<Access> access;
      private final ParameterizedType type;
      private final ParameterizedType superClass;
      private final List<ParameterizedType> interfaces = new ArrayList<>();
      private final List<AnnotationDefinition> annotations = new ArrayList<>();
      private final List<FieldDefinition> fields = new ArrayList<>();
      private final List<MethodDefinition> methods = new ArrayList<>();
      private final MethodDefinition classInitializer;
      private String source;
      private String debug;
  
      public ClassDefinition(
              EnumSet<Access> access,
              String name,
              ParameterizedType superClass,
              ParameterizedType... interfaces)
      {
          this(access, new ParameterizedType(name), superClass, interfaces);
      }
  
      public ClassDefinition(
              EnumSet<Access> access,
              ParameterizedType type,
              ParameterizedType superClass,
              ParameterizedType... interfaces)
      {
          requireNonNull(access, "access is null");
          requireNonNull(type, "type is null");
          requireNonNull(superClass, "superClass is null");
          requireNonNull(interfaces, "interfaces is null");
  
          this.access = access;
          this.type = type;
          this.superClass = superClass;
          this.interfaces.addAll(ImmutableList.copyOf(interfaces));
  
          classInitializer = new MethodDefinition(this, a(STATIC), "<clinit>", ParameterizedType.type(void.class), ImmutableList.of());
      }
  
      public Set<Access> getAccess()
      {
          return ImmutableSet.copyOf(access);
      }
  
      public String getName()
      {
          return type.getClassName();
      }
  
      public ParameterizedType getType()
      {
          return type;
      }
  
      public ParameterizedType getSuperClass()
      {
          return superClass;
      }
  
      public String getSource()
      {
          return source;
      }
  
      public List<ParameterizedType> getInterfaces()
      {
          return ImmutableList.copyOf(interfaces);
      }
  
      public List<AnnotationDefinition> getAnnotations()
      {
          return ImmutableList.copyOf(annotations);
      }
  
      public List<FieldDefinition> getFields()
      {
          return ImmutableList.copyOf(fields);
      }
  
      public List<MethodDefinition> getMethods()
      {
          return ImmutableList.copyOf(methods);
      }
  
      public boolean isInterface()
      {
          return access.contains(INTERFACE);
      }
  
      public void visit(ClassVisitor visitor)
      {
          // Generic signature if super class or any interface is generic
          String signature = null;
          if (superClass.isGeneric() || interfaces.stream().anyMatch(ParameterizedType::isGeneric)) {
              signature = genericClassSignature(superClass, interfaces);
          }
  
          String[] interfaces = new String[this.interfaces.size()];
          for (int i = 0; i < interfaces.length; i++) {
              interfaces[i] = this.interfaces.get(i).getClassName();
          }
          int accessModifier = toAccessModifier(access);
          visitor.visit(V17, isInterface() ? accessModifier : accessModifier | ACC_SUPER, type.getClassName(), signature, superClass.getClassName(), interfaces);
  
          // visit source
          if (source != null) {
              visitor.visitSource(source, debug);
          }
  
          // visit annotations
          for (AnnotationDefinition annotation : annotations) {
              annotation.visitClassAnnotation(visitor);
          }
  
          // visit fields
          for (FieldDefinition field : fields) {
              field.visit(visitor);
          }
  
          // visit clinit method
          if (!isInterface()) {
              classInitializer.visit(visitor, true);
          }
  
          // visit methods
          for (MethodDefinition method : methods) {
              method.visit(visitor);
          }
  
          // done
          visitor.visitEnd();
      }
  
      public AnnotationDefinition declareAnnotation(Class<?> type)
      {
          AnnotationDefinition annotationDefinition = new AnnotationDefinition(type);
          annotations.add(annotationDefinition);
          return annotationDefinition;
      }
  
      public AnnotationDefinition declareAnnotation(ParameterizedType type)
      {
          AnnotationDefinition annotationDefinition = new AnnotationDefinition(type);
          annotations.add(annotationDefinition);
          return annotationDefinition;
      }
  
      public FieldDefinition declareField(EnumSet<Access> access, String name, Class<?> type)
      {
          FieldDefinition fieldDefinition = new FieldDefinition(this, access, name, type);
          fields.add(fieldDefinition);
          return fieldDefinition;
      }
  
      public ClassDefinition addField(EnumSet<Access> access, String name, Class<?> type)
      {
          declareField(access, name, type);
          return this;
      }
  
      public FieldDefinition declareField(EnumSet<Access> access, String name, ParameterizedType type)
      {
          FieldDefinition fieldDefinition = new FieldDefinition(this, access, name, type);
          fields.add(fieldDefinition);
          return fieldDefinition;
      }
  
      public ClassDefinition addField(EnumSet<Access> access, String name, ParameterizedType type)
      {
          declareField(access, name, type);
          return this;
      }
  
      public ClassDefinition addField(FieldDefinition field)
      {
          fields.add(field);
          return this;
      }
  
      public MethodDefinition getClassInitializer()
      {
          if (isInterface()) {
              throw new IllegalAccessError("Interface does not have class initializer");
          }
          return classInitializer;
      }
  
      public MethodDefinition declareConstructor(
              EnumSet<Access> access,
              Parameter... parameters)
      {
          return declareMethod(access, "<init>", ParameterizedType.type(void.class), ImmutableList.copyOf(parameters));
      }
  
      public MethodDefinition declareConstructor(
              EnumSet<Access> access,
              Iterable<Parameter> parameters)
      {
          return declareMethod(access, "<init>", ParameterizedType.type(void.class), ImmutableList.copyOf(parameters));
      }
  
      public ClassDefinition declareDefaultConstructor(EnumSet<Access> access)
      {
          MethodDefinition constructor = declareConstructor(access);
          constructor
                  .getBody()
                  .append(constructor.getThis())
                  .invokeConstructor(superClass)
                  .ret();
          return this;
      }
  
      public ClassDefinition addMethod(MethodDefinition method)
      {
          methods.add(method);
          return this;
      }
  
      public ClassDefinition visitSource(String source, String debug)
      {
          this.source = source;
          this.debug = debug;
          return this;
      }
  
      public MethodDefinition declareMethod(
              EnumSet<Access> access,
              String name,
              ParameterizedType returnType,
              Parameter... parameters)
      {
          return declareMethod(access, name, returnType, ImmutableList.copyOf(parameters));
      }
  
      public MethodDefinition declareMethod(
              EnumSet<Access> access,
              String name,
              ParameterizedType returnType,
              Iterable<Parameter> parameters)
      {
          MethodDefinition methodDefinition = new MethodDefinition(this, access, name, returnType, parameters);
          for (MethodDefinition method : methods) {
              if (name.equals(method.getName()) && method.getParameterTypes().equals(methodDefinition.getParameterTypes())) {
                  throw new IllegalArgumentException("Method with same name and signature already exists: " + name);
              }
          }
          methods.add(methodDefinition);
          return methodDefinition;
      }
  
      public static String genericClassSignature(
              ParameterizedType classType,
              ParameterizedType... interfaceTypes)
      {
          return genericClassSignature(classType, ImmutableList.copyOf(interfaceTypes));
      }
  
      public static String genericClassSignature(
              ParameterizedType classType,
              List<ParameterizedType> interfaceTypes)
      {
          return Stream.concat(Stream.of(classType), interfaceTypes.stream())
                  .map(Object::toString)
                  .collect(Collectors.joining());
      }
  
      @Override
      public String toString()
      {
          final StringBuilder sb = new StringBuilder();
          sb.append("ClassDefinition");
          sb.append("{access=").append(access);
          sb.append(", type=").append(type);
          sb.append('}');
          return sb.toString();
      }
  }
  