#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright (C) 2005 Maciej Katafiasz
#
# Author: Maciej Katafiasz
#
# This file is part of Purrr programme.
#
# Purrr 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; either version 2, or (at your
# option) any later version.
#
# Purrr 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 Purrr; see the file COPYING. If not, write to the
# Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
# 02111-1307, USA.

import sys, os, re
import tempfile, stat
import optparse, string
import urllib

from renamer import FileRenamer
from matcher import FileMatcher
import constants


try:
    import pygtk
    pygtk.require('2.0')
    import gtk, gtk.glade, gnome, gnome.ui
    import gobject
except:
    print _("**ERROR**: Could not import PyGTK")
    print _("You need PyGTK 2.0 and Libglade to use Purrr")
    print _("Please install it before continuing")
    sys.exit(os.EX_CONFIG)

import gettext  
APP="purrr"  
DIR="/usr/local/lib/purrr/po"  
gettext.textdomain(APP)  
gettext.bindtextdomain(APP, DIR)
gtk.glade.textdomain(APP)  
gtk.glade.bindtextdomain(APP, DIR)  
_ = gettext.gettext

usage = _("%prog [options] files...")
parser = optparse.OptionParser(usage = usage, version = constants.version)
parser.set_defaults(as_extension = False, input_files = None, as_uris = False)
parser.add_option("-x", "--extension", action="store_true", dest="as_extension",
                  help=_("use reduced UI appropriate for filemanager extension"))
parser.add_option("-s", "--standalone", action="store_false", dest="as_extension",
                  help=_("use full UI appropriate for standalone application"))
parser.add_option("-u", "--uri-list", action="store_true", dest="as_uris",
                  help=_("treat parameters given on command line as URIs, rather than local filenames"))

options, input_files = parser.parse_args()

if not input_files:
    options.as_extension = False

gnome.program_init(constants.name, constants.version)


class Info:
    pass

class Callbacks:
    def __init__(self):
        self.file_selection_work_pending = False
        self.non_local_warning_message_visible = False
        self.non_existent_warning_message_visible = False
        self.existing_files_warning_message_visible = False
        self.directory_info_message_visible = False
        self.complete_can_close = False
        self.dirs = {}
        self.file_chooser = None
    
    def file_selection_changed_cb(self, sel):
        self.activity_occured()

    def show_non_local_warning(self):
        self.non_local_warning_message_visible = True
        app.get_widget("non_local_warning_message").show()

    def hide_non_local_warning(self):
        if self.non_local_warning_message_visible:
            self.non_local_warning_message_visible = False
            app.get_widget("non_local_warning_message").hide()

    def show_directory_info(self):
        self.directory_info_message_visible = True
        app.get_widget("directory_info_message").show()

    def hide_directory_info(self):
        if self.directory_info_message_visible:
            self.directory_info_message_visible = False
            app.get_widget("directory_info_message").hide()

    def show_non_existent_warning(self):
        self.non_existent_warning_message_visible = True
        app.get_widget("non_existent_warning_message").show()

    def hide_non_existent_warning(self):
        if self.non_existent_warning_message_visible:
            self.non_existent_warning_message_visible = False
            app.get_widget("non_existent_warning_message").hide()

    def show_existing_files_warning(self):
        self.existing_files_warning_message_visible = True
        app.get_widget("existing_files_warning_message").show()

    def hide_existing_files_warning(self):
        if self.existing_files_warning_message_visible:
            self.existing_files_warning_message_visible = False
            app.get_widget("existing_files_warning_message").hide()

    def show_clash_warning(self):
        self.clash_warning_visible = True
        app.get_widget("clash_warning_message").show()

    def hide_clash_warning(self):
        self.clash_warning_visible = False
        app.get_widget("clash_warning_message").hide()

    def show_failure_warning(self, name):
        self.failure_warning_visible = True
        l = app.get_widget("failure_message_label")
        # FIXME: do proper i18n!
        l.set_markup(constants.failure_message % name)
        app.get_widget("failure_warning_message").show()

    def hide_failure_warning(self):
        self.failure_warning_visible = False
        app.get_widget("failure_warning_message").hide()

    def activity_occured(self):
        self.hide_non_local_warning()
        self.hide_directory_info()
        self.hide_non_existent_warning()
        self.hide_existing_files_warning()
        self.hide_failure_warning()
        if self.complete_can_close:
            self.complete_can_close = False
            app.get_widget("quit_button").set_label(gtk.STOCK_CANCEL)

    def insert_uris_in_list(self, uris):
        # Parse URIs. If URIs isn't file:///, drop it on the floor
        for uri in uris:
            uri = uri.strip()
            if uri[0] == "#": continue
            if uri.find("file:///") != 0:
                self.show_non_local_warning()
                continue
            uri = urllib.url2pathname(uri[7:])
            insert_file_in_list(uri)

    def files_drop_cb(self, widget, context, x, y, selection, targetType, time):
        self.activity_occured()
        lst = selection.data.splitlines()
        self.insert_uris_in_list(lst)
        apply_template(app.get_widget("template_entry").get_text())

        # TreeModelFilter doesn't support drag and drop, silence it
        widget.stop_emission("drag-data-received")


