class LogBrowserMainWindow(MainWindowBase):

    _logger = _module_logger.getChild('LogBrowserMainWindow')

    SHA_SHORTCUT_LENGTH = 8

    ##############################################

    def __init__(self, parent=None):

        super(LogBrowserMainWindow,
              self).__init__(title='CodeReview Log Browser', parent=parent)

        self._icon_loader = IconLoader()
        self.setWindowIcon(self._icon_loader['code-review@svg'])

        self._current_revision = None
        self._diff = None
        self._current_patch_index = None
        self._diff_window = None

        self._review_note = None

        self._init_ui()
        self._create_actions()
        self._create_toolbar()

        self._application.directory_changed.connect(self._on_directory_changed)
        self._application.file_changed.connect(self._on_file_changed)

    ##############################################

    def _init_ui(self):

        # Table models are set in application

        central_widget = QtWidgets.QWidget(self)
        self.setCentralWidget(central_widget)

        top_vertical_layout = QtWidgets.QVBoxLayout(central_widget)

        self._message_box = MessageBox(self)
        top_vertical_layout.addWidget(self._message_box)

        horizontal_layout = QtWidgets.QHBoxLayout()
        top_vertical_layout.addLayout(horizontal_layout)

        vertical_layout = QtWidgets.QVBoxLayout()
        horizontal_layout.addLayout(vertical_layout)

        horizontal_layout2 = QtWidgets.QHBoxLayout()
        vertical_layout.addLayout(horizontal_layout2)
        button = QtWidgets.QPushButton('Diff')
        button.clicked.connect(self._on_diff_ab)
        horizontal_layout2.addWidget(button)
        button = QtWidgets.QPushButton()
        button.setIcon(self._icon_loader['edit-delete@svg'])
        horizontal_layout2.addWidget(button)
        self._diff_a = QtWidgets.QLineEdit(self)
        button.clicked.connect(lambda: self._diff_a.clear())
        horizontal_layout2.addWidget(self._diff_a)
        button = QtWidgets.QPushButton()
        button.setIcon(self._icon_loader['media-playlist-repeat@svg'])
        button.clicked.connect(self._on_diff_exchange)
        horizontal_layout2.addWidget(button)
        self._diff_b = QtWidgets.QLineEdit(self)
        horizontal_layout2.addWidget(self._diff_b)
        button = QtWidgets.QPushButton()
        button.setIcon(self._icon_loader['edit-delete@svg'])
        button.clicked.connect(lambda: self._diff_b.clear())
        horizontal_layout2.addWidget(button)
        # for widget in (self._diff_a, self._diff_b):
        #     widget.editingFinished.connect(self._on_diff_ab)

        self._branch_name = QtWidgets.QLineEdit(self)
        self._branch_name.setReadOnly(True)
        vertical_layout.addWidget(self._branch_name)

        self._row_count = QtWidgets.QLabel('')
        vertical_layout.addWidget(self._row_count)

        row = 0
        grid_layout = QtWidgets.QGridLayout()
        horizontal_layout.addLayout(grid_layout)
        label = QtWidgets.QLabel('Committer Filter')
        committer_filter = QtWidgets.QLineEdit()
        committer_filter.textChanged.connect(self._on_committer_filter_changed)
        for i, widget in enumerate((label, committer_filter)):
            grid_layout.addWidget(widget, row, i)
        row += 1

        horizontal_layout = QtWidgets.QHBoxLayout()
        top_vertical_layout.addLayout(horizontal_layout)
        label = QtWidgets.QLabel('Message Filter')
        message_filter = QtWidgets.QLineEdit()
        message_filter.textChanged.connect(self._on_message_filter_changed)
        for i, widget in enumerate((label, message_filter)):
            grid_layout.addWidget(widget, row, i)
        row += 1

        horizontal_layout = QtWidgets.QHBoxLayout()
        top_vertical_layout.addLayout(horizontal_layout)
        label = QtWidgets.QLabel('SHA Filter')
        sha_filter = QtWidgets.QLineEdit()
        sha_filter.textChanged.connect(self._on_sha_filter_changed)
        for i, widget in enumerate((label, sha_filter)):
            grid_layout.addWidget(widget, row, i)
        row += 1

        splitter = QtWidgets.QSplitter()
        top_vertical_layout.addWidget(splitter)
        splitter.setOrientation(Qt.Vertical)

        self._log_table = QtWidgets.QTableView()
        splitter.addWidget(self._log_table)

        bottom_widget = QtWidgets.QWidget()
        splitter.addWidget(bottom_widget)
        bottom_horizontal_layout = QtWidgets.QHBoxLayout()
        bottom_widget.setLayout(bottom_horizontal_layout)

        vertical_layout = QtWidgets.QVBoxLayout()
        bottom_horizontal_layout.addLayout(vertical_layout)
        self._commit_table = QtWidgets.QTableView()
        vertical_layout.addWidget(self._commit_table)

        vertical_layout = QtWidgets.QVBoxLayout()
        bottom_horizontal_layout.addLayout(vertical_layout)
        self._commit_sha = QtWidgets.QLineEdit()
        self._commit_sha.setReadOnly(True)
        vertical_layout.addWidget(self._commit_sha)
        self._parent_labels = []
        for i in range(2):
            horizontal_layout = QtWidgets.QHBoxLayout()
            vertical_layout.addLayout(horizontal_layout)
            button = QtWidgets.QPushButton('Go')
            button.clicked.connect(
                lambda state, index=i: self._on_go_clicked(index))
            horizontal_layout.addWidget(button)
            parent = QtWidgets.QLineEdit()
            parent.setReadOnly(True)
            horizontal_layout.addWidget(parent)
            self._parent_labels.append(parent)
        self._review_comment = QtWidgets.QTextEdit()
        vertical_layout.addWidget(self._review_comment)
        horizontal_layout = QtWidgets.QHBoxLayout()
        vertical_layout.addLayout(horizontal_layout)
        save_button = QtWidgets.QPushButton('Save')
        save_button.clicked.connect(self._on_save_review)
        horizontal_layout.addItem(
            QtWidgets.QSpacerItem(0, 10, QSizePolicy.Expanding))
        horizontal_layout.addWidget(save_button)

        table = self._log_table
        table.setSelectionMode(QtWidgets.QTableView.SingleSelection)
        table.setSelectionBehavior(QtWidgets.QTableView.SelectRows)
        table.verticalHeader().setVisible(False)
        table.setShowGrid(False)
        # table.setSortingEnabled(True)
        table.clicked.connect(self._update_commit_table)

        table = self._commit_table
        table.setSelectionMode(QtWidgets.QTableView.SingleSelection)
        table.setSelectionBehavior(QtWidgets.QTableView.SelectRows)
        table.verticalHeader().setVisible(False)
        table.setShowGrid(False)
        table.setSortingEnabled(True)
        table.clicked.connect(self._on_clicked_table)

        # horizontal_header = table_view.horizontalHeader()
        # horizontal_header.setMovable(True)

    ##############################################

    def finish_table_connections(self):
        self._log_table.selectionModel().currentRowChanged.connect(
            self._update_commit_table)
        #!# Fixme: reopen diff viewer window when repository change
        #!# self._commit_table.selectionModel().currentRowChanged.connect(self._on_clicked_table)

    ##############################################

    def _create_actions(self):

        self._stagged_mode_action = \
            QtWidgets.QAction('Stagged',
                              self,
                              toolTip='Stagged Mode',
                              shortcut='Ctrl+1',
                              checkable=True,
            )

        self._not_stagged_mode_action = \
            QtWidgets.QAction('Not Stagged',
                              self,
                              toolTip='Not Stagged Mode',
                              shortcut='Ctrl+2',
                              checkable=True,
            )

        self._all_change_mode_action = \
            QtWidgets.QAction('All',
                              self,
                              toolTip='All Mode',
                              shortcut='Ctrl+3',
                              checkable=True,
            )

        self._action_group = QtWidgets.QActionGroup(self)
        self._action_group.triggered.connect(self._update_working_tree_diff)
        for action in (
                self._all_change_mode_action,
                self._stagged_mode_action,
                self._not_stagged_mode_action,
        ):
            self._action_group.addAction(action)
        self._all_change_mode_action.setChecked(True)

        self._reload_action = \
            QtWidgets.QAction(self._icon_loader['view-refresh@svg'],
                              'Refresh',
                              self,
                              toolTip='Refresh',
                              shortcut='Ctrl+R',
                              triggered=self._reload_repository,
            )

    ##############################################

    def _create_toolbar(self):
        self._tool_bar = self.addToolBar('Diff on Working Tree')
        for item in self._action_group.actions():
            self._tool_bar.addAction(item)
        for item in (self._reload_action, ):
            self._tool_bar.addAction(item)

    ##############################################

    def init_menu(self):
        super(LogBrowserMainWindow, self).init_menu()

    ##############################################

    def show_message(self, message=None, timeout=0, warn=False):
        """ Hides the normal status indications and displays the given message for the specified
        number of milli-seconds (timeout). If timeout is 0 (default), the message remains displayed
        until the clearMessage() slot is called or until the showMessage() slot is called again to
        change the message.

        Note that showMessage() is called to show temporary explanations of tool tip texts, so
        passing a timeout of 0 is not sufficient to display a permanent message.
        """

        if warn:
            self._message_box.push_message(message)
        else:
            status_bar = self.statusBar()
            if message is None:
                status_bar.clearMessage()
            else:
                status_bar.showMessage(message, timeout)

    ##############################################

    def _on_directory_changed(self, path):

        self._logger.info(path)

        self._reload_repository()
        self._diff = self._application.repository.diff(**self._diff_kwargs)

        if self._diff_window is not None:
            if self.number_of_patches:
                self._current_patch_index = 0
                self._diff_window.update_patch_index()
                self.reload_current_patch()
            else:
                self._diff_window.close()

    ##############################################

    def _on_file_changed(self, path):

        self._logger.info(path)

        repository = self._application.repository
        if path == repository.join_repository_path(repository.INDEX_PATH):
            self._diff = self._application.repository.diff(**self._diff_kwargs)
        else:
            message = 'File {} changed'.format(path)
            self.show_message(message)
            self.reload_current_patch()

    ##############################################

    def _reload_repository(self):

        self._logger.info('Reload signal')
        index = self._log_table.currentIndex()
        self._application.reload_repository()
        if index.row() != -1:
            self._logger.info("Index is {}".format(index.row()))
            self._log_table.setCurrentIndex(index)
            # Fixme: ???
            # self._update_working_tree_diff()
            self.show_working_tree_diff()
        else:
            self.show_working_tree_diff()

    ##############################################

    def show_working_tree_diff(self):

        self._logger.info('Show WT')
        log_model = self._log_table.model()
        if log_model.rowCount():
            top_index = log_model.index(0, 0)
            self._log_table.setCurrentIndex(top_index)
            self._update_working_tree_diff()

    ##############################################

    def _update_working_tree_diff(self):
        # Check log table is on working tree
        if self._log_table.currentIndex().row() == 0:
            self._update_commit_table()

    ##############################################

    def _reset_parent(self):
        self._current_commit = None
        for parent in self._parent_labels:
            parent.clear()

    ##############################################

    def _update_commit_table(self, index=None):

        self._commit_sha.clear()
        self._reset_parent()

        if self._review_note is not None:
            self._on_save_review()
            self._review_note = None
        self._review_comment.clear()

        if index is not None:
            index = self._application.log_table_filter.mapToSource(index)
            index = index.row()
        else:
            index = 0

        # Diff a=old b=new commit

        if index:
            self._current_revision = index
            # log_table_model = self._log_table.model()
            log_table_model = self._application.log_table_model
            self._current_commit = log_table_model[index]

            sha = self._current_commit.hex
            self._commit_sha.setText('Commit: {}   /   {}'.format(
                sha[:self.SHA_SHORTCUT_LENGTH], sha))
            if len(self._current_commit.parents) > len(self._parent_labels):
                self.show_message('Fixme: More than 2 parents')
            for commit, parent_label in zip(self._current_commit.parents,
                                            self._parent_labels):
                parent_label.setText('Parent: {}   ({})'.format(
                    commit.hex[:self.SHA_SHORTCUT_LENGTH], commit.message))
                parent_label.setCursorPosition(0)

            self._review_note = self._application.review[sha]
            if self._review_note is not None:
                self._review_comment.setText(self._review_note.text)
            else:
                self._review_note = ReviewNote(sha)
            commit_a = self._current_commit.parents[0]  # take first parent
            kwargs = dict(a=commit_a, b=self._current_commit)

        else:  # working directory
            self._current_revision = None
            if self._stagged_mode_action.isChecked():
                # Changes between the index and your last commit
                kwargs = dict(a='HEAD', cached=True)
            elif self._not_stagged_mode_action.isChecked():
                # Changes in the working tree not yet staged for the next commit
                kwargs = {}
            elif self._all_change_mode_action.isChecked():
                # Changes in the working tree since your last commit
                kwargs = dict(a='HEAD')

        self._diff_kwargs = kwargs
        self._diff = self._application.repository.diff(**kwargs)

        commit_table_model = self._commit_table.model()
        commit_table_model.update(self._diff)
        self._commit_table.resizeColumnsToContents()

    ##############################################

    def _on_clicked_table(self, index):
        # called when a commit row is clicked
        self._logger.info('')
        self._current_patch_index = index.row()
        self.reload_current_patch()

    ##############################################

    @property
    def current_patch_index(self):
        return self._current_patch_index

    ##############################################

    @property
    def number_of_patches(self):
        return len(self._diff)

    ##############################################

    def _create_diff_viewer_window(self):

        self._logger.info("Open Diff Viewer")

        from CodeReview.GUI.DiffViewer.DiffViewerMainWindow import DiffViewerMainWindow

        repository = self._application.repository
        self._diff_window = DiffViewerMainWindow(self, repository=repository)
        self._diff_window.closed.connect(self._on_diff_window_closed)
        self._diff_window.showMaximized()

    ##############################################

    def _on_diff_window_closed(self):
        self._application.unwatch_files()  # Fixme: only current patch !
        self._diff_window = None
        self._logger.info("Diff Viewer closed")

    ##############################################

    def _show_patch(self, patch):

        self._logger.info('')

        self._application.unwatch_files()

        if self._diff_window is None:
            self._create_diff_viewer_window()

        delta = patch.delta
        old_path = delta.old_file.path
        new_path = delta.new_file.path
        if not delta.is_binary:
            self._logger.info('revision {} '.format(self._current_revision) +
                              new_path)
            # print(delta.status, delta.similarity, delta.additions, delta.deletions, delta.is_binary)
            # for hunk in delta.hunks:
            #     print(hunk.old_start, hunk.old_lines, hunk.new_start, hunk.new_lines, hunk.lines)
            if delta.status in (git.GIT_DELTA_MODIFIED, git.GIT_DELTA_RENAMED):
                paths = (old_path, new_path)
            elif delta.status == git.GIT_DELTA_ADDED:
                paths = (None, new_path)
            elif delta.status == git.GIT_DELTA_DELETED:
                paths = (old_path, None)
            repository = self._application.repository
            texts = [
                repository.file_content(blob_id)
                for blob_id in (delta.old_file.id, delta.new_file.id)
            ]
            metadatas = [
                dict(path=old_path,
                     document_type='file',
                     last_modification_date=None),
                dict(path=new_path,
                     document_type='file',
                     last_modification_date=None),
            ]
            self._diff_window.diff_documents(paths,
                                             texts,
                                             metadatas,
                                             workdir=repository.workdir)
            self._application.watch(new_path)
        else:
            self._logger.info(
                'revision {} Binary '.format(self._current_revision) +
                new_path)

        # Fixme: show image pdf ...
        # Monitor file change

    ##############################################

    @property
    def _last_path_index(self):
        return len(self._diff) - 1

    ##############################################

    def _next_previous_patch(self, forward):

        if forward:
            if self._current_patch_index < self._last_path_index:
                patch_index = self._current_patch_index + 1
            else:
                patch_index = 0
        else:
            if self._current_patch_index >= 1:
                patch_index = self._current_patch_index - 1
            else:
                patch_index = self._last_path_index

        self._current_patch_index = patch_index
        patch = self._diff[patch_index]
        self._show_patch(patch)

    ##############################################

    def previous_patch(self):
        self._next_previous_patch(forward=False)

    ##############################################

    def next_patch(self):
        self._next_previous_patch(forward=True)

    ##############################################

    def reload_current_patch(self):
        if self._current_patch_index is not None:
            patch = self._diff[self._current_patch_index]
            self._show_patch(patch)

    ##############################################

    def _on_committer_filter_changed(self, text):
        log_table_filter = self._application.log_table_filter
        log_table_filter.setFilterRegExp(
            QRegExp(text, Qt.CaseInsensitive, QRegExp.FixedString))
        log_table_filter.setFilterKeyColumn(
            LogTableModel.COLUMN_ENUM.committer)
        self._on_log_table_filter_changed(
        )  # seems to just work, no need to connect signal

    ##############################################

    def _on_message_filter_changed(self, text):
        log_table_filter = self._application.log_table_filter
        log_table_filter.setFilterRegExp(
            QRegExp(text, Qt.CaseInsensitive, QRegExp.FixedString))
        log_table_filter.setFilterKeyColumn(LogTableModel.COLUMN_ENUM.message)
        self._on_log_table_filter_changed()

    ##############################################

    def _on_sha_filter_changed(self, text):
        log_table_filter = self._application.log_table_filter
        if text:
            # Fixme: ???
            # regexp = '^' + text
            regexp = text
        else:
            regexp = ''
        log_table_filter.setFilterRegExp(
            QRegExp(regexp, Qt.CaseInsensitive, QRegExp.FixedString))
        log_table_filter.setFilterKeyColumn(LogTableModel.COLUMN_ENUM.sha)
        self._on_log_table_filter_changed()

    ##############################################

    def _on_log_table_filter_changed(self):
        log_table_filter = self._application.log_table_filter
        self._row_count.setText('{} commits'.format(
            log_table_filter.rowCount()))

    ##############################################

    def _on_save_review(self):
        self._review_note.text = self._review_comment.toPlainText()
        self._application.review.add(self._review_note)
        self._application.review.save()

    ##############################################

    def _on_go_clicked(self, parent_index):
        if self._current_commit is not None:
            try:
                parent_commit_sha = str(
                    self._current_commit.parent_ids[parent_index])
                index = self._application.log_table_model.find_commit(
                    parent_commit_sha)
                if index is not None:
                    self._logger.info('Found parent commit {} {}'.format(
                        parent_index, parent_commit_sha))
                    self._log_table.selectRow(index.row())
            except IndexError:
                pass

    ##############################################

    def _on_diff_exchange(self):
        diff_a = self._diff_a.text()
        diff_b = self._diff_b.text()
        self._diff_a.setText(diff_b)
        self._diff_b.setText(diff_a)
        self._on_diff_ab()

    ##############################################

    def _on_diff_ab(self):

        diff_a = self._diff_a.text()
        diff_b = self._diff_b.text()

        if diff_a and diff_b:
            try:
                kwargs = dict(a=diff_a, b=diff_b)
                self._diff = self._application.repository.diff(**kwargs)

                commit_table_model = self._commit_table.model()
                commit_table_model.update(self._diff)
                self._commit_table.resizeColumnsToContents()
            except ValueError as e:
                self._logger.warning(e)
                self.show_message(str(e))
