"""
vinegar ('when things go sour'): safe serialization of exceptions.

note that by changing the configuration parameters, this module can be
made non-secure
"""
import sys
import traceback
import builtins
from rpyc.core import brine
from rpyc.core import consts


class GenericException(Exception):
    pass

_generic_exceptions_cache = {}

STOP_ITERATION_MAGIC = 0

def dump(typ, val, tb, include_local_traceback):
    if type(typ) is str:
        return typ
    if typ is StopIteration:
        return consts.EXC_STOP_ITERATION # optimization
    
    if include_local_traceback:
        tbtext = "".join(traceback.format_exception(typ, val, tb))
    else:
        tbtext = "<traceback denied>"
    attrs = []
    args = []
    for name in dir(val):
        if name == "args":
            for a in val.args:
                if brine.dumpable(a):
                    args.append(a)
                else:
                    args.append(repr(a))
        elif not name.startswith("_") or name == "_remote_tb":
            attrval = getattr(val, name)
            if not brine.dumpable(attrval):
                attrval = repr(attrval)
            attrs.append((name, attrval))
    return (typ.__module__, typ.__name__), tuple(args), tuple(attrs), tbtext

def load(val, import_custom_exceptions, instantiate_custom_exceptions, instantiate_oldstyle_exceptions):
    if val == consts.EXC_STOP_ITERATION:
        return StopIteration # optimization
    if type(val) is str:
        return val # deprecated string exceptions
    
    (modname, clsname), args, attrs, tbtext = val
    if import_custom_exceptions and modname not in sys.modules:
        try:
            mod = __import__(modname, None, None, "*")
        except ImportError:
            pass
    cls = None
    if instantiate_custom_exceptions:
        cls = getattr(sys.modules[modname], clsname, None)
    elif modname == "builtins":
        cls = getattr(builtins, clsname, None)
    if not issubclass(cls, Exception) and not instantiate_oldstyle_exceptions:
        cls = None
    if cls is None:
        fullname = "%s.%s" % (modname, clsname)
        if fullname not in _generic_exceptions_cache:
            fakemodule = {"__module__" : "%s.%s" % (__name__, modname)}
            _generic_exceptions_cache[fullname] = type(fullname, (GenericException,), fakemodule)
        cls = _generic_exceptions_cache[fullname]

    exc = cls.__new__(cls)
    exc.args = args
    for name, attrval in attrs:
        setattr(exc, name, attrval)
    if hasattr(exc, "_remote_tb"):
        exc._remote_tb += (tbtext,)
    else:
        exc._remote_tb = (tbtext,)
    return exc


#===============================================================================
# customized except hook
#===============================================================================
if hasattr(sys, "excepthook"):
    _orig_excepthook = sys.excepthook
else:
    # ironpython forgot to implement excepthook, scheisse
    _orig_excepthook = None

def rpyc_excepthook(typ, val, tb):
    if hasattr(val, "_remote_tb"):
        sys.stderr.write("======= Remote traceback =======\n")
        tbtext = "\n--------------------------------\n\n".join(val._remote_tb)
        sys.stderr.write(tbtext)
        sys.stderr.write("\n======= Local exception ========\n")
    _orig_excepthook(typ, val, tb)

def install_rpyc_excepthook():
    if _orig_excepthook is not None:
        sys.excepthook = rpyc_excepthook

def uninstall_rpyc_excepthook():
    if _orig_excepthook is not None:
        sys.excepthook = _orig_excepthook


