/* -*- Mode: C; c-file-style: "gnu" -*-
   exceptions.c -- functions for dealing with exceptions in the interpreter.
   Created: Chris Toshok <toshok@hungry.com>
 */
/*
  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, 1999 The Hungry Programmers

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Library General Public
  License as published by the Free Software Foundation; either
  version 2 of the License, or (at your option) any later version.

  This library 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
  Library General Public License for more details.

  You should have received a copy of the GNU Library General Public
  License along with this library; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "jniint.h"
#include "native-threads.h"
#include "objects.h"
#include "interp.h"
#include "log.h"
#include "ClazzFile.h"
#include "exceptions.h"
#include "resolve.h"

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>

#include "op_stack.h"

#define MYLOG "Exceptions"

/*
 * Map program counter to line number.  Requires the LineNumberBlock
 * to be sorted by increasing pc values.
 */
int
method_pc_to_line_number(JNIEnv *env, MethodStruct *method, u2 pc)
{
  int i = 0;
  assert(NULL != env);
  assert(NULL != method);
  assert(sizeof(u2) < sizeof(int)); /* Need space to return -1 on errors */
  
  if (method->line_numbers)
    {
      for (i = 0; i < method->num_line_number_blocks; i++)
	{
	  if (pc >= method->line_numbers[i].start_pc)
	    return method->line_numbers[i].line_number;
	}
    }

  /* XXX Unknown line number */
  return -1;
}

void
print_exception_backtrace(JNIEnv *env, japhar_obj throwable_ref)
{
  HungryJNIEnv *henv = (HungryJNIEnv*)env;
  jclass throwable_class;
  jmethodID getMessage;
  jstring msg = NULL;
  jclass exception_class = (*env)->GetObjectClass(env, throwable_ref);
  ClazzFile *exception_cf = jclass_to_clazzfile(env, exception_class);
  char *exceptionname = getClassName(env, exception_cf);
  jthrowable exception_cache;
  ExceptionInfo *exc_info = NSA_GetNativeState(throwable_ref);
  BackTraceLevel *level;
  
  throwable_class = (*env)->FindClass(env, "java/lang/Throwable");

  getMessage = (*env)->GetMethodID(env, throwable_class,
				   "getMessage",
				   "()Ljava/lang/String;");

  /*
   * Cache exception and make sure the runtime don't think the call to
   * getMessage failed
   */
  exception_cache = henv->_exception;
  henv->_exception = NULL;

  msg = (*env)->CallObjectMethod(env, throwable_ref, getMessage);

  /* Don't know what to do if the call fails.  Die a horrible death? */
  assert(NULL == henv->_exception);
  henv->_exception = exception_cache;

  if (msg)
    {
      const jbyte *msg_bytes = (*env)->GetStringUTFChars(env, msg, NULL);

      fprintf (stderr, "%s (%s)\n", exceptionname, msg_bytes);

      (*env)->ReleaseStringUTFChars(env, msg, msg_bytes);
    }
  else
    fprintf (stderr, "%s\n", exceptionname);

  level = exc_info->head;
  while (level)
    {
      int line_number = method_pc_to_line_number(env, level->method,
						 level->pc);
      
      if (level->method->access_flags & ACC_NATIVE)
	fprintf (stderr, "        in %s.%s(%s%snative method)\n",
		 level->classname,
		 level->method->name,
		 level->filename ? level->filename : "",
		 level->filename ? ", " : "");
      else if (line_number == -1)
	fprintf (stderr, "        in %s.%s(%s%spc = %d)\n",
		 level->classname,
		 level->method->name,
		 level->filename ? level->filename : "",
		 level->filename ? ", " : "",
		 level->pc);
      else
	fprintf (stderr, "        at %s.%s(%s%s%d, pc = %d)\n",
		 level->classname,
		 level->method->name,
		 level->filename ? level->filename : "",
		 level->filename ? ":" : "line ",
		 line_number,
 		 level->pc);

      level = level->next;
    }
}

