#----------------------------------------------------------------------
# Name:        Help.py
# Purpose:
#
# Author:      Riaan Booysen
#
# Created:     1999, rewritten 2001
# RCS-ID:      $Id: Help.py,v 1.27 2005/05/18 13:19:42 riaan Exp $
# Copyright:   (c) 1999 - 2005 Riaan Booysen
# Licence:     GPL
#----------------------------------------------------------------------
#Boa:FramePanel:PyDocHelpPage

import os, sys, marshal, string, socket, webbrowser

import wx
import wx.html
from wx.lib.anchors import LayoutAnchors

import Preferences, Utils

[wxID_PYDOCHELPPAGE, wxID_PYDOCHELPPAGEBOXRESULTS,
 wxID_PYDOCHELPPAGEBTNSEARCH, wxID_PYDOCHELPPAGEBTNSTOP,
 wxID_PYDOCHELPPAGECHKRUNSERVER, wxID_PYDOCHELPPAGEPNLSTATUS,
 wxID_PYDOCHELPPAGESTXSTATUS, wxID_PYDOCHELPPAGETXTSEARCH,
] = [wx.NewId() for _init_ctrls in range(8)]

class PyDocHelpPage(wx.Panel):
    def _init_utils(self):
        # generated method, don't edit
        self.scrBrowse =wx.StockCursor(id=wx.CURSOR_HAND)

    def _init_ctrls(self, prnt):
        # generated method, don't edit
        wx.Panel.__init__(self, id=wxID_PYDOCHELPPAGE, name='PyDocHelpPage',
              parent=prnt, pos=wx.Point(443, 285), size=wx.Size(259, 456),
              style=wx.TAB_TRAVERSAL)
        self._init_utils()
        self.SetClientSize(wx.Size(251, 429))
        self.SetAutoLayout(True)

        self.txtSearch =wx.TextCtrl(id=wxID_PYDOCHELPPAGETXTSEARCH,
              name='txtSearch', parent=self, pos=wx.Point(10, 10),
              size=wx.Size(231, 21), style=0, value='')
        self.txtSearch.SetConstraints(LayoutAnchors(self.txtSearch, True, True,
              True, False))
        self.txtSearch.SetToolTipString('Enter name to search for')
        self.txtSearch.Bind(wx.EVT_TEXT_ENTER, self.OnTxtsearchTextEnter, id=wxID_PYDOCHELPPAGETXTSEARCH)

        self.boxResults =wx.ListBox(choices=[], id=wxID_PYDOCHELPPAGEBOXRESULTS,
              name='boxResults', parent=self, pos=wx.Point(2, 89),
              size=wx.Size(247, 338), style=0)
        self.boxResults.SetConstraints(LayoutAnchors(self.boxResults, True,
              True, True, True))
        self.boxResults.Bind(wx.EVT_LISTBOX, self.OnBoxresultsListboxDclick, id=wxID_PYDOCHELPPAGEBOXRESULTS)

        self.btnSearch =wx.Button(id=wxID_PYDOCHELPPAGEBTNSEARCH,
              label='Search', name='btnSearch', parent=self, pos=wx.Point(89,
              41), size=wx.Size(75, 23), style=0)
        self.btnSearch.SetConstraints(LayoutAnchors(self.btnSearch, False, True,
              True, False))
        self.btnSearch.Bind(wx.EVT_BUTTON, self.OnBtnsearchButton, id=wxID_PYDOCHELPPAGEBTNSEARCH)

        self.btnStop =wx.Button(id=wxID_PYDOCHELPPAGEBTNSTOP, label='Stop',
              name='btnStop', parent=self, pos=wx.Point(166, 41), size=wx.Size(75,
              23), style=0)
        self.btnStop.SetConstraints(LayoutAnchors(self.btnStop, False, True,
              True, False))
        self.btnStop.Enable(False)
        self.btnStop.Bind(wx.EVT_BUTTON, self.OnBtnstopButton, id=wxID_PYDOCHELPPAGEBTNSTOP)

        self.chkRunServer =wx.CheckBox(id=wxID_PYDOCHELPPAGECHKRUNSERVER,
              label='Server', name='chkRunServer', parent=self, pos=wx.Point(3,
              72), size=wx.Size(73, 13), style=0)
        self.chkRunServer.SetValue(self.runServer)
        self.chkRunServer.Bind(wx.EVT_CHECKBOX, self.OnChkrunserverCheckbox, id=wxID_PYDOCHELPPAGECHKRUNSERVER)

        self.pnlStatus =wx.Panel(id=wxID_PYDOCHELPPAGEPNLSTATUS,
              name='pnlStatus', parent=self, pos=wx.Point(80, 72),
              size=wx.Size(168, 16), style=wx.TAB_TRAVERSAL | wx.NO_BORDER)

        self.stxStatus =wx.StaticText(id=wxID_PYDOCHELPPAGESTXSTATUS,
              label='Server not running ', name='stxStatus',
              parent=self.pnlStatus, pos=wx.Point(0, 0), size=wx.Size(168, 16),
              style=wx.ST_NO_AUTORESIZE | wx.ALIGN_RIGHT)
        self.stxStatus.SetConstraints(LayoutAnchors(self.stxStatus, True, True,
              True, False))
        self.stxStatus.Bind(wx.EVT_LEFT_DOWN, self.OnStxstatusLeftDown)

    def __init__(self, parent, helpFrame):
        #print 'creating new PyDocPage'
        self.runServer = False
        self.runServer = helpFrame.pdRunServer

        self._init_ctrls(parent)

        self.helpFrame = helpFrame

        self.scanner = None
        self.server = self.helpFrame.controller.server = None
        self.url = ''

        self.statusHyperlinked = 0
        self.waiting = 0

        if self.runServer:
            self.chkRunServer.Disable()
            self.startPydocServer()

    def startPydocServer(self):
        # silence warnings
        import warnings
        warnings.filterwarnings('ignore', '', DeprecationWarning, 'pydoc')

        self.stxStatus.SetLabel('Starting pydoc server... ')

        if not testPydocServerAddress('localhost', 7464):
            self.stxStatus.SetLabel('Address in use, ')

            self.chkRunServer.Enable(True)
            self.chkRunServer.SetValue(False)
            self.runServer = False

            self.url = 'http://localhost:%d/'%7464
        else:
            self.waiting = 1
            import threading, pydoc
            threading.Thread(
                target=pydoc.serve, args=(7464, self.OnServerReady,
                    self.OnServerQuit)).start()

    # called from thread
    def OnServerReady(self, server):
        if self and self.helpFrame:
            self.server = self.helpFrame.controller.server = server
            self.url = server.url
            self.runServer = True
            self.waiting = 0

            wx.CallAfter(self.chkRunServer.Enable, True)
            wx.CallAfter(self.chkRunServer.SetValue, True)
            wx.CallAfter(self.hyperlinkLabel,
                              'http://localhost:%d/'%server.server_port)


    # called from thread
    def OnServerQuit(self):
        if self and self.stxStatus:
            self.server.server_close()
            self.server = None
            self.runServer = False
            self.url = ''
            self.waiting = 0

            wx.CallAfter(self.chkRunServer.Enable, True)
            wx.CallAfter(self.chkRunServer.SetValue, False)
            wx.CallAfter(self.stxStatus.SetLabel, 'Server quit. ')

    def OnBtnsearchButton(self, event):
        self.doSearch()

    def OnBtnstopButton(self, event):
        self.doStop()

    def doSearch(self):
        key = self.txtSearch.GetValue()
        self.btnStop.Enable(True)
        self.boxResults.Clear()

        import threading, pydoc
        if self.scanner:
            self.scanner.quit = 1
        self.scanner = pydoc.ModuleScanner()
        threading.Thread(target=self.scanner.run,
                         args=(self.OnUpdateResults, key,
                               self.OnFinishedResults)).start()

    # called from thread
    def OnUpdateResults(self, path, modname, desc):
        if self:
            if modname[-9:] == '.__init__':
                modname = modname[:-9] + ' (package)'
            wx.CallAfter(self.boxResults.Append,
                              modname + ' - ' + (desc or '(no description)'))

    def doStop(self):
        if self and self.scanner:
            self.scanner.quit = 1
            self.scanner = None

    # called from thread
    def OnFinishedResults(self):
        if self:
            self.scanner = None
            wx.CallAfter(self.btnStop.Disable)

    def OnBoxresultsListboxDclick(self, event):
        selection = self.boxResults.GetStringSelection()
        if selection and self.url:
            modname = selection.split()[0]
            self.helpFrame.html.LoadPage(self.url + modname + '.html')

    def OnTxtsearchTextEnter(self, event):
        self.doSearch()

    def OnChkrunserverCheckbox(self, event):
        self.chkRunServer.Disable()

        self.runServer = event.IsChecked()

        if self.runServer:
            self.startPydocServer()
        else:
            if self.server:
                self.restoreLabel('Stopping server...')
                self.server.quit = 1
                self.server.server_close()

    def hyperlinkLabel(self, text):
        f = self.stxStatus.GetFont()
        f.SetUnderlined(1)
        self.stxStatus.SetFont(f)
        self.stxStatus.SetForegroundColour(wx.Colour(0x11,0x22,0x88))
        self.stxStatus.SetLabel(text)
        self.pnlStatus.SetCursor(self.scrBrowse)
        self.statusHyperlinked = 1

    def restoreLabel(self, text):
        f = self.stxStatus.GetFont()
        f.SetUnderlined(0)
        self.stxStatus.SetFont(f)
        self.stxStatus.SetForegroundColour(wx.BLACK)
        self.stxStatus.SetLabel(text)
        self.pnlStatus.SetCursor(wx.STANDARD_CURSOR)
        self.statusHyperlinked = 0

    def OnStxstatusLeftDown(self, event):
        if self.statusHyperlinked:
            webbrowser.open(self.stxStatus.GetLabel())


