# -*- coding: utf-8 -*-

# Copyright (c) 2002, 2003 Detlev Offenbach <detlev@die-offenbachs.de>
# Copyright (c) 2000 Phil Thompson <phil@river-bank.demon.co.uk>
#

"""
Module implementing the debug base class.
"""

import sys
import traceback
import bdb
import os
import types
import atexit

from DebugProtocol import *

def printerr(s):
    import sys
    """
    Module function used for debugging the debug client.
    
    Arguments
    
        s -- the data to be printed
    """
    sys.__stderr__.write('%s\n' % str(s))
    sys.__stderr__.flush()

class DebugBase(bdb.Bdb):
    """
    Class implementing base class of the debugger.

    Provides simple wrapper methods around bdb for the 'owning' client to
    call to step etc.
    """
    def __init__(self, dbgClient):
        """
        Constructor
        
        Arguments
        
            dbgClient -- the owning client
        """
        bdb.Bdb.__init__(self)

        self._dbgClient = dbgClient
        self._mainThread = 1
        
        self.breaks = self._dbgClient.breakpoints
        
        self.currentFrame = None # current frame we are at
        self.stepFrame = None # frame that we are stepping in, can be different than currentFrame

    def getCurrentFrame(self):
        """
        Public method to return the current frame.
        
        Returns
        
            the current frame
        """
        return self.currentFrame        
            
    def step(self, traceMode):
        """
        Public method to perform a step operation in this thread.
        
        Arguments
        
            tracemode -- if it is non-zero, then the step is a step into,
              otherwise it is a step over.
        """
        self.stepFrame = self.currentFrame
        
        if traceMode:
            self.currentFrame = None
            self.set_step()
        else:
            self.set_next(self.currentFrame)
    
    def stepOut(self):
        """
        Public method to perform a step out of the current call.
        """
        self.stepFrame = self.currentFrame
        self.set_return(self.currentFrame)
    
    def go(self):
        """
        Public method to resume the thread.

        It resumes the thread stopping only at breakpoints or exceptions.
        """
        self.currentFrame = None
        self.set_continue()
        
    def dispatch_return(self, frame, arg):
        """
        Reimplemented from bdb.py to handle passive mode cleanly.
        """
        if self.stop_here(frame) or frame == self.returnframe:
            self.user_return(frame, arg)
            if self.quitting and not self._dbgClient.passive:
                raise bdb.BdbQuit
        return self.trace_dispatch

    def dispatch_exception(self, frame, arg):
        """
        Reimplemented from bdb.py to always call user_exception.
        """
        self.user_exception(frame, arg)
        if self.quitting: raise bdb.BdbQuit
        return self.trace_dispatch

    def set_continue(self):
        """
        Reimplemented from bdb.py to always get informed of exceptions.
        """
        # Modified version of the one found in bdb.py
        # Here we leave tracing switched on in order to get
        # informed of exceptions
        self.stopframe = self.botframe
        self.returnframe = None
        self.quitting = 0

    def set_quit(self):
        """
        Public method to quit. 
        
        It wraps call to bdb to clear the current frame properly.
        """
        self.currentFrame = None
        bdb.Bdb.set_quit(self)
        
    def fix_frame_filename(self, frame):
        """
        Protected method used to fixup the filename for a given frame.
        
        The logic employed here is that if a module was loaded
        from a .pyc file, then the correct .py to operate with
        should be in the same path as the .pyc. The reason this
        logic is needed is that when a .pyc file is generated, the
        filename embedded and thus what is readable in the code object
        of the frame object is the fully qualified filepath when the
        pyc is generated. If files are moved from machine to machine
        this can break debugging as the .pyc will refer to the .py
        on the original machine. Another case might be sharing
        code over a network... This logic deals with that.
        
        Arguments
        
            frame -- the frame object
        """
        # get module name from __file__
        if frame.f_globals.has_key('__file__'):
            root, ext = os.path.splitext(frame.f_globals['__file__'])
            if ext == '.pyc' or ext == '.py' or ext == '.pyo':
                fixedName = root + '.py'
                if os.path.exists(fixedName):
                    return fixedName

        return frame.f_code.co_filename

    def break_here(self, frame):
        """
        Reimplemented from bdb.py to fix the filename from the frame. 
        
        See fix_frame_filename for more info.
        
        Arguments
        
            frame -- the frame object
            
        Returns
        
            flag indicating the break status (boolean)
        """
        filename = self.canonic(self.fix_frame_filename(frame))
        if not self.breaks.has_key(filename):
            return 0
        lineno = frame.f_lineno
        if not lineno in self.breaks[filename]:
            return 0
        # flag says ok to delete temp. bp
        (bp, flag) = bdb.effective(filename, lineno, frame)
        if bp:
            self.currentbp = bp.number
            if (flag and bp.temporary):
                self.do_clear(filename, lineno)
            return 1
        else:
            return 0

    def break_anywhere(self, frame):
        """
        Reimplemented from bdb.py to fix the filename from the frame. 
        
        See fix_frame_filename for more info.
        
        Arguments
        
            frame -- the frame object
            
        Returns
        
            flag indicating the break status (boolean)
        """
        return self.breaks.has_key(
            self.canonic(self.fix_frame_filename(frame)))

    def get_break(self, filename, lineno):
        """
        Reimplemented from bdb.py to get the first breakpoint of a particular line.
        
        Because eric3 supports only one breakpoint per line, this overwritten
        method will return this one and only breakpoint.
        
        Arguments
        
            filename -- the filename of the bp to retrieve (string)
            
            lineno -- the linenumber of the bp to retrieve (integer)
            
        Returns
        
            breakpoint or None, if there is no bp
        """
        filename = self.canonic(filename)
        return self.breaks.has_key(filename) and \
            lineno in self.breaks[filename] and \
            bdb.Breakpoint.bplist[filename, lineno][0] or None
            
    def do_clear(self, filename, lineno):
        """
        Private method called to clear a temporary breakpoint.
        
        Arguments
        
            filename -- name of the file the bp belongs to
            
            lineno -- linenumber of the bp
        """
        self.clear_break(filename, lineno)
        self._dbgClient.write('%s%s,%d\n' % (ResponseClearBreak, filename, lineno))

    def user_line(self,frame):
        """
        Reimplemented to handle the program about to execute a particular line.
        
        Arguments
        
            frame -- the frame object
        """
        line = frame.f_lineno

        # We never stop an line 0.
        if line == 0:
            return

        fn = self._dbgClient.absPath(self.fix_frame_filename(frame))

        # See if we are skipping at the start of a newly loaded program.
        if self._dbgClient.mainFrame is None:
            if fn != self._dbgClient.getRunning():
                return
            self._dbgClient.mainFrame = frame

        self.currentFrame = frame
        
        fr = frame
        stack = []
        while fr is not None:
            # Reset the trace function so we can be sure
            # to trace all functions up the stack... This gets around
            # problems where an exception/breakpoint has occurred
            # but we had disabled tracing along the way via a None
            # return from dispatch_call
            fr.f_trace = self.trace_dispatch
            fname = self._dbgClient.absPath(self.fix_frame_filename(fr))
            fline = fr.f_lineno
            ffunc = fr.f_code.co_name
            
            if ffunc == '?':
                ffunc = ''
            
            stack.append((fname, fline, ffunc))
            
            if fr == self._dbgClient.mainFrame:
                fr = None
            else:
                fr = fr.f_back
        
        self._dbgClient.write('%s%s\n' % (ResponseLine,str(stack)))
        self._dbgClient.eventLoop()

    def user_exception(self,frame,(exctype,excval,exctb),unhandled=0):
        """
        Reimplemented to report an exception to the debug server.
        
        Arguments
        
            frame -- the frame object
            
            exctype -- the type of the exception
            
            excval -- data about the exception
            
            exctb -- traceback for the exception
            
            unhandled -- flag indicating an uncaught exception
        """
        if exctype == SystemExit:
            atexit._run_exitfuncs()
            self._dbgClient.progTerminated(excval)
            
        elif exctype in [SyntaxError, IndentationError]:
            try:
                message, (filename, linenr, charnr, text) = excval
            except:
                exclist = []
            else:
                exclist = [message, (filename, linenr, charnr)]
            
            self._dbgClient.write("%s%s\n" % (ResponseSyntax, str(exclist)))
            
        else:
            self.currentFrame = frame

            if type(exctype) == types.ClassType:
                exctype = exctype.__name__
                
            if excval is None:
                excval = ''
                
            if unhandled:
                exclist = ["unhandled %s" % str(exctype), str(excval)]
            else:
                exclist = [str(exctype), str(excval)]
            
            frlist = self.extract_stack(exctb)
            frlist.reverse()
            
            for fr in frlist:
                filename = self._dbgClient.absPath(self.fix_frame_filename(fr))
                linenr = fr.f_lineno
                
                exclist.append((filename, linenr))
            
            self._dbgClient.write("%s%s\n" % (ResponseException, str(exclist)))
            
        self._dbgClient.eventLoop()
        
    def extract_stack(self, exctb):
        """
        Protected member to return a list of stack frames.
        
        Arguments
        
            exctb -- exception traceback
            
        Returns
        
            list of stack frames
        """
        tb = exctb
        stack = []
        while tb is not None:
            stack.append(tb.tb_frame)
            tb = tb.tb_next
        tb = None
        return stack

    def user_return(self,frame,retval):
        """
        Reimplemented to report program termination to the debug server.
        
        Arguments
        
            frame -- the frame object
            
            retval -- the return value of the program
        """
        # The program has finished if we have just left the first frame.
        if frame == self._dbgClient.mainFrame and \
            self._mainThread:
            atexit._run_exitfuncs()
            self._dbgClient.progTerminated(retval)
        elif frame is not self.stepFrame:
            self.stepFrame = None
            self.user_line(frame)

    def stop_here(self,frame):
        """
        Reimplemented to filter out debugger files.
        
        Tracing is turned off for files that are part of the
        debugger that are called from the application being debugged.
        
        Arguments
        
            frame -- the frame object
            
        Returns
        
            flag indicating whether the debugger should stop here
        """
        fn = self.fix_frame_filename(frame)

        # Eliminate things like <string> and <stdin>.
        if fn[0] == '<':
            return 0

        #XXX - think of a better way to do this.  It's only a convience for
        #debugging the debugger - when the debugger code is in the current
        #directory.
        if os.path.basename(fn) in [\
            'AsyncFile.py', 
            'AsyncIOBase.py', 'AsyncIO.py',
            'DebugBase.py', 'DebugThread.py', 
            'DebugClientBase.py', 'DebugClient', 
            'DebugClientNoQt.py', 'DebugClientThreads.py'
        ]:
            return 0

        if self._dbgClient.shouldSkip(fn):
            return 0

        return bdb.Bdb.stop_here(self,frame)

