#!/usr/bin/env pythonw

import sys
import os
import pygame
import pygame.constants
import copy

# can't make this too low or it doesn't get done right... at 5, it seems to trigger too soon
display_paint_latency = 20 # ms

# old helper dict for key bindings
# PK = dict([(k.replace("K_",""), getattr(pygame.constants,k)) for k in dir(pygame.constants) if k.startswith("K_")])

class pygame_key_helper:
    def __init__(self, lead):
        for k in dir(pygame.constants):
            if k.startswith(lead):
                self.__dict__[k.replace(lead,"")] = getattr(pygame.constants,k)

PK = pygame_key_helper("K_")
PKMOD = pygame_key_helper("KMOD_")

import pygame.constants as PC

def NS_replace_icon():
    import AppKit
    # setting the app name/title as well would be nice, but doesn't seem possible from here
    ipath = __file__.replace(".py","") + ".icns"
    img = AppKit.NSImage.alloc().initWithContentsOfFile_(ipath)
    if img: pygame.macosx.app.setApplicationIconImage_(img)

import captfile

# viewimage.changepath
def changepath(imagepath, offset, names=[]):
    # ~/PIX/SL300RT/downcase/100cxbox/kicx0257.jpg.toenail
    d = os.path.dirname(imagepath)      # use epath, later
    if not d: d = "."
    b = os.path.basename(imagepath)
    # even with osx' caching of directories, it is much faster to cache this too:
    if not names:
        names[:] = [f for f in os.listdir(d) if f.lower().endswith(".jpg")]
        names.sort()
    # is this lot below avoidable if we simply do names.append(names[0]) after sort?
    try:
        return os.path.join(d,names[names.index(b)+offset])
    except IndexError:
        if offset > 0:
            return os.path.join(d,names[0])
        else:
            return os.path.join(d,names[-1])

def next_uncaptioned_path(imagepath, names=[]):
    # should steal cache from changepath!
    d = os.path.dirname(imagepath)      # use epath, later
    if not d: d = "."
    b = os.path.basename(imagepath)
    if not names:
        names[:] = [f for f in os.listdir(d) if f.lower().endswith(".jpg")]
        names.sort()

    fwnames = names[names.index(b):]
    for n in fwnames:
        nn = os.path.join(d, n)
        if not os.path.exists(captfile.capt_path_of_image(nn)):
            return nn
    # signal somehow
    return imagepath
    


def old_make_pygame_image(ipath):
    image = pygame.image.load(ipath)
    return image

import PIL
import PIL.Image

def make_pygame_image(ipath):
    if os.path.exists(ipath + ".toenail"):
        return old_make_pygame_image(ipath + ".toenail")
    srcim = PIL.Image.open(ipath)
    srcx, srcy = srcim.size
    # parameterize on window size
    rat = srcx/579.0
    if srcy/rat > 439:
        rat = srcy/439.0
    srcim.thumbnail((int(srcx/rat), int(srcy/rat)))
    image = pygame.image.fromstring(srcim.tostring("raw"), srcim.size, "RGB")
    # and stash a toenail here...
    srcim.save(ipath + ".toenail", "JPEG")
    return image


def flip_file(ipath, surf, direction=0):
    # if the label hasn't triggered yet, defer it
    pygame.time.set_timer(PC.USEREVENT, 0)

    if direction:
        ipath = changepath(ipath, direction)
    image = make_pygame_image(ipath)
    iconv = image.convert()
    surf.blit(iconv, (0,0))
    pygame.display.flip()
    # trigger for display paint
    pygame.time.set_timer(PC.USEREVENT, display_paint_latency)
    return ipath

