"""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
"""

try:
    import readline
except ImportError:
    readline = None
import os
import os.path as osp
import sys
from pdb import Pdb
from cStringIO import StringIO
import inspect

try:
    from IPython import PyColorize
except ImportError:
    def colorize(source, *args):
        """fallback colorize function"""
        return source
else:
    def colorize(source, start_lineno, curlineno):
        """"""
        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('%4s\t->\t%s' % (lineno, line))
            else:
                annotated.append('%4s\t\t%s' % (lineno, line))                
        return '\n'.join(annotated)

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):
        Pdb.__init__(self)
        self.reset()
        while tcbk.tb_next is not None:
            tcbk = tcbk.tb_next
        self._tcbk = tcbk
        self._histfile = osp.join(os.environ["HOME"], ".pdbhist")
        
    def setup_history_file(self):
        """if readline is available, read pdb history file
        """
        if readline is not None:
            try:
                readline.read_history_file(self._histfile)
            except IOError:
                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("%s.%s" % (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 / overidden 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 IOError:
                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 = 'emacsclient --no-wait +%s %s' % (lineno, filename)
        os.system(cmd)
        
    do_o = do_open


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

