#! /usr/bin/python
# nautilus-gallery-uploader
#
# nautilus-gallery-uploader is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3.
#
# nautilus-gallery-uploader is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with nautilus-gallery-uploader; if not, write to the Free Software Foundation,
# Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
#
# Copyright (C) 2009 Pietro Battiston <toobaz@email.it>

# Name=Upload pictures to Gallery
# Name[it]=Carica immagini su Gallery

from __future__ import division

__version__ = '1.10'

import gtk, gobject

gobject.threads_init()

import os, sys
from urllib2 import URLError


########################## CONFIGURATION #######################################

not_installed_dir = os.path.dirname(os.path.realpath(__file__))
if os.path.exists(not_installed_dir + '/stuff'):
    STUFF_DIR = not_installed_dir + '/stuff'
    LOCALE_DIR = not_installed_dir + '/locale'
else:
    for directory in [sys.prefix, sys.prefix + '/local']:
        installed_root_dir = directory + '/share'
        if os.path.exists(installed_root_dir + '/gallery-uploader/stuff'):
            STUFF_DIR = installed_root_dir + '/gallery-uploader/stuff'
            LOCALE_DIR = installed_root_dir + '/locale'
            break

from ConfigParser import ConfigParser

config_path = os.path.expanduser('~/.gup/config.ini')

p = ConfigParser()
p.read([config_path])

########################## END OF CONFIGURATION ################################


########################## LOCALIZATION ########################################

import locale
import gettext

APP = 'gallery_uploader'

gettext.install(APP, localedir=LOCALE_DIR, unicode=True)

# For gtk.Builders:
locale.bindtextdomain(APP, LOCALE_DIR)

########################## END OF LOCALIZATION #################################


from guplib.albums import Album
from guplib.gallery import InvalidPasswordError
from guplib.guplib import check_album_cache, display_albums, save_gallery_info, save_album_cache, upload


fields = ['url', 'user', 'password']

class Site(object):
    def __init__(self, name=None):
        if name:
            self.name = self.saved_name = name
            for field in fields:
                setattr(self, field, p.get(self.name, field))
        else:
            for field in ['name', 'saved_name'] + fields:
                setattr(self, field, '')

    def fill_from_form(self):
        getter_builder = build_builder('form')
        getter = getter_builder.get_object('getter')
        error_label = getter_builder.get_object('error_label')

        inputs = {}
        
        buttons = {}

        def check_validity(*args):
            valid = True
            errors = []

            # Not just rolling on "inputs" because order (of errors) counts.
            for field_name in ['name'] + fields:
                cont = inputs[field_name].get_text()
                if not cont:
                    valid = False

                if field_name == 'name':
                    if cont in p.sections() and not cont == self.name:
                        valid = False
                        errors.append( _('Name \"%s\" already taken.') % cont)
                elif field_name == 'url':
                    url_start = 'http://'
                    if cont and not cont.startswith(url_start):
                        valid = False
                        errors.append( _('The URL should start with \"%s\".') % url_start)

            for button in buttons:
                buttons[button].set_sensitive(valid)
            
            if errors:
                error_label.set_text('\n'.join(errors))
            else:
                error_label.set_text('')
            
        for button_name in ['save', 'apply']:
            buttons[button_name] = getter_builder.get_object('but_' + button_name)

        for field_name in ['name'] + fields:
            inputs[field_name] = getter_builder.get_object('inp_' +  field_name)
            if self.name:
                # Gallery is not new: fill it with information.
                if field_name == 'name':
                    inputs[field_name].set_text(self.name)
                else:
                    inputs[field_name].set_text(p.get(self.name, field_name))

        for field_name in inputs:
            inputs[field_name].connect('changed', check_validity)
            
        response = getter.run()
        if response in [0, gtk.RESPONSE_DELETE_EVENT]:
            getter.destroy()
            return
        
        for field_name in inputs:
                setattr(self, field_name, inputs[field_name].get_text())
        if response == 1:
            self.save()

        getter.destroy()
        return self.name

    def save(self):
        save_gallery_info(p, self.name, self)
        
        if self.saved_name and self.saved_name != self.name:
            # The name was changed; delete old section
            p.remove_section(self.saved_name)
            p_file = file(os.path.expanduser('~/.gup/config.ini'), 'w')
            p.write(p_file)
            p_file.close()

    def build_treestore(self, rel=False):

        # The script itself already calls a "reload" in the beginning, so cached
        # data is fresh. "rel" can be set if this class is used from outside the
        # script.
        self.reload = rel
        
        albums, dont_mind = check_album_cache(self.name, self)
        
        store = gtk.TreeStore(str, int)

        # A stack of Albums
        levels_stack = []
        
        tree = albums.tree()
        while True:
            try:
                level, name, ID = tree.next()
            except StopIteration:
                break

            while len(levels_stack) > level:
                # Back to the left!
                levels_stack.pop()

            if levels_stack:
                parent = levels_stack[-1]
                parent_id = parent.album_id
                parent_iter = parent.iter
            else:
                parent, parent_id, parent_iter = (None, None, None)

            new = Album(ID, name, parent_id)
            
            new.iter = store.insert(parent_iter, 10000, [name, ID])
            levels_stack.append(new)
        
        return store

