#
# This is an extension to the Nautilus file manager to allow better 
# integration with the Subversion source control system.
# 
# Copyright (C) 2006-2008 by Jason Field <jason@jasonfield.com>
# Copyright (C) 2007-2008 by Bruce van der Kooij <brucevdkooij@gmail.com>
# Copyright (C) 2008-2010 by Adam Plumb <adamplumb@gmail.com>
# 
# RabbitVCS 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 of the License, or
# (at your option) any later version.
# 
# RabbitVCS 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 RabbitVCS;  If not, see <http://www.gnu.org/licenses/>.
#

from __future__ import division
import threading
from datetime import datetime

import os.path
import pygtk
import gobject
import gtk
import cgi

from rabbitvcs.ui import InterfaceView
from rabbitvcs.ui.action import SVNAction, GitAction, vcs_action_factory
from rabbitvcs.ui.dialog import MessageBox
from rabbitvcs.util.contextmenu import GtkContextMenu
from rabbitvcs.util.contextmenuitems import *
import rabbitvcs.ui.widget
import rabbitvcs.util.helper
import rabbitvcs.vcs
from rabbitvcs.util.decorators import gtk_unsafe

from rabbitvcs import gettext
_ = gettext.gettext

DATETIME_FORMAT = rabbitvcs.util.helper.LOCAL_DATETIME_FORMAT

REVISION_LABEL = _("Revision")
DATE_LABEL = _("Date")
AUTHOR_LABEL = _("Author")

def revision_grapher(history):
    """
    Expects a list of revision items like so:
    [
        item.commit = "..."
        item.parents = ["...", "..."]
    ]
    
    Output can be put directly into the CellRendererGraph
    """
    items = []
    revisions = []
    last_lines = []
    color = "#d3b9d3"
    for item in history:
        commit = unicode(item.revision)
        parents = []
        for parent in item.parents:
            parents.append(unicode(parent))

        if commit not in revisions:
            revisions.append(commit)

        index = revisions.index(commit)
        next_revisions = revisions[:]
        
        parents_to_add = []
        for parent in parents:
            if parent not in next_revisions:
                parents_to_add.append(parent)
        
        next_revisions[index:index+1] = parents_to_add

        lines = []
        for i, revision in enumerate(revisions):
            if revision in next_revisions:
                lines.append((i, next_revisions.index(revision), color))
            elif revision == commit:
                for parent in parents:
                    lines.append((i, next_revisions.index(parent), color))

        node = (index, "#a9f9d2")

        items.append((item, node, last_lines, lines))
        revisions = next_revisions
        last_lines = lines

    return items

class Log(InterfaceView):
    """
    Provides an interface to the Log UI
    
    """

    selected_rows = []
    selected_row = []
    paths_selected_rows = []
    display_items = []

    limit = 100

    def __init__(self, path):
        """
        @type   path: string
        @param  path: A path for which to get log items
        
        """
        
        InterfaceView.__init__(self, "log", "Log")

        self.get_widget("Log").set_title(_("Log - %s") % path)
        self.vcs = rabbitvcs.vcs.VCS()

        self.filter_text = None
        self.path = path
        self.cache = LogCache()

        self.rev_first = None
        self.rev_start = None
        self.rev_end = None
        self.rev_max = 1
        self.previous_starts = []
        self.initialize_revision_labels()
        self.revision_number_column = 0
        self.head_row = 0
        self.get_widget("limit").set_text(str(self.limit))

        self.message = rabbitvcs.ui.widget.TextView(
            self.get_widget("message")
        )

        self.stop_on_copy = False
        self.revision_clipboard = gtk.Clipboard()

    #
    # UI Signal Callback Methods
    #

    def on_destroy(self, widget, data=None):
        self.destroy()

    def on_close_clicked(self, widget, data=None):
        if self.is_loading:
            self.action.set_cancel(True)
            self.action.stop()
            self.set_loading(False)

        self.close()
    
    def on_key_pressed(self, widget, data):
        InterfaceView.on_key_pressed(self, widget, data)
        if (data.state & gtk.gdk.CONTROL_MASK and
            gtk.gdk.keyval_name(data.keyval).lower() == "c"):
            if len(self.revisions_table.get_selected_rows()) > 0:
                self.copy_revision_text()

    def on_stop_on_copy_toggled(self, widget):
        self.stop_on_copy = self.get_widget("stop_on_copy").get_active()
        if not self.is_loading:
            self.refresh()
    
    def on_refresh_clicked(self, widget):
        self.limit = int(self.get_widget("limit").get_text())
        self.cache.empty()
        self.load()

    
    def on_search(self, widget):
        tb = self.get_widget("search_buffer")
        self.filter_text = tb.get_text(tb.get_start_iter(), tb.get_end_iter()).lower()

        self.refresh()

    #
    # Revisions table callbacks
    #

    # In this UI, we have an ability to filter and display only certain items.
    def get_displayed_row_items(self, col):
        items = []
        for row in self.selected_rows:
            items.append(self.display_items[row][col])
        
        return items

    def on_revisions_table_row_activated(self, treeview, event, col):
        paths = self.revisions_table.get_displayed_row_items(1)

        rabbitvcs.util.helper.launch_diff_tool(*paths)

    def on_revisions_table_mouse_event(self, treeview, data=None):
        if len(self.revisions_table.get_selected_rows()) == 0:
            self.message.set_text("")
            self.paths_table.clear()
            return

        if data is not None and data.button == 3:
            self.show_revisions_table_popup_menu(treeview, data)

        self.paths_table.clear()
        self.message.set_text("")

        self.update_revision_message()             


    #
    # Paths table callbacks
    #

    def on_paths_table_row_activated(self, treeview, data=None, col=None):
        try:
            revision1 = unicode(self.display_items[self.revisions_table.get_selected_rows()[0]].revision)
            revision2 = unicode(self.display_items[self.revisions_table.get_selected_rows()[0]+1].revision)
            path_item = self.paths_table.get_row(self.paths_table.get_selected_rows()[0])[1]
            url = self.root_url + path_item
            self.view_diff_for_path(url, unicode(revision1), unicode(revision2), sidebyside=True)
        except IndexError:
            pass

    def on_paths_table_mouse_event(self, treeview, data=None):
        if data is not None and data.button == 3:
            self.show_paths_table_popup_menu(treeview, data)
    
    def show_paths_table_popup_menu(self, treeview, data):
        revisions = []
        for row in self.revisions_table.get_selected_rows():
            line = {
                "revision": self.display_items[row].revision,
                "author": self.display_items[row].author,
                "message": self.display_items[row].message
            }

            if self.display_items[row].parents:
                line["parents"] = self.display_items[row].parents

            try:
                line["next_revision"] = self.display_items[row+1].revision
            except IndexError,e:
                pass

            try:
                line["previous_revision"] = self.display_items[row-1].revision
            except IndexError,e:
                pass          
                      
            revisions.append(line)
        
        revisions.reverse()
        
        paths = []
        for row in self.paths_table.get_selected_rows():
            paths.append(self.paths_table.get_row(row)[1])
        
        LogBottomContextMenu(self, data, paths, revisions).show()
    
    #
    # Helper methods
    #

    def load_or_refresh(self):
        if self.cache.has(self.rev_start):
            self.refresh()
        else:
            self.load()
          
    def get_selected_revision_numbers(self):
        if len(self.revisions_table.get_selected_rows()) == 0:
            return ""

        revisions = []
        for row in self.revisions_table.get_selected_rows():
            revisions.append(int(self.revisions_table.get_row(row)[self.revision_number_column]))

        revisions.sort()
        return rabbitvcs.util.helper.encode_revisions(revisions)

    def get_selected_revision_number(self):
        if len(self.revisions_table.get_selected_rows()):
            return self.revisions_table.get_row(self.revisions_table.get_selected_rows()[0])[self.revision_number_column]
        else:
            return ""
    
    def set_start_revision(self, rev):
        self.get_widget("start").set_text(str(rev))

    def set_end_revision(self, rev):
        self.get_widget("end").set_text(str(rev))

    def initialize_revision_labels(self):
        self.set_start_revision(_("N/A"))
        self.set_end_revision(_("N/A"))

    def set_loading(self, loading):
        self.is_loading = loading

    def show_revisions_table_popup_menu(self, treeview, data):
        revisions = []
        for row in self.revisions_table.get_selected_rows():
            line = {
                "revision": self.display_items[row].revision,
                "author": self.display_items[row].author,
                "message": self.display_items[row].message
            }
            try:
                line["next_revision"] = self.display_items[row+1].revision
            except IndexError,e:
                pass

            try:
                line["previous_revision"] = self.display_items[row-1].revision
            except IndexError,e:
                pass          
                      
            revisions.append(line)
            
        LogTopContextMenu(self, data, self.path, revisions).show()

    #
    # Other helper methods
    #

    def view_diff_for_path(self, url, revision1, revision2=None, sidebyside=False):
        if revision2 == None:
            revision2 = revision1

        options = [
            "%s@%s" % (url, revision2),
            "%s@%s" % (url, revision1),
            "--vcs=%s" % self.get_vcs_name()
        ]
        
        if sidebyside:
            options += ["-s"] 

        rabbitvcs.util.helper.launch_ui_window("diff", options)

    def get_vcs_name(self):
        vcs = rabbitvcs.vcs.VCS_DUMMY
        if hasattr(self, "svn"):
            vcs = rabbitvcs.vcs.VCS_SVN
        elif hasattr(self, "git"):
            vcs = rabbitvcs.vcs.VCS_GIT

        return vcs

