/*
 * 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;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.util.CheckClassAdapter;
import org.objectweb.asm.util.Textifier;
import org.objectweb.asm.util.TraceClassVisitor;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.security.AccessController;
import java.security.PrivilegedExceptionAction;

/**
 * A simple tuple of generated class data and type information about the class.
 * 
 * <p>Note: not using an @AutoValue since it copies
 */
final class ClassData {
  static ClassData create(TypeInfo type, byte[] b, int numFields, int numDetachStates) {
    return new ClassData(type, b, numFields, numDetachStates);
  }

  private final TypeInfo type;
  private final byte[] data;
  private final int numberOfFields;
  private final int numDetachStates;

  private ClassData(TypeInfo type, byte[] data, int numberOfFields, int numDetachStates) {
    this.type = checkNotNull(type);
    this.data = checkNotNull(data);
    this.numberOfFields = numberOfFields;
    this.numDetachStates = numDetachStates;
  }

  TypeInfo type() {
    return type;
  }

  /**
   * Caution, this returns the underlying array and is mutable.
   */
  byte[] data() {
    return data;
  }

  int numberOfFields() {
    return numberOfFields;
  }

  int numberOfDetachStates() {
    return numDetachStates;
  }

  /**
   * Runs the {@link CheckClassAdapter} on this class in basic analysis mode.
   *
   * <p>Basic anaylsis mode can flag verification errors that don't depend on knowing complete type
   * information for the classes and methods being called.  This is useful for flagging simple
   * generation mistakes (e.g. stack underflows, method return type mismatches, accessing invalid
   * locals).  Additionally, the error messages are more useful than what the java verifier normally
   * presents.
   */
  void checkClass() {
    ClassNode cv = new ClassNode();
    new ClassReader(data).accept(new CheckClassAdapter(cv, true /* check data flow */), 0);
    // double check our fields while we are here.
    checkState(type.internalName().equals(cv.name));
    checkState(numberOfFields == cv.fields.size());
  }

  URL asUrl() {
    try {
      // Needs specifyStreamHandler permission.
      return AccessController.doPrivileged(
          new PrivilegedExceptionAction<URL>() {
            @Override
            public URL run() throws MalformedURLException {
              return new URL(
                  "mem",
                  "",
                  -1,
                  type.internalName() + ".class",
                  new URLStreamHandler() {
                    @Override protected URLConnection openConnection(URL u) {
                      return new URLConnection(u) {
                        @Override public void connect() {}
                        @Override public InputStream getInputStream() {
                          return new ByteArrayInputStream(data);
                        }
                      };
                    }
                  });
            }
          });
    } catch (Exception e) {
      throw new IllegalStateException("Failed to create stream handler for resource url", e);
    }
  }

  @Override public String toString() {
    StringWriter sw = new StringWriter();
    new ClassReader(data)
        .accept(new TraceClassVisitor(null, new Textifier(), new PrintWriter(sw)), 0);
    return sw.toString();
  }
}