class display_surface:
    def __init__(self, surf):
        self.display_surf = surf
        self.overlay_surf = None
        self.base_surf = None

    # operations that used to be direct get copied...
    def get_height(self):
        return self.display_surf.get_height()
    def get_width(self):
        return self.display_surf.get_width()
    # but blit is special:
    def cls(self):
        self.overlay_surf = self.display_surf.convert_alpha()
        self.overlay_surf.fill((0,0,0,0))
        # self.overlay_surf.set_alpha(128)
    def blit(self, rect, coord):
        self.overlay_surf.blit(rect, coord)
    def flip(self):
        self.display_surf.blit(self.base_surf, (0,0))
        self.display_surf.blit(self.overlay_surf, (0,0))
        pygame.display.flip()

    def base_picture(self, ipath):
        image = make_pygame_image(ipath)
        self.base_surf = image.convert()
        # alpha??
        self.display_surf.blit(self.base_surf, (0,0))
        pygame.display.flip()

    def flip_file(self, ipath, direction=0):
        # if the label hasn't triggered yet, defer it
        pygame.time.set_timer(PC.USEREVENT, 0)
        if direction:
            ipath = changepath(ipath, direction)
        self.base_picture(ipath)
        # trigger for display paint
        pygame.time.set_timer(PC.USEREVENT, display_paint_latency)
        return ipath




