import ctypes
import os
from _pydev_bundle import pydev_log, pydev_monkey
from _pydevd_bundle.pydevd_constants import get_frame, IS_PY2, IS_PY37_OR_GREATER, IS_CPYTHON, IS_WINDOWS, IS_LINUX, IS_MACOS, \
    IS_64BIT_PROCESS, IS_AARCH64, IS_PYCHARM_ATTACH
from _pydev_imps._pydev_saved_modules import thread, threading

try:
    import cStringIO as StringIO #may not always be available @UnusedImport
except:
    try:
        import StringIO #@Reimport
    except:
        import io as StringIO


import sys #@Reimport
import traceback

_original_settrace = sys.settrace

class TracingFunctionHolder:
    '''This class exists just to keep some variables (so that we don't keep them in the global namespace). 
    '''
    _original_tracing = None
    _warn = True
    _lock = thread.allocate_lock()
    _traceback_limit = 1
    _warnings_shown = {}
 
 
def get_exception_traceback_str():
    exc_info = sys.exc_info()
    s = StringIO.StringIO()
    traceback.print_exception(exc_info[0], exc_info[1], exc_info[2], file=s)
    return s.getvalue()

def _get_stack_str(frame):
    
    msg = '\nIf this is needed, please check: ' + \
          '\nhttp://pydev.blogspot.com/2007/06/why-cant-pydev-debugger-work-with.html' + \
          '\nto see how to restore the debug tracing back correctly.\n' 
          
    if TracingFunctionHolder._traceback_limit:
        s = StringIO.StringIO()
        s.write('Call Location:\n')
        traceback.print_stack(f=frame, limit=TracingFunctionHolder._traceback_limit, file=s)
        msg = msg + s.getvalue()
    
    return msg

def _internal_set_trace(tracing_func):
    if TracingFunctionHolder._warn:
        frame = get_frame()
        if frame is not None and frame.f_back is not None:
            filename = frame.f_back.f_code.co_filename.lower()
            if not filename.endswith('threading.py') and not filename.endswith('pydevd_tracing.py'):
                message = \
                '\nPYDEV DEBUGGER WARNING:' + \
                '\nsys.settrace() should not be used when the debugger is being used.' + \
                '\nThis may cause the debugger to stop working correctly.' + \
                '%s' % _get_stack_str(frame.f_back)
                
                if message not in TracingFunctionHolder._warnings_shown:
                    #only warn about each message once...
                    TracingFunctionHolder._warnings_shown[message] = 1
                    sys.stderr.write('%s\n' % (message,))
                    sys.stderr.flush()

    if TracingFunctionHolder._original_tracing:
        TracingFunctionHolder._original_tracing(tracing_func)


def SetTrace(tracing_func):
    if TracingFunctionHolder._original_tracing is None:
        # This may happen before replace_sys_set_trace_func is called.
        sys.settrace(tracing_func)
        return

    try:
        TracingFunctionHolder._lock.acquire()
        TracingFunctionHolder._warn = False
        _internal_set_trace(tracing_func)
        TracingFunctionHolder._warn = True
    finally:
        TracingFunctionHolder._lock.release()


def replace_sys_set_trace_func():
    if TracingFunctionHolder._original_tracing is None:
        TracingFunctionHolder._original_tracing = sys.settrace
        sys.settrace = _internal_set_trace


def restore_sys_set_trace_func():
    if TracingFunctionHolder._original_tracing is not None:
        sys.settrace = TracingFunctionHolder._original_tracing
        TracingFunctionHolder._original_tracing = None


def load_python_helper_lib():
    if not IS_CPYTHON:
        pydev_log.info('Helper lib to set tracing to all threads not loaded: unsupported Python implementation')
        return None

    if ctypes is None:
        pydev_log.info('Helper lib to set tracing to all threads not loaded: ctypes not found')
        return None

    if sys.version_info[:2] > (3, 11):
        pydev_log.info('Helper lib to set tracing to all threads not loaded: unsupported Python version')
        return None

    if IS_WINDOWS:
        if IS_64BIT_PROCESS:
            suffix = 'amd64'
        else:
            suffix = 'x86'

        filename = os.path.join(os.path.dirname(__file__), 'pydevd_attach_to_process', 'attach_%s.dll' % (suffix,))

    elif IS_LINUX:
        if IS_64BIT_PROCESS:
            if IS_AARCH64:
                suffix = 'aarch64'
            else:
                suffix = 'amd64'
        else:
            suffix = 'x86'

        filename = os.path.join(os.path.dirname(__file__), 'pydevd_attach_to_process', 'attach_linux_%s.so' % (suffix,))

    elif IS_MACOS:
        filename = os.path.join(os.path.dirname(__file__), 'pydevd_attach_to_process', 'attach.dylib')

    else:
        pydev_log.info('Unable to set trace to all threads in platform: %s' % sys.platform)
        return None

    if not os.path.exists(filename):
        pydev_log.error('Expected: %s to exist.' % filename)
        return None

    try:
        # Load as pydll so that we don't release the gil.
        lib = ctypes.pydll.LoadLibrary(filename)
        return lib
    except:
        # Only show message if tracing is on (we don't have pre-compiled
        # binaries for all architectures -- i.e.: ARM).
        pydev_log.error('Error loading: %s' % filename)
        return None


