/**
 * Copyright (C) 2005 - 2017  Eric Van Dewoestine
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.eclim.plugin.jdt.command.launching;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import java.lang.reflect.Method;

import java.util.ArrayList;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.BuildLogger;
import org.apache.tools.ant.DefaultLogger;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;

import org.apache.tools.ant.taskdefs.ExecuteStreamHandler;
import org.apache.tools.ant.taskdefs.Java;
import org.apache.tools.ant.taskdefs.PumpStreamHandler;
import org.apache.tools.ant.taskdefs.Redirector;
import org.apache.tools.ant.taskdefs.StreamPumper;

import org.apache.tools.ant.taskdefs.condition.Os;

import org.apache.tools.ant.types.Commandline.Argument;
import org.apache.tools.ant.types.Environment.Variable;
import org.apache.tools.ant.types.Path;

import org.eclim.Services;

import org.eclim.annotation.Command;

import org.eclim.command.CommandLine;
import org.eclim.command.Options;

import org.eclim.plugin.core.command.AbstractCommand;

import org.eclim.plugin.core.util.ProjectUtils;

import org.eclim.plugin.jdt.util.ClasspathUtils;
import org.eclim.plugin.jdt.util.JavaUtils;

import org.eclim.util.StringUtils;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IncrementalProjectBuilder;

import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IPackageDeclaration;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;

import org.eclipse.jdt.core.compiler.IProblem;

import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;

import org.eclipse.jdt.launching.IVMInstall;
import org.eclipse.jdt.launching.JavaRuntime;

/**
 * Command to run the project's main class.
 *
 * @author Eric Van Dewoestine
 */