class caption_painter:
    def __init__(self, surf):
        pygame.font.init()
        self.surf = surf # this is the *display* surface
        self.setfont()
        self.setcolors()
        self.setcorner("ul")
    def setfont(self, name="Geneva", size=20):
        # print "setfont:", name, size
        self.fontname = name
        self.fontsize = size
        self.font = pygame.font.SysFont(self.fontname, self.fontsize)
    def font_bigger(self):
        self.setfont(size = int(self.fontsize + 2))
    def font_smaller(self):
        # > 1 should be enough, but pygame faults rendering, see footnote
        if self.fontsize > 6:
            self.setfont(size = int(self.fontsize - 1))
    def setcolors(self, fore="black", back="grey", highback="orange"):
        self.forename = fore
        self.backname = back
        self.highname = highback
        self.textcolor = pygame.color.Color(self.forename)
        self.backcolor = pygame.color.Color(self.backname)
        self.highcolor = pygame.color.Color(self.highname)
    def setcorner(self, name=None):
        if name:
            self.corner = name
        else:
            corners = ["ul", "ll", "lr", "ur"]
            corners.append(corners[0]) # make it a ring
            self.corner = corners[corners.index(self.corner)+1]
    def paint(self, ipath, savepath=[]):
        if not savepath or savepath[0] != ipath:
            self.title(ipath)
            self.load_caption(ipath)
            savepath[:] = [ipath]
        self.draw_caption(ipath)
    def title(self, ipath):
        # pygame.display.set_caption(os.path.basename(ipath).replace(".toenail", " (medium)"))
        pygame.display.set_caption(os.path.basename(ipath))
    def load_caption(self, ipath):
        self.saved_capts = ""
        if hasattr(self, "capts"):
            self.saved_capts = copy.copy(self.capts) # or just save the real fields?
        fields, vals = captfile.capt2dict(ipath)
        self.capts = ["%s: %s" % (f, vals[f]) for f in fields]
        self.origcapts = copy.copy(self.capts)
        self.ipath = ipath
        self.highidx = -1
        self.caret = 0
            
    def autofill(self):
        fields, vals = captfile.captcapts(self.capts)
        saved_fields, saved_vals = captfile.captcapts(self.saved_capts)
        for f in saved_fields:
            if (f not in fields) or not vals[f].strip():
                vals[f] = saved_vals[f]
                if f not in fields:
                    fields.append(f)
        self.capts = ["%s: %s" % (f, vals[f]) for f in fields]

    def revert_capts(self):
        self.capts = copy.copy(self.origcapts)
        self.highidx = -1
        self.caret = 0
    def modified_capts(self):
        if self.capts != self.origcapts:
            sys.stderr.write('\7')      # need a pygame beep too?
            return 1
        return None

    def save_caption(self, ipath):
        # capts is always in "display", but captfields takes file lines...
        # fix this interface on the captfile side?
        fields, vals = captfile.captcapts(self.capts)
        # maybe we should be passing the string, but...
        captfile.dict2capt(ipath, fields, vals)
        self.origcapts = copy.copy(self.capts)
        self.highidx = -1
        self.caret = 0


    def draw_caption(self, ipath):
        wmax = 0
        htot = 0
        for capt in self.capts:
            try:
                w,h = self.font.size(capt)
            except:
                print "Exception on", repr(capt)
                raise
            wmax = max(wmax, w + 1)     # leave room for cursor
            htot += h
        
        if self.corner == "ul":
            x,y = (0,0)
        elif self.corner == "ll":
            x,y = (0, self.surf.get_height() - htot)
        elif self.corner == "lr":
            x,y = (self.surf.get_width()-wmax, self.surf.get_height() - htot)
        elif self.corner == "ur":
            x,y = (self.surf.get_width()-wmax, 0)
        else:
            print >> sys.stderr, "Bad Corner!", self.corner
            return

        self.surf.cls()
        for c in range(len(self.capts)):
            # 1 for "antialiasing on"
            capt = self.capts[c]
            if c == self.highidx:
                cleft, cright = capt[:self.caret], capt[self.caret:]
                captionlineleft  = self.font.render(cleft, 1, self.textcolor, self.highcolor)
                captionlinecaret = self.font.render("", 1, self.highcolor, self.textcolor)
                captionlineright = self.font.render(cright, 1, self.textcolor, self.highcolor)
                #captionlineleft.set_alpha(192)
                #captionlinecaret.set_alpha(192)
                #captionlineright.set_alpha(192)
                self.surf.blit(captionlineleft, (x,y))
                self.surf.blit(captionlinecaret, (x + captionlineleft.get_width(),y))
                self.surf.blit(captionlineright, (x + captionlineleft.get_width() + captionlinecaret.get_width(),y))
                y += captionlineleft.get_height()
            else:
                captionline = self.font.render(capt, 1, self.textcolor, self.backcolor)
                #captionline.set_alpha(192)
                self.surf.blit(captionline, (x,y))
                y += captionline.get_height()

        self.surf.flip()
        # pygame.display.flip()

    def changecapt(self, offset):
        if len(self.capts) == 0:
            return
        self.highidx += offset
        if self.highidx < 0:
            self.highidx = len(self.capts) - 1
        if self.highidx >= len(self.capts):
            self.highidx = 0
        if len(self.capts):
            try:
                self.caret = self.capts[self.highidx].index(": ") + 2
            except ValueError:
                self.caret = len(self.capts[self.highidx])

    def nextcapt(self):
        self.changecapt(+1)
    def prevcapt(self):
        self.changecapt(-1)
    def next_or_new_capt(self):
        if self.highidx == len(self.capts) - 1:
            self.capts.append("")
        self.changecapt(+1)

    def safe_set_caret(self, newcaret):
        if newcaret < 0: return
        if self.highidx < 0: return
        if self.highidx > len(self.capts) - 1: return
        if newcaret > len(self.capts[self.highidx]): return
        self.caret = newcaret

    def movecaret(self, offset):
        self.safe_set_caret(self.caret + offset)

    def rightcaret(self):
        self.movecaret(+1)
    def leftcaret(self):
        self.movecaret(-1)
    def end_of_line_caret(self):
        if self.highidx < 0: return
        if self.highidx > len(self.capts) - 1: return
        self.safe_set_caret(len(self.capts[self.highidx]))
    def start_of_line_caret(self):
        self.caret = 0

    def kill_to_end_of_line(self):
        if self.highidx < 0: return
        if self.highidx > len(self.capts) - 1: return
        if self.caret < 0: return
        if self.caret > len(self.capts[self.highidx]): return
        self.capts[self.highidx] = self.capts[self.highidx][:self.caret]

    def insertchar(self, uchar):
        if self.highidx < 0: 
            self.next_or_new_capt()
        capt = self.capts[self.highidx]
        cleft, cright = capt[:self.caret], capt[self.caret:]
        self.capts[self.highidx] = cleft + uchar + cright
        self.caret += 1

    def delchar(self):
        if self.highidx < 0: self.nextcapt()
        if self.caret == 0: return
        capt = self.capts[self.highidx]
        self.capts[self.highidx] = capt[:self.caret-1] + capt[self.caret:]
        self.caret -= 1

    def fwdelchar(self):
        if self.highidx < 0: self.nextcapt()
        if self.caret == 0: return
        capt = self.capts[self.highidx]
        if self.caret == len(capt): return
        self.capts[self.highidx] = capt[:self.caret] + capt[self.caret+1:]

    # --------------------------------------------------------------------------------
    def old_paint(self, ipath):
        # pygame.display.set_caption(os.path.basename(ipath).replace(".toenail", " (medium)"))
        pygame.display.set_caption(os.path.basename(ipath))

        fields, vals = captfile.capt2dict(ipath)
        capts = ["%s: %s" % (f, vals[f]) for f in fields]

        wmax = 0
        htot = 0
        for capt in capts:
            w,h = self.font.size(capt)
            wmax = max(wmax, w)
            htot += h
        
        if self.corner == "ul":
            x,y = (0,0)
        elif self.corner == "ll":
            x,y = (0, self.surf.get_height() - htot)
        elif self.corner == "lr":
            x,y = (self.surf.get_width()-wmax, self.surf.get_height() - htot)
        elif self.corner == "ur":
            x,y = (self.surf.get_width()-wmax, 0)
        else:
            print >> sys.stderr, "Bad Corner!", self.corner
            return

        for capt in capts:
            # 1 for "antialiasing on"
            captionline = self.font.render(capt, 1, self.textcolor, self.backcolor)
            captionline.set_alpha(192)
            self.surf.blit(captionline, (x,y))
            y += captionline.get_height()

        pygame.display.flip()
        
        
