# 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 cgi import escape

from bzrlib import conflicts as mod_conflicts
from bzrlib import errors

from bzrlib.plugins.explorer.lib import (
    status_data,
    )
from bzrlib.plugins.explorer.lib.kinds import (
    ERROR_STATUS,
    WARNING_STATUS,
    GOOD_STATUS,
    html_status_message,
    html_action_links,
    path_for_icon,
    )
from bzrlib.plugins.explorer.lib.i18n import gettext, N_


# The colours used for section headings
_ALERT_COLOUR = "red"
_NOTICE_COLOUR = "purple"


# The separator between action links and associated text
_ACTION_SEP = "&nbsp;" * 2


## Define some decorators ##

def _edit_decorator(name, text=None):
    return _edit_with_actions_decorator(name, [], text)


def _edit_with_actions_decorator(name, actions, text=None):
    if text is None:
        text = name
    if not isinstance(text, basestring):
        text = unicode(text)
    edit_link = "edit %s" % (name,)
    edit_part ='<a href="%s">%s</a>' % (edit_link, escape(text))
    if actions:
        actions_part = html_action_links(actions, name)
        return _ACTION_SEP.join([actions_part, edit_part])
    else:
        return edit_part


def _text_decorator(text):
    return _text_with_actions_decorator(text, [])


def _text_with_actions_decorator(text, actions):
    if not isinstance(text, basestring):
        text = unicode(text)
    text_part ='%s' % escape(text)
    # We explicitly put the actions on the end here so that text
    # without actions doesn't look incorrectly indented
    if actions:
        actions_part = html_action_links(actions)
        return _ACTION_SEP.join([text_part, actions_part])
    else:
        return text_part


class _BaseStatusReport(object):

    def full_report(self):
        header = [self.overall_summary()]
        body = self.body_lines()
        footer = self.whats_next()
        if footer:
            title = self._line_break() + self._format_title(
                gettext("What's next?"), _NOTICE_COLOUR)
            footer = [title] + footer
        html_lines = header + body + footer
        #text = "<br>".join(html_lines)
        return html_lines

    def overall_summary(self):
        """Return HTML text for the report's overall summary."""
        return NotImplementedError(self.overall_summary)

    def body_lines(self):
        """Return HTML lines for the report's body."""
        return []

    def whats_next(self):
        """Return HTML lines for the what's next suggestions."""
        return []

    # Sections to show in the alert colour
    alert_sections = ['Conflicts', 'Unversioned', 'Non-existents']

    # Quick action links by section
    actions_map = {
        'Conflicts':          ['conflicts'],
        'Unversioned':        ['add', 'ignore'],
        'Pending merge tips': ['log'],
    }

    # Formatters by section
    def _conflicts_formatter(conflict):
        if isinstance(conflict, mod_conflicts.TextConflict):
            return _edit_decorator(conflict.path, conflict)
        else:
            # It may be useful to use an "open" decorator
            # for other conflicts with a path, as long
            # as the path exists in the working tree.
            # For now, we just display the text until we're
            # sure we want that.
            return _text_decorator(conflict)
    def _unversioned_formatter(items):
        return _edit_with_actions_decorator(items[0], ['delete'])
    def _added_formatter(items):
        return _edit_decorator(items[0])
    def _deleted_formatter(items):
        return items[0]
    def _renamed_formatter(items):
        text = "%s => %s" % (items[0], items[1])
        return _edit_decorator(items[1], text)
    def _kind_changed_formatter(items):
        text = "%s (%s => %s)" % (items[0], items[2], items[3])
        return _edit_decorator(items[0], text)
    def _modified_formatter(items):
        return _edit_with_actions_decorator(items[0], ['diff'])
    formatter_map = {
        'Conflicts':    _conflicts_formatter,
        'Unversioned':  _unversioned_formatter,
        'Added':        _added_formatter,
        'Deleted':      _deleted_formatter,
        'Renamed':      _renamed_formatter,
        'Kind changed': _kind_changed_formatter,
        'Modified':     _modified_formatter,
    }

    def section_lines(self, items, name):
        if not items:
            return []
        title = "%s (%d)" % (gettext(name), len(items))
        if name in self.alert_sections:
            colour = _ALERT_COLOUR
        else:
            colour = _NOTICE_COLOUR
        actions = self.actions_map.get(name, [])
        title = self._format_title(title, colour, actions)
        formatter = self.formatter_map.get(name, unicode)
        return [title] + map(formatter, items)

    def _format_title(self, title, colour=None, actions=None):
        if colour:
            title = "<font color=%s>%s</font>" % (colour, title)
        if actions:
            title = _ACTION_SEP.join([title, html_action_links(actions)])
        return "<br><b>%s</b>" % (title,)

    def _format_next_action(self, action):
        # We use the long descriptions for menu options.
        # TODO: Look-up the text help from a central registry instead of
        # repeating it here when there is a single action
        texts = {
            'conflicts': "Resolve conflicts",
            'diff': "Show differences that will be committed",
            'commit': "Commit the current content as a new revision",
        }
        text = texts.get(action, action)
        return _text_with_actions_decorator(gettext(text), [action])

    def _line_break(self):
        # We may want to style this up in the future
        return '<hr>'


