import os, pprint
from repr import Repr

import wx
import wx.lib.stattext

import Preferences, Utils
from Preferences import IS
from Explorers import Explorer

from Breakpoint import bplist

SEL_STATE = wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED

class DebuggerListCtrl(wx.ListView, Utils.ListCtrlSelectionManagerMix):
    def __init__(self, parent, wId):
        wx.ListView.__init__(self, parent, wId,
              style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_VRULES | wx.CLIP_CHILDREN)
        Utils.ListCtrlSelectionManagerMix.__init__(self)

wxID_STACKVIEW = wx.NewId()
class StackViewCtrl(DebuggerListCtrl):
    def __init__(self, parent, flist, debugger):
        DebuggerListCtrl.__init__(self, parent, wxID_STACKVIEW)

        self.InsertColumn(0, 'Frame', wx.LIST_FORMAT_LEFT, 150)
        self.InsertColumn(1, 'Line', wx.LIST_FORMAT_LEFT, 35)
        self.InsertColumn(2, 'Code', wx.LIST_FORMAT_LEFT, 300)

        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnStackItemSelected, id=wxID_STACKVIEW)
        self.Bind(wx.EVT_LEFT_DCLICK, self.OnGotoSource)

        self.flist = flist
        self.debugger = debugger
        self.stack = []

    def load_stack(self, stack, index=None):
        import linecache

        self.stack = stack
        data = []

        pos = 0
        count = self.GetItemCount()
        for entry in stack:
            lineno = entry['lineno']
            modname = entry['modname']
            filename = entry['client_filename']
            funcname = entry['funcname']
            sourceline = linecache.getline(filename, lineno)
            sourceline = sourceline.strip()
            if funcname in ("?", "", None):
                #item = "%s, line %d: %s" % (modname, lineno, sourceline)
                attrib = modname
            else:
                #item = "%s.%s(), line %d: %s" % (modname, funcname,
                #                                 lineno, sourceline)
                # XXX methods will be shown as "module.function"
                # when maybe they ought to be shown as "module.class.method".
                attrib = modname + '.' + funcname
            #if pos == index:
            #    item = "> " + item
            if pos >= count:
                # Insert.
                self.InsertStringItem(pos, attrib)
                count = count + 1
            else:
                # Update.
                self.SetStringItem(pos, 0, attrib, -1)
            self.SetStringItem(pos, 1, `lineno`, -1)
            self.SetStringItem(pos, 2, sourceline, -1)
            pos = pos + 1

        while pos < count:
            self.DeleteItem(count - 1)
            count = count - 1

    def OnStackItemSelected(self, event):
        selection = self.getSelection()
        stacklen = len(self.stack)
        if 0 <= selection < stacklen:
            self.debugger.invalidatePanes()
            self.debugger.updateSelectedPane()

    def selectCurrentEntry(self):
        selection = self.getSelection()
        newsel = self.GetItemCount() - 1
        if newsel != selection:
            if selection >= 0:
                item = self.GetItem(selection)
                item.m_state = item.m_state & ~wx.LIST_STATE_SELECTED
                self.SetItem(item)
            if newsel >= 0:
                item = self.GetItem(newsel)
                item.m_state = item.m_state | wx.LIST_STATE_SELECTED
                self.SetItem(item)
        if newsel >= 0:
            self.EnsureVisible(newsel)

    def OnGotoSource(self, event=None):
        selection = self.getSelection()
        if selection != -1:
            entry = self.stack[selection]
            lineno = entry['lineno']
            modname = entry['modname']
            filename = entry['client_filename']
            if not filename:
                return

            editor = self.debugger.editor
            editor.SetFocus()
            try:
                editor.openOrGotoModule(filename)
            except Explorer.TransportLoadError, err:
                serverPath = entry['filename']
                if serverPath[0] == '<' and serverPath[-1] == '>':
                    wx.LogError('Not a source file: %s, probably an executed '
                               'string.'%serverPath)
                    return

                res = wx.MessageBox('Could not open file: %s.\n\nIf This is a '
                      'server path for which you\nhave not defined a mapping '
                      'click "Yes" to browse to the file to the mapping can '
                      'be computed.\nPress "No" to open the path dialog.'%filename,
                      'File Open Error, try to compute path?',
                      wx.ICON_WARNING | wx.YES_NO | wx.CANCEL)
                if res == wx.YES:
                    clientPath = editor.openFileDlg(curfile=os.path.basename(filename))
                    if clientPath:
                        clientPath = prevClientPath = Explorer.splitURI(clientPath)[2]
                        prevServerPath = serverPath
                        while 1:
                            serverPath, serverBase = os.path.split(serverPath)
                            clientPath, clientBase = os.path.split(clientPath)

                            if serverBase != clientBase:
                                paths = self.debugger.serverClientPaths[:]
                                paths.append( (prevServerPath, prevClientPath) )
                                if self.debugger.OnPathMappings(paths=paths):
                                    self.refreshClientFilenames()
                                break

                            if not serverPath or not clientPath:
                                wx.LogError('Paths are identical')
                                break

                            prevClientPath = clientPath
                            prevServerPath = serverPath

                elif res == wxNO:
                    if self.debugger.OnPathMappings():
                        self.refreshClientFilenames()
                return

            model = editor.getActiveModulePage().model
            view = model.getSourceView()
            if view is not None:
                view.focus()
                view.SetFocus()
                view.selectLine(lineno - 1)

    def refreshClientFilenames(self):
        for entry in self.stack:
            entry['client_filename'] = \
                  self.debugger.serverFNToClientFN(entry['filename'])