# obsoleted by the above, but saving it until I get it checked in

def paint_file_capt(ipath, surf, fontcache = []):
    # set up the constants
    textcolor = pygame.color.Color("white")
    backcolor = pygame.color.Color("grey")
    if not fontcache:
        pygame.font.init()
        fontcache.append(pygame.font.SysFont("Geneva", 18))
    font = fontcache[0]
    # draw the lines
    # capts = ["Filename: %s" % ipath, "Size: %s" % os.path.getsize(ipath)]
    fields, vals = captfile.capt2dict(ipath)
    capts = ["%s: %s" % (f, vals[f]) for f in fields]
    # print ipath, capts
    y = 0
    for capt in capts:
        # 1 for "antialiasing on"
        captionline = font.render(capt, 1, textcolor, backcolor)
        captionline.set_alpha(192)
        surf.blit(captionline, (0,y))
        y += captionline.get_height()

    pygame.display.flip()

def is_text_key(ev):
    # probably more advanced, but start here...
    return len(ev.unicode) > 0

#
# remaining features:
#  [done]m-a "fill"
#  more editing strokes as I need them
#  completion on field names
#  completion on people names
#  completion/history on location names?  or just smart-edit?
#  ability to insert chars with modifiers, like \"o
#    [done]cheat: special case them one-by-one
#
# [done]do overlay text
#  [done]TAB to start, and cycle rows (RET too?)
#  text edit in the value only - [done]c-a, [done]c-e, m-b, m-f, c-b, c-f, [done]del, m-del
#  [done] did c-k, just to clean up option-char hacking
#  [done]c-s/m-s to save
#  [done]block next/prev if changes outstanding? or just make it modeful?
#  
#  
# 
# do "find exif thumbnail", and display that? [they're only 160x120, too small]
# [cheated, used open]do "switch to fullscreen and load the real image" - [done]need pil, maybe?
#
# also someday: pre-fetch the directory listing? perhaps in a subthread or something?
#
# last-changed pointer - or "next-uncaptioned"?
#  c-d
#  maybe tab should be fields again, not move-view
#  line wrap!