def drag_drop_cb(widget, context, x, y, timestamp):
    # TreeModelFilter doesn't support drag and drop, silence it
    widget.stop_emission("drag-drop")

def quit_cb(*args):
    gtk.main_quit()

def apply_template(template):
    info = Info()
    renamer.init(template)
    model = filtered.get_model()
    filtered.refilter()
    results = {}
    app.get_widget("rename_button").set_sensitive(True)
    app.get_widget("rename_menu").set_sensitive(True)
    cb.hide_clash_warning()
    scroll = False
    for r in filtered:
        p = filtered.get_path(r.iter)
        p = filtered.convert_path_to_child_path(p)
        name = model[p][0]
        info.old_name = name
        idx = name.rfind(".")
        if idx == -1: idx = len(name)
        info.extension = name[idx + 1:]
        info.base_name = name[:idx]
        new_name = renamer.next_name(info)
        try:
            x = results[new_name]
            if x:
                # make first conflicting row be marked, too
                p = filtered.convert_path_to_child_path(x.path)
                files[p][4] = gtk.STOCK_DIALOG_ERROR
                if not scroll:
                    # It's made this way because we're resizing
                    # shortly after, breaking the scroll
                    gobject.idle_add(file_list.scroll_to_cell, x.path)
                    scroll = True
            p = filtered.convert_path_to_child_path(r.path)
            files[p][4] = gtk.STOCK_DIALOG_ERROR
            app.get_widget("rename_button").set_sensitive(False)
            app.get_widget("rename_menu").set_sensitive(False)
            cb.show_clash_warning()
        except KeyError:
            files[p][4] = None
            results[new_name] = r
        model[p][1] = new_name


def template_changed_cb(entry):
    apply_template(entry.get_text())
    cb.activity_occured()

def filter_changed_cb(renderer, path, model):
    row = model[path]
    val = row[0]
    name = row[3]
    matcher.enable_filter(name, not val)
    row[0] = not val
    apply_template(app.get_widget("template_entry").get_text())
    cb.activity_occured()

def files_filter(model, iter, *user_data):
    val = model[iter][0]
    return bool(val and (not matcher.match(val)))

def add_files_cb(button):
    if not cb.file_chooser:
        cb.file_chooser = gtk.FileChooserDialog(title = _("Add files to rename"),
                                                parent = main_window,
                                                buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                                                           gtk.STOCK_OPEN, gtk.RESPONSE_OK))
        cb.file_chooser.set_select_multiple(True)
        
    resp = cb.file_chooser.run()
    cb.file_chooser.hide()
    if resp == gtk.RESPONSE_OK:
        cb.insert_uris_in_list(cb.file_chooser.get_uris())
        apply_template(app.get_widget("template_entry").get_text())

