/* -*- Mode: C; c-file-style: "gnu" -*-
   japhard.c -- Java readline based debugger.
   Created: Chris Toshok <toshok@hungry.com>, 30-Nov-1997
 */
/*
  This file is part of Japhar, the GNU Virtual Machine for Java Bytecodes.
  Japhar is a project of The Hungry Programmers, GNU, and OryxSoft.

  Copyright (C) 1997, 1998 The Hungry Programmers

  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 2 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, write to the Free Software
  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/
#include "config.h"

#include "jvmdi.h"
#include "gc.h"
#include "log.h"
#include "cmds.h"
#include "break.h"
#include "classpath.h"
#include "stack.h"
#include "readline/readline.h"
#include "readline/history.h"

#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <setjmp.h>

sigjmp_buf loop_jmp;
sigjmp_buf resume_jmp;

/* The JNIEnv* for the debugger thread. */
JNIEnv *debugger_env;

/* The VM we're using in the debugger. */
JavaVM *debugger_vm;

/* The class we're debugging. */
jclass class_to_debug;

/* Whether the user has issued the 'run' command. */
jboolean debugger_running = JNI_FALSE;

extern jthread current_thread;
extern jframeID current_frame;
extern jframeID break_frame;

extern int in_breakpoint;
extern int current_bpnum;

static JavaVMOption options[1024]; /* hope noone ever needs more than
                                      1024 command line options :) */
static int num_options = 0;
static char *clazzname = NULL;

static void usage(char *prog)
{
  fprintf (stderr, "Usage:  %s [args] [class to interpret]\n", prog);
  fprintf (stderr, "        example \"japhard Hello2 ...\"\n");
  fprintf (stderr, "        %s args:\n", prog);
  fprintf (stderr, "          -emacs    output control strings for emacs\n");
  fprintf (stderr, "          -asm      force the debugger to start in asm mode.\n");
  fprintf (stderr, "          -javapath <dir> Add <dir> to search path for .java files.\n");
  exit(1);
}

static void banner()
{
  fprintf (stderr, 
   "JAPHARD is free software and you are welcome to distribute copies of it\n"
   " under certain conditions; type \"show copying\" to see the conditions.\n"
   "There is absolutely no warranty for JAPHARD; type \"show warranty\" for details.\n"
   "JAPHARD %s, Copyright 1997, 1998 The Hungry Programmers, Inc.\n", VERSION);
}

/* Strip whitespace from the start and end of STRING.  Return a pointer 
   into STRING. */
static char *
stripwhite (char *string)
{
  register char *s, *t;

  for (s = string; whitespace(*s); s++)
    ;

  if (*s == 0)
    return s;

  t = s + strlen(s) - 1;

  while(t > s && whitespace(*t))
    t --;
  *++t = '\0';

  return s;
}

/* Generator function for command completion.  STATE lets us know
   whether to start from scratch; without any state (STATE == 0), then
   we start at the top of the list. */
static char *
command_generator(char *text, int state)
{
  static int list_index, len;
  char *name;

  /* if this is a new word to complete, initialize now.  This includes
     saving the length of TEXT for efficiency, and initializing the
     index variable to 0 */
  if (!state)
    {
      list_index = 0;
      len = strlen(text);
    }

  /* Return the next name which partially matches from the command list. */
  while ((name = commands[list_index].name))
    {
      list_index ++;
      if (strncmp(name, text, len) == 0)
	return (strdup(name));
    }

  /* if no names matched, then return NULL */
  return ((char*)NULL);
}

/* Attempt to complete on the contents of TEXT.  START and END show
   the region of TEXT that contains the word to complete.  We can use
   the entire line in case we want to do some simple parsing.  Return
   the array of matches, or NULL if there aren't any. */
static char**
japhard_completion( char *text, int start, int end)
{
  char **matches;

  matches = (char**)NULL;

  /* If this word is at the start of the line, then it is a command to
     complete.  Otherwise, it is the name of a file in the current
     directory. */
  if (start == 0)
    matches = completion_matches(text, command_generator);

  return matches;
}

/* Tell the GNU Readline library how to complete.  We want to try and
   complete on command names if this is the first word in the line, or
   on filenames if not. */
