/*
 * Copyright (c) 1998-2006 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source 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.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.java;

import java.io.PrintWriter;
import java.io.InputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;

import java.util.WeakHashMap;
import java.util.HashMap;

import java.util.logging.Logger;
import java.util.logging.Level;

import com.caucho.bytecode.ByteCodeParser;
import com.caucho.bytecode.JavaClass;
import com.caucho.bytecode.Attribute;
import com.caucho.bytecode.OpaqueAttribute;

import com.caucho.loader.SimpleLoader;

import com.caucho.util.Log;
import com.caucho.util.L10N;
import com.caucho.util.CharBuffer;

import com.caucho.vfs.Vfs;
import com.caucho.vfs.ReadStream;

/**
 * Prints a stack trace using JSR-45
 */
public class ScriptStackTrace {
  private static final L10N L = new L10N(ScriptStackTrace.class);
  private static final Logger log = Log.open(ScriptStackTrace.class);
  
  private static WeakHashMap<Class,LineMap> _scriptMap =
    new WeakHashMap<Class,LineMap>();
  
  /**
   * Filter a stack trace, replacing names.
   */
  public static void printStackTrace(Throwable e, PrintWriter out)
  {
    StackTraceElement lastHead = null;

    ClassLoader loader = SimpleLoader.create(WorkDir.getLocalWorkDir());
    
    while (true) {
      if (e.getMessage() != null)
	out.println(e.getClass().getName() + ": " + e.getMessage());
      else
	out.println(e.getClass().getName());

      StackTraceElement []trace = e.getStackTrace();
      StackTraceElement nextHead = trace.length > 0 ? trace[0] : null;

      for (int i = 0; i < trace.length; i++) {
	if (trace[i].equals(lastHead))
	  break;
	
	out.print("\tat ");

	printStackTraceElement(trace[i], out, loader);
      }

      lastHead = nextHead;

      Throwable cause = e.getCause();

      if (cause != null) {
	out.print("Caused by: ");
	e = cause;
      }
      else
	break;
    }
  }

  /**
   * Prints a single stack trace element.
   */
  private static void printStackTraceElement(StackTraceElement trace,
					     PrintWriter out,
					     ClassLoader loader)
  {
    try {
      LineMap map = getScriptLineMap(trace.getClassName(), loader);

      if (map != null) {
	LineMap.Line line = map.getLine(trace.getLineNumber());
	if (line != null) {
	  out.print(trace.getClassName() + "." + trace.getMethodName());
	  out.print("(" + line.getSourceFilename() + ":");
	  out.println(line.getSourceLine(trace.getLineNumber()) + ")");
	  return;
	}
      }
    } catch (Throwable e) {
    }
    
    out.println(trace);
  }

  /**
   * Loads the local line map for a class.
   */
  public static LineMap getScriptLineMap(String className, ClassLoader loader)
  {
    try {
      Class cl = loader.loadClass(className);

      LineMap map = _scriptMap.get(cl);

      if (map == null) {
	map = loadScriptMap(cl);
	_scriptMap.put(cl, map);
      }

      return map;
    } catch (Throwable e) {
      return null;
    }
  }
  

  /**
   * Loads the script map for a class
   */
  private static LineMap loadScriptMap(Class cl)
  {
    ClassLoader loader = cl.getClassLoader();

    if (loader == null)
      return new LineMap(); // null map
    
    try {
      String pathName = cl.getName().replace('.', '/') + ".class";
      
      InputStream is = loader.getResourceAsStream(pathName);

      if (is == null)
	return null;
      
      try {
	JavaClass jClass = new ByteCodeParser().parse(is);

	Attribute attr = jClass.getAttribute("SourceDebugExtension");

	if (attr == null)
	  return new LineMap();
	else if (attr instanceof OpaqueAttribute) {
	  byte []value = ((OpaqueAttribute) attr).getValue();

	  ByteArrayInputStream bis = new ByteArrayInputStream(value);

	  ReadStream rs = Vfs.openRead(bis);
	  rs.setEncoding("UTF-8");

	  try {
	    return parseSmap(rs);
	  } finally {
	    rs.close();
	  }
	}
	else
	  throw new IllegalStateException(L.l("Expected opaque attribute at '{0}'",
					      attr));
      } finally {
	if (is != null)
	  is.close();
      }
    } catch (Throwable e) {
      log.log(Level.FINER, e.toString(), e);

      return new LineMap(); // null map
    }
  }

