# Copyright (c) 2011-2016 Godefroid Chapelle and ipdb development team
#
# This file is part of ipdb.
# Redistributable under the revised BSD license
# https://opensource.org/licenses/BSD-3-Clause

from __future__ import print_function
import os
import sys

from decorator import contextmanager

__version__ = '0.13.13'

from IPython import get_ipython
from IPython.core.debugger import BdbQuit_excepthook
from IPython.terminal.ipapp import TerminalIPythonApp
from IPython.terminal.embed import InteractiveShellEmbed
try:
    import configparser
except:
    import ConfigParser as configparser


def _get_debugger_cls():
    shell = get_ipython()
    if shell is None:
        # Not inside IPython
        # Build a terminal app in order to force ipython to load the
        # configuration
        ipapp = TerminalIPythonApp()
        # Avoid output (banner, prints)
        ipapp.interact = False
        ipapp.initialize(["--no-term-title"])
        shell = ipapp.shell
    else:
        # Running inside IPython

        # Detect if embed shell or not and display a message
        if isinstance(shell, InteractiveShellEmbed):
            sys.stderr.write(
                "\nYou are currently into an embedded ipython shell,\n"
                "the configuration will not be loaded.\n\n"
            )

    # Let IPython decide about which debugger class to use
    # This is especially important for tools that fiddle with stdout
    return shell.debugger_cls


def _init_pdb(context=None, commands=[]):
    if context is None:
        context = os.getenv("IPDB_CONTEXT_SIZE", get_context_from_config())
    debugger_cls = _get_debugger_cls()
    try:
        p = debugger_cls(context=context)
    except TypeError:
        p = debugger_cls()
    p.rcLines.extend(commands)
    return p


def wrap_sys_excepthook():
    # make sure we wrap it only once or we would end up with a cycle
    #  BdbQuit_excepthook.excepthook_ori == BdbQuit_excepthook
    if sys.excepthook != BdbQuit_excepthook:
        BdbQuit_excepthook.excepthook_ori = sys.excepthook
        sys.excepthook = BdbQuit_excepthook


def set_trace(frame=None, context=None, cond=True):
    if not cond:
        return
    wrap_sys_excepthook()
    if frame is None:
        frame = sys._getframe().f_back
    p = _init_pdb(context).set_trace(frame)
    if p and hasattr(p, 'shell'):
        p.shell.restore_sys_module_state()


def get_context_from_config():
    parser = get_config()
    try:
        return parser.getint("ipdb", "context")
    except (configparser.NoSectionError, configparser.NoOptionError):
        return 3
    except ValueError:
        value = parser.get("ipdb", "context")
        raise ValueError(
            "In %s,  context value [%s] cannot be converted into an integer."
            % (parser.filepath, value)
        )


class ConfigFile(object):
    """
    Filehandle wrapper that adds a "[ipdb]" section to the start of a config
    file so that users don't actually have to manually add a [ipdb] section.
    Works with configparser versions from both Python 2 and 3
    """

    def __init__(self, filepath):
        self.first = True
        with open(filepath) as f:
            self.lines = f.readlines()

    # Python 2.7 (Older dot versions)
    def readline(self):
        try:
            return self.__next__()
        except StopIteration:
            return ''

    # Python 2.7 (Newer dot versions)
    def next(self):
        return self.__next__()

    # Python 3
    def __iter__(self):
        return self

    def __next__(self):
        if self.first:
            self.first = False
            return "[ipdb]\n"
        if self.lines:
            return self.lines.pop(0)
        raise StopIteration


def get_config():
    """
    Get ipdb config file settings.
    All available config files are read.  If settings are in multiple configs,
    the last value encountered wins.  Values specified on the command-line take
    precedence over all config file settings.
    Returns: A ConfigParser object.
    """
    parser = configparser.ConfigParser()

    filepaths = []

    # Low priority goes first in the list
    for cfg_file in ("setup.cfg", ".ipdb", "pyproject.toml"):
        cwd_filepath = os.path.join(os.getcwd(), cfg_file)
        if os.path.isfile(cwd_filepath):
            filepaths.append(cwd_filepath)

    # Medium priority (whenever user wants to set a specific path to config file)
    home = os.getenv("HOME")
    if home:
        default_filepath = os.path.join(home, ".ipdb")
        if os.path.isfile(default_filepath):
            filepaths.append(default_filepath)

    # High priority (default files)
    env_filepath = os.getenv("IPDB_CONFIG")
    if env_filepath and os.path.isfile(env_filepath):
        filepaths.append(env_filepath)

    if filepaths:
        # Python 3 has parser.read_file(iterator) while Python2 has
        # parser.readfp(obj_with_readline)
        try:
            read_func = parser.read_file
        except AttributeError:
            read_func = parser.readfp
        for filepath in filepaths:
            parser.filepath = filepath
            # Users are expected to put an [ipdb] section
            # only if they use setup.cfg
            if filepath.endswith('setup.cfg'):
                with open(filepath) as f:
                    parser.remove_section("ipdb")
                    read_func(f)
            # To use on pyproject.toml, put [tool.ipdb] section
            elif filepath.endswith('pyproject.toml'):
                try:
                    import tomllib
                    file_mode = "rb"
                except ImportError:
                    try:
                        import tomli as tomllib
                        file_mode = "rb"
                    except ImportError:
                        import toml as tomllib
                        file_mode = "r"
                with open(filepath, file_mode) as f:
                    toml_file = tomllib.load(f)
                    if "tool" in toml_file and "ipdb" in toml_file["tool"]:
                        if not parser.has_section("ipdb"):
                            parser.add_section("ipdb")
                        for key, value in toml_file["tool"]["ipdb"].items():
                            parser.set("ipdb", key, str(value))
            else:
                read_func(ConfigFile(filepath))
    return parser