static void
initialize_readline()
{
  /* Alloc conditional parsing of the ~/.inputrc file. */
  rl_readline_name = "Japhard";

  /* Tell the completer that we want a crack first. */
  rl_attempted_completion_function = (CPPFunction*)japhard_completion;
}

/* Look up NAME as the name of a command, and return a pointer to that
   command.  Return a NULL pointer if NAME isn't a command name. */
static COMMAND *
find_command(char *name)
{
  register int i;

  for (i = 0; commands[i].name; i ++)
    if (strcmp(name, commands[i].name) == 0)
      return (&commands[i]);

  return ((COMMAND*)NULL);
}

/* execute a command line. */
static int
execute_line(char *line)
{
  register int i;
  COMMAND *command;
  char *word;

  /* Isolate the command word. */
  i = 0;
  while (line[i] && whitespace (line[i]))
    i++;
  word = line + i;

  while (line[i] && !whitespace(line[i]))
    i ++;

  if (line[i])
    line[i++] = '\0';

  command = find_command(word);

  if (!command)
    {
      fprintf (stderr, "%s: no such command.\n", word);
      return -1;
    }

  /* get the argument to command, if any. */
  while (whitespace (line[i]))
    i ++;

  word = line + i;

  /* Call the function. */
  return ((*(command->func))(word));
}

static void
japhard_jvmdi_event_hook(JNIEnv *env, JVMDI_Event *event)
{
  switch (event->kind)
    {
    case JVMDI_EVENT_SINGLE_STEP:
      break;
    case JVMDI_EVENT_BREAKPOINT:
      {
	jint threads_count;
	jthread *threads;

	/* stop all the interpreter threads */
	if (JVMDI_GetAllThreads(debugger_env,
				&threads_count,
				&threads) == JVMDI_ERROR_NONE)
	  {
	    int i;
	    for (i = 0; i < threads_count; i ++)
	      JVMDI_SuspendThread(debugger_env, threads[i]);

	    free(threads);

	    current_thread = event->u.breakpoint.thread;
	    if (JVMDI_GetCurrentFrame(debugger_env,
				      current_thread,
				      &break_frame) != JVMDI_ERROR_NONE)
	      {
		fprintf(stderr, "uh oh... GetCurrentFrame failed.\n");
	      }
	    else
	      {
		in_breakpoint = 1;
		
		current_frame = break_frame;
		current_bpnum = BREAK_getBreakpointNum(event->u.breakpoint.clazz,
						       event->u.breakpoint.method,
						       event->u.breakpoint.location);
		
		print_current_frame();
	      }

	    if (!sigsetjmp(resume_jmp, ~0))
	      siglongjmp(loop_jmp, 1);
	  }
	break;
      }
    case JVMDI_EVENT_FRAME_POP:
      break;
    case JVMDI_EVENT_VM_DEATH:
      break;
    }
}

