"""
@created: 2018-08-19 18:00:00
@author: (c) 2018 Jorj X. McKie
Display a PyMuPDF Document using Tkinter
-------------------------------------------------------------------------------
Dependencies:
-------------
PyMuPDF, PySimpleGUI (requires Python 3), Tkinter, PIL
License:
--------
GNU GPL V3+
Description
------------
Get filename and start displaying page 1. Please note that all file types
of MuPDF are supported (including EPUB e-books and HTML files for example).
Pages can be directly jumped to, or buttons can be used for paging.

This version contains enhancements:
* Use of PIL improves response times by a factor 3 or more.
* Zooming is now flexible: only one button serves as a toggle. Arrow keys can
  be used for moving the window when zooming.

We also interpret keyboard events (PageDown / PageUp) and mouse wheel actions
to support paging as if a button was clicked. Similarly, we do not include
a 'Quit' button. Instead, the ESCAPE key can be used, or cancelling the window.
To improve paging performance, we are not directly creating pixmaps from
pages, but instead from the fitz.DisplayList of the page. A display list
will be stored in a list and looked up by page number. This way, zooming
pixmaps and page re-visits will re-use a once-created display list.

"""
import sys
import fitz
import sys
if sys.version_info[0] >= 3:
    import PySimpleGUI as sg
else:
    import PySimpleGUI27 as sg
import tkinter as tk
from PIL import Image, ImageTk
import time

if len(sys.argv) == 1:
    fname = sg.PopupGetFile('Document Browser', 'Document file to open', no_window=True,
                              file_types = (
                                            ("PDF Files",     "*.pdf"),
                                            ("XPS Files",     "*.*xps"),
                                            ("Epub Files",    "*.epub"),
                                            ("Fiction Books", "*.fb2"),
                                            ("Comic Books",   "*.cbz"),
                                            ("HTML",   "*.htm*")
                                            # add more document types here
                                           )
                             )
else:
    fname = sys.argv[1]

if not fname:
    sg.Popup("Cancelling:", "No filename supplied")
    raise SystemExit("Cancelled: no filename supplied")

doc = fitz.open(fname)
page_count = len(doc)

# used for response time statistics only
fitz_img_time = 0.0
tk_img_time   = 0.0
img_count     = 1

# allocate storage for page display lists
dlist_tab = [None] * page_count

title = "PyMuPDF display of '%s', pages: %i" % (fname, page_count)

def get_page(pno, zoom = False, max_size = None, first = False):
    """Return a PNG image for a document page number.
    """
    dlist = dlist_tab[pno]   # get display list of page number
    if not dlist:            # create if not yet there
        dlist_tab[pno] = doc[pno].getDisplayList()
        dlist = dlist_tab[pno]
    r = dlist.rect           # the page rectangle
    clip = r
    # ensure image fits screen:
    # exploit, but do not exceed width or height
    zoom_0 = 1
    if max_size:
        zoom_0 = min(1, max_size[0] / r.width, max_size[1] / r.height)
        if zoom_0 == 1:
            zoom_0 = min(max_size[0] / r.width, max_size[1] / r.height)
    mat_0 = fitz.Matrix(zoom_0, zoom_0)

    if not zoom:             # show total page
        pix = dlist.getPixmap(matrix = mat_0, alpha=False)
    else:
        mp = r.tl + (r.br - r.tl) * 0.5     # page rect center
        w2 = r.width / 2
        h2 = r.height / 2
        clip = r * 0.5
        tl = zoom[0]          # old top-left
        tl.x += zoom[1] * (w2 / 2)
        tl.x = max(0, tl.x)
        tl.x = min(w2, tl.x)
        tl.y += zoom[2] * (h2 / 2)
        tl.y = max(0, tl.y)
        tl.y = min(h2, tl.y)
        clip = fitz.Rect(tl, tl.x + w2, tl.y + h2)

        mat = mat_0 * fitz.Matrix(2, 2)      # zoom matrix
        pix = dlist.getPixmap(alpha=False, matrix=mat, clip=clip)

    if first:                     # first call: tkinter still inactive
        img = pix.getPNGData()    # so use fitz png output
    else:                         # else take tk photo image
        pilimg = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
        img = ImageTk.PhotoImage(pilimg)

    return img, clip.tl           # return image, clip position