class StatusReport(_BaseStatusReport):

    def __init__(self, branch, wt, relpath=None):
        self.branch = branch
        self.tree = wt
        self.relpath = relpath
        self._collect_data()

    def _collect_data(self):
        wt = self.tree
        self._data = None
        self._exception = None
        if wt is None:
            return
        relpath = self.relpath
        if relpath:
            specific_files = [relpath]
            pending_merges = None
        else:
            specific_files = None
            pending_merges = status_data.get_pending_merges(wt)
        try:
            delta, conflicts, nonexistents = status_data.get_status_info(wt,
                specific_files=specific_files, versioned=False)
            if (delta.has_changed() or delta.unversioned or len(conflicts) > 0
                or nonexistents or pending_merges):
                self._data = (delta, conflicts, nonexistents, pending_merges)
        except errors.BzrError, ex:
            self._exception = ex

    def overall_summary(self):
        if self._exception:
            return html_status_message(ERROR_STATUS, unicode(self._exception))
        elif self._data:
            indicator = WARNING_STATUS
            msg = gettext("Working tree differs from revision %s.")
        elif self.tree:
            indicator = GOOD_STATUS
            msg = gettext("Working tree is up to date at revision %s.")
        else:
            indicator = GOOD_STATUS
            msg = gettext(
                "This branch has no working tree. Last revision is %s.")
        revno = self.branch.last_revision_info()[0]
        return html_status_message(indicator, msg % (revno,))

    def body_lines(self):
        """Generate the lines in the report body."""
        lines = []
        if self._data:
            (delta, conflicts, nonexistents, pending_merges) = self._data
            modified = self._collect_modified(delta)
            lines.extend(self.section_lines(conflicts, N_("Conflicts")))
            lines.extend(self.section_lines(nonexistents, N_("Non-existents")))
            lines.extend(self.section_lines(delta.unversioned, N_("Unversioned")))
            lines.extend(self.section_lines(delta.added, N_("Added")))
            lines.extend(self.section_lines(delta.removed, N_("Deleted")))
            lines.extend(self.section_lines(delta.renamed, N_("Renamed")))
            lines.extend(self.section_lines(delta.kind_changed, N_("Kind changed")))
            lines.extend(self.section_lines(modified, N_("Modified")))
            lines.extend(self.section_lines(pending_merges, N_("Pending merge tips")))
        return lines

    def _collect_modified(self, delta):
        modified = []
        modified.extend(delta.modified)
        for old,newpath,id_,kind,text_modified,meta_modified in delta.renamed:
            if text_modified or meta_modified:
                modified.append((newpath,id_,kind,text_modified,meta_modified))
        return sorted(modified)

    def whats_next(self):
        """Return HTML lines for the what's next suggestions."""
        lines = []
        if self._data:
            (delta, conflicts, nonexistents, pending_merges) = self._data
            if len(conflicts) > 0:
                lines.append(self._format_next_action('conflicts'))
            if len(delta.unversioned) > 0:
                # TODO: Split this into multiple actions later
                text = gettext("Add, ignore or delete unversioned items")
                lines.append(_text_with_actions_decorator(text, ['add', 'ignore']))
            if len(lines) == 0:
                if delta.has_changed():
                    lines.append(self._format_next_action('diff'))
                if delta.has_changed() or pending_merges:
                    lines.append(self._format_next_action('commit'))
        return lines