static void
parse_command_line (int argc, char **argv)
{
  int i;
  char *classpath = NULL;

  options[num_options++].optionString =
    "-Djava.library.path=/usr/local/japhar/lib" /* XXX */;
  options[num_options++].optionString =
    "-Dsun.boot.library.path=/usr/local/japhar/lib" /* XXX */;
  options[num_options++].optionString =
    "-Djava.awt.graphicsenv=sun/awt/X11GraphicsEnvironment" /* XXX */;

  for (i = 1; i < argc; i ++)
    {
      char *arg;
      jboolean is_switch = JNI_FALSE;
      int two_dashes = 0;

      arg = argv[i];

      /* handle both -arg and --arg */
      if (arg[0] == '-') { arg++; is_switch = JNI_TRUE; }
      if (arg[0] == '-') { arg++; two_dashes = 1; }
      
      if (!strcmp(arg, "version"))
	{
          printf ("GNU JapharD %s <URL:http://www.japhar.org/>\n"
                  "Copyright (C) 1998 The Hungry Programmers\n"
		  "\n"
		  "Send mail requests to japhar@japhar.org\n"
		  "\n"
                  "Japhar comes with ABSOLUTELY NO WARRANTY.\n"
		  "You may redistribute copies of Japhar under the terms of the GNU\n"
		  "Library General Public License.  For more information about these matters, see\n"
		  "the files named COPYING.\n",
		  VERSION);
          exit (0);
	}
      else if (!strcmp(arg, "help"))
	{
	  usage(argv[0]);
	  exit(0);
	}
      else if (!strncmp(arg, "classpath=", 10 /*strlen(classpath=)*/)) 
	{
	  classpath = arg + 10 /* strlen(classpath=) */;
	}
      else if (!strncmp(arg, "verbose:", 8 /* strlen(verbose:) */))
	{
	  options[num_options++].optionString = argv[i] + two_dashes;
	}
      else if (arg[0] == 'D')
	{
	  options[num_options++].optionString = argv[i] + two_dashes;
	}
      else if (arg[0] == 'X')
	{
#if 0
	  if (!strcmp(arg[0], "Xjdk-1.1"))
	    jni_version = JNI_VERSION_1_1;
	  else if (!strcmp(arg[0], "Xjdk-1.2"))
	    jni_version = JNI_VERSION_1_2;
#endif

	  options[num_options++].optionString = argv[i] + two_dashes;
	}
      else
	{
	  if (is_switch)
	    {
	      fprintf (stderr, "unrecognized option: %s\n", argv[i]);
	      usage(argv[0]);
	      exit(0);
	    }
	  else
	    {
	      clazzname = argv[i];
	      break;
	    }
	}
    }

  {
    char *cp_value;
    char *new_cp_option;

    if (classpath)
      cp_value = classpath;
    else
      {
	char *classpath_env = getenv("CLASSPATH");
	char *sys_classpath = CLASSPATH_getSystemClasspath();

	if (classpath_env)
	  {
	    cp_value = (char*)malloc(strlen(classpath_env)
				     + strlen(sys_classpath)
				     + 1 /* : */
				     + 1 /* \0 */);
	    strcpy(cp_value, classpath_env);
	    strcat(cp_value, ":");
	    strcat(cp_value, sys_classpath);
	  }
	else
	  {
	    cp_value = strdup(sys_classpath);
	  }

	free(sys_classpath);
      }
    
    new_cp_option = (char*)malloc(strlen("-Djava.class.path=") 
				  + strlen(cp_value) + 2 /* for ":." */ + 1);
    strcpy(new_cp_option, "-Djava.class.path=");
    strcat(new_cp_option, cp_value);
    strcat(new_cp_option, ":.");
    
    options[num_options++].optionString = new_cp_option;
    free(cp_value);
  } 

  if (!clazzname)
    { 
      usage (argv[0]);
      exit (0);
    }
}

static void
japhard_exit(int code)
{
  printf ("java program exitted with status %d.\n", code);
  debugger_running = 0;
}

static void
japhard_abort()
{
  printf ("Fatal VM error.  exitting japhard.\n");
  exit(1);
}

int
main(int argc,
     char **argv)
{
  JavaVMInitArgs vm_args;

  banner();

  LOG_init();

  /* bind our completer. */
  initialize_readline();

  vm_args.version = JNI_VERSION_1_2;

  /* parse the command line arguments, extracting the clazzname */
  parse_command_line(argc, argv);

  options[ num_options ].optionString = "exit";
  options[ num_options++ ].extraInfo = japhard_exit;

  options[ num_options ].optionString = "abort";
  options[ num_options++ ].extraInfo = japhard_abort;

  vm_args.options = options;
  vm_args.nOptions = num_options;
  vm_args.ignoreUnrecognized = JNI_FALSE;

  /*
  ** load and initialize a JavaVM, return a JNI interface pointer in
  ** env.
  */
  JNI_CreateJavaVM(&debugger_vm, &debugger_env, &vm_args);

  JVMDI_SetEventHook(debugger_env, japhard_jvmdi_event_hook);

  class_to_debug = (*debugger_env)->FindClass(debugger_env, clazzname);

  if ((*debugger_env)->ExceptionOccurred(debugger_env))
    (*debugger_env)->FatalError(debugger_env, "Couldn't load class");

  /*  GC_addRoot(debugger_vm, (GC_obj*)class_to_debug);*/

  while (1)
    {
      char *s, *line;

      sigsetjmp(loop_jmp, ~0);

      line = readline("(japhard) ");

      if (!line)
	break;

      /* Remove leading and trailing whitespace from the line.  Then,
	 if there is anything left, add it to the history list and
	 execute it. */
      s = stripwhite(line);

      if (*s)
	{
	  add_history(s);
	  execute_line(s);
	}

      free(line);
    }

  return 0;
}
