#!/usr/bin/env python

"""
metapho: an image tagger and viewer.

This is the runnable script, which calls up a GTK-based user interface
for viewing images and tagging them.
"""

# Copyright 2013-2017 by Akkana Peck: share and enjoy under the GPL v2 or later.

import metapho
from metapho import __version__    # See comment in main()
import metapho.gtkpho as gtkpho

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk as gtk

import sys, os

# import traceback


class MetaPhoWindow(object):
    """The main controller window for metapho.
       This holds any child widgets, like the image viewer and tags window,
       and manages key events and other user commands.
    """

    def __init__(self, file_list):
        for filename in file_list:
            metapho.g_image_list.append(metapho.Image(filename))

        self.imgno = -1

        # The size of the image viewing area:
        self.imgwidth = 640
        self.imgheight = 600

        self.isearch = False

        self.zoomview = None

        self.win = gtk.Window(gtk.WINDOW_TOPLEVEL)
        self.win.set_border_width(10)

        main_hbox = gtk.HBox(spacing=8)

        self.viewer = gtkpho.ImageViewer()
        self.viewer.set_size_request(self.imgwidth, self.imgheight)
        main_hbox.pack_start(self.viewer)

        self.tagger = gtkpho.TagViewer(self.win)
        main_hbox.pack_start(self.tagger, expand=True)

        self.win.add(main_hbox)

        self.win.connect("delete_event", self.quit)
        self.win.connect("destroy", self.quit)

        self.win.connect("map-event", self.mapped)

        self.win.connect("key-press-event", self.key_press_event)

        self.win.show_all();

        self.tagger.read_all_tags_for_images()

        self.tagger.display_tags()

    def quit(self, widget=None, data=None):
        # If focus is currently in a text entry, there may be changed
        # text that hasn't been updated yet in the tags list.
        # Call the tagger to warn about that.
        if type(self.win.get_focus()) is gtk.Entry:
            self.tagger.check_entry_tag(self.win.get_focus())

        self.tagger.write_tag_file()

        # Can't call main_quit here: RuntimeError: called outside of a mainloop
        # Apparently this is what you're supposed to do instead:
        self.win.connect('event-after', gtk.main_quit)

    def mapped(self, widget, event):
        if self.imgno < 0:
            self.first_image()

            # Now that the image and tagger are loaded,
            # make sure all the entries are the right color.
            # XXX Seems like this could be done when the button colors are set.
            # self.tagger.highlight_entries()

    def first_image(self):
        self.imgno = -1
        # raise(RuntimeException("first_image: where are we?"))
        self.next_image()

    def lastImage(self):
        self.imgno = len(metapho.g_image_list)
        self.prev_image()

    def next_image(self):
        """Advance to the next image, if possible.
           Tell the viewer to load and show the image.
        """
        loaded = False

        # Save the tags of the current image, so we can copy them
        # into the next image if it doesn't have any yet.
        self.tagger.sync()
        oldtags = None
        try:
            if self.imgno >= 0 and metapho.g_image_list[self.imgno].tags:
                oldtags = metapho.g_image_list[self.imgno].tags
        except:
            print("Couldn't load image #", self.imgno)
            pass

        while self.imgno < len(metapho.g_image_list)-1 and not loaded:
            self.imgno += 1
            img = metapho.g_image_list[self.imgno]
            if img.displayed:
                loaded = self.viewer.load_image(img)
                if loaded:
                    # self.viewer.scale_and_rotate()
                    self.viewer.show_image()
                    self.win.set_title("%s (%d of %d)" % (
                        os.path.basename(img.filename),
                        self.imgno + 1,
                        metapho.num_displayed_images()))
                else:
                    img.displayed = False
                    # Should arguably delete it from the list
                    # so we don't continue to save tags for a
                    # file we can't load. But what if it's just
                    # temporarily unreadable and the user can fix it?
                    #del(metapho.g_image_list[self.imgno])
                    # The loop is about to increment imgno, but we actually want
                    # it to stay the same since deleting the nonexistent image
                    # slid the next image into the current position;
                    # so decrement imgno now.
                    #self.imgno -= 1

        if loaded:
            # If we have an image, and it has no tags set yet,
            # clone the tags from the previous image:
            if oldtags and not metapho.g_image_list[self.imgno].tags:
                metapho.g_image_list[self.imgno].tags = oldtags[:]
                self.tagger.changed = True

            self.tagger.set_image(metapho.g_image_list[self.imgno])

        else :           # couldn't load anything in the list
            dialog = gtk.MessageDialog(self.win,
                                       gtk.DIALOG_DESTROY_WITH_PARENT,
                                       gtk.MESSAGE_QUESTION,
                                       gtk.BUTTONS_OK_CANCEL,
                                       "No more images: quit?")
            dialog.set_default_response(gtk.RESPONSE_OK)
            response = dialog.run()
            dialog.destroy()
            if response == gtk.RESPONSE_OK:
                self.quit()

    def prev_image(self):
        loaded = False
        while self.imgno >= 1 and not loaded:
            self.imgno -= 1
            img = metapho.g_image_list[self.imgno]
            if img.displayed:
                loaded = self.viewer.load_image(img)
                if loaded:
                    # self.viewer.scale_and_rotate()
                    self.viewer.show_image()
                else:
                    img.displayed = False
                    # See comment in next_image
                    #del(metapho.g_image_list[self.imgno])

        if loaded:
            self.tagger.set_image(metapho.g_image_list[self.imgno])
        else :          # couldn't load anything in the list
            print("Can't go before first image")

    def delete_confirm(self):
        """Ask the user whether to really delete an image.
           Return True for yes, False for no.
           Accept some keystrokes beyond the usual ones,
           e.g. d or ctrl-d confirms the delete.
        """
        dialog = gtk.MessageDialog(self.win,
                                   gtk.DIALOG_DESTROY_WITH_PARENT,
                                   gtk.MESSAGE_QUESTION,
                                   #gtk.BUTTONS_YES_NO,
                                   gtk.BUTTONS_CANCEL,
                                   "Delete %s ?" % \
                                     metapho.g_image_list[self.imgno])
        delete_btn = dialog.add_button("Delete", gtk.RESPONSE_YES)

        # Handle key events on the dialog,
        # to make it easier for the user to respond.
        # d (with or without ctrl) confirms the delete.
        # n or q cancels (in addition to the usual ESC).
        def delete_dialog_key_press(widget, event, dialog):
            if event.string in ('q', 'n'):
                dialog.emit("response", gtk.RESPONSE_NO)
                return True
            elif event.keyval == gtk.keysyms.d :  # d with or without ctrl
                dialog.emit("response", gtk.RESPONSE_YES)
                return True
            return False
        dialog.connect("key-press-event", delete_dialog_key_press, dialog)

        response = dialog.run()
        dialog.destroy()
        if response == gtk.RESPONSE_YES:
            return True
        return False

    def key_press_event(self, widget, event):
        """Handle a key press event anywhere in the window"""
        if self.isearch:
            return self.isearch_key_press(widget, event)

        entry_focused = type(self.win.get_focus()) is gtk.Entry

        # ctrl-space goes to the next image, even if we're typing
        # in an entry. Nothing should be focused afterward.
        # or out of the entries if we're already typing in one.
        # Ctrl-space also goes to the next image.
        if (event.keyval == gtk.keysyms.space and \
            event.state & gtk.gdk.CONTROL_MASK):
            self.next_image()
            return True

        # ESC shifts focus out of the current entry (if any)
        # and makes sure nothing is focused.
        if event.keyval == gtk.keysyms.Escape:
            self.tagger.focus_none()
            return True

        # Return shifts focus to the next tag entry (never out of the entries).
        if event.keyval == gtk.keysyms.Return:
            self.tagger.focus_next_entry()
            return True

        if event.keyval == gtk.keysyms.Return and entry_focused:
            # Return when in an entry goes to the next entry
            self.tagger.focus_next_entry()
            return True

        # For any other keys, if focus is in a text entry, just let
        # the user type, and don't try to navigate.
        if entry_focused:
            #print "Focus is in an entry"
            return False

        # Ctrl-d means delete the current image (after confirmation)
        if event.keyval == gtk.keysyms.d and \
                event.state & gtk.gdk.CONTROL_MASK:
            if self.delete_confirm():
                metapho.g_image_list[self.imgno].delete()
                self.imgno -= 1
                self.next_image()
            return True

        # Ctrl-U: clear tags, then leave focus in the first empty tag field.
        if event.keyval == gtk.keysyms.u and \
                event.state & gtk.gdk.CONTROL_MASK:
            self.tagger.clear_tags(metapho.g_image_list[self.imgno])
            # Turns out auto-focusing the next entry is annoying,
            # so don't do it:
            # self.tagger.focus_next_entry()
            return True

        # Ctrl-q quits.
        if event.keyval == gtk.keysyms.q and \
                event.state & gtk.gdk.CONTROL_MASK:
            self.quit()
            return True

        # Ctrl-z brings up a zoomable image viewer window
        if event.keyval == gtk.keysyms.z and \
                event.state & gtk.gdk.CONTROL_MASK:
            if not self.zoomview:
                self.zoomview = gtkpho.ImageViewerWindow(
                    metapho.g_image_list[self.imgno],
                    exit_on_q=False)
            else:
                self.zoomview.add_image(metapho.g_image_list[self.imgno])
            self.zoomview.run()
            return True

        if event.string == " ":
            self.next_image()
            return True
        if event.keyval == gtk.keysyms.BackSpace:
            self.prev_image()
            return True
        if event.keyval == gtk.keysyms.Home:
            self.first_image()
            return True
        if event.keyval == gtk.keysyms.End:
            self.lastImage()
            return True
        if event.keyval == gtk.keysyms.Right:
            self.viewer.rotate(270)
            return True
        if event.keyval == gtk.keysyms.Left:
            self.viewer.rotate(90)
            return True
        if event.keyval in [ gtk.keysyms.Up, gtk.keysyms.Down ]:
            self.viewer.rotate(180)
            return True

        # Alpha: it's a tag
        if event.string.isalpha():
            self.tagger.toggle_tag_by_letter(event.string,
                                    metapho.g_image_list[self.imgno])
            return True

        # Digits: go to a specific tag category
        # (ignore digits too large to have a category).
        if event.string.isdigit():
            try:
                self.tagger.show_category_by_number(int(event.string))
            except IndexError:
                pass
            return True

        # + or -: go to next or previous tag
        if event.string == '+':
            self.tagger.next_category(1)
            return True
        if event.string == '-':
            self.tagger.next_category(-1)
            return True

        if event.string == '/':
            self.search_string = ''
            self.tagger.title.set_text("search: ")
            self.isearch = True
            return True

        # A key we didn't understand
        # print("Read key:", event.string, "keyval", event.keyval)
        return False

    def update_image_title(self):
        self.tagger.title.set_text("%s (%d of %d)" % (
                    os.path.basename(
                        metapho.g_image_list[self.imgno].filename),
                    self.imgno + 1,
                    metapho.num_displayed_images()))

    def isearch_key_press(self, widget, event):
        """Handle key presses when we're in isearch mode,
           typing in a search pattern.
        """

        # Return shifts out of isearch mode
        # but also accepts (shifts focus to) the first match.
        if event.keyval == gtk.keysyms.Return:
            self.tagger.focus_first_match(self.search_string)
            self.isearch = False
            self.update_image_title()
            return True

        # ESC shifts out of isearch mode.
        if event.keyval == gtk.keysyms.Escape:
            self.isearch = False
            self.tagger.highlight_entries()
            return True

        if event.string:
            self.search_string += event.string
            self.tagger.title.set_text("search: " + self.search_string)
            self.tagger.highlight_entries(self.search_string)
            return True

        return False

    def main(self):
        gtk.main()


def main():
    def Usage():
        print("Usage: %s file [file file ...]" \
            % os.path.basename(sys.argv[0]))

    if len(sys.argv) <= 1:
        Usage()
        sys.exit(1)
    if sys.argv[1] == "-h" or sys.argv[1] == "--help":
        Usage()
        sys.exit(0)
    if sys.argv[1] == "-v" or sys.argv[1] == "--version":
        # referencing metapho.__version__ dies with:
        # UnboundLocalError: local variable 'metapho' referenced before assignment
        # but from metapho import __version, then printing __version_, works.
        # Go figure.
        print(__version__)
        sys.exit(0)

    metapho = MetaPhoWindow(sys.argv[1:])
    # metapho.first_image()
    try:
        metapho.main()
    except KeyboardInterrupt:
        # Deliberately don't call self.quit() -- we may be using Ctrl-C
        # as a way to quit without updating anything.
        print('\n')
        # This doesn't do anything useful:
        # traceback.print_stack()
        sys.exit(1)

if __name__ == '__main__':
    main()
