/*
 * Copyright 2008 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.gwt.dev.shell.rewrite;

import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.javac.asmbridge.EmptyVisitor;
import com.google.gwt.dev.shell.JsValueGlue;
import com.google.gwt.dev.util.log.perf.SimpleEvent;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.Method;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;

/**
 * This class performs any and all byte code rewriting needed to make hosted
 * mode work. Currently, it performs the following rewrites:
 * <ol>
 * <li>Rewrites all native methods into non-native thunks to call JSNI via
 * {@link com.google.gwt.dev.shell.JavaScriptHost}.</li>
 * <li>Rewrites all JSO types into an interface type (which retains the original
 * name) and an implementation type (which has a $ appended).</li>
 * <li>All JSO interface types are empty and mirror the original type hierarchy.
 * </li>
 * <li>All JSO impl types contain the guts of the original type, except that all
 * instance methods are reimplemented as statics.</li>
 * <li>Calls sites to JSO types rewritten to dispatch to impl types. Any virtual
 * calls are also made static. Static field references to JSO types reference
 * static fields in the impl class.</li>
 * <li>JavaScriptObject$ implements all the interface types and is the only
 * instantiable type.</li>
 * </ol>
 *
 * @see RewriteJsniMethods
 * @see RewriteRefsToJsoClasses
 * @see WriteJsoInterface
 * @see WriteJsoImpl
 */
public class HostedModeClassRewriter {
  /*
   * Note: this rewriter operates on a class-by-class basis and has no global
   * view on the entire system. However, its operation requires certain global
   * state information. Therefore, all such global state must be passed into the
   * constructor.
   */

  /**
   * Maps instance methods to the class in which they are declared. This must be
   * provided by the caller since it requires global program state.
   */
  public interface InstanceMethodOracle {

    /**
     * For a given instance method and declared enclosing class (which must be a
     * JSO subtype), find the class in which that method was originally
     * declared. Methods declared on Object will return "java/lang/Object".
     * Static methods will always return <code>declaredClass</code>.
     *
     * @param declaredClass a descriptor of the static type of the qualifier
     * @param signature the binary signature of the method
     * @return the descriptor of the class in which that method was declared,
     *         which will either be <code>declaredClass</code> or some
     *         superclass
     * @throws IllegalArgumentException if the method could not be found
     */
    String findOriginalDeclaringClass(String declaredClass, String signature);
  }

  /**
   * Contains data about how SingleJsoImpl methods are to be dispatched.
   */
  public interface SingleJsoImplData {
    /**
     * Returns the method declarations that should be generated for a given
     * mangled method name. {@link #getDeclarations} and
     * {@link #getImplementations} maintain a pairwise mapping.
     */
    List<Method> getDeclarations(String mangledName);

    /**
     * Returns the implementations that the method declarations above should
     * delegate to.{@link #getDeclarations} and {@link #getImplementations}
     * maintain a pairwise mapping.
     */
    List<Method> getImplementations(String mangledName);

    /**
     * Returns all of the mangled method names for SingleJsoImpl methods.
     */
    SortedSet<String> getMangledNames();

    /**
     * Returns the internal names of all interface types implemented by JSOs.
     */
    Set<String> getSingleJsoIntfTypes();
  }

  static final String JAVASCRIPTOBJECT_DESC = JsValueGlue.JSO_CLASS.replace(
      '.', '/');

  static final String JAVASCRIPTOBJECT_IMPL_DESC = JsValueGlue.JSO_IMPL_CLASS.replace(
      '.', '/');

  static final String REFERENCE_FIELD = JsValueGlue.HOSTED_MODE_REFERENCE;

  static String addSyntheticThisParam(String owner, String methodDescriptor) {
    return "(L" + owner + ";" + methodDescriptor.substring(1);
  }

  private static String toDescriptor(String jsoSubtype) {
    return jsoSubtype.replace('.', '/');
  }

  /**
   * An unmodifiable set of descriptors containing the implementation form of
   * <code>JavaScriptObject</code> and all subclasses.
   */
  private final Set<String> jsoImplDescs;

  /**
   * An unmodifiable set of descriptors containing the interface form of
   * <code>JavaScriptObject</code> and all subclasses.
   */
  private final Set<String> jsoIntfDescs;

  private final SingleJsoImplData jsoData;

  /**
   * Records the superclass of every JSO for generating empty JSO interfaces.
   */
  private final Map<String, List<String>> jsoSuperDescs;

  /**
   * Maps methods to the class in which they are declared.
   */
  private InstanceMethodOracle mapper;

