// Copyright (c) Corporation for National Research Initiatives
package org.python.core;
import java.io.*;

/**
 * A wrapper for all python exception. Note that the wellknown python exception are <b>not</b>
 * subclasses of PyException. Instead the python exception class is stored in the <code>type</code>
 * field and value or class instance is stored in the <code>value</code> field.
 */
public class PyException extends RuntimeException
{

    /**
     * The python exception class (for class exception) or identifier (for string exception).
     */
    public PyObject type;

    /**
     * The exception instance (for class exception) or exception value (for string exception).
     */
    public PyObject value = Py.None;

    /** The exception traceback object. */
    public PyTraceback traceback;

    /**
     * Whether the exception was re-raised, such as when a traceback is specified to
     * 'raise', or via a 'finally' block.
     */
    private boolean isReRaise = false;

    private boolean normalized = false;

    public PyException() {
        this(Py.None, Py.None);
    }

    public PyException(PyObject type) {
        this(type, Py.None);
    }

    public PyException(PyObject type, PyObject value) {
        this(type, value, null);
    }

    public PyException(PyObject type, PyObject value, PyTraceback traceback) {
        this.type = type;
        this.value = value;
        if (traceback != null) {
            this.traceback = traceback;
            isReRaise = true;
        } else {
            PyFrame frame = Py.getFrame();
            if (frame != null && frame.tracefunc != null) {
                frame.tracefunc = frame.tracefunc.traceException(frame, this);
            }
        }
    }

    public PyException(PyObject type, String value) {
        this(type, new PyString(value));
    }

    private boolean printingStackTrace = false;
    public void printStackTrace() {
        Py.printException(this);
    }

    public Throwable fillInStackTrace() {
        return Options.includeJavaStackInExceptions ? super.fillInStackTrace() : this;
    }

    public synchronized void printStackTrace(PrintStream s) {
        if (printingStackTrace) {
            super.printStackTrace(s);
        } else {
            try {
                printingStackTrace = true;
                Py.displayException(type, value, traceback, new PyFile(s));
            } finally {
                printingStackTrace = false;
            }
        }
    }

    public synchronized void super__printStackTrace(PrintWriter w) {
        try {
            printingStackTrace = true;
            super.printStackTrace(w);
        } finally {
            printingStackTrace = false;
        }
    }

    public synchronized String toString() {
        ByteArrayOutputStream buf = new ByteArrayOutputStream();
        if (!printingStackTrace) {
            printStackTrace(new PrintStream(buf));
        }
        return buf.toString();
    }

    /**
     * Instantiates the exception value if it is not already an
     * instance.
     *
     */
    public void normalize() {
        if (normalized) {
            return;
        }
        PyObject inClass = null;
        if (isExceptionInstance(value)) {
            inClass = value.fastGetClass();
        }

        if (isExceptionClass(type)) {
            if (inClass == null || !Py.isSubClass(inClass, type)) {
                PyObject[] args;

                // Don't decouple a tuple into args when it's a
                // KeyError, pass it on through below
                if (value == Py.None) {
                    args = Py.EmptyObjects;
                } else if (value instanceof PyTuple && type != Py.KeyError) {
                    args = ((PyTuple)value).getArray();
                } else {
                    args = new PyObject[] {value};
                }

                value = type.__call__(args);
            } else if (inClass != type) {
                type = inClass;
            }
        }
        normalized = true;
    }

    /**
     * Register frame as having been visited in the traceback.
     *
     * @param here the current PyFrame
     */
    public void tracebackHere(PyFrame here) {
        tracebackHere(here, false);
    }

    /**
     * Register frame as having been visited in the traceback.
     *
     * @param here the current PyFrame
     * @param isFinally whether caller is a Python finally block
     */
    public void tracebackHere(PyFrame here, boolean isFinally) {
        if (!isReRaise && here != null) {
            // the frame is either inapplicable or already registered (from a finally)
            // during a re-raise
            traceback = new PyTraceback(traceback, here);
        }
        // finally blocks immediately tracebackHere: so they toggle isReRaise to skip the
        // next tracebackHere hook
        isReRaise = isFinally;
    }