def remove_files_cb(button):
    rows = []
    for r in active_rows():
        rows.append(gtk.TreeRowReference(
            filtered, filtered.convert_path_to_child_path(r.path)))
    for r in rows:
        i = files.get_iter(r.get_path())
        files.remove(i)
    apply_template(app.get_widget("template_entry").get_text())

def insert_file_in_list(f):
    try:
        st = os.stat(f)
        if stat.S_ISDIR(st.st_mode):
            cb.show_directory_info()
        name = os.path.basename(f)
        dirname = os.path.dirname(f) or os.getcwd()
        itr = files.insert_before(None, None, (name, None, dirname,
                                         True, None))
    except OSError:
        cb.show_non_existent_warning()

def active_rows():
    sel = file_list.get_selection().get_selected_rows()
    model = sel[0]
    if len(sel[1]) > 0:
        for path in sel[1]:
            yield model[path]

    elif model:
        for row in model:
            yield row
    else:
        # Empty file list, do nothing
        pass

def rename_cb(*args):
    excludes = {}
    results = {}
    dirs = {}
    current_name = ""
    for row in filtered:
        # File names in list will be freed, so they are OK to be target names
        excludes[row[0]] = None
    for row in filtered:
        try:
            excludes[row[1]]
            continue
        except KeyError:
            try:
                os.stat(row[2] + "/" + row[1])
                cb.show_existing_files_warning()
                return
            except OSError:
                continue
    for row in filtered:
        try:
            # Warning -- heavily relies on POSIX semantics
            if stat.S_ISDIR(os.stat(row[2] + "/" + row[0]).st_mode):
                # Oh well, we aren't going to get atomic directory rename
                # But we can't overwrite either so it's actually good
                os.rename(row[2] + "/" + row[0], row[2] + "/" + row[1])
                dirs[row] = None
                continue
                
            tmpf = tempfile.NamedTemporaryFile(prefix = constants.name, dir = row[2])
            p = filtered.convert_path_to_child_path(row.path)
            results[row[0]] = (tmpf, gtk.TreeRowReference(files, p))
            os.rename(row[2] + "/" + row[0], tmpf.name)
        except OSError, error:
            # Something went wrong, rollback
            for res in results:
                tmpf = results[res][0]
                path = results[res][1].get_path()
                os.rename(tmpf.name, files[path][2] + "/" + files[path][0])
                try:
                    tmpf.close()
                except OSError:
                    # Make it not complain about non-existent (temporary) files
                    pass
            cb.activity_occured()
            cb.show_failure_warning(row[0])
            return
    for res in results:
        try:
            tmpf = results[res][0]
            path = results[res][1].get_path()
            os.rename(tmpf.name, files[path][2] + "/" + files[path][1])
            try:
                tmpf.close()
            except OSError:
                # Make it not complain about non-existent (temporary) files
                pass
            path = filtered.convert_path_to_child_path(path)
            files[path][0] = files[path][1]
            results[res] = None
        except OSError, error:
            # Oh sh!t, we're probably screwed, but try to rollback
            # And I hate the code repetition here, but it's necessary
            for res in results:
                if not results[res]:
                    continue
                tmpf = results[res][0]
                path = results[res][1].get_path()
                os.rename(tmpf.name, files[path][2] + "/" + files[path][0])
                try:
                    tmpf.close()
                except OSError:
                    # Make it not complain about non-existent (temporary) files
                    pass
            cb.activity_occured()
            cb.show_failure_warning(row[0])
            return
    for d in dirs:
        path = filtered.convert_path_to_child_path(d.path)
        files[path][0] = files[path][1]

    cb.activity_occured()
    cb.complete_can_close = True
    app.get_widget("quit_button").set_label(gtk.STOCK_CLOSE)

    