class DiffViewerMainWindow(MainWindowBase):

    _logger = _module_logger.getChild('DiffViewerMainWindow')

    closed = pyqtSignal()

    ##############################################

    def __init__(self, parent=None, repository=None):

        # Note: parent is None when diff viewer is the main application
        # Fixme: subclass standard and/ git diff

        super().__init__(title='CodeReview Diff Viewer', parent=parent)

        self._repository = repository
        self._staged = False # Fixme: only git

        self._current_path = None

        self._init_ui()
        self._create_actions()
        self._create_toolbar()

        icon_loader = IconLoader()
        self.setWindowIcon(icon_loader['code-review@svg'])

        self._lexer_cache = LexerCache()

        if self.is_main_window:
            self._application.directory_changed.connect(self._on_file_system_changed)
            self._application.file_changed.connect(self._on_file_system_changed)

    ##############################################

    def _init_ui(self):

        self._central_widget = QtWidgets.QWidget(self)
        self.setCentralWidget(self._central_widget)
        self._vertical_layout = QtWidgets.QVBoxLayout(self._central_widget)

        self._message_box = MessageBox(self)
        self._diff_view = DiffView()

        for widget in (self._message_box, self._diff_view):
            self._vertical_layout.addWidget(widget)

    ##############################################

    def _create_actions(self):

        icon_loader = IconLoader()

        self._previous_file_action = \
            QtWidgets.QAction(icon_loader['go-previous@svg'],
                              'Previous',
                              self,
                              toolTip='Previous file',
                              shortcut='Ctrl+P',
                              triggered=self._previous_file,
            )

        self._next_file_action = \
            QtWidgets.QAction(icon_loader['go-next@svg'],
                              'Next',
                              self,
                              toolTip='Next file',
                              shortcut='Ctrl+N',
                              triggered=self._next_file,
            )

        self._refresh_action = \
            QtWidgets.QAction(icon_loader['view-refresh@svg'],
                              'Refresh',
                              self,
                              toolTip='Refresh',
                              shortcut='Ctrl+R',
                              triggered=self._refresh,
            )

        self._line_number_action = \
            QtWidgets.QAction(icon_loader['line-number-mode@svg'],
                              'Line Number Mode',
                              self,
                              toolTip='Line Number Mode',
                              shortcut='Ctrl+N',
                              checkable=True,
                              triggered=self._set_document_models,
            )

        self._align_action = \
            QtWidgets.QAction(icon_loader['align-mode@svg'],
                              'Align Mode',
                              self,
                              toolTip='Align Mode',
                              shortcut='Ctrl+L',
                              checkable=True,
                              triggered=self._set_document_models,
            )

        self._complete_action = \
            QtWidgets.QAction(icon_loader['complete-mode@svg'],
                              'Complete Mode',
                              self,
                              toolTip='Complete Mode',
                              shortcut='Ctrl+A',
                              checkable=True,
                              triggered=self._set_document_models,
            )

        self._highlight_action = \
            QtWidgets.QAction(icon_loader['highlight@svg'],
                              'Highlight',
                              self,
                              toolTip='Highlight text',
                              shortcut='Ctrl+H',
                              checkable=True,
                              triggered=self._refresh,
            )

        # Fixme: only git
        if self._repository:
            self._stage_action = \
                                 QtWidgets.QAction('Stage',
                                                   self,
                                                   toolTip='Stage file',
                                                   shortcut='Ctrl+S',
                                                   checkable=True,
                                                   triggered=self._stage,
                                 )
        else:
            self._stage_action = None

    ##############################################

    def _create_toolbar(self):

        self._algorithm_combobox = QtWidgets.QComboBox(self)
        for algorithm in ('Patience',):
            self._algorithm_combobox.addItem(algorithm, algorithm)
        self._algorithm_combobox.currentIndexChanged.connect(self._refresh)

        self._lines_of_context_combobox = QtWidgets.QComboBox(self)
        for number_of_lines_of_context in (3, 6, 12):
            self._lines_of_context_combobox.addItem(str(number_of_lines_of_context),
                                                    number_of_lines_of_context)
        self._lines_of_context_combobox.currentIndexChanged.connect(self._refresh)

        self._font_size_combobox = QtWidgets.QComboBox(self)
        min_font_size, max_font_size = 4, 20
        application_font_size = QtWidgets.QApplication.font().pointSize()
        min_font_size = min(min_font_size, application_font_size)
        max_font_size = max(max_font_size, application_font_size)
        for font_size in range(min_font_size, max_font_size +1):
            self._font_size_combobox.addItem(str(font_size), font_size)
        self._font_size_combobox.setCurrentIndex(application_font_size - min_font_size)
        self._font_size_combobox.currentIndexChanged.connect(self._on_font_size_change)
        self._on_font_size_change(refresh=False)

        if self._repository:
            self._status_label = QtWidgets.QLabel('Status: ???')
        else:
            self._status_label = None

        items = [
            self._algorithm_combobox,
            QtWidgets.QLabel(' '), # Fixme
            QtWidgets.QLabel('Context:'),
            self._lines_of_context_combobox,
            QtWidgets.QLabel(' '), # Fixme
            QtWidgets.QLabel('Font Size:'),
            self._font_size_combobox,
            self._line_number_action,
            self._align_action,
            self._complete_action,
            self._highlight_action,
            self._refresh_action,
            self._stage_action,
            self._status_label,
        ]
        if self._application._main_window is not None:
            self._patch_index_label = QtWidgets.QLabel()
            self.update_patch_index()
            items.extend((
                self._previous_file_action,
                self._patch_index_label,
                self._next_file_action,
            ))

        self._tool_bar = self.addToolBar('Diff Viewer')
        for item in items:
            if item is not None: # for self._stage_action
                if isinstance(item, QtWidgets.QAction):
                    self._tool_bar.addAction(item)
                else:
                    self._tool_bar.addWidget(item)

    ##############################################

    def init_menu(self):

        super().init_menu()

    ##############################################

    @property
    def is_main_window(self):
        return self.parent() is None
        # return self._application.main_window is self

    @property
    def is_not_main_window(self):
        return self.parent() is not None
        # return self._application.main_window is self

    ##############################################

    def closeEvent(self, event):

        # Fixme: else the window is reopened ???
        if self.is_main_window:
            self._application.directory_changed.disconnect(self._on_file_system_changed)
            self._application.file_changed.disconnect(self._on_file_system_changed)
        else:
            self.closed.emit()

        super().closeEvent(event)

    ##############################################

    def show_message(self, message=None, timeout=0, warn=False):

        """ Hides the normal status indications and displays the given message for the specified
        number of milli-seconds (timeout). If timeout is 0 (default), the message remains displayed
        until the clearMessage() slot is called or until the showMessage() slot is called again to
        change the message.

        Note that showMessage() is called to show temporary explanations of tool tip texts, so
        passing a timeout of 0 is not sufficient to display a permanent message.
        """

        if warn:
            self._message_box.push_message(message)
        else:
            status_bar = self.statusBar()
            if message is None:
                status_bar.clearMessage()
            else:
                status_bar.showMessage(message, timeout)

    ################################################

    def open_files(self, file1, file2, show=False):

        # Fixme: Actually it only supports file diff

        paths = (file1, file2)
        texts = (None, None)
        metadatas = [dict(path=file1, document_type='file', last_modification_date=None),
                     dict(path=file2, document_type='file', last_modification_date=None)]
        self.diff_documents(paths, texts, metadatas, show=show)

    ##############################################

    def _absolute_path(self, path):

        return os.path.join(self._workdir, path)

    ##############################################

    def _read_file(self, path):

        with open(self._absolute_path(path)) as f:
            text = f.read()
        return text

    ##############################################

    def _is_directory(self, path):

        if path is None:
            return False
        else:
            return os.path.isdir(self._absolute_path(path))

    ##############################################

    def _get_lexer(self, path, text):

        if path is None:
            return None

        return self._lexer_cache.guess(path, text)

    ##############################################

    def diff_documents(self, paths, texts, metadatas=None, workdir='', show=False):

        self._paths = list(paths)
        self._texts = list(texts)
        self._metadatas = metadatas
        self._workdir = workdir

        file1, file2 = self._paths
        if self._is_directory(file1) or self._is_directory(file2):
            self._highlighted_documents = [TextDocumentModel(metadata) for metadata in self._metadatas]
        else:
            self.diff_text_documents(show)

        self._set_document_models()

        if self._repository:
            self._staged = self._repository.is_staged(file2)
            self._status_label.clear()
            if self._repository.is_modified(file2):
                self._status_label.setText('<font color="red">Modified !</font>')
            if self._staged:
                self._logger.info("File {} is staged".format(file2))
            self._stage_action.blockSignals(True)
            self._stage_action.setChecked(self._staged)
            self._stage_action.blockSignals(False)

        # Fixme:
        # Useless if application is LogBrowserApplication
        # file_system_watcher = self._application.file_system_watcher
        # files = file_system_watcher.files()
        # if files:
        #     file_system_watcher.removePaths(files)
        # self._logger.info("Monitor {}".format(file2))
        # file_system_watcher.addPath(file2)

    ##############################################

    def diff_text_documents(self, show=False):

        OLD, NEW = list(range(2))
        for i in (OLD, NEW):
            if self._paths[i] is None:
                self._texts[i] = ''
            elif self._texts[i] is None:
                self._texts[i] = self._read_file(self._paths[i])
        lexers = [self._get_lexer(path, text) for path, text in zip(self._paths, self._texts)]
        raw_text_documents = [RawTextDocument(text) for text in self._texts]

        highlight = self._highlight_action.isChecked()
        number_of_lines_of_context = self._lines_of_context_combobox.currentData()

        self._highlighted_documents = []
        if not show:
            file_diff = TwoWayFileDiffFactory().process(* raw_text_documents,
                                                        number_of_lines_of_context=number_of_lines_of_context)
            document_models = TextDocumentDiffModelFactory().process(file_diff)
            for raw_text_document, document_model, lexer in zip(raw_text_documents, document_models, lexers):
                if lexer is not None and highlight:
                    highlighted_text = HighlightedText(raw_text_document, lexer)
                    highlighted_document = highlight_document(document_model, highlighted_text)
                else:
                    highlighted_document = document_model
                self._highlighted_documents.append(highlighted_document)
        else: # Only show the document
            # Fixme: broken, chunk_type is ???
            # self._diff_view.set_document_models(self._highlighted_documents, complete_mode)
            # File "/home/gv/fabrice/unison-osiris/git-python/CodeReview/DiffWidget.py", line 333, in set_document_models
            # cursor.begin_block(side, text_block.frame_type)
            # File "/home/gv/fabrice/unison-osiris/git-python/CodeReview/DiffWidget.py", line 99, in begin_block
            # if ((side == LEFT and frame_type == chunk_type.insert) or
            # File "/home/gv/fabrice/unison-osiris/git-python/CodeReview/Tools/EnumFactory.py", line 107, in __eq__
            # return self._value == int(other)
            # TypeError: int() argument must be a string or a number, not 'NoneType'
            for raw_text_document, lexer in zip(raw_text_documents, self._lexers):
                highlighted_document = highlight_text(raw_text_document, lexer)
                self._highlighted_documents.append(highlighted_document)

        if self._metadatas is not None:
            for highlighted_document, metadata in zip(self._highlighted_documents, self._metadatas):
                highlighted_document.metadata = metadata

    ################################################

    def _set_document_models(self):

        aligned_mode = self._align_action.isChecked()
        complete_mode = self._complete_action.isChecked()
        line_number_mode = self._line_number_action.isChecked()
        # Fixme: right way ?
        self._diff_view.set_document_models(self._highlighted_documents,
                                            aligned_mode, complete_mode, line_number_mode)

    ##############################################

    def _on_font_size_change(self, index=None, refresh=True):

        self._diff_view.set_font(self._font_size_combobox.currentData())
        if refresh:
            self._refresh() # Fixme: block position are not updated

    ##############################################

    def _on_file_system_changed(self, path):

        # only used for main window

        self._logger.info(path)
        self._refresh()

    ##############################################

    def _refresh(self):

        if self.is_main_window:
            # Fixme: better way ???
            texts = (None, None) # to force to reread files
            self.diff_documents(self._paths, texts, self._metadatas, self._workdir) # Fixme: show ???
        else:
            main_window = self.parent()
            main_window.reload_current_patch()

    ##############################################

    def update_patch_index(self):

        main_window = self.parent()
        self._patch_index_label.setText('{}/{}'.format(
            main_window.current_patch_index +1,
            main_window.number_of_patches))

    ##############################################

    def _previous_next_file(self, forward):

        # Fixme: only for Git
        if self.is_not_main_window:
            main_window = self.parent()
            if forward:
                main_window.next_patch()
            else:
                main_window.previous_patch()
            self.update_patch_index()
        # else: directory diff is not implemented

    ##############################################

    def _previous_file(self):
        self._previous_next_file(False)

    ##############################################

    def _next_file(self):
        self._previous_next_file(True)

    ##############################################

    def _stage(self):

        # Fixme: only for Git

        file_path = self._paths[1]
        if self._staged:
            self._repository.unstage(file_path)
            action = 'Unstage'
        else:
            self._repository.stage(file_path)
            action = 'Stage'
        self._logger.info("{} {}".format(action, file_path))
        self._staged = not self._staged
