# Copyright (C) 2009 Canonical Ltd
#
# 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 2 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, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA

import sys

from PyQt4 import QtCore, QtGui

from bzrlib.errors import LockContention
try:
    from bzrlib.osutils import parent_directories
except ImportError:
    from bzrlib.plugins.explorer.lib.helpers import parent_directories

from bzrlib.plugins.explorer.lib import helpers, kinds
from bzrlib.plugins.explorer.lib.helpers import build_combo_with_labels
from bzrlib.plugins.explorer.lib.i18n import gettext, N_

from bzrlib.plugins.qbzr.lib import treewidget
from bzrlib.plugins.qbzr.lib.uifactory import ui_current_widget


class WorkingTreeBrowser(QtGui.QDockWidget):

    def __init__(self, action_callback, browse_action, *args):
        self._action_callback = action_callback
        self._browse_action = browse_action
        QtGui.QDockWidget.__init__(self, *args)
        self.setObjectName("WorkingTreeDockWidget")
        self._style = 'qbrowse'
        self._tree = None
        self._branch = None

        # Show either a message or the actual browser
        msg = QtGui.QTextBrowser()
        msg.setText(gettext("No working tree"))
        self._browser = _QBrowseStyleBrowser(action_callback, browse_action, self)
        self._stacked = QtGui.QStackedWidget()
        self._stacked.addWidget(msg)
        self._stacked.addWidget(self._browser)
        self.setWidget(self._stacked)

    def set_tree(self, tree, branch):
        """Display the given working tree.

        :param tree: the WorkingTree to display.
            If None, the view is cleared.
        """
        if tree is None:
            # Show the message
            self._stacked.setCurrentIndex(0)
        else:
            # Show the browser
            self._browser.set_tree(tree, branch)
            if self._style == 'qbrowse':
                index = 1
            else:
                index = 2
            self._stacked.setCurrentIndex(index)
        self._tree = tree
        self._branch = branch

    def refresh(self):
        """Refresh the browser."""
        if self._tree is not None:
            self._browser.refresh()

    def get_style(self):
        return self._style

    def set_style(self, style):
        """Set the style: either qbrowse or classic."""
        self._style = style
        if style == 'classic':
            if self._stacked.count() == 2:
                classic_browser = _ClassicBrowser(self._action_callback,
                    self._browse_action)
                self._stacked.addWidget(classic_browser)
            index = 2
        else:
            index = 1
        self._browser = self._stacked.widget(index)
        self._browser.set_tree(self._tree, self._branch)
        self._stacked.setCurrentIndex(index)

    def set_default_action(self, default_action):
        """Set the default action to either 'open' or 'edit'."""
        self._browser.set_default_action(default_action)