class SVNLog(Log):
    def __init__(self, path, merge_candidate_revisions=None):
        Log.__init__(self, path)
                
        self.svn = self.vcs.svn()
        self.merge_candidate_revisions = merge_candidate_revisions

        self.revisions_table = rabbitvcs.ui.widget.Table(
            self.get_widget("revisions_table"),
            [gobject.TYPE_STRING, gobject.TYPE_STRING, 
                gobject.TYPE_STRING, gobject.TYPE_STRING,
                rabbitvcs.ui.widget.TYPE_HIDDEN],
            [_("Revision"), _("Author"), 
                _("Date"), _("Message"),
                _("Color")],
            callbacks={
                "mouse-event":   self.on_revisions_table_mouse_event
            }
        )

        for i in range(4):
            column = self.revisions_table.get_column(i)
            cell = column.get_cell_renderers()[0]
            column.add_attribute(cell, 'foreground', 4)

        self.paths_table = rabbitvcs.ui.widget.Table(
            self.get_widget("paths_table"),
            [gobject.TYPE_STRING, gobject.TYPE_STRING, 
                gobject.TYPE_STRING, gobject.TYPE_STRING], 
            [_("Action"), _("Path"), 
                _("Copy From Path"), _("Copy From Revision")],
            callbacks={
                "mouse-event":      self.on_paths_table_mouse_event,
                "row-activated":    self.on_paths_table_row_activated
            },
            flags={
                "sortable": True, 
                "sort_on": 1
            }
        )

        self.initialize_root_url()
        self.load_or_refresh()

    def initialize_root_url(self):
        action = SVNAction(
            self.svn,
            notification=False,
            run_in_thread=False
        )
        
        self.root_url = action.run_single(
            self.svn.get_repo_root_url,
            self.path
        )

    #
    # Log-loading callback methods
    #
    
    def refresh(self):
        """
        Refresh the items in the main log table that shows Revision/Author/etc.
        
        """
        
        self.revision_items = []
        self.revisions_table.clear()
        self.message.set_text("")
        self.paths_table.clear()
        
        if self.rev_start and self.cache.has(self.rev_start):
            self.revision_items = self.cache.get(self.rev_start)
        else:
            # Make sure the int passed is the order the log call was made
            self.revision_items = self.action.get_result(0)

        if not self.revision_items or len(self.revision_items) == 0:
            return
        
        # Get the starting/ending point from the actual returned revisions
        self.rev_start = unicode(self.revision_items[0].revision)
        self.rev_end = unicode(self.revision_items[-1].revision)

        if not self.rev_first:
            self.rev_first = self.rev_start
        
        self.cache.set(self.rev_start, self.revision_items)
        
        # The first time the log items return, the rev_start will be as large
        # as it will ever be.  So set this to our maximum revision.
        if self.rev_start > self.rev_max:
            self.rev_max = self.rev_start
        
        self.display_items = []

        for item in self.revision_items:
            msg = cgi.escape(item.message).lower()

            should_add = not self.filter_text
            should_add = should_add or msg.find(self.filter_text) > -1
            should_add = should_add or item.author.lower().find(self.filter_text) > -1
            should_add = should_add or str(item.revision).lower().find(self.filter_text) > -1
            should_add = should_add or str(item.date).lower().find(self.filter_text) > -1

            if should_add:
                self.display_items.append(item)

        self.set_start_revision(self.rev_start)
        self.set_end_revision(self.rev_end)

        self.check_previous_sensitive()
        self.check_next_sensitive()

        for item in self.display_items:
            msg = cgi.escape(rabbitvcs.util.helper.format_long_text(item.message, 80))
            rev = item.revision
            color = "#000000"
            if (self.merge_candidate_revisions != None and
                int(rev.short()) not in self.merge_candidate_revisions):
                color = "#c9c9c9"

            self.populate_table(rev, item.author, item.date, msg, color)

            # Stop on copy after adding the item to the table
            # so the user can look at the item that was copied
            if self.stop_on_copy:
                for path in item.changed_paths:
                    if path.copy_from_path or path.copy_from_revision:
                        self.set_loading(False)
                        return

        self.set_loading(False)

    def populate_table(self, revision, author, date, msg, color):
        self.revisions_table.append([
            unicode(revision),
            author,
            rabbitvcs.util.helper.format_datetime(date),
            msg,
            color
        ])


    def load(self):
        self.set_loading(True)

        self.action = SVNAction(
            self.svn,
            notification=False
        )        

        start = self.svn.revision("head")
        if self.rev_start:
            start = self.svn.revision("number", number=self.rev_start)

        self.action.append(
            self.svn.log, 
            self.path,
            revision_start=start,
            limit=self.limit,
            discover_changed_paths=True
        )
        self.action.append(self.refresh)
        self.action.start()

    def edit_revprop(self, prop_name, prop_value, callback=None):

        failure = False
        url = self.svn.get_repo_url(self.path)

        self.action = SVNAction(
            self.svn,
            notification=False
        )

        for row in self.revisions_table.get_selected_rows():
            item = self.display_items[row]
            self.action.append(
                self.svn.revpropset,
                prop_name,
                prop_value,
                url,
                item.revision
            )
            
            callback(row, prop_value)
        
        self.action.start()

    def on_log_message_edited(self, index, val):
        self.display_items[index].message = val
        self.revisions_table.set_row_item(index, 3, val)
        self.message.set_text(val)

    def on_author_edited(self, index, val):
        self.display_items[index].author = val
        self.revisions_table.set_row_item(index, 1, val)

    def copy_revision_text(self):
        text = "" 
        for selected_row in self.revisions_table.get_selected_rows():
            item = self.display_items[selected_row]
            
            text += "%s: %s\n" % (REVISION_LABEL, unicode(item.revision))
            text += "%s: %s\n" % (AUTHOR_LABEL, unicode(item.author))
            text += "%s: %s\n" % (DATE_LABEL, unicode(item.date))
            text += "%s\n\n"   % item.message
            if item.changed_paths is not None:
                for subitem in item.changed_paths:
                    text += "%s\t%s" % (subitem.action, subitem.path)

                    if subitem.copy_from_path or subitem.copy_from_revision:
                        text += " (Copied from %s %s)" % (subitem.copy_from_path, subitem.copy_from_revision)

                    text += "\n"

            text += "\n\n\n"

        self.revision_clipboard.set_text(text)

    def update_revision_message(self):
        combined_paths = []
        subitems = []
               
        for selected_row in self.revisions_table.get_selected_rows():
            item = self.display_items[selected_row]
            
            if len(self.revisions_table.get_selected_rows()) == 1:
                self.message.set_text(item.message)
            else:
                indented_message = item.message.replace("\n","\n\t")
                self.message.append_text(
					"%s %s:\n\t%s\n" % (REVISION_LABEL,
                                        unicode(item.revision),
                                        indented_message))
            if item.changed_paths is not None:
                for subitem in item.changed_paths:
                    if subitem.path not in combined_paths:
                        combined_paths.append(subitem.path)
                        
                        subitems.append([
                            subitem.action,
                            subitem.path,
                            subitem.copy_from_path,
                            subitem.copy_from_revision
                        ])

        subitems.sort(lambda x, y: cmp(x[1],y[1]))
        for subitem in subitems:
            self.paths_table.append([
                subitem[0],
                subitem[1],
                subitem[2],
                subitem[3]
            ])

    def on_previous_clicked(self, widget):
        self.rev_start = self.previous_starts.pop()
        self.load_or_refresh()
                    
    def on_next_clicked(self, widget):
        self.previous_starts.append(self.rev_start)
        self.rev_start = int(self.rev_end) - 1

        if self.rev_start < 1:
            self.rev_start = 1

        self.load_or_refresh()

    def check_previous_sensitive(self):
        sensitive = (self.rev_start < self.rev_max)
        self.get_widget("previous").set_sensitive(sensitive)

    def check_next_sensitive(self):
        sensitive = True
        if self.rev_end == 1:
            sensitive = False
        if len(self.revision_items) < self.limit:
            sensitive = False

        self.get_widget("next").set_sensitive(sensitive)