    /**
     * Logic for the raise statement
     *
     * @param type the first arg to raise, a type or an instance
     * @param value the second arg, the instance of the class or arguments to its
     * constructor
     * @param traceback a traceback object
     * @return a PyException wrapper
     */
    public static PyException doRaise(PyObject type, PyObject value, PyObject traceback) {
        if (type == null) {
            ThreadState state = Py.getThreadState();
            type = state.exception.type;
            value = state.exception.value;
            traceback = state.exception.traceback;
        }

        if (traceback == Py.None) {
            traceback = null;
        } else if (traceback != null && !(traceback instanceof PyTraceback)) {
            throw Py.TypeError("raise: arg 3 must be a traceback or None");
        }

        if (value == null) {
            value = Py.None;
        }

        // Repeatedly, replace a tuple exception with its first item
        while (type instanceof PyTuple && ((PyTuple)type).size() > 0) {
            type = type.__getitem__(0);
        }

        if (type.getType() == PyString.TYPE) {
            Py.warning(Py.DeprecationWarning, "raising a string exception is deprecated");
        } else if (isExceptionClass(type)) {
            PyException pye = new PyException(type, value, (PyTraceback)traceback);
            pye.normalize();
            return pye;
        } else if (isExceptionInstance(type)) {
            // Raising an instance.  The value should be a dummy.
            if (value != Py.None) {
                throw Py.TypeError("instance exception may not have a separate value");
            } else {
                // Normalize to raise <class>, <instance>
                value = type;
                type = type.fastGetClass();
            }
        } else {
            // Not something you can raise.  You get an exception
            // anyway, just not what you specified :-)
            throw Py.TypeError("exceptions must be classes, instances, or strings (deprecated), "
                               + "not " + type.getType().fastGetName());
        }

        return new PyException(type, value, (PyTraceback)traceback);
    }

    /**
     * Determine if this PyException is a match for exc.
     *
     * @param exc a PyObject exception type
     * @return true if a match
     */
    public boolean match(PyObject exc) {
        if (exc instanceof PyTuple) {
            for (PyObject item : ((PyTuple)exc).getArray()) {
                if (match(item)) {
                    return true;
                }
            }
            return false;
        }

        normalize();
        // FIXME, see bug 737978
        //
        // A special case for IOError's to allow them to also match
        // java.io.IOExceptions.  This is a hack for 1.0.x until I can do
        // it right in 1.1
        if (exc == Py.IOError) {
            if (__builtin__.isinstance(value, PyType.fromClass(IOException.class))) {
                return true;
            }
        }
        // FIXME too, same approach for OutOfMemoryError
        if (exc == Py.MemoryError) {
            if (__builtin__.isinstance(value,
                                       PyType.fromClass(OutOfMemoryError.class))) {
                return true;
            }
        }

        if (isExceptionClass(type) && isExceptionClass(exc)) {
            return Py.isSubClass(type, exc);
        }

        return type == exc;
    }

    /**
     * Determine whether obj is a Python exception class
     *
     * @param obj a PyObject
     * @return true if an exception
     */
    public static boolean isExceptionClass(PyObject obj) {
        if (obj instanceof PyClass) {
            return true;
        }
        if (!(obj instanceof PyType)) {
            return false;
        }
        PyType type = ((PyType)obj);
        if (type.isSubType(PyBaseException.TYPE)) {
            return true;
        }
        return type.getProxyType() != null && Throwable.class.isAssignableFrom(type.getProxyType());
    }

    /**
     * Determine whether obj is an Python exception instance
     *
     * @param obj a PyObject
     * @return true if an exception instance
     */
    public static boolean isExceptionInstance(PyObject obj) {
        return obj instanceof PyInstance || obj instanceof PyBaseException
           || obj.getJavaProxy() instanceof Throwable;
    }

    /**
     * Get the name of the exception's class
     *
     * @param obj a PyObject exception
     * @return String exception name
     */
    public static String exceptionClassName(PyObject obj) {
        return obj instanceof PyClass ? ((PyClass)obj).__name__ : ((PyType)obj).fastGetName();
    }
}
