/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.zeppelin.java;

import com.thoughtworks.qdox.JavaProjectBuilder;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaMethod;
import com.thoughtworks.qdox.model.JavaSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.ToolProvider;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.PrintStream;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;

/**
 * StaticRepl for compiling the java code in memory
 */
public class StaticRepl {
  private static final Logger LOGGER = LoggerFactory.getLogger(StaticRepl.class);

  public static String execute(String generatedClassName, String code) throws Exception {

    // Java parsing
    JavaProjectBuilder builder = new JavaProjectBuilder();
    JavaSource src = builder.addSource(new StringReader(code));

    // get all classes in code (paragraph)
    List<JavaClass> classes = src.getClasses();
    String mainClassName = null;

    // Searching for class containing Main method
    for (JavaClass javaClass : classes) {
      boolean hasMain = false;

      for (JavaMethod method : javaClass.getMethods()) {
        if (method.getName().equals("main") && method.isStatic()) {
          mainClassName = javaClass.getName();
          hasMain = true;
          break;
        }
      }
      if (hasMain) {
        break;
      }

    }

    // if there isn't Main method, will return error
    if (mainClassName == null) {
      LOGGER.error("Exception for Main method", "There isn't any class "
          + "containing static main method.");
      throw new Exception("There isn't any class containing static main method.");
    }

    // replace name of class containing Main method with generated name
    code = code.replace(mainClassName, generatedClassName);

    JavaFileObject file = new JavaSourceFromString(generatedClassName, code);
    Iterable<? extends JavaFileObject> compilationUnits = List.of(file);

    ByteArrayOutputStream baosOut = new ByteArrayOutputStream();
    ByteArrayOutputStream baosErr = new ByteArrayOutputStream();

    // Creating new stream to get the output data
    PrintStream newOut = new PrintStream(baosOut);
    PrintStream newErr = new PrintStream(baosErr);
    // Save the old System.out!
    PrintStream oldOut = System.out;
    PrintStream oldErr = System.err;
    // Tell Java to use your special stream
    System.setOut(newOut);
    System.setErr(newErr);

    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
    CompilationTask task = compiler.getTask(null, null, diagnostics, null, null, compilationUnits);

    // executing the compilation process
    boolean success = task.call();

    // if success is false will get error
    if (!success) {
      for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
        if (diagnostic.getLineNumber() == -1) {
          continue;
        }
        System.err.println("line " + diagnostic.getLineNumber() + " : "
            + diagnostic.getMessage(null));
      }
      System.out.flush();
      System.err.flush();

      System.setOut(oldOut);
      System.setErr(oldErr);
      LOGGER.error("Exception in Interpreter while compilation", baosErr.toString());
      throw new Exception(baosErr.toString());
    } else {
      try {

        // creating new class loader
        URLClassLoader classLoader = URLClassLoader.newInstance(new URL[]{new File("").toURI()
            .toURL()});
        // execute the Main method
        Class.forName(generatedClassName, true, classLoader)
            .getDeclaredMethod("main", new Class[]{String[].class})
            .invoke(null, new Object[]{null});

        System.out.flush();
        System.err.flush();

        // set the stream to old stream
        System.setOut(oldOut);
        System.setErr(oldErr);

        return baosOut.toString();

      } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException
          | InvocationTargetException e) {
        LOGGER.error("Exception in Interpreter while execution", e);
        System.err.println(e);
        e.printStackTrace(newErr);
        throw new Exception(baosErr.toString(), e);

      } finally {

        System.out.flush();
        System.err.flush();

        System.setOut(oldOut);
        System.setErr(oldErr);
      }
    }

  }

}

class JavaSourceFromString extends SimpleJavaFileObject {
  final String code;

  JavaSourceFromString(String name, String code) {
    super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
    this.code = code;
  }

  @Override
  public CharSequence getCharContent(boolean ignoreEncodingErrors) {
    return code;
  }
}
