/*
 * Copyright 2000-2017 JetBrains s.r.o.
 *
 * 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.intellij.debugger.impl;

import com.intellij.debugger.engine.DebugProcess;
import com.intellij.debugger.engine.JVMNameUtil;
import com.intellij.debugger.engine.evaluation.EvaluateException;
import com.intellij.debugger.engine.evaluation.EvaluationContext;
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl;
import com.intellij.debugger.jdi.VirtualMachineProxyImpl;
import com.intellij.openapi.util.io.StreamUtil;
import com.intellij.rt.debugger.ImageSerializer;
import com.sun.jdi.*;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * @author egor
 */
public class ClassLoadingUtils {
  private ClassLoadingUtils() {}

  public static ClassLoaderReference getClassLoader(EvaluationContext context, DebugProcess process) throws EvaluateException {
    try {
      // TODO [egor]: cache?
      ClassType loaderClass = (ClassType)process.findClass(context, "java.net.URLClassLoader", context.getClassLoader());
      Method ctorMethod = loaderClass.concreteMethodByName(JVMNameUtil.CONSTRUCTOR_NAME, "([Ljava/net/URL;Ljava/lang/ClassLoader;)V");
      ClassLoaderReference reference = (ClassLoaderReference)process.newInstance(context, loaderClass, ctorMethod, Arrays
        .asList(createURLArray(context), context.getClassLoader()));
      DebuggerUtilsEx.keep(reference, context);
      return reference;
    }
    catch (Exception e) {
      throw new EvaluateException("Error creating evaluation class loader: " + e, e);
    }
  }

  public static void defineClass(String name,
                                 byte[] bytes,
                                 EvaluationContext context,
                                 DebugProcess process,
                                 ClassLoaderReference classLoader) throws EvaluateException {
    try {
      VirtualMachineProxyImpl proxy = (VirtualMachineProxyImpl)process.getVirtualMachineProxy();
      Method defineMethod =
        ((ClassType)classLoader.referenceType()).concreteMethodByName("defineClass", "(Ljava/lang/String;[BII)Ljava/lang/Class;");
      StringReference nameObj = proxy.mirrorOf(name);
      DebuggerUtilsEx.keep(nameObj, context);
      process.invokeMethod(context, classLoader, defineMethod,
                           Arrays.asList(nameObj,
                                         mirrorOf(bytes, context, process),
                                         proxy.mirrorOf(0),
                                         proxy.mirrorOf(bytes.length)));
    }
    catch (Exception e) {
      throw new EvaluateException("Error during class " + name + " definition: " + e, e);
    }
  }

  /**
   * Finds and if necessary defines helper class
   * May modify class loader in evaluationContext
   */
  @Nullable
  public static ClassType getHelperClass(String name, EvaluationContext evaluationContext, DebugProcess process) throws EvaluateException {
    // TODO [egor]: cache and load in boostrap class loader
    try {
      ClassLoaderReference classLoader = evaluationContext.getClassLoader();
      return (ClassType)process.findClass(evaluationContext, name, classLoader);
    } catch (EvaluateException e) {
      Throwable cause = e.getCause();
      if (cause instanceof InvocationException) {
        if ("java.lang.ClassNotFoundException".equals(((InvocationException)cause).exception().type().name())) {
          // need to define
          ClassLoaderReference classLoader = getClassLoader(evaluationContext, process);
          InputStream stream = ImageSerializer.class.getResourceAsStream("/" + name.replaceAll("[.]", "/") + ".class");
          try {
            if (stream == null) return null;
            defineClass(name, StreamUtil.loadFromStream(stream), evaluationContext, process, classLoader);
            ((EvaluationContextImpl)evaluationContext).setClassLoader(classLoader);
            return (ClassType)process.findClass(evaluationContext, name, classLoader);
          }
          catch (IOException ioe) {
            throw new EvaluateException("Unable to read " + name + " class bytes", ioe);
          }
          finally {
            try {
              if (stream != null) {
                stream.close();
              }
            }
            catch (IOException ignored) {}
          }
        }
      }
      throw e;
    }
  }

  private static ArrayReference createURLArray(EvaluationContext context)
    throws EvaluateException, InvalidTypeException, ClassNotLoadedException {
    DebugProcess process = context.getDebugProcess();
    ArrayType arrayType = (ArrayType)process.findClass(context, "java.net.URL[]", context.getClassLoader());
    ArrayReference arrayRef = arrayType.newInstance(1);
    DebuggerUtilsEx.keep(arrayRef, context);
    ClassType classType = (ClassType)process.findClass(context, "java.net.URL", context.getClassLoader());
    VirtualMachineProxyImpl proxy = (VirtualMachineProxyImpl)process.getVirtualMachineProxy();
    StringReference url = proxy.mirrorOf("file:a");
    DebuggerUtilsEx.keep(url, context);
    ObjectReference reference = process.newInstance(context,
                                                    classType,
                                                    classType.concreteMethodByName(JVMNameUtil.CONSTRUCTOR_NAME, "(Ljava/lang/String;)V"),
                                                    Collections.singletonList(url));
    DebuggerUtilsEx.keep(reference, context);
    arrayRef.setValues(Collections.singletonList(reference));
    return arrayRef;
  }

  private static ArrayReference mirrorOf(byte[] bytes, EvaluationContext context, DebugProcess process)
    throws EvaluateException, InvalidTypeException, ClassNotLoadedException {
    ArrayType arrayClass = (ArrayType)process.findClass(context, "byte[]", context.getClassLoader());
    ArrayReference reference = process.newInstance(arrayClass, bytes.length);
    DebuggerUtilsEx.keep(reference, context);
    List<Value> mirrors = new ArrayList<>(bytes.length);
    for (byte b : bytes) {
      mirrors.add(((VirtualMachineProxyImpl)process.getVirtualMachineProxy()).mirrorOf(b));
    }
    reference.setValues(mirrors);
    return reference;
  }
}

@@ -1373,16 +1373,62 @@
 Utils %7B%0A
+  private static final int BATCH_SIZE = 4096;%0A
   privat
@@ -6866,41 +6866,232 @@
   %7D%0A
-    reference.setValues(mirrors);
+%0A    int loaded = 0;%0A    while (loaded %3C mirrors.size()) %7B%0A      int chunkSize = Math.min(BATCH_SIZE, mirrors.size() - loaded);%0A      reference.setValues(loaded, mirrors, loaded, chunkSize);%0A      loaded += chunkSize;%0A    %7D%0A
 %0A   

Send class bytecode using chunks with limited size