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

#  A gedit plugin that provides a panel for Bazaar
#  Copyright © 2010-2014  B. Clausius <barcc@gmx.de>
#
#  This program 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 3 of the License, or
#  (at your option) any later version.
#
#  This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.

from __future__ import absolute_import, print_function, division

import sys, os
from collections import defaultdict, OrderedDict

from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GtkSource
from gi.repository import Pango
from gi.repository import Gedit

from . import models
from . import logview
from . import initialize
from .task import TaskManager
from .dialog import error_dialog, info_dialog, warning_dialog, question_dialog
from .bzr_const import IGNORE_FILENAME, NULL_REVISION
from .helper import ConnectHelper


_ui_file = os.path.splitext(__file__)[0]+'.ui'

if sys.version_info[0] < 3:
    str2or3 = lambda s: s.encode('utf-8')
else:
    str2or3 = lambda s: s
    

class TreeData (GObject.Object):
    __gsignals__ = {
        'refresh':                  (GObject.SignalFlags.RUN_FIRST,
                                    None,
                                    ()),
        'reload-files':             (GObject.SignalFlags.RUN_FIRST,
                                    None,
                                    (object,)),
        'revisionhistory-changed':  (GObject.SignalFlags.RUN_FIRST,
                                    None,
                                    ()),
        'filelist-added':           (GObject.SignalFlags.RUN_FIRST,
                                    None,
                                    # revid_old, revid_new, group, incomplete, fileinfos
                                    (str, str, str, bool, object)),
        'filelist-changed':         (GObject.SignalFlags.RUN_FIRST,
                                    None,
                                    ()),
        'diff-changed':             (GObject.SignalFlags.RUN_FIRST,
                                    None,
                                    # revid_old, revid_new, rel_path, diff_text, diff_map
                                    (str, str, str, str, object)),
        'infovalue-changed':        (GObject.SignalFlags.RUN_FIRST,
                                    None,
                                    # expander group, widget name, value, hide/show
                                    (str, str, object, object)),
        'push-result':              (GObject.SignalFlags.RUN_FIRST,
                                    None,
                                    (str, str)),
        'commit-result':            (GObject.SignalFlags.RUN_FIRST,
                                    None,
                                    (str,)),
        'commitmessage-changed':    (GObject.SignalFlags.RUN_FIRST,
                                    None,
                                    (str,)),
        'pending-revisions-changed':(GObject.SignalFlags.RUN_FIRST,
                                    None,
                                    # is_checkout, list of pending revisions
                                    (bool, object)),
    }
    
    file_groups = OrderedDict((
                    #('goupid', (displayname, selectable for commit),),
                    ('added', ('Added', True),),
                    ('removed', ('Removed', True),),
                    ('renamed', ('Renamed', True),),
                    ('kind_changed', ('Kind changed',True),),
                    ('modified', ('Modified', True),),
                    ('unchanged', ('Unchanged', False),),
                    ('ignored', ('Ignored', False),),
                    ('unknown', ('Unknown', False),),
                ))
                
    def __init__(self, wt_basedir):
        GObject.Object.__init__(self)
        
        self.wt_basedir = wt_basedir
        
        self.last_revid = None
        self.last_revno = None
        # cached diff of the workingtree
        self.diff_info = {} # key: rel filepath, value: (diff_text,diff_map)
        
        self.deferred = None
        
        # revid, revno, summary, message, committer, timestamp, tags, authors
        self.history_model = Gtk.ListStore(str, str, str, str, str, str, object, str)
        self.info_dict = {}  # key: (groupname, widget_name), value: (text, show)
        
    def reset_diffs(self):
        self.diff_info.clear()
        
    def reset(self):
        self.deferred = None
        self.reset_diffs()
        self.info_dict.clear()
        
    def refresh(self):
        self.reset()
        #FIXME: Souce marks must be updated too
        self.request_revision_history()
        self.request_pending_revisions()
        self.request_info()
        self.emit('refresh')
        
    def request_revision_history(self):
        if self.wt_basedir:
            TaskManager.task(self, 'get_revision_history', self.wt_basedir)
        else:
            GLib.idle_add(self.set_revision_history, 0, [
                                (None, None, '<i>Not a branch</i>', None, None, None, [], None)])
            
    def set_revision_history(self, first_line, revision_history):
        if first_line == 0:
            if len(revision_history) > 1:
                self.last_revid, self.last_revno = revision_history[1][:2]
                if self.last_revno is not None:
                    self.last_revno = int(self.last_revno)
            else:
                self.last_revid = self.last_revno = None
        if first_line == 0:
            self.history_model.clear()
        for row in revision_history:
            self.history_model.append(row)
        if first_line == 0:
            self.emit('revisionhistory-changed')
        
    def request_files(self, revid_old, revid_new):
        if not self.wt_basedir:
            self.emit('filelist-added', None, None, '', False, [])
            self.emit('filelist-changed')
            return
        if self.deferred is None:
            self._request_files(revid_old, revid_new)
            self.deferred = False
        else:
            self.deferred = (revid_old, revid_new)
                            
    def _request_files(self, revid_old, revid_new):
        TaskManager.task(self, 'get_filelist', self.wt_basedir,
                    revid_old, revid_new)
        
    def set_files(self, revid_old, revid_new, group, incomplete, fileinfos):
        if not self.deferred:
            self.emit('filelist-added', revid_old, revid_new, group, incomplete, fileinfos)
            
    def set_files_end(self):
        if self.deferred:
            self._request_files(*self.deferred)
            self.deferred = False
        else:
            self.deferred = None
        self.emit('filelist-changed')
        
    def request_diff(self, revid_old, revid_new, rel_path):
        if rel_path:
            try:
                del self.diff_info['']
            except KeyError:
                pass
        if self.wt_basedir:
            TaskManager.task(self, 'get_diff', self.wt_basedir, rel_path, revid_old, revid_new)
        else:
            GLib.idle_add(self.set_diff, revid_old, revid_new, rel_path,
                                                'File is not in a branch', None)
        
    def set_diff(self, revid_old, revid_new, rel_path, diff_text, diff_map):
        if revid_old in [self.last_revid, None] and revid_new == NULL_REVISION:
            self.diff_info[rel_path] = (diff_text, diff_map)
        self.emit('diff-changed', revid_old, revid_new, rel_path, diff_text, diff_map)
        
    def get_wt_diff(self, rel_path):
        try:
            return self.diff_info[rel_path]
        except KeyError:
            self.request_diff(None, NULL_REVISION, rel_path)
            return None, None
            
    def request_info(self):
        if not self.info_dict and self.wt_basedir:
            TaskManager.task(self, 'get_info', self.wt_basedir)
            
    def set_info(self, group, name, value, show):
        self.info_dict[(group, name)] = (value, show)
        self.emit('infovalue-changed', group, name, value, show)
        
    def request_push(self, use_existing_dir=False, create_prefix=False):
        if self.wt_basedir:
            TaskManager.task(self, 'do_push', self.wt_basedir,
                        use_existing_dir, create_prefix)
            
    def request_revert(self, files):
        TaskManager.task(self, 'do_revert', self.wt_basedir, files)
        
    def request_add(self, rel_files):
        TaskManager.task(self, 'do_add', self.wt_basedir, rel_files)
        
    def request_ignore(self, files):
        TaskManager.task(self, 'do_ignore', self.wt_basedir, files)
        
    def request_uncommit(self):
        TaskManager.task(self, 'do_uncommit', self.wt_basedir, list(self.diff_info.keys()))
        
    def request_set_location(self, widget_name, value):
        TaskManager.task(self, 'set_location', self.wt_basedir, widget_name, value)
        
    def request_commit(self, message, specific_files, local, unknowns, pointless):
        TaskManager.task(self, 'do_commit', self.wt_basedir,
                         message, specific_files, local, unknowns, pointless, list(self.diff_info.keys()))
        
    def request_save_commitmessage(self, message):
        TaskManager.task(self, 'save_commitmessage', self.wt_basedir, message)
        
    def request_pending_revisions(self):
        TaskManager.task(self, 'get_pending_revisions', self.wt_basedir)
        
        