class _ActionPanel(QtGui.QToolBar):

    def __init__(self, action_callback, browse_action, *args):
        self._action_callback = action_callback
        self._browse_action = browse_action
        QtGui.QDialogButtonBox.__init__(self, *args)
        self._root = None
        self._selected_fileinfo = None
        self._button_style = QtCore.Qt.ToolButtonTextUnderIcon

        # Build the buttons and add them to a panel
        self._open_button = self._build_button(
            kinds.icon_for_kind(kinds.OPEN_ACTION),
            gettext("&Open"),
            gettext("Open selected item"),
            self._do_open_file)
        self._edit_button = self._build_button(
            kinds.icon_for_kind(kinds.EDIT_ACTION),
            gettext("&Edit"),
            gettext("Edit selected item"),
            self._do_edit_file)
        self.addSeparator()
        self._build_button(
            kinds.icon_for_kind(kinds.OPEN_FOLDER_ACTION),
            gettext("&Manage"),
            gettext("Open file manager on folder"),
            self._do_open_folder)
        self._build_button(
            kinds.icon_for_kind(kinds.NEW_FILE_ACTION),
            gettext("&New File"),
            gettext("Create a new file"),
            self._do_new_file)
        self._build_button(
            kinds.icon_for_kind(kinds.NEW_FOLDER_ACTION),
            gettext("New &Folder"),
            gettext("Create a new folder"),
            self._do_new_folder)
        self.addSeparator()
        self._build_button(
            self._browse_action.icon(),
            self._browse_action.iconText(),
            self._browse_action.statusTip(),
            self._do_browse_items)
        self.addSeparator()
        self._build_button(
            kinds.icon_for_kind(kinds.COLLAPSE_ALL_ACTION),
            gettext("&Collapse"),
            gettext("Fully collapse tree"),
            self._do_collapse_all)
        self._build_button(
            kinds.icon_for_kind(kinds.EXPAND_ALL_ACTION),
            gettext("&Expand"),
            gettext("Fully expand tree"),
            self._do_expand_all)

    def _build_button(self, icon, text, tooltip, callback, arrow=None):
        button = QtGui.QToolButton()
        button.setText(text)
        if arrow:
            button.setArrowType(arrow)
        else:
            button.setIcon(icon)
        button.setToolTip(tooltip)
        #button.setToolButtonStyle(self._button_style)
        button.setAutoRaise(True)
        #self.addButton(button, QtGui.QDialogButtonBox.ActionRole)
        self.addWidget(button)
        self.connect(button, QtCore.SIGNAL("clicked(bool)"), callback)
        return button

    def _get_folder(self):
        fileinfo = self._selected_fileinfo
        if fileinfo is None:
            folder = self._root
        elif fileinfo.isDir():
            folder = fileinfo.canonicalFilePath()
        else:
            folder = fileinfo.canonicalPath()
        return unicode(folder)

    def _do_browse_items(self):
        self._action_callback("browse", None)

    def _do_open_folder(self):
        self._action_callback("open", self._get_folder())

    def _do_open_file(self):
        fileinfo = self._selected_fileinfo
        path = fileinfo.canonicalFilePath()
        self._action_callback("open", unicode(path))

    def _do_edit_file(self):
        fileinfo = self._selected_fileinfo
        path = fileinfo.canonicalFilePath()
        self._action_callback("edit", unicode(path))

    def _do_new_file(self):
        destination = self._get_folder()
        self._action_callback("new-file", destination)
        self._tree_viewer.refresh()

    def _do_new_folder(self):
        destination = self._get_folder()
        self._action_callback("new-directory", destination)
        self._tree_viewer.refresh()

    def _do_collapse_all(self):
        self._tree_viewer.collapseAll()

    def _do_expand_all(self):
        self._tree_viewer.expandAll()

    def set_viewer(self, tree_viewer):
        self._tree_viewer = tree_viewer

    def set_root(self, path):
        self._root = path

    def set_selection(self, fileinfo, model_index=None):
        self._selected_fileinfo = fileinfo
        self._selected_index = model_index
        edit_is_enabled = True
        if fileinfo is None or fileinfo.isDir():
            edit_is_enabled = False
        self._edit_button.setEnabled(edit_is_enabled)
        self._open_button.setEnabled(edit_is_enabled)


class _FilterPanel(QtGui.QWidget):

    def __init__(self, *args):
        QtGui.QWidget.__init__(self, *args)
        self._tree_viewer = None
        self._apply_filter_callback = ''

        # Build the controls
        filter_label = QtGui.QLabel(gettext("Filter:"))
        self._filter_field = QtGui.QLineEdit()
        self._filter_cancel_button = QtGui.QToolButton()
        self._filter_cancel_button.setAutoRaise(True)
        self._filter_cancel_button.setEnabled(False)
        self._filter_cancel_button.setIcon(kinds.icon_for_kind(
            kinds.CANCEL_ACTION))
        self._filter_cancel_button.setToolTip(gettext("Clear the filter"))

        # Connect up the controls to handlers
        self.connect(self._filter_field, QtCore.SIGNAL("textChanged(QString)"),
            self._apply_filter)
        self.connect(self._filter_cancel_button, QtCore.SIGNAL("clicked(bool)"),
            self.clear_filter)

        # Put the controls together
        layout = QtGui.QHBoxLayout()
        layout.addWidget(filter_label)
        layout.addWidget(self._filter_field)
        layout.addWidget(self._filter_cancel_button)
        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)

    def _apply_filter(self, text):
        self._apply_filter_callback(text)
        if text:
            self._filter_cancel_button.setEnabled(True)
        else:
            self._filter_cancel_button.setEnabled(False)
        self._current_filter = text

    def clear_filter(self):
        self._filter_field.setText('')

    def get_filter(self):
        return unicode(self._filter_field.text())

    def set_apply_filter(self, apply_filter_callback):
        self._apply_filter_callback = apply_filter_callback