class DiffViewerMainWindow(MainWindowBase):

    _logger = _module_logger.getChild('DiffViewerMainWindow')

    closed = pyqtSignal()

    ##############################################

    def __init__(self, parent=None):

        super(DiffViewerMainWindow, self).__init__(title='CodeReview Diff Viewer', parent=parent)

        self._current_path = None
        self._init_ui()
        self._create_actions()
        self._create_toolbar()

        icon_loader = IconLoader()
        self.setWindowIcon(icon_loader['code-review@svg'])

    ##############################################

    def _init_ui(self):

        self._central_widget = QtWidgets.QWidget(self)
        self.setCentralWidget(self._central_widget)
        self._vertical_layout = QtWidgets.QVBoxLayout(self._central_widget)

        self._message_box = MessageBox(self)
        self._diff_view = DiffView()

        for widget in (self._message_box, self._diff_view):
            self._vertical_layout.addWidget(widget)

    ##############################################

    def _create_actions(self):

        icon_loader = IconLoader()

        self._previous_file_action = \
            QtWidgets.QAction(icon_loader['go-previous'],
                              'Previous',
                              self,
                              toolTip='Previous file',
                              shortcut='Ctrl+P',
                              triggered=self._previous_file,
            )

        self._next_file_action = \
            QtWidgets.QAction(icon_loader['go-next'],
                              'Next',
                              self,
                              toolTip='Next file',
                              shortcut='Ctrl+N',
                              triggered=self._next_file,
            )

        self._refresh_action = \
            QtWidgets.QAction(icon_loader['view-refresh'],
                              'Refresh',
                              self,
                              toolTip='Refresh',
                              shortcut='Ctrl+R',
                              triggered=self._refresh,
            )

        self._line_number_action = \
            QtWidgets.QAction(icon_loader['line-number-mode@svg'],
                              'Line Number Mode',
                              self,
                              toolTip='Line Number Mode',
                              shortcut='Ctrl+N',
                              checkable=True,
                              triggered=self._set_document_models,
            )

        self._align_action = \
            QtWidgets.QAction(icon_loader['align-mode@svg'],
                              'Align Mode',
                              self,
                              toolTip='Align Mode',
                              shortcut='Ctrl+L',
                              checkable=True,
                              triggered=self._set_document_models,
            )

        self._complete_action = \
            QtWidgets.QAction(icon_loader['complete-mode@svg'],
                              'Complete Mode',
                              self,
                              toolTip='Complete Mode',
                              shortcut='Ctrl+A',
                              checkable=True,
                              triggered=self._set_document_models,
            )

        self._highlight_action = \
            QtWidgets.QAction(icon_loader['highlight@svg'],
                              'Highlight',
                              self,
                              toolTip='Highlight text',
                              shortcut='Ctrl+H',
                              checkable=True,
                              triggered=self._refresh,
            )

    ##############################################

    def _create_toolbar(self):

        self._algorithm_combobox = QtWidgets.QComboBox(self)
        for algorithm in ('Patience',):
            self._algorithm_combobox.addItem(algorithm, algorithm)
        self._algorithm_combobox.currentIndexChanged.connect(self._refresh)

        self._lines_of_context_combobox = QtWidgets.QComboBox(self)
        for number_of_lines_of_context in (3, 6, 12):
            self._lines_of_context_combobox.addItem(str(number_of_lines_of_context),
                                                    number_of_lines_of_context)
        self._lines_of_context_combobox.currentIndexChanged.connect(self._refresh)

        self._font_size_combobox = QtWidgets.QComboBox(self)
        min_font_size, max_font_size = 4, 20
        application_font_size = QtWidgets.QApplication.font().pointSize()
        min_font_size = min(min_font_size, application_font_size)
        max_font_size = max(max_font_size, application_font_size)
        for font_size in range(min_font_size, max_font_size +1):
            self._font_size_combobox.addItem(str(font_size), font_size)
        self._font_size_combobox.setCurrentIndex(application_font_size - min_font_size)
        self._font_size_combobox.currentIndexChanged.connect(self._on_font_size_change)
        self._on_font_size_change(refresh=False)

        items = [
            self._algorithm_combobox,
            QtWidgets.QLabel(' '), # Fixme
            QtWidgets.QLabel('Context:'),
            self._lines_of_context_combobox,
            QtWidgets.QLabel(' '), # Fixme
            QtWidgets.QLabel('Font Size:'),
            self._font_size_combobox,
            self._line_number_action,
            self._align_action,
            self._complete_action,
            self._highlight_action,
            self._refresh_action,
        ]
        if self._application._main_window is not None:
            self._patch_index_label = QtWidgets.QLabel()
            self._update_patch_index()
            items.extend((
                self._previous_file_action,
                self._patch_index_label,
                self._next_file_action,
            ))

        self._tool_bar = self.addToolBar('Diff Viewer')
        for item in items:
            if isinstance(item, QtWidgets.QAction):
                self._tool_bar.addAction(item)
            else:
                self._tool_bar.addWidget(item)

    ##############################################

    def init_menu(self):

        super(DiffViewerMainWindow, self).init_menu()

    ##############################################

    def closeEvent(self, event):

        if self._application.main_window is not self:
            self.closed.emit()

        super(DiffViewerMainWindow, self).closeEvent(event)

    ##############################################

    def show_message(self, message=None, timeout=0, warn=False):

        """ Hides the normal status indications and displays the given message for the specified
        number of milli-seconds (timeout). If timeout is 0 (default), the message remains displayed
        until the clearMessage() slot is called or until the showMessage() slot is called again to
        change the message.

        Note that showMessage() is called to show temporary explanations of tool tip texts, so
        passing a timeout of 0 is not sufficient to display a permanent message.
        """

        if warn:
            self._message_box.push_message(message)
        else:
            status_bar = self.statusBar()
            if message is None:
                status_bar.clearMessage()
            else:
                status_bar.showMessage(message, timeout)

    ################################################

    def open_files(self, file1, file2, show=False):

        paths = (file1, file2)
        texts = (None, None)
        metadatas = [dict(path=file1, document_type='file', last_modification_date=None),
                     dict(path=file2, document_type='file', last_modification_date=None)]
        self.diff_documents(paths, texts, metadatas, show=show)

    ##############################################

    def _absolute_path(self, path):

        return os.path.join(self._workdir, path)

    ##############################################

    def _read_file(self, path):

        with open(self._absolute_path(path)) as f:
            text = f.read()
        return text

    ##############################################

    def _is_directory(self, path):

        if path is None:
            return False
        else:
            return os.path.isdir(self._absolute_path(path))

    ##############################################

    @staticmethod
    def _get_lexer(path, text):

        if path is None:
            return None

        try:
            # get_lexer_for_filename(filename)
            return pygments_lexers.guess_lexer_for_filename(path, text, stripnl=False)
        except pygments_lexers.ClassNotFound:
            try:
                return pygments_lexers.guess_lexer(text, stripnl=False)
            except pygments_lexers.ClassNotFound:
                return None

    ##############################################

    def diff_documents(self, paths, texts, metadatas=None, workdir='', show=False):

        self._paths = list(paths)
        self._texts = list(texts)
        self._metadatas = metadatas
        self._workdir = workdir

        file1, file2 = self._paths
        if self._is_directory(file1) or self._is_directory(file2):
            self._highlighted_documents = [TextDocumentModel(metadata) for metadata in self._metadatas]
        else:
            self.diff_text_documents(show)

        self._set_document_models()

    ##############################################

    def diff_text_documents(self, show=False):

        OLD, NEW = list(range(2))
        for i in (OLD, NEW):
            if self._paths[i] is None:
                self._texts[i] = ''
            elif self._texts[i] is None:
                self._texts[i] = self._read_file(self._paths[i])
        lexers = [self._get_lexer(path, text) for path, text in zip(self._paths, self._texts)]
        raw_text_documents = [RawTextDocument(text) for text in self._texts]

        highlight = self._highlight_action.isChecked()
        number_of_lines_of_context = self._lines_of_context_combobox.currentData()

        self._highlighted_documents = []
        if not show:
            file_diff = TwoWayFileDiffFactory().process(* raw_text_documents,
                                                        number_of_lines_of_context=number_of_lines_of_context)
            document_models = TextDocumentDiffModelFactory().process(file_diff)
            for raw_text_document, document_model, lexer in zip(raw_text_documents, document_models, lexers):
                if lexer is not None and highlight:
                    highlighted_text = HighlightedText(raw_text_document, lexer)
                    highlighted_document = highlight_document(document_model, highlighted_text)
                else:
                    highlighted_document = document_model
                self._highlighted_documents.append(highlighted_document)
        else: # Only show the document
            # Fixme: broken, chunk_type is ???
            # self._diff_view.set_document_models(self._highlighted_documents, complete_mode)
            # File "/home/gv/fabrice/unison-osiris/git-python/CodeReview/DiffWidget.py", line 333, in set_document_models
            # cursor.begin_block(side, text_block.frame_type)
            # File "/home/gv/fabrice/unison-osiris/git-python/CodeReview/DiffWidget.py", line 99, in begin_block
            # if ((side == LEFT and frame_type == chunk_type.insert) or
            # File "/home/gv/fabrice/unison-osiris/git-python/CodeReview/Tools/EnumFactory.py", line 107, in __eq__
            # return self._value == int(other)
            # TypeError: int() argument must be a string or a number, not 'NoneType'
            for raw_text_document, lexer in zip(raw_text_documents, self._lexers):
                highlighted_document = highlight_text(raw_text_document, lexer)
                self._highlighted_documents.append(highlighted_document)

        if self._metadatas is not None:
            for highlighted_document, metadata in zip(self._highlighted_documents, self._metadatas):
                highlighted_document.metadata = metadata

    ################################################

    def _set_document_models(self):

        aligned_mode = self._align_action.isChecked()
        complete_mode = self._complete_action.isChecked()
        line_number_mode = self._line_number_action.isChecked()
        # Fixme: right way ?
        self._diff_view.set_document_models(self._highlighted_documents,
                                            aligned_mode, complete_mode, line_number_mode)

    ##############################################

    def _on_font_size_change(self, index=None, refresh=True):

        self._diff_view.set_font(self._font_size_combobox.currentData())
        if refresh:
            self._refresh() # Fixme: block position are not updated

    ##############################################

    def _refresh(self):

        main_window = self.parent()
        if main_window is not None:
            main_window.reload_current_patch()
        else:
            # Fixme: better way ???
            texts = (None, None) # to force to reread files
            self.diff_documents(self._paths, texts, self._metadatas, self._workdir) # Fixme: show ???

    ##############################################

    def _update_patch_index(self):

        main_window = self.parent()
        self._patch_index_label.setText('{}/{}'.format(main_window.current_patch +1,
                                                       main_window.number_of_patches))

    ##############################################

    def _previous_file(self):

        main_window = self.parent()
        if main_window is not None:
            main_window.previous_patch()
            self._update_patch_index()

    ##############################################

    def _next_file(self):

        main_window = self.parent()
        if main_window is not None:
            main_window.next_patch()
            self._update_patch_index()