#-------------------------------------------------------------------------------


def tagEater(strg):
    res = ''
    inTag = 0
    for i in range(len(strg)-1, -1, -1):
        if strg[i] == '>':
            inTag = 1
            continue
        elif strg[i] == '<':
            inTag = 0
            continue
        if not inTag:
            res = strg[i] + res
    return res

def showMainHelp(bookname):
    getHelpController().Display(bookname).ExpandBook(bookname)

def showCtrlHelp(wxClass, method=''):
    getHelpController().Display(wxClass).ExpandCurrAsWxClass(method)

def showHelp(filename):
    getHelpController().Display(filename)

def showContextHelp(word):
    if word.startswith('EVT_'):
        word = 'wx%sEvent' % ''.join([s.lower().capitalize() 
                                      for s in word[4:].split('_')])
    elif word in sys.builtin_module_names:
        word = '%s (built-in module)'%word
    else:
        try:
            libPath = os.path.dirname(os.__file__)
        except AttributeError, error:
            pass
        else:
            if os.path.isfile('%s/%s.py'%(libPath, word)):
                word = '%s (standard module)'%word
    if string.strip(word):
        getHelpController().Display(word).IndexFind(word)
    else:
        getHelpController().DisplayContents()

def decorateWxPythonWithDocStrs(dbfile):
    namespace = Utils.getEntireWxNamespace()

    try:
        db = marshal.load(open(dbfile, 'rb'))
    except IOError:
        print 'wxPython Doc strings: %s failed to load'%dbfile
    else:
        for name, doc in db['classes'].items():
            try:
                wxClass = namespace[name]
                wxClass.__doc__ = doc

                wxClass = namespace[name+'Ptr']
                wxClass.__doc__ = doc
            except:
                pass

        for name, doc in db['methods'].items():
            try:
                cls, mth = string.split(name, '.')
                wxMeth = getattr(namespace[cls], mth)
                wxMeth.im_func.__doc__ = doc

                wxMeth = getattr(namespace[cls+'Ptr'], mth)
                wxMeth.im_func.__doc__ = doc
            except:
                pass

