/*
 * Copyright 2015 Google Inc.
 *
 * 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.template.soy.jbcsrc.internal;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.template.soy.jbcsrc.restricted.BytecodeUtils.OBJECT;

import com.google.protobuf.GeneratedMessage;
import com.google.template.soy.jbcsrc.restricted.Flags;
import com.google.template.soy.jbcsrc.restricted.TypeInfo;
import com.google.template.soy.jbcsrc.shared.Names;
import java.util.ArrayList;
import java.util.List;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.util.CheckClassAdapter;

/**
 * A subclass of {@link ClassWriter} that allows us to specialize {@link
 * ClassWriter#getCommonSuperClass} for compiler generated types as well as set common defaults for
 * all classwriters used by {@code jbcsrc}.
 */
public final class SoyClassWriter extends ClassVisitor {
  /** Returns a new SoyClassWriter for writing a new class of the given type. */
  public static Builder builder(TypeInfo type) {
    return new Builder(type);
  }

  public static final class Builder {
    private final TypeInfo type;
    private int access = Opcodes.ACC_FINAL | Opcodes.ACC_SUPER;
    private TypeInfo baseClass = OBJECT;
    private List<String> interfaces = new ArrayList<>();
    private String fileName; // optional

    private Builder(TypeInfo type) {
      this.type = checkNotNull(type);
    }

    /**
     * Set the access permissions on the generated class. The default is package private and {@code
     * final}.
     *
     * @param access The access permissions, a bit mask composed from constants like {@link
     *     Opcodes#ACC_PUBLIC}
     */
    public Builder setAccess(int access) {
      this.access = access;
      return this;
    }

    /** Sets the base class for this type. The default is {@code Object}. */
    public Builder extending(TypeInfo baseClass) {
      this.baseClass = checkNotNull(baseClass);
      return this;
    }

    /** Adds an {@code interface} to the class. */
    public Builder implementing(TypeInfo typeInfo) {
      interfaces.add(typeInfo.internalName());
      return this;
    }

    public Builder sourceFileName(String fileName) {
      this.fileName = checkNotNull(fileName);
      return this;
    }

    public SoyClassWriter build() {
      return new SoyClassWriter(new Writer(), this);
    }
  }

  private final Writer writer;
  private final TypeInfo typeInfo;
  private int numFields;
  private int numDetachStates;

  private SoyClassWriter(Writer writer, Builder builder) {
    super(writer.api(), Flags.DEBUG ? new CheckClassAdapter(writer, false) : writer);
    this.writer = writer;
    this.typeInfo = builder.type;
    super.visit(
        Opcodes.V1_7,
        builder.access,
        builder.type.internalName(),
        null /* not generic */,
        builder.baseClass.internalName(),
        builder.interfaces.toArray(new String[builder.interfaces.size()]));
    if (builder.fileName != null) {
      super.visitSource(
          builder.fileName,
          // No JSR-45 style source maps, instead we write the line numbers in the normal locations.
          null);
    }
  }

  /** Sets the number of 'detach states' needed by the compiled class. */
  public void setNumDetachStates(int numDetachStates) {
    checkArgument(numDetachStates >= 0);
    this.numDetachStates = numDetachStates;
  }

  /** @deprecated Don't call visitSource(), SoyClassWriter calls it for you during construction. */
  @Deprecated
  @Override
  public void visitSource(String source, String debug) {
    throw new UnsupportedOperationException(
        "Don't call visitSource(), SoyClassWriter calls it for you");
  }

  /** @deprecated Don't call visit(), SoyClassWriter calls it for you during construction. */
  @Deprecated
  @Override
  public void visit(int v, int a, String n, String s, String b, String[] i) {
    throw new UnsupportedOperationException("Don't call visit(), SoyClassWriter calls it for you");
  }

  @Override
  public FieldVisitor visitField(
      int access, String name, String desc, String signature, Object value) {
    numFields++;
    return super.visitField(access, name, desc, signature, value);
  }

  /** Returns the bytecode of the class that was build with this class writer. */
  public ClassData toClassData() {
    return ClassData.create(typeInfo, writer.toByteArray(), numFields, numDetachStates);
  }

  private static final class Writer extends ClassWriter {
    private static final String PROTO_EXTENDABLE_BUILDER =
        Type.getInternalName(GeneratedMessage.ExtendableBuilder.class);

    Writer() {
      super(COMPUTE_FRAMES | COMPUTE_MAXS);
    }

    int api() {
      return api;
    }

    @Override
    protected String getCommonSuperClass(String left, String right) {
      // TODO(lukes): we know the names and superclasses of all the classes we generate prior to
      // this method being called, so we could build a smarter system just by building up that
      // graph as we generate classes.
      // similarly for soy.data classes we could consider adding special cases. The fact that this
      // class falls back to doing classpath lookups makes it hard to test in a unit test :(
      boolean leftIsGenerated = left.startsWith(Names.INTERNAL_CLASS_PREFIX);
      boolean rightIsGenerated = right.startsWith(Names.INTERNAL_CLASS_PREFIX);
      if (!leftIsGenerated && !rightIsGenerated) {
        // Test for proto extendable builders.  We need to calculate common baseclasses for these
        // since we generate calls to methods on ExtendableBuilder
        if ((left.equals(PROTO_EXTENDABLE_BUILDER) && right.endsWith("$Builder"))
            || (right.equals(PROTO_EXTENDABLE_BUILDER) && left.endsWith("$Builder"))) {
          return PROTO_EXTENDABLE_BUILDER;
        }
        // The only cases that should occur here should be about our runtime types (like StringData)
        try {
          return super.getCommonSuperClass(left, right);
        } catch (RuntimeException re) {
          throw new RuntimeException(
              "unable to calculate common base class of: " + left + " and " + right);
        }
      }
      // The only reason a generated type will get compared to a non-generated type is if they
      // happen to share a local variable slot.  This is because ASM doesn't know that the old
      // variable has gone 'out of scope' and a new one entered it.  The best advice from the asm
      // community so far has been 'just return object', so that is what we are doing
      // See http://mail.ow2.org/wws/arc/asm/2015-06/msg00008.html
      return OBJECT.internalName();
    }
  }
}

@@ -6885,24 +6885,28 @@
 nd %22 + right
+, re
 );%0A        %7D

Fix 1 ErrorProneStyle finding: * This catch block catches an exception and re-throws another, but swallows the caught exception rather than setting it as a cause. This can make debugging harder. (see [] This CL looks good? Just LGTM and Approve it! This CL doesn’t look good? This is what you can do: * Suggest a fix on the CL ([] * Revert this CL, by replying "REVERT: <provide reason>" * File a bug under [] if this change looks generally problematic. * Revert this CL and not get a CL that cleans up these paths in the future by replying "BLACKLIST: <provide reason>". This is not reversible! We recommend to blacklist the respective paths in your CL Robot configuration instead: []