import sys
import traceback

__all__ = ['ExceptionInfo', 'Traceback']

DEFAULT_MAX_FRAMES = sys.getrecursionlimit() // 8


class _Code:

    def __init__(self, code):
        self.co_filename = code.co_filename
        self.co_name = code.co_name
        self.co_argcount = code.co_argcount
        self.co_cellvars = ()
        self.co_firstlineno = code.co_firstlineno
        self.co_flags = code.co_flags
        self.co_freevars = ()
        self.co_code = b''
        self.co_lnotab = b''
        self.co_names = code.co_names
        self.co_nlocals = code.co_nlocals
        self.co_stacksize = code.co_stacksize
        self.co_varnames = ()
        if sys.version_info >= (3, 11):
            self.co_qualname = code.co_qualname
            self._co_positions = list(code.co_positions())

    if sys.version_info >= (3, 11):
        @property
        def co_positions(self):
            return self._co_positions.__iter__


class _Frame:
    Code = _Code

    def __init__(self, frame):
        self.f_builtins = {}
        self.f_globals = {
            "__file__": frame.f_globals.get("__file__", "__main__"),
            "__name__": frame.f_globals.get("__name__"),
            "__loader__": None,
        }
        self.f_locals = fl = {}
        try:
            fl["__traceback_hide__"] = frame.f_locals["__traceback_hide__"]
        except KeyError:
            pass
        self.f_back = None
        self.f_trace = None
        self.f_exc_traceback = None
        self.f_exc_type = None
        self.f_exc_value = None
        self.f_code = self.Code(frame.f_code)
        self.f_lineno = frame.f_lineno
        self.f_lasti = frame.f_lasti
        # don't want to hit https://bugs.python.org/issue21967
        self.f_restricted = False

    if sys.version_info >= (3, 11):
        @property
        def co_positions(self):
            return self.f_code.co_positions


class _Object:

    def __init__(self, **kw):
        [setattr(self, k, v) for k, v in kw.items()]

    if sys.version_info >= (3, 11):
        __default_co_positions__ = ((None, None, None, None),)

        @property
        def co_positions(self):
            return getattr(
                self,
                "_co_positions",
                self.__default_co_positions__
            ).__iter__

        @co_positions.setter
        def co_positions(self, value):
            self._co_positions = value  # noqa


class _Truncated:

    def __init__(self):
        self.tb_lineno = -1
        self.tb_frame = _Object(
            f_globals={"__file__": "",
                       "__name__": "",
                       "__loader__": None},
            f_fileno=None,
            f_code=_Object(co_filename="...",
                           co_name="[rest of traceback truncated]"),
        )
        self.tb_next = None
        self.tb_lasti = 0

    if sys.version_info >= (3, 11):
        @property
        def co_positions(self):
            return self.tb_frame.co_positions


class Traceback:
    Frame = _Frame

    def __init__(self, tb, max_frames=DEFAULT_MAX_FRAMES, depth=0):
        self.tb_frame = self.Frame(tb.tb_frame)
        self.tb_lineno = tb.tb_lineno
        self.tb_lasti = tb.tb_lasti
        self.tb_next = None
        if tb.tb_next is not None:
            if depth <= max_frames:
                self.tb_next = Traceback(tb.tb_next, max_frames, depth + 1)
            else:
                self.tb_next = _Truncated()


class RemoteTraceback(Exception):
    def __init__(self, tb):
        self.tb = tb

    def __str__(self):
        return self.tb


class ExceptionWithTraceback(Exception):
    def __init__(self, exc, tb):
        self.exc = exc
        self.tb = '\n"""\n%s"""' % tb
        super().__init__()

    def __str__(self):
        return self.tb

    def __reduce__(self):
        return rebuild_exc, (self.exc, self.tb)


def rebuild_exc(exc, tb):
    exc.__cause__ = RemoteTraceback(tb)
    return exc


class ExceptionInfo:
    """Exception wrapping an exception and its traceback.

    :param exc_info: The exception info tuple as returned by
        :func:`sys.exc_info`.

    """

    #: Exception type.
    type = None

    #: Exception instance.
    exception = None

    #: Pickleable traceback instance for use with :mod:`traceback`
    tb = None

    #: String representation of the traceback.
    traceback = None

    #: Set to true if this is an internal error.
    internal = False

    def __init__(self, exc_info=None, internal=False):
        self.type, exception, tb = exc_info or sys.exc_info()
        try:
            self.tb = Traceback(tb)
            self.traceback = ''.join(
                traceback.format_exception(self.type, exception, tb),
            )
            self.internal = internal
        finally:
            del tb
        self.exception = ExceptionWithTraceback(exception, self.traceback)

    def __str__(self):
        return self.traceback

    def __repr__(self):
        return "<%s: %r>" % (self.__class__.__name__, self.exception, )

    @property
    def exc_info(self):
        return self.type, self.exception, self.tb