[wxID_BREAKVIEW, wxID_BREAKSOURCE, wxID_BREAKEDIT, wxID_BREAKDELETE,
 wxID_BREAKENABLED, wxID_BREAKREFRESH, wxID_BREAKIGNORE] = Utils.wxNewIds(7)

class BreakViewCtrl(DebuggerListCtrl):
    def __init__(self, parent, debugger):
        DebuggerListCtrl.__init__(self, parent, wxID_BREAKVIEW)

        self.InsertColumn(0, 'Module', wx.LIST_FORMAT_LEFT, 90)
        self.InsertColumn(1, 'Line', wx.LIST_FORMAT_CENTER, 40)
        self.InsertColumn(2, 'Ignore', wx.LIST_FORMAT_CENTER, 45)
        self.InsertColumn(3, 'Hits', wx.LIST_FORMAT_CENTER, 45)
        self.InsertColumn(4, 'Condition', wx.LIST_FORMAT_LEFT, 250)

        self.brkImgLst = wx.ImageList(16, 16)
        self.brkImgLst.Add(IS.load('Images/Debug/Breakpoint-red.png'))
        self.brkImgLst.Add(IS.load('Images/Debug/Breakpoint-yellow.png'))
        self.brkImgLst.Add(IS.load('Images/Debug/Breakpoint-gray.png'))
        self.brkImgLst.Add(IS.load('Images/Debug/Breakpoint-blue.png'))

        self.Bind(wx.EVT_LEFT_DCLICK, self.OnGotoSource)

        self.debugger = debugger

        self.menu = wx.Menu()

        self.menu.Append(wxID_BREAKSOURCE, 'Goto source')
        self.menu.Append(wxID_BREAKREFRESH, 'Refresh')
        self.menu.AppendSeparator()
        self.menu.Append(wxID_BREAKIGNORE, 'Edit ignore count')
        self.menu.Append(wxID_BREAKEDIT, 'Edit condition')
        self.menu.Append(wxID_BREAKDELETE, 'Delete')
        self.menu.AppendSeparator()
        self.menu.Append(wxID_BREAKENABLED, 'Enabled', '', True)

        self.menu.Check(wxID_BREAKENABLED, True)

        self.Bind(wx.EVT_MENU, self.OnGotoSourceRight, id=wxID_BREAKSOURCE)
        self.Bind(wx.EVT_MENU, self.OnRefresh, id=wxID_BREAKREFRESH)
        self.Bind(wx.EVT_MENU, self.OnEditIgnore, id=wxID_BREAKIGNORE)
        self.Bind(wx.EVT_MENU, self.OnEditCondition, id=wxID_BREAKEDIT)
        self.Bind(wx.EVT_MENU, self.OnDelete, id=wxID_BREAKDELETE)
        self.Bind(wx.EVT_MENU, self.OnToggleEnabled, id=wxID_BREAKENABLED)
        self.pos = None

        self.setPopupMenu(self.menu)

        self.AssignImageList(self.brkImgLst, wx.IMAGE_LIST_SMALL)

        self.bps = []
        self.stats_map = {}

    def destroy(self):
        self.menu.Destroy()
        self.brkImgLst = None

    def updateBreakpointStats(self, stats):
        """Received from debugger.

        stats is a list of mappings."""
        stats_map = {}
        for item in stats:
            fn = item['client_filename']
            lineno = item['lineno']
            stats_map[(fn, lineno)] = item
            if not bplist.hasBreakpoint(fn, lineno):
                # A hard breakpoint was hit and a new breakpoint was created.
                bplist.addBreakpoint(fn, lineno)
        self.stats_map = stats_map

    def refreshList(self):
        self.DeleteAllItems()
        bps = bplist.getBreakpointList()
        # Sort by filename and lineno.
        bps.sort(lambda a, b:
                 cmp((a['filename'], a['lineno']),
                     (b['filename'], b['lineno'])))
        self.bps = bps
        for p in range(len(bps)):
            bp = bps[p]
            # setup prelim image
            imgIdx = 0
            if not bp['enabled']: imgIdx = 2
            elif bp['temporary']: imgIdx = 3

            self.InsertImageStringItem(
                p, os.path.basename(bp['filename']), imgIdx)
            self.SetStringItem(p, 1, str(bp['lineno']))

            hits = ''
            ignore = ''
            cond = ''
            if self.stats_map:
                item = self.stats_map.get((bp['filename'], bp['lineno']), None)
                if item is not None:
                    hits = str(item['hits'])
                    ignore = str(item['ignore'])
                    cond = item['cond'] or ''

            self.SetStringItem(p, 2, ignore)
            self.SetStringItem(p, 3, hits)
            self.SetStringItem(p, 4, cond)

    def addBreakpoint(self, filename, lineno):
        self.refreshList()

    def selectBreakpoint(self, filename, lineno):
        idx = 0
        for bp in self.bps:
            if bp['filename']==filename and bp['lineno']==lineno:
                self.SetItemState(idx, SEL_STATE, SEL_STATE)
                self.EnsureVisible(idx)
                return
            idx = idx + 1

    def OnGotoSource(self, event=None):
        sel = self.getSelection()
        if sel != -1:
            self.gotoSourceForItem(sel)

    def OnGotoSourceRight(self, event):
        sel = self.getSelection()
        if sel != -1:
            self.gotoSourceForItem(sel)

    def gotoSourceForItem(self, sel):
        bp = self.bps[sel]
        filename = bp['filename']
        if not filename:
            return
        editor = self.debugger.editor
        editor.SetFocus()
        model, ctrlr = editor.openOrGotoModule(filename)
        view = model.getSourceView()
        if view is not None:
            view.focus()
            view.GotoLine(bp['lineno'] - 1)

    def OnDelete(self, event):
        sel = self.getSelection()
        if sel != -1:
            bp = self.bps[sel]
            filename = bp['filename']
            bplist.deleteBreakpoints(filename, bp['lineno'])

            # Delete in debug server
            server_fn = self.debugger.clientFNToServerFN(filename)
            self.debugger.invokeInDebugger(
                'clearBreakpoints', (server_fn, bp['lineno']))

            # Unmark the breakpoint in the editor (if open)
            sourceView = self.debugger.getEditorSourceView(filename)
            if sourceView:
                sourceView.deleteBreakMarkers(bp['lineno'])

            #self.debugger.requestDebuggerStatus()
            self.refreshList()

    def OnRefresh(self, event):
        self.refreshList()

    def OnToggleEnabled(self, event):
        sel = self.getSelection()
        if sel != -1:
            bp = self.bps[sel]
            filename = bp['filename']
            lineno = bp['lineno']
            enabled = bp['enabled'] = not bp['enabled']
            bplist.enableBreakpoints(filename, lineno, enabled)
            server_fn = self.debugger.clientFNToServerFN(filename)
            self.debugger.invokeInDebugger(
                'enableBreakpoints', (server_fn, lineno, enabled))
            self.refreshList()

            sourceView = self.debugger.getEditorSourceView(filename)
            if sourceView:
                sourceView.deleteBreakMarkers(bp['lineno'])
                sourceView.setBreakMarker(bp)

    def getPopupMenu(self):
        wx.Yield()
        sel = self.getSelection()

        self.menu.Enable(wxID_BREAKSOURCE, sel != -1)
        self.menu.Enable(wxID_BREAKIGNORE, sel != -1)
        self.menu.Enable(wxID_BREAKEDIT, sel != -1)
        self.menu.Enable(wxID_BREAKDELETE, sel != -1)
        self.menu.Enable(wxID_BREAKENABLED, sel != -1)

        if sel != -1:
            bp = self.bps[sel]
            self.menu.Check(wxID_BREAKENABLED, bp['enabled'])

        return DebuggerListCtrl.getPopupMenu(self)

    def OnEditCondition(self, event):
        sel = self.getSelection()
        if sel != -1:
            bp = self.bps[sel]
            filename = bp['filename']
            lineno = bp['lineno']
            cond = bp['cond']

            dlg = wx.TextEntryDialog(self, 'Condition to break on:',
                  'Change condition', cond)
            try:
                if dlg.ShowModal() == wx.ID_OK:
                    cond = dlg.GetValue().strip()

                    bplist.conditionalBreakpoints(filename, lineno, cond)
                    # Update debug server
                    server_fn = self.debugger.clientFNToServerFN(filename)
                    self.debugger.invokeInDebugger(
                        'conditionalBreakpoints', (server_fn, lineno, cond))

                    self.debugger.requestDebuggerStatus()
                    #self.refreshList()
            finally:
                dlg.Destroy()

    def OnEditIgnore(self, event):
        sel = self.getSelection()
        if sel != -1:
            bp = self.bps[sel]
            filename = bp['filename']
            lineno = bp['lineno']
            ignore = bp['ignore']

            dlg = wx.TextEntryDialog(self, 'Number of hits to ignore:',
                  'Change ignore count', `ignore`)
            try:
                if dlg.ShowModal() == wx.ID_OK:
                    ignore = int(dlg.GetValue())
                    # Update debugger list and debug server
                    bplist.ignoreBreakpoints(filename, lineno, ignore)
                    server_fn = self.debugger.clientFNToServerFN(filename)
                    self.debugger.invokeInDebugger(
                        'ignoreBreakpoints', (server_fn, lineno, ignore))

                    self.debugger.requestDebuggerStatus()
                    #self.refreshList()
            finally:
                dlg.Destroy()

