#-----------------------------------------------------------------------------
# Name:        ErrorStackFrm.py
# Purpose:
#
# Author:      Riaan Booysen
#
# Created:     2001
# RCS-ID:      $Id: ErrorStackFrm.py,v 1.30 2007/07/02 15:01:04 riaan Exp $
# Copyright:   (c) 2001 - 2007 Riaan Booysen
# Licence:     GPL
#-----------------------------------------------------------------------------
##Boa:Frame:ErrorStackMF
# remove between #-- comments before editing visually

import os, sys, pickle

import wx
import wx.stc

import Preferences, Utils
from Utils import _

[wxID_EO_LOADHIST, wxID_EO_SAVEHIST, wxID_EO_CLRHIST, wxID_EO_CLOSEDIFF,
 wxID_EO_CLOSEINPT, wxID_EO_KILLPROC, wxID_EO_CHECKPROCS] = Utils.wxNewIds(7)

[wxID_ERRORSTACKMFSTATUSBAR, wxID_ERRORSTACKMFERRORSTACKTC, wxID_ERRORSTACKMF,
 wxID_ERRORSTACKMFNOTEBOOK, wxID_ERRORSTACKMFOUTPUTTC,
 wxID_ERRORSTACKMFERRORTC] = [wx.NewId() for _init_ctrls in range(6)]