class PanelFiles (object):
    def __init__(self, builder):
        self.view = builder.get_object('treeview_files')
        
        self.current_path = None  # absolute real path of file in active gedit document
        self.file_tpath = {}
        self.group_tpath = {}
        self.cursor_file = None  # Selected file or file to be selected
        self.group_visible = defaultdict(lambda:True)
        self.group_visible['unchanged'] = False
        self.group_visible['ignored'] = False
        self.group_counts = defaultdict(int)
        self.stat_changed = 0   # Number of changed files in the wt
        
        cell = builder.get_object('cellrenderer_commit')
        cell.connect("toggled", self.on_cell_toggle_commit)
        
        self.store = models.FilesModel()
        
        for row in [
                    ["<i>Not in a branch</i>", '', False, 'nobranch', None, None, False, '', False],
                    ['<i>No changes</i>', '', False, 'message', None, None, False, '', False],
                    ['all', None, True, 'all', None, None, True, '', False]
                ]+[
                    [group, None, True, group, None, None, selectable, '', False]
                        for group, (unused_displayname, selectable) in TreeData.file_groups.items()
                ]:
            titer = self.store.append(None, row)
            self.group_tpath[row[3]] = self.store.get_path(titer)
            
        self.view.set_model(self.store.modelfilter)
        
    def update(self, current_path):
        if self.current_path == current_path:
            return
        try:
            current_tpath = self.file_tpath[self.current_path]
        except KeyError:
            pass
        else:
            self.store.row_highlight(current_tpath, False)
        self.current_path = current_path
        try:
            current_tpath = self.file_tpath[self.current_path]
        except KeyError:
            pass
        else:
            self.store.row_highlight(current_tpath, True)
        
    def deactivate(self):
        self.view.set_model(None)
        self.store = None
        self.view = None
        
    def add_files(self, wt_basedir, group, incomplete, fileinfos):
        '''Insert files
        
        params:
            wt_basedir:     Directory of the workingtree, if empty, there is no branch and the other
                            params are ignored
            group:          Add the files to this group, (added, modified, ...)
            incomplete:     ...
            fileinfos:      list of (display_string, rel_path, file_id) of the files to add
        '''
        try:
            group_displayname, group_selectable = TreeData.file_groups[group]
        except KeyError:
            group_selectable = False
        if group_selectable:
            self.stat_changed -= self.group_counts[group]
            self.stat_changed += len(fileinfos)
        self.group_counts[group] = len(fileinfos)
        group_count = self.group_counts[group]
        
        cursor_tpath, unused_column  = self.view.get_cursor()
        cursor_tpath = self.store.filterpath_to_path(cursor_tpath)
        if cursor_tpath:
            self.cursor_file = self.store[cursor_tpath].abspath or None
            
        if not wt_basedir:
            for path, tpath in self.file_tpath.items():
                del self.file_tpath[path]
                del self.store[tpath]
            for tpath in self.group_tpath.values():
                self.store[tpath].visible = False
            cursor_tpath_new = self.group_tpath['nobranch']
            self.store[cursor_tpath_new].visible = True
            if cursor_tpath != cursor_tpath_new:
                self.view.set_cursor(self.store.path_to_filterpath(cursor_tpath_new), None, False)
            return
        tpath = self.group_tpath['nobranch']
        self.store[tpath].visible = False
        
        if self.stat_changed:
            tpath = self.group_tpath['all']
            self.store[tpath].display_string = 'All ({})'.format(self.stat_changed)
            self.store[tpath].abspath = ''
            self.store[tpath].visible = True
            tpath = self.group_tpath['message']
            self.store[tpath].visible = False
        else:
            tpath = self.group_tpath['all']
            self.store[tpath].visible = False
            tpath = self.group_tpath['message']
            self.store[tpath].visible = True
            
        tpath = self.group_tpath[group]
        titer = self.store.get_iter(tpath)
        cursor_tpath_new = None
        
        #remove rows
        ctiter = self.store.iter_children(titer)
        while ctiter:
            path = self.store[ctiter].abspath
            try:
                del self.file_tpath[path]
            except KeyError:
                pass
            self.store.remove(ctiter)
            ctiter = self.store.iter_children(titer)
            if self.cursor_file == path:
                cursor_tpath = None
                    
        if not fileinfos:
            self.store[tpath].visible = False
        else:
            self.store[tpath].display_string = '<b>{}</b> ({}{})'.format(
                                            group_displayname, '>' if incomplete else '', group_count)
            self.store[tpath].abspath = ''
            self.store[tpath].visible = self.group_visible[group]
            
            # insert rows
            for display_string, rel_path, file_id in fileinfos:
                abs_path = str2or3(os.path.join(wt_basedir, rel_path))
                ctiter = self.store.append(titer, ['', abs_path, True, group, rel_path or None,
                                                   file_id, group_selectable, display_string, True])
                self.store.row_highlight(ctiter, self.current_path == abs_path)
                ctpath = self.store.get_path(ctiter)
                self.file_tpath[abs_path] = ctpath
                if self.cursor_file == abs_path:
                    cursor_tpath_new = ctpath
            ftpath = self.store.iter_to_filterpath(titer)
            if ftpath:
                self.view.expand_row(ftpath, False)
            
        cursor_tpath_new = self.store.path_to_filterpath(cursor_tpath_new)
        if cursor_tpath_new and cursor_tpath_new != cursor_tpath:
            self.view.set_cursor(cursor_tpath_new, None, False)
            
    def set_check_column_visible(self, visible, sensitive):
        checked_col = self.view.get_column(0)
        checked_col.set_visible(visible)
        checked_col_cell_renderer = checked_col.get_cells()[0]
        checked_col_cell_renderer.set_property('activatable', sensitive)
        
    def get_specific_files(self):
        """Return the list of selected paths, and file info.
        
        :return: [paths]
        """
        files = []
        def row_func(model, unused_path, titer, unused_data):
            row = model[titer]
            if not row.checkbox_visible:
                return
            if row.checked:
                if row.group == 'all':
                    row_func.commit_all_changes = True  # all comes before any other file
                elif row.relpath:
                    files.append(row.relpath)
            elif row.relpath:
                row_func.commit_all_changes = False
        row_func.commit_all_changes = False
        self.store.foreach(row_func, None)
        return None if row_func.commit_all_changes else files
        
    def on_cell_toggle_commit(self, unused_cell, tpath):
        tpath = self.store.filterpath_to_path(tpath)
        row = self.store[tpath]
        toggle_val = not row.checked
        def toggle_row(model, tpath, unused_titer, unused_data):
            model[tpath].checked = toggle_val
        if row.group == 'all':
            self.store.foreach(toggle_row, None)
        else:
            row.checked = toggle_val
            row_iter = row.iterchildren()
            for row in row_iter:
                row.checked = toggle_val
                
    def get_menu_items(self, widget_helper, x, y):
        menu_items = []
        
        if x is None:
            tpath_col_pos = self.view.get_cursor()
        else:
            tpath_col_pos = self.view.get_path_at_pos(x, y)
        if tpath_col_pos is not None:
            tpath = self.store.filterpath_to_path(tpath_col_pos[0])
            row = self.store[tpath]
            group = row.group
            rel_path = row.relpath
            if group == 'all':
                files = rel_files = None
            elif rel_path:
                files = [row.abspath]
                rel_files = [rel_path]
            else:
                files = []
                rel_files = []
                for r in row.iterchildren():
                    files.append(r.abspath)
                    rel_files.append(r.relpath)
                    
            def append_item(menu_items, text, func, *args):
                menu_item = Gtk.MenuItem(text)
                menu_items.append(menu_item)
                menu_item.connect("activate", func, *args)
                return menu_item
                
            if group == 'nobranch':
                append_item(menu_items, "Initialize branch", widget_helper.on_activate_init_branch)
            if group in ['all', 'removed', 'unknown']:
                if rel_path:
                    append_item(menu_items, "Add", widget_helper.on_activate_add, rel_files)
                    append_item(menu_items, "Ignore", widget_helper.on_activate_ignore, files)
                else:
                    append_item(menu_items, "Add all", widget_helper.on_activate_add, rel_files)
            elif group == 'ignored':
                if rel_path:
                    append_item(menu_items, "Add", widget_helper.on_activate_add, rel_files)
            if group in ['all', 'added', 'removed', 'renamed', 'kind_changed', 'modified']:
                append_item(menu_items, "Revert changes" if rel_path else "Revert all",
                                                widget_helper.on_activate_revert, files)
            if group == 'ignored':
                ignore_filename = os.path.join(widget_helper.treedata.wt_basedir, IGNORE_FILENAME)
                menu_item = append_item(menu_items, "Edit ignore file",
                                                widget_helper.on_activate_edit_ignores)
                menu_item.props.sensitive = os.path.exists(ignore_filename)
                
        def append_checkitem(menu_items, text, active, func, *args):
            menu_item = Gtk.CheckMenuItem(text)
            menu_items.append(menu_item)
            menu_item.set_active(active)
            menu_item.connect("activate", func, *args)
            return menu_item
            
        menu_item = Gtk.SeparatorMenuItem.new()
        menu_items.append(menu_item)
        append_checkitem(menu_items, 'Show unchanged', self.group_visible['unchanged'],
                self.on_activate_show_group, 'unchanged')
        append_checkitem(menu_items, 'Show ignores', self.group_visible['ignored'],
                self.on_activate_show_group, 'ignored')
        return menu_items
        
    def on_activate_show_group(self, unused_menu_item, group):
        self.group_visible[group] = not self.group_visible[group]
        tpath = self.group_tpath[group]
        self.store[tpath].visible = self.group_visible[group]
        tpath = self.store.path_to_filterpath(tpath)
        if tpath:
            self.view.expand_row(tpath, False)
            