void
toplevel_exception_handler(JNIEnv *env,
			   japhar_obj throwable_ref)
{
  /* clear out the existing exception so we don't throw repeatedly. */
  (*env)->ExceptionClear(env);

  print_exception_backtrace(env, throwable_ref);
}

void
cleanup_exception(JNIEnv *env, japhar_obj throwable_ref)
{
  ExceptionInfo *exc_info = NSA_GetNativeState(throwable_ref);
  BackTraceLevel *level;

  if (exc_info == NULL) return;

  level = exc_info->head;
  while (level)
    {
      BackTraceLevel *next_level;
      free(level->classname);

      next_level = level->next;
      free(level);
      level = next_level;
    }

  free(exc_info);
  NSA_SetNativeState(throwable_ref, NULL);
}

static void
add_level_for_frame(JNIEnv *env, japhar_obj throwable_ref,
		    StackFrame *throw_frame)
{
  ExceptionInfo *exc_info = NSA_GetNativeState(throwable_ref);
  BackTraceLevel *new_level = (BackTraceLevel*)malloc(sizeof(BackTraceLevel));
  
  new_level->classname = strdup(getClassName(env, METHOD(throw_frame)->clazz));
  new_level->filename = METHOD(throw_frame)->clazz->source_filename;
  new_level->method = METHOD(throw_frame);
  new_level->pc = PC(throw_frame);
  new_level->next = NULL;
  new_level->prev = NULL;
  
  /* link the new level into the list of levels */
  if (exc_info->tail)
    {
      new_level->prev = exc_info->tail;
      exc_info->tail->next = new_level;
      exc_info->tail = new_level;
    }
  else
    {
      exc_info->head =
	exc_info->tail = new_level;
    }
}

static jboolean
is_throwable(JNIEnv *env, ClazzFile *cf)
{
  static ClazzFile *throwable_clazz = NULL;

  if (!throwable_clazz) throwable_clazz = find_class(env, "java/lang/Throwable");

  while (cf)
    {
      if (cf == throwable_clazz)
	return JNI_TRUE;
      else
	cf = getSuperClass(env, cf);
    }

  return JNI_FALSE;
}

/*
** This method is kinda weird.  if we start at the topframe, recording
** everything, we end up with a stack trace that looks like this:
**
** java/lang/RuntimeException (Hi there xtoph)
**   in java/lang/Throwable.fillInStackTrace()Ljava/lang/Throwable;(Throwable.java, pc = 20432)
**   at java/lang/Throwable.<init>(Ljava/lang/String;)V(Throwable.java:93, pc = 8)
**   at java/lang/Exception.<init>(Ljava/lang/String;)V(Exception.java:42, pc = 5)
**   at java/lang/RuntimeException.<init>(Ljava/lang/String;)V(RuntimeException.java:47, pc = 5)
**   at Foo.fweep()V(Foo.java:4, pc = 9)
**   at Foo.bweep()V(Foo.java:8, pc = 3)
**   at Foo.main([Ljava/lang/String;)V(Foo.java:17, pc = 3)
**
** What we *want* is something that looks like this:
**
** java.lang.RuntimeException: Hi there xtoph
**         at Foo.fweep(Foo.java:4)
**         at Foo.bweep(Foo.java:9)
**         at Foo.main(Foo.java:18)
**
** so, we need to bump back up the stack until we get to a frame
** that's not the <init> method of something subclasses from
** java/lang/Throwable.
*/
void
fill_in_backtrace_from_stack(JNIEnv *env,
			     japhar_obj throwable_ref)
{
  HungryJNIEnv *henv = (HungryJNIEnv*)env;
  StackFrame *frame = TOPFRAME(henv->_java_info);
  int found_first_non_init_frame = 0;

  /* first we skip the fillInStackTrace frame that is always present */
  frame = get_frame_parent(frame);

  while (frame < henv->_java_info->stack_highwater)
    {
      if (!found_first_non_init_frame)
	{
	  if (!strcmp(METHOD(frame)->name, "<init>")
	      && is_throwable(env, METHOD(frame)->clazz))
	    {
	      frame = get_frame_parent(frame);
	      continue;
	    }
	  else
	    {
	      found_first_non_init_frame = 1;
	    }
	  /* fall through */
	}

      add_level_for_frame(env, throwable_ref, frame);

      frame = get_frame_parent(frame);
    }
}