class ErrorStackMF(wx.Frame, Utils.FrameRestorerMixin):
    def _init_coll_notebook_Pages(self, parent):

        parent.AddPage(select = True, imageId = self.tracebackImgIdx,
              page = self.errorStackTC, text = self.tracebackText)
        parent.AddPage(select = False, imageId = self.outputImgIdx,
              page = self.outputTC, text = self.outputText)
        parent.AddPage(select = False, imageId = self.errorsImgIdx,
              page = self.errorTC, text = self.errorsText)

    def _init_coll_statusBar_Fields(self, parent):
        parent.SetFieldsCount(1)

        parent.SetStatusText('', 0)

        parent.SetStatusWidths([-1])

    def _init_utils(self):
        self.images = wx.ImageList(16, 16)

    def _init_ctrls(self, prnt):
        wx.Frame.__init__(self, size = wx.Size(330, 443), id = wxID_ERRORSTACKMF,
              title = _('Traceback and Output browser'), parent = prnt,
              name = 'ErrorStackMF',
              style=wx.DEFAULT_FRAME_STYLE | wx.FRAME_TOOL_WINDOW,
              pos = wx.Point(464, 228))
        self._init_utils()
        self.Bind(wx.EVT_CLOSE, self.OnErrorstackmfClose)

        self.notebook = wx.Notebook(size = wx.Size(330, 418),
              id = wxID_ERRORSTACKMFNOTEBOOK, parent = self,
              name = 'notebook',
              style = self.notebookStyle, pos = wx.Point(0, 0))

        self.statusBar = wx.StatusBar(id = wxID_ERRORSTACKMFSTATUSBAR,
              parent = self, name = 'statusBar', style = 0)
        self.SetStatusBar(self.statusBar)

        self.outputTC = wx.TextCtrl(size = wx.Size(326, 384), value = '',
              pos = wx.Point(0, 0), parent = self.notebook,
              name = 'outputTC', style=wx.TE_MULTILINE | wx.TE_RICH,
              id = wxID_ERRORSTACKMFOUTPUTTC)

        self.errorTC = wx.TextCtrl(size = wx.Size(326, 384), value = '',
              pos = wx.Point(0, 0), parent = self.notebook, name = 'errorTC',
              style=wx.TE_MULTILINE | wx.TE_RICH, id = wxID_ERRORSTACKMFERRORTC)
        self.errorTC.SetForegroundColour(wx.Colour(64, 0, 0))

        #--
        # Special case to fix GTK redraw problem
        if wx.Platform == '__WXGTK__':
            prxy, errorStackTC = Utils.wxProxyPanel(self.notebook, wx.TreeCtrl,
                  size = wx.Size(312, 390), id = wxID_ERRORSTACKMFERRORSTACKTC,
                  name = 'errorStackTC', style=wx.TR_HAS_BUTTONS | wx.SUNKEN_BORDER,
                  pos = wx.Point(0, 0))
            self.errorStackTC = prxy
            self._init_coll_notebook_Pages(self.notebook)
            self.errorStackTC = errorStackTC
        else:
            self.errorStackTC = wx.TreeCtrl(size = wx.Size(312, 390),
                  id = wxID_ERRORSTACKMFERRORSTACKTC, parent = self.notebook,
                  name = 'errorStackTC', pos = wx.Point(4, 22),
                  style=wx.TR_HAS_BUTTONS | wx.SUNKEN_BORDER)
            self._init_coll_notebook_Pages(self.notebook)
        #--

    historySize = 50
    def __init__(self, parent, editor):
        self.notebookStyle = 0
        self.tracebackImgIdx = 0
        self.tracebackText = _('Tracebacks')
        self.outputImgIdx = 1
        self.outputText = _('Output')
        self.errorsImgIdx = 2
        self.errorsText = _('Errors')

        self.processesPage = None
        self.processesImgIdx = 3
        self.processesText = _('Tasks')

        self.diffPage = None
        self.diffImgIdx = 4

        self.inputPage = None
        self.inputImgIdx = 5

        self.history = []
        self.historyIdx = None

        if Preferences.eoErrOutNotebookStyle == 'side':
            self.notebookStyle = wx.NB_LEFT
            self.tracebackText = '  '
            self.outputText = '  '
            self.errorsText = '  '

        if Preferences.eoErrOutNotebookStyle == 'text':
            self.tracebackImgIdx = self.outputImgIdx = self.errorsImgIdx = \
                  self.diffImgIdx = self.inputImgIdx = self.processesImgIdx = -1

        self._init_ctrls(parent)

        self._init_coll_statusBar_Fields(self.statusBar)

        self.outputTC.SetFont(Preferences.eoErrOutFont)
        self.errorTC.SetFont(Preferences.eoErrOutFont)
        self.errorTC.SetForegroundColour(wx.Colour(64, 0, 0))

        if Preferences.eoErrOutNotebookStyle == 'side':
            self.notebook.SetPadding(wx.Size(1, 5))

        if Preferences.eoErrOutNotebookStyle != 'text':
            for img in ('Images/Shared/Traceback.png',
                        'Images/Shared/Info.png',
                        'Images/Shared/Error.png',
                        'Images/Shared/Processes.png',
                        'Images/CvsPics/Diff.png',
                        'Images/Shared/Input.png',
                       ):
                self.images.Add(Preferences.IS.load(img))
            self.notebook.AssignImageList(self.images)

        self.SetIcon(Preferences.IS.load('Images/Icons/OutputError.ico'))

        self.editor = editor
        self.vetoEvents = False
        self.errorStackTC.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.OnErrorstacktcTreeItemActivated, id=wxID_ERRORSTACKMFERRORSTACKTC)
        self.errorStackTC.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnErrorstacktcTreeSelChanged, id=wxID_ERRORSTACKMFERRORSTACKTC)
        #self.errorStackTC.Bind(wx.EVT_LEFT_DOWN, self.OnErrorstacktcLeftDown)

        #self.lastClick = (0, 0)

        self.menu = wx.Menu()
        self.Bind(wx.EVT_MENU, self.OnLoadHistory, id=wxID_EO_LOADHIST)
        self.menu.Append(wxID_EO_LOADHIST, _('Load history...'))
        self.Bind(wx.EVT_MENU, self.OnSaveHistory, id=wxID_EO_SAVEHIST)
        self.menu.Append(wxID_EO_SAVEHIST, _('Save history...'))
        self.Bind(wx.EVT_MENU, self.OnClearHistory, id=wxID_EO_CLRHIST)
        self.menu.Append(wxID_EO_CLRHIST, _('Clear history'))
        self.menu.AppendSeparator()
        self.Bind(wx.EVT_MENU, self.OnCloseDiff, id=wxID_EO_CLOSEDIFF)
        self.menu.Append(wxID_EO_CLOSEDIFF, _('Close diff page'))
        self.menu.Enable(wxID_EO_CLOSEDIFF, False)
        self.Bind(wx.EVT_MENU, self.OnCloseInput, id=wxID_EO_CLOSEINPT)
        self.menu.Append(wxID_EO_CLOSEINPT, _('Close input page'))
        self.menu.Enable(wxID_EO_CLOSEINPT, False)

        self.notebook.Bind(wx.EVT_RIGHT_UP, self.OnRightDown)

        self.processesMenu = wx.Menu()
        self.Bind(wx.EVT_MENU, self.OnKillProcess, id=wxID_EO_KILLPROC)
        self.processesMenu.Append(wxID_EO_KILLPROC, _('Kill process'))
        self.processesMenu.AppendSeparator()
        self.Bind(wx.EVT_MENU, self.OnCheckProcesses, id=wxID_EO_CHECKPROCS)
        self.processesMenu.Append(wxID_EO_CHECKPROCS, _('Check processes'))

        self.displayProcesses()
        self.processesPage.Bind(wx.EVT_RIGHT_DOWN, self.OnProcessesRightDown)

        self.winConfOption = 'errout'
        if Preferences.eoErrOutDockWindow == 'undocked':
            self.loadDims()

    def setDefaultDimensions(self):
        self.SetDimensions(0,
          Preferences.paletteHeight + Preferences.windowManagerTop + \
          Preferences.windowManagerBottom,
          Preferences.inspWidth,
          Preferences.bottomHeight)

    def addTracebackNode(self, err, parsedTracebacks):
        tree = self.errorStackTC
        root = tree.GetRootItem()
        if err.error and err.stack:
            errTI = tree.AppendItem(root, ' : '.join(err.error).strip())
            for si in err.stack:
                siTI = tree.AppendItem(errTI, '%d: %s: %s' % (si.lineNo,
                      os.path.basename(si.file), si.line.strip()))
                tree.SetPyData(siTI, si)
            if err.stack:
                tree.SetItemHasChildren(errTI, True)
                tree.SetPyData(errTI, err.stack[-1])
                parsedTracebacks += 1
        return parsedTracebacks


    def updateCtrls(self, errorList, outputList=None, rootName='Error',
          runningDir='', errRaw=None, addToHistory=True):

        if addToHistory:
            if errorList or outputList or errRaw:
                self.history.append( (errorList, outputList, rootName,
                                      runningDir, errRaw) )
                while len(self.history) > self.historySize:
                    del self.history[0]
                self.historyIdx = None

        self.runningDir = runningDir
        self.tracebackType = rootName
        tree = self.errorStackTC
        tree.DeleteAllItems()
        rtTI = tree.AddRoot(rootName+'s')
        parsedTracebacks = 0
        for err in errorList:
            parsedTracebacks += self.addTracebackNode(err, parsedTracebacks)

        tree.SetItemHasChildren(rtTI, True)
        tree.Expand(rtTI)
        firstErr, cookie = tree.GetFirstChild(rtTI)
        if firstErr.IsOk():
            tree.Expand(firstErr)

        if outputList:
            self.outputTC.SetValue(Utils.stringToControl(''.join(outputList), safe=True))
        else:
            self.outputTC.SetValue('')

        if errRaw:
            self.errorTC.SetValue(Utils.stringToControl(''.join(errRaw), safe=True))
        else:
            self.errorTC.SetValue('')

        selIdx = -1
        if parsedTracebacks:
            selIdx = 0
        elif errorList:
            selIdx = 2
        elif outputList:
            selIdx = 1
        elif errRaw:
            selIdx = 2

        if selIdx >= 0:
            self.notebook.SetSelection(selIdx)

        tree.Refresh()

        return parsedTracebacks

    def display(self, errs=None):
        # XXX errs not used !
        # docked on this frame
        if self.notebook.GetParent() == self:
            self.Show()
        # docked in inspector
        elif self.notebook.GetGrandParent() == self.editor.inspector.pages:
            inspPages = self.notebook.GetGrandParent()
            inspPages.SetFocus()
            for idx in range(inspPages.GetPageCount()-1, -1, -1):
                if inspPages.GetPageText(idx) == 'ErrOut':
                    inspPages.SetSelection(idx)
                    break
        # docked in editor
        elif self.notebook.GetGrandParent() == self.editor:
            splitter = self.editor.tabsSplitter
            win2 = splitter.GetWindow2()
            if win2 and win2.GetSize().y == splitter.GetMinimumPaneSize():
                splitter.openBottomWindow()

    def appendToTextCtrl(self, tc, txt,
                         TEXTCTRL_MAXLEN=30000, TEXTCTRL_GOODLEN=20000):
        # Before appending to the output, remove old data.
        cursz = tc.GetLastPosition()
        newsz = cursz + len(txt)
        if newsz >= TEXTCTRL_MAXLEN:
            olddata = tc.GetValue()[newsz - TEXTCTRL_GOODLEN:]
            tc.SetValue(olddata)
        tc.SetFocus()
        tc.AppendText(txt)

        # XXX just make editor err out visible for now, the others would be
        # XXX too jarring
        if self.notebook.GetGrandParent() == self.editor:
            splitter = self.editor.tabsSplitter
            win2 = splitter.GetWindow2()
            if win2 and not win2.GetSize().y:
                for i in range(self.notebook.GetPageCount()):
                    # Try to show the tab where the text was just added.
                    if self.notebook.GetPage(i) == tc:
                        self.notebook.SetSelection(i)
                        tc.Refresh()
                        break
                splitter.openBottomWindow()

    def appendToOutput(self, txt):
        self.appendToTextCtrl(self.outputTC, txt)

    def appendToErrors(self, txt):
        self.appendToTextCtrl(self.errorTC, txt)

    def Destroy(self):
        self.menu.Destroy()
        self.vetoEvents = True
        wx.Frame.Destroy(self)

    def findPage(self, name):
        for idx in range(self.notebook.GetPageCount()):
            if self.notebook.GetPageText(idx) == name:
                return idx
        return -1

    def displayDiff(self, diffResult):
        if not self.diffPage:
            self.diffPage = wx.stc.StyledTextCtrl(self.notebook, -1,
                style=wx.SUNKEN_BORDER | wx.CLIP_CHILDREN)
            self.diffPage.SetMarginWidth(1, 0)
            self.diffPage.SetLexer(wx.stc.STC_LEX_DIFF)
            self.diffPage.StyleClearAll()
            # XXX Should be moved to StyleEditor
            fontPropStr = 'face:%s,size:%d'%(
                       Preferences.eoErrOutFont.GetFaceName(),
                       Preferences.eoErrOutFont.GetPointSize())
            for num, style in (
                  (wx.stc.STC_DIFF_DEFAULT,  fontPropStr),
                  (wx.stc.STC_DIFF_COMMENT,  fontPropStr+',back:#EEEEFF'),      # comment
                  (wx.stc.STC_DIFF_COMMAND,  fontPropStr+',fore:#FFFFCC,back:#000000,bold'), #diff
                  (wx.stc.STC_DIFF_HEADER,   fontPropStr+',back:#FFFFCC'),      #"--- ","+++ ",
                  (wx.stc.STC_DIFF_POSITION, fontPropStr+',back:#CCCCFF,bold'), #'@'
                  (wx.stc.STC_DIFF_DELETED,  fontPropStr+',back:#FFCCCC'),      #'-'
                  (wx.stc.STC_DIFF_ADDED,    fontPropStr+',back:#CCFFCC'),      #'+'
                 ):
                self.diffPage.StyleSetSpec(num, style)
            self.diffPage.SetText(diffResult)
            self.notebook.AddPage(text=_('Diffs'), select=not not diffResult,
                page=self.diffPage, imageId=self.diffImgIdx)
            self.menu.Enable(wxID_EO_CLOSEDIFF, True)
        else:
            self.diffPage.SetText(diffResult)
            if diffResult:
                pageIdx = self.findPage(_('Diffs'))
                if pageIdx != -1:
                    self.notebook.SetSelection(pageIdx)

        self.display()

    displayPageIdx = 3
    def displayInput(self, display=True):
        if not self.inputPage:
            self.inputPage = wx.TextCtrl(self.notebook, -1, value='',
                style=wx.TE_MULTILINE | wx.TE_RICH | wx.SUNKEN_BORDER | wx.CLIP_CHILDREN)
            self.inputPage.Bind(wx.EVT_LEFT_DCLICK, self.OnInputDoubleClick)
            self.notebook.InsertPage(self.displayPageIdx, self.inputPage,
                  _('Input'), True, self.inputImgIdx)
            self.menu.Enable(wxID_EO_CLOSEINPT, True)
        else:
            self.notebook.SetSelection(self.displayPageIdx)
        self.display()

    def displayProcesses(self):
        if not self.processesPage:
            self.processesPage = wx.ListView(self.notebook, -1,
                  style=wx.LC_LIST | wx.LC_ALIGN_TOP)
            self.notebook.AddPage(self.processesPage, self.processesText,
                  False, self.processesImgIdx)
        else:
            pageIdx = self.findPage(self.processesText)
            if pageIdx != -1:
                self.notebook.SetSelection(pageIdx)

    def processStarted(self, name, pid, script='', processType=''):
        if self.processesPage:
            idx = self.processesPage.GetItemCount()
            if script:
                name = '%s (%s)'%(name, script)
            self.processesPage.InsertStringItem(idx, '%s : %s'%(name, pid))
            self.processesPage.SetItemData(idx, pid)

            self.checkProcesses()

    def processFinished(self, pid):
        if self.processesPage:
            for idx in range(self.processesPage.GetItemCount()):
                if self.processesPage.GetItemData(idx) == pid:
                    self.processesPage.DeleteItem(idx)
                    break
            self.checkProcesses()


    def checkProcesses(self):
        if self.processesPage:
            idxs = range(self.processesPage.GetItemCount())
            idxs.reverse()
            for idx in idxs:
                pid = self.processesPage.GetItemData(idx)
                if not wx.Process.Exists(pid):
                    self.processesPage.DeleteItem(idx)

    def killProcess(self, pid):
        res = wx.Process.Kill(pid, wx.SIGTERM)
        if res == wx.KILL_ERROR:
            res = wx.Process.Kill(pid, wx.SIGKILL)
            if res == wx.KILL_ERROR:
                wx.LogError(_('Cannot kill process %d.')%pid)

        if res == wx.KILL_ACCESS_DENIED:
            wx.LogError(_('Cannot kill process %d, access denied.')%pid)
        elif res == wx.KILL_OK:
            self.editor.setStatus(_('Killed process %d.')%pid)

    def checkProcessesAtExit(self):
        if self.processesPage:
            self.checkProcesses()
            wx.Yield()
            cnt = self.processesPage.GetItemCount()
            if cnt:
                self.display()
                pageIdx = self.findPage(self.processesText)
                self.notebook.SetSelection(pageIdx)

                if wx.MessageBox(_('There are still running processes that were '
                             'started from Boa, please close or kill them '
                             'before quitting.\n\nClick Cancel to quit anyway.'),
                             _('Child processes running'),
                             wx.ICON_WARNING | wx.OK | wx.CANCEL) == wx.CANCEL:
                    return True
                return False
        return True

    def stepBackInHistory(self):
        if len(self.history) > 1:
            if self.historyIdx is None:
                self.historyIdx = max(len(self.history)-2, 0)
            else:
                self.historyIdx = max(self.historyIdx - 1, 0)

            self.updateCtrls(
                  *(self.history[self.historyIdx]+(False,)))

    def stepFwdInHistory(self):
        if len(self.history) > 1 and self.historyIdx is not None:
            self.historyIdx = min(self.historyIdx + 1, len(self.history)-1)

            self.updateCtrls(
                  *(self.history[self.historyIdx]+(False,)))

    def OnErrorstacktcTreeItemActivated(self, event):
        data = self.errorStackTC.GetPyData(event.GetItem())
        if data is None:
            return
        if data.file.find('://') != -1 or os.path.isabs(data.file):
            fn = data.file
        elif self.runningDir:
            fn = os.path.join(self.runningDir, data.file)
        else:
            fn = os.path.abspath(data.file)

        model, controller = self.editor.openOrGotoModule(fn)#, self.app)
        srcView = model.getSourceView()
        srcView.focus()
        srcView.gotoLine(data.lineNo - 1)
        srcView.setLinePtr(data.lineNo - 1)
        self.editor.setStatus(' : '.join(data.error), self.tracebackType)

    def OnErrorstackmfClose(self, event):
        self.Show(True)
        self.Show(False)

    def OnErrorstacktcTreeSelChanged(self, event):
        if self.vetoEvents: return
        selLine = self.errorStackTC.GetItemText(event.GetItem())
        if wx.Platform == '__WXGTK__':
            self.errorStackTC.SetToolTipString(selLine)
        self.statusBar.SetStatusText(selLine)