class _TreeViewer(QtGui.QTreeView):

    def __init__(self, action_callback, selection_callback,
            filter_callback, *args):
        self._action_callback = action_callback
        self._selection_callback = selection_callback
        self._filter_callback = filter_callback
        QtGui.QTreeView.__init__(self, *args)
        self._default_action = "open"

        # Use a proxy model so we can filter on the path.
        self._source_model = _TreeModel()
        self._model = _PathBasedSortFilterProxyModel(self._filter_callback)
        self._model.setSourceModel(self._source_model)
        self.setModel(self._model)
        self._configure_ui()

        # Track the selection
        self.connect(self, QtCore.SIGNAL("doubleClicked(QModelIndex)"),
            self._do_double_clicked)
        sel_model = self.selectionModel()
        self.connect(sel_model,
            QtCore.SIGNAL("currentChanged(QModelIndex,QModelIndex)"),
            self._do_current_changed)

    def _configure_ui(self):
        # Hide all except the Name column
        for i in  range(1, self._model.columnCount()):
            self.setColumnHidden(i, True)
        if getattr(self, 'setHeaderHidden', None):
            self.setHeaderHidden(True)

    def _do_double_clicked(self, model_index):
        fileinfo = self._get_fileinfo(model_index)
        if fileinfo is not None and not fileinfo.isDir():
            path = fileinfo.canonicalFilePath()
            self._action_callback(self._default_action, unicode(path))

    def _do_current_changed(self, model_index, prev_model_index):
        fileinfo = self._get_fileinfo(model_index)
        self._selection_callback(fileinfo, model_index)

    def _get_fileinfo(self, model_index):
        model = model_index.model()
        if model is None:
            fileinfo = None
        else:
            source_index = model.mapToSource(model_index)
            fileinfo = self._source_model.fileInfo(source_index)
        return fileinfo

    def set_tree(self, tree, branch):
        """Change the view to display the given working tree."""
        path = tree.basedir
        source_index = self._source_model.index(path)
        index = self._model.mapFromSource(source_index)
        self.setRootIndex(index)
        self.selectionModel().reset()
        self._model.set_root(path)

    def refresh(self):
        """Refresh the view."""
        self._source_model.refresh()

    def invalidate_filter(self):
        self._model.invalidateFilter()

    def set_default_action(self, default_action):
        """Set the default action to either 'open' or 'edit'."""
        self._default_action = default_action


class _TreeModel(QtGui.QDirModel):

    def __init__(self, parent=None):
        QtGui.QDirModel.__init__(self, parent)
        # Set the sort flags: directories first,
        # on Windows items are case-insensitive
        flags = QtCore.QDir.SortFlags(QtCore.QDir.Name|QtCore.QDir.DirsFirst)
        if sys.platform == 'win32':
            flags |= QtCore.QDir.IgnoreCase
        self.setSorting(flags)