def main(ipath):
    pygame.display.set_caption("viewImage")
    pygame.display.set_mode((579,434))  # from toenail size
    # or use pygame.image.fromstring on something from PIL
    dsurf = display_surface(pygame.display.get_surface())
    dsurf.base_picture(ipath)

    # really aggressive repeat for fast browsing
    pygame.key.set_repeat(500,1)
    # trigger for display paint
    pygame.time.set_timer(PC.USEREVENT, display_paint_latency)
    captions = caption_painter(dsurf)

    edit_commands = [
        [PKMOD.CTRL, PK.TAB,       captions.setcorner],
        [PKMOD.META, PK.MINUS,     captions.font_smaller],
        [PKMOD.META, PK.EQUALS,    captions.font_bigger],
        [0,          PK.UP,        captions.prevcapt],
        [0,          PK.DOWN,      captions.nextcapt],
        [0,          PK.TAB,       captions.nextcapt], # duplicate, but I keep hitting it
        [0,          PK.RETURN,    captions.next_or_new_capt],
        [0,          PK.LEFT,      captions.leftcaret],
        [PKMOD.CTRL, PK.b,         captions.leftcaret],
        [0,          PK.RIGHT,     captions.rightcaret],
        [PKMOD.CTRL, PK.f,         captions.rightcaret],
        [0,          PK.BACKSPACE, captions.delchar],
        [PKMOD.CTRL, PK.d,         captions.fwdelchar],
        [PKMOD.CTRL, PK.a,         captions.start_of_line_caret],
        [PKMOD.CTRL, PK.e,         captions.end_of_line_caret],
        [PKMOD.CTRL, PK.k,         captions.kill_to_end_of_line],
        [PKMOD.META, PK.r,         captions.revert_capts],
        [PKMOD.META, PK.a,         captions.autofill],
        # [PKMOD.META, PK.i,         captions.fill_default_titles],
        ]
    def edit_command(ev):
        for mod, key, verb in edit_commands:
            if ((not mod) or (mod and (mod & ev.mod))) and (key == ev.key):
                # dsurf.flip_file(ipath)
                verb()
                captions.paint(ipath)
                return 1
        return None

    while 1:
        for event in pygame.event.get():
            # sys.stderr.write(".")
            if event.type == PC.QUIT:
                print "Done."
                print >> open(os.path.expanduser("~/.pygimage.last"),"w"), ipath
                return
            elif event.type == PC.USEREVENT:
                # this event is a one-shot, so stop the re-trigger immediately
                pygame.time.set_timer(PC.USEREVENT, 0)
                captions.paint(ipath)
            elif event.type == PC.KEYDOWN:
                # print event, len(event.unicode)
                # print pygame.key.name(event.key)
                # <Event(2-KeyDown {'key': 304, 'unicode': u'', 'mod': 0})> <type 'Event'>
                # <Event(2-KeyDown {'key': 113, 'unicode': u'Q', 'mod': 1})> <type 'Event'>
                if event.key == PK.ESCAPE: # should be for modes, later
                    return
                meta = (event.mod & PKMOD.META) and not event.mod & ~PKMOD.META
                ctrl = (event.mod & PKMOD.CTRL) and not event.mod & ~PKMOD.CTRL
                ctrlmeta = ((event.mod & PKMOD.CTRL) 
                            and (event.mod & PKMOD.META)
                            and not (event.mod & ~(PKMOD.CTRL|PKMOD.META)))
                # if not meta and not ctrl and event.mod:
                #    print "extra ctl:", event.mod, PKMOD.__dict__.items()
                if meta and event.key == PK.q:
                    print "Done."
                    print >> open(os.path.expanduser("~/.pygimage.last"),"w"), ipath
                    return
                elif meta and event.key == PK.n:
                    if not captions.modified_capts():
                        ipath = dsurf.flip_file(ipath, +1)
                elif meta and event.key == PK.p:
                    if not captions.modified_capts():
                        ipath = dsurf.flip_file(ipath, -1)
                elif meta and event.key == PK.s:
                    dsurf.flip_file(ipath)
                    captions.save_caption(ipath)
                elif meta and event.key == PK.f:
                    # implement Full Screen view some day
                    # maybe just spawnv open?
                    os.spawnlp(os.P_NOWAIT, "open", "open", ipath)
                elif meta and event.key == PK.RIGHT:
                    ipath = next_uncaptioned_path(ipath)
                    dsurf.flip_file(ipath)
                # elif event.key == PK.TAB:
                #     captions.setcorner()
                #     flip_file(ipath, dsurf)
                elif ctrlmeta and event.key == PK.a:
                    dsurf.flip_file(ipath)
                    captions.autofill()
                    captions.save_caption(ipath)
                    ipath = dsurf.flip_file(ipath, +1)
                elif edit_command(event):
                    pass
                # elif event.key == PK.UP:
                #     flip_file(ipath, dsurf)
                #     captions.changecapt(-1)
                # elif event.key == PK.DOWN or event.key == PK.RETURN:
                #     flip_file(ipath, dsurf)
                #     captions.changecapt(+1)
                # elif event.key == PK.LEFT:
                #     flip_file(ipath, dsurf)
                #     captions.movecaret(-1)
                # elif event.key == PK.RIGHT:
                #     flip_file(ipath, dsurf)
                #     captions.movecaret(+1)
                # elif event.key == PK.BACKSPACE:
                #     flip_file(ipath, dsurf)
                #     captions.delchar()
                elif is_text_key(event):
                    # print "text:", event, pygame.key.name(event.key), len(event.unicode)
                    # dsurf.flip_file(ipath)
                    uchar = event.unicode
                    try:
                        uchar.encode("iso-8859-1")
                        if uchar == u"ø":
                            uchar = u"ö" # I need this more...
                        captions.insertchar(uchar)
                        captions.paint(ipath)
                    except UnicodeEncodeError:
                        print >> sys.stderr, "Can't represent!", repr(event.unicode)
                else:
                    # print event, pygame.key.name(event.key), len(event.unicode)
                    pass

