#!/usr/bin/env python
# -*- coding: utf-8 -*-

# vim: tabstop=4 shiftwidth=4 softtabstop=4

#    Copyright (C) 2013 Yahoo! Inc. All Rights Reserved.
#
#    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 errno
import functools
import getpass
import json
import logging
import optparse
import os
import re
import sys
import threading
import time

import Queue

from datetime import datetime

from gerritlib import gerrit
import six
import urwid
from urwid.signals import connect_signal
from urwid import widget

logging.basicConfig(level=logging.ERROR,
                    format='%(asctime)s %(levelname)s: %(message)s',
                    stream=sys.stderr)
LOG = logging.getLogger(__name__)

### DEFAULT SETTINGS

GERRIT_HOST = 'review.openstack.org'
GERRIT_PORT = 29418
BACKOFF_ATTEMPTS = 5
VISIBLE_LIST_LEN = 50
PREFETCH_LEN = VISIBLE_LIST_LEN
ALARM_FREQ = 1.0
SANITY_QUERY = 'status:open limit:%s'
QUEUE_MAX_SIZE = 1000

### GUI CONSTANTS

PALETTE = (
    ('body', urwid.DEFAULT, urwid.DEFAULT),
    ('merged', urwid.LIGHT_CYAN, urwid.DEFAULT, 'bold'),
    ('approved', urwid.LIGHT_GREEN, urwid.DEFAULT),
    ('abandoned', urwid.YELLOW, urwid.DEFAULT),
    ('verified', urwid.LIGHT_GRAY, urwid.DEFAULT),
    ('restored', urwid.LIGHT_BLUE, urwid.DEFAULT),
    ('rejected', urwid.LIGHT_RED, urwid.DEFAULT, 'bold'),
    ('failed', urwid.LIGHT_RED, urwid.DEFAULT, 'bold'),
    ('succeeded', urwid.LIGHT_GREEN, urwid.DEFAULT),
    ('open', urwid.WHITE, urwid.DEFAULT),
)
COLUMNS = (
    'Username',
    "Topic",
    "Url",
    "Project",
    'Subject',
    'Created On',
    'Updated On',
    'Status',
    'Comment',
)
COLUMN_TRUNCATES = {
    # This determines how the columns will be trucated (at what length will
    # truncation be forced to avoid huge strings).
    'comment': 140,
    'subject': 60,
}
COLUMN_TRUNCATES['reason'] = COLUMN_TRUNCATES['comment']
COLUMN_ATTRIBUTES = {
    'Created On': (urwid.WEIGHT, 0.33),
    'Updated On': (urwid.WEIGHT, 0.33),
    'Status': (urwid.FIXED, 9),
    'Username': (urwid.WEIGHT, 0.33),
    'Project': (urwid.WEIGHT, 0.5),
    'Topic': (urwid.WEIGHT, 0.33),
    'Url': (urwid.FIXED, 35),
    'Subject': (urwid.WEIGHT, 0.8),
    'Comment': (urwid.WEIGHT, 0.8),
}
HIGHLIGHT_WORDS = {
    # These words get special colored highlighting.
    #
    # word -> palette name
    'succeeded': 'succeeded',
    'success': 'succeeded',
    'successful': 'succeeded',
    'failure': 'failed',
    'failed': 'failed',
    'fails': 'failed',
}
COLUMN_2_IDX = dict((k, i) for (i, k) in enumerate(COLUMNS))
REFRESH_KEYS = (
    urwid.CURSOR_UP,
    urwid.CURSOR_DOWN,
    urwid.CURSOR_PAGE_UP,
    urwid.CURSOR_PAGE_DOWN,
)
SORT_CHANGE_KEYS = ('s', 'S')
QUIT_KEYS = ('q', 'Q', 'esc')

### HELPERS


def _format_text(text, align=None):
    text_pieces = []
    for t in re.split(r"([\s.\-,!])", text):
        if t.lower() in HIGHLIGHT_WORDS:
            text_pieces.append((HIGHLIGHT_WORDS[t.lower()], t))
        else:
            text_pieces.append(t)
    return _make_text(text_pieces, align=align)