class wxHtmlHelpControllerEx(wx.html.HtmlHelpController):
    frameX = None
    def Display(self, text):
        wx.html.HtmlHelpController.Display(self, text)
        if not self.frameX:
            self.frameX = wxHelpFrameEx(self)
        #frameX.restore()
        frame = self.frameX.frame
        if not frame.IsShown():
            frame.Show(True)
        if frame.IsIconized():
            frame.Iconize(False)
        frame.Raise()
        return self.frameX

    def UseConfig(self, config):
        # Fix config file if stored as minimised
        if config.ReadInt('hcX') == -32000:
            map(config.DeleteEntry, ('hcX', 'hcY', 'hcW', 'hcH'))

        wx.html.HtmlHelpController.UseConfig(self, config)

        self.config = config

class _CloseEvtHandler(wx.EvtHandler):
    def __init__(self, frameEx):
        wx.EvtHandler.__init__(self)
        frameEx.frame.Bind(wx.EVT_CLOSE, self.OnClose)
        self.frameEx = frameEx
        self.frame = frameEx.frame

    def OnClose(self, event):
        if hasattr(self.frameEx, 'pydocPage') and self.frameEx.pydocPage:
            config = self.frameEx.controller.config
            config.WriteInt('pdRunServer', self.frameEx.pydocPage.runServer)
            config.Flush()

        # catching the close event when running standalone
        if __name__ == '__main__':
            if not canClosePydocServer():
                if pydocWarning():
                    return
            event.Skip()
            self.frame.PopEventHandler().Destroy()

        self.frame.Hide()

wxID_COPYTOCLIP =wx.NewId()