root = tk.Tk()
max_width = root.winfo_screenwidth() - 20
max_height = root.winfo_screenheight() - 135
max_size = (max_width, max_height)
root.destroy()
del root

window = sg.Window(title, return_keyboard_events = True,
                   location = (0,0), use_default_focus = False, no_titlebar=False)

cur_page = 0
data, clip_pos = get_page(cur_page,
                          zoom = False,
                          max_size = max_size,
                          first = True)

image_elem = sg.Image(data = data)

goto = sg.InputText(str(cur_page + 1), size=(5, 1), do_not_clear=True,
                    key = "PageNumber")

layout = [
    [
        sg.ReadButton('Next'),
        sg.ReadButton('Prev'),
        sg.Text('Page:'),
        goto,
        sg.Text('(%i)' % page_count),
        sg.ReadButton('Zoom'),
        sg.Text('(toggle on/off, use arrows to navigate while zooming)'),
    ],
    [image_elem],
]

window.Layout(layout)

# now define the buttons / events we want to handle
enter_buttons = [chr(13), "Return:13"]
quit_buttons = ["Escape:27", chr(27)]
next_buttons = ["Next", "Next:34", "MouseWheel:Down"]
prev_buttons = ["Prev", "Prior:33", "MouseWheel:Up"]
Up    = "Up:38"
Left  = "Left:37"
Right = "Right:39"
Down  = "Down:40"
zoom_buttons = ["Zoom", Up, Down, Left, Right]

# all the buttons we will handle
my_keys = enter_buttons + next_buttons + prev_buttons + zoom_buttons

# old page store and zoom toggle
old_page = 0
old_zoom = False

while True:
    button, value = window.Read()
    if button is None and (value is None or value['PageNumber'] is None):
        break
    if button in quit_buttons:
        break

    zoom_pressed = False
    zoom = False

    if button in enter_buttons:
        try:
            cur_page = int(value['PageNumber']) - 1  # check if valid
            while cur_page < 0:
                cur_page += page_count
        except:
            cur_page = 0  # this guy's trying to fool me

    elif button in next_buttons:
        cur_page += 1
    elif button in prev_buttons:
        cur_page -= 1
    elif button == Up:
        zoom = (clip_pos, 0, -1)
    elif button == Down:
        zoom = (clip_pos, 0, 1)
    elif button == Left:
        zoom = (clip_pos, -1, 0)
    elif button == Right:
        zoom = (clip_pos, 1, 0)
    elif button == "Zoom":
        zoom_pressed = True
        zoom = (clip_pos, 0, 0)

    # sanitize page number
    if cur_page >= page_count:  # wrap around
        cur_page = 0
    while cur_page < 0:         # pages > 0 look nicer
        cur_page += page_count

    if zoom_pressed and old_zoom:
        zoom = zoom_pressed = old_zoom = False

    t0 = time.perf_counter()
    data, clip_pos = get_page(cur_page, zoom = zoom, max_size = max_size,
                              first = False)
    t1 = time.perf_counter()
    image_elem.Update(data = data)
    t2 = time.perf_counter()
    fitz_img_time += t1 - t0
    tk_img_time   += t2 - t1
    img_count     += 1
    old_page = cur_page
    old_zoom = zoom_pressed or zoom

    # update page number field
    if button in my_keys:
        goto.Update(str(cur_page + 1))


# print some response time statistics
if img_count > 0:
    print("response times for '%s'" % doc.name)
    print("%.4f" % (fitz_img_time/img_count), "sec fitz avg. image time")
    print("%.4f" % (tk_img_time/img_count), "sec tk avg. image time")
    print("%.4f" % ((fitz_img_time + tk_img_time)/img_count), "sec avg. total time")
    print(img_count, "images read")
    print(page_count, "pages")