class PanelTabDiff (GObject.Object):
    __gsignals__ = {
        'diff-position-changed':    (GObject.SignalFlags.RUN_FIRST,
                                    None,
                                    # rel filename, lineno
                                    (str, int)),
                    }
    def __init__(self, builder):
        GObject.Object.__init__(self)
        self.view = GtkSource.View()
        self.container = builder.get_object('scrolledwindow_diff')
        self.container.add(self.view)
        self.view.show()
        
        settings = Gio.Settings.new('org.gnome.gedit.preferences.editor')
        slm = GtkSource.LanguageManager()
        gsl = GtkSource.LanguageManager.guess_language(slm, None, "text/x-patch")
        style_scheme_name = settings.get_string('scheme')
        style_scheme_mgr = GtkSource.StyleSchemeManager()
        style_scheme = style_scheme_mgr.get_scheme(style_scheme_name)
        
        self.buffer = GtkSource.Buffer()
        if style_scheme is not None:
            self.buffer.set_style_scheme(style_scheme)
        self.buffer.set_language(gsl)
        self.buffer.set_highlight_syntax(True)
        
        self.view.set_buffer(self.buffer)
        self.view.set_editable(False)
        # read system's monospace font, fallback to 'monospace 10' or "Monospace"
        mono_font = settings.get_string("editor-font")
        font_desc = Pango.FontDescription(mono_font)
        if not font_desc:
            font_desc = Pango.FontDescription("Monospace")
        self.view.modify_font(font_desc)
        
        self.diff_map = None # maps lineno of diff to lineno of the file
        
        self.buffer.connect("mark-set", self.on_buffer_diff_mark_set)
        self.view.connect("populate-popup", self.on_textview_populate_popup)
        
    def deactivate(self):
        self.buffer = None
        self.view = None
        self.container = None
        
    def set_diff(self, diff_text, diff_map):
        scrollbar = self.container.get_vscrollbar()
        adj = scrollbar.props.adjustment
        value = scrollbar and adj.props.value / adj.props.upper
        
        self.diff_map = diff_map
        self.buffer.set_text(diff_text)
        
        if diff_map is None:
            return
            
        if value:
            GLib.idle_add(self.idle_set_scrollbar_adjustment, scrollbar, value)
            
    def idle_set_scrollbar_adjustment(self, scrollbar, value):
        adj = scrollbar.props.adjustment
        adj.props.value = value * adj.props.upper
        
    def on_buffer_diff_mark_set(self, unused_textbuffer, difftab_iter, textmark):
        '''Cursor in the diff changed, set the arrow source mark in the editor
        to the corresponding line'''
        if textmark.get_name() != 'insert':
            return
            
        lineno = difftab_iter.get_line()
        try:
            lineno_map = self.diff_map
            filename, lineno, unused_modify = lineno_map[lineno]
        except (KeyError, TypeError):
            filename, lineno = None, 0
        self.emit('diff-position-changed', filename, lineno)
        
    def on_textview_populate_popup(self, unused_textview, menu):
        menuitem = Gtk.CheckMenuItem('Wrap long lines')
        menuitem.set_active(self.view.get_wrap_mode() != Gtk.WrapMode.NONE)
        menuitem.connect("toggled", self.on_menuitem_long_lines_toggled)
        menuitem.show()
        menu.append(menuitem)
        
    def on_menuitem_long_lines_toggled(self, menuitem):
        self.view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR if menuitem.props.active else Gtk.WrapMode.NONE)
        
        
