# Copyright 2014 OpenStack Foundation
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import collections
import datetime
import logging
try:
    import ordereddict
except:
    pass
import textwrap

from six.moves.urllib import parse as urlparse
import urwid

from gertty import gitrepo
from gertty import keymap
from gertty import mywid
from gertty import sync
from gertty.view import side_diff as view_side_diff
from gertty.view import unified_diff as view_unified_diff
from gertty.view import mouse_scroll_decorator
import gertty.view

try:
    OrderedDict = collections.OrderedDict
except AttributeError:
    OrderedDict = ordereddict.OrderedDict

class EditTopicDialog(mywid.ButtonDialog):
    signals = ['save', 'cancel']
    def __init__(self, app, topic):
        self.app = app
        save_button = mywid.FixedButton('Save')
        cancel_button = mywid.FixedButton('Cancel')
        urwid.connect_signal(save_button, 'click',
                             lambda button:self._emit('save'))
        urwid.connect_signal(cancel_button, 'click',
                             lambda button:self._emit('cancel'))
        super(EditTopicDialog, self).__init__("Edit Topic",
                                              "Edit the change topic.",
                                              entry_prompt="Topic: ",
                                              entry_text=topic,
                                              buttons=[save_button,
                                                       cancel_button],
                                              ring=app.ring)

    def keypress(self, size, key):
        if not self.app.input_buffer:
            key = super(EditTopicDialog, self).keypress(size, key)
        keys = self.app.input_buffer + [key]
        commands = self.app.config.keymap.getCommands(keys)
        if keymap.ACTIVATE in commands:
            self._emit('save')
            return None
        return key

class EditHashtagsDialog(mywid.ButtonDialog):
    signals = ['save', 'cancel']
    def __init__(self, app, hashtags):
        self.app = app
        save_button = mywid.FixedButton('Save')
        cancel_button = mywid.FixedButton('Cancel')
        urwid.connect_signal(save_button, 'click',
                             lambda button:self._emit('save'))
        urwid.connect_signal(cancel_button, 'click',
                             lambda button:self._emit('cancel'))
        super(EditHashtagsDialog, self).__init__("Edit Hashtags",
                                                 "Edit the change hashtags.",
                                                 entry_prompt="Hashtags: ",
                                                 entry_text=hashtags,
                                                 buttons=[save_button,
                                                          cancel_button],
                                                 ring=app.ring)

    def keypress(self, size, key):
        if not self.app.input_buffer:
            key = super(EditHashtagsDialog, self).keypress(size, key)
        keys = self.app.input_buffer + [key]
        commands = self.app.config.keymap.getCommands(keys)
        if keymap.ACTIVATE in commands:
            self._emit('save')
            return None
        return key

class CherryPickDialog(urwid.WidgetWrap, mywid.LineBoxTitlePropertyMixin):
    signals = ['save', 'cancel']

    def _cherry_pick_message(self, change):
        revision = change.revisions[-1]
        return "{}\n(cherry picked from commit {})".format(
            revision.message.rstrip('\n'),
            revision.commit,
        )

    def __init__(self, app, change):
        save_button = mywid.FixedButton('Propose Change')
        cancel_button = mywid.FixedButton('Cancel')
        urwid.connect_signal(save_button, 'click',
                             lambda button:self._emit('save'))
        urwid.connect_signal(cancel_button, 'click',
                             lambda button:self._emit('cancel'))
        button_widgets = [('pack', save_button),
                          ('pack', cancel_button)]
        button_columns = urwid.Columns(button_widgets, dividechars=2)
        rows = []
        self.entry = mywid.MyEdit(edit_text=self._cherry_pick_message(change),
                                  multiline=True, ring=app.ring)
        self.branch_buttons = []
        rows.append(urwid.Text(u"Branch:"))
        for branch in change.project.branches:
            b = mywid.FixedRadioButton(self.branch_buttons, branch.name,
                                       state=(branch.name == change.branch))
            rows.append(b)
        rows.append(urwid.Divider())
        rows.append(urwid.Text(u"Commit message:"))
        rows.append(self.entry)
        rows.append(urwid.Divider())
        rows.append(button_columns)
        pile = urwid.Pile(rows)
        fill = urwid.Filler(pile, valign='top')
        super(CherryPickDialog, self).__init__(urwid.LineBox(fill,
                                                             'Propose Change to Branch'))