def _make_text(text, align=None):
    if not align:
        align = 'left'
    return urwid.Text(text, wrap='any', align=align)


def _get_key_path():
    home_dir = os.path.expanduser("~")
    ssh_dir = os.path.join(home_dir, ".ssh")
    if not os.path.isdir(ssh_dir):
        return None
    for k in ('id_rsa', 'id_dsa'):
        path = os.path.join(ssh_dir, k)
        if os.path.isfile(path):
            return path
    return None


def _format_date(when=None):
    if when is None:
        when = datetime.now()
    return when.strftime('%I:%M %p %m/%d/%Y')


def _get_date(k, row):
    v = _get_text(k, row)
    if not v:
        return None
    try:
        return datetime.fromtimestamp(int(v))
    except (ValueError, TypeError):
        return None


def _get_text(k, container):
    if k not in container:
        return ""
    text = container[k]
    if not isinstance(text, six.string_types):
        text = str(text)
    max_len = COLUMN_TRUNCATES.get(k.lower())
    if max_len is not None and len(text) > max_len:
        text = text[0:max_len] + "..."
    return text


class GerritWatcher(threading.Thread):
    def __init__(self, queue, server, port, username, keyfile, **kwargs):
        super(GerritWatcher, self).__init__()
        self.queue = queue
        self.keyfile = keyfile
        self.port = port
        self.server = server
        self.username = username
        self.daemon = True
        self.gerrit = None
        self.prefetch = int(kwargs.get("prefetch", 0))
        self.has_prefetched = False
        self.record_file = kwargs.get("record")
        self.has_read_record = False

    def _prefetch_check(self):
        fetch_am = 1
        if not self.has_prefetched:
            fetch_am = max(fetch_am, self.prefetch)

        def event_sort(ev1, ev2):
            p1 = ev1['patchSet']
            p2 = ev2['patchSet']
            return cmp(p1['createdOn'], p2['createdOn'])

        q = SANITY_QUERY % (fetch_am)
        LOG.info("Using '%s' for sanity query.", q)
        results = self.gerrit.bulk_query(q)
        translated = []
        for r in results:
            if not isinstance(r, (dict)):
                continue
            if r.get('type') == 'stats':
                continue
            # Translate it into what looks like a patch-set created
            # event and then send this via the queue to showup on the gui
            ev = {
                'type': 'patchset-created',
                'uploader': r.pop('owner'),
                'patchSet': {
                    'createdOn': r.pop('createdOn'),
                    'lastUpdated': r.pop('lastUpdated', None),
                },
            }
            ev['change'] = dict(r)
            translated.append(ev)

        # For some reason we can get more than requested, even though
        # we send a limit, huh??
        LOG.info("Received %s sanity check results.", len(translated))
        translated = translated[0:fetch_am]
        self.has_prefetched = True
        return list(sorted(translated, cmp=event_sort))

    def _connect_and_prefetch(self):
        prefetched_events = []
        try:
            if self.gerrit is None:
                self.gerrit = gerrit.Gerrit(self.server, self.username,
                                            self.port, self.keyfile)
            # NOTE(harlowja): only after the sanity query passes do we have
            # some level of confidence that the watcher thread will actually
            # correctly connect.
            prefetched_events = self._prefetch_check()
            self.gerrit.startWatching()
            LOG.info('Start watching gerrit event stream.')
        except Exception:
            LOG.exception('Exception while connecting to gerrit')
        return prefetched_events

    @property
    def connected(self):
        if self.gerrit is None:
            return False
        if self.gerrit.watcher_thread is None:
            return False
        if not self.gerrit.watcher_thread.is_alive():
            return False
        return True

    def _attempt_connect_and_prefetch(self):
        if self.connected:
            return []
        prefetched_events = []
        for i in xrange(0, BACKOFF_ATTEMPTS):
            prefetched_events = self._connect_and_prefetch()
            if not self.connected:
                sleep_time = 2**i
                if i + 1 < BACKOFF_ATTEMPTS:
                    LOG.warn("Trying connection again in %s seconds",
                             sleep_time)
                time.sleep(sleep_time)
            else:
                break
        if not self.connected:
            LOG.fatal("Could not connect to %s:%s", self.server, self.port)
        return prefetched_events

    def _handle_event(self, event):
        LOG.debug('Placing event on producer queue: %s', event)
        self.queue.put(event)

    def _consume_record(self):
        if not self.record_file:
            return

        def check_input(data):
            if not isinstance(data, (dict)):
                return False
            for k in ('change', 'type'):
                if k not in data:
                    return False
            return True

        try:
            with open(self.record_file, "rb") as fh:
                for line in iter(fh):
                    line = line.strip()
                    if not line:
                        continue
                    try:
                        event = json.loads(line)
                        if check_input(event):
                            yield event
                    except ValueError:
                        pass
        except IOError as e:
            if e.errno != errno.ENOENT:
                LOG.exception("Could not read record file at %s",
                              self.record_file)

    def _record_event(self, event):
        if not self.record_file:
            return
        try:
            event_f = json.dumps(event)
        except (ValueError, TypeError):
            LOG.exception("Failed at formatting event to json")
            return
        try:
            with open(self.record_file, "ab") as fh:
                fh.write(event_f)
                fh.write("\n")
        except IOError:
            LOG.exception("Failed writing event to %s", self.record_file)

    def _consume(self):
        try:
            event = self.gerrit.getEvent()
            self._handle_event(event)
            self._record_event(event)
        except Exception:
            LOG.exception('Exception encountered in event loop')
            if self.gerrit.watcher_thread is not None \
               and not self.gerrit.watcher_thread.is_alive():
                self.gerrit = None

    def run(self):
        while True:
            prefetch_events = self._attempt_connect_and_prefetch()
            if not self.has_read_record:
                for event in self._consume_record():
                    self._handle_event(event)
                self.has_read_record = True
            for event in prefetch_events:
                self._handle_event(event)
                self._record_event(event)
            self._consume()


