# 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


from PyQt4 import QtCore, QtGui

from bzrlib import bzrdir, errors, osutils, log, trace, transport, urlutils

from bzrlib.plugins.explorer.lib.html_extras import extra_revisions_as_html
from bzrlib.plugins.explorer.lib.html_log import log_as_html
from bzrlib.plugins.explorer.lib.kinds import (
    BOUND_BRANCH_KIND,
    BRANCH_KIND,
    CHECKOUT_KIND,
    REPOSITORY_KIND,
    DELETE_ACTION,
    BRANCH_ACTION,
    OPEN_FOLDER_ACTION,
    LOG_ACTION,
    icon_for_kind,
    ERROR_STATUS,
    html_status_message,
    )
from bzrlib.plugins.explorer.lib.status_report import StatusReport
from bzrlib.plugins.explorer.lib.filesystemwatcher import QFileSystemWatcher
from bzrlib.plugins.explorer.widgets import (
    conditional_dataview,
    tab_widget,
    )
from bzrlib.plugins.explorer.lib.i18n import gettext, N_


class _RepositoryViewBase(object):
    """The base class for repository views.
    
    This is still a work in progress. Some things it might do in the future:

    * Rename and Delete actions for locations
    * Make recursing into control directories looking for more optional
    """

    def __init__(self, model, action_callback):
        self._model = model
        self._action_callback = action_callback
        self._relpath = None
        self._root_transport = None
        self._init_data()
        self._view = self._build_view()
        self._init_watcher()
        
    def _init_data(self):
        # When set, this is a tuple of ...
        # all, branches, bound_branches, checkouts, repositories
        self._data = None
        # This is a dictionary of data viewers, indexed by kind
        self._data_viewers = {}
        # This is the mapping from relative-path to kind
        self._kinds = {}
        # This is the mapping from relative-path to control objects,
        # e.g. Branch, Repository, etc.
        self._control_objects = {}

    def _build_view(self):
        """Return the constructed UI.

        :return: a QWidget.
        """
        # Build the main display area. We initially have the filter bar off
        # and provide a checkbox in the button box for users to display it.
        self._filter_bar = QtGui.QTabBar()
        self._filter_bar.setVisible(False)
        footer_texts = self.get_footer_texts()
        self._details_panel = _DetailsPanel(self._action_callback)
        button_bar = self._build_button_bar()
        self.main = QtGui.QStackedWidget()
        self._build_region("all", gettext("All"),
            "list", True, footer_texts, self._details_panel, button_bar)
        self._build_region(BRANCH_KIND, gettext("Branches"),
            "tree", ["Name", "Parent"], footer_texts)
        self._build_region(BOUND_BRANCH_KIND, gettext("Bound Branches"),
            "tree", ["Name", "Bound To"], footer_texts)
        self._build_region(CHECKOUT_KIND, gettext("Checkouts"),
            "tree", ["Name", "Switched To"], footer_texts)
        self._build_region(REPOSITORY_KIND, gettext("Repositories"),
            "tree", ["Name"], footer_texts)

        # Hook up the filter bar
        self._filter_bar.connect(self._filter_bar,
            QtCore.SIGNAL("currentChanged(int)"), self.main.setCurrentIndex)

        # Put it all together
        layout = QtGui.QVBoxLayout()
        layout.addWidget(self._filter_bar)
        layout.addWidget(self.main)
        ui = QtGui.QWidget()
        ui.setLayout(layout)
        return ui

    def _build_button_bar(self):
        # Build the set of action buttons
        button_bar = QtGui.QDialogButtonBox()
        filter_checkbox = QtGui.QCheckBox(gettext("Show filter bar"))
        button_bar.addButton(filter_checkbox, QtGui.QDialogButtonBox.ResetRole)
        manage_icon = icon_for_kind(OPEN_FOLDER_ACTION)
        manage_button = QtGui.QPushButton(manage_icon, gettext("&Manage"))
        button_bar.addButton(manage_button, QtGui.QDialogButtonBox.ActionRole)
        branch_icon = icon_for_kind(BRANCH_ACTION)
        branch_button = QtGui.QPushButton(branch_icon, gettext("&Branch"))
        button_bar.addButton(branch_button, QtGui.QDialogButtonBox.ActionRole)
        log_icon = icon_for_kind(LOG_ACTION)
        log_button = QtGui.QPushButton(log_icon, gettext("Log"))
        button_bar.addButton(log_button, QtGui.QDialogButtonBox.ActionRole)

        delete_icon = icon_for_kind(DELETE_ACTION)
        delete_button = QtGui.QPushButton(delete_icon, gettext("Delete"))
        delete_button.setEnabled(False)
        # Not quite ready to expose something this destructive yet ...
        # Maybe we should expose it via experimental mode for a while first?
        # Note: It would be safer to only offer Delete as a What's Next
        # action when everything looks ok, including an empty shelf.
        # The main advantage of Delete here would be the potential to
        # implicitly refresh the list after it finishes? Also, the
        # "Local Changes" stuff is only implemented for Branches so far.
        #button_bar.addButton(delete_button, QtGui.QDialogButtonBox.DestructiveRole)
        open_button = button_bar.addButton(QtGui.QDialogButtonBox.Open)
        open_button.setEnabled(False)

        # Hook up the buttons and remember them
        button_bar.connect(filter_checkbox, QtCore.SIGNAL("toggled(bool)"),
            self._filter_bar.setVisible)
        button_bar.connect(open_button, QtCore.SIGNAL("clicked(bool)"),
            self.do_open_location)
        button_bar.connect(delete_button, QtCore.SIGNAL("clicked(bool)"),
            self.do_delete_location)
        button_bar.connect(branch_button, QtCore.SIGNAL("clicked(bool)"),
            self.do_new_branch)
        button_bar.connect(manage_button, QtCore.SIGNAL("clicked(bool)"),
            self.do_manage_location)
        button_bar.connect(log_button, QtCore.SIGNAL("clicked(bool)"),
            self.do_log_location)
        self._open_button = open_button
        self._delete_button = delete_button
        self._branch_button = branch_button
        self._manage_button = manage_button
        return button_bar

    def _build_region(self, model_key, label, view_type, view_or_headers,
        footer_texts, details_panel=None, button_bar=None):
        footer_text = footer_texts[model_key]
        if details_panel:
            details_widget = details_panel.ui()
        else:
            details_widget = None
        data_viewer = conditional_dataview.QBzrConditionalDataView(view_type,
            view_or_headers, footer_text, details_widget)
        view = data_viewer.view()
        #view.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
        self._data_viewers[model_key] = data_viewer
        layout = QtGui.QVBoxLayout()
        layout.addWidget(data_viewer)
        if button_bar:
            layout.addWidget(button_bar)
        panel = QtGui.QWidget()
        panel.setLayout(layout)
        icon = icon_for_kind(model_key)
        self._filter_bar.addTab(icon, label)
        self.main.addWidget(panel)
        self.main.connect(data_viewer.view(),
            QtCore.SIGNAL("doubleClicked(QModelIndex)"),
            self.do_double_clicked)
        if details_panel:
            self._sel_model = view.selectionModel()
            self.main.connect(self._sel_model,
                QtCore.SIGNAL("selectionChanged(QItemSelection,QItemSelection)"),
                self.do_selection_changed_on_all_tab)

    def _init_watcher(self):
        """create a QFileSystemWatcher to watch for changes in
        the repository"""
        root_transport = self._get_root_transport()
        repo_url = root_transport.external_url()
        if repo_url.startswith("file://"):
            repo_path = urlutils.local_path_from_url(repo_url)
            directory_watcher = QFileSystemWatcher(
                                       [QtCore.QString(repo_path)], self._view)
            directory_watcher.directoryChanged.connect(
                                        self._repo_directory_changed)

    def _repo_directory_changed(self, directory):
        """refresh view after short delay, called by the QFileSystemWatcher"""
        # more reliable to delay refresh a bit else the external change
        # may not have completed
        QtCore.QTimer.singleShot(500, self.refresh_view)

    def do_open_location(self):
        abspath = self._get_abspath(self._relpath)
        self._action_callback("open-location", abspath)

    def do_delete_location(self):
        print "deleting location %s ..." % (self._relpath,)

    def do_new_branch(self):
        selected = self._relpath
        if (selected and self._kinds[selected] in
            [BRANCH_KIND, BOUND_BRANCH_KIND]):
            context = {'branch_root': selected}
        else:
            # TODO: Maybe use a default branch like 'trunk' if
            # we know or can guess the right one
            context = None
        self._action_callback("branch", None, context=context)

    def do_manage_location(self):
        path = self._model.location
        if self._relpath is not None:
            path = osutils.pathjoin(path, self._relpath)
        self._action_callback("open", path)

    def do_log_location(self):
        if self._sel_model.hasSelection():
            context = {'selected': self._get_selected_as_string()}
            self._action_callback("log", None, context=context)
        else:
            # Log the whole repository
            root = self._model.location
            self._action_callback("log", root)

    def _get_selected_as_string(self):
        root = self._model.location
        paths = []
        for i in self._sel_model.selectedIndexes():
            path = self._get_relpath_for_index(i)
            # XXX: For non-local repositories, prepend the root here?
            #path = osutils.pathjoin(root, path)
            if ' ' in path:
                path = '"%s"' % (path,)
            paths.append(path)
        return " ".join(paths)

    def _get_relpath_for_index(self, model_index):
        """Get the relative path of a selection or None if no selection."""
        model = model_index.model()
        if model is None:
            return None
        row = model_index.row()
        id_index = model.index(row, 0)
        return unicode(model.data(id_index).toString())

    def _get_abspath(self, relpath):
        return self._get_root_transport().abspath(urlutils.escape(relpath))

    def do_double_clicked(self, model_index):
        self._relpath = self._get_relpath_for_index(model_index)
        self.do_open_location()

    def do_selection_changed_on_all_tab(self, curr_item_sel, prev_item_sel):
        selected_count = curr_item_sel.count()
        if selected_count == 1:
            model_index = curr_item_sel.indexes()[0]
            self.select_path(self._get_relpath_for_index(model_index))
        else:
            self.select_path(None)

    def select_path(self, relpath):
        self._relpath = relpath
        something_selected = self._relpath is not None
        self._open_button.setEnabled(something_selected)
        self._delete_button.setEnabled(something_selected)
        details_panel = self._details_panel
        if details_panel:
            if something_selected:
                kind = self._kinds[self._relpath]
                control_obj = self._control_objects[self._relpath]
                details_panel.show_details(kind, control_obj)
            else:
                details_panel.show_details(None, None)

    def ui(self):
        self.refresh_view()
        return self._view

    def refresh_view(self):
        self._collect_data()
        self._update_view()
        # XXX: Maybe we should only refresh the details panel if
        # it is shown (i.e. All tab selected)?
        all_view = self._data_viewers["all"].view()
        curr_index = all_view.currentIndex()
        self.select_path(self._get_relpath_for_index(curr_index))

    def _update_view(self):
        all, branches, bound_branches, checkouts, repos = self._data
        self._update_model("all", all)
        self._update_model(BRANCH_KIND, branches)
        self._update_model(BOUND_BRANCH_KIND, bound_branches)
        self._update_model(CHECKOUT_KIND, checkouts)
        self._update_model(REPOSITORY_KIND, repos)

    def _collect_data(self):
        root_transport = self._get_root_transport()
        repo_url = root_transport.external_url()
        if repo_url.startswith("file://"):
            repo_path = urlutils.local_path_from_url(repo_url)
        def evaluate(bzrdir):
            this_repo = bzrdir.root_transport.base == root_transport.base
            return this_repo, bzrdir
        controls = bzrdir.BzrDir.find_bzrdirs(root_transport,
            evaluate=evaluate)
        branches = []
        bound_branches = []
        checkouts = []
        nested_repos = []
        for control in controls:
            transport = control.root_transport
            if transport == root_transport:
                # skip the repository itself
                continue

            # Get the relpath
            transport_url = urlutils.strip_trailing_slash(transport.base)
            if transport_url.startswith("file://"):
                local_path = urlutils.local_path_from_url(transport_url)
                relpath = local_path[len(repo_path):]
            else:
                relpath = transport_url[len(repo_url):]

            # Get the interesting details
            try:
                br = control.open_branch()
                self._control_objects[relpath] = br
            except errors.NotBranchError:
                try:
                    nested_repo = control.open_repository()
                    self._control_objects[relpath] = nested_repo
                    nested_repos.append((relpath,))
                except errors.NoRepositoryPresent:
                    trace.mutter("%s is not a branch or repo" % (relpath,))
                continue
            except errors.NoRepositoryPresent:
                trace.mutter(
                    "%s is a symlink to a repository branch - ignoring" %
                    (relpath,))
                continue
            except StandardError, ex:
                trace.mutter("error opening control dir %s: %s" %
                    (relpath, ex))
                continue
            switched_to = control.get_branch_reference()
            bound_location = br.get_bound_location()
            if switched_to is not None:
                if switched_to.startswith(repo_url):
                    switched_to = switched_to[len(repo_url):]
                checkouts.append((relpath, switched_to))
            elif bound_location is not None:
                if bound_location.startswith(repo_url):
                    bound_location = bound_location[len(repo_url):]
                bound_branches.append((relpath, bound_location))
            else:
                parent = br.get_parent()
                if parent is not None and parent.startswith(repo_url):
                    parent = parent[len(repo_url):]
                branches.append((relpath, parent))

        # Build the all data model. Columns are name and icon.
        all = []
        all.extend([(br[0], BRANCH_KIND) for br in branches])
        all.extend([(bb[0], BOUND_BRANCH_KIND) for bb in bound_branches])
        all.extend([(nr[0], REPOSITORY_KIND) for nr in nested_repos])
        all.sort()

        # Put checkouts and more interesting entries at the top
        more_interesting = []
        other = []
        for name, kind in all:
            # Note: we may want to make this list a preference one day,
            # particularly for non-English speaking users. Also, bzr.dev
            # probably ought to be <reponame>.dev :-)
            if name.lower() in ['trunk', 'bzr.dev']:
                more_interesting.append((name, kind))
            else:
                other.append((name, kind))
        if more_interesting:
            all = more_interesting + other
        if checkouts:
            all = [(co[0], CHECKOUT_KIND) for co in checkouts] + all

        # Save stuff for later
        self._data = all, branches, bound_branches, checkouts, nested_repos
        self._kinds = dict(all)

    def _update_model(self, kind, tuple_list):
        if kind == 'all':
            decorator_provider = lambda row, record: icon_for_kind(record[1])
        else:
            icon = icon_for_kind(kind)
            if icon:
                decorator_provider = lambda row, record: icon
            else:
                decorator_provider = None
        data_viewer = self._data_viewers[kind]
        data_viewer.setData(tuple_list, decorator_provider)