class ReviewDialog(urwid.WidgetWrap, mywid.LineBoxTitlePropertyMixin):
    signals = ['submit', 'save', 'cancel']
    def __init__(self, app, revision_key, message=''):
        self.revision_key = revision_key
        self.app = app
        save_button = mywid.FixedButton(u'Save')
        submit_button = mywid.FixedButton(u'Save and Submit')
        cancel_button = mywid.FixedButton(u'Cancel')
        urwid.connect_signal(save_button, 'click',
            lambda button:self._emit('save'))
        urwid.connect_signal(submit_button, 'click',
            lambda button:self._emit('submit'))
        urwid.connect_signal(cancel_button, 'click',
            lambda button:self._emit('cancel'))

        rows = []
        categories = []
        values = {}
        descriptions = {}
        self.button_groups = {}
        with self.app.db.getSession() as session:
            revision = session.getRevision(self.revision_key)
            change = revision.change
            buttons = [('pack', save_button)]
            if revision.can_submit:
                buttons.append(('pack', submit_button))
            buttons.append(('pack', cancel_button))
            buttons = urwid.Columns(buttons, dividechars=2)
            if revision == change.revisions[-1]:
                for label in change.labels:
                    d = descriptions.setdefault(label.category, {})
                    d[label.value] = label.description
                    vmin = d.setdefault('min', label.value)
                    if label.value < vmin:
                        d['min'] = label.value
                    vmax = d.setdefault('max', label.value)
                    if label.value > vmax:
                        d['max'] = label.value
                for label in change.permitted_labels:
                    if label.category not in categories:
                        categories.append(label.category)
                        values[label.category] = []
                    values[label.category].append(label.value)
                draft_approvals = {}
                prior_approvals = {}
                for approval in change.approvals:
                    if self.app.isOwnAccount(approval.reviewer):
                        if approval.draft:
                            draft_approvals[approval.category] = approval
                        else:
                            prior_approvals[approval.category] = approval
                for category in categories:
                    rows.append(urwid.Text(category))
                    group = []
                    self.button_groups[category] = group
                    current = draft_approvals.get(category)
                    if current is None:
                        current = prior_approvals.get(category)
                    if current is None:
                        current = 0
                    else:
                        current = current.value
                    for value in sorted(values[category], reverse=True):
                        if value > 0:
                            strvalue = '+%s' % value
                        elif value == 0:
                            strvalue = ' 0'
                        else:
                            strvalue = str(value)
                        strvalue += '  ' + descriptions[category][value]
                        b = urwid.RadioButton(group, strvalue, state=(value == current))
                        b._value = value
                        if value > 0:
                            if value == descriptions[category]['max']:
                                b = urwid.AttrMap(b, 'max-label')
                            else:
                                b = urwid.AttrMap(b, 'positive-label')
                        elif value < 0:
                            if value == descriptions[category]['min']:
                                b = urwid.AttrMap(b, 'min-label')
                            else:
                                b = urwid.AttrMap(b, 'negative-label')
                        rows.append(b)
                    rows.append(urwid.Divider())
            m = revision.getPendingMessage()
            if not m:
                m = revision.getDraftMessage()
            if m:
                message = m.message
        self.message = mywid.MyEdit(u"Message: \n", edit_text=message,
                                    multiline=True, ring=app.ring)
        rows.append(self.message)
        rows.append(urwid.Divider())
        rows.append(buttons)
        pile = urwid.Pile(rows)
        fill = urwid.Filler(pile, valign='top')
        super(ReviewDialog, self).__init__(urwid.LineBox(fill, 'Review'))

    def getValues(self):
        approvals = {}
        for category, group in self.button_groups.items():
            for button in group:
                if button.state:
                    approvals[category] = button._value
        message = self.message.edit_text.strip()
        return (approvals, message)

    def keypress(self, size, key):
        if not self.app.input_buffer:
            key = super(ReviewDialog, self).keypress(size, key)
        keys = self.app.input_buffer + [key]
        commands = self.app.config.keymap.getCommands(keys)
        if keymap.PREV_SCREEN in commands:
            self._emit('cancel')
            return None
        return key

class ReviewButton(mywid.FixedButton):
    def __init__(self, revision_row):
        super(ReviewButton, self).__init__(('revision-button', u'Review'))
        self.revision_row = revision_row
        self.change_view = revision_row.change_view
        urwid.connect_signal(self, 'click',
            lambda button: self.openReview())

    def openReview(self, message=''):
        self.dialog = ReviewDialog(self.change_view.app,
                                   self.revision_row.revision_key,
                                   message=message)
        urwid.connect_signal(self.dialog, 'save',
            lambda button: self.closeReview(True, False))
        urwid.connect_signal(self.dialog, 'submit',
            lambda button: self.closeReview(True, True))
        urwid.connect_signal(self.dialog, 'cancel',
            lambda button: self.closeReview(False, False))
        self.change_view.app.popup(self.dialog,
                                   relative_width=50, relative_height=75,
                                   min_width=60, min_height=20)

    def closeReview(self, upload, submit):
        approvals, message = self.dialog.getValues()
        self.change_view.saveReview(self.revision_row.revision_key, approvals,
                                    message, upload, submit)
        self.change_view.app.backScreen()

class RevisionRow(urwid.WidgetWrap):
    revision_focus_map = {
                          'revision-name': 'focused-revision-name',
                          'revision-commit': 'focused-revision-commit',
                          'revision-comments': 'focused-revision-comments',
                          'revision-drafts': 'focused-revision-drafts',
                          }

    def __init__(self, app, change_view, repo, revision, expanded=False):
        super(RevisionRow, self).__init__(urwid.Pile([]))
        self.app = app
        self.change_view = change_view
        self.revision_key = revision.key
        self.project_name = revision.change.project.name
        self.commit_sha = revision.commit
        self.can_submit = revision.can_submit
        self.title = mywid.TextButton(u'', on_press = self.expandContract)
        table = mywid.Table(columns=3)
        total_added = 0
        total_removed = 0
        for rfile in revision.files:
            if rfile.status is None:
                continue
            added = rfile.inserted or 0
            removed = rfile.deleted or 0
            total_added += added
            total_removed += removed
            table.addRow([urwid.Text(('filename', rfile.display_path), wrap='clip'),
                          urwid.Text([('lines-added', '+%i' % (added,)), ', '],
                                     align=urwid.RIGHT),
                          urwid.Text(('lines-removed', '-%i' % (removed,)))])
        table.addRow([urwid.Text(''),
                      urwid.Text([('lines-added', '+%i' % (total_added,)), ', '],
                                 align=urwid.RIGHT),
                      urwid.Text(('lines-removed', '-%i' % (total_removed,)))])
        table = urwid.Padding(table, width='pack')

        focus_map={'revision-button': 'focused-revision-button'}
        self.review_button = ReviewButton(self)
        buttons = [self.review_button,
                   mywid.FixedButton(('revision-button', "Diff"),
                                     on_press=self.diff),
                   mywid.FixedButton(('revision-button', "Local Checkout"),
                                     on_press=self.checkout),
                   mywid.FixedButton(('revision-button', "Local Cherry-Pick"),
                                     on_press=self.cherryPick)]
        if self.can_submit:
            buttons.append(mywid.FixedButton(('revision-button', "Submit"),
                                             on_press=lambda x: self.change_view.doSubmitChange()))

        buttons = [('pack', urwid.AttrMap(b, None, focus_map=focus_map)) for b in buttons]
        buttons = urwid.Columns(buttons + [urwid.Text('')], dividechars=2)
        buttons = urwid.AttrMap(buttons, 'revision-button')
        self.more = urwid.Pile([table, buttons])
        padded_title = urwid.Padding(self.title, width='pack')
        self.pile = urwid.Pile([padded_title])
        self._w = urwid.AttrMap(self.pile, None, focus_map=self.revision_focus_map)
        self.expanded = False
        self.update(revision)
        if expanded:
            self.expandContract(None)

    def update(self, revision):
        line = [('revision-name', 'Patch Set %s ' % revision.number),
                ('revision-commit', revision.commit)]
        num_drafts = sum([len(f.draft_comments) for f in revision.files])
        if num_drafts:
            pending_message = revision.getPendingMessage()
            if not pending_message:
                line.append(('revision-drafts', ' (%s draft%s)' % (
                            num_drafts, num_drafts>1 and 's' or '')))
        num_comments = sum([len(f.comments) for f in revision.files]) - num_drafts
        if num_comments:
            line.append(('revision-comments', ' (%s inline comment%s)' % (
                        num_comments, num_comments>1 and 's' or '')))
        self.title.text.set_text(line)

    def expandContract(self, button):
        if self.expanded:
            self.pile.contents.pop()
            self.expanded = False
        else:
            self.pile.contents.append((self.more, ('pack', None)))
            self.expanded = True

    def diff(self, button):
        self.change_view.diff(self.revision_key)

    def checkout(self, button):
        self.app.localCheckoutCommit(self.project_name, self.commit_sha)

    def cherryPick(self, button):
        self.app.localCherryPickCommit(self.project_name, self.commit_sha)