class GitLog(Log):
    def __init__(self, path):
        Log.__init__(self, path)
        
        self.git = self.vcs.git(path)
        self.limit = 500
        
        self.get_widget("stop_on_copy").hide()
        
        self.revision_number_column = 1

        self.revisions_table = rabbitvcs.ui.widget.Table(
            self.get_widget("revisions_table"),
            [rabbitvcs.ui.widget.TYPE_GRAPH, gobject.TYPE_STRING, 
                rabbitvcs.ui.widget.TYPE_MARKUP, 
                rabbitvcs.ui.widget.TYPE_MARKUP, rabbitvcs.ui.widget.TYPE_MARKUP], 
            [_("Graph"), _("Revision"), _("Author"), 
                _("Date"), _("Message")],
            filters=[{
                "callback": rabbitvcs.ui.widget.git_revision_filter,
                "user_data": {
                    "column": 1
                }
            }],
            callbacks={
                "mouse-event":   self.on_revisions_table_mouse_event
            }
        )

        self.paths_table = rabbitvcs.ui.widget.Table(
            self.get_widget("paths_table"),
            [gobject.TYPE_STRING, gobject.TYPE_STRING], 
            [_("Action"), _("Path")],
            callbacks={
                "mouse-event":      self.on_paths_table_mouse_event,
                "row-activated":    self.on_paths_table_row_activated
            },
            flags={
                "sortable": True, 
                "sort_on": 1
            }
        )
        self.start_point = 0
        self.initialize_root_url()
        self.load_or_refresh()

    #
    # Log-loading callback methods
    #
    
    def refresh(self):
        """
        Refresh the items in the main log table that shows Revision/Author/etc.
        
        """
        
        self.revisions_table.clear()
        self.message.set_text("")
        self.paths_table.clear()
        
        # Make sure the int passed is the order the log call was made
        self.revision_items = self.action.get_result(0)
        
        if not self.revision_items or len(self.revision_items) == 0:
            return
        
        self.set_start_revision(self.revision_items[0].revision.short())
        self.set_end_revision(self.revision_items[-1].revision.short())

        self.display_items = []

        for item in self.revision_items:
            msg = cgi.escape(item.message).lower()

            should_add = not self.filter_text
            should_add = should_add or msg.find(self.filter_text) > -1
            should_add = should_add or item.author.lower().find(self.filter_text) > -1
            should_add = should_add or unicode(item.revision).lower().find(self.filter_text) > -1
            should_add = should_add or str(item.date).lower().find(self.filter_text) > -1

            if should_add:
                self.display_items.append(item)

        grapher = revision_grapher(self.display_items)
        max_columns = 1
        for (item, node, in_lines, out_lines) in grapher:
            if max_columns < len(out_lines):
                max_columns = len(out_lines)

        if not self.filter_text:
            graph_column = self.revisions_table.get_column(0)
            cell = graph_column.get_cell_renderers()[0]
            self.revisions_table.set_column_width(0, 16*max_columns)

        index = 0
        for (item, node, in_lines, out_lines) in grapher:
            revision = unicode(item.revision)
            msg = cgi.escape(rabbitvcs.util.helper.format_long_text(item.message, 80))
            author = item.author
            date = rabbitvcs.util.helper.format_datetime(item.date)
            
            if item.head:
                self.head_row = index
                msg = "<b>%s</b>" % msg
                author = "<b>%s</b>" % author
                date = "<b>%s</b>" % date            
            
            graph_render = ()
            if not self.filter_text:
                graph_render = (node, in_lines, out_lines)

            self.revisions_table.append([
                graph_render,
                revision,
                author,
                date,
                msg
            ])

            index += 1

        self.check_previous_sensitive()
        self.check_next_sensitive()
        self.set_loading(False)
    
    def load(self):
        self.set_loading(True)

        self.action = GitAction(
            self.git,
            notification=False,
            run_in_thread=True
        )        

        self.action.append(
            self.git.log,
            path=self.path,
            skip=self.start_point,
            limit=self.limit+1
        )
        self.action.append(self.refresh)
        self.action.start()

    def copy_revision_text(self):
        text = ""
        for selected_row in self.revisions_table.get_selected_rows():
            item = self.display_items[selected_row]

            text += "%s: %s\n" % (REVISION_LABEL, unicode(item.revision.short()))
            text += "%s: %s\n" % (AUTHOR_LABEL, unicode(item.author))
            text += "%s: %s\n" % (DATE_LABEL, unicode(item.date))
            text += "%s\n\n" % item.message
            
        self.revision_clipboard.set_text(text)

    def update_revision_message(self):
        combined_paths = []
        subitems = []
        
        for selected_row in self.revisions_table.get_selected_rows():
            item = self.display_items[selected_row]

            if len(self.revisions_table.get_selected_rows()) == 1:
                self.message.set_text(item.message)
            else:
                indented_message = item.message.replace("\n","\n\t")
                self.message.append_text(
					"%s %s:\n\t%s\n" % (REVISION_LABEL,
                                        item.revision.short(),
                                        indented_message))

            for subitem in item.changed_paths:
                
                if subitem.path not in combined_paths:
                    combined_paths.append(subitem.path)
                    
                    subitems.append([
                        subitem.action,
                        subitem.path
                    ])

        subitems.sort(lambda x, y: cmp(x[1],y[1]))
        for subitem in subitems:
            self.paths_table.append([
                subitem[0],
                subitem[1]
            ])

    def on_previous_clicked(self, widget):
        self.start_point -= self.limit
        if self.start_point < 0:
            self.start_point = 0
        self.load_or_refresh()
                    
    def on_next_clicked(self, widget):
        self.start_point += self.limit
        self.load()

    def check_previous_sensitive(self):
        sensitive = (self.start_point > 0)
        self.get_widget("previous").set_sensitive(sensitive)

    def check_next_sensitive(self):
        sensitive = True
        if len(self.revision_items) < self.limit:
            sensitive = False

        self.get_widget("next").set_sensitive(sensitive)

    def initialize_root_url(self):
        self.root_url = self.git.get_repository() + "/"