  /**
   * Creates a new {@link HostedModeClassRewriter} for a specified set of
   * subclasses of JavaScriptObject.
   *
   * @param jsoSubtypes a set of binary type names representing JavaScriptObject
   *          and all of its subtypes of
   * @param mapper maps methods to the class in which they are declared
   */
  public HostedModeClassRewriter(Set<String> jsoSubtypes,
      Map<String, List<String>> jsoSuperTypes, SingleJsoImplData jsoData,
      InstanceMethodOracle mapper) {
    Set<String> buildJsoIntfDescs = new HashSet<String>();
    Set<String> buildJsoImplDescs = new HashSet<String>();
    Map<String, List<String>> buildJsoSuperDescs = new HashMap<String, List<String>>();
    for (String jsoSubtype : jsoSubtypes) {
      String desc = toDescriptor(jsoSubtype);
      buildJsoIntfDescs.add(desc);
      buildJsoImplDescs.add(desc + "$");

      List<String> superTypes = jsoSuperTypes.get(jsoSubtype);
      assert (superTypes != null);
      assert (superTypes.size() > 0);
      for (ListIterator<String> i = superTypes.listIterator(); i.hasNext();) {
        i.set(toDescriptor(i.next()));
      }
      buildJsoSuperDescs.put(desc, Collections.unmodifiableList(superTypes));
    }

    this.jsoIntfDescs = Collections.unmodifiableSet(buildJsoIntfDescs);
    this.jsoImplDescs = Collections.unmodifiableSet(buildJsoImplDescs);
    this.jsoSuperDescs = Collections.unmodifiableMap(buildJsoSuperDescs);
    this.jsoData = jsoData;
    this.mapper = mapper;
  }

  /**
   * Returns <code>true</code> if the class is the implementation class for a
   * JSO subtype.
   */
  public boolean isJsoImpl(String className) {
    return jsoImplDescs.contains(toDescriptor(className));
  }

  /**
   * Returns <code>true</code> if the class is the interface class for a JSO
   * subtype.
   */
  public boolean isJsoIntf(String className) {
    return jsoIntfDescs.contains(toDescriptor(className));
  }

  /**
   * Performs rewriting transformations on a class.
   *
   * @param typeOracle a typeOracle modeling the user classes
   * @param className the name of the class
   * @param classBytes the bytes of the class
   * @param anonymousClassMap a map between the anonymous class names of java
   *          compiler used to compile code and jdt. Emma-specific.
   */
  public byte[] rewrite(TypeOracle typeOracle, String className,
      byte[] classBytes, Map<String, String> anonymousClassMap) {
    try (SimpleEvent ignored = new SimpleEvent("Class bytes rewrite: " + className)) {
      String desc = toDescriptor(className);
      assert (!jsoIntfDescs.contains(desc));

      // The ASM model is to chain a bunch of visitors together.
      ClassWriter writer = new ClassWriter(0);
      ClassVisitor v = writer;

      // v = new CheckClassAdapter(v);
      // v = new TraceClassVisitor(v, new PrintWriter(System.out));
      v = new UseMirroredClasses(v, className);

      v = new RewriteSingleJsoImplDispatches(v, typeOracle, jsoData);

      v = new RewriteRefsToJsoClasses(v, jsoIntfDescs, mapper);

      if (jsoImplDescs.contains(desc)) {
        v = WriteJsoImpl.create(v, desc, jsoIntfDescs, mapper, jsoData);
      }

      v = new RewriteJsniMethods(v, anonymousClassMap);

      new ClassReader(classBytes).accept(v, 0);
      return writer.toByteArray();
    }
  }

  public byte[] writeJsoIntf(final String className, byte classBytes[]) {
    String desc = toDescriptor(className);
    assert (jsoIntfDescs.contains(desc));
    assert (jsoSuperDescs.containsKey(desc));
    List<String> superDescs = jsoSuperDescs.get(desc);
    assert (superDescs != null);
    assert (superDescs.size() > 0);

    // The ASM model is to chain a bunch of visitors together.
    ClassWriter writer = new ClassWriter(0);
    final ClassVisitor v = writer;

    // v = new CheckClassAdapter(v);
    // v = new TraceClassVisitor(v, new PrintWriter(System.out));

    String[] interfaces;
    // TODO(bov): something better than linear?
    if (superDescs.contains("java/lang/Object")) {
      interfaces = null;
    } else {
      interfaces = superDescs.toArray(new String[superDescs.size()]);
    }
    v.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC | Opcodes.ACC_INTERFACE, desc,
        null, "java/lang/Object", interfaces);
    if (classBytes != null) {
      // Java7 enforces innerclass/outerclass consistency. In order to fix this, copy from original
      ClassVisitor cv = new EmptyVisitor() {
        @Override
        public void visitInnerClass(String name, String outerName, String innerName,
            int access) {
          // copy inner class table from original JSO to synthetic interface
          v.visitInnerClass(name, outerName, innerName, access);
        }

        @Override
        public void visitOuterClass(String owner, String name, String desc) {
          // copy outer class table from original JSO to synthetic interface
          v.visitOuterClass(owner, name, desc);
        }
      };
      new ClassReader(classBytes).accept(cv, 0);
    }
    v.visitEnd();
    return writer.toByteArray();
  }
}