class Progress(object):
    """
    Executor of an action, which shows, while the action goes on, a progress
    bar, then leaves.
    The action is defined through overriding "act", which is repeatly called.
    The progress is described by setting members "fraction" and "text".
    The former is a float from 0 to 1. When it is None, the progressbar just
    floats back and forth.
    The latter is a string to show over the bar, or None to show nothing.
    """
    def __init__(self, what, title, message):
        dialog_builder = build_builder('subprocess')
        self.dialog = dialog_builder.get_object('dialog')
        self.dialog.set_title(title)
        self.label = dialog_builder.get_object('label')
        self.label.set_text(message)

        self.progress = dialog_builder.get_object('progress')
        
        self.what = what
        self.fraction = None
        self.text = None
        self.ran = False
        self.error = None
        
        self.timeout_id = gobject.timeout_add(100, self.update)
        resp = self.dialog.run()
        if resp == gtk.RESPONSE_DELETE_EVENT:
            self.dialog.destroy()
            gobject.source_remove(self.timeout_id)
            self.error = True

    def update(self):
        if not self.ran:
            self.act()
            self.ran = True

        alive = self.alive()

        if not alive:
            self.dialog.destroy()
            if self.error:
                dialog = error_dialog(*self.error)
            return
        
        if self.fraction != None:
            self.progress.set_fraction(self.fraction)
        else:
            self.progress.pulse()
        
        if self.text:
            self.progress.set_text(self.text)
        return True

class ThreadedProgress(Progress):
    """
    An executor working in a new thread.
    Only "run" needs to be defined additionally. Notice that in "run", "self" is
    the ThreadedProgress, _not_ (as usual) the Thread object.
    """

    def act(self):
        import threading
        self.thread = threading.Thread()
           
        # Here we pass "run" that is already bound to "self": this is why the
        # thread's "self" is the ThreadedProgress.
        self.thread.run = self.run
        self.thread.start()
        
    def alive(self):
        try:
            return self.thread.is_alive()
        except:
            # Python < 2.6
            return self.thread.isAlive()

class InfoReloader(ThreadedProgress):
    """
    Reload info for a gallery.
    Argument "what" is a 2-uple (gallery_name, options).
    """
    def run(self):
        name, options = self.what
        options.reload = True
        try:
            albums, don_t_mind = check_album_cache(name, options)
            save_album_cache(options.name, albums)
        except URLError, e:
            self.error = _('Error while retrieving updated information:'), str(e), False
            return
        except InvalidPasswordError:
            self.error = _('Invalid credentials'), _('Could not connect to %s with given username and password: either they are wrong, or the account is temporarily disabled.') % options.url, False
        
        self.finished = True
 