class LogBrowserMainWindow(MainWindowBase):

    _logger = _module_logger.getChild('LogBrowserMainWindow')

    ##############################################

    def __init__(self, parent=None):

        super(LogBrowserMainWindow,
              self).__init__(title='CodeReview Log Browser', parent=parent)

        icon_loader = IconLoader()
        self.setWindowIcon(icon_loader['code-review@svg'])

        self._current_revision = None
        self._diff = None
        self._current_patch = None
        self._diff_window = None

        self._init_ui()
        self._create_actions()
        self._create_toolbar()

        self._application.file_system_changed.connect(
            self._on_file_system_changed)

    ##############################################

    def _init_ui(self):

        # Table models are set in application

        self._central_widget = QtWidgets.QWidget(self)
        self.setCentralWidget(self._central_widget)

        self._vertical_layout = QtWidgets.QVBoxLayout(self._central_widget)
        self._message_box = MessageBox(self)
        splitter = QtWidgets.QSplitter()
        splitter.setOrientation(Qt.Vertical)
        self._log_table = QtWidgets.QTableView()
        self._commit_table = QtWidgets.QTableView()

        for widget in (self._message_box, splitter):
            self._vertical_layout.addWidget(widget)
        for widget in (self._log_table, self._commit_table):
            splitter.addWidget(widget)

        table = self._log_table
        table.setSelectionMode(QtWidgets.QTableView.SingleSelection)
        table.setSelectionBehavior(QtWidgets.QTableView.SelectRows)
        table.verticalHeader().setVisible(False)
        table.setShowGrid(False)
        # table.setSortingEnabled(True)
        table.clicked.connect(self._update_commit_table)

        table = self._commit_table
        table.setSelectionMode(QtWidgets.QTableView.SingleSelection)
        table.setSelectionBehavior(QtWidgets.QTableView.SelectRows)
        table.verticalHeader().setVisible(False)
        table.setShowGrid(False)
        table.setSortingEnabled(True)
        table.clicked.connect(self._show_patch)

        # horizontal_header = table_view.horizontalHeader()
        # horizontal_header.setMovable(True)

    ##############################################

    def _create_actions(self):

        icon_loader = IconLoader()

        self._stagged_mode_action = \
            QtWidgets.QAction('Stagged',
                              self,
                              toolTip='Stagged Mode',
                              shortcut='Ctrl+1',
                              checkable=True,
            )

        self._not_stagged_mode_action = \
            QtWidgets.QAction('Not Stagged',
                              self,
                              toolTip='Not Stagged Mode',
                              shortcut='Ctrl+2',
                              checkable=True,
            )

        self._all_change_mode_action = \
            QtWidgets.QAction('All',
                              self,
                              toolTip='All Mode',
                              shortcut='Ctrl+3',
                              checkable=True,
            )

        self._action_group = QtWidgets.QActionGroup(self)
        self._action_group.triggered.connect(self._update_working_tree_diff)
        for action in (
                self._all_change_mode_action,
                self._stagged_mode_action,
                self._not_stagged_mode_action,
        ):
            self._action_group.addAction(action)
        self._all_change_mode_action.setChecked(True)

        self._reload_action = \
            QtWidgets.QAction(icon_loader['view-refresh@svg'],
                              'Refresh',
                              self,
                              toolTip='Refresh',
                              shortcut='Ctrl+R',
                              triggered=self._reload_repository,
            )

    ##############################################

    def _create_toolbar(self):

        self._tool_bar = self.addToolBar('Diff on Working Tree')
        for item in self._action_group.actions():
            self._tool_bar.addAction(item)
        for item in (self._reload_action, ):
            self._tool_bar.addAction(item)

    ##############################################

    def init_menu(self):

        super(LogBrowserMainWindow, self).init_menu()

    ##############################################

    def show_message(self, message=None, timeout=0, warn=False):
        """ Hides the normal status indications and displays the given message for the specified
        number of milli-seconds (timeout). If timeout is 0 (default), the message remains displayed
        until the clearMessage() slot is called or until the showMessage() slot is called again to
        change the message.

        Note that showMessage() is called to show temporary explanations of tool tip texts, so
        passing a timeout of 0 is not sufficient to display a permanent message.
        """

        if warn:
            self._message_box.push_message(message)
        else:
            status_bar = self.statusBar()
            if message is None:
                status_bar.clearMessage()
            else:
                status_bar.showMessage(message, timeout)

    ##############################################

    def _on_file_system_changed(self, path):

        self._logger.info('File system changed {}'.format(path))
        self._reload_repository()

    ##############################################

    def _reload_repository(self):

        self._logger.info('Reload signal')
        index = self._log_table.currentIndex()
        self._application.reload_repository()
        if index.row() != -1:
            self._logger.info("Index is {}".format(index.row()))
            self._log_table.setCurrentIndex(index)
            # Fixme: ???
            # self._update_working_tree_diff()
            self.show_working_tree_diff()
        else:
            self.show_working_tree_diff()

    ##############################################

    def show_working_tree_diff(self):

        self._logger.info('Show WT')
        log_model = self._log_table.model()
        if log_model.rowCount():
            top_index = log_model.index(0, 0)
            self._log_table.setCurrentIndex(top_index)
            self._update_working_tree_diff()

    ##############################################

    def _update_working_tree_diff(self):

        # Check log table is on working tree
        if self._log_table.currentIndex().row() == 0:
            self._update_commit_table()

    ##############################################

    def _update_commit_table(self, index=None):

        if index is not None:
            index = index.row()
        else:
            index = 0

        if index:
            self._current_revision = index
            log_table_model = self._log_table.model()
            commit1 = log_table_model[index]
            try:
                commit2 = log_table_model[index + 1]
                kwargs = dict(a=commit2, b=commit1)  # Fixme:
            except IndexError:
                kwargs = dict(a=commit1)
        else:  # working directory
            self._current_revision = None
            if self._stagged_mode_action.isChecked():
                # Changes between the index and your last commit
                kwargs = dict(a='HEAD', cached=True)
            elif self._not_stagged_mode_action.isChecked():
                # Changes in the working tree not yet staged for the next commit
                kwargs = {}
            elif self._all_change_mode_action.isChecked():
                # Changes in the working tree since your last commit
                kwargs = dict(a='HEAD')

        self._diff = self._application.repository.diff(**kwargs)

        commit_table_model = self._commit_table.model()
        commit_table_model.update(self._diff)
        self._commit_table.resizeColumnsToContents()

    ##############################################

    def _show_patch(self, index):

        self._current_patch = index.row()
        self._show_current_patch()

    ##############################################

    def _on_diff_window_closed(self):

        self._diff_window = None

    ##############################################

    @property
    def current_patch(self):
        return self._current_patch

    ##############################################

    @property
    def number_of_patches(self):
        return len(self._diff)

    ##############################################

    def _show_current_patch(self):

        repository = self._application.repository

        if self._diff_window is None:
            from CodeReview.GUI.DiffViewer.DiffViewerMainWindow import DiffViewerMainWindow
            self._diff_window = DiffViewerMainWindow(self,
                                                     repository=repository)
            self._diff_window.closed.connect(self._on_diff_window_closed)
            self._diff_window.showMaximized()

        patch = self._diff[self._current_patch]
        delta = patch.delta
        if not delta.is_binary:
            self._logger.info('revision {} '.format(self._current_revision) +
                              delta.new_file.path)
            # print(delta.status, delta.similarity, delta.additions, delta.deletions, delta.is_binary)
            # for hunk in delta.hunks:
            #     print(hunk.old_start, hunk.old_lines, hunk.new_start, hunk.new_lines, hunk.lines)
            if delta.status in (git.GIT_DELTA_MODIFIED, git.GIT_DELTA_RENAMED):
                paths = (delta.old_file.path, delta.new_file.path)
            elif delta.status == git.GIT_DELTA_ADDED:
                paths = (None, delta.new_file.path)
            elif delta.status == git.GIT_DELTA_DELETED:
                paths = (delta.old_file.path, None)
            texts = [
                repository.file_content(blob_id)
                for blob_id in (delta.old_file.id, delta.new_file.id)
            ]
            metadatas = [
                dict(path=delta.old_file.path,
                     document_type='file',
                     last_modification_date=None),
                dict(path=delta.new_file.path,
                     document_type='file',
                     last_modification_date=None)
            ]
            self._diff_window.diff_documents(paths,
                                             texts,
                                             metadatas,
                                             workdir=repository.workdir)
        else:
            self._logger.info(
                'revision {} Binary '.format(self._current_revision) +
                delta.new_file.path)
        # Fixme: show image pdf ...

    ##############################################

    @property
    def _last_path_index(self):

        return len(self._diff) - 1

    ##############################################

    def previous_patch(self):

        if self._current_patch >= 1:
            self._current_patch -= 1
        else:
            self._current_patch = self._last_path_index
        self._show_current_patch()

    ##############################################

    def next_patch(self):

        if self._current_patch < self._last_path_index:
            self._current_patch += 1
        else:
            self._current_patch = 0
        self._show_current_patch()

    ##############################################

    def reload_current_patch(self):

        self._show_current_patch()
