content: /*
   * [The "BSD licence"]
   * Copyright (c) 2010 Ben Gruver (JesusFreke)
   * All rights reserved.
   *
   * Redistribution and use in source and binary forms, with or without
   * modification, are permitted provided that the following conditions
   * are met:
   * 1. Redistributions of source code must retain the above copyright
   *    notice, this list of conditions and the following disclaimer.
   * 2. Redistributions in binary form must reproduce the above copyright
   *    notice, this list of conditions and the following disclaimer in the
   *    documentation and/or other materials provided with the distribution.
   * 3. The name of the author may not be used to endorse or promote products
   *    derived from this software without specific prior written permission.
   *
   * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
   * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
   * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
   * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
   * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
   * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
   * INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
   * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   */
  
  package com.taobao.android.baksmali.adaptors;
  
  import com.google.common.collect.Lists;
  import com.taobao.android.apatch.ApkPatch;
  import com.taobao.android.apatch.annotation.MethodReplaceAnnotation;
  import com.taobao.android.apatch.utils.TypeGenUtil;
  import com.taobao.android.baksmali.util.ReferenceUtil;
  import com.taobao.android.object.DexDiffInfo;
  
  import org.jf.baksmali.BaksmaliOptions;
  import org.jf.dexlib2.AccessFlags;
  import org.jf.dexlib2.dexbacked.DexBackedClassDef;
  import org.jf.dexlib2.dexbacked.DexBackedDexFile.InvalidItemIndex;
  import org.jf.dexlib2.iface.Annotation;
  import org.jf.dexlib2.iface.ClassDef;
  import org.jf.dexlib2.iface.Field;
  import org.jf.dexlib2.iface.Method;
  import org.jf.dexlib2.iface.MethodImplementation;
  import org.jf.dexlib2.iface.instruction.Instruction;
  import org.jf.dexlib2.iface.instruction.formats.Instruction21c;
  import org.jf.dexlib2.iface.reference.FieldReference;
  import org.jf.util.IndentingWriter;
  import org.jf.util.StringUtils;
  
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Set;
  
  import javax.annotation.Nonnull;
  
  public class ClassDefinition {
  
      @Nonnull
      public final BaksmaliOptions options;
      @Nonnull
      public final ClassDef classDef;
      @Nonnull
      private final HashSet<String> fieldsSetInStaticConstructor;
      @Nonnull
      public final boolean isScan;
      @Nonnull
      public final boolean fullMethod;
  
      protected boolean validationErrors;
  
      public ClassDefinition(@Nonnull BaksmaliOptions options, @Nonnull ClassDef classDef,
                             @Nonnull boolean isScan, @Nonnull boolean fullMethod) {
          this.options = options;
          this.classDef = classDef;
          this.isScan = isScan;
          this.fullMethod = fullMethod;
          fieldsSetInStaticConstructor = findFieldsSetInStaticConstructor();
      }
  
      public ClassDefinition(BaksmaliOptions options, ClassDef classDef) {
          this.options = options;
          this.classDef = classDef;
          this.isScan = false;
          this.fieldsSetInStaticConstructor = findFieldsSetInStaticConstructor();
          this.fullMethod = true;
      }
  
  
      public boolean hadValidationErrors() {
          return validationErrors;
      }
  
      @Nonnull
      private HashSet<String> findFieldsSetInStaticConstructor() {
          HashSet<String> fieldsSetInStaticConstructor = new HashSet<String>();
  
          for (Method method : classDef.getDirectMethods()) {
              if (method.getName().equals("<clinit>")) {
                  MethodImplementation impl = method.getImplementation();
                  if (impl != null) {
                      for (Instruction instruction : impl.getInstructions()) {
                          switch (instruction.getOpcode()) {
                              case SPUT:
                              case SPUT_BOOLEAN:
                              case SPUT_BYTE:
                              case SPUT_CHAR:
                              case SPUT_OBJECT:
                              case SPUT_SHORT:
                              case SPUT_WIDE: {
                                  Instruction21c ins = (Instruction21c) instruction;
                                  FieldReference fieldRef = null;
                                  try {
                                      fieldRef = (FieldReference) ins.getReference();
                                  } catch (InvalidItemIndex ex) {
                                      // just ignore it for now. We'll deal with it later, when processing the instructions
                                      // themselves
                                  }
                                  if (fieldRef != null &&
                                          fieldRef.getDefiningClass().equals((classDef.getType()))) {
                                      fieldsSetInStaticConstructor.add(ReferenceUtil.getShortFieldDescriptor(fieldRef));
                                  }
                                  break;
                              }
                          }
                      }
                  }
              }
          }
          return fieldsSetInStaticConstructor;
      }
  
      public void writeTo(IndentingWriter writer) throws IOException {
          writeClass(writer);
          writeSuper(writer);
          writeSourceFile(writer);
          writeInterfaces(writer);
          writeAnnotations(writer);
          Set<String> staticFields = writeStaticFields(writer);
          writeInstanceFields(writer, staticFields);
          Set<String> directMethods = writeDirectMethods(writer);
          writeVirtualMethods(writer, directMethods);
      }
  
      private void writeClass(IndentingWriter writer) throws IOException {
          writer.write(".class ");
          writeAccessFlags(writer);
          writer.write(TypeGenUtil.newType(classDef.getType()));
          writer.write('\n');
      }
  
      private void writeAccessFlags(IndentingWriter writer) throws IOException {
          for (AccessFlags accessFlag : AccessFlags.getAccessFlagsForClass(classDef.getAccessFlags())) {
              writer.write(accessFlag.toString());
              writer.write(' ');
          }
      }
  
      private void writeSuper(IndentingWriter writer) throws IOException {
          String superClass = classDef.getSuperclass();
          if (superClass != null) {
              writer.write(".super ");
              writer.write(superClass);
              writer.write('\n');
  
  
              if (isScan) {
                  //    System.out.println("writeSuper: " + superClass + " " + classDef.getType());
                  ArrayList<String> derivedClasses = null;
                  if (ApkPatch.superClasses.containsKey(superClass)) {
                      derivedClasses = ApkPatch.superClasses.get(superClass);
                  } else {
                      derivedClasses = new ArrayList<String>();
                      ApkPatch.superClasses.put(superClass, derivedClasses);
                  }
                  derivedClasses.add(classDef.getType());
              }
          }
      }
  
      private void writeSourceFile(IndentingWriter writer) throws IOException {
          String sourceFile = classDef.getSourceFile();
          if (sourceFile != null) {
              writer.write(".source \"");
              StringUtils.writeEscapedString(writer, sourceFile);
              writer.write("\"\n");
          }
      }
  
      private void writeInterfaces(IndentingWriter writer) throws IOException {
          List<String> interfaces = Lists.newArrayList(classDef.getInterfaces());
          Collections.sort(interfaces);
  
          if (interfaces.size() != 0) {
              writer.write('\n');
              writer.write("# interfaces\n");
              for (String interfaceName : interfaces) {
                  writer.write(".implements ");
                  writer.write(interfaceName);
                  writer.write('\n');
              }
          }
      }
  
      private void writeAnnotations(IndentingWriter writer) throws IOException {
          Collection<? extends Annotation> classAnnotations = classDef.getAnnotations();
          if (classAnnotations.size() != 0) {
              writer.write("\n\n");
              writer.write("# annotations\n");
  
              String containingClass = null;
              if (options.implicitReferences) {
                  containingClass = classDef.getType();
              }
  
              AnnotationFormatter.writeTo(writer, classAnnotations, containingClass);
          }
      }
  
      private Set<String> writeStaticFields(IndentingWriter writer) throws IOException {
          if (!fullMethod && !DexDiffInfo.addedClasses.contains(classDef)) {
              return null;
          }
          boolean wroteHeader = false;
          Set<String> writtenFields = new HashSet<String>();
  
          Iterable<? extends Field> staticFields;
          if (classDef instanceof DexBackedClassDef) {
              staticFields = ((DexBackedClassDef) classDef).getStaticFields(false);
          } else {
              staticFields = classDef.getStaticFields();
          }
  
          for (Field field : staticFields) {
              if (!wroteHeader) {
                  writer.write("\n\n");
                  writer.write("# static fields");
                  wroteHeader = true;
              }
              writer.write('\n');
  
              boolean setInStaticConstructor;
              IndentingWriter fieldWriter = writer;
              String fieldString = ReferenceUtil.getShortFieldDescriptor(field);
              if (!writtenFields.add(fieldString)) {
                  writer.write("# duplicate field ignored\n");
                  fieldWriter = new CommentingIndentingWriter(writer);
                  System.err.println(String.format("Ignoring duplicate field: %s->%s", classDef.getType(), fieldString));
                  setInStaticConstructor = false;
              } else {
                  setInStaticConstructor = fieldsSetInStaticConstructor.contains(fieldString);
              }
              FieldDefinition.writeTo(options, fieldWriter, field, setInStaticConstructor);
          }
          return writtenFields;
      }
  
      private void writeInstanceFields(IndentingWriter writer, Set<String> staticFields) throws IOException {
          if (!fullMethod&& !DexDiffInfo.addedClasses.contains(classDef)) {
              return;
          }
          boolean wroteHeader = false;
          Set<String> writtenFields = new HashSet<String>();
  
          Iterable<? extends Field> instanceFields;
          if (classDef instanceof DexBackedClassDef) {
              instanceFields = ((DexBackedClassDef) classDef).getInstanceFields(false);
          } else {
              instanceFields = classDef.getInstanceFields();
          }
  
          for (Field field : instanceFields) {
              if (!wroteHeader) {
                  writer.write("\n\n");
                  writer.write("# instance fields");
                  wroteHeader = true;
              }
              writer.write('\n');
  
              IndentingWriter fieldWriter = writer;
              String fieldString = ReferenceUtil.getShortFieldDescriptor(field);
              if (!writtenFields.add(fieldString)) {
                  writer.write("# duplicate field ignored\n");
                  fieldWriter = new CommentingIndentingWriter(writer);
                  System.err.println(String.format("Ignoring duplicate field: %s->%s", classDef.getType(), fieldString));
              } else if (staticFields.contains(fieldString)) {
                  System.err.println(String.format("Duplicate static+instance field found: %s->%s",
                          classDef.getType(), fieldString));
                  System.err.println("You will need to rename one of these fields, including all references.");
  
                  writer.write("# There is both a static and instance field with this signature.\n" +
                          "# You will need to rename one of these fields, including all references.\n");
              }
              FieldDefinition.writeTo(options, fieldWriter, field, false);
          }
      }
  
      private Set<String> writeDirectMethods(IndentingWriter writer) throws IOException {
          boolean wroteHeader = false;
          Set<String> writtenMethods = new HashSet<String>();
  
          Iterable<? extends Method> directMethods;
          Set<? extends Method> modifieds = null;
          if (classDef instanceof DexBackedClassDef) {
              directMethods = ((DexBackedClassDef) classDef).getDirectMethods(false);
              modifieds = (Set<? extends Method>) DexDiffInfo.modifiedMethods;
          } else {
              directMethods = classDef.getDirectMethods();
          }
  
          MethodReplaceAnnotation replaceAnnotaion;
          for (Method method : directMethods) {
  
  
              if (!fullMethod && !DexDiffInfo.addedClasses.contains(classDef)) {
                  if (!modifieds.contains(method) && !DexDiffInfo.addedMethods.contains(method)) {
                      continue;
                  }
              }
  
              if (!wroteHeader) {
                  writer.write("\n\n");
                  writer.write("# direct methods");
                  wroteHeader = true;
              }
              writer.write('\n');
  
              // TODO: check for method validation errors
              String methodString = ReferenceUtil.getMethodDescriptor(method, true);
  
              IndentingWriter methodWriter = writer;
              if (!writtenMethods.add(methodString)) {
                  writer.write("# duplicate method ignored\n");
                  methodWriter = new CommentingIndentingWriter(writer);
              }
  
              MethodImplementation methodImpl = method.getImplementation();
              if (methodImpl == null) {
                  MethodDefinition.writeEmptyMethodTo(methodWriter, method, options);
              } else {
                  MethodDefinition methodDefinition = new MethodDefinition(this, method, methodImpl);
                  methodDefinition.setFullMethod(fullMethod);
                  methodDefinition.writeTo(methodWriter);
              }
          }
          return writtenMethods;
      }
  
      private void writeVirtualMethods(IndentingWriter writer, Set<String> directMethods) throws IOException {
          boolean wroteHeader = false;
          Set<String> writtenMethods = new HashSet<String>();
  
          Iterable<? extends Method> virtualMethods;
          Set<? extends Method> modifieds = null;
          if (classDef instanceof DexBackedClassDef) {
              virtualMethods = ((DexBackedClassDef) classDef).getVirtualMethods(false);
              modifieds = (Set<? extends Method>) DexDiffInfo.modifiedMethods;
          } else {
              virtualMethods = classDef.getVirtualMethods();
          }
  
          MethodReplaceAnnotation replaceAnnotaion;
          for (Method method : virtualMethods) {
  
              if (!fullMethod && !DexDiffInfo.addedClasses.contains(classDef)) {
                  if (!modifieds.contains(method) && !DexDiffInfo.addedMethods.contains(method)) {
                      continue;
                  }
              }
  
  
              if (!wroteHeader) {
                  writer.write("\n\n");
                  writer.write("# virtual methods");
                  wroteHeader = true;
              }
              writer.write('\n');
  
              // TODO: check for method validation errors
              String methodString = ReferenceUtil.getMethodDescriptor(method, true);
  
              IndentingWriter methodWriter = writer;
              if (!writtenMethods.add(methodString)) {
                  writer.write("# duplicate method ignored\n");
                  methodWriter = new CommentingIndentingWriter(writer);
              } else if (directMethods.contains(methodString)) {
                  writer.write("# There is both a direct and virtual method with this signature.\n" +
                          "# You will need to rename one of these methods, including all references.\n");
                  System.err.println(String.format("Duplicate direct+virtual method found: %s->%s",
                          classDef.getType(), methodString));
                  System.err.println("You will need to rename one of these methods, including all references.");
              }
  
              MethodImplementation methodImpl = method.getImplementation();
              if (methodImpl == null) {
                  MethodDefinition.writeEmptyMethodTo(methodWriter, method, options);
              } else {
                  MethodDefinition methodDefinition = new MethodDefinition(this, method, methodImpl);
                  methodDefinition.writeTo(methodWriter);
              }
          }
      }
  
      public ClassDef getClassDef() {
          return classDef;
      }
  }
  