class Uploader(ThreadedProgress):
    """
    Upload pictures.
    Argument "what" is a 3-ple (album_id, options, list_of_files).
    """
    def run(self):
        album_id, options, files = self.what
        options.album = album_id
        options.delete = False
        total = len(files)
        count = 1
        for a_file in files:
            try:
                print "upload!"
                self.fraction = (count - 1) / total
                self.text = _("Uploading picture %(count)d of %(total)d") % locals()
                upload(options, [a_file])
                count += 1
            except URLError:
                self.error = _('Network error'), _("The URL %s doesn't exist\nor is currently not reachable.") % options.url, False
                return
 
        self.finished = True


########################## DIALOGS #############################################

def build_builder(name):
    builder = gtk.Builder()
    builder.set_translation_domain(APP)
    builder.add_from_file(STUFF_DIR + '/UI_' + name + '.glade')
    return builder


def chooser(title, text):
    dialog_builder = build_builder('choose')
    dialog = dialog_builder.get_object('dialog')
    dialog.set_title(title)
    label = dialog_builder.get_object('label')
    label.set_text(text)
    tree = dialog_builder.get_object('tree')

    
    def check_validity(selection, sensitives):
        for button in sensitives:
            button.set_sensitive(bool(selection.get_selected()[1]))
    
    sensitives = [dialog_builder.get_object(name) for name in ('ok', 'delete', 'edit')]
   
    selection = tree.get_selection()

    selection.connect('changed', check_validity, sensitives)
    
    interesting = ('new', 'delete', 'edit', 'expand', 'collapse', 'accounts')
    
    return dialog, tree, dict(zip(interesting, [dialog_builder.get_object(name) for name in interesting]))

def areyousurer(text, parent=None):
    dialog_builder = build_builder('delete')
    dialog = dialog_builder.get_object('dialog')
    dialog.set_transient_for(parent)
    dialog.set_markup(text)
    return dialog

def error_dialog(message, text, debug=True):
    dialog_builder = build_builder('error')
    dialog = dialog_builder.get_object('dialog')
    dialog.set_markup(message)
    if debug:
        debug_buffer = dialog_builder.get_object('debug_buffer')
        debug_buffer.set_text(text)
    else:
        debug_area = dialog_builder.get_object('debug_area')
        debug_area.hide()
        text_label = dialog_builder.get_object('text_label')
        text_label.set_text(text)

    # FIXME: temporary? See http://bugzilla.gnome.org/show_bug.cgi?id=587901
    dialog.set_skip_taskbar_hint(False)

    dialog.run()
    dialog.destroy()

########################## END OF DIALOGS ######################################

def usage():
    print "\n", _("Usage: %s [IMAGE1, [IMAGE2 ...]]" %sys.argv[0])

def upload_to_site(site, files):
    """
    Take a configuration of a gallery installation and upload 
    """
    
    if not files:
        error_dialog(_('No files provided'), _('There were no files selected for upload.'))
        usage()
        sys.exit(1)

    reloader = InfoReloader((site.name, site), _('Refreshing'), _('Please wait while information about remote albums hierarchy is downloaded.'))

    if reloader.error:
        galleries_catalog(files)
        return
    
    dialog, tree, buttons = chooser( _('Album selection'), _('Select the album in which to upload the pictures:'))
    
    model = site.build_treestore()
    tree.set_model(model)
    col = gtk.TreeViewColumn()
    cell = gtk.CellRendererText()
    col.pack_start(cell)
    col.add_attribute(cell, 'text', 0)
    tree.append_column(col)
    tree.expand_row((0), False)
    
    buttons['expand'].connect('clicked', (lambda args : tree.expand_all()))            
    buttons['collapse'].connect('clicked', (lambda args : tree.collapse_all()))            

    # Temporary? (as long as "new" and "delete" albums actions aren't available)
    for button in ['new', 'delete', 'edit']:
        buttons[button].hide()

    response = dialog.run()

    if response == gtk.RESPONSE_DELETE_EVENT:
        dialog.destroy()
        sys.exit(0)
    elif response == -2:
        dialog.destroy()
        galleries_catalog(files)
        return

    selection_iter = tree.get_selection().get_selected()[1]
    selected_id = model.get_value(selection_iter, 1)
    dialog.destroy()
    
    uploader = Uploader((selected_id, site, files), _('Uploading'), _('Uploading pictures. Please be patient.'))
    
    if uploader.error:
        galleries_catalog(files)
    else:
        dialog_builder = build_builder('bye')
        dialog = dialog_builder.get_object('dialog')
        # FIXME: temporary? See http://bugzilla.gnome.org/show_bug.cgi?id=587901
        dialog.set_skip_taskbar_hint(False)
        dialog.run()
        dialog.destroy()