def populate_file_list(files_list):
    if options.as_uris:
        cb.insert_uris_in_list(input_files)
    else:
        for i in files_list:
            insert_file_in_list(i)
    apply_template(app.get_widget("template_entry").get_text())

def about_cb(*args):
    about_box = gnome.ui.About(constants.name,
                               constants.version,
                               constants.copyright,
                               constants.comments,
                               constants.authors,
                               constants.documenters,
                               constants.translators,
                               gtk.gdk.pixbuf_new_from_file(constants.logo))
    about_box.show()
    


app = gtk.glade.XML(constants.glade_file)
app.signal_autoconnect(locals())

renamer = FileRenamer("")
matcher = FileMatcher()
cb = Callbacks()

files = gtk.TreeStore(gobject.TYPE_STRING, # original name
                      gobject.TYPE_STRING, # new name
                      gobject.TYPE_STRING, # directory
                      gobject.TYPE_BOOLEAN, # basically unused
                      gobject.TYPE_STRING)  # stock-id of warning icon
filtered = files.filter_new()
filtered.set_visible_func(files_filter)
filters = gtk.ListStore(gobject.TYPE_BOOLEAN, gobject.TYPE_STRING,
                        gobject.TYPE_STRING, gobject.TYPE_STRING)

file_list = app.get_widget("file_list")
file_list.set_model(filtered)
file_list.set_reorderable(True)
sel = file_list.get_selection()
sel.set_mode(gtk.SELECTION_MULTIPLE)

file_list.drag_dest_set(gtk.DEST_DEFAULT_ALL, [("text/uri-list", 0, 0)], gtk.gdk.ACTION_COPY)
file_list.connect("drag-data-received", cb.files_drop_cb)

renderer = gtk.CellRendererText()
column = gtk.TreeViewColumn(_("Old name"), renderer, text = 0, sensitive = 3)
column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
column.set_fixed_width(200)
column.set_resizable(True)
file_list.append_column(column)

renderer = gtk.CellRendererPixbuf()
renderer.set_property("stock_id", gtk.STOCK_GO_FORWARD)
column = gtk.TreeViewColumn("  ", renderer)
column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
file_list.append_column(column)

renderer = gtk.CellRendererText()
column = gtk.TreeViewColumn(_("New name"), renderer, text = 1)
renderer = gtk.CellRendererPixbuf()
column.pack_end(renderer)
column.add_attribute(renderer, "stock-id", 4)
renderer.set_property("xalign", 1.0)
renderer.set_property("xpad", 0)
column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
column.set_resizable(True)
file_list.append_column(column)

filter_list = app.get_widget("filter_list")
filter_list.set_model(filters)

renderer = gtk.CellRendererToggle()
renderer.connect("toggled", filter_changed_cb, filters)
column = gtk.TreeViewColumn(_("Enabled"), renderer, active = 0)
column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
column.set_resizable(True)
filter_list.append_column(column)

renderer = gtk.CellRendererText()
column = gtk.TreeViewColumn(_("Name"), renderer, text = 1)
column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
filter_list.append_column(column)

renderer = gtk.CellRendererText()
column = gtk.TreeViewColumn(_("Regex"), renderer, text = 2)
column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
column.set_resizable(True)
filter_list.append_column(column)

for f in matcher.filters:
    filters.insert_before(None, (f["enabled"],
                                 f["description"],
                                 f["filter"],
                                 f["name"]))

populate_file_list(input_files)
    
sel = file_list.get_selection()
sel.connect("changed", cb.file_selection_changed_cb)

main_window = app.get_widget("main_window")

main_window.show()

# Workaround Glade bug
fp = app.get_widget("files_paned")
fp.set_position(-1)

if options.as_extension:
    arb = app.get_widget("add_remove_buttons")
    mb = app.get_widget("menu_bar")
    arb.hide()
    mb.hide()

gtk.main()