def post_mortem(tb=None):
    wrap_sys_excepthook()
    p = _init_pdb()
    p.reset()
    if tb is None:
        # sys.exc_info() returns (type, value, traceback) if an exception is
        # being handled, otherwise it returns None
        tb = sys.exc_info()[2]
    if tb:
        p.interaction(None, tb)


def pm():
    post_mortem(sys.last_traceback)


def run(statement, globals=None, locals=None):
    _init_pdb().run(statement, globals, locals)


def runcall(*args, **kwargs):
    return _init_pdb().runcall(*args, **kwargs)


def runeval(expression, globals=None, locals=None):
    return _init_pdb().runeval(expression, globals, locals)


@contextmanager
def launch_ipdb_on_exception():
    try:
        yield
    except Exception:
        e, m, tb = sys.exc_info()
        print(m.__repr__(), file=sys.stderr)
        post_mortem(tb)
    finally:
        pass


# iex is a concise alias
iex = launch_ipdb_on_exception()


_usage = """\
usage: python -m ipdb [-m] [-c command] ... pyfile [arg] ...

Debug the Python program given by pyfile.

Initial commands are read from .pdbrc files in your home directory
and in the current directory, if they exist.  Commands supplied with
-c are executed after commands from .pdbrc files.

To let the script run until an exception occurs, use "-c continue".
To let the script run up to a given line X in the debugged file, use
"-c 'until X'"

Option -m is available only in Python 3.7 and later.

ipdb version %s.""" % __version__


def main():
    import traceback
    import sys
    import getopt

    try:
        from pdb import Restart
    except ImportError:
        class Restart(Exception):
            pass

    if sys.version_info >= (3, 7):
        opts, args = getopt.getopt(sys.argv[1:], 'mhc:', ['help', 'command='])
    else:
        opts, args = getopt.getopt(sys.argv[1:], 'hc:', ['help', 'command='])

    commands = []
    run_as_module = False
    for opt, optarg in opts:
        if opt in ['-h', '--help']:
            print(_usage)
            sys.exit()
        elif opt in ['-c', '--command']:
            commands.append(optarg)
        elif opt in ['-m']:
            run_as_module = True

    if not args:
        print(_usage)
        sys.exit(2)

    mainpyfile = args[0]     # Get script filename
    if not run_as_module and not os.path.exists(mainpyfile):
        print('Error:', mainpyfile, 'does not exist')
        sys.exit(1)

    sys.argv = args     # Hide "pdb.py" from argument list

    # Replace pdb's dir with script's dir in front of module search path.
    if not run_as_module:
        sys.path[0] = os.path.dirname(mainpyfile)

    # Note on saving/restoring sys.argv: it's a good idea when sys.argv was
    # modified by the script being debugged. It's a bad idea when it was
    # changed by the user from the command line. There is a "restart" command
    # which allows explicit specification of command line arguments.
    pdb = _init_pdb(commands=commands)
    while 1:
        try:
            import pdb as stdlib_pdb
            if hasattr(stdlib_pdb.Pdb, "_run"):
                # Looks like Pdb from Python 3.11+
                if run_as_module:
                    pdb._run(stdlib_pdb._ModuleTarget(mainpyfile))
                else:
                    pdb._run(stdlib_pdb._ScriptTarget(mainpyfile))
            else:
                if run_as_module:
                    pdb._runmodule(mainpyfile)
                else:
                    pdb._runscript(mainpyfile)
            if pdb._user_requested_quit:
                break
            print("The program finished and will be restarted")
        except Restart:
            print("Restarting", mainpyfile, "with arguments:")
            print("\t" + " ".join(sys.argv[1:]))
        except SystemExit:
            # In most cases SystemExit does not warrant a post-mortem session.
            print("The program exited via sys.exit(). Exit status: ", end='')
            print(sys.exc_info()[1])
        except:
            traceback.print_exc()
            print("Uncaught exception. Entering post mortem debugging")
            print("Running 'cont' or 'step' will restart the program")
            t = sys.exc_info()[2]
            pdb.interaction(None, t)
            print("Post mortem debugger finished. The " + mainpyfile +
                  " will be restarted")


if __name__ == '__main__':
    main()