def galleries_catalog(files):
    dialog, tree, buttons = chooser(_('Gallery selection'), _('Select the gallery you want to upload pictures to:'))

    model = gtk.ListStore(str)
    for gallery in p.sections():
        model.insert(10000, [gallery])
    tree.set_model(model)

    col = gtk.TreeViewColumn()
    cell = gtk.CellRendererText()
    col.pack_start(cell)
    col.add_attribute(cell, 'text', 0)
    tree.append_column(col)
            
    def delete(*args):
        # Get selection
        selection_iter = tree.get_selection().get_selected()[1]
        site_name = model.get_value(selection_iter, 0)

        dialog_surer = areyousurer(_("Are you sure you want to delete configuration for gallery installation \"%s\"?") % site_name, dialog)
        he_s_sure = dialog_surer.run()
        dialog_surer.destroy()

        if he_s_sure == 0:
            p.remove_section(site_name)
            p_file = file(os.path.expanduser('~/.gup/config.ini'), 'w')
            p.write(p_file)
            p_file.close()
            model.remove(selection_iter)

    buttons['delete'].connect('clicked', delete)

    for button in ['expand', 'collapse', 'accounts']:
        buttons[button].hide()

    response = dialog.run()

    if response == gtk.RESPONSE_DELETE_EVENT:
        dialog.destroy()
        sys.exit(0)
    if response in [6, 0]:
        # Get selection
        selection_iter = tree.get_selection().get_selected()[1]
        site_name = model.get_value(selection_iter, 0)
    else:
        site_name = None
        
    dialog.destroy()

    site = Site(site_name)
    
    if response in [5, 6]:
        if not site.fill_from_form():
            galleries_catalog(files)
            return

    upload_to_site(site, files)

def main():

    gtk.window_set_default_icon_from_file(STUFF_DIR + '/gallery.svg')

    if sys.argv[1:]:
        files = sys.argv[1:]
    else:
        import galleryuploader_browser
        galleryuploader_browser.build_builder = build_builder
        b = galleryuploader_browser.Browser()
        files = b.run()
        b.destroy()
        if not files:
            print "no files"
            sys.exit(0)

    for a_file in files:
        if not os.path.exists(a_file):
            print _("Error: file \"%s\" doesn't exist." %a_file)
            usage()
            sys.exit(1)
        if not os.path.isfile(a_file):
            print _("Error: \"%s\" is not a regular file." %a_file)
            usage()
            sys.exit(1)
        if not os.access(a_file, os.R_OK):
            print _("Error: can't read file \"%s\"." %a_file)
            usage()
            sys.exit(1)
        
    sec_num = len(p.sections())
    
    if not sec_num:
        site = Site()
        if not site.fill_from_form():
            sys.exit(0)
        upload_to_site(site, files)
    elif sec_num == 1:
        site = Site(p.sections()[0])
        upload_to_site(site, files)
    else:
        galleries_catalog(files)


if __name__ == '__main__':
    main()