# Note, this works nicely because of OOR
class wxHelpFrameEx:
    def __init__(self, helpctrlr):
        self.controller = helpctrlr
        self.frame = helpctrlr.GetFrame()
        #self.frame.frameEx = self

        wxID_QUITHELP, wxID_FOCUSHTML = wx.NewId(), wx.NewId()
        self.frame.Bind(wx.EVT_MENU, self.OnQuitHelp, id=wxID_QUITHELP)
        self.frame.Bind(wx.EVT_MENU, self.OnFocusHtml, id=wxID_FOCUSHTML)

        self.frame.PushEventHandler(_CloseEvtHandler(self))

        # helpfrm.cpp defines no accelerators so this is ok
        self.frame.SetAcceleratorTable(
              wx.AcceleratorTable([(0, wx.WXK_ESCAPE, wxID_QUITHELP),
                                  (wx.ACCEL_CTRL, ord('H'), wxID_FOCUSHTML),]))

        _none, self.toolbar, self.splitter = self.frame.GetChildren()

        self.html, nav = self.splitter.GetChildren()

        self.navPages = nav.GetChildren()[0]

        # Extend toolbar
        if self.toolbar.FindById(wxID_COPYTOCLIP) is None:
            self.toolbar.AddSeparator()
            self.copyToClipId = wxID_COPYTOCLIP
            self.toolbar.AddTool(id = self.copyToClipId, isToggle=0,
                bitmap=Preferences.IS.load('Images/Shared/CopyHelp.png'),
                pushedBitmap=wx.NullBitmap,
                shortHelpString='Copy contents as text to clipboard',
                longHelpString='')
            self.frame.Bind(wx.EVT_TOOL, self.OnCopyPage, id=self.copyToClipId)
            self.toolbar.Realize()

#        else:
#            self.navPages = nav.GetChildren()[0]

        assert self.navPages.GetPageText(0) == 'Contents'
        self.contentsPanel = self.navPages.GetPage(0)

        self.contentsAddBookmark, self.contentsDelBookmark, \
              self.contentsChooseBookmark, self.contentsTree = \
              self.contentsPanel.GetChildren()

        assert self.navPages.GetPageText(1) == 'Index'
        self.indexPanel = self.navPages.GetPage(1)

        self.indexTextCtrl, btn1, btn2 = \
              self.indexPanel.GetChildren()[:3]

        # done this way to work on 2.3.2 and 2.3.3
        if btn1.GetLabel() == 'Show all':
            self.indexShowAllBtn, self.indexFindBtn = btn1, btn2
        else:
            self.indexShowAllBtn, self.indexFindBtn = btn2, btn1

        if Preferences.usePydocHelp and self.navPages.GetPageCount() == 3:
            self.pdRunServer = self.controller.config.ReadInt('pdRunServer', False)

            self.pydocPage = PyDocHelpPage(self.navPages, self)
            self.navPages.AddPage(self.pydocPage, 'Pydoc')
        else:
            self.pydocPage = None

##    def restore(self):
##        Utils.FrameRestorerMixin.restore(self.frame)

    def IndexFind(self, text):
        self.controller.DisplayIndex()
        self.indexTextCtrl.SetValue(text)

        wx.PostEvent(self.frame, wx.CommandEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED,
              self.indexFindBtn.GetId()))

    def ShowNavPanel(self, show = True):
        if show:
            self.splitter.SplitVertically(self.navPages, self.html)
        else:
            self.splitter.Unsplit(self.navPages)

    def ExpandBook(self, name):
        self.navPages.SetSelection(0)
        rn = self.contentsTree.GetRootItem()
        nd, ck = self.contentsTree.GetFirstChild(rn)
        while nd.IsOk():
            if self.contentsTree.GetItemText(nd) == name:
                self.contentsTree.Expand(nd)
                break
            nd, ck = self.contentsTree.GetNextChild(rn, ck)

    def ExpandCurrAsWxClass(self, anchor):
        self.navPages.SetSelection(0)
        self.contentsTree.Expand(self.contentsTree.GetSelection())
        page = self.html.GetOpenedPage()
        if anchor:
            self.controller.Display('%s#%s' % (page, string.lower(anchor)))

    def OnQuitHelp(self, event):
        self.frame.Close()

    def OnFocusHtml(self, event):
        self.html.SetFocus()

    def OnCopyPage(self, event):
        Utils.writeTextToClipboard(self.html.SelectionToText())
        #Utils.html2txt(open(self.html.GetOpenedPage()).read()))

