/* 
 * 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 java.lang;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;

import org.jikesrvm.VM;
import org.jikesrvm.runtime.StackTrace;
import org.jikesrvm.scheduler.Scheduler;

import org.vmmagic.pragma.NoEscapes;

/**
 * This class must be implemented by the VM vendor, or the reference
 * implementation can be used if the documented natives are implemented.
 * 
 * This class is the superclass of all classes which can be thrown by the
 * virtual machine. The two direct subclasses represent recoverable exceptions
 * (Exception) and unrecoverable errors (Error). This class provides common
 * methods for accessing a string message which provides extra information about
 * the circumstances in which the throwable was created, and for filling in a
 * walkback (i.e. a record of the call stack at a particular point in time)
 * which can be printed later.
 * 
 * @see Error
 * @see Exception
 * @see RuntimeException
 */
public class Throwable implements java.io.Serializable {
    private static final long serialVersionUID = -3042686055658047285L;

    /** The stack trace for this throwable */
    private transient StackTrace vmStackTrace;

    /**
     * Zero length array of stack trace elements, returned when handling an OOM or
     * an unexpected error
     */
    private static final StackTraceElement[] zeroLengthStackTrace = new StackTraceElement[0];

    /**
     * The message provided when the exception was created.
     */
    private String detailMessage;

    /**
     * The cause of this Throwable. Null when there is no cause.
     */
    private Throwable cause = this;

    private StackTraceElement[] stackTrace;

    /**
     * Constructs a new instance of this class with its walkback filled in.
     */
    @NoEscapes
    public Throwable() {
        super();
        fillInStackTrace();
    }

    /**
     * Constructs a new instance of this class with its walkback and message
     * filled in.
     * 
     * @param detailMessage String The detail message for the exception.
     */
    public Throwable(String detailMessage) {
        this();
        this.detailMessage = detailMessage;
    }

    /**
     * Constructs a new instance of this class with its walkback, message and
     * cause filled in.
     * 
     * @param detailMessage String The detail message for the exception.
     * @param throwable The cause of this Throwable
     */
    public Throwable(String detailMessage, Throwable throwable) {
        this();
        this.detailMessage = detailMessage;
        cause = throwable;
    }

    /**
     * Constructs a new instance of this class with its walkback and cause
     * filled in.
     * 
     * @param throwable The cause of this Throwable
     */
    public Throwable(Throwable throwable) {
        this();
        this.detailMessage = throwable == null ? null : throwable.toString();
        cause = throwable;
    }

    /**
     * Record in the receiver a walkback from the point where this message was
     * sent. The message is public so that code which catches a throwable and
     * then <em>re-throws</em> it can adjust the walkback to represent the
     * location where the exception was re-thrown.
     * 
     * @return the receiver
     */
    public Throwable fillInStackTrace() {
      if (VM.fullyBooted) {
        if (Scheduler.getCurrentThread().getThreadForStackTrace().isGCThread()) {
          VM.sysWriteln("Exception in GC thread");
          Scheduler.dumpVirtualMachine();
        } else {
          try {
            vmStackTrace = new StackTrace();
          } catch (OutOfMemoryError oome) {
            // ignore
          } catch (Throwable t) {
            VM.sysFail("VMThrowable.fillInStackTrace(): Cannot fill in a stack trace; got a weird Throwable when I tried to");
          }
        }
      } else {
        VM.sysWriteln("Request to fill in stack trace before VM booted");
        Scheduler.dumpStack();
      }
      return this;
    }

    /**
     * Answers the extra information message which was provided when the
     * throwable was created. If no message was provided at creation time, then
     * answer null.
     * 
     * @return String The receiver's message.
     */
    public String getMessage() {
        return detailMessage;
    }

    /**
     * Answers the extra information message which was provided when the
     * throwable was created. If no message was provided at creation time, then
     * answer null. Subclasses may override this method to answer localized text
     * for the message.
     * 
     * @return String The receiver's message.
     */
    public String getLocalizedMessage() {
        return getMessage();
    }