class _PathBasedSortFilterProxyModel(QtGui.QSortFilterProxyModel):

    def __init__(self, filter_callback, parent=None):
        QtGui.QSortFilterProxyModel.__init__(self, parent)
        self._filter_callback = filter_callback
        self._root = None
        self._interesting = None

    def set_root(self, root):
        self._root = root

    def invalidateFilter(self):
        self._text = self._filter_callback()
        if self._text == '':
            self._interesting = None
        else:
            # We only want to show directories containing matching items so
            # we need to do a pass in advance working out what's of interest
            self._source = self.sourceModel()
            root_index = self._source.index(self._root)
            self._interesting = self._find_interesting(self._source,
                root_index, self._text)
        QtGui.QSortFilterProxyModel.invalidateFilter(self)

    def _find_interesting(self, source, root_index, text):
        interesting = set([''])
        directories = set()
        for source_index in self._iter_children(source, root_index):
            fileinfo = source.fileInfo(source_index)
            abspath = unicode(fileinfo.absoluteFilePath())
            relpath = abspath[len(self._root) + 1:]
            if relpath.find(text) >= 0:
                interesting.add(relpath)
                directories.update(parent_directories(relpath))
        interesting.update(directories)
        #print "\ninteresting for %s ...\n%s" % (text, "\n".join(sorted(interesting)))
        return interesting

    def _iter_children(self, source, parent):
        for i in range(0, source.rowCount(parent)):
            index = source.index(i, 0, parent)
            yield index
            for child in self._iter_children(source, index):
                yield child
        
    def filterAcceptsRow(self, source_row, source_parent):
        if self._interesting is None:
            return True
        result = False
        source_index = self._source.index(source_row, 0, source_parent)
        fileinfo = self._source.fileInfo(source_index)
        abspath = unicode(fileinfo.absoluteFilePath())
        if abspath.startswith(self._root):
            # We're inside the tree of interest
            relpath = abspath[len(self._root) + 1:]
            result = relpath in self._interesting
        elif fileinfo.isDir():
            # We're interested if it's a parent of the root
            result = self._root.startswith(abspath)
        return result


class _AbstractBrowser(QtGui.QWidget):

    filter_panel_factory = _FilterPanel
    tree_viewer_factory = _TreeViewer
    action_panel_factory = _ActionPanel

    def __init__(self, action_callback, browse_action, *args):
        self._action_callback = action_callback
        self._browse_action = browse_action
        QtGui.QWidget.__init__(self, *args)
        # Map of tree base directories to expanded folders.
        # NOTE: We arguably ought to clear out this information on a
        # rebuild/refresh because directories may be ordered differently.
        # OTOH, it's pretty harmless to expand the wrong folder and we
        # are more likely to give a correct result than not. Filtering
        # is different though - we certainly want to reset this data then.
        self._expanded_items = {}
        self._current_basedir = None

        # Build the ui components
        self._action_panel = self.action_panel_factory(action_callback,
            browse_action)
        self._filter_panel = self.filter_panel_factory()
        self._tree_viewer = self.tree_viewer_factory(action_callback,
            self._action_panel.set_selection, self._filter_panel.get_filter)
        self._action_panel.set_viewer(self._tree_viewer)
        self._filter_panel.set_apply_filter(self._apply_filter)

        # Put them together
        layout = QtGui.QVBoxLayout()
        layout.addWidget(self._filter_panel)
        layout.addWidget(self._tree_viewer)
        layout.addWidget(self._action_panel)
        self.setLayout(layout)

    def set_tree(self, tree, branch):
        """Display the given working tree."""
        # Remember what's expanded currently
        expanded = helpers.get_tree_expansion(self._tree_viewer,
            only_expanded=True)[0]
        #print "saving: %r" % (self._expanded_items[self._current_basedir],)
        self._expanded_items[self._current_basedir] = expanded
        if tree is None:
            return
        self._tree_viewer.set_tree(tree, branch)
        self._action_panel.set_root(tree.basedir)
        self._action_panel.set_selection(None)
        self._filter_panel.clear_filter()
        # Restore what should be expanded at this location
        self._current_basedir = tree.basedir
        expanded = self._expanded_items.get(self._current_basedir)
        #print "applying: %s" % (expanded, )
        helpers.set_tree_expansion(self._tree_viewer, expanded, None)

    def refresh(self):
        """Refresh the browser."""
        try:
            self._tree_viewer.refresh()
        except LockContention as e:
            return

    def set_default_action(self, default_action):
        """Set the default action to either 'open' or 'edit'."""
        self._tree_viewer.set_default_action(default_action)


class _ClassicBrowser(_AbstractBrowser):

    def _apply_filter(self, text):
        self._tree_viewer.invalidate_filter()
        if text:
            # Filtering active. We want to see all matches and
            # this mostly works. It seems to struggle though when
            # the filter gets shorter - maybe a Qt 4.4 bug w.r.t.
            # propagating exactly what's changed to the view?
            self._tree_viewer.expandAll()
        else:
            # Filtering cleared. Restore the initial view with
            # everything collapsed. (If the user has selectively
            # expanded some directories out before filtering,
            # collapsing all here implies losing that setup. We
            # could always remember the expanded directories at
            # the start of filtering and restore them at this
            # point if it proves a problem in practice.)
            self._tree_viewer.collapseAll()
            self._tree_viewer.refresh()


