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

"""
@created: 2017-03-10 13:40:00

@author: Jorj X. McKie

Let the user select a Python file to display and update its links.

Dependencies:
PyMuPDF v1.10.x, wxPython Phoenix version

License:
 GNU GPL 3.x, GNU AFFERO GPL 3

Copyright:
 (c) 2017-2018 Jorj X. McKie

"""
from __future__ import print_function
import fitz
import wx
import sys, os
print("Python:", sys.version)
print("wxPython:", wx.version())
print(fitz.__doc__)
try:
    from PageFormat import FindFit
except ImportError:
    def FindFit(*args):
        return "not implemented"
try:
    from icons import ico_pdf            # PDF icon in upper left screen corner
    do_icon = True
except ImportError:
    do_icon = False

app = None
app = wx.App()
assert wx.VERSION[0:3] >= (3,0,3), "need wxPython Phoenix version"
assert tuple(map(int, fitz.VersionBind.split("."))) >= (1,10,0),\
       "need PyMuPDF 1.10.0 or later"
cur_hand  = wx.Cursor(wx.CURSOR_HAND)
cur_cross = wx.Cursor(wx.CURSOR_CROSS)
cur_nwse  = wx.Cursor(wx.CURSOR_SIZENWSE)
cur_norm  = wx.Cursor(wx.CURSOR_DEFAULT)

if str != bytes:                       # we are on Python 3
    stringtypes = (str, bytes)
else:
    stringtypes = (str, unicode)

def getint(v):
    try:
        return int(v)
    except:
        pass
    if type(v) not in stringtypes:
        return 0
    a = "0"
    for d in v:
        if d in "0123456789":
            a += d
    return int(a)