# Footers for each tab
_REPOSITORY_FOOTERS = {
    "all":
        gettext("Locations found below this repository: %(rows)d"),
    BRANCH_KIND:
        gettext("Branches found below this repository: %(rows)d"),
    BOUND_BRANCH_KIND:
        gettext("Bound branches found below this repository: %(rows)d"),
    CHECKOUT_KIND:
        gettext("Checkouts found below this repository: %(rows)d"),
    REPOSITORY_KIND:
        gettext("Repositories found below this repository: %(rows)d"),
    }
_VIRTUAL_REPO_FOOTERS = {
    "all":
        gettext("Locations found below this virtual repository: %(rows)d"),
    BRANCH_KIND:
        gettext("Branches found below this virtual repository: %(rows)d"),
    BOUND_BRANCH_KIND:
        gettext("Bound branches found below this virtual repository: %(rows)d"),
    CHECKOUT_KIND:
        gettext("Checkouts found below this virtual repository: %(rows)d"),
    REPOSITORY_KIND:
        gettext("Repositories found below this virtual repository: %(rows)d"),
    }


class RepositoryView(_RepositoryViewBase):
    """The default view for shared repositories."""
 
    def get_footer_texts(self):
        return _REPOSITORY_FOOTERS

    def _get_root_transport(self):
        if self._root_transport is None:
            self._root_transport = self._model.repository.bzrdir.root_transport
        return self._root_transport