def _consume_queue(queue):
    events = []
    while True:
        try:
            events.append(queue.get(block=False))
        except Queue.Empty:
            break
    return events


def _get_change_status(event):
    change_type = None
    for approval in event.get('approvals', []):
        if not isinstance(approval, (dict)):
            continue
        try:
            approval_value = int(approval['value'])
        except (ValueError, TypeError, KeyError):
            approval_value = None
        if approval.get('type') == 'VRIF':
            if approval_value == -2:
                change_type = 'Failed'
            if approval_value == -1:
                change_type = 'Verified'
            if approval_value == 2:
                change_type = 'Succeeded'
        if approval.get('type') == 'CRVW':
            if approval_value == -2:
                change_type = 'Rejected'
            if approval_value == 2:
                change_type = 'Approved'
    return change_type


class ToggleText(urwid.Text):
    ignore_focus = False

    def render(self, size, focus=False):
        # Be nice if there was a way to avoid having to copy this code from
        # the parent class....
        (maxcol,) = size
        text, attr = self.get_text()
        if focus:
            text = u'•' + (text)
        self._invalidate()
        trans = self.get_line_translation(maxcol, ta=(text, attr))
        return widget.apply_text_layout(text, attr, trans, maxcol)

    def keypress(self, size, key):
        return key

    def selectable(self):
        return True

    def rows(self, size, focus=False):
        (maxcol,) = size
        text, attr = self.get_text()
        if focus:
            text = u'•' + (text)
        self._invalidate()
        return len(self.get_line_translation(maxcol, ta=(text, attr)))

    def pack(self, size=None, focus=False):
        text, attr = self.get_text()
        if focus:
            text = u'•' + (text)
        self._invalidate()

        # Be nice if there was a way to avoid having to copy this code from
        # the parent class....
        if size is not None:
            (maxcol,) = size
            if not hasattr(self.layout, "pack"):
                return size
            trans = self.get_line_translation(maxcol, ta=(text, attr))
            cols = self.layout.pack(maxcol, trans)
            return (cols, len(trans))

        i = 0
        cols = 0
        while i < len(text):
            j = text.find('\n', i)
            if j == -1:
                j = len(text)
            c = urwid.calc_width(text, i, j)
            if c > cols:
                cols = c
            i = j + 1
        return (cols, text.count('\n') + 1)