def set_trace_to_threads(tracing_func):
    lib = load_python_helper_lib()
    if lib is None:  # This is the case if it's not CPython.
        return -1

    if hasattr(sys, 'getswitchinterval'):
        get_interval, set_interval = sys.getswitchinterval, sys.setswitchinterval
    else:
        get_interval, set_interval = sys.getcheckinterval, sys.setcheckinterval

    prev_value = get_interval()
    ret = 0
    try:
        if not IS_PY37_OR_GREATER:
            # Prevent going to any other thread... if we switch the thread during this operation we
            # could potentially corrupt the interpreter.
            # Note: on CPython 3.7 onwards this is not needed (we have a different implementation
            # for setting the tracing for other threads in this case).
            set_interval(2 ** 15)

        set_trace_func = TracingFunctionHolder._original_tracing or sys.settrace

        # Note: use sys._current_frames() keys to get the thread ids because it'll return
        # thread ids created in C/C++ where there's user code running, unlike the APIs
        # from the threading module which see only threads created through it (unless
        # a call for threading.current_thread() was previously done in that thread,
        # in which case a dummy thread would've been created for it).
        thread_idents = set(sys._current_frames().keys())

        if IS_WINDOWS and IS_PYCHARM_ATTACH:
            # On Windows, when attaching to a process, some existing threads may not
            # appear in sys._current_frames()` but be available through the `threading`
            # facilities. See: PY-44778.
            thread_idents = thread_idents.union(
                set(t.ident for t in threading.enumerate()))

        thread_idents = thread_idents.difference(
            # Ignore pydevd threads.
            set(t.ident for t in threading.enumerate() if getattr(t, 'pydev_do_not_trace', False))
        )

        curr_ident = thread.get_ident()
        curr_thread = threading._active.get(curr_ident)

        for thread_ident in thread_idents:
            # If that thread is not available in the threading module we also need to create a
            # dummy thread for it (otherwise it'll be invisible to the debugger).
            if thread_ident not in threading._active:

                class _DummyThread(threading._DummyThread):

                    def _set_ident(self):
                        # Note: Hack to set the thread ident that we want.
                        if IS_PY2:
                            self._Thread__ident = thread_ident
                        else:
                            self._ident = thread_ident

                t = _DummyThread()
                # Reset to the base class (don't expose our own version of the class).
                t.__class__ = threading._DummyThread

                with threading._active_limbo_lock:
                    # On Py2 it'll put in active getting the current indent, not using the
                    # ident that was set, so, we have to update it (should be harmless on Py3
                    # so, do it always).
                    threading._active[thread_ident] = t
                    threading._active[curr_ident] = curr_thread

                    if t.ident != thread_ident:
                        # Check if it actually worked.
                        pydev_log.error('pydevd: creation of _DummyThread with fixed thread ident did not succeed.')

            # Some (ptvsd) tests failed because of this, so, leave it always disabled for now.
            # show_debug_info = 1 if DebugInfoHolder.DEBUG_TRACE_LEVEL >= 1 else 0
            show_debug_info = 0

            if IS_PY37_OR_GREATER:
                # Hack to increase _Py_TracingPossible.
                # See comments on py_settrace_37.hpp
                proceed = thread.allocate_lock()
                proceed.acquire()

                def dummy_trace_on_py37(frame, event, arg):
                    return dummy_trace_on_py37

                def increase_tracing_count_on_py37():
                    SetTrace(dummy_trace_on_py37)
                    proceed.release()

                start_new_thread = pydev_monkey.get_original_start_new_thread(thread)
                start_new_thread(increase_tracing_count_on_py37, ())
                proceed.acquire()  # Only proceed after the release() is done.
                proceed = None

            result = lib.AttachDebuggerTracing(
                ctypes.c_int(show_debug_info),
                ctypes.py_object(set_trace_func),
                ctypes.py_object(tracing_func),
                ctypes.c_uint(thread_ident),
                ctypes.py_object(None),
            )
            if result != 0:
                pydev_log.info('Unable to set tracing for existing threads. Result: %s' % result)
                ret = result
    finally:
        if not IS_PY37_OR_GREATER:
            set_interval(prev_value)

    return ret