class SVNLogDialog(SVNLog):
    def __init__(self, path, ok_callback=None, multiple=False, merge_candidate_revisions=None):
        """
        Override the normal SVNLog class so that we can hide the window as we need.
        Also, provide a callback for when the OK button is clicked so that we
        can get some desired data.
        """
        SVNLog.__init__(self, path, merge_candidate_revisions)
        self.ok_callback = ok_callback
        self.multiple = multiple
        
    def on_destroy(self, widget):
        pass
    
    def on_close_clicked(self, widget, data=None):
        self.hide()
        if self.ok_callback is not None:
            if self.multiple == True:
                self.ok_callback(self.get_selected_revision_numbers())
            else:
                self.ok_callback(self.get_selected_revision_number())

class GitLogDialog(GitLog):
    def __init__(self, path, ok_callback=None, multiple=False):
        """
        Override the normal GitLog class so that we can hide the window as we need.
        Also, provide a callback for when the OK button is clicked so that we
        can get some desired data.
        """
        GitLog.__init__(self, path)
        self.ok_callback = ok_callback
        self.multiple = multiple
        
    def on_destroy(self, widget):
        pass
    
    def on_close_clicked(self, widget, data=None):
        self.hide()
        if self.ok_callback is not None:
            if self.multiple == True:
                self.ok_callback(self.get_selected_revision_numbers())
            else:
                self.ok_callback(self.get_selected_revision_number())

