# -*- coding: utf-8 -*-
"""One of the oft-cited tenets of Python is that it is better to ask
forgiveness than permission. That is, there are many cases where it is
more inclusive and correct to handle exceptions than spend extra lines
and execution time checking for conditions. This philosophy makes good
exception handling features all the more important. Unfortunately
Python's :mod:`traceback` module is woefully behind the times.

The ``tbutils`` module provides two disparate but complementary featuresets:

  1. With :class:`ExceptionInfo` and :class:`TracebackInfo`, the
     ability to extract, construct, manipulate, format, and serialize
     exceptions, tracebacks, and callstacks.
  2. With :class:`ParsedException`, the ability to find and parse tracebacks
     from captured output such as logs and stdout.

There is also the :class:`ContextualTracebackInfo` variant of
:class:`TracebackInfo`, which includes much more information from each
frame of the callstack, including values of locals and neighboring
lines of code.
"""

from __future__ import print_function

import re
import sys
import linecache


# TODO: chaining primitives?  what are real use cases where these help?

# TODO: print_* for backwards compatability
# __all__ = ['extract_stack', 'extract_tb', 'format_exception',
#            'format_exception_only', 'format_list', 'format_stack',
#            'format_tb', 'print_exc', 'format_exc', 'print_exception',
#            'print_last', 'print_stack', 'print_tb']


__all__ = ['ExceptionInfo', 'TracebackInfo', 'Callpoint',
           'ContextualExceptionInfo', 'ContextualTracebackInfo',
           'ContextualCallpoint', 'print_exception', 'ParsedException']


class Callpoint(object):
    """The Callpoint is a lightweight object used to represent a single
    entry in the code of a call stack. It stores the code-related
    metadata of a given frame. Available attributes are the same as
    the parameters below.

    Args:
        func_name (str): the function name
        lineno (int): the line number
        module_name (str): the module name
        module_path (str): the filesystem path of the module
        lasti (int): the index of bytecode execution
        line (str): the single-line code content (if available)

    """
    __slots__ = ('func_name', 'lineno', 'module_name', 'module_path', 'lasti',
                 'line')

    def __init__(self, module_name, module_path, func_name,
                 lineno, lasti, line=None):
        self.func_name = func_name
        self.lineno = lineno
        self.module_name = module_name
        self.module_path = module_path
        self.lasti = lasti
        self.line = line

    def to_dict(self):
        "Get a :class:`dict` copy of the Callpoint. Useful for serialization."
        ret = {}
        for slot in self.__slots__:
            try:
                ret[slot] = getattr(self, slot)
            except AttributeError:
                pass
        return ret

    @classmethod
    def from_current(cls, level=1):
        "Creates a Callpoint from the location of the calling function."
        frame = sys._getframe(level)
        return cls.from_frame(frame)

    @classmethod
    def from_frame(cls, frame):
        "Create a Callpoint object from data extracted from the given frame."
        func_name = frame.f_code.co_name
        lineno = frame.f_lineno
        module_name = frame.f_globals.get('__name__', '')
        module_path = frame.f_code.co_filename
        lasti = frame.f_lasti
        line = _DeferredLine(module_path, lineno, frame.f_globals)
        return cls(module_name, module_path, func_name,
                   lineno, lasti, line=line)

    @classmethod
    def from_tb(cls, tb):
        """Create a Callpoint from the traceback of the current
        exception. Main difference with :meth:`from_frame` is that
        ``lineno`` and ``lasti`` come from the traceback, which is to
        say the line that failed in the try block, not the line
        currently being executed (in the except block).
        """
        func_name = tb.tb_frame.f_code.co_name
        lineno = tb.tb_lineno
        lasti = tb.tb_lasti
        module_name = tb.tb_frame.f_globals.get('__name__', '')
        module_path = tb.tb_frame.f_code.co_filename
        line = _DeferredLine(module_path, lineno, tb.tb_frame.f_globals)
        return cls(module_name, module_path, func_name,
                   lineno, lasti, line=line)

    def __repr__(self):
        cn = self.__class__.__name__
        args = [getattr(self, s, None) for s in self.__slots__]
        if not any(args):
            return super(Callpoint, self).__repr__()
        else:
            return '%s(%s)' % (cn, ', '.join([repr(a) for a in args]))

    def tb_frame_str(self):
        """Render the Callpoint as it would appear in a standard printed
        Python traceback. Returns a string with filename, line number,
        function name, and the actual code line of the error on up to
        two lines.
        """
        ret = '  File "%s", line %s, in %s\n' % (self.module_path,
                                                 self.lineno,
                                                 self.func_name)
        if self.line:
            ret += '    %s\n' % (str(self.line).strip(),)
        return ret


class _DeferredLine(object):
    """The _DeferredLine type allows Callpoints and TracebackInfos to be
    constructed without potentially hitting the filesystem, as is the
    normal behavior of the standard Python :mod:`traceback` and
    :mod:`linecache` modules. Calling :func:`str` fetches and caches
    the line.

    Args:
        filename (str): the path of the file containing the line
        lineno (int): the number of the line in question
        module_globals (dict): an optional dict of module globals,
            used to handle advanced use cases using custom module loaders.

    """
    def __init__(self, filename, lineno, module_globals=None):
        self.filename = filename
        self.lineno = lineno
        # TODO: this is going away when we fix linecache
        # TODO: (mark) read about loader
        self.module_globals = {}
        if module_globals is not None:
            for k in ('__name__', '__loader__'):
                v = module_globals.get(k)
                if v is None:
                    self.module_globals[k] = v

    def __eq__(self, other):
        return (self.lineno, self.filename) == (other.lineno, other.filename)

    def __ne__(self, other):
        return not self == other

    def __str__(self):
        if hasattr(self, '_line'):