#! /usr/bin/env python3

dev = False
profiling = False

import tkinter as tk
from multiprocessing import Process
import sys, threading, queue, time

if not dev:
    module_name = 'pacgraph'
    module_path = '/usr/bin/pacgraph'
    if sys.version_info[0] >= 3 and sys.version_info[1] >= 12:
        import importlib.util
        import importlib.machinery
        loader = importlib.machinery.SourceFileLoader(module_name, module_path)
        spec = importlib.util.spec_from_loader(module_name, loader)
        if spec is None:
            raise ImportError(f'Failed to load pacgraph module at {module_path}')
        pacgraph = importlib.util.module_from_spec(spec)
        loader.exec_module(pacgraph)
    else:
        import imp
        imp.load_source(module_name, module_path)
        import pacgraph

colors = {'sel'  : '#000',
          'uns'  : '#888',
          'dep'  : '#008',
          'req'  : '#00F',
          'bg'   : '#FFF',
          'line' : '#DDD',
          'line2': '#777',
          'dot'  : '#F00',
          }

# UI thoughts:
# hold right plays detailed history
# right drag fast moves ahead/back in history

class Motion(object):
    def __init__(self):
        self.mouse = None    # prev canvas coord
        self.scale = 1.0
        self.typed = ''      # keyboard search buffer
        self.offset = (0,0)  # unzoomed coords
        self.history = []    # list of (package, [coords])
        self.size = canvas.winfo_width(), canvas.winfo_height()
    def button_up(self, event):
        self.mouse = None
    def drag(self, event):
        if self.mouse is None:
            self.mouse = (event.x, event.y)
            return
        mdx = event.x - self.mouse[0]
        mdy = event.y - self.mouse[1]
        self.mouse = (event.x, event.y)
        self.moveall(mdx, mdy)
    def moveall(self, dx, dy):
        canvas.move(tk.ALL, dx, dy)
        scaled = xy_add((dx,dy), (0,0), self.scale)
        self.offset = xy_add(scaled, self.offset, 1.0)
    def zoom(self, event, factor):
        # buggy?
        self.scale *= factor
        #self.offset = xy_add(self.offset, (0,0), factor)
        new_p = lambda p: str(max(1, int(p * 0.4 * self.scale)))
        cx,cy = origin()
        canvas.scale(tk.ALL, cx, cy, factor, factor)
        for n,v in list(cant.items()):
            canvas.itemconfig(v.tk, font=('Monospace', new_p(v.font_pt)))
    def zoom_in(self, event):
        if self.scale*1.1 > 2:
            return
        self.zoom(event, 1.1)
    def zoom_out(self, event):
        if self.scale*0.8 < 0.1:
            return
        self.zoom(event, 0.8)
    def resize(self, event):
        dx = event.width  - self.size[0]
        dy = event.height - self.size[1]
        self.moveall(dx//2, dy//2)
        self.size = event.width, event.height
    def search(self, event):
        # prototype, eventually zoom-to-fit typed matches
        #print event.char, event.keysym, event.keycode
        ks = event.keysym
        c = event.char
        matches = []
        if ks in ['space', 'BackSpace', 'Escape', 'Delete', 'Return']:
            self.typed = ''
        if c.isalpha():
            self.typed += c
            matches = [n for n in cant if self.typed in n]
        for name in list(cant):
            if name in matches:
                color_text(name, 'sel')
            else:
                color_text(name, 'uns')

class Container(object):
    pass

def origin():
    "center of the canvas"
    return canvas.winfo_width()//2, canvas.winfo_height()//2

def xy_add(p1, p2, scale=1.0):
    "add two and scale points"
    return (p1[0]+p2[0]) * scale, (p1[1]+p2[1]) * scale

def zoom_shift(p):
    "real coords -> canvas coords"
    # buggy?
    px,py = p
    ox,oy = motion.offset
    cx,cy = origin()
    z = motion.scale
    return z * (px - cx + ox) + cx, z * (py - cy + oy) + cy

def color_text(name, color):
    canvas.itemconfig(cant[name].tk, fill=colors[color])

def hilite(event, name, selected):
    loaded = set(cant)
    ci = canvas.itemconfig
    if not name:
        [color_text(l, 'uns') for l in cant]
        ci('dot', fill='', outline='')
        ci('line', fill=colors['line'])
        return
    if selected:
        [ci(i, fill=colors['dot'], outline=colors['dot']) for i in cant[name].dots_tk]
        [ci(i.tk, fill=colors['line2']) for i in cant[name].lines_tk]
        color_text(name, 'sel')
        for l in cant[name].links & loaded:
            color_text(l, 'dep')
        for l in cant[name].inverse & loaded:
            color_text(l, 'req')
    else:
        color_text(name, 'uns')
        for l in cant[name].all & loaded:
            color_text(l, 'uns')
        [ci(i, fill='', outline='') for i in cant[name].dots_tk]
        [ci(i.tk, fill=colors['line']) for i in cant[name].lines_tk]

def sync_place():
    "requires an existing place_iter"
    global cant
    frame_delay = 10  # milliseconds
    hilite(None, None, False)
    try:
        name, centers = next(place_iter)
    except StopIteration:
        # zoom is broken during the animation
        # so be lame and don't enable until now
        canvas.bind('<Button-4>', motion.zoom_in)
        canvas.bind('<Button-5>', motion.zoom_out)
        if profiling:
            sys.exit()
        return
    node = tree[name]
    center = centers[-1]
    motion.history.append((name, centers))
    lines = [(center, cant[l].center, l) for l in set(cant) & node.all]
    node.lines_tk = []
    node.dots_tk = []
    for line in lines:
        l = Container()
        n = line[2]
        l.p = line[0] + line[1]
        l.c = colors['line']
        lp2 = zoom_shift(line[0]) + zoom_shift(line[1])
        l.tk = canvas.create_line(lp2, tag='line', fill=l.c)
        canvas.tag_lower(l.tk)
        node.lines_tk.append(l)
        cant[n].lines_tk.append(l)
    for c in centers:
        tkc = zoom_shift(c)
        d = canvas.create_oval(tkc+tkc, tags='dot', fill=colors['dot'], outline=colors['dot'])
        node.dots_tk.append(d)
    p = node.font_pt
    node.center = center
    tkcenter = zoom_shift((center[0], center[1]+p//4))
    node.tk = canvas.create_text(
            tkcenter[0], tkcenter[1], 
            text=name, anchor=tk.S, fill=colors['sel'], 
            font=('Monospace', str(max(1, int(p*0.4*motion.scale)))))
    cant[name] = node
    hilite(None, name, True)
    cant[name].center = center
    n = name
    canvas.tag_bind(cant[n].tk, '<Enter>', lambda e, n=n: hilite(e, n, True))
    canvas.tag_bind(cant[n].tk, '<Leave>', lambda e, n=n: hilite(e, n, False))
    canvas.after(frame_delay, sync_place)

def main():
    global canvas, tree, motion, cant, place_iter
    loaders = {'arch': pacgraph.Arch, 'debian': pacgraph.Debian,
               'redhat': pacgraph.Redhat, 'gentoo': pacgraph.Gentoo,
               'frugalware': pacgraph.Frugal}
    distro = pacgraph.distro_detect2()
    if not distro:
        pacgraph.distro_detect()
    loader = loaders[distro]()
    if len(sys.argv) == 1:
        print('Loading local repo.')
        tree = loader.local_load()
    else:
        print('Loading repository.')
        tree = loader.repo_load()
    print('Preparing %i nodes.' % len(tree))
    tree = pacgraph.pt_sizes(tree, 10, 100)
    print('Hover, drag, scroll and type to control.')
    print('Red dots are the path taken to find empty space.')
    
    root = tk.Tk()
    canvas = tk.Canvas(root, bg=colors['bg'])
    canvas.pack(expand=1, fill=tk.BOTH)
    canvas.tk_focusFollowsMouse()
    motion = Motion()
    cant = {}
   
    canvas.bind('<B1-Motion>', motion.drag)
    canvas.bind('<ButtonRelease-1>', motion.button_up)
    canvas.bind('<B2-Motion>', motion.drag)
    canvas.bind('<ButtonRelease-2>', motion.button_up)
    canvas.bind('<B3-Motion>', motion.drag)
    canvas.bind('<ButtonRelease-3>', motion.button_up)
    #canvas.bind('<Button-4>', motion.zoom_in)
    #canvas.bind('<Button-5>', motion.zoom_out)
    canvas.bind('<Key>', motion.search)
    canvas.bind('<Configure>', motion.resize)
    
    place_iter = pacgraph.place(tree, detail=True)
    canvas.after(500, sync_place)
    root.mainloop()

if not profiling:
    main()
else:
    import cProfile
    cProfile.run('main()', sort=1)