    /**
     * This native must be implemented to use the reference implementation of
     * this class. The result of this native is cloned, and returned from the
     * public API getStackTrace().
     * 
     * Answers an array of StackTraceElement. Each StackTraceElement represents
     * a entry on the stack.
     * 
     * @return an array of StackTraceElement representing the stack
     */
    private StackTraceElement[] getStackTraceImpl() {
	if (vmStackTrace == null) {
	    return zeroLengthStackTrace;
	} else if (Scheduler.getCurrentThread().getThreadForStackTrace().isGCThread()) {
	    VM.sysWriteln("Throwable.getStackTrace called from GC thread: dumping stack using scheduler");
	    Scheduler.dumpStack();
	    return zeroLengthStackTrace;
	}

	StackTrace.Element[] vmElements;
	try {
	    vmElements = vmStackTrace.getStackTrace(this);
	} catch (Throwable t) {
	    VM.sysWriteln("Error calling StackTrace.getStackTrace: dumping stack using scheduler");
	    Scheduler.dumpStack();
	    return zeroLengthStackTrace;
	}
	if (vmElements == null) {
	    VM.sysWriteln("Error calling StackTrace.getStackTrace returned null");
	    Scheduler.dumpStack();
	    return zeroLengthStackTrace;
	}
	if (VM.fullyBooted) {
	    try {
		StackTraceElement[] elements = new StackTraceElement[vmElements.length];
		for (int i=0; i < vmElements.length; i++) {
		    StackTrace.Element vmElement = vmElements[i];
		    String fileName = vmElement.getFileName();
		    int lineNumber = vmElement.getLineNumber();
		    String className = vmElement.getClassName();
		    if (className == null) className = "";
		    String methodName = vmElement.getMethodName();
		    if (methodName == null) methodName = "";
		    boolean isNative = vmElement.isNative();
		    elements[i] = new StackTraceElement(className, methodName, fileName, lineNumber);
		}
		return elements;
	    } catch (Throwable t) {
		VM.sysWriteln("Error constructing StackTraceElements: dumping stack");
	    }
	} else {
	    VM.sysWriteln("Dumping stack using sysWrite in not fullyBooted VM");
	}
	for (StackTrace.Element vmElement : vmElements) {
	    if (vmElement == null) {
		VM.sysWriteln("Error stack trace with null entry");
		Scheduler.dumpStack();
		return zeroLengthStackTrace;
	    }
	    String fileName = vmElement.getFileName();
	    int lineNumber = vmElement.getLineNumber();
	    String className = vmElement.getClassName();
	    String methodName = vmElement.getMethodName();
	    VM.sysWrite("   at ");
	    if (className != "") {
		VM.sysWrite(className);
		VM.sysWrite(".");
	    }
	    VM.sysWrite(methodName);
	    if (fileName != null) {
		VM.sysWrite("(");
		VM.sysWrite(fileName);
		if (lineNumber > 0) {
		    VM.sysWrite(":");
		    VM.sysWrite(vmElement.getLineNumber());
		}
		VM.sysWrite(")");
	    }
	    VM.sysWriteln();
	}
	return zeroLengthStackTrace;
    }

    /**
     * Answers an array of StackTraceElement. Each StackTraceElement represents
     * a entry on the stack.
     * 
     * @return an array of StackTraceElement representing the stack
     */
    public StackTraceElement[] getStackTrace() {
        return getInternalStackTrace().clone();
    }

    /**
     * Sets the array of StackTraceElements. Each StackTraceElement represents a
     * entry on the stack. A copy of this array will be returned by
     * getStackTrace() and printed by printStackTrace().
     * 
     * @param trace The array of StackTraceElement
     */
    public void setStackTrace(StackTraceElement[] trace) {
        StackTraceElement[] newTrace = trace.clone();
        for (java.lang.StackTraceElement element : newTrace) {
            if (element == null) {
                throw new NullPointerException();
            }
        }
        stackTrace = newTrace;
    }

    /**
     * Outputs a printable representation of the receiver's walkback on the
     * System.err stream.
     */
    public void printStackTrace() {
        printStackTrace(System.err);
    }

    /**
     * Count the number of duplicate stack frames, starting from the end of the
     * stack.
     * 
     * @param currentStack a stack to compare
     * @param parentStack a stack to compare
     * 
     * @return the number of duplicate stack frames.
     */
    private static int countDuplicates(StackTraceElement[] currentStack,
            StackTraceElement[] parentStack) {
        int duplicates = 0;
        int parentIndex = parentStack.length;
        for (int i = currentStack.length; --i >= 0 && --parentIndex >= 0;) {
            StackTraceElement parentFrame = parentStack[parentIndex];
            if (parentFrame.equals(currentStack[i])) {
                duplicates++;
            } else {
                break;
            }
        }
        return duplicates;
    }