### QBrowse style browser support ###

# The map from filter category to filter flags for TreeFilterProxyModels
_FILTER_FLAGS_MAP = {
    # The flag order is UNCHANGED, CHANGED, UNVERSIONED, IGNORED
    'all':              [True, True, True, True],
    'changed':          [False, True, False, False],
    'versioned':        [True, True, False, False],
    'unversioned':      [False, False, True, False],
    'ignored':          [False, False, True, True],
    'unignored':        [True, True, True, False],
    }

class _QBrowseStyleFilterPanel(QtGui.QWidget):

    def __init__(self, *args):
        QtGui.QWidget.__init__(self, *args)
        self._tree_viewer = None

        # Build the controls
        self._category_combo = build_combo_with_labels([
            ('all', gettext("All")),
            ('changed', gettext("Changed")),
            ('versioned', gettext("Versioned")),
            ('unversioned', gettext("Unversioned")),
            ('ignored', gettext("Ignored")),
            ('unignored', gettext("Unignored")),
            ])
        self._category_combo.setCurrentIndex(5)
        self._text_filter = _FilterPanel()

        # Connect up the controls to handlers
        QtCore.QObject.connect(self._category_combo,
            QtCore.SIGNAL("currentIndexChanged(int)"),
            self._apply_category)
        self._text_filter.set_apply_filter(self._apply_text)

        # Put the controls together
        layout = QtGui.QHBoxLayout()
        layout.addWidget(self._category_combo)
        layout.addWidget(self._text_filter)
        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)

    def _flags_for_index(self, index):
        category = str(self._category_combo.itemData(index).toString())
        return _FILTER_FLAGS_MAP[category]

    def _apply_category(self, index):
        self._apply_filter_callback(self._flags_for_index(index), True,
            self._text_filter.get_filter(), False)

    def _apply_text(self, text):
        index = self._category_combo.currentIndex()
        self._apply_filter_callback(self._flags_for_index(index), False,
            text, True)

    def clear_filter(self):
        self._text_filter.clear_filter()

    def get_filter(self):
        index = self._category_combo.currentIndex()
        return self._flags_for_index(index), self._text_filter.get_filter()

    def set_apply_filter(self, apply_filter_callback):
        self._apply_filter_callback = apply_filter_callback


class _QBrowseFilterProxyModel(treewidget.TreeFilterProxyModel):
    """Filter a working tree by a text string.

    If the text matches a directory, the directory and all it's contents
    are shown. If the text matches a file, the parent directories and
    the item are shown.

    Note: For efficency, we pre-calculate the interesting entries
    once each time the text is changed. We then simply look up
    each path in the set of interesting paths inside the per-row
    filtering callback.
    """

    def __init__(self, *args):
        treewidget.TreeFilterProxyModel.__init__(self, *args)
        self.text_to_match = None

    def setTextToMatch(self, text):
        self.text_to_match = text
        self.invalidateFilter()

    def filter_id(self, id, item_data):
        """Determines wether a item should be displayed.
        Returns :
            * True: Show the item
            * False: Donot show the item
            * None: Show the item if there are any children that are visible.
        """
        show = treewidget.TreeFilterProxyModel.filter_id(self, id, item_data)
        if self.text_to_match and show:
            if item_data.path.find(self.text_to_match) >= 0:
                return True
            else:
                return None
        return show

