from _pydevd_bundle.pydevd_constants import DebugInfoHolder, SHOW_COMPILE_CYTHON_COMMAND_LINE, NULL, LOG_TIME, ForkSafeLock
from contextlib import contextmanager
import traceback
import os
import sys


class _LoggingGlobals(object):
    _warn_once_map = {}
    _debug_stream_filename = None
    _debug_stream = NULL
    _debug_stream_initialized = False
    _initialize_lock = ForkSafeLock()


def initialize_debug_stream(reinitialize=False):
    """
    :param bool reinitialize:
        Reinitialize is used to update the debug stream after a fork (thus, if it wasn't
        initialized, we don't need to do anything, just wait for the first regular log call
        to initialize).
    """
    if reinitialize:
        if not _LoggingGlobals._debug_stream_initialized:
            return
    else:
        if _LoggingGlobals._debug_stream_initialized:
            return

    with _LoggingGlobals._initialize_lock:
        # Initialization is done lazilly, so, it's possible that multiple threads try to initialize
        # logging.

        # Check initial conditions again after obtaining the lock.
        if reinitialize:
            if not _LoggingGlobals._debug_stream_initialized:
                return
        else:
            if _LoggingGlobals._debug_stream_initialized:
                return

        _LoggingGlobals._debug_stream_initialized = True

        # Note: we cannot initialize with sys.stderr because when forking we may end up logging things in 'os' calls.
        _LoggingGlobals._debug_stream = NULL
        _LoggingGlobals._debug_stream_filename = None

        if not DebugInfoHolder.PYDEVD_DEBUG_FILE:
            _LoggingGlobals._debug_stream = sys.stderr
        else:
            # Add pid to the filename.
            try:
                target_file = DebugInfoHolder.PYDEVD_DEBUG_FILE
                debug_file = _compute_filename_with_pid(target_file)
                _LoggingGlobals._debug_stream = open(debug_file, "w")
                _LoggingGlobals._debug_stream_filename = debug_file
            except Exception:
                _LoggingGlobals._debug_stream = sys.stderr
                # Don't fail when trying to setup logging, just show the exception.
                traceback.print_exc()


def _compute_filename_with_pid(target_file, pid=None):
    # Note: used in tests.
    dirname = os.path.dirname(target_file)
    basename = os.path.basename(target_file)
    try:
        os.makedirs(dirname)
    except Exception:
        pass  # Ignore error if it already exists.

    name, ext = os.path.splitext(basename)
    if pid is None:
        pid = os.getpid()
    return os.path.join(dirname, "%s.%s%s" % (name, pid, ext))


def log_to(log_file: str, log_level: int = 3) -> None:
    with _LoggingGlobals._initialize_lock:
        # Can be set directly.
        DebugInfoHolder.DEBUG_TRACE_LEVEL = log_level

        if DebugInfoHolder.PYDEVD_DEBUG_FILE != log_file:
            # Note that we don't need to reset it unless it actually changed
            # (would be the case where it's set as an env var in a new process
            # and a subprocess initializes logging to the same value).
            _LoggingGlobals._debug_stream = NULL
            _LoggingGlobals._debug_stream_filename = None

            DebugInfoHolder.PYDEVD_DEBUG_FILE = log_file

            _LoggingGlobals._debug_stream_initialized = False


def list_log_files(pydevd_debug_file):
    log_files = []
    dirname = os.path.dirname(pydevd_debug_file)
    basename = os.path.basename(pydevd_debug_file)
    if os.path.isdir(dirname):
        name, ext = os.path.splitext(basename)
        for f in os.listdir(dirname):
            if f.startswith(name) and f.endswith(ext):
                log_files.append(os.path.join(dirname, f))
    return log_files


@contextmanager
def log_context(trace_level, stream):
    """
    To be used to temporarily change the logging settings.
    """
    with _LoggingGlobals._initialize_lock:
        original_trace_level = DebugInfoHolder.DEBUG_TRACE_LEVEL
        original_debug_stream = _LoggingGlobals._debug_stream
        original_pydevd_debug_file = DebugInfoHolder.PYDEVD_DEBUG_FILE
        original_debug_stream_filename = _LoggingGlobals._debug_stream_filename
        original_initialized = _LoggingGlobals._debug_stream_initialized

        DebugInfoHolder.DEBUG_TRACE_LEVEL = trace_level
        _LoggingGlobals._debug_stream = stream
        _LoggingGlobals._debug_stream_initialized = True
    try:
        yield
    finally:
        with _LoggingGlobals._initialize_lock:
            DebugInfoHolder.DEBUG_TRACE_LEVEL = original_trace_level
            _LoggingGlobals._debug_stream = original_debug_stream
            DebugInfoHolder.PYDEVD_DEBUG_FILE = original_pydevd_debug_file
            _LoggingGlobals._debug_stream_filename = original_debug_stream_filename
            _LoggingGlobals._debug_stream_initialized = original_initialized