class ReviewDate(urwid.Text):
    def __init__(self, when=None):
        super(ReviewDate, self).__init__('')
        self.when = when
        if when is not None:
            self.set_text(_format_date(when))


class ReviewTable(urwid.ListBox):
    def __init__(self, max_size=1):
        super(ReviewTable, self).__init__(urwid.SimpleListWalker([]))
        assert int(max_size) > 0, "Max size must be > 0"
        self._max_size = int(max_size)
        self._sort_by = [
            (None, None),  # no sorting
            ('Created On (Desc)', self._sort_date("Created On", False)),
            ('Created On (Asc)', self._sort_date("Created On", True)),
            ('Updated On (Desc)', self._sort_date("Updated On", False)),
            ('Updated On (Asc)', self._sort_date("Updated On", True)),
            ('Subject (Desc)', self._sort_text("Subject", False)),
            ('Subject (Asc)', self._sort_text("Subject", True)),
            ('Username (Desc)', self._sort_text("Username", False)),
            ('Username (Asc)', self._sort_text("Username", True)),
            ('Project (Desc)', self._sort_text("Project", False)),
            ('Project (Asc)', self._sort_text("Project", True)),
            ('Topic (Desc)', self._sort_text("Topic", False)),
            ('Topic (Asc)', self._sort_text("Topic", True)),
        ]
        self._sort_idx = 0
        self._rows = []

    def _sort_text(self, col_name, asc):
        col_idx = COLUMN_2_IDX[col_name]
        flip_map = {
            0: 0,
            -1: 1,
            1: -1,
        }

        def sorter(i1, i2):
            t1 = i1.contents[col_idx][0]
            t2 = i2.contents[col_idx][0]
            r = cmp(t1.text, t2.text)
            if not asc:
                r = flip_map[r]
            return r

        return sorter

    def _sort_date(self, col_name, asc):
        col_idx = COLUMN_2_IDX[col_name]
        flip_map = {
            0: 0,
            -1: 1,
            1: -1,
        }

        def sorter(i1, i2):
            d1 = i1.contents[col_idx][0]
            d2 = i2.contents[col_idx][0]
            if d1.when is None and d2.when is None:
                r = 0
            if d1.when is None and d2.when is not None:
                r = -1
            if d1.when is not None and d2.when is None:
                r = 1
            if d1.when is not None and d2.when is not None:
                r = cmp(d1.when, d2.when)
            if not asc:
                r = flip_map[r]
            return r

        return sorter

    @property
    def entries(self):
        return len(self.body)

    @property
    def max_size(self):
        return self._max_size

    def _add_row(self, row):
        if len(row.contents) != len(COLUMNS):
            raise RuntimeError("Attempt to add a row with differing"
                               " column count")
        if len(self._rows) >= self.max_size:
            self._rows.pop()
        self._rows.insert(0, row)
        (_sort_title, sort_functor) = self._sort_by[self._sort_idx]
        if sort_functor:
            self._refill(sorted(self._rows, cmp=sort_functor))
        else:
            if len(self.body) >= self.max_size:
                self.body.pop()
            self.body.insert(0, row)

    def _find_change(self, change):
        url_i = COLUMN_2_IDX['Url']
        m_c = None
        for c in self.body:
            url = c.contents[url_i]
            if url[0].text == change.get('url'):
                m_c = c
                break
        return m_c

    def _update_last_updated(self, match):
        updated_i = COLUMN_2_IDX['Updated On']
        new_contents = list(match.contents[updated_i])
        new_contents[0] = ReviewDate(datetime.now())
        match.contents[updated_i] = tuple(new_contents)
        (sort_title, sort_functor) = self._sort_by[self._sort_idx]
        if sort_title and sort_title.startswith("Updated On"):
            self._refill(sorted(self._rows, cmp=sort_functor))

    def _set_status(self, match, text):
        if not text or match is None:
            return None
        status_i = COLUMN_2_IDX['Status']
        new_contents = list(match.contents[status_i])
        new_contents[0] = urwid.AttrWrap(_make_text(text), text.lower())
        match.contents[status_i] = tuple(new_contents)
        return match

    def on_change_merged(self, event):
        change = event['change']
        match = self._find_change(change)
        if match is not None:
            self._set_status(match, 'Merged')
            self._update_last_updated(match)

    def on_change_restored(self, event):
        change = event['change']
        match = self._find_change(change)
        if match is not None:
            reason = _get_text('reason', event)
            if len(reason):
                comment_i = COLUMN_2_IDX['Comment']
                new_column = list(match.contents[comment_i])
                new_column[0] = _format_text(reason)
                match.contents[comment_i] = tuple(new_column)
            self._set_status(match, 'Restored')
            self._update_last_updated(match)

    def on_comment_added(self, event):
        change = event['change']
        match = self._find_change(change)
        if match is not None:
            comment = _get_text('comment', event)
            if len(comment):
                comment_i = COLUMN_2_IDX['Comment']
                new_column = list(match.contents[comment_i])
                new_column[0] = _format_text(comment)
                match.contents[comment_i] = tuple(new_column)
            self._set_status(match, _get_change_status(event))
            self._update_last_updated(match)

    def on_change_abandoned(self, event):
        change = event['change']
        match = self._find_change(change)
        if match is not None:
            reason = _get_text('reason', event)
            if len(reason):
                comment_i = COLUMN_2_IDX['Comment']
                new_column = list(match.contents[comment_i])
                new_column[0] = _format_text(reason)
                match.contents[comment_i] = tuple(new_column)
            self._set_status(match, 'Abandoned')
            self._update_last_updated(match)

    def on_patchset_created(self, event):
        change = event['change']
        match = self._find_change(change)
        if match is not None:
            # NOTE(harlowja): already being actively displayed
            return
        patch_set = event['patchSet']
        uploader = event['uploader']
        last_updated = _get_date('lastUpdated', patch_set)
        if last_updated is None:
            last_updated = datetime.now()
        row = [
            ToggleText(_get_text('username', uploader),
                       wrap='space', align='left'),
            _get_text('topic', change),
            _get_text('url', change),
            _get_text('project', change),
            _get_text('subject', change),
            ReviewDate(_get_date('createdOn', patch_set)),
            ReviewDate(last_updated),
            "",  # status
            "",  # comment
        ]
        attr_row = []
        for (i, v) in enumerate(row):
            col_name = COLUMNS[i]
            try:
                col_attrs = list(COLUMN_ATTRIBUTES[col_name])
            except (KeyError, TypeError):
                col_attrs = []
            if isinstance(v, six.string_types):
                col_attrs.append(_format_text(v))
            else:
                col_attrs.append(v)
            attr_row.append(tuple(col_attrs))
        cols = urwid.Columns(attr_row, dividechars=1)
        self._set_status(cols, 'Open')
        self._add_row(cols)

    def _refill(self, new_body):
        while len(self.body):
            self.body.pop()
        self.body.extend(new_body)

    def next_sort(self):
        self._sort_idx += 1
        self._sort_idx = self._sort_idx % len(self._sort_by)
        (sort_title, sort_functor) = self._sort_by[self._sort_idx]
        if not all([sort_title, sort_functor]):
            self._refill(self._rows)
        else:
            self._refill(sorted(self._rows, cmp=sort_functor))
        return sort_title