class PanelTabLog (object):
    def __init__(self, builder, model):
        self.view = logview.LogView(builder, model)
        self.label = builder.get_object("label_log")
        
    def deactivate(self):
        self.view = None
        self.label = None
        
    def update(self):
        self.view.set_cursor()
        GLib.idle_add(self.view.resize_columns)
            
    def get_menu_items(self, widget_helper, unused_x, unused_y):
        menu_items = []
        
        menu_item = Gtk.CheckMenuItem('Uncommit last revision')
        menu_item.connect("activate", widget_helper.on_activate_uncommit)
        menu_items.append(menu_item)
        
        menu_item = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_COPY, None)
        menu_item.connect("activate", widget_helper.on_activate_log_copy)
        menu_items.append(menu_item)
        
        return menu_items
        
        
class PanelTabCommit:
    def __init__(self, builder):
        self.textview_message = builder.get_object('textview_message')
        self.textview_message.modify_font(Pango.FontDescription("Monospace"))
        
        treeview_pending = builder.get_object('treeview_pending')
        #                                   revision_id, date, committer, summary,
        self.liststore_pending = Gtk.ListStore(str,         str,  str,       str,    )
        treeview_pending.set_model(self.liststore_pending)
        
        self.vbox_pending = builder.get_object('vbox_pending')
        # Pending information defaults to hidden
        self.vbox_pending.hide()
        self.pending_revisions = False
        
        self.checkbutton_local = builder.get_object('checkbutton_check_local')
        
        self.textview_message.grab_focus()
        
    def deactivate(self):
        pass
        
    def get_menu_items(self, unused_widget_helper, unused_x, unused_y):
        return []
        
    def get_commitmessage(self):
        buf = self.textview_message.get_buffer()
        start, end = buf.get_bounds()
        text = buf.get_text(start, end, False)
        text = text.replace('\r\n', '\n').replace('\r', '')
        try:
            text = text.decode('utf-8')
        except AttributeError:
            pass
        return text
        
    def set_commitmessage(self, message):
        self.textview_message.get_buffer().set_text(message)
        
    def set_pending_revisions(self, is_checkout, pending_revisions):
        self.pending_revisions = bool(pending_revisions)
        
        # TODO: We'd really prefer this to be a nested list
        self.liststore_pending.clear()
        for rev_info, children in pending_revisions or []:
            self.liststore_pending.append(rev_info)
            for child in children:
                self.liststore_pending.append(child)
                
        if self.pending_revisions:
            self.vbox_pending.show()
        else:
            self.vbox_pending.hide()
        if is_checkout:
            self.checkbutton_local.show()
        else:
            self.checkbutton_local.hide()
            
        