import time

_last_log_time = time.time()

# Set to True to show pid in each logged message (usually the file has it, but sometimes it's handy).
_LOG_PID = False


def _pydevd_log(level, msg, *args):
    """
    Levels are:

    0 most serious warnings/errors (always printed)
    1 warnings/significant events
    2 informational trace
    3 verbose mode
    """
    if level <= DebugInfoHolder.DEBUG_TRACE_LEVEL:
        # yes, we can have errors printing if the console of the program has been finished (and we're still trying to print something)
        try:
            try:
                if args:
                    msg = msg % args
            except:
                msg = "%s - %s" % (msg, args)

            if LOG_TIME:
                global _last_log_time
                new_log_time = time.time()
                time_diff = new_log_time - _last_log_time
                _last_log_time = new_log_time
                msg = "%.2fs - %s\n" % (
                    time_diff,
                    msg,
                )
            else:
                msg = "%s\n" % (msg,)

            if _LOG_PID:
                msg = "<%s> - %s\n" % (
                    os.getpid(),
                    msg,
                )

            try:
                try:
                    initialize_debug_stream()  # Do it as late as possible
                    _LoggingGlobals._debug_stream.write(msg)
                except TypeError:
                    if isinstance(msg, bytes):
                        # Depending on the StringIO flavor, it may only accept unicode.
                        msg = msg.decode("utf-8", "replace")
                        _LoggingGlobals._debug_stream.write(msg)
            except UnicodeEncodeError:
                # When writing to the stream it's possible that the string can't be represented
                # in the encoding expected (in this case, convert it to the stream encoding
                # or ascii if we can't find one suitable using a suitable replace).
                encoding = getattr(_LoggingGlobals._debug_stream, "encoding", "ascii")
                msg = msg.encode(encoding, "backslashreplace")
                msg = msg.decode(encoding)
                _LoggingGlobals._debug_stream.write(msg)

            _LoggingGlobals._debug_stream.flush()
        except:
            pass
        return True


def _pydevd_log_exception(msg="", *args):
    if msg or args:
        _pydevd_log(0, msg, *args)
    try:
        initialize_debug_stream()  # Do it as late as possible
        traceback.print_exc(file=_LoggingGlobals._debug_stream)
        _LoggingGlobals._debug_stream.flush()
    except:
        raise


def verbose(msg, *args):
    if DebugInfoHolder.DEBUG_TRACE_LEVEL >= 3:
        _pydevd_log(3, msg, *args)


def debug(msg, *args):
    if DebugInfoHolder.DEBUG_TRACE_LEVEL >= 2:
        _pydevd_log(2, msg, *args)


def info(msg, *args):
    if DebugInfoHolder.DEBUG_TRACE_LEVEL >= 1:
        _pydevd_log(1, msg, *args)


warn = info


def critical(msg, *args):
    _pydevd_log(0, msg, *args)


def exception(msg="", *args):
    try:
        _pydevd_log_exception(msg, *args)
    except:
        pass  # Should never fail (even at interpreter shutdown).


error = exception


def error_once(msg, *args):
    try:
        if args:
            message = msg % args
        else:
            message = str(msg)
    except:
        message = "%s - %s" % (msg, args)

    if message not in _LoggingGlobals._warn_once_map:
        _LoggingGlobals._warn_once_map[message] = True
        critical(message)


def exception_once(msg, *args):
    try:
        if args:
            message = msg % args
        else:
            message = str(msg)
    except:
        message = "%s - %s" % (msg, args)

    if message not in _LoggingGlobals._warn_once_map:
        _LoggingGlobals._warn_once_map[message] = True
        exception(message)


def debug_once(msg, *args):
    if DebugInfoHolder.DEBUG_TRACE_LEVEL >= 3:
        error_once(msg, *args)


def show_compile_cython_command_line():
    if SHOW_COMPILE_CYTHON_COMMAND_LINE:
        dirname = os.path.dirname(os.path.dirname(__file__))
        error_once(
            'warning: Debugger speedups using cython not found. Run \'"%s" "%s" build_ext --inplace\' to build.',
            sys.executable,
            os.path.join(dirname, "setup_pydevd_cython.py"),
        )
