import linecache
import os.path
import re
import sys
import traceback  # @Reimport

# IFDEF CYTHON
# import dis
# ENDIF

from _pydev_bundle import pydev_log
from _pydevd_bundle import pydevd_dont_trace
from _pydevd_bundle import pydevd_vars
from _pydevd_bundle.pydevd_breakpoints import get_exception_breakpoint
from _pydevd_bundle.pydevd_comm_constants import (CMD_STEP_CAUGHT_EXCEPTION, CMD_STEP_RETURN, CMD_STEP_OVER, CMD_SET_BREAK, \
    CMD_STEP_INTO, CMD_SMART_STEP_INTO, CMD_RUN_TO_LINE, CMD_SET_NEXT_STATEMENT, CMD_STEP_INTO_MY_CODE)
from _pydevd_bundle.pydevd_constants import STATE_SUSPEND, get_current_thread_id, STATE_RUN, dict_iter_values, IS_PY3K, \
    dict_keys, RETURN_VALUES_DICT, NO_FTRACE
from _pydevd_bundle.pydevd_dont_trace_files import DONT_TRACE, PYDEV_FILE
from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, just_raised, remove_exception_from_frame, ignore_exception_trace
from _pydevd_bundle.pydevd_utils import get_clsname_for_code
from pydevd_file_utils import get_abs_path_real_path_and_base_from_frame, is_real_file

try:
    from inspect import CO_GENERATOR
except:
    CO_GENERATOR = 0
from _pydevd_bundle.pydevd_constants import IS_PY2

try:
    from _pydevd_bundle.pydevd_signature import send_signature_call_trace, send_signature_return_trace
except ImportError:
    def send_signature_call_trace(*args, **kwargs):
        pass

basename = os.path.basename

IGNORE_EXCEPTION_TAG = re.compile('[^#]*#.*@IgnoreException')
DEBUG_START = ('pydevd.py', '_exec')
DEBUG_START_PY3K = ('_pydev_execfile.py', 'execfile')
TRACE_PROPERTY = 'pydevd_traceproperty.py'
get_file_type = DONT_TRACE.get


def handle_breakpoint_condition(py_db, info, breakpoint, new_frame):
    condition = breakpoint.condition
    try:
        if breakpoint.handle_hit_condition(new_frame):
            return True

        if condition is None:
            return False

        return eval(condition, new_frame.f_globals, new_frame.f_locals)
    except Exception as e:
        if IS_PY2:
            # Must be bytes on py2.
            if isinstance(condition, unicode):
                condition = condition.encode('utf-8')

        if not isinstance(e, py_db.skip_print_breakpoint_exception):
            sys.stderr.write('Error while evaluating expression: %s\n' % (condition,))

            etype, value, tb = sys.exc_info()
            traceback.print_exception(etype, value, tb.tb_next)

        if not isinstance(e, py_db.skip_suspend_on_breakpoint_exception):
            try:
                # add exception_type and stacktrace into thread additional info
                etype, value, tb = sys.exc_info()
                error = ''.join(traceback.format_exception_only(etype, value))
                stack = traceback.extract_stack(f=tb.tb_frame.f_back)

                # On self.set_suspend(thread, CMD_SET_BREAK) this info will be
                # sent to the client.
                info.conditional_breakpoint_exception = \
                    ('Condition:\n' + condition + '\n\nError:\n' + error, stack)
            except:
                traceback.print_exc()
            return True
        
        return False

    finally:
        etype, value, tb = None, None, None


def handle_breakpoint_expression(breakpoint, info, new_frame):
    try:
        try:
            val = eval(breakpoint.expression, new_frame.f_globals, new_frame.f_locals)
        except:
            val = sys.exc_info()[1]
    finally:
        if val is not None:
            info.pydev_message = str(val)