class _QBrowseTreeWidget(treewidget.TreeWidget):
    """Subclass the standard QBzr TreeWidget to patch in more features.

    The additional features are:

    * text filtering, not just state filtering
    * notifcation when the tree is changed via a context menu action
    """

    def __init__(self, *args):
        treewidget.TreeWidget.__init__(self, *args)
        # Patch in a custom FilterProxyModel that handles text filtering
        self.tree_filter_model = _QBrowseFilterProxyModel()
        self.tree_filter_model.setSourceModel(self.tree_model)
        self.setModel(self.tree_filter_model)

    def set_tree(self, *args):
        try:
            treewidget.TreeWidget.set_tree(self, *args)
            # Ensure the filtering is applied
            self.invalidate_filter()
        except LockContention as e:
             return

    @ui_current_widget
    def add(self):
        treewidget.TreeWidget.add(self)
        self.notify_tree_change()
        
    @ui_current_widget
    def revert(self):
        treewidget.TreeWidget.revert(self)
        self.notify_tree_change()
    
    @ui_current_widget
    def merge(self):
        treewidget.TreeWidget.merge(self)
        self.notify_tree_change()

    def resolve(self):
        treewidget.TreeWidget.resolve(self)
        self.notify_tree_change()
    
    def mark_move(self):
        treewidget.TreeWidget.mark_move(self)
        self.notify_tree_change()

    @ui_current_widget
    def rename(self):
        treewidget.TreeWidget.rename(self)
        self.notify_tree_change()
        
    @ui_current_widget
    def remove(self):
        treewidget.TreeWidget.remove(self)
        self.notify_tree_change()

    def notify_tree_change(self):
        """Hook called when the tree is changed by a context menu action."""


class _QBrowseStyleTreeViewer(_QBrowseTreeWidget):

    def __init__(self, action_callback, selection_callback,
            filter_callback, *args):
        self._action_callback = action_callback
        self._selection_callback = selection_callback
        _QBrowseTreeWidget.__init__(self, *args)
        self._default_action = "open"

        # Track the selection
        self.connect(self.selectionModel(),
            QtCore.SIGNAL("currentChanged(QModelIndex,QModelIndex)"),
            self._do_current_changed)

    def do_default_action(self, model_index):
        """Override the handler provided by treewidget.TreeWidget."""
        item_data = self.get_selection_items([model_index])[0]
        fileinfo = QtCore.QFileInfo(item_data.path)
        if fileinfo is not None and not fileinfo.isDir():
            path = fileinfo.canonicalFilePath()
            self._action_callback(self._default_action, unicode(path))

    def _do_current_changed(self, model_index, prev_model_index):
        item_data = self.get_selection_items([model_index])[0]
        fileinfo = QtCore.QFileInfo(item_data.path)
        self._selection_callback(fileinfo, model_index)

    def invalidate_filter(self):
        self.tree_filter_model.invalidateFilter()

    def set_default_action(self, default_action):
        """Set the default action to either 'open' or 'edit'."""
        self._default_action = default_action

    def notify_tree_change(self):
        """Hook called when the tree is changed by a context menu action."""
        self._action_callback("refresh-page", None)


class _QBrowseStyleBrowser(_AbstractBrowser):

    filter_panel_factory = _QBrowseStyleFilterPanel
    tree_viewer_factory = _QBrowseStyleTreeViewer

    def _apply_filter(self, flags, flags_changed, text, text_changed):
        if flags_changed:
            self._tree_viewer.tree_filter_model.setFilters(flags)
        if text_changed:
            self._tree_viewer.tree_filter_model.setTextToMatch(text)
            # Automatically expand the tree if the match count is small.
            # The number could be configurable one day provided the doc
            # was very clear about potential vs actual match counts
            parents = [QtCore.QModelIndex()] # start at the root
            next_parents = []
            expanded = 0
            while parents and expanded <= 15:
                next_parents = []
                for parent in parents:
                    matches = self._tree_viewer.tree_filter_model.rowCount(parent)
                    if matches and matches <= 5:
                        self._tree_viewer.expand(parent)
                    if (self._tree_viewer.isExpanded(parent) or
                        parent == QtCore.QModelIndex()):
                        expanded += matches
                        for i in range(matches):
                            next_parents.append(
                                self._tree_viewer.tree_filter_model.index(i, 0, parent))
                parents = next_parents
        # Reset our memory wrt what's expanded. This is perhaps a little
        # heavy handed wrt text matching as that isn't remembered across
        # tree switches. (Then again, perhaps it should be.)
        self._expanded_items = {}