void
throw_exception(JNIEnv *env, japhar_obj throwable_ref, StackFrame *f)
{
  StackFrame *throw_frame, *new_throw_frame;
#ifdef LOGGING
  jclass throwable_class = (*env)->GetObjectClass(env, throwable_ref);
  ClazzFile *throwable_cf = jclass_to_clazzfile(env, throwable_class);
#endif

  assert(throwable_ref);
  if (!throwable_ref) return;

  JAVARLOG0(MYLOG, 1, "in throw_exception()\n");

  throw_frame = f;

  JAVARLOG4(MYLOG, 1, 
	    "Exception %s thrown from %s.%s - at pc %d\n",
	    getClassName(ENV(throw_frame), throwable_cf),
	    getClassName(ENV(throw_frame), METHOD(throw_frame)->clazz),
	    METHOD(throw_frame)->name,
	    PC(throw_frame));

  while (throw_frame != THREAD_INFO(f)->stack_highwater)
    {
      if (throw_frame->flags & FRAME_NATIVE)
	{
	  JAVARLOG2(MYLOG, 3, "... we hit a native frame (%s.%s)\n",
		    getClassName(ENV(throw_frame), METHOD(throw_frame)->clazz),
	            METHOD(throw_frame)->name);

	  /* if we hit a native frame, we just return.
	     the interpreter loop will return to it's caller
	     if the exception hasn't been handled here. */
	  
	  return;
	}
      else
	{
	  int i;

	  JAVARLOG3(MYLOG, 3, "... we're at a java frame (%s.%s pc %d)\n",
		    getClassName(ENV(throw_frame), METHOD(throw_frame)->clazz),
		    METHOD(throw_frame)->name,
		    PC(throw_frame));

	  for (i = 0; i < NUMEXCEPTIONS(throw_frame); i ++)
	    {
	      ClazzFile *catch_class;
	      
	      if (PC(throw_frame) < EXCEPTIONS(throw_frame)[i].start_pc
		  || PC(throw_frame) > EXCEPTIONS(throw_frame)[i].end_pc)
		continue;

	      catch_class = ExceptionBlock_getHandlerClazz(ENV(throw_frame),
							   METHOD(throw_frame)->clazz,
							   &(EXCEPTIONS(throw_frame)[i]));
	      
	      if (!is_instance_of(env, throwable_ref, catch_class))
		continue;

	      /* we found a match.  reset the pc, push the exception
		 on the stack and return, but only after we mark where
		 the exception was thrown from. */

	      PC(throw_frame) = EXCEPTIONS(throw_frame)[i].handler_pc;
	      
	      OPSTACK(throw_frame)->stack_top = OPSTACK_TOP(throw_frame);

	      op_stack_push_object(env, OPSTACK(throw_frame), throwable_ref);
	      
	      (*env)->ExceptionClear(env);

	      JAVARLOG4(MYLOG, 1, 
			"Exception %s caught by %s.%s - at pc %d\n",
			getClassName(ENV(throw_frame), catch_class),
			getClassName(ENV(throw_frame), METHOD(throw_frame)->clazz),
			METHOD(throw_frame)->name,
			PC(throw_frame));
	      
	      return;
	    }

	  /* if we didn't find a match, pop the stack 
	     frame and do it again. */
	  new_throw_frame = PARENT(throw_frame);
	  pop_frame(THREAD_INFO(throw_frame));
	  throw_frame = new_throw_frame;
	}
    }

  /* we fall off here, and will either unwind to the thread's start
     function, which will return and clean up the thread, or it will
     return to the toplevel JNI call that started this thread's
     interpreter loop. */
}

void
throw_Exception(JNIEnv *env,
		const char *exception_name,
		const char *msg)
{
  jclass exception = (*env)->FindClass(env, exception_name);
  
  if (!exception)
    {
      (*env)->FatalError(env, "Unable to raise exception.");
    }

  /* NULL == msg means no message */
  (*env)->ThrowNew(env, exception, msg);
}