    /**
     * Answers an array of StackTraceElement. Each StackTraceElement represents
     * a entry on the stack. Cache the stack trace in the stackTrace field,
     * returning the cached field when it has already been initialized.
     * 
     * @return an array of StackTraceElement representing the stack
     */
    private StackTraceElement[] getInternalStackTrace() {
        if (stackTrace == null) {
            stackTrace = getStackTraceImpl();
        }
        return stackTrace;
    }

    /**
     * Outputs a printable representation of the receiver's walkback on the
     * stream specified by the argument.
     * 
     * @param err PrintStream The stream to write the walkback on.
     */
    public void printStackTrace(PrintStream err) {
        err.println(toString());
        // Don't use getStackTrace() as it calls clone()
        // Get stackTrace, in case stackTrace is reassigned
        StackTraceElement[] stack = getInternalStackTrace();
        for (java.lang.StackTraceElement element : stack) {
            err.println("\tat " + element);
        }

        StackTraceElement[] parentStack = stack;
        Throwable throwable = getCause();
        while (throwable != null) {
            err.print("Caused by: ");
            err.println(throwable);
            StackTraceElement[] currentStack = throwable.getInternalStackTrace();
            int duplicates = countDuplicates(currentStack, parentStack);
            for (int i = 0; i < currentStack.length - duplicates; i++) {
                err.println("\tat " + currentStack[i]);
            }
            if (duplicates > 0) {
                err.println("\t... " + duplicates + " more");
            }
            parentStack = currentStack;
            throwable = throwable.getCause();
        }
    }

    /**
     * Outputs a printable representation of the receiver's walkback on the
     * writer specified by the argument.
     * 
     * @param err PrintWriter The writer to write the walkback on.
     */
    public void printStackTrace(PrintWriter err) {
        err.println(toString());
        // Don't use getStackTrace() as it calls clone()
        // Get stackTrace, in case stackTrace is reassigned
        StackTraceElement[] stack = getInternalStackTrace();
        for (java.lang.StackTraceElement element : stack) {
            err.println("\tat " + element);
        }

        StackTraceElement[] parentStack = stack;
        Throwable throwable = getCause();
        while (throwable != null) {
            err.print("Caused by: ");
            err.println(throwable);
            StackTraceElement[] currentStack = throwable.getInternalStackTrace();
            int duplicates = countDuplicates(currentStack, parentStack);
            for (int i = 0; i < currentStack.length - duplicates; i++) {
                err.println("\tat " + currentStack[i]);
            }
            if (duplicates > 0) {
                err.println("\t... " + duplicates + " more");
            }
            parentStack = currentStack;
            throwable = throwable.getCause();
        }
    }

    /**
     * Answers a string containing a concise, human-readable description of the
     * receiver.
     * 
     * @return String a printable representation for the receiver.
     */
    @Override
    public String toString() {
        String msg = getLocalizedMessage();
        String name = getClass().getName();
        if (msg == null) {
            return name;
        }
        return new StringBuffer(name.length() + 2 + msg.length()).append(name).append(": ")
                .append(msg).toString();
    }

    /**
     * Initialize the cause of the receiver. The cause cannot be reassigned.
     * 
     * @param throwable The cause of this Throwable
     * 
     * @exception IllegalArgumentException when the cause is the receiver
     * @exception IllegalStateException when the cause has already been
     *            initialized
     * 
     * @return the receiver.
     */
    public synchronized Throwable initCause(Throwable throwable) {
        if (cause == this) {
            if (throwable != this) {
                cause = throwable;
                return this;
            }
            throw new IllegalArgumentException("Cause cannot be the receiver");
        }
        throw new IllegalStateException("Cause already initialized");
    }

    /**
     * Answers the cause of this Throwable, or null if there is no cause.
     * 
     * @return Throwable The receiver's cause.
     */
    public Throwable getCause() {
        if (cause == this) {
            return null;
        }
        return cause;
    }

    private void writeObject(ObjectOutputStream s) throws IOException {
        // ensure the stackTrace field is initialized
        getInternalStackTrace();
        s.defaultWriteObject();
    }
}