class PanelTabInfo (object):
    def __init__(self, builder):
        self.view = builder.get_object('vbox_info')
        
        self.view.connect_after('realize', self.on_realize)
        
    def deactivate(self):
        self.view = None
        
    def on_realize(self, unused_widget):
        width_request = 0
        for expander in self.view.get_children():
            assert type(expander) is Gtk.Expander
            vbox = expander.get_child()
            if vbox is None:
                continue
            assert type(vbox) is Gtk.Box
            for hbox in vbox.get_children():
                assert type(hbox) is Gtk.Box
                for label in hbox.get_children():
                    assert type(label) is Gtk.Label
                    width_request = max(width_request, label.get_allocation().width)
                    break
        for expander in self.view.get_children():
            vbox = expander.get_child()
            if vbox is None:
                continue
            for hbox in vbox.get_children():
                for label in hbox.get_children():
                    label.set_size_request(width_request, -1)
                    break
        #self.view.queue_resize()
        
        
class WidgetHelper (GObject.Object, ConnectHelper):
    '''Wraps the widget that displays the branch'''
    
    __gsignals__ = {
        'open-location':  (GObject.SignalFlags.RUN_FIRST, None, (Gio.File,)),
        'init-branch-dialog':   (GObject.SignalFlags.RUN_FIRST, None, (str, str)),
    }
    
    def __init__(self, treedata):
        GObject.Object.__init__(self)
        ConnectHelper.__init__(self)
        
        self.treedata = treedata
        self.current_path = None  # absolute real path of file in active gedit document
        if self.treedata.wt_basedir:
            self.revid_new, self.revno_new = NULL_REVISION, 0
        else:
            self.revid_new, self.revno_new = None, None
        self.revid_old = self.treedata.last_revid
        self.revno_old = self.treedata.last_revno
        self.selected_file = None  # relative path to selected file in the file_list
        self.commit_iterator = None
        self.commitmessage_changed = False
        
        # load ui
        builder = Gtk.Builder()
        self.builder = builder
        builder.add_from_file(_ui_file)
        builder.connect_signals(self)
        self.widget_bzrpanel = builder.get_object("widget_bzrpanel")
        self.notebook_cmd = builder.get_object("notebook_cmd")
        self.spinner_worker = builder.get_object("spinner_worker")
        TaskManager.connect_busy_changed(self.on_busy_changed)
        #TODO: set the error function once for the app
        # The parent window for the error dialog should be the active window
        # or the window the requested the operation that caused the error.
        TaskManager.set_error_func(error_dialog)
        
        # ui elements
        self.file_list = PanelFiles(builder)
        self.tab_diff = PanelTabDiff(builder)
        self.tab_log = PanelTabLog(builder, self.treedata.history_model)
        self.tab_commit = PanelTabCommit(builder)
        self.tab_info = PanelTabInfo(builder)
        self.panel_tabs = [self.tab_diff, self.tab_log, self.tab_commit, self.tab_info]
        self.branch_views = self.panel_tabs[:] + [self.file_list]
        
        self.current_tabview = self.panel_tabs[0] # current panel tab
        
        self._connect(self.treedata, 'revisionhistory-changed', self.on_treedata_revisionhistory_changed)
        self._connect(self.treedata, 'filelist-added', self.on_treedata_filelist_added)
        self._connect(self.treedata, 'filelist-changed', self.on_treedata_filelist_changed)
        self._connect(self.treedata, 'diff-changed', self.on_treedata_diff_changed)
        self._connect(self.treedata, 'infovalue-changed', self.on_treedata_infovalue_changed)
        self._connect(self.treedata, 'push-result', self.on_treedata_push_result)
        self._connect(self.treedata, 'commit-result', self.on_treedata_commit_result)
        self._connect(self.treedata, 'commitmessage-changed', self.on_treedata_commitmessage_changed)
        self._connect(self.treedata, 'pending-revisions-changed', self.on_treedata_pending_revisions_changed)
        
        self._connect(self.file_list.view, 'cursor-changed', self.on_treeview_files_cursor_changed)
        self._connect(self.file_list.view, 'row-activated', self.on_treeview_files_row_activated)
        self._connect(self.tab_log.view, 'revision-selected', self.on_revision_selected)
        self._connect(self.tab_commit.textview_message.get_buffer(), 'changed',
                                                            self.on_textview_message_changed)
        self._connect(self.tab_commit.textview_message, 'focus-out-event',
                                                            self.on_textview_message_focus_out_event)
        
        def connect_button_press_event(widget, origin, stop_other):
            widget.connect('popup-menu', self.on_popup_menu, origin)
            widget.connect('button-press-event', self.on_button_press_event, origin, stop_other)
        connect_button_press_event(self.file_list.view,               self.file_list, False)
        connect_button_press_event(self.tab_log.view.treeview, self.tab_log,   False)
        #connect_button_press_event(self.tab_info.view,                self.tab_info,  False)
        #connect_button_press_event(self.notebook_cmd,                 None,           True)
        
        self.changed_entry_name = None
        
        self.treedata.request_revision_history()
        self.treedata.request_save_commitmessage(None)
        self.treedata.request_pending_revisions()
        self.treedata.request_info()
        
    def deactivate(self):
        self.disconnect_all()
        
        del self.panel_tabs[:]
        while self.branch_views:
            branch_view = self.branch_views.pop()
            branch_view.deactivate()
            
        self.notebook_cmd = None
        self.current_tabview = None
        self.branch_views = None
        self.panel_tabs = None
        self.tab_info = None
        self.tab_log = None
        self.tab_diff = None
        self.file_list = None
        self.notebook_cmd = None
        self.widget_bzrpanel = None
        self.current_path = None
        
    def update_changed(self, current_path):
        self.current_path = current_path
        self.file_list.update(current_path)
        
    def set_revision_label(self):
        if not self.treedata.wt_basedir:
            revision_string = 'No Revisions'
        elif self.revid_new is None:
            revision_string = 'Revisions'
        else:
            format_ =  'Revision {} - Revision {}' if self.revno_new else 'Revision {} - top'
            revision_string = format_.format(self.revno_old, self.revno_new)
        self.tab_log.label.set_text(revision_string)
        
    def on_treeview_files_cursor_changed(self, unused_widget):
        if self.treedata.wt_basedir:
            tpath, unused_column = self.file_list.view.get_cursor()
            tpath = self.file_list.store.filterpath_to_path(tpath)
            self.selected_file = tpath and self.file_list.store[tpath].relpath or ''
            if self.revid_old in [None, self.treedata.last_revid] and self.revid_new == NULL_REVISION:
                diff_text, diff_map = self.treedata.get_wt_diff(self.selected_file)
                if diff_text is not None:
                    self.tab_diff.set_diff(diff_text, diff_map)
                return
        else:
            self.selected_file = None
        self.treedata.request_diff(self.revid_old, self.revid_new, self.selected_file)
        
    def on_treeview_files_row_activated(self, unused_widget, tpath, unused_column):
        tpath_ = self.file_list.store.filterpath_to_path(tpath)
        rel_path = tpath_ and self.file_list.store[tpath_].relpath
        if not rel_path:
            if self.file_list.view.row_expanded(tpath):
                self.file_list.view.collapse_row(tpath)
            else:
                self.file_list.view.expand_row(tpath, False)
        else:
            path = self.file_list.store[tpath_].abspath
            location = Gio.File.new_for_path(path)
            self.emit('open-location', location)
            
    def on_notebook_cmd_switch_page(self, unused_notebook, unused_page, page_num):
        self.current_tabview = self.panel_tabs[page_num]
        if self.current_tabview == self.tab_log:
            GLib.idle_add(self.tab_log.view.resize_columns)
        if self.current_tabview == self.tab_commit:
            self.file_list.set_check_column_visible(visible=True,
                                        sensitive=not self.tab_commit.pending_revisions)
        else:
            self.file_list.set_check_column_visible(visible=False, sensitive=False)
                
    def on_treedata_revisionhistory_changed(self, unused_treedata):
        self.set_revision_label()
        self.tab_log.update()
        
    def on_treedata_filelist_added(self, treedata, revid_old, revid_new, group, incomplete, fileinfos):
        if revid_old != self.revid_old or revid_new != self.revid_new:
            return
        self.file_list.add_files(treedata.wt_basedir, group, incomplete, fileinfos)
        
    def on_treedata_filelist_changed(self, unused_treedata):
        cursor_tpath, unused_column  = self.file_list.view.get_cursor()
        if not cursor_tpath:
            self.tab_diff.buffer.set_text('')
            
    def on_treedata_diff_changed(self, unused_treedata, revid_old, revid_new, wt_path, diff_text, diff_map):
        if revid_old != self.revid_old or revid_new != self.revid_new or wt_path != self.selected_file:
            return
        self.tab_diff.set_diff(diff_text, diff_map)
        
    def on_revision_selected(self, log_helper, revid_new, revno_new, revid_old, revno_old):
        self.revid_new = revid_new
        self.revno_new = revno_new
        self.revid_old = revid_old
        self.revno_old = revno_old
        self.set_revision_label()
        self.tab_log.label.props.tooltip_text = log_helper.get_message()
        self.treedata.request_files(revid_old, revid_new)
        
    def on_button_press_event(self, widget, event, origin, stop_other):
        if event.button != 3:
            return False
        button = event.button
        time = event.time
        x = int(event.x)
        y = int(event.y)
        GLib.idle_add(lambda: self.on_popup_menu(widget, origin, button, time, x, y) and False)
        return stop_other
        
    def on_popup_menu(self, widget, origin, button=Gtk.ButtonsType.NONE, time=0, x=None, y=None):
        menu = Gtk.Menu()
        try:
            get_menu_items = origin.get_menu_items
        except AttributeError:
            pass
        else:
            for menu_item in get_menu_items(self, x, y):
                menu.append(menu_item)
                
        if len(menu):
            menu.show_all()
            menu.attach_to_widget(widget, None)
            menu.popup(None, None, None, None, button, time)
            return True
        return False
        
    def on_toolbutton_refresh_clicked(self, unused_widget):
        self.treedata.refresh()
        
    def on_toolbutton_next_revision_clicked(self, unused_widget, allow_retry=True):
        view = self.tab_log.view.treeview
        tpath, tcol = view.get_cursor()
        if tpath is None:
            # Revision model is not ready yet
            if allow_retry:
                GLib.idle_add(self.on_toolbutton_next_revision_clicked, None, False)
            return
            
        if tpath.get_indices()[0] > 0:
            tpath.prev()
            view.set_cursor(tpath, tcol, False)
            
    def on_toolbutton_prev_revision_clicked(self, unused_widget, allow_retry=True):
        view = self.tab_log.view.treeview
        tpath, tcol = view.get_cursor()
        if tpath is None:
            # Revision model is not ready yet
            if allow_retry:
                GLib.idle_add(self.on_toolbutton_prev_revision_clicked, None, False)
            return
            
        if tpath.get_indices()[0] < len(self.tab_log.view.model) - 1:
            tpath.next()
            view.set_cursor(tpath, tcol, False)
            
    def on_toolbutton_stopworker_clicked(self, unused_widget):
        TaskManager.stop()
        
    def on_button_commit_clicked(self, unused_widget):
        if not self.treedata.wt_basedir:
            return
        self.commit_iterator = self.commit_generator()
        try:
            commitargs = next(self.commit_iterator)
        except StopIteration:
            return
        self.treedata.request_commit(*commitargs)
        
    def on_treedata_commit_result(self, unused_treedata, result):
        try:
            args = self.commit_iterator.send(result)
        except StopIteration:
            return
        self.treedata.request_commit(*args)
        
    def check_commit(self, window):
        for window_ in Gedit.App.get_default().get_windows():
            if window_.get_unsaved_documents():
                response = question_dialog(
                    'Commit with unsaved files?',
                    'You may want to save all files before commit.',
                    parent=window)
                if response == Gtk.ResponseType.YES:
                    break
                else:
                    return None
                    
        message = self.tab_commit.get_commitmessage()
        if message == '':
            response = question_dialog(
                'Commit with an empty message?',
                'You can describe your commit intent in the message.',
                parent=window)
            if response == Gtk.ResponseType.NO:
                # Kindly give focus to message area
                self.tab_commit.textview_message.grab_focus()
                return None
        return message
        
    def commit_generator(self):
        window = self.widget_bzrpanel.get_toplevel()
        message = self.check_commit(window)
        if message is None:
            return
        specific_files = self.file_list.get_specific_files()
        if self.tab_commit.pending_revisions:
            specific_files = None
        local = self.tab_commit.checkbutton_local.get_active()
        unknowns = False
        pointless = False
        while True:
            result = yield message, specific_files, local, unknowns, pointless
            if result == 'unknowns':
                unknowns = True
                response = question_dialog(
                    "Commit with unknowns?",
                    "Unknown files exist in the working tree.\nCommit anyway?",
                    parent=window)
            elif result == 'pointless':
                pointless = True
                response = question_dialog(
                    'Commit with no changes?',
                    'There are no changes in the working tree.\nCommit anyway?',
                    parent=window)
            elif result == 'success':
                # destroy old comments if any
                return
            else:
                assert False
            if response != Gtk.ResponseType.YES:
                return
                
    def on_activate_init_branch(self, unused_menu_item):
        branch_dir = os.path.dirname(self.current_path or os.path.expanduser('~/new-project/trunk/'))
        self.emit('init-branch-dialog', branch_dir, self.current_path)
        
    def on_activate_revert(self, unused_menu_item, files):
        if files is None:
            window = self.widget_bzrpanel.get_toplevel()
            response = question_dialog('Revert all', 'Do you want to revert all files?', parent=window)
            if response != Gtk.ResponseType.YES:
                return
        self.treedata.request_revert(files)
        
    def on_activate_add(self, unused_menu_item, rel_files):
        self.treedata.request_add(rel_files)
        
    def on_activate_ignore(self, unused_menu_item, files):
        self.treedata.request_ignore(files)
        
    def on_activate_edit_ignores(self, unused_menu_item):
        path = os.path.join(self.treedata.wt_basedir, IGNORE_FILENAME)
        location = Gio.File.new_for_path(path)
        self.emit('open-location', location)
        
    def on_activate_uncommit(self, unused_menu_item):
        window = self.widget_bzrpanel.get_toplevel()
        response = question_dialog('Uncommit', 'Do you want to uncommit last revision?', parent=window)
        if response == Gtk.ResponseType.YES:
            self.treedata.request_uncommit()
            
    def on_activate_log_copy(self, unused_menu_item):
        text = self.tab_log.view.get_message()
        if text is not None:
            clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
            clipboard.set_text(text, len(text))
            clipboard.store()
            
    def on_button_push_location_clicked(self, unused_button):
        self.treedata.request_push()
        
    def on_treedata_push_result(self, unused_treedata, result, message):
        #if result == 'no_push_location':
        #elif result == 'invalid_target_bzr_dir':
        window = self.widget_bzrpanel.get_toplevel()
        if result == 'unversioned_target_dir_exists':
            response = question_dialog('Push',
                    'Target directory "%s" already exists, but does not have a .bzr directory.'
                    '\n\nUse existing directory?' % message,
                    parent=window)
            if response == Gtk.ResponseType.YES:
                self.treedata.request_push(use_existing_dir=True)
        elif result == 'parent_directory_not_exists':
            response = question_dialog('Push',
                    'Parent directory of "%s" does not exist.'
                    '\n\nCreate directory?' % message,
                    parent=window)
            if response == Gtk.ResponseType.YES:
                self.treedata.request_push(create_prefix=True)
        #elif result == 'too_many_redirections':
        #elif result == 'diverged_branches':
        #elif result == 'NoRoundtrippingSupport':
        #elif result == 'NoRepositoryPresent':
        elif result == 'workingtree_not_updated':
            warning_dialog('Push', message, window)
        elif result == 'OK':
            info_dialog('Push successful', message, window)
        else:
            error_dialog('Push failed', message, window)
        
    def on_busy_changed(self, unused_worker, busy):
        self.spinner_worker.set_visible(busy)
        self.spinner_worker.props.active = busy
        
    def on_treedata_infovalue_changed(self, unused_treedata, group, name, value, show):
        expander = self.builder.get_object('expander_'+group)
        prefix, key = name.split('_', 1)
        hbox = self.builder.get_object('hbox_'+key)
        widget = self.builder.get_object(name)
        if expander is None or hbox is None or widget is None:
            return
        set_text = widget.set_label if prefix == 'button' else widget.set_text
        try:
            widget.handler_block_by_func(self.on_entry_xxx_changed)
        except TypeError:
            handler_unblock_by_func = lambda x: None
        else:
            handler_unblock_by_func = widget.handler_unblock_by_func
        try:
            if show is False:
                set_text('')
                hbox.hide()
            elif show is True:
                set_text(value)
                hbox.show()
            else:
                set_text(value or '')
        finally:
            handler_unblock_by_func(self.on_entry_xxx_changed)
            
    def on_entry_xxx_activate_or_focus_out_event(self, entry, unused_event=None):
        name = Gtk.Buildable.get_name(entry)
        try:
            if self.changed_entry_name != name:
                return
        finally:
            self.changed_entry_name = None
            
        new_text = entry.get_text()
        self.treedata.request_set_location(name, new_text)
        
    def on_entry_xxx_changed(self, entry):
        name = Gtk.Buildable.get_name(entry)
        if self.changed_entry_name != name:
            self.changed_entry_name = name
            
    def on_textview_message_changed(self, unused_textbuffer):
        self.commitmessage_changed = True
        
    def on_textview_message_focus_out_event(self, unused_widget, unused_event):
        if self.commitmessage_changed:
            message = self.tab_commit.get_commitmessage()
            self.commitmessage_changed = False
            self.treedata.request_save_commitmessage(message)
            
    def on_treedata_commitmessage_changed(self, unused_treedata, message):
        self.tab_commit.set_commitmessage(message)
            
    def on_treedata_pending_revisions_changed(self, unused_treedata, is_checkout, pending_revisions):
        self.tab_commit.set_pending_revisions(is_checkout, pending_revisions)
        if self.current_tabview == self.tab_commit:
            self.file_list.set_check_column_visible(visible=True, sensitive=not pending_revisions)
        
        