##    def OnErrorstacktcLeftDown(self, event):
##        self.lastClick = event.GetPosition().asTuple()
##        event.Skip()

    def OnInputDoubleClick(self, event):
        filename = self.editor.openFileDlg()
        if filename:
            from Explorers import Explorer
            self.inputPage.SetValue(Explorer.openEx(filename).load())

    def OnRightDown(self, event):
        sp = self.notebook.ClientToScreen(event.GetPosition())
        mp = self.ScreenToClient(sp)
        self.PopupMenu(self.menu, mp)

    def OnLoadHistory(self, event):
        fn = self.editor.openFileDlg('AllFiles')
        if fn:
            from Explorers import Explorer
            data = Explorer.openEx(fn).load()
            self.history = pickle.loads(data)

    def OnSaveHistory(self, event):
        fn, ok = self.editor.saveAsDlg('history.pcl', 'AllFiles')
        if ok:
            data = pickle.dumps(self.history)
            from Explorers import Explorer
            n = Explorer.openEx(fn)
            n.save(n.resourcepath, data)

    def OnCloseDiff(self, event):
        if self.diffPage:
            pageIdx = self.findPage(_('Diffs'))
            if pageIdx != -1:
                self.notebook.DeletePage(pageIdx)
                self.diffPage = None
                self.menu.Enable(wxID_EO_CLOSEDIFF, False)

    def OnCloseInput(self, event):
        if self.inputPage:
            self.notebook.DeletePage(3)
            self.inputPage = None
            self.menu.Enable(wxID_EO_CLOSEINPT, False)


    def OnClearHistory(self, event):
        self.updateCtrls([], addToHistory=False)
        self.history = []
        self.historyIdx = None

        self.editor.setStatus(_('History cleared.'))

    def OnProcessesRightDown(self, event):
##        event.Skip()
##        wx.Yield()
        sp = self.processesPage.ClientToScreen(event.GetPosition())
        mp = self.ScreenToClient(sp)
        self.PopupMenu(self.processesMenu, mp)

    def OnKillProcess(self, event):
        if self.processesPage:
            idx = self.processesPage.GetFirstSelected()
            while idx != -1:
                pid = self.processesPage.GetItemData(idx)
                self.killProcess(pid)

                idx = self.processesPage.GetNextSelected(idx)

        self.checkProcesses()

    def OnCheckProcesses(self, event):
        if self.processesPage:
            self.checkProcesses()


if __name__ == '__main__':
    app = wx.PySimpleApp()

    frame = ErrorStackMF(None, None)
    frame.Show(True)
    app.MainLoop()