# XXX Expose classes' dicts as indented items

wxID_NSVIEW = wx.NewId()
class NamespaceViewCtrl(DebuggerListCtrl):
    def __init__(self, parent, debugger, is_local, name):
        DebuggerListCtrl.__init__(self, parent, wxID_NSVIEW)

        self.InsertColumn(0, 'Attribute', wx.LIST_FORMAT_LEFT, 125)
        self.InsertColumn(1, 'Value', wx.LIST_FORMAT_LEFT, 200)

        self.is_local = is_local

        self.menu = wx.Menu()

        idAs = wx.NewId()
        idA = wx.NewId()
        self.menu.Append(idAs, 'Add as watch')
        self.menu.Append(idA, 'Add a %s watch' % name)
        self.Bind(wx.EVT_MENU, self.OnAddAsWatch, id=idAs)
        self.Bind(wx.EVT_MENU, self.OnAddAWatch, id=idA)
        outputId = wx.NewId()
        self.menu.Append(outputId, 'Write value to Output')
        self.Bind(wx.EVT_MENU, self.OnValueToOutput, id=outputId)

        self.Bind(wx.EVT_LEFT_DCLICK, self.OnDoubleClick)

        self.pos = None

        self.setPopupMenu(self.menu)

        self.repr = Repr()
        self.repr.maxstring = 100
        self.repr.maxother = 100
        self.names = []

        self.debugger = debugger

    def destroy(self):
        self.menu.Destroy()

    def showLoading(self):
        self.DeleteAllItems()
        self.InsertStringItem(0, '...')

    def load_dict(self, nsdict, force=0):
        self.DeleteAllItems()

        if not nsdict:
            pass
        else:
            self.names = nsdict.keys()
            self.names.sort()
            row = 0
            for name in self.names:
                svalue = nsdict[name]

                self.InsertStringItem(row, name)
                self.SetStringItem(row, 1, svalue, -1)

                row = row + 1

    def OnAddAsWatch(self, event):
        selected = self.getSelection()
        if selected != -1:
            name = self.names[selected]
            self.debugger.add_watch(name, self.is_local)

    def OnAddAWatch(self, event):
        self.debugger.add_watch('', self.is_local)

    def OnValueToOutput(self, event):
        selected = self.getSelection()
        if selected != -1:
            name = self.names[selected]
            self.debugger.valueToOutput(name)

    def OnDoubleClick(self, event):
        if event.ControlDown():
            self.OnValueToOutput(event)
        else:
            self.OnAddAsWatch(event)