class SizingFrame(urwid.Frame):
    def __init__(self, review_table):
        super(SizingFrame, self).__init__(urwid.AttrWrap(review_table, 'body'))
        self._review_table = review_table
        connect_signal(self._review_table.body, 'modified',
                       self._set_refresh_header_footer)
        self._refresh_headers = True
        self._last_size = (-1, -1)
        self._footer_pieces = [None, None, None]

    @property
    def review_table(self):
        return self._review_table

    @property
    def right_footer(self):
        if self._footer_pieces[2] is None:
            self._footer_pieces[2] = urwid.Text('', align='right')
        return self._footer_pieces[2]

    @property
    def left_footer(self):
        if self._footer_pieces[0] is None:
            self._footer_pieces[0] = urwid.Text('', align='left')
        return self._footer_pieces[0]

    @property
    def center_footer(self):
        if self._footer_pieces[1] is None:
            self._footer_pieces[1] = urwid.Text('', align='center')
        return self._footer_pieces[1]

    def _set_refresh_header_footer(self):
        self._refresh_headers = True

    def _make_header(self, percent_below=None):
        table_header = []
        for col_name in COLUMNS:
            try:
                col_attrs = list(COLUMN_ATTRIBUTES[col_name])
            except KeyError:
                col_attrs = []
            col_attrs.append(_make_text(col_name))
            table_header.append(tuple(col_attrs))
        table_cols = urwid.Columns(table_header, dividechars=1)
        h_div = urwid.Divider(u'─')
        if percent_below is not None:
            comp = urwid.Text(u"⇡%i%%⇡" % (percent_below * 100))
            header_cols = urwid.Columns([
                h_div,
                (urwid.FLOW, comp),
                h_div,
            ])
        else:
            header_cols = urwid.Columns([h_div])
        return urwid.Pile([table_cols, header_cols])

    def _make_footer(self, percent_above=None):
        footer_pieces = [
            self.left_footer,
            (urwid.FLOW, self.center_footer),
            self.right_footer,
        ]
        cols = urwid.Columns(footer_pieces)
        f_div = urwid.Divider(u'─')
        if percent_above is not None:
            comp = urwid.Text(u"⇣%i%%⇣" % (percent_above * 100))
            footer_cols = urwid.Columns([
                f_div,
                (urwid.FLOW, comp),
                f_div,
            ])
        else:
            footer_cols = urwid.Columns([f_div])
        return urwid.Pile([footer_cols, cols])

    def _reset_footer_header(self, size):
        item_count = self.review_table.entries
        if item_count == 0:
            self.header = self._make_header()
            self.footer = self._make_footer()
            return
        middle, top, bottom = self.review_table.calculate_visible(size, False)
        row_offset, focus_widget, focus_pos, focus_rows, cursor = middle
        items_above_visible = len(top[1])
        items_below_visible = len(bottom[1])
        items_visible = 1 + items_below_visible + items_above_visible
        initial_focus_pos = focus_pos - items_above_visible
        items_above = initial_focus_pos
        items_below = item_count - (items_above + items_visible)
        if items_above == 0:
            self.header = self._make_header()
        else:
            self.header = self._make_header(float(items_above) / item_count)
        if items_below == 0:
            self.footer = self._make_footer()
        else:
            self.footer = self._make_footer(float(items_below) / item_count)

    def keypress(self, size, key):
        m_key = self._command_map[key]
        if m_key in REFRESH_KEYS:
            self._set_refresh_header_footer()
        key = super(SizingFrame, self).keypress(size, key)
        if not key:
            return None
        if key in SORT_CHANGE_KEYS:
            sort_title = self.review_table.next_sort()
            if sort_title:
                self.center_footer.set_text("Sort: %s" % (sort_title))
            else:
                self.center_footer.set_text('')
            return None
        return key

    def render(self, size, focus=False):
        if self._refresh_headers or self._last_size != size:
            if size is None or len(size) != 2:
                self.header = self._make_header()
                self.footer = self._make_footer()
            else:
                used_rows, _orig_rows = self.frame_top_bottom(size,
                                                              focus=focus)
                (maxcols, maxrows) = size
                real_size = (maxcols, max(0, maxrows - sum(used_rows)))
                self._reset_footer_header(real_size)
            self._refresh_headers = False
            self._last_size = size
        return super(SizingFrame, self).render(size, focus)