class ChangeButton(urwid.Button):
    button_left = urwid.Text(u' ')
    button_right = urwid.Text(u' ')

    def __init__(self, change_view, change_key, text):
        super(ChangeButton, self).__init__('')
        self.set_label(text)
        self.change_view = change_view
        self.change_key = change_key
        urwid.connect_signal(self, 'click',
            lambda button: self.openChange())

    def set_label(self, text):
        super(ChangeButton, self).set_label(text)

    def openChange(self):
        try:
            self.change_view.app.changeScreen(ChangeView(self.change_view.app, self.change_key))
        except gertty.view.DisplayError as e:
            self.change_view.app.error(e.message)

class ChangeMessageBox(mywid.HyperText):
    def __init__(self, change_view, change, message):
        super(ChangeMessageBox, self).__init__(u'')
        self.change_view = change_view
        self.app = change_view.app
        self.refresh(change, message)

    def formatReply(self):
        text = self.message_text
        pgraphs = []
        pgraph_accumulator = []
        wrap = True
        for line in text.split('\n')[2:]:
            if line.startswith('> '):
                wrap = False
                line = '> ' + line
            if not line:
                if pgraph_accumulator:
                    pgraphs.append((wrap, '\n'.join(pgraph_accumulator)))
                    pgraph_accumulator = []
                    wrap = True
                continue
            pgraph_accumulator.append(line)
        if pgraph_accumulator:
            pgraphs.append((wrap, '\n'.join(pgraph_accumulator)))
            pgraph_accumulator = []
            wrap = True
        wrapper = textwrap.TextWrapper(initial_indent='> ',
                                       subsequent_indent='> ')
        wrapped_pgraphs = []
        for wrap, pgraph in pgraphs:
            if wrap:
                wrapped_pgraphs.append('\n'.join(wrapper.wrap(pgraph)))
            else:
                wrapped_pgraphs.append(pgraph)
        return '\n>\n'.join(wrapped_pgraphs)

    def reply(self):
        reply_text = self.formatReply()
        if reply_text:
            reply_text = self.message_author + ' wrote:\n\n' + reply_text + '\n'
        row = self.change_view.revision_rows[self.revision_key]
        row.review_button.openReview(reply_text)

    def refresh(self, change, message):
        self.revision_key = message.revision.key
        self.message_created = message.created
        self.message_author = message.author_name
        self.message_text = message.message
        created = self.app.time(message.created)
        lines = message.message.split('\n')
        if message.draft:
            lines.insert(0, '')
            lines.insert(0, 'Patch Set %s:' % (message.revision.number,))
        if self.app.isOwnAccount(message.author):
            name_style = 'change-message-own-name'
            header_style = 'change-message-own-header'
            reviewer_string = message.author_name
        else:
            name_style = 'change-message-name'
            header_style = 'change-message-header'
            if message.author.email:
                reviewer_string = "%s <%s>" % (
                    message.author_name,
                    message.author.email)
            else:
                reviewer_string = message.author_name

        text = [(name_style, reviewer_string),
                (header_style, ': '+lines.pop(0)),
                (header_style,
                 created.strftime(' (%Y-%m-%d %H:%M:%S%z)'))]
        if message.draft and not message.pending:
            text.append(('change-message-draft', ' (draft)'))
        else:
            link = mywid.Link('< Reply >',
                              'revision-button',
                              'focused-revision-button')
            urwid.connect_signal(link, 'selected',
                                 lambda link:self.reply())
            text.append(' ')
            text.append(link)

        if lines and lines[-1]:
            lines.append('')
        comment_text = ['\n'.join(lines)]
        for commentlink in self.app.config.commentlinks:
            comment_text = commentlink.run(self.app, comment_text)

        inline_comments = {}
        for revno, revision in enumerate(change.revisions):
            for file in revision.files:
                comments = [c for c in file.comments
                            if c.author.id == message.author.id
                            and c.created == message.created]
                for comment in comments:
                    path = comment.file.path
                    inline_comments.setdefault(path, [])
                    comment_ps = revno + 1
                    if comment_ps == message.revision.number:
                        comment_ps = None
                    inline_comments[path].append((comment_ps or 0, comment.line or 0, comment.message))
        for v in inline_comments.values():
            v.sort()

        if inline_comments:
            comment_text.append(u'\n')
        for key, value in inline_comments.items():
            if key == '/PATCHSET_LEVEL':
                key = 'Patchset'
            if key == '/COMMIT_MSG':
                key = 'Commit message'
            comment_text.append(('filename-inline-comment', u'%s' % key))
            for patchset, line, comment in value:
                location_str = ''
                if patchset:
                    location_str += "PS%i" % patchset
                    if line: location_str += ", "
                if line:
                    location_str += str(line)
                if location_str:
                    location_str += ": "
                comment_text.append(u'\n  %s%s\n' % (location_str, comment))

        self.set_text(text+comment_text)

class CommitMessageBox(mywid.HyperText):
    def __init__(self, app, message):
        self.app = app
        super(CommitMessageBox, self).__init__(message)

    def set_text(self, text):
        text = [text]
        for commentlink in self.app.config.commentlinks:
            text = commentlink.run(self.app, text)
        super(CommitMessageBox, self).set_text(text)