wxID_WATCHVIEW = wx.NewId()
class WatchViewCtrl(DebuggerListCtrl):
    def __init__(self, parent, images, debugger):
        DebuggerListCtrl.__init__(self, parent, wxID_WATCHVIEW)

        self.InsertColumn(0, 'Attribute', wx.LIST_FORMAT_LEFT, 125)
        self.InsertColumn(1, 'Value', wx.LIST_FORMAT_LEFT, 200)

        self.repr = Repr()
        self.repr.maxstring = 60
        self.repr.maxother = 60
        self.debugger = debugger

        self.watches = []

        self.AssignImageList(images, wx.IMAGE_LIST_SMALL)

        self.menu = wx.Menu()

        wid = wx.NewId()
        self.menu.Append(wid, 'Add local watch')
        self.Bind(wx.EVT_MENU, self.OnAddLocal, id=wid)
        wid = wx.NewId()
        self.menu.Append(wid, 'Add global watch')
        self.Bind(wx.EVT_MENU, self.OnAddGlobal, id=wid)
        self.editId = wx.NewId()
        self.menu.Append(self.editId, 'Edit watch')
        self.Bind(wx.EVT_MENU, self.OnEdit, id=self.editId)
        self.outputId = wx.NewId()
        self.menu.Append(self.outputId, 'Write value to Output')
        self.Bind(wx.EVT_MENU, self.OnValueToOutput, id=self.outputId)
        self.deleteId = wx.NewId()
        self.menu.Append(self.deleteId, 'Delete')
        self.Bind(wx.EVT_MENU, self.OnDelete, id=self.deleteId)
        self.expandId = wx.NewId()
        self.menu.Append(self.expandId, 'Expand')
        self.Bind(wx.EVT_MENU, self.OnExpand, id=self.expandId)
        wid = wx.NewId()
        self.menu.Append(wid, 'Delete All')
        self.Bind(wx.EVT_MENU, self.OnDeleteAll, id=wid)

        self.Bind(wx.EVT_LEFT_DCLICK, self.OnDoubleClick)

        self.pos = None

        self.setPopupMenu(self.menu)

    def destroy(self):
        self.menu.Destroy()

    def add_watch(self, name, local, pos=-1):
        if name:
            if pos < 0 or pos >= len(self.watches):
                self.watches.append((name, local))
                pos = len(self.watches)-1
            else:
                self.watches.insert(pos, (name, local))
        else:
            dlg = wx.TextEntryDialog(
                self, 'Expression:', 'Add a watch:', '')
            try:
                if dlg.ShowModal() == wx.ID_OK:
                    self.watches.append((dlg.GetValue(), local))
                    pos = len(self.watches)-1
            finally:
                dlg.Destroy()
        #self.SetItemState(pos, SEL_STATE, SEL_STATE)
        #self.EnsureVisible(pos)

    def showLoading(self):
        self.load_dict(None, loading=1)

    def load_dict(self, svalues, force=0, loading=0):
        count = self.GetItemCount()
        row = 0
        for name, local in self.watches:
            if svalues:
                svalue = svalues.get(name, '???')
            elif loading:
                svalue = '...'
            else:
                svalue = '???'
            if local:
                idx = 3
            else:
                idx = 4
            if row >= count:
                # Insert.
                self.InsertImageStringItem(row, name, idx)
                count = count + 1
            else:
                # Update.
                self.SetStringItem(row, 0, name, idx)
            self.SetStringItem(row, 1, svalue, idx)
            row = row + 1
        while row < count:
            self.DeleteItem(count - 1)
            count = count - 1

    def OnAddLocal(self, event):
        self.add_watch('', True)
        self.debugger.updateSelectedPane(force=1)

    def OnAddGlobal(self, event):
        self.add_watch('', False)
        self.debugger.updateSelectedPane(force=1)

    def OnEdit(self, event):
        selected = self.getSelection()

        if selected != -1:
            name, local = self.watches[selected]
            dlg = wx.TextEntryDialog(
                self, 'Expression:', 'Edit watch:', name)
            try:
                if dlg.ShowModal() == wx.ID_OK:
                    self.watches[selected] = (dlg.GetValue(), local)
                    self.debugger.updateSelectedPane(force=1)
            finally:
                dlg.Destroy()

    def OnDelete(self, event):
        selected = self.getSelection()
        if selected != -1:
            del self.watches[selected]
            self.DeleteItem(selected)
            self.debugger.updateSelectedPane(force=1)

    def OnDeleteAll(self, event):
        del self.watches[:]
        self.DeleteAllItems()

    def OnExpand(self, event):
        selected = self.getSelection()
        if selected != -1:
            name, local = self.watches[selected]
            self.debugger.requestWatchSubobjects(name, local, selected + 1)

    def getPopupMenu(self):
        sel = self.getSelection()

        self.menu.Enable(self.editId, sel != -1)
        self.menu.Enable(self.deleteId, sel != -1)
        self.menu.Enable(self.expandId, sel != -1)

        return DebuggerListCtrl.getPopupMenu(self)

    def OnValueToOutput(self, event):
        selected = self.getSelection()

        if selected != -1:
            name = self.watches[selected][0]
            self.debugger.valueToOutput(name)

    def OnDoubleClick(self, event):
        if event.ControlDown():
            self.OnValueToOutput(event)
        else:
            self.OnEdit(event)


