# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common.  If not, see <http://www.gnu.org/licenses/>.
"""Customized version of pdb's default debugger.

- sets up a history file
- uses ipython if available to colorize lines of code
- overrides list command to search for current block instead
  of using 5 lines of context




"""


__docformat__ = "restructuredtext en"

try:
    import readline
except ImportError:
    # mypy: Incompatible types in assignment (expression has type "None",
    # mypy: variable has type Module))
    # conditional import
    readline = None  # type: ignore
import os
import sys
from pdb import Pdb
import inspect

from io import StringIO

try:
    from IPython import PyColorize
except ImportError:

    def colorize(source, start_lineno, curlineno):
        """fallback colorize function"""
        return source

    def colorize_source(source):
        return source

else:

    def colorize(source, start_lineno, curlineno):
        """colorize and annotate source with linenos
        (as in pdb's list command)
        """
        parser = PyColorize.Parser()
        output = StringIO()
        parser.format(source, output)
        annotated = []
        for index, line in enumerate(output.getvalue().splitlines()):
            lineno = index + start_lineno
            if lineno == curlineno:
                annotated.append(f"{lineno:>4}\t->\t{line}")
            else:
                annotated.append(f"{lineno:>4}\t\t{line}")
        return "\n".join(annotated)

    def colorize_source(source):
        """colorize given source"""
        parser = PyColorize.Parser()
        output = StringIO()
        parser.format(source, output)
        return output.getvalue()


def getsource(obj):
    """Return the text of the source code for an object.

    The argument may be a module, class, method, function, traceback, frame,
    or code object.  The source code is returned as a single string.  An
    IOError is raised if the source code cannot be retrieved."""
    lines, lnum = inspect.getsourcelines(obj)
    return "".join(lines), lnum


################################################################
class Debugger(Pdb):
    """custom debugger

    - sets up a history file
    - uses ipython if available to colorize lines of code
    - overrides list command to search for current block instead
      of using 5 lines of context
    """

    def __init__(self, tcbk=None):
        Pdb.__init__(self)
        self.reset()
        if tcbk:
            while tcbk.tb_next is not None:
                tcbk = tcbk.tb_next
        self._tcbk = tcbk
        self._histfile = os.path.expanduser("~/.pdbhist")

    def setup_history_file(self):
        """if readline is available, read pdb history file"""
        if readline is not None:
            try:
                # XXX try..except shouldn't be necessary
                # read_history_file() can accept None
                readline.read_history_file(self._histfile)
            except OSError:
                pass

    def start(self):
        """starts the interactive mode"""
        self.interaction(self._tcbk.tb_frame, self._tcbk)

    def setup(self, frame, tcbk):
        """setup hook: set up history file"""
        self.setup_history_file()
        Pdb.setup(self, frame, tcbk)

    def set_quit(self):
        """quit hook: save commands in the history file"""
        if readline is not None:
            readline.write_history_file(self._histfile)
        Pdb.set_quit(self)

    def complete_p(self, text, line, begin_idx, end_idx):
        """provide variable names completion for the ``p`` command"""
        namespace = dict(self.curframe.f_globals)
        namespace.update(self.curframe.f_locals)
        if "." in text:
            return self.attr_matches(text, namespace)
        return [varname for varname in namespace if varname.startswith(text)]

    def attr_matches(self, text, namespace):
        """implementation coming from rlcompleter.Completer.attr_matches
        Compute matches when text contains a dot.

        Assuming the text is of the form NAME.NAME....[NAME], and is
        evaluatable in self.namespace, it will be evaluated and its attributes
        (as revealed by dir()) are used as possible completions.  (For class
        instances, class members are also considered.)

        WARNING: this can still invoke arbitrary C code, if an object
        with a __getattr__ hook is evaluated.

        """
        import re

        m = re.match(r"(\w+(\.\w+)*)\.(\w*)", text)
        if not m:
            return
        expr, attr = m.group(1, 3)
        object = eval(expr, namespace)
        words = dir(object)
        if hasattr(object, "__class__"):
            words.append("__class__")
            words = words + self.get_class_members(object.__class__)
        matches = []
        n = len(attr)
        for word in words:
            if word[:n] == attr and word != "__builtins__":
                matches.append(f"{expr}.{word}")
        return matches

    def get_class_members(self, klass):
        """implementation coming from rlcompleter.get_class_members"""
        ret = dir(klass)
        if hasattr(klass, "__bases__"):
            for base in klass.__bases__:
                ret = ret + self.get_class_members(base)
        return ret

    # specific / overridden commands
    def do_list(self, arg):
        """overrides default list command to display the surrounding block
        instead of 5 lines of context
        """
        self.lastcmd = "list"
        if not arg:
            try:
                source, start_lineno = getsource(self.curframe)
                print(colorize("".join(source), start_lineno, self.curframe.f_lineno))
            except KeyboardInterrupt:
                pass
            except OSError:
                Pdb.do_list(self, arg)
        else:
            Pdb.do_list(self, arg)

    do_l = do_list

    def do_open(self, arg):
        """opens source file corresponding to the current stack level"""
        filename = self.curframe.f_code.co_filename
        lineno = self.curframe.f_lineno
        cmd = f"emacsclient --no-wait +{lineno} {filename}"
        os.system(cmd)

    do_o = do_open


def pm():
    """use our custom debugger"""
    dbg = Debugger(sys.last_traceback)
    dbg.start()


def set_trace():
    Debugger().set_trace(sys._getframe().f_back)