@mouse_scroll_decorator.ScrollByWheel
class ChangeView(urwid.WidgetWrap):
    def getCommands(self):
        return [
            (keymap.LOCAL_CHECKOUT,
             "Checkout the most recent revision into the local repo"),
            (keymap.DIFF,
             "Show the diff of the most recent revision"),
            (keymap.TOGGLE_HIDDEN,
             "Toggle the hidden flag for the current change"),
            (keymap.NEXT_CHANGE,
             "Go to the next change in the list"),
            (keymap.PREV_CHANGE,
             "Go to the previous change in the list"),
            (keymap.REVIEW,
             "Leave a review for the most recent revision"),
            (keymap.TOGGLE_HELD,
             "Toggle the held flag for the current change"),
            (keymap.TOGGLE_HIDDEN_COMMENTS,
             "Toggle display of hidden comments"),
            (keymap.SEARCH_RESULTS,
             "Back to the list of changes"),
            (keymap.TOGGLE_REVIEWED,
             "Toggle the reviewed flag for the current change"),
            (keymap.TOGGLE_STARRED,
             "Toggle the starred flag for the current change"),
            (keymap.LOCAL_CHERRY_PICK,
             "Cherry-pick the most recent revision onto the local repo"),
            (keymap.ABANDON_CHANGE,
             "Abandon this change"),
            (keymap.EDIT_COMMIT_MESSAGE,
             "Edit the commit message of this change"),
            (keymap.REBASE_CHANGE,
             "Rebase this change (remotely)"),
            (keymap.RESTORE_CHANGE,
             "Restore this change"),
            (keymap.READY_CHANGE,
             "Mark this change ready for review"),
            (keymap.REFRESH,
             "Refresh this change"),
            (keymap.EDIT_TOPIC,
             "Edit the topic of this change"),
            (keymap.EDIT_HASHTAGS,
             "Edit the hashtags of this change"),
            (keymap.SUBMIT_CHANGE,
             "Submit this change"),
            (keymap.CHERRY_PICK_CHANGE,
             "Propose this change to another branch"),
            (keymap.WIP_CHANGE,
             "Mark this change work in progress"),
            ]

    def help(self):
        key = self.app.config.keymap.formatKeys
        commands = self.getCommands()
        ret = [(c[0], key(c[0]), c[1]) for c in commands]
        for k in self.app.config.reviewkeys.values():
            action = ', '.join(['{category}:{value}'.format(**a) for a in k['approvals']])
            ret.append(('', keymap.formatKey(k['key']), action))
        return ret

    def __init__(self, app, change_key):
        super(ChangeView, self).__init__(urwid.Pile([]))
        self.log = logging.getLogger('gertty.view.change')
        self.app = app
        self.change_key = change_key
        self.revision_rows = {}
        self.message_rows = {}
        self.last_revision_key = None
        self.hide_comments = True
        self.marked_seen = False
        self.change_id_label = mywid.TextButton(u'', on_press=self.searchChangeId)
        self.owner_label = mywid.TextButton(u'', on_press=self.searchOwner)
        self.project_label = mywid.TextButton(u'', on_press=self.searchProject)
        self.branch_label = urwid.Text(u'', wrap='clip')
        self.topic_label = mywid.TextButton(u'', on_press=self.searchTopic)
        self.hashtags_label = mywid.HyperText(u'')
        self.created_label = urwid.Text(u'', wrap='clip')
        self.updated_label = urwid.Text(u'', wrap='clip')
        self.status_label = urwid.Text(u'', wrap='clip')
        self.permalink_label = mywid.TextButton(u'', on_press=self.openPermalink)
        change_info = []
        change_info_map={'change-data': 'focused-change-data'}
        for l, v in [("Change-Id", urwid.Padding(urwid.AttrMap(self.change_id_label, None,
                                                               focus_map=change_info_map),
                                                 width='pack')),
                     ("Owner", urwid.Padding(urwid.AttrMap(self.owner_label, None,
                                                           focus_map=change_info_map),
                                             width='pack')),
                     ("Project", urwid.Padding(urwid.AttrMap(self.project_label, None,
                                                           focus_map=change_info_map),
                                             width='pack')),
                     ("Branch", self.branch_label),
                     ("Topic", urwid.Padding(urwid.AttrMap(self.topic_label, None,
                                                           focus_map=change_info_map),
                                             width='pack')),
                     ("Hashtags", self.hashtags_label),
                     ("Created", self.created_label),
                     ("Updated", self.updated_label),
                     ("Status", self.status_label),
                     ("Permalink", urwid.Padding(urwid.AttrMap(self.permalink_label, None,
                                                               focus_map=change_info_map),
                                                 width='pack')),
                     ]:
            row = urwid.Columns([(12, urwid.Text(('change-header', l), wrap='clip')), v])
            change_info.append(row)
        change_info = urwid.Pile(change_info)
        self.commit_message = CommitMessageBox(app, u'')
        votes = mywid.Table([])
        self.depends_on = RelatedChangesSection([])
        self.depends_on_rows = {}
        self.needed_by = RelatedChangesSection([])
        self.needed_by_rows = {}
        self.conflicts_with = RelatedChangesSection([])
        self.conflicts_with_rows = {}
        self.related_changes = urwid.Pile([self.depends_on, self.needed_by, self.conflicts_with])
        self.results = mywid.HyperText(u'') # because it scrolls better than a table
        self.grid = urwid.GridFlow([change_info, self.commit_message, votes, self.results],
                                   cell_width=80, h_sep=2, v_sep=1, align='left')
        self.listbox = urwid.ListBox(urwid.SimpleFocusListWalker([]))
        self._w.contents.append((self.app.header, ('pack', 1)))
        self._w.contents.append((urwid.Divider(), ('pack', 1)))
        self._w.contents.append((self.listbox, ('weight', 1)))
        self._w.set_focus(2)

        self.listbox.body.append(self.grid)
        self.listbox.body.append(urwid.Divider())
        self.listbox.body.append(self.related_changes)
        self.listbox.body.append(urwid.Divider())
        self.listbox_patchset_start = len(self.listbox.body)

        self.checkGitRepo()
        self.refresh()
        self.listbox.set_focus(0)
        change_info.set_focus(0)

    def checkGitRepo(self):
        missing_revisions = set()
        change_number = None
        change_id = None
        shas = set()
        with self.app.db.getSession() as session:
            change = session.getChange(self.change_key)
            change_project_name = change.project.name
            change_number = change.number
            change_id = change.id
            for revision in change.revisions:
                shas.add(revision.parent)
                shas.add(revision.commit)
        repo = gitrepo.get_repo(change_project_name, self.app.config)
        missing_revisions = repo.checkCommits(shas)
        if missing_revisions:
            if self.app.sync.offline:
                raise gertty.view.DisplayError("Git commits not present in local repository")
            self.app.log.warning("Missing some commits for change %s %s",
                change_number, missing_revisions)
            task = sync.SyncChangeTask(change_id, force_fetch=True,
                                       priority=sync.HIGH_PRIORITY)
            self.app.sync.submitTask(task)
            succeeded = task.wait(300)
            if not succeeded:
                raise gertty.view.DisplayError("Git commits not present in local repository")

    def interested(self, event):
        if not ((isinstance(event, sync.ChangeAddedEvent) and
                 self.change_key in event.related_change_keys)
                or
                (isinstance(event, sync.ChangeUpdatedEvent) and
                 self.change_key in event.related_change_keys)):
            self.log.debug("Ignoring refresh change due to event %s" % (event,))
            return False
        self.log.debug("Refreshing change due to event %s" % (event,))
        return True

    def refresh(self):
        with self.app.db.getSession() as session:
            change = session.getChange(self.change_key, lazy=False)
            # When we first open the change, update its last_seen
            # time.
            if not self.marked_seen:
                change.last_seen = datetime.datetime.utcnow()
                self.marked_seen = True
            self.topic = change.topic or ''
            self.hashtags = ', '.join([h.name for h in change.hashtags])
            self.pending_status_message = change.pending_status_message or ''
            reviewed = hidden = starred = held = ''
            if change.reviewed:
                reviewed = ' (reviewed)'
            if change.hidden:
                hidden = ' (hidden)'
            if change.starred:
                starred = '* '
            if change.held:
                held = ' (held)'
            self.title = '%sChange %s%s%s%s' % (starred, change.number, reviewed,
                                                hidden, held)
            self.app.status.update(title=self.title)
            self.project_key = change.project.key
            self.project_name = change.project.name
            self.change_rest_id = change.id
            self.change_id = change.change_id
            if change.owner:
                self.owner_email = change.owner.email
            else:
                self.owner_email = None

            self.change_id_label.text.set_text(('change-data', change.change_id))
            if change.owner.email:
                owner_string = '%s <%s>' % (change.owner_name,
                                            change.owner.email)
            else:
                owner_string = change.owner_name
            self.owner_label.text.set_text(('change-data', owner_string))
            self.project_label.text.set_text(('change-data', change.project.name))
            self.branch_label.set_text(('change-data', change.branch))
            self.topic_label.text.set_text(('change-data', self.topic))
            hashtag_buttons = []
            for x in change.hashtags:
                if hashtag_buttons:
                    hashtag_buttons.append(' ')
                link = mywid.Link(x.name, 'change-data', 'focused-change-data')
                urwid.connect_signal(
                    link, 'selected',
                    lambda link, x=x: self.searchHashtags(x.name))
                hashtag_buttons.append(link)
            self.hashtags_label.set_text(('change-data', hashtag_buttons or u''))
            self.created_label.set_text(('change-data', str(self.app.time(change.created))))
            self.updated_label.set_text(('change-data', str(self.app.time(change.updated))))
            stat = change.wip and ('state-wip', 'WIP') or change.status
            self.status_label.set_text(('change-data', stat))
            self.permalink_url = urlparse.urljoin(self.app.config.url, str(change.number))
            self.permalink_label.text.set_text(('change-data', self.permalink_url))
            self.commit_message.set_text(change.revisions[-1].message)

            categories = []
            approval_headers = [urwid.Text(('table-header', 'Name'))]
            for label in change.labels:
                if label.category in categories:
                    continue
                approval_headers.append(urwid.Text(('table-header', label.category)))
                categories.append(label.category)
            votes = mywid.Table(approval_headers)
            approvals_for_account = {}
            pending_message = change.revisions[-1].getPendingMessage()
            for approval in change.approvals:
                # Don't display draft approvals unless they are pending-upload
                if approval.draft and not pending_message:
                    continue
                approvals = approvals_for_account.get(approval.reviewer.id)
                if not approvals:
                    approvals = {}
                    row = []
                    if self.app.isOwnAccount(approval.reviewer):
                        style = 'reviewer-own-name'
                    else:
                        style = 'reviewer-name'
                    row.append(urwid.Text((style, approval.reviewer_name)))
                    for i, category in enumerate(categories):
                        w = urwid.Text(u'', align=urwid.CENTER)
                        approvals[category] = w
                        row.append(w)
                    approvals_for_account[approval.reviewer.id] = approvals
                    votes.addRow(row)
                if str(approval.value) != '0':
                    cat_min, cat_max = change.getMinMaxPermittedForCategory(approval.category)
                    if approval.value > 0:
                        val = '+%i' % approval.value
                        if approval.value == cat_max:
                            val = ('max-label', val)
                        else:
                            val = ('positive-label', val)
                    else:
                        val = '%i' % approval.value
                        if approval.value == cat_min:
                            val = ('min-label', val)
                        else:
                            val = ('negative-label', val)
                    approvals[approval.category].set_text(val)
            votes = urwid.Padding(votes, width='pack')

            # TODO: update the existing table rather than replacing it
            # wholesale.  It will become more important if the table
            # gets selectable items (like clickable names).
            self.grid.contents[2] = (votes, ('given', 80))

            self.refreshDependencies(session, change)

            repo = gitrepo.get_repo(change.project.name, self.app.config)
            # The listbox has both revisions and messages in it (and
            # may later contain the vote table and change header), so
            # keep track of the index separate from the loop.
            listbox_index = self.listbox_patchset_start
            for revno, revision in enumerate(change.revisions):
                self.last_revision_key = revision.key
                row = self.revision_rows.get(revision.key)
                if not row:
                    row = RevisionRow(self.app, self, repo, revision,
                                      expanded=(revno==len(change.revisions)-1))
                    self.listbox.body.insert(listbox_index, row)
                    self.revision_rows[revision.key] = row
                row.update(revision)
                # Revisions are extremely unlikely to be deleted, skip
                # that case.
                listbox_index += 1
            if len(self.listbox.body) == listbox_index:
                self.listbox.body.insert(listbox_index, urwid.Divider())
            listbox_index += 1
            # Get the set of messages that should be displayed
            display_messages = []
            result_systems = {}
            for message in change.messages:
                if (message.revision == change.revisions[-1] and
                    message.author and message.author.name):
                    for commentlink in self.app.config.commentlinks:
                        results = commentlink.getTestResults(self.app, message.message)
                        if results:
                            result_system = result_systems.get(message.author.name,
                                                               OrderedDict())
                            result_systems[message.author.name] = result_system
                            result_system.update(results)
                skip = False
                if self.hide_comments and message.author and message.author.name:
                    for regex in self.app.config.hide_comments:
                        if regex.match(message.author.name):
                            skip = True
                            break
                if not skip:
                    display_messages.append(message)
            # The set of message keys currently displayed
            unseen_keys = set(self.message_rows.keys())
            # Make sure all of the messages that should be displayed are
            for message in display_messages:
                row = self.message_rows.get(message.key)
                if not row:
                    box = ChangeMessageBox(self, change, message)
                    row = urwid.Padding(box, width=80)
                    self.listbox.body.insert(listbox_index, row)
                    self.message_rows[message.key] = row
                else:
                    unseen_keys.remove(message.key)
                    if message.created != row.original_widget.message_created:
                        row.original_widget.refresh(change, message)
                listbox_index += 1
            # Remove any messages that should not be displayed
            for key in unseen_keys:
                row = self.message_rows.get(key)
                self.listbox.body.remove(row)
                del self.message_rows[key]
                listbox_index -= 1
            self._updateTestResults(change, result_systems)

    def _updateTestResults(self, change, result_systems):
        text = []
        for system, results in result_systems.items():
            for job, result in results.items():
                text.append(result)

        # Add check results
        revision = change.revisions[-1]
        for check in revision.checks:
            # link checker name/url, color result, in time
            link = mywid.Link('{:<42}'.format(check.checker.name), 'link', 'focused-link')
            urwid.connect_signal(link, 'selected', lambda link:self.app.openURL(check.url))
            color = 'check-%s' % check.state
            result = (color, check.state)
            line = [link, result]
            if check.finished and check.started:
                line.append(' in %s' % (check.finished-check.started))
            line.append('\n')
            text.append(line)

        if text:
            self.results.set_text(text)
        else:
            self.results.set_text('')

    def _updateDependenciesWidget(self, changes, widget, widget_rows, header):
        if not changes:
            if len(widget.contents) > 0:
                widget.contents[:] = []
            return

        if len(widget.contents) == 0:
            widget.contents.append((urwid.Text(('table-header', header)),
                                    widget.options()))

        unseen_keys = set(widget_rows.keys())
        i = 1
        for key, subject in changes.items():
            row = widget_rows.get(key)
            if not row:
                row = urwid.AttrMap(urwid.Padding(ChangeButton(self, key, subject), width='pack'),
                                    'link', focus_map={None: 'focused-link'})
                row = (row, widget.options('pack'))
                widget.contents.insert(i, row)
                if not widget.selectable():
                    widget.set_focus(i)
                if not self.related_changes.selectable():
                    self.related_changes.set_focus(widget)
                widget_rows[key] = row
            else:
                row[0].original_widget.original_widget.set_label(subject)
                unseen_keys.remove(key)
            i += 1
        for key in unseen_keys:
            row = widget_rows[key]
            widget.contents.remove(row)
            del widget_rows[key]

    def refreshDependencies(self, session, change):
        revision = change.revisions[-1]

        # Handle depends-on
        parents = {}
        parent = session.getRevisionByCommit(revision.parent)
        if parent:
            subject = parent.change.subject
            show_merged = False
            if parent != parent.change.revisions[-1]:
                subject += ' [OUTDATED]'
                show_merged = True
            if parent.change.status == 'ABANDONED':
                subject += ' [ABANDONED]'
            if show_merged or parent.change.status != 'MERGED':
                parents[parent.change.key] = subject
        self._updateDependenciesWidget(parents,
                                       self.depends_on, self.depends_on_rows,
                                       header='Depends on:')

        # Handle needed-by
        children = {}
        children.update((r.change.key, r.change.subject)
                        for r in session.getRevisionsByParent([revision.commit for revision in change.revisions])
                        if (r.change.status != 'MERGED' and
                            r.change.status != 'ABANDONED' and
                            r == r.change.revisions[-1]))
        self._updateDependenciesWidget(children,
                                       self.needed_by, self.needed_by_rows,
                                       header='Needed by:')

        # Handle conflicts_with
        conflicts = {}
        conflicts.update((c.key, c.subject)
                        for c in change.conflicts
                        if (c.status != 'MERGED' and
                            c.status != 'ABANDONED'))
        self._updateDependenciesWidget(conflicts,
                                       self.conflicts_with, self.conflicts_with_rows,
                                       header='Conflicts with:')


    def toggleReviewed(self):
        with self.app.db.getSession() as session:
            change = session.getChange(self.change_key)
            change.reviewed = not change.reviewed
            self.app.project_cache.clear(change.project)

    def toggleHidden(self):
        with self.app.db.getSession() as session:
            change = session.getChange(self.change_key)
            change.hidden = not change.hidden
            self.app.project_cache.clear(change.project)

    def toggleStarred(self):
        with self.app.db.getSession() as session:
            change = session.getChange(self.change_key)
            change.starred = not change.starred
            change.pending_starred = True
        self.app.sync.submitTask(
            sync.ChangeStarredTask(self.change_key, sync.HIGH_PRIORITY))

    def toggleHeld(self):
        return self.app.toggleHeldChange(self.change_key)

    def keypress(self, size, key):
        if not self.app.input_buffer:
            key = super(ChangeView, self).keypress(size, key)
        keys = self.app.input_buffer + [key]
        commands = self.app.config.keymap.getCommands(keys)
        if keymap.TOGGLE_REVIEWED in commands:
            self.toggleReviewed()
            self.refresh()
            return None
        if keymap.TOGGLE_HIDDEN in commands:
            self.toggleHidden()
            self.refresh()
            return None
        if keymap.TOGGLE_STARRED in commands:
            self.toggleStarred()
            self.refresh()
            return None
        if keymap.TOGGLE_HELD in commands:
            self.toggleHeld()
            self.refresh()
            return None
        if keymap.REVIEW in commands:
            row = self.revision_rows[self.last_revision_key]
            row.review_button.openReview()
            return None
        if keymap.DIFF in commands:
            row = self.revision_rows[self.last_revision_key]
            row.diff(None)
            return None
        if keymap.LOCAL_CHECKOUT in commands:
            row = self.revision_rows[self.last_revision_key]
            row.checkout(None)
            return None
        if keymap.LOCAL_CHERRY_PICK in commands:
            row = self.revision_rows[self.last_revision_key]
            row.cherryPick(None)
            return None
        if keymap.SEARCH_RESULTS in commands:
            widget = self.app.findChangeList()
            if widget:
                self.app.backScreen(widget)
            return None
        if ((keymap.NEXT_CHANGE in commands) or
            (keymap.PREV_CHANGE in commands)):
            widget = self.app.findChangeList()
            if widget:
                if keymap.NEXT_CHANGE in commands:
                    new_change_key = widget.getNextChangeKey(self.change_key)
                else:
                    new_change_key = widget.getPrevChangeKey(self.change_key)
                if new_change_key:
                    try:
                        view = ChangeView(self.app, new_change_key)
                        self.app.changeScreen(view, push=False)
                    except gertty.view.DisplayError as e:
                        self.app.error(e.message)
            return None
        if keymap.TOGGLE_HIDDEN_COMMENTS in commands:
            self.hide_comments = not self.hide_comments
            self.refresh()
            return None
        if keymap.ABANDON_CHANGE in commands:
            self.abandonChange()
            return None
        if keymap.WIP_CHANGE in commands:
            self.wipChange()
            return None
        if keymap.READY_CHANGE in commands:
            self.readyChange()
            return None
        if keymap.EDIT_COMMIT_MESSAGE in commands:
            self.editCommitMessage()
            return None
        if keymap.REBASE_CHANGE in commands:
            self.rebaseChange()
            return None
        if keymap.RESTORE_CHANGE in commands:
            self.restoreChange()
            return None
        if keymap.REFRESH in commands:
            self.app.sync.submitTask(
                sync.SyncChangeTask(self.change_rest_id, priority=sync.HIGH_PRIORITY))
            self.app.status.update()
            return None
        if keymap.SUBMIT_CHANGE in commands:
            self.doSubmitChange()
            return None
        if keymap.EDIT_TOPIC in commands:
            self.editTopic()
            return None
        if keymap.EDIT_HASHTAGS in commands:
            self.editHashtags()
            return None
        if keymap.CHERRY_PICK_CHANGE in commands:
            self.cherryPickChange()
            return None
        if key in self.app.config.reviewkeys:
            self.reviewKey(self.app.config.reviewkeys[key])
            return None
        return key

    def diff(self, revision_key):
        if self.app.config.diff_view == 'unified':
            screen = view_unified_diff.UnifiedDiffView(self.app, revision_key)
        else:
            screen = view_side_diff.SideDiffView(self.app, revision_key)
        self.app.changeScreen(screen)

    def abandonChange(self):
        dialog = mywid.TextEditDialog(u'Abandon Change', u'Abandon message:',
                                      u'Abandon Change',
                                      self.pending_status_message)
        urwid.connect_signal(dialog, 'cancel', self.app.backScreen)
        urwid.connect_signal(dialog, 'save', lambda button:
                                 self.doAbandonRestoreChange(dialog, 'ABANDONED'))
        self.app.popup(dialog)

    def restoreChange(self):
        dialog = mywid.TextEditDialog(u'Restore Change', u'Restore message:',
                                      u'Restore Change',
                                      self.pending_status_message)
        urwid.connect_signal(dialog, 'cancel', self.app.backScreen)
        urwid.connect_signal(dialog, 'save', lambda button:
                                 self.doAbandonRestoreChange(dialog, 'NEW'))
        self.app.popup(dialog)

    def doAbandonRestoreChange(self, dialog, state):
        change_key = None
        with self.app.db.getSession() as session:
            change = session.getChange(self.change_key)
            change.status = state
            change.pending_status = True
            change.pending_status_message = dialog.entry.edit_text
            change_key = change.key
        self.app.sync.submitTask(
            sync.ChangeStatusTask(change_key, sync.HIGH_PRIORITY))
        self.app.backScreen()
        self.refresh()

    def wipChange(self):
        dialog = mywid.TextEditDialog(u'Mark Change Work In Progress',
                                      u'WIP message:',
                                      u'WIP Change',
                                      self.pending_status_message)
        urwid.connect_signal(dialog, 'cancel', self.app.backScreen)
        urwid.connect_signal(dialog, 'save', lambda button:
                                 self.doWip(dialog, True))
        self.app.popup(dialog)

    def readyChange(self):
        dialog = mywid.TextEditDialog(u'Mark Change Ready For Review',
                                      u'Ready message:',
                                      u'Ready Change',
                                      self.pending_status_message)
        urwid.connect_signal(dialog, 'cancel', self.app.backScreen)
        urwid.connect_signal(dialog, 'save', lambda button:
                                 self.doWip(dialog, False))
        self.app.popup(dialog)

    def doWip(self, dialog, state):
        change_key = None
        with self.app.db.getSession() as session:
            change = session.getChange(self.change_key)
            change.wip = state
            change.pending_wip = True
            change.pending_wip_message = dialog.entry.edit_text
            change_key = change.key
        self.app.sync.submitTask(
            sync.ChangeWIPTask(change_key, sync.HIGH_PRIORITY))
        self.app.backScreen()
        self.refresh()

    def editCommitMessage(self):
        with self.app.db.getSession() as session:
            change = session.getChange(self.change_key)
            dialog = mywid.TextEditDialog(u'Edit Commit Message', u'Commit message:',
                                          u'Save', change.revisions[-1].message)
        urwid.connect_signal(dialog, 'cancel', self.app.backScreen)
        urwid.connect_signal(dialog, 'save', lambda button:
                                 self.doEditCommitMessage(dialog))
        self.app.popup(dialog,
                       relative_width=50, relative_height=75,
                       min_width=60, min_height=20)

    def doEditCommitMessage(self, dialog):
        revision_key = None
        with self.app.db.getSession() as session:
            change = session.getChange(self.change_key)
            revision = change.revisions[-1]
            revision.message = dialog.entry.edit_text
            revision.pending_message = True
            revision_key = revision.key
        self.app.sync.submitTask(
            sync.ChangeCommitMessageTask(revision_key, sync.HIGH_PRIORITY))
        self.app.backScreen()
        self.refresh()

    def rebaseChange(self):
        dialog = mywid.YesNoDialog(u'Rebase Change',
                                   u'Perform a remote rebase of this change?')
        urwid.connect_signal(dialog, 'no', self.app.backScreen)
        urwid.connect_signal(dialog, 'yes', self.doRebaseChange)
        self.app.popup(dialog)

    def doRebaseChange(self, button=None):
        change_key = None
        with self.app.db.getSession() as session:
            change = session.getChange(self.change_key)
            change.pending_rebase = True
            change_key = change.key
        self.app.sync.submitTask(
            sync.RebaseChangeTask(change_key, sync.HIGH_PRIORITY))
        self.app.backScreen()
        self.refresh()

    def cherryPickChange(self):
        with self.app.db.getSession() as session:
            change = session.getChange(self.change_key)
            dialog = CherryPickDialog(self.app, change)
        urwid.connect_signal(dialog, 'cancel', self.app.backScreen)
        urwid.connect_signal(dialog, 'save', lambda button:
                                 self.doCherryPickChange(dialog))
        self.app.popup(dialog,
                       relative_width=50, relative_height=75,
                       min_width=60, min_height=20)


    def doCherryPickChange(self, dialog):
        cp_key = None
        with self.app.db.getSession() as session:
            change = session.getChange(self.change_key)
            branch = None
            for button in dialog.branch_buttons:
                if button.state:
                    branch = button.get_label()
            message = dialog.entry.edit_text
            self.app.log.debug("Creating pending cherry-pick of %s to %s" %
                               (change.revisions[-1].commit, branch))
            cp = change.revisions[-1].createPendingCherryPick(branch, message)
            cp_key = cp.key
        self.app.sync.submitTask(
            sync.SendCherryPickTask(cp_key, sync.HIGH_PRIORITY))
        self.app.backScreen()
        self.refresh()

    def doSubmitChange(self):
        change_key = None
        with self.app.db.getSession() as session:
            change = session.getChange(self.change_key)
            change.status = 'SUBMITTED'
            change.pending_status = True
            change.pending_status_message = None
            change_key = change.key
        self.app.sync.submitTask(
            sync.ChangeStatusTask(change_key, sync.HIGH_PRIORITY))
        self.refresh()

    def editTopic(self):
        dialog = EditTopicDialog(self.app, self.topic)
        urwid.connect_signal(dialog, 'save',
            lambda button: self.closeEditTopic(dialog, True))
        urwid.connect_signal(dialog, 'cancel',
            lambda button: self.closeEditTopic(dialog, False))
        self.app.popup(dialog)

    def closeEditTopic(self, dialog, save):
        if save:
            change_key = None
            with self.app.db.getSession() as session:
                change = session.getChange(self.change_key)
                change.topic = dialog.entry.edit_text
                change.pending_topic = True
                change_key = change.key
            self.app.sync.submitTask(
                sync.SetTopicTask(change_key, sync.HIGH_PRIORITY))
        self.app.backScreen()
        self.refresh()

    def editHashtags(self):
        dialog = EditHashtagsDialog(self.app, self.hashtags)
        urwid.connect_signal(dialog, 'save',
            lambda button: self.closeEditHashtags(dialog, True))
        urwid.connect_signal(dialog, 'cancel',
            lambda button: self.closeEditHashtags(dialog, False))
        self.app.popup(dialog)

    def closeEditHashtags(self, dialog, save):
        if save:
            change_key = None
            with self.app.db.getSession() as session:
                change = session.getChange(self.change_key)
                change.setHashtags([x.strip() for x in dialog.entry.edit_text.split(',')])
                change.pending_hashtags = True
                change_key = change.key
            self.app.sync.submitTask(
                sync.SetHashtagsTask(change_key, sync.HIGH_PRIORITY))
        self.app.backScreen()
        self.refresh()

    def openPermalink(self, widget):
        self.app.openURL(self.permalink_url)

    def searchChangeId(self, widget):
        self.app.doSearch("status:open change:%s" % (self.change_id,))

    def searchOwner(self, widget):
        if self.owner_email:
            self.app.doSearch("status:open owner:%s" % (self.owner_email,))

    def searchProject(self, widget):
        self.app.doSearch("status:open project:%s" % (self.project_name,))

    def searchTopic(self, widget):
        if self.topic:
            self.app.doSearch("status:open topic:%s" % (self.topic,))

    def searchHashtags(self, name):
        self.app.doSearch("status:open hashtag:%s" % (name,))

    def reviewKey(self, reviewkey):
        approvals = {}
        for a in reviewkey['approvals']:
            approvals[a['category']] = a['value']
        self.app.log.debug("Reviewkey %s with approvals %s" %
                           (reviewkey['key'], approvals))
        row = self.revision_rows[self.last_revision_key]
        message = reviewkey.get('message', '')
        submit = reviewkey.get('submit', False)
        self.saveReview(row.revision_key, approvals, message, True, submit)

    def saveReview(self, revision_key, approvals, message, upload, submit):
        message_keys = self.app.saveReviews([revision_key], approvals,
                                            message, upload, submit)
        if upload:
            for message_key in message_keys:
                self.app.sync.submitTask(
                    sync.UploadReviewTask(message_key, sync.HIGH_PRIORITY))
        self.refresh()
        if self.app.config.close_change_on_review:
            self.app.backScreen()

class RelatedChangesSection(urwid.Pile):
    def selectable(self):
        return True
