# 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 os, sys
from PyQt4 import QtCore, QtGui

from bzrlib import (
    bzrdir,
    osutils,
    registry,
    urlutils,
    )
from bzrlib.urlutils import strip_trailing_slash

from bzrlib.plugins.explorer.lib import (
    experimental_mode,
    )
from bzrlib.plugins.explorer.lib.extensions import bookmarks
from bzrlib.plugins.explorer.lib.kinds import (
    BRANCH_KIND,
    BOUND_BRANCH_KIND,
    CHECKOUT_KIND,
    REPOSITORY_KIND,
    VIRTUAL_REPO_KIND,
    GENERIC_VIEW,
    STATUS_VIEW,
    SUBMIT_VIEW,
    icon_for_kind,
    )
from bzrlib.plugins.explorer.lib.i18n import gettext, N_


class ViewInfo(object):
    """Information about a view."""

    def __init__(self, widget_class, label, icon_kind=None, description=None):
        self.widget_class = widget_class
        self.label = label
        self.icon_kind = icon_kind
        self.description = description or label

    def get_icon(self):
        if self.icon_kind is None:
            return icon_for_kind(GENERIC_VIEW)
        else:
            return icon_for_kind(self.icon_kind)


# Registry of widgets to display indexed by kind. Each entry is
# actually a list of ViewInfo objects, allowing plugins
# to add tabs to the beginning or end, or override widgets if they
# really feel that's necessary. If the list has multiple entries,
# each widget is displayed on a tab with the matching label.
# If the list has 1 entry, just the widget is displayed.
# If the list has no entries, a simple message is displayed.
# Each widget is created via widget-class(model).ui() where
# model is the LocationModel.
view_registry = registry.Registry()


class _DefaultView(object):
    """The view displayed when no other view is defined."""

    def __init__(self, model, action_callback):
        self.model = model
        # No support for calling back actions here

    def ui(self):
        ui = QtGui.QTextBrowser()
        ui.setText(self.model.display_diagnostics())
        return ui

    def refresh_view(self):
        pass


class _AbstractLocationModel(object):
    """Base class for models that get displayed as locations."""

    def kind(self):
        """What's the kind of this location?"""
        return None

    def category(self):
        """Return one of local, remote, None."""
        return None

    def display_name(self):
        """Return a name suitable for displaying in a tab or title bar."""
        return NotImplementedError(self.display_name)

    def display_local_name(self):
        """Return a name to display - context is implied. """
        return None

    def display_icon(self):
        return None

    def display_tooltip(self):
        return self.display_name()

    def view(self):
        """Return the widget to display for this location.

        :result: a QWidget
        """
        return NotImplementedError(self.view)

    def reopen_location(self):
        """Re-open this location."""
        return

    def refresh_view(self):
        """Refresh the view for this location."""
        return

    def context(self):
        """Return a dictionary of context information."""
        return {}

    def group(self):
        """Return the group key or None if not part of a group."""
        return None
    
    def supports_wt(self):
        """Return true if model should have a working tree"""
        return False

    def has_wt(self):
        """Return true if model has a working tree"""
        return False


class ExtensibleLocationModel(_AbstractLocationModel):
    """A location model that can be extended via the view registry."""

    def __init__(self, location, action_callback=None, dry_run=False):
        if location.startswith("."):
            self.location = osutils.abspath(location)
        elif location.startswith("file://"):
            self.location = urlutils.local_path_from_url(location)
        else:
            self.location = location
        self.action_callback = action_callback
        self.dry_run = dry_run
        self.reopen_location()

    def view(self):
        """Return the widget to display for this location.

        :result: a QWidget
        """
        # See the comment where view_registry is defined.
        view_info_list = [ViewInfo(_DefaultView, gettext('Default'))]
        if not self.dry_run:
            try:
                view_info_list = view_registry.get(self.kind())
            except KeyError:
                pass
        if len(view_info_list) == 1:
            klass = view_info_list[0].widget_class
            view = klass(self, self.action_callback)
            self._views = [view]
            return view.ui()
        else:
            views = []
            tabs = QtGui.QTabWidget()
            # Note: East and West rotate both the text and icon which ends up
            # being a bad thing because then the status icon ('commit') looks
            # like the push or pull one. I suspect East and West isn't
            # portable to OS X as well, so stick with plain ol' North or South.
            tabs.setTabPosition(QtGui.QTabWidget.North)
            for view_info in view_info_list:
                klass = view_info.widget_class
                view = klass(self, self.action_callback)
                views.append(view)
                icon = view_info.get_icon()
                index = tabs.addTab(view.ui(), icon, view_info.label)
                tabs.setTabToolTip(index, view_info.description)
            self._views = views
            return tabs

    def refresh_view(self):
        for view in self._views:
            view.refresh_view()

    def url_for_display(self, url):
        """Return human-readable URL or local path for file:/// URLs.
        Wrapper around bzrlib.urlutils.unescape_for_display
        """
        if not url:
            return url
        return urlutils.unescape_for_display(url, 'utf-8')

    def display_icon(self):
        return icon_for_kind(self.kind())

    def display_tooltip(self):
        kind = self.kind()
        return "%s %s" % (gettext(kind), self.location)


