import sys
import pdb
import re
import linecache
import builtins
import tokenize
import traceback
from io import BytesIO

from doctest2 import util
from doctest2.constants import *
from . import common

exec_mode_map = {
    EXEC_INTERACTIVE_INTERPRETER: 'single',
    EXEC_FRAGMENT: 'exec',}

name = 'python'

ps1 = '>>>'
ps2 = '...'

def find_comments(source):
    for token in util.munging_tokenize(BytesIO(source.encode('utf-8')).readline):
        if token.type == tokenize.COMMENT:
            assert token.string.startswith('#')
            yield token.string[1:]

# A regular expression for handling `want` strings that contain
# expected exceptions.  It divides `want` into four pieces:
#    - the output before the exception (`output`)
#    - the traceback header line (`hdr`)
#    - the traceback stack (`stack`)
#    - the exception message (`msg`), as generated by
#      traceback.format_exception_only()
# `msg` may have multiple lines.  We assume/require that the
# exception message is the first non-indented line starting with a word
# character following the traceback header line.
_EXCEPTION_RE = re.compile(r"""
    # Grab the traceback header.  Different versions of Python have
    # said different things on the first traceback line.
    (?P<output>.*)
    ^(?P<hdr> Traceback\ \(
        (?: most\ recent\ call\ last
        |   innermost\ last
        ) \) :
    )
    \s* $                # toss trailing whitespace on the header.
    (?P<stack> .*?)      # don't blink: absorb stuff until...
    ^ (?P<msg> \w+ .*)   #     a line *starts* with alphanum.
    """, re.VERBOSE | re.MULTILINE | re.DOTALL)

class ExpectedOutput:
    def __init__(self, stdout, exc_msg=None):
        if exc_msg is not None and not exc_msg.endswith('\n'):
            exc_msg += '\n'
        self.stdout = stdout
        self.exc_msg = exc_msg

def parse_expected(want):
    # If `want` contains a traceback message, then extract it.
    m = _EXCEPTION_RE.match(want)
    if m:
        return ExpectedOutput(m.group('output'), m.group('msg'))
    else:
        return ExpectedOutput(want, None)

class PythonExceptionOutput:
    def __init__(self, stdout, exception):
        self.stdout = stdout
        self.exception = exception
    
    def to_string(self):
        return self.stdout + util._exception_traceback(self.exception)
    
    def to_traceback(self):
        return util._exception_traceback(self.exception)
    
    def check(self, expected, check_func, optionflags):
        exc_msg = traceback.format_exception_only(*self.exception[:2])[-1]

        # If `expected.exc_msg` is None, then we weren't expecting
        # an exception.
        if expected.exc_msg is None:
            return BOOM
        # We expected an exception:  see whether it matches.
        elif (check_func(expected.exc_msg, exc_msg, optionflags) and
                check_func(expected.stdout, self.stdout, optionflags)):
            return SUCCESS
        # Another chance if they didn't care about the detail.
        elif optionflags & IGNORE_EXCEPTION_DETAIL:
            m1 = re.match(r'(?:[^:]*\.)?([^:]*:)', expected.exc_msg)
            m2 = re.match(r'(?:[^:]*\.)?([^:]*:)', exc_msg)
            if (m1 and m2 and
                    check_func(m1.group(1), m2.group(1), optionflags) and
                    check_func(expected.stdout, self.stdout, optionflags)):
                return SUCCESS
        return FAILURE

class Executor:
    def __init__(self, test, clear_globs=True, exec_mode=EXEC_INTERACTIVE_INTERPRETER):
        # Create a fake output target for capturing doctest output.
        self.fakeout = util._SpoofOut()
        self.test = test
        self.clear_globs = clear_globs
        if exec_mode not in [EXEC_INTERACTIVE_INTERPRETER, EXEC_FRAGMENT]:
            raise ValueError(
                "exec_mode must be "
                "EXEC_INTERACTIVE_INTERPRETER or EXEC_FRAGMENT, "
                "got %r instead" % exec_Mode)
        
        self.compile_mode = exec_mode_map[exec_mode]
    
    def __enter__(self):
        self.old_stdout = sys.stdout
        sys.stdout = self.fakeout

        # Patch pdb.set_trace to restore sys.stdout during interactive
        # debugging (so it's not still redirected to self.fakeout).
        # Note that the interactive output will go to *our*
        # save_stdout, even if that's not the real sys.stdout; this
        # allows us to write test cases for the set_trace behavior.
        self.old_trace = sys.gettrace()
        self.old_set_trace = pdb.set_trace
        self.debugger = util._OutputRedirectingPdb(self.old_stdout)
        self.debugger.reset()
        pdb.set_trace = self.debugger.set_trace

        # Patch linecache.getlines, so we can see the example's source
        # when we're inside the debugger.
        self.old_linecache_getlines = linecache.getlines
        linecache.getlines = self._patched_linecache_getlines

        # Make sure sys.displayhook just prints the value to stdout
        self.old_displayhook = sys.displayhook
        sys.displayhook = sys.__displayhook__
        
        return self
    
    def __exit__(self, _exc_type, _exc_value, _traceback):
        sys.stdout = self.old_stdout
        pdb.set_trace = self.old_set_trace
        sys.settrace(self.old_trace)
        linecache.getlines = self.old_linecache_getlines
        sys.displayhook = self.old_displayhook
        if self.clear_globs:
            self.test.globs.clear()
            builtins._ = None

    _LINECACHE_FILENAME_RE = re.compile(r'<doctest '
                                         r'(?P<name>.+)'
                                         r'\[(?P<examplenum>\d+)\]>$')
    def _patched_linecache_getlines(self, filename, module_globals=None):
        m = self._LINECACHE_FILENAME_RE.match(filename)
        if m and m.group('name') == self.test.name:
            example = self.test.examples[int(m.group('examplenum'))]
            return example.source.splitlines(True)
        else:
            return self.old_linecache_getlines(filename, module_globals)
    
    def execute(self, compileflags, example, examplenum):
        test = self.test
        assert self.test.examples[examplenum] == example
        # Use a special filename for compile(), so we can retrieve
        # the source code during interactive debugging (see
        # __patched_linecache_getlines).
        filename = '<doctest %s[%d]>' % (test.name, examplenum)

        # Run the example in the given context (globs), and record
        # any exception that gets raised.  (But don't intercept
        # keyboard interrupts.)
        try:
            # Don't blink!  This is where the user's code gets run.
            exec(compile(example.source, filename, self.compile_mode,
                         compileflags, 1), test.globs)
        except KeyboardInterrupt:
            raise
        except:
            exception = sys.exc_info()
        else:
            exception = None

        self.debugger.set_continue() # ==== Example Finished ====

        got = self.fakeout.getvalue()  # the actual output
        self.fakeout.truncate(0)
        
        if exception is None:
            return common.SimpleOutput(got)
        else:
            return PythonExceptionOutput(got, exception)