wxHF_TOOLBAR                = 0x0001
wxHF_CONTENTS               = 0x0002
wxHF_INDEX                  = 0x0004
wxHF_SEARCH                 = 0x0008
wxHF_BOOKMARKS              = 0x0010
wxHF_OPEN_FILES             = 0x0020
wxHF_PRINT                  = 0x0040
wxHF_FLAT_TOOLBAR           = 0x0080
wxHF_MERGE_BOOKS            = 0x0100
wxHF_ICONS_BOOK             = 0x0200
wxHF_ICONS_BOOK_CHAPTER     = 0x0400
wxHF_ICONS_FOLDER           = 0x0000
wxHF_DEFAULT_STYLE          = (wxHF_TOOLBAR | wxHF_CONTENTS | wxHF_INDEX | \
                               wxHF_SEARCH | wxHF_BOOKMARKS | wxHF_PRINT)

_hc = None

def getHelpController():
    if not _hc:
        initHelp()
    return _hc

def getCacheDir():
    cacheDir = os.path.join(Preferences.rcPath, 'docs-cache')
    if not os.path.isdir(cacheDir):
        cacheDir = os.path.join(Preferences.pyPath, 'Docs', 'cache')
    return cacheDir

# needed for .htb files
wx.FileSystem.AddHandler(wx.ZipFSHandler())

use_standard_controller = False
def initHelp(calledAtStartup=False):
    jn = os.path.join
    docsDir = jn(Preferences.pyPath, 'Docs')

    global _hc
    if use_standard_controller:
        _hc = wx.html.HtmlHelpController(wxHF_ICONS_BOOK_CHAPTER | \
            wxHF_DEFAULT_STYLE | (Preferences.flatTools and wxHF_FLAT_TOOLBAR or 0))
    else:
        _hc = wxHtmlHelpControllerEx(wxHF_ICONS_BOOK_CHAPTER | \
            wxHF_DEFAULT_STYLE | (Preferences.flatTools and wxHF_FLAT_TOOLBAR or 0))
        cf = wx.FileConfig(localFilename=os.path.normpath(jn(Preferences.rcPath,
            'helpfrm.cfg')), style=wx.CONFIG_USE_LOCAL_FILE)
        _hc.UseConfig(cf)

    cacheDir = getCacheDir()
    _hc.SetTempDir(cacheDir)

    conf = Utils.createAndReadConfig('Explorer')
    books = eval(conf.get('help', 'books'), {})
    for book in books:
        if calledAtStartup:
            print 'Help: loading %s'% os.path.basename(book)
        bookPath = os.path.normpath(jn(docsDir, book))
        if os.path.exists(bookPath):
            _hc.AddBook(bookPath,
                  not os.path.exists(jn(cacheDir,
                  os.path.basename(book)+'.cached')) or not calledAtStartup)

def initWxPyDocStrs():
    docStrs = os.path.join(Preferences.pyPath, 'Docs', 'wxDocStrings.msh')
    decorateWxPythonWithDocStrs(docStrs)


def pydocWarning():
    return wx.MessageBox('The pydoc server has not completely started up yet,\n '
                        'it is safer to wait for it to finish before shutting '
                        'down.\n\nDo you want to wait?', 'Pydoc server busy',
                        wx.YES_NO | wx.ICON_EXCLAMATION) == wx.YES

def canClosePydocServer():
    if _hc:
        f = _hc.frameX
        if f and f.pydocPage and f.pydocPage.waiting:
            return False
    return True

def testPydocServerAddress(host, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        try:
            sock.bind((host, port))
        except socket.error, (code, msg):
            if code == 10048: # address in use
                return False
            raise
    finally:
        sock.close()
    return True

def delHelp():
    global _hc
    if _hc:
        try:
            _hc.config.Flush()
        except AttributeError:
            pass
        else:
            if hasattr(_hc, 'server') and _hc.server and not _hc.server.quit:
                _hc.server.quit = 1
                _hc.server.server_close()
    
            f = _hc.GetFrame()
            if f:
                f.PopEventHandler().Destroy()
                f.Destroy()
        _hc.Destroy()
        _hc = None

def main(args):
    app = wx.PySimpleApp()
    wx.InitAllImageHandlers()
    initHelp()
    if args:
        showContextHelp(args[0])
    else:
        _hc.Display('')
    app.MainLoop()
    delHelp()

def _test(word):
    app = wx.PySimpleApp()
    wx.InitAllImageHandlers()
    initHelp()
    if word:
        showContextHelp(word)
    else:
        _hc.Display('')
    app.MainLoop()
    delHelp()


if __name__ == '__main__':
    #initWxPyDocStrs()
    main(sys.argv[1:])
    #_test('Window deletion overview')