class LogCache:
    def __init__(self, cache={}):
        self.cache = cache
    
    def set(self, key, val):
        self.cache[key] = val
    
    def get(self, key):
        return self.cache[key]
    
    def has(self, key):
        return (key in self.cache)
    
    def empty(self):
        self.cache = {}


class MenuViewDiffWorkingCopy(MenuItem):
    identifier = "RabbitVCS::View_Diff_Working_Copy"
    label = _("View diff against working copy")
    icon = "rabbitvcs-diff"

class MenuViewDiffPreviousRevision(MenuItem):
    identifier = "RabbitVCS::View_Diff_Previous_Revision"
    label = _("View diff against previous revision")
    icon = "rabbitvcs-diff"

class MenuViewDiffRevisions(MenuItem):
    identifier = "RabbitVCS::View_Diff_Revisions"
    label = _("View diff between revisions")
    icon = "rabbitvcs-diff"

class MenuCompareWorkingCopy(MenuItem):
    identifier = "RabbitVCS::Compare_Working_Copy"
    label = _("Compare with working copy")
    icon = "rabbitvcs-compare"

class MenuComparePreviousRevision(MenuItem):
    identifier = "RabbitVCS::Compare_Previous_Revision"
    label = _("Compare with previous revision")
    icon = "rabbitvcs-compare"

class MenuCompareRevisions(MenuItem):
    identifier = "RabbitVCS::Compare_Revisions"
    label = _("Compare revisions")
    icon = "rabbitvcs-compare"

class MenuShowChangesPreviousRevision(MenuItem):
    # This is for the revs list
    identifier = "RabbitVCS::Show_Changes_Previous_Revision"
    label = _("Show changes against previous revision")
    icon = "rabbitvcs-changes"
    
class MenuShowChangesRevisions(MenuItem):
    # This is for the revs list
    identifier = "RabbitVCS::Show_Changes_Revisions"
    label = _("Show changes between revisions")
    icon = "rabbitvcs-changes"

class MenuUpdateToThisRevision(MenuItem):
    identifier = "RabbitVCS::Update_To_This_Revision"
    label = _("Update to this revision")
    tooltip = _("Update the selected path to this revision")
    icon = "rabbitvcs-update"

class MenuRevertChangesFromThisRevision(MenuItem):
    identifier = "RabbitVCS::Revert_Changes_From_This_Revision"
    label = _("Revert changes from this revision")
    tooltip = _("Update the selected path by reverse merging the changes")
    icon = "rabbitvcs-revert"    

class MenuCopyClipboard(MenuItem):
    identifier = "RabbitVCS::Copy_Clipboard"
    label = _("Copy to clipboard")
    tooltip = _("Copy to clipboard the full data of these revisions")

class MenuEditAuthor(MenuItem):
    identifier = "RabbitVCS::Edit_Author"
    label = _("Edit author...")

class MenuEditAuthor(MenuItem):
    identifier = "RabbitVCS::Edit_Author"
    label = _("Edit author...")

class MenuEditLogMessage(MenuItem):
    identifier = "RabbitVCS::Edit_Log_Message"
    label = _("Edit log message...")

class MenuEditRevisionProperties(MenuItem):
    identifier = "RabbitVCS::Edit_Revision_Properties"
    label = _("Edit revision properties...")
    icon = gtk.STOCK_EDIT

class MenuSeparatorLast(MenuSeparator):
    identifier = "RabbitVCS::Separator_Last"

class LogTopContextMenuConditions:
    def __init__(self, caller, vcs, path, revisions):
        self.caller = caller
        self.vcs = vcs
        self.path = path
        self.revisions = revisions
        
        self.vcs_name = caller.get_vcs_name()
        
    def view_diff_working_copy(self, data=None):
        return (self.vcs.is_in_a_or_a_working_copy(self.path) and len(self.revisions) == 1)

    def copy_clipboard(self, data=None):
        return (len(self.revisions) > 0)

    def view_diff_previous_revision(self, data=None):
        item = self.revisions[0]["revision"]
        return ("previous_revision" in self.revisions[0] and len(self.revisions) == 1)

    def view_diff_revisions(self, data=None):
        return (len(self.revisions) > 1)
        
    def compare_working_copy(self, data=None):
        return (self.vcs.is_in_a_or_a_working_copy(self.path) and len(self.revisions) == 1) 

    def compare_previous_revision(self, data=None):
        item = self.revisions[0]["revision"]
        return ("previous_revision" in self.revisions[0] and len(self.revisions) == 1)

    def compare_revisions(self, data=None):
        return (len(self.revisions) > 1)

    def show_changes_previous_revision(self, data=None):
        item = self.revisions[0]["revision"]
        return ("previous_revision" in self.revisions[0] and len(self.revisions) == 1)

    def show_changes_revisions(self, data=None):
        return (len(self.revisions) > 1)

    def update_to_this_revision(self, data=None):
        return (self.vcs_name == rabbitvcs.vcs.VCS_SVN and len(self.revisions) == 1)

    # TODO Evaluate multiple revisions later
    # TODO Git?
    def revert_changes_from_this_revision(self, data=None):
        return (self.vcs_name == rabbitvcs.vcs.VCS_SVN and len(self.revisions) == 1)

    def checkout(self, data=None):
        return (len(self.revisions) == 1)

    def branches(self, data=None):
        return (len(self.revisions) == 1 and self.vcs_name == rabbitvcs.vcs.VCS_GIT)

    def tags(self, data=None):
        return (len(self.revisions) == 1 and self.vcs_name == rabbitvcs.vcs.VCS_GIT)

    def branch_tag(self, data=None):
        return (self.vcs_name == rabbitvcs.vcs.VCS_SVN and len(self.revisions) == 1)

    def export(self, data=None):
        return (len(self.revisions) == 1)

    def edit_author(self, data=None):
        return self.vcs_name == rabbitvcs.vcs.VCS_SVN

    def edit_log_message(self, data=None):
        return self.vcs_name == rabbitvcs.vcs.VCS_SVN

    def edit_revision_properties(self, data=None):
        return (self.vcs_name == rabbitvcs.vcs.VCS_SVN and len(self.revisions) == 1)

    def separator(self, data=None):
        return True

    def separator_last(self, data=None):
        return (self.vcs_name == rabbitvcs.vcs.VCS_SVN)
        
    def merge(self, data=None):
        return (self.vcs_name == rabbitvcs.vcs.VCS_GIT)

    def reset(self, data=None):
        return (self.vcs_name == rabbitvcs.vcs.VCS_GIT)