  /**
   * Parses the SMAP file.
   */
  private static LineMap parseSmap(ReadStream is)
    throws IOException
  {
    String smap = is.readln();

    if (! smap.equals("SMAP"))
      throw new IOException(L.l("Illegal header"));
      

    String outputFile = is.readln().trim();
    String defaultStratum = is.readln().trim();

    String stratum = defaultStratum;
    HashMap<String,String> fileMap = new HashMap<String,String>();

    LineMap lineMap = new LineMap(outputFile);
      
    loop:
    while (true) {
      int ch = is.read();

      if (ch < 0)
	break;

      if (ch != '*')
	throw new IOException(L.l("unexpected character '{0}'",
				  String.valueOf((char) ch)));

      int code = is.read();
      String value = is.readln();

      switch (code) {
      case 'E':
	break loop;

      case 'S':
	stratum = value.trim();
	break;

      case 'F':
	while ((ch = is.read()) > 0 && ch != '*') {
	  if (ch == '+') {
	    String first = is.readln().trim();
	    String second = is.readln().trim();

	    int p = first.indexOf(' ');
	    String key = first.substring(0, p);
	    String file = first.substring(p + 1).trim();

	    if (fileMap.size() == 0)
	      fileMap.put("", second);

	    fileMap.put(key, second);
	  }
	  else {
	    String first = is.readln().trim();
	  
	    int p = first.indexOf(' ');
	    String key = first.substring(0, p);
	    String file = first.substring(p + 1).trim();

	    if (fileMap.size() == 0)
	      fileMap.put("", file);

	    fileMap.put(key, file);
	  }
	}
	if (ch == '*')
	  is.unread();
	break;

      case 'L':
	while ((ch = is.read()) != '*' && ch > 0) {
	  is.unread();
	  
	  String line = is.readln().trim();

	  addMap(line, fileMap, lineMap);
	}
	if (ch == '*')
	  is.unread();
	break;
	
      default:
	while ((ch = is.read()) != '*') {
	  is.readln();
	}
	if (ch == '*')
	  is.unread();
	break;
      }
    }
    
    
    return lineMap;
  }

  private static void addMap(String line,
			     HashMap<String,String> fileMap,
			     LineMap lineMap)
  {
    int colon = line.indexOf(':');

    if (colon < 0)
      return;

    int hash = line.indexOf('#');
    int startLine = 0;
    String fileId = "";
    int repeatCount = 1;

    if (hash < 0)
      startLine = Integer.parseInt(line.substring(0, colon));
    else {
      startLine = Integer.parseInt(line.substring(0, hash));

      int comma = line.indexOf(',', hash);

      if (comma > 0 && comma < colon) {
	fileId = line.substring(hash + 1, comma).trim();
	repeatCount = Integer.parseInt(line.substring(comma + 1, colon));
      }
      else
	fileId = line.substring(hash + 1, colon).trim();
    }

    int outputLine = -1;
    int outputIncrement = 1;

    int comma = line.indexOf(',', colon);

    if (comma > 0) {
      outputLine = Integer.parseInt(line.substring(colon + 1, comma));
      outputIncrement = Integer.parseInt(line.substring(comma + 1));
    }
    else
      outputLine = Integer.parseInt(line.substring(colon + 1));

    String file = fileMap.get(fileId);

    lineMap.addLine(startLine, file, repeatCount, outputLine, outputIncrement);
  }
}