class DebugStatusBar(wx.StatusBar):
    def __init__(self, parent):
        wx.StatusBar.__init__(self, parent, -1, style=0)
        self.SetFieldsCount(2)
        self.SetMinHeight(30)
        #self.SetStatusWidths([-1, -1, 16])

        self.stateCols = {'except': wx.Colour(0xFF, 0xFF, 0x44),#wxNamedColour('yellow'),
                          'info':   wx.NamedColour('white'),
                          'break':  wx.Colour(0xFF, 0x44, 0x44),#wxNamedColour('red'),
                          'busy':   wx.Colour(0xBB, 0xE0, 0xFF)}

        self.instr_ptr = wx.lib.stattext.GenStaticText(self, -1, ' ',
              style=wx.ALIGN_CENTER | wx.ST_NO_AUTORESIZE)
        self.instr_ptr.SetBackgroundColour(wx.Colour(0xEE, 0xEE, 0xEE))
        self._setCtrlDims(self.instr_ptr, self.GetFieldRect(0))

        self.state = wx.lib.stattext.GenStaticText(self, -1, 'Ready.',
              style=wx.ALIGN_CENTER | wx.ST_NO_AUTORESIZE)
        self.state.SetBackgroundColour(self.stateCols['info'])
        self._setCtrlDims(self.state, self.GetFieldRect(1))

        dc = wx.ClientDC(self)
        dc.SetFont(self.GetFont())
        (w,h) = dc.GetTextExtent('X')
        h = int(h * 1.8)
        self.SetSize(wx.Size(100, h))

        self.Bind(wx.EVT_SIZE, self.OnSize)

    def _setCtrlDims(self, ctrl, rect):
        ctrl.SetDimensions(rect.x+2, rect.y+2, rect.width-4, rect.height-4)

    def updateState(self, message, sts_type='except'):
        if message:
            self.state.SetBackgroundColour(self.stateCols[sts_type])
        else:
            self.state.SetBackgroundColour(self.stateCols['info'])
        self.state.SetLabel(message)
        self.state.SetToolTipString(message)

        self._setCtrlDims(self.state, self.GetFieldRect(1))

    def updateInstructionPtr(self, status):
        self.instr_ptr.SetLabel(status)
        self._setCtrlDims(self.instr_ptr, self.GetFieldRect(0))

    def OnSize(self, event):
        self._setCtrlDims(self.instr_ptr, self.GetFieldRect(0))
        self._setCtrlDims(self.state, self.GetFieldRect(1))