#=======================================================================================================================
# PyDBFrame
#=======================================================================================================================
# IFDEF CYTHON
# cdef class PyDBFrame:
# ELSE
class PyDBFrame:
    '''This makes the tracing for a given frame, so, the trace_dispatch
    is used initially when we enter into a new context ('call') and then
    is reused for the entire context.
    '''
    # ENDIF

    # Note: class (and not instance) attributes.

    # Same thing in the main debugger but only considering the file contents, while the one in the main debugger
    # considers the user input (so, the actual result must be a join of both).
    filename_to_lines_where_exceptions_are_ignored = {}
    filename_to_stat_info = {}

    # IFDEF CYTHON
    # cdef tuple _args
    # cdef int should_skip
    # cdef int _bytecode_offset
    # cdef list _instructions
    # def __init__(self, tuple args):
    #     self._args = args # In the cython version we don't need to pass the frame
    #     self.should_skip = -1  # On cythonized version, put in instance.
    #     self._bytecode_offset = 0
    #     self._instructions = None
    # ELSE
    should_skip = -1  # Default value in class (put in instance on set).

    def __init__(self, args):
        # args = main_debugger, filename, base, info, t, frame
        # yeap, much faster than putting in self and then getting it from self later on
        self._args = args
        self._bytecode_offset = 0
    # ENDIF

    def set_suspend(self, *args, **kwargs):
        self._args[0].set_suspend(*args, **kwargs)

    def do_wait_suspend(self, *args, **kwargs):
        self._args[0].do_wait_suspend(*args, **kwargs)

    # IFDEF CYTHON
    # def trace_exception(self, frame, str event, arg):
    #     cdef bint should_stop;
    # ELSE
    def trace_exception(self, frame, event, arg):
        # ENDIF
        if event == 'exception':
            should_stop, frame = self.should_stop_on_exception(frame, event, arg)

            if should_stop:
                self.handle_exception(frame, event, arg)
                return self.trace_dispatch

        return self.trace_exception

    def trace_return(self, frame, event, arg):
        if event == 'return':
            main_debugger, filename = self._args[0], self._args[1]
            send_signature_return_trace(main_debugger, frame, filename, arg)
        return self.trace_return

    # IFDEF CYTHON
    # def should_stop_on_exception(self, frame, str event, arg):
    #     cdef PyDBAdditionalThreadInfo info;
    #     cdef bint flag;
    # ELSE
    def should_stop_on_exception(self, frame, event, arg):
        # ENDIF

        # main_debugger, _filename, info, _thread = self._args
        main_debugger = self._args[0]
        info = self._args[2]
        should_stop = False

        # STATE_SUSPEND = 2
        if info.pydev_state != 2:  # and breakpoint is not None:
            exception, value, trace = arg

            if trace is not None and hasattr(trace, 'tb_next'):
                # on jython trace is None on the first event and it may not have a tb_next.

                exception_breakpoint = get_exception_breakpoint(
                    exception, main_debugger.break_on_caught_exceptions)
                is_real = is_real_file(frame.f_code.co_filename)

                if exception_breakpoint is not None:
                    if exception_breakpoint.condition is not None:
                        # Always add exception to frame (must remove later after we proceed).
                        add_exception_to_frame(frame, (exception, value, trace))
                        eval_result = handle_breakpoint_condition(main_debugger, info, exception_breakpoint, frame)
                        remove_exception_from_frame(frame)
                        if not eval_result:
                            return False, frame

                    if exception_breakpoint.ignore_libraries:
                        if not main_debugger.is_exception_trace_in_project_scope(trace):
                            return False, frame

                    if ignore_exception_trace(trace):
                        return False, frame

                    was_just_raised = just_raised(trace)
                    if was_just_raised:

                        if main_debugger.skip_on_exceptions_thrown_in_same_context:
                            # Option: Don't break if an exception is caught in the same function from which it is thrown
                            return False, frame

                    if exception_breakpoint.notify_on_first_raise_only:
                        if main_debugger.skip_on_exceptions_thrown_in_same_context:
                            # In this case we never stop if it was just raised, so, to know if it was the first we
                            # need to check if we're in the 2nd method.
                            if not was_just_raised and not just_raised(trace.tb_next):
                                return False, frame  # I.e.: we stop only when we're at the caller of a method that throws an exception

                        else:
                            if not was_just_raised and not main_debugger.is_top_level_trace_in_project_scope(trace):
                                return False, frame  # I.e.: we stop only when it was just raised