class VirtualRepoView(_RepositoryViewBase):
    """The default view for virtual repositories."""
 
    def get_footer_texts(self):
        return _VIRTUAL_REPO_FOOTERS

    def _get_root_transport(self):
        if self._root_transport is None:
            self._root_transport = transport.get_transport(self._model.location)
        return self._root_transport


class _DetailsPanel(object):

    def __init__(self, action_callback, recent_history_limit=5):
        self._action_callback = action_callback
        self.recent_history_limit = recent_history_limit
        self.stacked = self._build_ui()
        self.branch = None
        # The set of tabs which have been refreshed since the branch changed
        self.refreshed_tabs = set()

    def _build_ui(self):
        self.recent_history = QtGui.QTextBrowser()
        self.local_changes = QtGui.QTextBrowser()
        self.missing_revisions = QtGui.QTextBrowser()
        self.details = QtGui.QTabWidget()
        self.details.addTab(self.recent_history, gettext("Recent History"))
        self.details.addTab(self.local_changes, gettext("Local Changes"))
        self.details.addTab(self.missing_revisions, gettext("Missing Revisions"))
        self.details.connect(self.details, QtCore.SIGNAL("currentChanged(int)"),
            self._refresh_branch_info)

        # Show either the details or a message
        no_details_msg = QtGui.QTextBrowser()
        no_details_msg.setText(gettext("No details"))
        cannot_open_msg = QtGui.QTextBrowser()
        cannot_open_txt = gettext("This location cannot be opened. "
            "It may have been deleted. Please refresh this page.")
        cannot_open_msg.setText(html_status_message(ERROR_STATUS,
            cannot_open_txt))
        stacked = QtGui.QStackedWidget()
        stacked.addWidget(no_details_msg)
        stacked.addWidget(self.details)
        stacked.addWidget(cannot_open_msg)
        return stacked

    def ui(self):
        return self.stacked

    def show_details(self, kind, control_obj):
        self.refreshed_tabs = set()
        if kind is None:
            self.branch = None
            self.stacked.setCurrentIndex(0)
            return
        if not control_obj.bzrdir.root_transport.has('.'):
            # no longer accessible
            self.stacked.setCurrentIndex(2)
            self.branch = None
        elif kind in [REPOSITORY_KIND, CHECKOUT_KIND]:
            # nothing to show (that might change in the future though)
            self.stacked.setCurrentIndex(0)
            self.branch = None
        else:
            self.stacked.setCurrentIndex(1)
            self.branch = control_obj
            try:
                self.tree = control_obj.bzrdir.open_workingtree()
            except (errors.NoWorkingTree, errors.NotLocalUrl):
                self.tree = None
            self._refresh_branch_info()

    def _refresh_branch_info(self):
        current = self.details.currentIndex()
        if current in self.refreshed_tabs:
            return
        fetching = gettext("Fetching ...")
        if current == 0:
            self.recent_history.setText(fetching)
            def task():
                rqst = log.make_log_request_dict(limit=self.recent_history_limit)
                text = log_as_html(self.branch, rqst)
                self.recent_history.setText(text)
            msg = gettext("Fetching recent history")
            self._action_callback("task", (msg, task))
        elif current == 1:
            self.local_changes.setText(fetching)
            def task():
                status_report = StatusReport(self.branch, self.tree)
                status_summary = status_report.overall_summary()
                unique_revs = extra_revisions_as_html(self.branch)
                text = "%s<br>%s" % (status_summary, unique_revs)
                self.local_changes.setText(text)
            msg = gettext("Fetching local changes")
            self._action_callback("task", (msg, task))
        elif current == 2:
            self.missing_revisions.setText(fetching)
            def task():
                text = extra_revisions_as_html(self.branch, in_parent=True)
                self.missing_revisions.setText(text)
            msg = gettext("Fetching missing revisions")
            self._action_callback("task", (msg, task))
        self.refreshed_tabs.add(current)