###


def main():
    parser = optparse.OptionParser()
    parser.add_option("-u", "--user", dest="username", action='store',
                      help="gerrit user [default: %default]", metavar="USER",
                      default=getpass.getuser())
    parser.add_option("-s", "--server", dest="server", action='store',
                      help="gerrit server [default: %default]",
                      metavar="SERVER", default=GERRIT_HOST)
    parser.add_option("-p", "--port", dest="port", action='store',
                      type="int", help="gerrit port [default: %default]",
                      metavar="PORT", default=GERRIT_PORT)
    parser.add_option("--prefetch", dest="prefetch", action='store',
                      type="int", help="prefetch amount [default: %default]",
                      metavar="COUNT", default=PREFETCH_LEN)
    parser.add_option("-k", "--keyfile", dest="keyfile", action='store',
                      help="gerrit ssh keyfile [default: %default]",
                      metavar="FILE", default=_get_key_path())
    parser.add_option("--project", dest="projects", action='append',
                      help="only show given projects reviews",
                      metavar="PROJECT", default=[])
    parser.add_option("-i", "--items", dest="back", action='store',
                      type="int",
                      help="how many items to keep visible"
                           " [default: %default]",
                      metavar="COUNT", default=VISIBLE_LIST_LEN)
    parser.add_option("-r", "--record-file", dest="record", action='store',
                      help="record file to store past events (also used for "
                           "initial view population if provided)",
                      metavar="FILE",
                      default=None)
    (options, args) = parser.parse_args()
    if options.back <= 0:
        parser.error("Item count must be greater or equal to one.")
    gerrit_config = {
        'keyfile': options.keyfile,
        'port': int(options.port),
        'server': options.server,
        'username': options.username,
        'prefetch': max(0, options.prefetch),
        'record': options.record,
    }
    event_queue = Queue.Queue(maxsize=QUEUE_MAX_SIZE)
    gerrit_reader = GerritWatcher(event_queue, **gerrit_config)
    gerrit_details = collections.defaultdict(int)

    review_table = ReviewTable(max_size=options.back)
    frame = SizingFrame(review_table)
    frame.left_footer.set_text("Initializing...")

    def filter_event(event):
        if len(options.projects) == 0:
            return False
        project = None
        try:
            project = event['change']['project']
        except (KeyError, TypeError, ValueError):
            pass
        if project in options.projects:
            return False
        return True

    def on_unhandled_input(key):
        if key in QUIT_KEYS:
            raise urwid.ExitMainLoop()

    def process_event(event):
        if not isinstance(event, (dict)) or not 'type' in event:
            return
        if filter_event(event):
            return
        event_type = str(event['type'])
        gerrit_details[event_type] += 1
        if event_type == 'patchset-created':
            review_table.on_patchset_created(event)
        elif event_type == 'comment-added':
            review_table.on_comment_added(event)
        elif event_type == 'change-merged':
            review_table.on_change_merged(event)
        elif event_type == 'change-restored':
            review_table.on_change_restored(event)
        elif event_type == 'change-abandoned':
            review_table.on_change_abandoned(event)
        else:
            raise RuntimeError("Unknown event type: '%s'" % (event_type))

    def update_footer(events):
        detail_text = "%s, %s events received (%sp, %sc, %sm, %sr, %sa)"
        detail_text = detail_text % (_format_date(),
                                     sum(gerrit_details.values()),
                                     gerrit_details.get('patchset-created', 0),
                                     gerrit_details.get('comment-added', 0),
                                     gerrit_details.get('change-merged', 0),
                                     gerrit_details.get('change-restored', 0),
                                     gerrit_details.get('change-abandoned', 0))
        frame.right_footer.set_text(detail_text)
        if gerrit_reader.is_alive():
            if not gerrit_reader.connected:
                frame.left_footer.set_text("Connecting...")
            else:
                if events == 0:
                    frame.left_footer.set_text("Waiting for events...")
                else:
                    frame.left_footer.set_text("Processing events...")
        else:
            frame.left_footer.set_text("Initializing...")

    def process_gerrit(loop, user_data):
        evs = _consume_queue(event_queue)
        for e in evs:
            try:
                process_event(e)
            except Exception:
                LOG.exception("Failed handling event: %s", e)
        update_footer(len(evs))

    def on_idle(loop):
        loop.set_alarm_in(ALARM_FREQ, process_gerrit)

    loop = urwid.MainLoop(urwid.LineBox(frame), PALETTE,
                          handle_mouse=False,
                          unhandled_input=on_unhandled_input)
    gerrit_reader.start()
    loop.event_loop.enter_idle(functools.partial(on_idle, loop))
    loop.run()


if __name__ == "__main__":
    main()