class LogTopContextMenuCallbacks:
    def __init__(self, caller, vcs, path, revisions):
        self.caller = caller
        self.vcs = vcs
        self.path = path
        self.revisions = revisions

        self.vcs_name = self.caller.get_vcs_name()

    def find_parent(self, revision):
        if ("parents" in revision) and len(revision["parents"]) > 0:
            parent = unicode(revision["parents"][0])
        elif ("next_revision" in revision):
            parent = unicode(revision["next_revision"])
        else:
            parent = unicode(int(unicode(revision["revision"])) - 1)

        return parent
        
    def view_diff_working_copy(self, widget, data=None):
        rabbitvcs.util.helper.launch_ui_window("diff", [
            "%s@%s" % (self.path, unicode(self.revisions[0]["revision"])),
            "--vcs=%s" % self.caller.get_vcs_name()
        ])

    def copy_clipboard(self, widget, data=None):
        self.caller.copy_revision_text()

    def view_diff_previous_revision(self, widget, data=None):
        parent = self.find_parent(self.revisions[0])

        rabbitvcs.util.helper.launch_ui_window("diff", [
            "%s@%s" % (self.path, parent),
            "%s@%s" % (self.path, unicode(self.revisions[0]["revision"])),
            "--vcs=%s" % self.caller.get_vcs_name()
        ])

    def view_diff_revisions(self, widget, data=None):
        path_older = self.path
        if self.vcs_name == rabbitvcs.vcs.VCS_SVN:
            path_older = self.vcs.svn().get_repo_url(self.path)
    
        rabbitvcs.util.helper.launch_ui_window("diff", [
            "%s@%s" % (path_older, self.revisions[1]["revision"].value),
            "%s@%s" % (self.path, unicode(self.revisions[0]["revision"])),
            "--vcs=%s" % self.caller.get_vcs_name()
        ])

    def compare_working_copy(self, widget, data=None):
        path_older = self.path
        if self.vcs_name == rabbitvcs.vcs.VCS_SVN:
            path_older = self.vcs.svn().get_repo_url(self.path)
    
        rabbitvcs.util.helper.launch_ui_window("diff", [
            "-s",
            "%s@%s" % (path_older, unicode(self.revisions[0]["revision"])),
            "%s" % (self.path),
            "--vcs=%s" % self.caller.get_vcs_name()
        ])

    def compare_previous_revision(self, widget, data=None):
        parent = self.find_parent(self.revisions[0])

        rabbitvcs.util.helper.launch_ui_window("diff", [
            "-s",
            "%s@%s" % (self.path, parent),
            "%s@%s" % (self.path, unicode(self.revisions[0]["revision"])),
            "--vcs=%s" % self.caller.get_vcs_name()
        ])

    def compare_revisions(self, widget, data=None):
        path_older = self.path
        if self.vcs_name == rabbitvcs.vcs.VCS_SVN:
            path_older = self.vcs.svn().get_repo_url(self.path)

        rabbitvcs.util.helper.launch_ui_window("diff", [
            "-s",
            "%s@%s" % (path_older, self.revisions[1]["revision"].value),
            "%s@%s" % (self.path, unicode(self.revisions[0]["revision"])),
            "--vcs=%s" % self.caller.get_vcs_name()
        ])

    def show_changes_previous_revision(self, widget, data=None):
        rev_first = unicode(self.revisions[0]["revision"])
        parent = self.find_parent(self.revisions[0])
        
        path = self.path
        if self.vcs_name == rabbitvcs.vcs.VCS_SVN:
            path = self.vcs.svn().get_repo_url(self.path)

        rabbitvcs.util.helper.launch_ui_window("changes", [
            "%s@%s" % (path, parent),
            "%s@%s" % (path, unicode(rev_first)),
            "--vcs=%s" % self.caller.get_vcs_name()
        ])

    def show_changes_revisions(self, widget, data=None):
        rev_first = unicode(self.revisions[0]["revision"])
        rev_last = unicode(self.revisions[0]["next_revision"])

        path = self.path
        if self.vcs_name == rabbitvcs.vcs.VCS_SVN:
            path = self.vcs.svn().get_repo_url(self.path)

        rabbitvcs.util.helper.launch_ui_window("changes", [
            "%s@%s" % (path, unicode(rev_first)),
            "%s@%s" % (path, unicode(rev_last)),
            "--vcs=%s" % self.caller.get_vcs_name()
        ])

    def update_to_this_revision(self, widget, data=None):        
        rabbitvcs.util.helper.launch_ui_window("updateto", [
            self.path,
            "-r", unicode(self.revisions[0]["revision"]),
            "--vcs=%s" % self.caller.get_vcs_name()
        ])

    def revert_changes_from_this_revision(self, widget, data=None):        
        rabbitvcs.util.helper.launch_ui_window("merge", [
            self.path,
            unicode(self.revisions[0]["revision"]) + "-" + str(int(unicode(self.revisions[0]["revision"])) - 1),
            "--vcs=%s" % self.caller.get_vcs_name()
        ])        
        
    def checkout(self, widget, data=None):
        url = ""
        if self.vcs_name == rabbitvcs.vcs.VCS_SVN:
            url = self.vcs.svn().get_repo_url(self.path)

        rabbitvcs.util.helper.launch_ui_window("checkout", [
            self.path, 
            url, 
            "-r", unicode(self.revisions[0]["revision"]),
            "--vcs=%s" % self.caller.get_vcs_name()
        ])

    def branch_tag(self, widget, data=None):
        rabbitvcs.util.helper.launch_ui_window("branch", [
            self.path, 
            "-r", unicode(self.revisions[0]["revision"]),
            "--vcs=%s" % self.caller.get_vcs_name()
        ])

    def branches(self, widget, data=None):
        rabbitvcs.util.helper.launch_ui_window("branches", [
            self.path, 
            "-r", unicode(self.revisions[0]["revision"]),
            "--vcs=%s" % self.caller.get_vcs_name()
        ])

    def tags(self, widget, data=None):
        rabbitvcs.util.helper.launch_ui_window("tags", [
            self.path, 
            "-r", unicode(self.revisions[0]["revision"]),
            "--vcs=%s" % self.caller.get_vcs_name()
        ])
        
    def export(self, widget, data=None):
        rabbitvcs.util.helper.launch_ui_window("export", [
            self.path, 
            "-r", unicode(self.revisions[0]["revision"]),
            "--vcs=%s" % self.caller.get_vcs_name()
        ])

    def merge(self, widget, data=None):
        extra = []
        if self.vcs_name == rabbitvcs.vcs.VCS_GIT:
            extra.append(unicode(self.revisions[0]["revision"]))
            try:
                fromrev = unicode(self.revisions[1]["revision"])
                extra.append(fromrev)
            except IndexError, e:
                pass
        
        extra += ["--vcs=%s" % self.caller.get_vcs_name()]
        
        rabbitvcs.util.helper.launch_ui_window("merge", [self.path] + extra)

    def reset(self, widget, data=None):
        rabbitvcs.util.helper.launch_ui_window("reset", [
            self.path, 
            "-r", unicode(self.revisions[0]["revision"]),
            "--vcs=%s" % self.caller.get_vcs_name()
        ])

    def edit_author(self, widget, data=None):
        message = ""
        if len(self.revisions) == 1:
            author = self.revisions[0]["author"]

        from rabbitvcs.ui.dialog import TextChange
        dialog = TextChange(_("Edit author"), author)
        (result, new_author) = dialog.run()

        if result == gtk.RESPONSE_OK:
            self.caller.edit_revprop("svn:author", new_author, self.caller.on_author_edited)

    def edit_log_message(self, widget, data=None):
        message = ""
        if len(self.revisions) == 1:
            message = self.revisions[0]["message"]

        from rabbitvcs.ui.dialog import TextChange
        dialog = TextChange(_("Edit log message"), message)
        (result, new_message) = dialog.run()

        if result == gtk.RESPONSE_OK:
            self.caller.edit_revprop("svn:log", new_message, self.caller.on_log_message_edited)

    def edit_revision_properties(self, widget, data=None):
        url = self.vcs.svn().get_repo_url(self.path)
        
        rabbitvcs.util.helper.launch_ui_window("revprops", [
            "%s@%s" % (url, unicode(self.revisions[0]["revision"])),
            "--vcs=%s" % self.caller.get_vcs_name()
        ])