# the SL300RT includes 160x120 JPEG snapshots, but that's too small to be useful
# maybe as a load-and-expand, then higher-res load of toenail later?


if __name__ == "__main__":
    pygame.init()
    try: 
        NS_replace_icon()
    except:
        pass
    try:
        path = sys.argv[1]
    except IndexError:
        path = open(os.path.expanduser("~/.pygimage.last"),"r").readline().strip()
    main(path)
    pygame.quit()


# fault from rendering tiny fonts, then growing them larger:
## *** malloc[9653]: Deallocation of a pointer not malloced: 0x1903a02; This could be a double free(), or free() called with the middle of an allocated block; Try setting environment variable MallocHelp to see tools to help debug
## *** malloc[9653]: Deallocation of a pointer not malloced: 0x4fb7600; This could be a double free(), or free() called with the middle of an allocated block; Try setting environment variable MallocHelp to see tools to help debug
## *** malloc[9653]: Deallocation of a pointer not malloced: 0xf7ffe330; This could be a double free(), or free() called with the middle of an allocated block; Try setting environment variable MallocHelp to see tools to help debug
## *** malloc[9653]: Deallocation of a pointer not malloced: 0x190da36; This could be a double free(), or free() called with the middle of an allocated block; Try setting environment variable MallocHelp to see tools to help debug
## Pygame Parachute Traceback:
##   File "./pygimage.py", line 183, in main
## Fatal Python error: (pygame parachute) Segmentation Fault
## Abort trap

# going up from 4, which didn't render:
## setfont: Geneva 4
## Pygame Parachute Traceback:
##   File "./pygimage.py", line 118, in paint
## Fatal Python error: (pygame parachute) Bus Error
## Abort trap