# abbreviations to get rid of those long pesky names ...
#==============================================================================
# Define our dialog as a subclass of wx.Dialog.
# Only special thing is, that we are being invoked with a filename ...
#==============================================================================
class PDFdisplay(wx.Dialog):
    def __init__(self, parent, filename):
        defPos = wx.DefaultPosition
        defSiz = wx.DefaultSize
        zoom   = 1.2                        # zoom factor of display
        wx.Dialog.__init__ (self, parent, id = -1,
            title = "Link Maintenance of ",
            pos = defPos, size = defSiz,
            style = wx.CAPTION|wx.CLOSE_BOX|
                    wx.DEFAULT_DIALOG_STYLE)

        #======================================================================
        # display an icon top left of dialog, append filename to title
        #======================================================================
        if do_icon:
            self.SetIcon(ico_pdf.img.GetIcon())      # set a screen icon
        self.SetTitle(self.Title + filename)
        KHAKI = wx.Colour(240, 230, 140)
        self.SetBackgroundColour(KHAKI)

        #======================================================================
        # open the document with MuPDF when dialog gets created
        #======================================================================
        self.doc = fitz.open(filename) # create Document object
        if self.doc.needsPass:         # check password protection
            self.decrypt_doc()
        if self.doc.isEncrypted:       # quit if we cannot decrypt
            self.Destroy()
            return
        self.pdf_vsn_ok = self.doc.metadata["format"].split()[1] > "1.1"
        self.link_code = {"NONE":0, "GOTO":1, "URI":2, "LAUNCH":3,
                           "NAMED":0, "GOTOR":5}
        self.link_to_idx = {0:0, 1:1, 2:2, 3:3, 4:0, 5:4}
        self.last_pno = -1             # memorize last page displayed
        self.link_rects = []           # store link rectangles here
        self.link_bottom_rects = []    # store bottom rectangles here
        self.link_texts = []           # store link texts here
        self.current_idx = -1          # store entry of found rectangle
        self.page_links = []           # list of links of page
        self.update_links = True       # False if unsupported links
        self.page_height = 0           # page height in pixels
        self.adding_link = False       # indicate new link in the making
        self.dragging_link = False     # indicate moving rect
        self.dragstart_x = -1          # for drags: original x
        self.dragstart_y = -1          # for drags: original y
        self.resize_rect = False       # indicate resizing rect
        self.sense = 5                 # cursor tolerance, e.g. min. rectangle
                                       # side is 2 * self.sense pixels

        # forward button
        self.btn_Next = wx.Button(self, -1, "forw",
                           defPos, defSiz, wx.BU_EXACTFIT)
        # backward button
        self.btn_Previous = wx.Button(self, -1, "back",
                               defPos, defSiz, wx.BU_EXACTFIT)
        #======================================================================
        # text field for entering a target page. wx.TE_PROCESS_ENTER is
        # required to get data entry fired as events.
        #======================================================================
        self.TextToPage = wx.TextCtrl(self, -1, "1", defPos,
                             wx.Size(40, -1), wx.TE_RIGHT|wx.TE_PROCESS_ENTER)
        # displays total pages and page paper format
        self.statPageMax = wx.StaticText(self, -1,
                              "of " + str(len(self.doc)) + " pages.",
                              defPos, defSiz, 0)
        self.paperform = wx.StaticText(self, -1, "", defPos, defSiz, 0)
        #======================================================================
        # define zooming matrix for displaying PDF page images
        # we increase images by 20%, so take 1.2 as scale factors
        #======================================================================
        self.zoom = fitz.Matrix(zoom, zoom)    # will use a constant zoom
        self.shrink = ~self.zoom               # corresp. shrink matrix
        self.bitmap = self.pdf_show(1)
        self.PDFimage = wx.StaticBitmap(self, -1, self.bitmap,
                           defPos, defSiz, style = 0)
        
        #======================================================================
        # Fields defining a PDF link
        #======================================================================
        self.t_Update = wx.StaticText(self, -1, "")
        self.t_Save = wx.StaticText(self, -1, "")

        self.linkTypeStrings = ["NONE", "GOTO", "URI", "LAUNCH", "GOTOR"]
        self.linkType = wx.Choice(self, -1, defPos, defSiz,
                           self.linkTypeStrings)
        self.fromLeft = wx.SpinCtrl(self, -1, "",
                           defPos, wx.Size(60, -1), 
                           wx.TE_RIGHT|wx.SP_ARROW_KEYS|wx.TE_PROCESS_ENTER,
                           0, 9999)
        self.fromTop = wx.SpinCtrl(self, -1, "",
                          defPos, wx.Size(60, -1), 
                          wx.TE_RIGHT|wx.SP_ARROW_KEYS|wx.TE_PROCESS_ENTER,
                          0, 9999)
        self.fromWidth = wx.SpinCtrl(self, -1, "",
                            defPos, wx.Size(60, -1), 
                            wx.TE_RIGHT|wx.SP_ARROW_KEYS|wx.TE_PROCESS_ENTER,
                            0, 9999)
        self.fromHeight = wx.SpinCtrl(self, -1, "",
                             defPos, wx.Size(60, -1), 
                             wx.TE_RIGHT|wx.SP_ARROW_KEYS|wx.TE_PROCESS_ENTER,
                             0, 9999)
        self.toFile = wx.TextCtrl(self, -1, "", defPos,
                         wx.Size(350,-1), wx.TE_PROCESS_ENTER)
        self.toURI = wx.TextCtrl(self, -1, "", defPos,
                        wx.Size(350,-1), wx.TE_PROCESS_ENTER)
        self.toPage = wx.TextCtrl(self, -1, "", defPos,
                         wx.Size(60, -1), wx.TE_RIGHT|wx.TE_PROCESS_ENTER)
        self.toLeft = wx.TextCtrl(self, -1, "", defPos,
                         wx.Size(60, -1), wx.TE_RIGHT|wx.TE_PROCESS_ENTER)
        self.toHeight = wx.TextCtrl(self, -1, "", defPos,
                           wx.Size(60, -1), wx.TE_RIGHT|wx.TE_PROCESS_ENTER)
        self.toName = wx.TextCtrl(self, -1, "", defPos,
                         wx.Size(300,-1), wx.TE_PROCESS_ENTER)
        self.btn_Update = wx.Button(self, -1, "UPDATE PAGE",
                             defPos, defSiz, wx.BU_EXACTFIT)
        self.btn_NewLink = wx.Button(self, -1, "NEW LINK",
                              defPos, self.btn_Update.Size, 0)
        self.btn_Save = wx.Button(self, -1, "SAVE FILE",
                           defPos, self.btn_Update.Size, 0)
        #======================================================================
        # sizers of the dialog
        #======================================================================
        szr00 = wx.BoxSizer(wx.HORIZONTAL)        
        szr10 = wx.BoxSizer(wx.VERTICAL)
        szr20 = wx.BoxSizer(wx.HORIZONTAL)
        szr30 = wx.GridBagSizer(5, 5)
        
        szr20.Add(self.btn_Next, 0, wx.ALL, 5)
        szr20.Add(self.btn_Previous, 0, wx.ALL, 5)
        szr20.Add(self.TextToPage, 0, wx.ALL, 5)
        szr20.Add(self.statPageMax, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
        szr20.Add(self.paperform, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
#==============================================================================
#       use GridBagSizer for link details  
#==============================================================================
        line_span = wx.GBSpan(1, 5)    # use for item taking full line
        #----------------------------------------------------------------------
        # link details header
        #----------------------------------------------------------------------
        t = wx.StaticText(self, -1, "Link Details", defPos, defSiz,
                          wx.ALIGN_CENTER)
        t.SetBackgroundColour("STEEL BLUE")
        t.SetForegroundColour("WHITE")
        szr30.Add(t, (0,0), line_span, wx.EXPAND) # overall header
        szr30.Add(wx.StaticLine(self, -1, defPos, defSiz, wx.LI_HORIZONTAL),
                  (1,0), line_span, wx.EXPAND)
        #----------------------------------------------------------------------
        # hot spot header and fields
        #----------------------------------------------------------------------
        t = wx.StaticText(self, -1, "Hot Spot", defPos, defSiz,
                          wx.ALIGN_CENTER)
        t.SetBackgroundColour("GOLD")
        szr30.Add(t,
                  (2,0), line_span, wx.EXPAND) # hot area header
        szr30.Add(wx.StaticText(self, -1, "Left:"),
                  (3,0), (1,1), wx.ALIGN_RIGHT)
        szr30.Add(self.fromLeft,
                  (3,1), (1,1), wx.ALIGN_LEFT)
        szr30.Add(wx.StaticText(self, -1, "Top:"),
                  (3,2), (1,1), wx.ALIGN_RIGHT)
        szr30.Add(self.fromTop, (3,3), (1,1), wx.ALIGN_LEFT)
        szr30.Add(wx.StaticText(self, -1, "Width:"),
                  (4,0), (1,1), wx.ALIGN_RIGHT)
        szr30.Add(self.fromWidth,
                  (4,1), (1,1), wx.ALIGN_LEFT)
        szr30.Add(wx.StaticText(self, -1, "Height:"),
                  (4,2), (1,1), wx.ALIGN_RIGHT)
        szr30.Add(self.fromHeight,
                  (4,3), (1,1), wx.ALIGN_LEFT)

        szr30.Add(wx.StaticLine(self, -1, defPos, defSiz, wx.LI_HORIZONTAL),
                  (5,0), line_span, wx.EXPAND)
        #----------------------------------------------------------------------
        # link destination header and fields
        #----------------------------------------------------------------------
        t = wx.StaticText(self, -1, "Link Destination", defPos, defSiz,
                          wx.ALIGN_CENTER)
        t.SetBackgroundColour("YELLOW GREEN")
        szr30.Add(t, (6,0), line_span, wx.EXPAND)
        szr30.Add(wx.StaticText(self, -1, "Link Type:"),
                  (7,0), (1,1), wx.ALIGN_RIGHT)
        szr30.Add(self.linkType,
                  (7,1))
        szr30.Add(wx.StaticText(self, -1, " to page:"),
                  (8,0), (1,1), wx.ALIGN_RIGHT)
        szr30.Add(self.toPage,
                  (8,1))
        szr30.Add(wx.StaticText(self, -1, "at left/top:"),
                  (8,2), (1,1), wx.ALIGN_RIGHT)
        szr30.Add(self.toLeft,
                  (8,3))
        szr30.Add(self.toHeight,
                  (8,4), (1,1), wx.ALIGN_LEFT)
        szr30.Add(wx.StaticText(self, -1, " or to name:"),
                  (9,0), (1,1), wx.ALIGN_RIGHT)
        szr30.Add(self.toName, (9,1), (1,4))
        szr30.Add(wx.StaticText(self, -1, " File:"),
                  (10,0), line_span)
        szr30.Add(self.toFile,
                  (11,0), line_span)
        szr30.Add(wx.StaticText(self, -1, " URL:"),
                  (12,0), line_span)
        szr30.Add(self.toURI,
                  (13,0), line_span)
        #----------------------------------------------------------------------
        # buttons
        #----------------------------------------------------------------------
        szr30.Add(wx.StaticLine(self, -1, defPos, defSiz, wx.LI_HORIZONTAL),
                  (14,0), line_span, wx.EXPAND)
        szr30.Add(self.btn_Update, (15,0), (1,1))
        szr30.Add(self.t_Update, (15,1), (1,4))
        szr30.Add(self.btn_NewLink, (16,0), (1,1))
        szr30.Add(wx.StaticText(self, -1, "Create a new link",
                                defPos, defSiz, wx.ALIGN_LEFT),
                  (16,1), (1,4))
        szr30.Add(self.btn_Save, (17,0), (1,1))
        szr30.Add(self.t_Save, (17,1), (1,4))
        szr30.Add(wx.StaticLine(self, -1, defPos, defSiz, wx.LI_HORIZONTAL),
                  (18,0), line_span, wx.EXPAND)
        
        szr10.Add(szr20, 0, wx.EXPAND, 5)
        szr10.Add(self.PDFimage, 0, wx.ALL, 5)
        
        szr00.Add(szr30, 0, wx.ALL, 5)
        szr00.Add(wx.StaticLine(self, -1, defPos, defSiz, wx.LI_VERTICAL),
                  0, wx.EXPAND, 5)
        szr00.Add(szr10, 0, wx.EXPAND, 5)
        
        szr00.Fit(self)
        self.SetSizer(szr00)
        self.Layout()
        
        self.Centre(wx.BOTH)

        # Bind dialog items to event handlers
        self.btn_Save.Bind(wx.EVT_BUTTON, self.on_save_file)
        self.btn_Update.Bind(wx.EVT_BUTTON, self.on_update_page_links)
        self.btn_Next.Bind(wx.EVT_BUTTON, self.on_next_page)
        self.btn_Previous.Bind(wx.EVT_BUTTON, self.on_previous_page)
        self.btn_NewLink.Bind(wx.EVT_BUTTON, self.on_new_link)
        self.TextToPage.Bind(wx.EVT_TEXT_ENTER, self.on_goto_page)
        self.PDFimage.Bind(wx.EVT_ENTER_WINDOW, self.on_enter_window)
        self.PDFimage.Bind(wx.EVT_MOUSEWHEEL, self.on_mouse_wheel)
        self.PDFimage.Bind(wx.EVT_MOTION, self.on_move_mouse)
        self.PDFimage.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
        self.PDFimage.Bind(wx.EVT_LEFT_UP, self.on_left_up)
        self.linkType.Bind(wx.EVT_CHOICE, self.on_linkType_changed)
        self.fromHeight.Bind(wx.EVT_SPINCTRL, self.on_link_changed)
        self.fromLeft.Bind(wx.EVT_SPINCTRL, self.on_link_changed)
        self.fromTop.Bind(wx.EVT_SPINCTRL, self.on_link_changed)
        self.fromWidth.Bind(wx.EVT_SPINCTRL, self.on_link_changed)
        self.toLeft.Bind(wx.EVT_TEXT_ENTER, self.on_link_changed)
        self.toHeight.Bind(wx.EVT_TEXT_ENTER, self.on_link_changed)
        self.toURI.Bind(wx.EVT_TEXT_ENTER, self.on_link_changed)
        self.toFile.Bind(wx.EVT_TEXT_ENTER, self.on_link_changed)
        self.toPage.Bind(wx.EVT_TEXT_ENTER, self.on_link_changed)
        self.toName.Bind(wx.EVT_TEXT_ENTER, self.on_link_changed)
        self.toLeft.Bind(wx.EVT_TEXT, self.on_link_changed)
        self.toHeight.Bind(wx.EVT_TEXT, self.on_link_changed)
        self.toURI.Bind(wx.EVT_TEXT, self.on_link_changed)
        self.toFile.Bind(wx.EVT_TEXT, self.on_link_changed)
        self.toPage.Bind(wx.EVT_TEXT, self.on_link_changed)
        self.toName.Bind(wx.EVT_TEXT, self.on_link_changed)
        self.btn_Update.Disable()
        self.btn_Save.Disable()
        self.clear_link_details()

    def __del__(self):
        pass

#==============================================================================
# Button handlers and other functions
#==============================================================================

    def on_enter_window(self, evt):
        if not self.update_links:
            self.btn_NewLink.Disable()
            self.enable_update()
        self.draw_links()
        evt.Skip()
        return
    
    def on_move_mouse(self, evt):
        pos = evt.GetPosition()
        in_rect = self.get_linkrect_idx(pos)     # rect number we are in
        in_brect = self.get_bottomrect_idx(pos)  # bottom-right corner we are in
        self.PDFimage.SetCursor(cur_norm)        # standard cursor
            
        if in_brect >= 0:                        # cursor if in br corner
            self.PDFimage.SetCursor(cur_nwse)
        elif in_rect >= 0:                       # cursor if in a rect
            self.PDFimage.SetCursor(cur_hand)
        
        if self.adding_link:                     # painting new hot spot?
            if in_rect >= 0 or in_brect >= 0:    # must be outside others
                evt.Skip()
                return
            self.PDFimage.SetCursor(cur_cross)   # cursor if painting
            if evt.LeftIsDown():                 # mouse pressed? go!
                w = abs(pos.x - self.addrect.x)  # new rect values
                h = abs(pos.y - self.addrect.y)
                x = min(self.addrect.x, pos.x)
                y = min(self.addrect.y, pos.y)
                if not self.is_in_free_area(wx.Rect(x, y, w, h)):
                    evt.Skip()                   # do not allow overlaps
                    return
                if h <= self.sense or w <= self.sense: # too small!
                    evt.Skip()
                    return
                self.fromHeight.SetValue(h)      # update ...
                self.fromWidth.SetValue(w)       # ... spin ...
                self.fromLeft.SetValue(x)        # ... controls
                self.fromTop.SetValue(y)
                self.redraw_bitmap()
                self.draw_links()
                self.draw_rect(x, y, w, h, "BLUE")  # draw rectangle
                self.current_idx = -1            # means: not in an old rect
            evt.Skip()
            return

        if len(self.page_links) == 0:            # there are no links yet
            evt.Skip()
            return
        
        if self.resize_rect:                     # resizing hot spot?
            self.PDFimage.SetCursor(cur_nwse)    # adjust cursor
            if evt.LeftIsDown():                 # mouse pressed? go!
                r = self.link_rects[in_brect]    # resizing this rectangle
                w = pos.x - r.x                  # new width
                h = pos.y - r.y                  # new height
                nr = wx.Rect(r.x, r.y, w, h)     # new retangle
                # if large anough and no overlaps:
                if w >= 2 * self.sense and h >= 2 * self.sense and \
                    self.is_in_free_area(nr, ok = in_brect):
                    l = self.page_links[in_brect]     # page link entry 
                    l["from"] = self.wxRect_to_Rect(nr)    # get fitz format
                    l["update"] = True
                    self.page_links[in_brect] = l     # store change link
                    self.fromHeight.SetValue(h)
                    self.fromWidth.SetValue(w)
                    self.redraw_bitmap()
                    self.draw_links()
            evt.Skip()
            return
            
        if in_rect >= 0:                         # still here and inside a rect?
            self.PDFimage.SetCursor(cur_hand)    # adjust cursor
            self.PDFimage.SetToolTip(self.link_texts[in_rect])
            if self.dragging_link:               # are we moving the hot spot?
                if evt.LeftIsDown():
                    r = self.link_rects[in_rect] # this is the rectangle
                    x = pos.x - self.dragstart_x # new left ...
                    y = pos.y - self.dragstart_y # ... and top values
                    w = r.width                  # shape does ...
                    h = r.height                 # ... not change
                    nr = wx.Rect(x, y, w, h)     # new rectangle
                    if self.is_in_free_area(nr, ok = in_rect):  # no overlaps?
                        self.fromLeft.SetValue(x)     # new screen value
                        self.fromTop.SetValue(y)      # new screen value
                        fr = self.wxRect_to_Rect(nr)  # fitz format of new rect
                        l = self.page_links[in_rect]  # this is the link
                        l["from"] = fr                # update its hot spot
                        l["update"] = True       # we need to update
                        self.page_links[in_rect] = l
                        self.redraw_bitmap()
                        self.draw_links()
            evt.Skip()
            return
        
        self.PDFimage.UnsetToolTip()

        evt.Skip()
        return

    def on_mouse_wheel(self, evt):
        # process wheel as paging operations
        d = evt.GetWheelRotation()               # int indicating direction
        if d < 0:
            self.on_next_page(evt)
        elif d > 0:
            self.on_previous_page(evt)
        return

    def on_next_page(self, event):                # means: page forward
        page = getint(self.TextToPage.Value) + 1  # current page + 1
        page = min(page, self.doc.pageCount)      # cannot go beyond last page
        self.TextToPage.ChangeValue(str(page))    # put target page# in screen
        self.new_image(page)                      # refresh the layout
        event.Skip()

    def on_previous_page(self, event):            # means: page back
        page = getint(self.TextToPage.Value) - 1  # current page - 1
        page = max(page, 1)                       # cannot go before page 1
        self.TextToPage.ChangeValue(str(page))    # put target page# in screen
        self.new_image(page)
        event.Skip()

    def on_goto_page(self, event):                # means: go to page number
        page = getint(self.TextToPage.Value)      # get page# from screen
        page = min(page, len(self.doc))           # cannot go beyond last page
        page = max(page, 1)                       # cannot go before page 1
        self.TextToPage.ChangeValue(str(page))    # make sure it's on the screen
        self.new_image(page)
        event.Skip()

    def on_update_page_links(self, evt):
        """ Perform PDF update of changed links."""
        if not self.update_links:                 # skip if unsupported links
            evt.Skip()
            return
        pg = self.doc[getint(self.TextToPage.Value) -1] # read PDF page (again!)
        for i in range(len(self.page_links)):
            l = self.page_links[i]
            if l.get("update", False):            # "update" must be True
                if l["xref"] == 0:                # no xref => new link
                    pg.insertLink(l)
                elif l["kind"] == fitz.LINK_NONE:
                    pg.deleteLink(l)              # delete invalid link
                else:
                    pg.updateLink(l)              # else update link
            l["update"] = False                   # reset update indicator
            self.page_links[i] = l                # update page links list
        self.btn_Update.Disable()                 # disable update button
        self.t_Update.Label = ""                  # and its message
        self.btn_Save.Enable()
        self.t_Save.Label = "There are changes. Press here to save them."
        self.current_idx = -1
        evt.Skip()
        return
    
    def on_link_changed(self, evt):
        if self.current_idx < 0:                 # no link selected
            self.clear_link_details()
            evt.Skip()
            return
        repaint = False                           # reset when we must repaint
        lnk = self.page_links[self.current_idx]   # we deal with this link
        n = self.linkType.GetSelection()
        lstr = self.linkType.GetString(n)
        lnk["kind"] = self.link_code[lstr]
        # rectangle in link details on screen:
        r = wx.Rect(self.fromLeft.Value, self.fromTop.Value,
                    self.fromWidth.Value, self.fromHeight.Value)
        lr = self.link_rects[self.current_idx]    # stored rectangle

        if tuple(r) != tuple(lr):                 # something has changed
            if self.is_in_free_area(r, ok = self.current_idx):
                lnk["from"] = self.wxRect_to_Rect(r)
                lnk["update"] = True              # update rectangle in list
                repaint = True
            else:                                 # reset values: invalid change
                self.fromHeight.SetValue(lr.Height)
                self.fromWidth.SetValue(lr.Width)
                self.fromTop.SetValue(lr.y)
                self.fromLeft.SetValue(lr.x)

        new_to = lnk.get("to", fitz.Point(0,0))
        try:
            new_to.x = float(self.toLeft.Value)
        except:
            pass

        try:
            new_to.y = float(self.toHeight.Value)
        except:
            pass

        lnk["to"] = new_to                        # destination point
        
        if self.toPage.IsModified:                # dest page modified
            if self.toPage.Value.isdecimal():
                lnk["page"] = int(self.toPage.Value) - 1
            else:
                lnk["page"] = -1
            repaint = True
            lnk["update"] = True

        if self.toName.IsModified:                # dest file modified
            if self.toName.Value:
                lnk["to"] = self.toName.Value
            repaint = True
            lnk["update"] = True
               
        if self.toFile.IsModified:                # dest file modified
            lnk["file"] = self.toFile.Value
            repaint = True
            lnk["update"] = True
               
        if self.toURI.IsModified:                 # dest URI modified
            lnk["uri"] = self.toURI.Value
            repaint = True
            lnk["update"] = True

        self.page_links[self.current_idx] = lnk   # update page link list
        if repaint:
            self.redraw_bitmap()
            self.draw_links()
            r = self.link_rects[self.current_idx]
            self.draw_rect(r.x, r.y, r.width, r.height, "RED")
        evt.Skip()
        return
    
    def on_linkType_changed(self, evt):
        """ Changed link kind - prepare available fields."""
        if self.current_idx < 0:
            self.clear_link_details()
            evt.Skip()
            return
        n = self.linkType.GetSelection()
        lt_str = self.linkType.GetString(n)
        lt = self.link_code[lt_str]
        self.prep_link_details(lt)
            
        lnk = self.page_links[self.current_idx]
        lnk["update"] = True
        lnk["kind"] = lt
        self.enable_update()

        if lt == fitz.LINK_GOTO:
            if not self.toPage.Value.isdecimal():
                self.toPage.ChangeValue("1")
            self.toPage.Enable()
            if not self.toLeft.Value.isdecimal():
                self.toLeft.ChangeValue("0")
            self.toLeft.Enable()
            if not self.toHeight.Value.isdecimal():
                self.toHeight.ChangeValue("0")
            self.toHeight.Enable()
            lnk["page"] = int(self.toPage.Value) - 1
            lnk["to"] = fitz.Point(int(self.toLeft.Value),
                                   int(self.toHeight.Value))

        elif lt == fitz.LINK_GOTOR:
            if not self.toFile.Value:
                self.toFile.SetValue(self.text_in_rect())
                self.toFile.MarkDirty()               

            self.toPage.ChangeValue("-1")
            self.toLeft.ChangeValue("")
            self.toHeight.ChangeValue("")
            self.toName.ChangeValue("")
            self.toLeft.Enable()
            self.toPage.Enable()
            self.toFile.Enable()
            self.toHeight.Enable()
            self.toName.Enable()
            lnk["file"] = self.toFile.Value
            lnk["page"] = -1
            lnk["to"] = ""

        elif lt == fitz.LINK_URI:
            if not self.toURI.Value:
                self.toURI.SetValue(self.text_in_rect())
                self.toURI.MarkDirty()
            lnk["uri"] = self.toURI.Value
            self.toURI.Enable()
            
        elif lt == fitz.LINK_LAUNCH:
            if not self.toFile.Value:
                self.toFile.SetValue(self.text_in_rect())
                self.toFile.MarkDirty()
            lnk["file"] = self.toFile.Value
            self.toFile.Enable()
            
        self.page_links[self.current_idx] = lnk
            
        evt.Skip()
        return
    
    def on_save_file(self, evt):
        indir, infile = os.path.split(self.doc.name)
        odir = indir
        ofile = infile
        if self.doc.needsPass or not self.doc.can_save_incrementally():
            ofile = ""
        sdlg = wx.FileDialog(self, "Specify Output", odir, ofile,
                                   "PDF files (*.pdf)|*.pdf", wx.FD_SAVE)
        if sdlg.ShowModal() == wx.ID_CANCEL:
            evt.Skip()
            return
        outfile = sdlg.GetPath()
        if self.doc.needsPass or not self.doc.can_save_incrementally():
            title =  "Repaired / decrypted PDF requires new output file"
            while outfile == self.doc.name:
                sdlg = wx.FileDialog(self, title, odir, "",
                                     "PDF files (*.pdf)|*.pdf", wx.FD_SAVE)
                if sdlg.ShowModal() == wx.ID_CANCEL:
                    evt.Skip()
                    return
                outfile = sdlg.GetPath()
        self.doc._delXmlMetadata()
        if outfile == self.doc.name:
            self.doc.saveIncr()                       # equal: update input file
        else:
            self.doc.save(outfile, garbage=4)
        
        sdlg.Destroy()
        self.btn_Save.Disable()
        evt.Skip()
        return

    def on_new_link(self, evt):
        self.adding_link = True
        self.linkType.SetSelection(1)
        self.prep_link_details(1)
        self.current_idx = -1
        self.fromHeight.SetValue(0)
        self.fromLeft.SetValue(0)
        self.fromTop.SetValue(0)
        self.fromWidth.SetValue(0)
        self.toPage.ChangeValue("1")
        self.toLeft.ChangeValue("0")
        self.toHeight.ChangeValue("0")
        evt.Skip()
        return
    
    def on_left_up(self, evt):
        if self.adding_link:
            n = self.linkType.GetSelection()
            wxr = wx.Rect(self.fromLeft.Value, self.fromTop.Value,
                           self.fromWidth.Value, self.fromHeight.Value)
            l_from = self.wxRect_to_Rect(wxr)
            l_to = fitz.Point(float(self.toLeft.Value),
                              float(self.toHeight.Value))
            lnk = {"kind": n, "xref": 0, "from": l_from, "to": l_to,
                   "page": int(self.toPage.Value) - 1, "update": True}
            
            self.link_rects.append(wxr)
            self.page_links.append(lnk)
            p = wxr.BottomRight
            br = wx.Rect(p.x - self.sense, p.y - self.sense,
                         2*self.sense, 2*self.sense)
            self.link_bottom_rects.append(br)
            self.link_texts.append("page " + self.toPage.Value)
            self.adding_link = False
            del self.addrect
            self.current_idx = -1
            self.PDFimage.SetCursor(cur_norm)
            self.clear_link_details()
            self.toPage.Enable()
            self.toLeft.Enable()
            self.toHeight.Enable()
            self.linkType.Enable()
            evt.Skip()
            return
        self.adding_link = False
        self.dragging_link = False
        self.dragstart_x = -1
        self.dragstart_y = -1
        self.resize_rect = False
        self.PDFimage.SetCursor(cur_norm)
        evt.Skip()
        return
    
    def on_left_down(self, evt):
        pos = evt.GetPosition()
        in_rect = self.get_linkrect_idx(pos)
        in_brect = self.get_bottomrect_idx(pos)

        if self.adding_link:
            # pos is top-left of new rectangle
            if in_rect >= 0 or in_brect >= 0:
                self.adding_link = False
            else:
                self.addrect = wx.Rect(pos.x, pos.y, 0, 0)
                self.fromTop.SetValue(pos.y)
                self.fromLeft.SetValue(pos.x)
            evt.Skip()
            return
        
        if in_brect >= 0:
            self.resize_rect = True
            self.current_idx = in_brect
            evt.Skip()
            return
        
        if in_rect >= 0:
            self.dragging_link = True       # we are about to drag
            r = self.link_rects[in_rect]    # the wx.Rect we will be dragging
            self.dragstart_x = pos.x - r.x  # delta to left
            self.dragstart_y = pos.y - r.y  # delta to top 
        
        if in_rect < 0:
            self.current_idx = -1
            evt.Skip()
            return
        self.current_idx = in_rect
        lnk = self.page_links[in_rect]
        r = self.link_rects[in_rect]
        self.fromLeft.SetValue(r.x)
        self.fromTop.SetValue(r.y)
        self.fromHeight.SetValue(r.Height)
        self.fromWidth.SetValue(r.Width)
        self.draw_links()
        self.draw_rect(r.x, r.y, r.width, r.height, "RED")

        lidx = self.link_to_idx[lnk["kind"]]
        self.linkType.SetSelection(lidx)
         
        self.prep_link_details(lnk["kind"])
        if lnk["kind"] in (fitz.LINK_GOTO, fitz.LINK_GOTOR):
            if lnk["page"] >= 0:
                self.toPage.ChangeValue(str(lnk["page"] + 1))
                self.toLeft.ChangeValue(str(lnk["to"][0]))
                self.toHeight.ChangeValue(str(lnk["to"][1]))
            else:
                self.toPage.ChangeValue("-1")
                self.toLeft.ChangeValue("")
                self.toHeight.ChangeValue("")
                self.toName.ChangeValue(lnk["to"])
            if lnk["kind"] == fitz.LINK_GOTOR:
                self.toFile.ChangeValue(lnk["file"])
                self.toFile.Enable()
            self.toPage.Enable()
            self.toLeft.Enable()
            self.toHeight.Enable()
            if lnk["kind"] == fitz.LINK_GOTOR or self.pdf_vsn_ok:
                self.toName.Enable()
        
        elif lnk["kind"] == fitz.LINK_URI:
            self.toURI.ChangeValue(lnk["uri"])
            self.toURI.Enable()
        
        elif lnk["kind"] in (fitz.LINK_GOTOR, fitz.LINK_LAUNCH):
            self.toFile.ChangeValue(lnk["file"])
            self.toFile.Enable()

        evt.Skip()
        return

    def enable_update(self):
        if self.update_links:
            self.btn_Update.Enable()
            self.t_Update.Label = "Contains changed links. Press to update."
        else:
            self.btn_Update.Disable()
            self.t_Update.Label = "Contains unsupported links. Cannot update!"
        return
        
    def draw_links(self):
        dc = wx.ClientDC(self.PDFimage)
        dc.SetPen(wx.Pen("BLUE", width=1))
        dc.SetBrush(wx.Brush("BLUE", style=wx.BRUSHSTYLE_TRANSPARENT))
        self.link_rects = []
        self.link_bottom_rects = []
        self.link_texts = []
        for lnk in self.page_links:
            if lnk.get("update", False):
                self.enable_update()
            wxr = self.Rect_to_wxRect(lnk["from"])
            dc.DrawRectangle(wxr[0], wxr[1], wxr[2], wxr[3])
            self.link_rects.append(wxr)
            p = wxr.BottomRight
            br = wx.Rect(p.x - self.sense, p.y - self.sense,
                         2*self.sense, 2*self.sense)
            self.link_bottom_rects.append(br)
            if lnk["kind"] == fitz.LINK_GOTO:
                if lnk["page"] >= 0:
                    txt = "page " + str(lnk["page"] + 1)
                else:
                    txt = "name " + str(lnk["to"])
            elif lnk["kind"] == fitz.LINK_GOTOR:
                txt = lnk["file"]
                if lnk["page"] >= 0:
                    txt += " p. " + str(lnk["page"] + 1)
                else:
                    txt += "(" + str(lnk["to"]) + ")"
            elif lnk["kind"] == fitz.LINK_URI:
                txt = lnk["uri"]
            elif lnk["kind"] == fitz.LINK_LAUNCH:
                txt = "open " + lnk["file"]
            elif lnk["kind"] == fitz.LINK_NONE:
                txt = "none"
            else:
                txt = "unkown destination"
            self.link_texts.append(txt)
        return

    def redraw_bitmap(self):
        """Refresh bitmap image."""
        w = self.bitmap.Size[0]
        h = self.bitmap.Size[1]
        x = y = 0
        rect = wx.Rect(x, y, w, h)
        bm = self.bitmap.GetSubBitmap(rect)
        dc = wx.ClientDC(self.PDFimage)     # make a device control out of img
        dc.DrawBitmap(bm, x, y)             # refresh bitmap before draw
        return

    def draw_rect(self, x, y, w, h, c):
        dc = wx.ClientDC(self.PDFimage)
        dc.SetPen(wx.Pen(c, width=1))
        dc.SetBrush(wx.Brush(c, style=wx.BRUSHSTYLE_TRANSPARENT))
        dc.DrawRectangle(x, y, w, h)
        return

    def clear_link_details(self):
        self.linkType.SetSelection(wx.NOT_FOUND)
        self.fromLeft.SetValue("")
        self.fromTop.SetValue("")
        self.fromHeight.SetValue("")
        self.fromWidth.SetValue("")
        self.toFile.ChangeValue("")
        self.toPage.ChangeValue("")
        self.toHeight.ChangeValue("")
        self.toLeft.ChangeValue("")
        self.toURI.ChangeValue("")
        self.toName.ChangeValue("")
        self.toName.Disable()
        self.btn_Update.Disable()
        self.t_Update.Label = ""
        self.toFile.Disable()
        self.toLeft.Disable()
        self.toHeight.Disable()
        self.toURI.Disable()
        self.toPage.Disable()
        self.adding_link = False
        self.resize_rect = False
        
        return

    def prep_link_details(self, lt):
        if lt not in (fitz.LINK_GOTOR, fitz.LINK_LAUNCH):
            self.toFile.ChangeValue("")
            self.toFile.Disable()
        
        if lt not in (fitz.LINK_GOTO, fitz.LINK_GOTOR):
            self.toPage.ChangeValue("")
            self.toHeight.ChangeValue("")
            self.toLeft.ChangeValue("")
            self.toName.ChangeValue("")
            self.toLeft.Disable()
            self.toHeight.Disable()
            self.toPage.Disable()
            self.toName.Disable()
        
        if lt != fitz.LINK_URI:
            self.toURI.ChangeValue("")
            self.toURI.Disable()
        
        return

    def text_in_rect(self):
        wxr = wx.Rect(self.fromLeft.Value, self.fromTop.Value,
                      self.fromWidth.Value, self.fromHeight.Value)
        r = self.wxRect_to_Rect(wxr)
        text = wx.EmptyString
        for b in self.text_blocks:
            if fitz.Rect(b[:4]) in r:
                text += b[4]
        return text
        
        
    def Rect_to_wxRect(self, fr):
        """ Return a zoomed wx.Rect for given fitz.Rect."""
        r = (fr * self.zoom).irect   # zoomed IRect
        return wx.Rect(r.x0, r.y0, r.width, r.height)   # wx.Rect version
    
    def wxRect_to_Rect(self, wr):
        """ Return a shrunk fitz.Rect for given wx.Rect."""
        r = fitz.Rect(wr.x, wr.y, wr.x + wr.width, wr.y + wr.height)
        return r * self.shrink        # shrunk fitz.Rect version
    
    def is_in_free_area(self, nr, ok = -1):
        """ Determine if rect covers a free area inside the bitmap."""
        for i, r in enumerate(self.link_rects):
            if r.Intersects(nr) and i != ok:
                return False
        bmrect = wx.Rect(0,0,dlg.bitmap.Size[0],dlg.bitmap.Size[1])
        return bmrect.Contains(nr)
        
    def get_linkrect_idx(self, pos):
        """ Determine if cursor is inside one of the link hot spots."""
        for i, r in enumerate(self.link_rects):
            if r.Contains(pos):
                return i
        return -1
        
    def get_bottomrect_idx(self, pos):
        """ Determine if cursor is on bottom right corner of a hot spot."""
        for i, r in enumerate(self.link_bottom_rects):
            if r.Contains(pos):
                return i
        return -1

#==============================================================================
# Read / render a PDF page. Parameters are: pdf = document, page = page number
#==============================================================================
    def new_image(self, pno):
        if pno == self.last_pno:
            return
        self.last_pno = pno
        self.clear_link_details()
        self.link_rects = []
        self.link_bottom_rects = []
        self.link_texts = []
        self.bitmap = self.pdf_show(pno)         # get page bitmap
        # following takes care of changed page sizes --------------------------
        bm = wx.Bitmap(self.bitmap.Size[0], self.bitmap.Size[1],
                            self.bitmap.Depth)
        self.PDFimage.SetBitmap(bm)              # empty bitmap for adjustment
        self.Fit()                               # tell dialog to fit its kids
        self.Layout()                            # probably not needed
        # end of page adjustments ---------------------------------------------
        self.PDFimage.SetBitmap(self.bitmap)     # put it in screen
        self.draw_links()                        # draw link rectangles
        return

    def pdf_show(self, pno):
        page = self.doc[getint(pno) - 1]         # load page & get Pixmap
        pix = page.getPixmap(matrix = self.zoom, alpha = False)
        bmp = wx.Bitmap.FromBuffer(pix.w, pix.h, pix.samples)
        paper = FindFit(page.rect.width, page.rect.height)
        self.paperform.Label = "Page format: " + paper
        self.page_links = page.getLinks()
        self.update_links = True
        if len(self.page_links) > 0:
            if self.page_links[0]["xref"] < 1:
                self.update_links = False
        self.page_height = page.rect.height
        self.text_blocks = page.getTextBlocks()
        return bmp

    def decrypt_doc(self):
        # let user enter document password
        pw = None
        dlg = wx.TextEntryDialog(self, 'Please Enter Password',
                 'Document needs password to open', '',
                 style = wx.TextEntryDialogStyle|wx.TE_PASSWORD)
        while pw is None:
            rc = dlg.ShowModal()
            if rc == wx.ID_OK:
                pw = str(dlg.GetValue().encode("utf-8"))
                self.doc.authenticate(pw)
            else:
                return
            if self.doc.isEncrypted:
                pw = None
                dlg.SetTitle("Wrong password. Enter correct one or cancel.")
        return

#==============================================================================
# main program
#------------------------------------------------------------------------------
# Show a standard FileSelect dialog to choose a file
#==============================================================================
# Wildcard: only offer PDF files
wild = "*.pdf"

#==============================================================================
# define the file selection dialog
#==============================================================================
dlg = wx.FileDialog(None, message = "Choose a file to display",
                    wildcard = wild, style=wx.FD_OPEN|wx.FD_CHANGE_DIR)

# We got a file only when one was selected and OK pressed
if dlg.ShowModal() == wx.ID_OK:
    filename = dlg.GetPath()
else:
    filename = None

# destroy this dialog
dlg.Destroy()

# only continue if we have a filename
if filename:
    # create the dialog
    dlg = PDFdisplay(None, filename)
    # show it - this will only return for final housekeeping
    rc = dlg.ShowModal()
app = None