class LogTopContextMenu:
    """
    Defines context menu items for a table with files
    
    """
    def __init__(self, caller, event, path, revisions=[]):
        """    
        @param  caller: The calling object
        @type   caller: object
        
        @param  base_dir: The curent working directory
        @type   base_dir: string
        
        @param  path: The loaded path
        @type   path: string
        
        @param  revisions: The selected revisions
        @type   revisions: list of rabbitvcs.vcs.Revision object
        
        """        
        self.caller = caller
        self.event = event
        self.path = path
        self.revisions = revisions
        self.vcs = rabbitvcs.vcs.VCS()

        self.conditions = LogTopContextMenuConditions(
            self.caller,
            self.vcs, 
            self.path, 
            self.revisions
        )
        
        self.callbacks = LogTopContextMenuCallbacks(
            self.caller,
            self.vcs, 
            self.path,
            self.revisions
        )

        # The first element of each tuple is a key that matches a
        # ContextMenuItems item.  The second element is either None when there
        # is no submenu, or a recursive list of tuples for desired submenus.
        self.structure = [      
            (MenuViewDiffWorkingCopy, None),
            (MenuViewDiffPreviousRevision, None),
            (MenuViewDiffRevisions, None),
            (MenuCompareWorkingCopy, None),
            (MenuComparePreviousRevision, None),
            (MenuCompareRevisions, None),
            (MenuShowChangesPreviousRevision, None),
            (MenuShowChangesRevisions, None),
            (MenuSeparator, None),
            (MenuCopyClipboard, None),
            (MenuSeparator, None),
            (MenuUpdateToThisRevision, None),
            (MenuRevertChangesFromThisRevision, None),
            (MenuCheckout, None),
            (MenuBranches, None),
            (MenuTags, None),
            (MenuBranchTag, None),
            (MenuExport, None),
            (MenuMerge, None),
            (MenuReset, None),
            (MenuSeparatorLast, None),
            (MenuEditAuthor, None),
            (MenuEditLogMessage, None),
            (MenuEditRevisionProperties, None)
        ]
        
    def show(self):
        if len(self.revisions) == 0:
            return

        context_menu = GtkContextMenu(self.structure, self.conditions, self.callbacks)
        context_menu.show(self.event)


class LogBottomContextMenuConditions:
    def __init__(self, caller, vcs, paths, revisions):
        self.caller = caller
        self.vcs = vcs
        self.paths = paths
        self.revisions = revisions

    def view_diff_working_copy(self, data=None):
        return False

    def view_diff_previous_revision(self, data=None):
        item = self.revisions[0]["revision"]
        return ("previous_revision" in self.revisions[0] and len(self.revisions) == 1)

    def view_diff_revisions(self, data=None):
        return (len(self.paths) == 1 and len(self.revisions) > 1)

    def compare_working_copy(self, data=None):
        return False

    def compare_previous_revision(self, data=None):
        item = self.revisions[0]["revision"]
        return ("previous_revision" in self.revisions[0] and len(self.revisions) == 1)

    def compare_revisions(self, data=None):
        return (len(self.paths) == 1 and len(self.revisions) > 1)

    def show_changes_previous_revision(self, data=None):
        item = self.revisions[0]["revision"]
        return ("previous_revision" in self.revisions[0] and len(self.revisions) == 1)

    def show_changes_revisions(self, data=None):
        return (len(self.paths) == 1 and len(self.revisions) > 1)

    def _open(self, data=None):
        return True

    def annotate(self, data=None):
        return (len(self.paths) == 1)

    def separator(self, data=None):
        return True