class LocationModel(ExtensibleLocationModel):

    def reopen_location(self):
        (tree, branch, repository, relpath) = \
            bzrdir.BzrDir.open_containing_tree_branch_or_repository(
            self.location)
        self.branch = branch
        self.repository = repository
        self.tree = tree

        # We explicitly throw away the relative path information
        self.relpath = None

        # Set the interesting attributes of this location
        self._local = True
        self._bound = False
        self._shared = False
        self._branch_root = None
        self._repo_root = None
        if branch:
            if tree:
                tree_base = tree.bzrdir.root_transport.base
            else:
                tree_base =  None
            br_base = branch.bzrdir.root_transport.base
            self._branch_root = strip_trailing_slash(br_base)
            # check for (lightweight) checkout
            if tree_base and tree_base != br_base:
                self.interesting_object = tree
                self.interesting_kind = CHECKOUT_KIND
                self._root = strip_trailing_slash(tree_base)
            else:
                self.interesting_object = branch
                # If this enough? Location might be set but not bound?
                if branch.get_bound_location() is not None:
                    self.interesting_kind = BOUND_BRANCH_KIND
                else:
                    self.interesting_kind = BRANCH_KIND
                self._root = self._branch_root
            self._local = self._root.startswith("file://")
            if repository:
                self._repo_root = strip_trailing_slash(
                    repository.bzrdir.root_transport.external_url())
        elif repository:
            self.interesting_object = repository
            self.interesting_kind = REPOSITORY_KIND
            self._repo_root = strip_trailing_slash(
                repository.bzrdir.root_transport.external_url())
            self._root = self._repo_root
            self._local = self._root.startswith("file://")
            self._shared = repository.is_shared()
        self._context = {
            "root": self._root,
            "relpath": self.relpath,
            }
        if self._branch_root is not None:
            self._context['branch_root'] = self._branch_root
        if self._repo_root is not None:
            self._context['repo_root'] = self._repo_root

    def kind(self):
        """What's the kind of this location?

        :return: one of repository, branch, bound branch or checkout.
          Note that 'checkout' imples lightweight checkout.
         (A heavyweight checkout is called a bound branch in explorer.)
        """
        return self.interesting_kind

    def supports_wt(self):
        """Return True if model should have a working tree"""
        return (self.interesting_kind in [
            BRANCH_KIND,
            BOUND_BRANCH_KIND,
            CHECKOUT_KIND,
            ])

    def has_wt(self):
        """Return True if model has a working tree"""
        return (self.supports_wt() and self.tree is not None)

    def category(self):
        """Return one of local, remote, None."""
        return {True: "local", False: "remote"}[self._local]

    def shared(self):
        """Is the repository shared?"""
        return self._shared

    def working_tree(self):
        """Does this location have a working tree?"""
        return self.tree is not None

    def context(self):
        """Return a dictionary of context information.

        Keys in the result are:

        * root - the root of the location (branch, checkout or repository)
        * selected - a space separated list of paths (relative to root)
        * filename - first selected item filename (relative to root)
        * dirname - first selected item directory name (relative to root)
        * basename - first selected item basename (relative to dirname)
        * branch_root - the root of the branch (for a checkout or branch)
        * repo_root - the root of the repository (for a checkout, branch or
              repository)
        """
        return self._context

    def _get_name_details(self):
        """Return the kind and naming details."""
        # Get the pieces
        kind = self.kind()
        locations = {
                'this_location': None,
                'master_location': None,
                'repository': None,
                }
        shared_tree_repo_found = False
        def format_location(location):
            return self.url_for_display(strip_trailing_slash(location))
        if kind == CHECKOUT_KIND:
            locations['this_location'] = format_location(self._root)
            locations['master_location'] = format_location(self.branch.base)
        elif kind == BOUND_BRANCH_KIND:
            locations['this_location'] = format_location(self.branch.base)
            locations['master_location'] = format_location(self.branch.get_bound_location())
        elif kind == BRANCH_KIND:
            locations['this_location'] = format_location(self.branch.base)
        if self.repository:
            repository_location = format_location(self.repository.bzrdir.root_transport.external_url())
            if repository_location != locations['this_location']:
                locations['repository'] = repository_location
                if os.path.basename(locations['repository']) == '.bzrbranches':
                    locations['repository'] = os.path.split(locations['repository'])[0]
                    shared_tree_repo_found = True
                elif locations['repository'].endswith('/.bzr/branches'):
                    locations['repository'] = locations['repository'][:-len('/.bzr/branches')]
                    shared_tree_repo_found = True
        for key in locations.keys():
            if locations[key] is not None:
                locations[key + '_base'] = os.path.basename(locations[key])
            else:
                locations[key + '_base'] = None
        return kind, locations, shared_tree_repo_found

    def display_name(self):
        """Return a name suitable for displaying in a tab or title bar.
        
        We try to provide all the essential information as consisely as
        we can. The output is dependent on what type of location this is.
        Only the basename of checkouts, branches and repositories are shown.
        Relative paths are only shown if interesting. Examples are:
        
          branch [repository]
          [repository]
          checkout -> branch [repository]
        """
        kind, locations, shared_tree_repo_found = self._get_name_details()
        if kind == BRANCH_KIND:
            if locations['repository'] is not None:
                format = "%(this_location_base)s [%(repository_base)s]"
            else:
                format = "%(this_location_base)s"
        elif kind == CHECKOUT_KIND:
            format = "%(this_location_base)s -> %(master_location_base)s"
            if locations['repository'] is not None and not shared_tree_repo_found:
                format += " %(repository)s"
        elif kind == BOUND_BRANCH_KIND:
            format = "%(this_location_base)s -> %(master_location_base)s"
        elif kind == REPOSITORY_KIND:
            format = "[%(repository_base)s]"
        else:
            format = None
        return format % locations

    def display_local_name(self):
        """Return a name to display - context is implied. """
        kind, locations, _ = self._get_name_details()
        if kind == BRANCH_KIND:
            format = "%(this_location_base)s"
        elif kind in [CHECKOUT_KIND, BOUND_BRANCH_KIND]:
            format = "%(this_location_base)s -> %(master_location_base)s"
        elif kind == REPOSITORY_KIND:
            # Just the icon would do but it looks too plain if that's
            # the only object open. If the space could be better used
            # for branch names, maybe shorten this to "Repo" (or just
            # "R" later ...
            format = gettext("Repository")
        else:
            format = None
        return format % locations

    def display_tooltip(self):
        kind, locations, shared_tree_repo_found = self._get_name_details()
        format = kind + " "
        if kind == BRANCH_KIND:
            format += "%(this_location)s"
            if locations['repository'] is not None:
                format += "\n" + gettext("in repository") + " %(repository)s"
        elif kind == CHECKOUT_KIND:
            format += "%(this_location)s\n-> %(master_location)s"
            if locations['repository'] is not None and not shared_tree_repo_found:
                format += "\n" + gettext("in repository") + " %(repository)s"
        elif kind == BOUND_BRANCH_KIND:
            format += "%(this_location)s\n-> %(master_location)s"
        elif kind == REPOSITORY_KIND:
            format += "%(this_location)s"

        return format % locations

    def display_diagnostics(self):
        """Return diagnostic information as a string.
        
        The output may be HTML.
        """
        locations = "Locations", [
            ("Path",self.location),
            ("Root", self._root),
            ("Branch Root", self._branch_root),
            ("Repository Root", self._repo_root),
            ]
        attrs = "Attributes", [
            ("Kind", self.kind()),
            ("Category", self.category()),
            ("WorkingTree", self.working_tree()),
            ("Shared", self.shared()),
            ]
        text = ''
        for section in [locations, attrs]:
            title, data = section
            text += "<b>%s:</b><br>" % title
            for n, v in data:
                text += "%s = %s<br>" % (n, v)
            text += "<br>"
        return text

    def group(self):
        """Return the group key or None if not part of a group."""
        return self._repo_root