@Command(
  name = "java",
  options =
    "REQUIRED p project ARG," +
    "OPTIONAL d debug NOARG," +
    "OPTIONAL c classname ARG," +
    "OPTIONAL w workingdir ARG," +
    "OPTIONAL v vmargs ANY," +
    "OPTIONAL s sysprops ANY," +
    "OPTIONAL e envargs ANY," +
    "OPTIONAL a args ANY"
)
public class JavaCommand
  extends AbstractCommand
{
  private static final String WORKINGDIR_OPTION = "w";
  private static final String VMARGS_OPTION = "v";
  private static final String SYSPROPS_OPTION = "s";
  private static final String ENVARGS_OPTION = "e";

  @Override
  public Object execute(CommandLine commandLine)
    throws Exception
  {
    String projectName = commandLine.getValue(Options.PROJECT_OPTION);
    String mainClass = commandLine.getValue(Options.CLASSNAME_OPTION);
    boolean debug = commandLine.hasOption(Options.DEBUG_OPTION);
    String workingDir = commandLine.getValue(WORKINGDIR_OPTION);

    IProject project = ProjectUtils.getProject(projectName);
    project.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, null);

    IJavaProject javaProject = JavaUtils.getJavaProject(project);

    Project antProject = new Project();
    BuildLogger buildLogger = new DefaultLogger();
    buildLogger.setMessageOutputLevel(debug ? Project.MSG_DEBUG : Project.MSG_INFO);
    buildLogger.setOutputPrintStream(getContext().out);
    buildLogger.setErrorPrintStream(getContext().err);
    antProject.addBuildListener(buildLogger);
    antProject.setBasedir(ProjectUtils.getPath(project));
    antProject.setDefaultInputStream(System.in);

    if (mainClass == null){
      mainClass =
        getPreferences().getValue(project, "org.eclim.java.run.mainclass");
    }

    if (mainClass == null ||
        mainClass.trim().equals(StringUtils.EMPTY) ||
        mainClass.trim().equals("none"))
    {
      // first try to locate a main method.
      mainClass = findMainClass(javaProject);
      if (mainClass == null){
        throw new RuntimeException(Services.getMessage(
              "setting.not.set", "org.eclim.java.run.mainclass"));
      }
    }

    if (mainClass.endsWith(".java") || mainClass.endsWith(".class")){
      mainClass = mainClass.substring(0, mainClass.lastIndexOf('.'));
    }

    // validate that the main class doesn't contain errors
    IType type = javaProject.findType(mainClass);
    if (type != null){
      ICompilationUnit src = type.getCompilationUnit();
      if (src != null){
        IProblem[] problems = JavaUtils.getProblems(src);
        for (IProblem problem : problems){
          if (problem.isError()){
            println(Services.getMessage("src.contains.errors"));
            return null;
          }
        }
      }
    }

    Java java = new MyJava();
    java.setTaskName("java");
    java.setProject(antProject);
    java.setClassname(mainClass);
    java.setFork(true);

    // use the project configured jvm if possible
    IVMInstall jvm = JavaRuntime.getVMInstall(javaProject);
    if (jvm != null){
      String path = jvm.getInstallLocation() + "/bin/java";
      if (new File(path).exists()){
        java.setJvm(path);
      }
    }

    if (workingDir != null){
      java.setDir(new File(workingDir));
    }

    // construct classpath
    Path classpath = new Path(antProject);
    String[] paths = ClasspathUtils.getClasspath(javaProject);
    for (String path : paths){
      Path.PathElement pe = classpath.createPathElement();
      pe.setPath(path);
    }

    java.setClasspath(classpath);

    // add default vm args
    String[] defaultArgs =
      getPreferences().getArrayValue(project, "org.eclim.java.run.jvmargs");
    for(String vmarg : defaultArgs){
      if (!vmarg.startsWith("-")){
        continue;
      }
      Argument a = java.createJvmarg();
      a.setValue(vmarg);
    }

    // add any supplied vm args
    String[] vmargs = commandLine.getValues(VMARGS_OPTION);
    if (vmargs != null && vmargs.length > 0){
      for(String vmarg : vmargs){
        if (!vmarg.startsWith("-")){
          continue;
        }
        Argument a = java.createJvmarg();
        a.setValue(vmarg);
      }
    }

    // add any supplied system properties
    String[] props = commandLine.getValues(SYSPROPS_OPTION);
    if (props != null && props.length > 0){
      for(String prop : props){
        String[] sysprop = StringUtils.split(prop, "=", 2);
        if (sysprop.length != 2){
          continue;
        }
        if (sysprop[0].startsWith("-D")){
          sysprop[0] = sysprop[0].substring(2);
        }
        Variable var = new Variable();
        var.setKey(sysprop[0]);
        var.setValue(sysprop[1]);
        java.addSysproperty(var);
      }
    }

    // add any env vars
    String[] envs = commandLine.getValues(ENVARGS_OPTION);
    if (envs != null && envs.length > 0){
      for(String env : envs){
        String[] envvar = StringUtils.split(env, "=", 2);
        if (envvar.length != 2){
          continue;
        }
        Variable var = new Variable();
        var.setKey(envvar[0]);
        var.setValue(envvar[1]);
        java.addEnv(var);
      }
    }

    // add any supplied command line args
    String[] args = commandLine.getValues(Options.ARGS_OPTION);
    if (args != null && args.length > 0){
      for(String arg : args){
        Argument a = java.createArg();
        a.setValue(arg);
      }
    }

    java.execute();

    return null;
  }

  private String findMainClass(IJavaProject javaProject)
    throws Exception
  {
    ArrayList<IJavaElement> srcs = new ArrayList<IJavaElement>();
    for(IClasspathEntry entry : javaProject.getResolvedClasspath(true)){
      if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE){
        for(IPackageFragmentRoot root : javaProject.findPackageFragmentRoots(entry)){
          srcs.add(root);
        }
      }
    }

    final ArrayList<IMethod> methods = new ArrayList<IMethod>();
    int context = IJavaSearchConstants.DECLARATIONS;
    int type = IJavaSearchConstants.METHOD;
    int matchType = SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE;
    IJavaSearchScope scope = SearchEngine.createJavaSearchScope(
        srcs.toArray(new IJavaElement[srcs.size()]));
    SearchPattern pattern =
      SearchPattern.createPattern("main(String[])", type, context, matchType);
    SearchRequestor requestor = new SearchRequestor(){
      public void acceptSearchMatch(SearchMatch match){
        if(match.getAccuracy() != SearchMatch.A_ACCURATE){
          return;
        }

        try{
          IMethod method = (IMethod)match.getElement();
          String[] params = method.getParameterTypes();
          if (params.length != 1){
            return;
          }

          if (!Signature.SIG_VOID.equals(method.getReturnType())){
            return;
          }

          int flags = method.getFlags();
          if (!Flags.isPublic(flags) || !Flags.isStatic(flags)){
            return;
          }

          methods.add(method);
        }catch(JavaModelException e){
          // ignore
        }
      }
    };

    SearchEngine engine = new SearchEngine();
    SearchParticipant[] participants =
      new SearchParticipant[]{SearchEngine.getDefaultSearchParticipant()};
    engine.search(pattern, participants, scope, requestor, null);

    // if we found only 1 result, we can use it.
    if (methods.size() == 1){
      IMethod method = methods.get(0);
      ICompilationUnit cu = method.getCompilationUnit();
      IPackageDeclaration[] packages = cu.getPackageDeclarations();
      if (packages != null && packages.length > 0){
        return packages[0].getElementName() + "." + cu.getElementName();
      }
      return cu.getElementName();
    }
    return null;
  }

  /* All of this is to ensure that System.out calls by the running class are
   * flushed immediatly to the console for things like user input prompts. */

  private class MyJava
    extends Java
  {
    public MyJava()
    {
      super();
      this.redirector = new MyRedirector(this);
    }
  }

  private class MyRedirector
    extends Redirector
  {
    public MyRedirector(Task task)
    {
      super(task);
    }

    @Override
    public synchronized ExecuteStreamHandler createHandler()
      throws BuildException
    {
      return new MyPumpStreamHandler();
    }

    @Override
    public synchronized void complete()
      throws IOException
    {
      getContext().out.flush();
      getContext().err.flush();
    }
  }

  private class MyPumpStreamHandler
    extends PumpStreamHandler
  {
    public MyPumpStreamHandler()
    {
      super(new FlushingOutputStream(
            getContext().out), getContext().err, getContext().in, true);
    }

    protected Thread createPump(
        InputStream is, OutputStream os,
        boolean closeWhenExhausted, boolean nonBlockingIO)
    {
      Thread pump = super.createPump(is, os, closeWhenExhausted, nonBlockingIO);
      try{
        Method getPumper = pump.getClass().getDeclaredMethod("getPumper");
        getPumper.setAccessible(true);
        StreamPumper pumper = (StreamPumper)getPumper.invoke(pump);
        Method setAutoflush = pumper.getClass()
          .getDeclaredMethod("setAutoflush", Boolean.TYPE);
        setAutoflush.setAccessible(true);
        setAutoflush.invoke(pumper, Boolean.TRUE);
      }catch(Exception e){
        throw new RuntimeException(e);
      }
      return pump;
    }
  }

  private class FlushingOutputStream
    extends OutputStream
  {
    private OutputStream out;

    public FlushingOutputStream(OutputStream out)
    {
      this.out = out;
    }

    @Override
    public void write(int b)
      throws IOException
    {
      out.write(b);
      out.flush();
    }

    @Override
    public void write(byte[] b)
      throws IOException
    {
      out.write(b);
      out.flush();
    }

    @Override
    public void write(byte[] b, int off, int len)
      throws IOException
    {
      out.write(b, off, len);
      out.flush();
    }
  }
}