class PanelHelper(GObject.Object):
    '''Wraps the panel
    '''
    __gsignals__ = {
        'diff-position-changed':    (GObject.SignalFlags.RUN_FIRST,
                                    None,
                                    # abs filename, lineno
                                    (str, int)),
        'wt-basedir-changed':       (GObject.SignalFlags.RUN_FIRST,
                                    None,
                                    ()),
                    }
    def __init__ (self, window=None):
        GObject.Object.__init__ (self)
        self.window = window
        self.box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0)
        
        self.widget_dict = {}  # keys: TreeData, values: WidgetHelper
        self.current_path = None
        self.current_widget = None  # Helper for the current Branch
        self.init_branch_dialog = None
        
    def deactivate(self):
        for td in self.widget_dict.values():
            td.deactivate()
        self.widget_dict.clear()
        
        self.current_widget = None
        self.widget_dict = None
        self.box = None
        self.window = None
        
    def remove_widget(self):
        self.current_path = None
        if self.current_widget is not None:
            self.box.remove(self.current_widget.widget_bzrpanel)
            self.current_widget = None
        
    def set_widget(self, current_path, treedata):
        self.current_path = current_path
        # get or create widget
        try:
            current_widget = self.widget_dict[treedata]
        except KeyError:
            current_widget = WidgetHelper(treedata)
            self.widget_dict[treedata] = current_widget
            current_widget.connect('open-location', self.on_open_location)
            current_widget.tab_diff.connect('diff-position-changed', self.on_diff_position_changed)
            current_widget.treedata.connect('reload-files', self.on_treedata_reload_files)
            current_widget.connect('init-branch-dialog', self.on_init_branch_dialog)
        # show widget in the panel
        if self.current_widget != current_widget:
            if self.current_widget:
                self.box.remove(self.current_widget.widget_bzrpanel)
            self.current_widget = current_widget
            self.box.pack_start(self.current_widget.widget_bzrpanel, True, True, 0)
            self.box.show()
        self.current_widget.update_changed(current_path)
        
    def on_open_location(self, unused_widget, location):
        tab = self.window.get_tab_from_location(location)
        if tab is None:
            tab = self.window.create_tab_from_location(location, None, 0, 0, False, True)
        else:
            self.window.set_active_tab(tab)
        GLib.idle_add(tab.get_view().grab_focus)
        
    def on_diff_position_changed(self, unused_diff_tab, filename, lineno):
        '''Set the arrow source mark in the source view
        associated with the line of the cursor in the diff'''
        if filename is not None:
            filename = os.path.join(self.current_widget.treedata.wt_basedir, filename)
        self.emit('diff-position-changed', filename, lineno)
            
    def on_treedata_reload_files(self, unused_treedata, files):
        for doc in self.window.get_documents():
            location = doc.get_location()
            path = location and location.get_path()
            if path in files and not doc.get_modified():
                insert_iter = doc.get_iter_at_mark(doc.get_insert())
                doc.load(location, doc.get_encoding(),
                         insert_iter.get_line(), insert_iter.get_line_offset(),
                         False)
                
    def on_init_branch_dialog(self, unused_widget, branch_dir, current_path):
        self.init_branch_dialog = initialize.InitDialog(branch_dir, parent=self.window)
        self.init_branch_dialog.connect('init-repository', self.on_init_repository, current_path)
        self.init_branch_dialog.connect('init-branch', self.on_init_branch, current_path)
        self.init_branch_dialog.run()
        self.init_branch_dialog.destroy()
        self.init_branch_dialog = None
            
    def on_init_repository(self, dialog, location, current_path):
        TaskManager.task(self, 'init_repository', location, current_path)
        
    def on_init_branch(self, dialog, location, current_path):
        TaskManager.task(self, 'init_branch', location, current_path)
        
    def init_bzr_result(self, msg, error, location, current_path):
        if error is None:
            self.emit('wt-basedir-changed')
        else:
            window = self.init_branch_dialog and self.init_branch_dialog.dialog_initialize
            error_dialog(msg, error, window)
            