class LogBrowserMainWindow(MainWindowBase):

    _logger = _module_logger.getChild('LogBrowserMainWindow')

    ##############################################

    def __init__(self, parent=None):

        super(LogBrowserMainWindow, self).__init__(title='CodeReview Log Browser', parent=parent)

        icon_loader = IconLoader()
        self.setWindowIcon(icon_loader['code-review@svg'])

        self._current_revision = None
        self._diff = None
        self._current_patch_index = None
        self._diff_window = None

        self._init_ui()
        self._create_actions()
        self._create_toolbar()

        self._application.directory_changed.connect(self._on_directory_changed)
        self._application.file_changed.connect(self._on_file_changed)

    ##############################################

    def _init_ui(self):

        # Table models are set in application

        self._central_widget = QtWidgets.QWidget(self)
        self.setCentralWidget(self._central_widget)

        self._vertical_layout = QtWidgets.QVBoxLayout(self._central_widget)
        self._message_box = MessageBox(self)
        splitter = QtWidgets.QSplitter()
        splitter.setOrientation(Qt.Vertical)
        self._log_table = QtWidgets.QTableView()
        self._commit_table = QtWidgets.QTableView()

        for widget in (self._message_box, splitter):
            self._vertical_layout.addWidget(widget)
        for widget in (self._log_table, self._commit_table):
            splitter.addWidget(widget)

        table = self._log_table
        table.setSelectionMode(QtWidgets.QTableView.SingleSelection)
        table.setSelectionBehavior(QtWidgets.QTableView.SelectRows)
        table.verticalHeader().setVisible(False)
        table.setShowGrid(False)
        # table.setSortingEnabled(True)
        table.clicked.connect(self._update_commit_table)

        table = self._commit_table
        table.setSelectionMode(QtWidgets.QTableView.SingleSelection)
        table.setSelectionBehavior(QtWidgets.QTableView.SelectRows)
        table.verticalHeader().setVisible(False)
        table.setShowGrid(False)
        table.setSortingEnabled(True)
        table.clicked.connect(self._on_clicked_table)

        # horizontal_header = table_view.horizontalHeader()
        # horizontal_header.setMovable(True)

    ##############################################

    def finish_table_connections(self):

        self._log_table.selectionModel().currentRowChanged.connect(self._update_commit_table)
        #!# Fixme: reopen diff viewer window when repository change
        #!# self._commit_table.selectionModel().currentRowChanged.connect(self._on_clicked_table)

    ##############################################

    def _create_actions(self):

        icon_loader = IconLoader()

        self._stagged_mode_action = \
            QtWidgets.QAction('Stagged',
                              self,
                              toolTip='Stagged Mode',
                              shortcut='Ctrl+1',
                              checkable=True,
            )

        self._not_stagged_mode_action = \
            QtWidgets.QAction('Not Stagged',
                              self,
                              toolTip='Not Stagged Mode',
                              shortcut='Ctrl+2',
                              checkable=True,
            )

        self._all_change_mode_action = \
            QtWidgets.QAction('All',
                              self,
                              toolTip='All Mode',
                              shortcut='Ctrl+3',
                              checkable=True,
            )

        self._action_group = QtWidgets.QActionGroup(self)
        self._action_group.triggered.connect(self._update_working_tree_diff)
        for action in (self._all_change_mode_action,
                       self._stagged_mode_action,
                       self._not_stagged_mode_action,
                       ):
            self._action_group.addAction(action)
        self._all_change_mode_action.setChecked(True)

        self._reload_action = \
            QtWidgets.QAction(icon_loader['view-refresh@svg'],
                              'Refresh',
                              self,
                              toolTip='Refresh',
                              shortcut='Ctrl+R',
                              triggered=self._reload_repository,
            )

    ##############################################

    def _create_toolbar(self):

        self._tool_bar = self.addToolBar('Diff on Working Tree')
        for item in self._action_group.actions():
            self._tool_bar.addAction(item)
        for item in (self._reload_action,):
            self._tool_bar.addAction(item)

    ##############################################

    def init_menu(self):

        super(LogBrowserMainWindow, self).init_menu()

    ##############################################

    def show_message(self, message=None, timeout=0, warn=False):

        """ Hides the normal status indications and displays the given message for the specified
        number of milli-seconds (timeout). If timeout is 0 (default), the message remains displayed
        until the clearMessage() slot is called or until the showMessage() slot is called again to
        change the message.

        Note that showMessage() is called to show temporary explanations of tool tip texts, so
        passing a timeout of 0 is not sufficient to display a permanent message.
        """

        if warn:
            self._message_box.push_message(message)
        else:
            status_bar = self.statusBar()
            if message is None:
                status_bar.clearMessage()
            else:
                status_bar.showMessage(message, timeout)

    ##############################################

    def _on_directory_changed(self, path):

        self._logger.info(path)

        self._reload_repository()
        self._diff = self._application.repository.diff(**self._diff_kwargs)

        if self._diff_window is not None:
            if self.number_of_patches:
                self._current_patch_index = 0
                self._diff_window.update_patch_index()
                self.reload_current_patch()
            else:
                self._diff_window.close()

    ##############################################

    def _on_file_changed(self, path):

        self._logger.info(path)

        repository = self._application.repository
        if path == repository.join_repository_path(repository.INDEX_PATH):
            self._diff = self._application.repository.diff(**self._diff_kwargs)
        else:
            message = 'File {} changed'.format(path)
            self.show_message(message)
            self.reload_current_patch()

    ##############################################

    def _reload_repository(self):

        self._logger.info('Reload signal')
        index = self._log_table.currentIndex()
        self._application.reload_repository()
        if index.row() != -1:
            self._logger.info("Index is {}".format(index.row()))
            self._log_table.setCurrentIndex(index)
            # Fixme: ???
            # self._update_working_tree_diff()
            self.show_working_tree_diff()
        else:
            self.show_working_tree_diff()

    ##############################################

    def show_working_tree_diff(self):

        self._logger.info('Show WT')
        log_model = self._log_table.model()
        if log_model.rowCount():
            top_index = log_model.index(0, 0)
            self._log_table.setCurrentIndex(top_index)
            self._update_working_tree_diff()

    ##############################################

    def _update_working_tree_diff(self):

        # Check log table is on working tree
        if self._log_table.currentIndex().row() == 0:
            self._update_commit_table()

    ##############################################

    def _update_commit_table(self, index=None):

        if index is not None:
            index = index.row()
        else:
            index = 0

        if index:
            self._current_revision = index
            log_table_model = self._log_table.model()
            commit1 = log_table_model[index]
            try:
                commit2 = log_table_model[index +1]
                kwargs = dict(a=commit2, b=commit1) # Fixme:
            except IndexError:
                kwargs = dict(a=commit1)
        else: # working directory
            self._current_revision = None
            if self._stagged_mode_action.isChecked():
                # Changes between the index and your last commit
                kwargs = dict(a='HEAD', cached=True)
            elif self._not_stagged_mode_action.isChecked():
                # Changes in the working tree not yet staged for the next commit
                kwargs = {}
            elif self._all_change_mode_action.isChecked():
                # Changes in the working tree since your last commit
                kwargs = dict(a='HEAD')

        self._diff_kwargs = kwargs
        self._diff = self._application.repository.diff(**kwargs)

        commit_table_model = self._commit_table.model()
        commit_table_model.update(self._diff)
        self._commit_table.resizeColumnsToContents()

    ##############################################

    def _on_clicked_table(self, index):

        # called when a commit row is clicked
        self._logger.info('')
        self._current_patch_index = index.row()
        self.reload_current_patch()

    ##############################################

    @property
    def current_patch_index(self):
        return self._current_patch_index

    ##############################################

    @property
    def number_of_patches(self):
        return len(self._diff)

    ##############################################

    def _create_diff_viewer_window(self):

        self._logger.info("Open Diff Viewer")

        from CodeReview.GUI.DiffViewer.DiffViewerMainWindow import DiffViewerMainWindow

        repository = self._application.repository
        self._diff_window = DiffViewerMainWindow(self, repository=repository)
        self._diff_window.closed.connect(self._on_diff_window_closed)
        self._diff_window.showMaximized()

    ##############################################

    def _on_diff_window_closed(self):

        self._application.unwatch_files() # Fixme: only current patch !
        self._diff_window = None
        self._logger.info("Diff Viewer closed")

    ##############################################

    def _show_patch(self, patch):

        self._logger.info("")

        self._application.unwatch_files()

        if self._diff_window is None:
            self._create_diff_viewer_window()

        delta = patch.delta
        old_path = delta.old_file.path
        new_path = delta.new_file.path
        if not delta.is_binary:
            self._logger.info('revision {} '.format(self._current_revision) + new_path)
            # print(delta.status, delta.similarity, delta.additions, delta.deletions, delta.is_binary)
            # for hunk in delta.hunks:
            #     print(hunk.old_start, hunk.old_lines, hunk.new_start, hunk.new_lines, hunk.lines)
            if delta.status in (git.GIT_DELTA_MODIFIED, git.GIT_DELTA_RENAMED):
                paths = (old_path, new_path)
            elif delta.status == git.GIT_DELTA_ADDED:
                paths = (None, new_path)
            elif delta.status == git.GIT_DELTA_DELETED:
                paths = (old_path, None)
            repository = self._application.repository
            texts = [repository.file_content(blob_id)
                     for blob_id in (delta.old_file.id, delta.new_file.id)]
            metadatas = [dict(path=old_path, document_type='file', last_modification_date=None),
                         dict(path=new_path, document_type='file', last_modification_date=None)]
            self._diff_window.diff_documents(paths, texts, metadatas, workdir=repository.workdir)
            self._application.watch(new_path)
        else:
            self._logger.info('revision {} Binary '.format(self._current_revision) + new_path)
        # Fixme: show image pdf ...

        # Monitor file change

    ##############################################

    @property
    def _last_path_index(self):

        return len(self._diff) -1

    ##############################################

    def _next_previous_patch(self, forward):

        if forward:
            if self._current_patch_index < self._last_path_index:
                patch_index = self._current_patch_index + 1
            else:
                patch_index = 0
        else:
            if self._current_patch_index >= 1:
                patch_index = self._current_patch_index - 1
            else:
                patch_index = self._last_path_index

        self._current_patch_index = patch_index
        patch = self._diff[patch_index]
        self._show_patch(patch)

    ##############################################

    def previous_patch(self):
        self._next_previous_patch(forward=False)

    ##############################################

    def next_patch(self):
        self._next_previous_patch(forward=True)

    ##############################################

    def reload_current_patch(self):

        if self._current_patch_index is not None:
            patch = self._diff[self._current_patch_index]
            self._show_patch(patch)