class LogBottomContextMenuCallbacks:
    def __init__(self, caller, vcs, paths, revisions):
        self.caller = caller
        self.vcs = vcs
        self.svn = self.vcs.svn()
        self.vcs_name = self.caller.get_vcs_name()
            
        self.paths = paths
        self.revisions = revisions

    def find_parent(self, revision):
        if ("parents" in revision) and len(revision["parents"]) > 0:
            parent = unicode(revision["parents"][0])
        elif ("next_revision" in revision):
            parent = unicode(revision["next_revision"])
        else:
            parent = unicode(int(unicode(revision["revision"])) - 1)

        return parent

    def view_diff_previous_revision(self, widget, data=None):
        rev = unicode(self.revisions[0]["revision"])

        parent = self.find_parent(self.revisions[0])

        path_item = self.paths[0]
        url = self.caller.root_url + path_item
        self.caller.view_diff_for_path(url, rev, parent)

    def view_diff_revisions(self, widget, data=None):
        rev_first = unicode(self.revisions[0]["revision"])
        rev_last = unicode(self.revisions[-1]["revision"])
        path_item = self.paths[0]
        url = self.caller.root_url + path_item
        self.caller.view_diff_for_path(url, latest_revision_number=rev_last,
                                       earliest_revision_number=rev_first)

    def compare_previous_revision(self, widget, data=None):
        rev = unicode(self.revisions[0]["revision"])
        
        parent = self.find_parent(self.revisions[0])
            
        path_item = self.paths[0]
        url = self.caller.root_url + path_item
        self.caller.view_diff_for_path(url, rev, parent, sidebyside=True)
    
    def compare_revisions(self, widget, data=None):
        earliest_rev = unicode(self.revisions[0]["revision"])
        latest_rev = unicode(self.revisions[-1]["revision"])
        path_item = self.paths[0]
        url = self.caller.root_url + path_item
        self.caller.view_diff_for_path(url,
                                        latest_rev,
                                        sidebyside=True,
                                        earliest_revision_number=earliest_rev)

    def show_changes_previous_revision(self, widget, data=None):
        rev_first = unicode(self.revisions[0]["revision"])
        
        parent = self.find_parent(self.revisions[0])

        url = self.paths[0]
        if self.vcs_name == rabbitvcs.vcs.VCS_SVN:
            url = self.caller.root_url + self.paths[0]

        rabbitvcs.util.helper.launch_ui_window("changes", [
            "%s@%s" % (url, parent),
            "%s@%s" % (url, rev_last),
            "--vcs=%s" % self.caller.get_vcs_name()
        ])
    
    def show_changes_revisions(self, widget, data=None):
        rev_first = unicode(self.revisions[0]["revision"])
        rev_last = unicode(self.revisions[-1]["revision"])

        url = self.paths[0]
        if self.vcs_name == rabbitvcs.vcs.VCS_SVN:
            url = self.caller.root_url + self.paths[0]
        
        rabbitvcs.util.helper.launch_ui_window("changes", [
            "%s@%s" % (url, rev_first),
            "%s@%s" % (url, rev_last),
            "--vcs=%s" % self.caller.get_vcs_name()
        ])
        

    def _open(self, widget, data=None):
        for path in self.paths:
            path = self.caller.root_url + path
            rabbitvcs.util.helper.launch_ui_window("open", [
                path, 
                "--vcs=%s" % self.vcs_name, 
                "-r", unicode(self.revisions[0]["revision"])
            ])

    def annotate(self, widget, data=None):
        url = self.paths[0]
        if self.vcs_name == rabbitvcs.vcs.VCS_SVN:
            url = self.caller.root_url + self.paths[0]

        rabbitvcs.util.helper.launch_ui_window("annotate", [
            url, 
            "--vcs=%s" % self.vcs_name, 
            "-r", unicode(self.revisions[0]["revision"])
        ])

class LogBottomContextMenu:
    """
    Defines context menu items for a table with files
    
    """
    def __init__(self, caller, event, paths, revisions):
        """    
        @param  caller: The calling object
        @type   caller: object
        
        @param  base_dir: The curent working directory
        @type   base_dir: string
        
        @param  paths: The selected paths
        @type   paths: list

        @param  revision: The selected revision
        @type   revision: rabbitvcs.vcs.Revision object
        
        """        
        self.caller = caller
        self.event = event
        self.paths = paths
        self.revisions = revisions
        self.vcs = rabbitvcs.vcs.VCS()
        self.svn = self.vcs.svn()

        self.conditions = LogBottomContextMenuConditions(
            self.caller,
            self.vcs, 
            self.paths, 
            self.revisions
        )
        
        self.callbacks = LogBottomContextMenuCallbacks(
            self.caller,
            self.vcs, 
            self.paths,
            self.revisions
        )

        # The first element of each tuple is a key that matches a
        # ContextMenuItems item.  The second element is either None when there
        # is no submenu, or a recursive list of tuples for desired submenus.
        self.structure = [
            (MenuViewDiffRevisions, None),
            (MenuViewDiffPreviousRevision, None),
            (MenuComparePreviousRevision, None),
            (MenuCompareRevisions, None),
            (MenuShowChangesPreviousRevision, None),
            (MenuShowChangesRevisions, None),
            (MenuSeparator, None),
            (MenuOpen, None),
            (MenuAnnotate, None)
        ]
        
    def show(self):
        if len(self.paths) == 0:
            return

        context_menu = GtkContextMenu(self.structure, self.conditions, self.callbacks)
        context_menu.show(self.event)

classes_map = {
    rabbitvcs.vcs.VCS_SVN: SVNLog,
    rabbitvcs.vcs.VCS_GIT: GitLog
}

dialogs_map = {
    rabbitvcs.vcs.VCS_SVN: SVNLogDialog,
    rabbitvcs.vcs.VCS_GIT: GitLogDialog
}

def log_factory(path, vcs):
    if not vcs:
        guess = rabbitvcs.vcs.guess(path)
        vcs = guess["vcs"]

    return classes_map[vcs](path)

def log_dialog_factory(path, ok_callback=None, multiple=False, vcs=None):
    if not vcs:
        guess = rabbitvcs.vcs.guess(path)
        vcs = guess["vcs"]
    
    return dialogs_map[vcs](path, ok_callback, multiple)

if __name__ == "__main__":
    from rabbitvcs.ui import main, VCS_OPT
    (options, paths) = main(
        [VCS_OPT],
        usage="Usage: rabbitvcs log [--vcs=svn|git] [url_or_path]"
    )

    window = log_factory(paths[0], vcs=options.vcs)
    window.register_gtk_quit()
    gtk.main()