class VirtualRepoModel(ExtensibleLocationModel):
    """A workspace page."""

    def kind(self):
        """What's the kind of this location?"""
        return VIRTUAL_REPO_KIND

    def display_name(self):
        """Return a name suitable for displaying in a tab or title bar."""
        return osutils.basename(self.location)

    def working_tree(self):
        """Does this location have a working tree?"""
        return False

    def category(self):
        """Return one of local, remote, None."""
        # TODO: detect remote virtual repositories
        return "local"

    def context(self):
        """Return a dictionary of context information."""
        return {"root": urlutils.local_path_to_url(self.location)}


# Register the standard per-kind views
from bzrlib.plugins.explorer.lib.view_repository import (
    RepositoryView,
    VirtualRepoView,
    )
from bzrlib.plugins.explorer.lib.view_workingtree import WorkingTreeView

_status_title = gettext('Working Tree Status')
_status_desc  = gettext('Working Tree Status (Commit Preview)')
views = [ViewInfo(WorkingTreeView, _status_title, STATUS_VIEW, _status_desc)]
for kind in (BRANCH_KIND, BOUND_BRANCH_KIND, CHECKOUT_KIND):
    view_registry.register(kind, views)

_repo_overview_desc = gettext('Repository Overview')
view_registry.register(REPOSITORY_KIND, [
    ViewInfo(RepositoryView, gettext("Overview"), None, _repo_overview_desc),
    ])
view_registry.register(VIRTUAL_REPO_KIND, [
    ViewInfo(VirtualRepoView, gettext("Overview"), None, _repo_overview_desc),
    ])