class SubmitReport(_BaseStatusReport):

    # Formatters by section
    def _deleted_formatter(items):
        return items[0]
    def _renamed_formatter(items):
        text = "%s => %s" % (items[0], items[1])
        return _edit_decorator(items[1], text)
    def _kind_changed_formatter(items):
        text = "%s (%s => %s)" % (items[0], items[2], items[3])
        return _edit_decorator(items[0], text)

    # Create formatters on the fly where diff is used as
    # the ancestor branch varies
    def _make_diff_based_formatter(self):
        if self.target == 'parent':
            parent_branch = self.branch.get_parent()
            diff_path = 'diff(-rancestor:%s..-1)' % parent_branch
        else:
            diff_path = 'diff(-rsubmit:..-1)'
        def _diff_based_formatter(items):
            return _edit_with_actions_decorator(items[0], [diff_path])
        return _diff_based_formatter

    formatter_map = {
        'Added':        None, # Added in _collect_data
        'Deleted':      _deleted_formatter,
        'Renamed':      _renamed_formatter,
        'Kind changed': _kind_changed_formatter,
        'Modified':     None, # Added in _collect_data
    }

    def __init__(self, branch, target='submit'):
        self.branch = branch
        self.target = target
        self._collect_data()

    def _collect_data(self):
        self._data = None
        self._exception = None
        try:
            if self.target == 'submit':
                delta = status_data.get_submission_info(self.branch)
            elif self.target == 'parent':
                delta = status_data.get_parent_submission_info(self.branch)
            else:
                raise errors.BzrError("Unrecognised target type")
            self.formatter_map['Added'] = self._make_diff_based_formatter()
            self.formatter_map['Modified'] = self._make_diff_based_formatter()
            if delta.has_changed():
                self._data = delta
        except errors.BzrError, ex:
            self._exception = ex

    def overall_summary(self):
        if self._exception:
            return html_status_message(ERROR_STATUS, unicode(self._exception))
        elif self._data:
            indicator = WARNING_STATUS
            msg = gettext("Branch has changes not present in its %s branch." % self.target)
        else:
            indicator = GOOD_STATUS
            msg = gettext("Branch is fully merged into its %s branch." % self.target)
        return html_status_message(indicator, msg)

    def body_lines(self):
        """Generate the lines in the report body."""
        lines = []
        if self._data:
            delta = self._data
            lines.extend(self.section_lines(delta.added, "Added"))
            lines.extend(self.section_lines(delta.removed, "Deleted"))
            lines.extend(self.section_lines(delta.renamed, "Renamed"))
            lines.extend(self.section_lines(delta.kind_changed, "Kind changed"))
            lines.extend(self.section_lines(delta.modified, "Modified"))
        return lines

    def whats_next(self):
        """Return HTML lines for the what's next suggestions."""
        lines = []
        if self._data:
            delta = self._data
            if delta.has_changed():
                open_parent_msg = gettext(
                    "Open %s branch and merge these changes" % self.target)
                lines.append(_text_decorator(open_parent_msg))
        return lines
