#!/usr/bin/env python
#
# Copyright (C) 2011 John Feuerstein <john@feurix.com>
#
#   Project URL: http://feurix.org/projects/speedpad/
#    Mirror URL: http://code.google.com/p/speedpad/
#
# 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 3 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 Library 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
__author__ = 'John Feuerstein <john@feurix.com>'
__copyright__ = 'Copyright (C) 2011 %s' % __author__
__license__ = 'GNU GPLv3'
__version__ = '1.0'

import codecs
import collections
import curses
import curses.ascii
import datetime
import fcntl
import itertools
import operator
import signal
import struct
import textwrap
import time
import tty


class OutputEncodingMixIn(object):

    def encode(self, s):
        if self.output_encoding is not None:
            return self.output_encoding.encode(s)[0]
        return s


class InputDecodingMixIn(object):

    def decode(self, s):
        if self.input_encoding is not None:
            return self.input_encoding.decode(s)[0]
        return s


class Box(InputDecodingMixIn, OutputEncodingMixIn):

    BOX_YMIN = 1
    BOX_XMIN = 1

    def __init__(self, uly, ulx, lry, lrx, color=curses.A_NORMAL,
                 input_encoding=None, output_encoding=None):
        """Instantiate box window

        The parameters uly and ulx specify the coordinates of the upper left
        box corner. The parameters lry and lrx specify the coordinates of
        the lower right box corner.
        """
        self.input_encoding = input_encoding
        self.output_encoding = output_encoding
        self.uly = uly
        self.ulx = ulx
        self.lry = lry
        self.lrx = lrx
        self.boxlines = lry - uly
        self.boxcols = lrx - ulx
        self.color = color

        if self.boxlines < Box.BOX_YMIN or self.boxcols < Box.BOX_XMIN:
            raise ValueError("invalid box size")

        self.box = curses.newwin(self.boxlines, self.boxcols, uly, ulx)
        self.box.attron(self.color)

    def reset(self):
        self.box.erase()
        self.box.attron(self.color)
        self.box.move(0, 0)

    def erase(self):
        self.box.erase()
        self.box.attron(self.color)

    def noutrefresh(self):
        self.box.noutrefresh()

    def resize(self, ydiff, xdiff):
        self.lry = max(self.uly + Box.BOX_YMIN, self.lry + ydiff)
        self.lrx = max(self.ulx + Box.BOX_XMIN, self.lrx + xdiff)
        self.boxlines = self.lry - self.uly
        self.boxcols = self.lrx - self.ulx
        self.box.resize(self.boxlines, self.boxcols)

    def move(self, ydiff, xdiff):
        self.uly += ydiff
        self.ulx += xdiff
        self.lry += ydiff
        self.lrx += xdiff
        self.box.mvwin(self.uly, self.ulx)


class PadBox(Box):

    PAD_YMIN = 2
    PAD_XMIN = 2

    def __init__(self, uly, ulx, lry, lrx,
                 padlines=PAD_YMIN, padcols=PAD_XMIN, soy=0, sox=0,
                 boxcolor=curses.A_NORMAL, padcolor=curses.A_NORMAL,
                 input_encoding=None, output_encoding=None):
        """Instantiate pad box

        The parameters uly and ulx specify the coordinates of the upper left
        box corner. The parameters lry and lrx specify the coordinates of
        the lower right box corner.

        The parameters padlines and padcols specify the pad size, its visible
        area is limited by the size of the box window.

        The parameters soy and sox specify the amount of lines and columns to
        keep below and behind the cursor while scrolling, respectively.
        """
        super(PadBox, self).__init__(uly, ulx, lry, lrx, color=boxcolor,
                                     input_encoding=input_encoding,
                                     output_encoding=output_encoding)
        self.padlines = padlines
        self.padcols = padcols
        self.ypos = 0
        self.xpos = 0
        self.soy = soy
        self.sox = sox
        self.ymax = max(0, self.boxlines - soy)
        self.xmax = max(0, self.boxcols - sox)
        self.color = padcolor

        if self.padlines < PadBox.PAD_YMIN or self.padcols < PadBox.PAD_XMIN:
            raise ValueError("invalid pad size")

        self.pad = curses.newpad(self.padlines, self.padcols)
        self.pad.attron(self.color)

    def reset(self):
        self.pad.erase()
        self.pad.attron(self.color)
        self.ypos = 0
        self.xpos = 0
        self.pad.move(0, 0)

    def noutrefresh(self):
        super(PadBox, self).noutrefresh()
        self.pad.noutrefresh(self.ypos, self.xpos,
                             self.uly, self.ulx,
                             self.lry - 1, self.lrx - 1)

    def resize(self, ydiff, xdiff):
        super(PadBox, self).resize(ydiff, xdiff)
        self.ymax = max(0, self.boxlines - self.soy)
        self.xmax = max(0, self.boxcols - self.sox)

    def scroll(self, ydiff, xdiff):
        self.ypos = min(self.padlines - self.boxlines,
                        max(0, self.ypos + ydiff))
        self.xpos = min(self.padcols - self.boxcols,
                        max(0, self.xpos + xdiff))

    def extract(self, ypos, xpos, n=1):
        oldy, oldx = self.pad.getyx()
        res = self.pad.instr(ypos, xpos, n)
        self.pad.move(oldy, oldx)
        return res

    def sol(self, ypos, skip=0):
        oldy, oldx = self.pad.getyx()
        startpos = 0
        for xpos in xrange(skip, self.padcols):
            if self.pad.inch(ypos, xpos) != curses.ascii.SP:
                startpos = xpos
                break
        self.pad.move(oldy, oldx)
        return startpos

    def eol(self, ypos):
        oldy, oldx = self.pad.getyx()
        endpos = 0
        for xpos in xrange(self.padcols - 1, -1, -1):
            if self.pad.inch(ypos, xpos) != curses.ascii.SP:
                endpos = xpos + 1
                break
        self.pad.move(oldy, oldx)
        return endpos


class SpeedBox(Box):

    BOX_XMIN = 60

    def __init__(self, *args, **kwargs):
        super(SpeedBox, self).__init__(*args, **kwargs)
        if self.boxcols < SpeedBox.BOX_XMIN:
            raise ValueError("invalid box size")
        self.players = []

    def load(self, quote):
        for player in self.players:
            player.progressbar.end = quote.strlen

    def update(self, player):
        player.progressbar.cur = player.pos

    def register(self, player):
        player.progressbar = ProgressBar(SpeedBox.BOX_XMIN - 17)
        self.players.append(player)

    def resize(self, ydiff, xdiff):
        for player in self.players:
            player.progressbar.resize(ydiff, xdiff)
        super(SpeedBox, self).resize(ydiff, xdiff)

    def reset(self):
        for player in self.players:
            player.progressbar.reset()
            player.reset()

    def draw(self, speedunit):
        ymax = self.boxlines - 1
        players = self.players
        players.sort(key=operator.attrgetter('pos'), reverse=True)
        players.sort(key=operator.attrgetter('speed'), reverse=True)
        for ypos, player in enumerate(players):
            if ypos > ymax: break
            if len(players) > 1:
                self.box.addstr(ypos, 0, "#%d" % min(9, (ypos + 1)),
                                curses.A_BOLD)
            self.box.addstr(ypos, 4, self.encode(player.name[:8].ljust(8)))
            self.box.addstr(ypos, 13, "%4d %s" %
                            (min(9999, speedunit(player.speed)), speedunit))
            player.progressbar.draw(self.box, ypos, 22, color=player.color)
            self.box.insstr(ypos, self.boxcols - 4,
                            "%3d%%" % (player.progressbar.pos * 100))


class QuoteBox(PadBox):

    def load(self, quote):
        for ypos, line in enumerate(quote):
            self.pad.addstr(ypos, 0, self.encode(line))

    def draw_stats(self, quote):
        for ypos, line in enumerate(quote):
            self.pad.addstr(ypos, 0, self.encode(line))
        for ypos, xpos in quote.stats.typos:
            self.pad.chgat(ypos, xpos, 1, curses.A_REVERSE)


class InputBox(PadBox):

    def __init__(self, *args, **kwargs):
        self.tabsize = kwargs.pop('tabsize', 8)
        super(InputBox, self).__init__(*args, **kwargs)
        self.pad.keypad(1)
        self.pad.timeout(0)

    def decode_chars(self, chars):
        """Decode multi-byte character into unicode string"""
        s = ''.join(map(chr, chars))
        s = self.decode(s)
        return s

    def getch(self):
        """Get character from user (possibly multi-byte)

        Return first byte and multi-byte representation.
        """
        ch = self.pad.getch()
        chars = [ch]
        if ch < 0:
            return ch, chars
        if self.input_encoding.name == 'utf-8':
            def next():
                ch = self.pad.getch()
                if not 128 <= ch <= 191:
                    raise UnicodeDecodeError(
                            "invalid continuation byte: %r" % ch)
                return ch
            if ch <= 127:           # 1 byte
                pass
            elif 194 <= ch <= 223:  # 2 bytes
                chars.append(next())
            elif 224 <= ch <= 239:  # 3 bytes
                chars.append(next())
                chars.append(next())
            elif 240 <= ch <= 244:  # 4 bytes
                chars.append(next())
                chars.append(next())
                chars.append(next())
            elif 192 <= ch <= 193 or 245 <= ch <= 255:
                raise UnicodeDecodeError("invalid first byte: %r" % ch)
        return ch, chars

    def putch(self, ch):
        """Add a character and adjust cursor position"""
        ypos, xpos = self.pad.getyx()
        if ypos == self.padlines - 1 and xpos == self.padcols - 1:
            return
        if ch == curses.ascii.NL:
            if ypos < self.padlines - 1:
                self.pad.move(ypos + 1, 0)
        elif ch in (curses.ascii.BS, curses.ascii.DEL, curses.KEY_BACKSPACE):
            if xpos > 0:
                if self.tabsize:
                    trail = xpos % self.tabsize or self.tabsize
                    prevtabstop = xpos - trail
                    rightborder = max(prevtabstop, self.eol(ypos))
                    self.pad.move(ypos, min(xpos - 1, rightborder))
                else:
                    self.pad.move(ypos, min(xpos - 1, self.eol(ypos)))
                self.pad.delch()
            elif ypos > 0:
                self.pad.move(ypos - 1, self.eol(ypos - 1))
        else:
            self.pad.addch(ch)

    def continue_comment(self, oldypos, oldsol, oldeol, neweol, indent=False):
        """Calculate and return comment continuation

        The parameters oldypos, oldsol, and oldeol specify coordinates of the
        current line (the line we want to continue).

        The parameter neweol gives us a hint about the maximum length of the
        new line.
        """
        lead = self.extract(oldypos, oldsol, 2)
        trail = self.extract(oldypos, max(0, oldeol - 2), 2)
        if lead == '*/':
            if oldsol > 0 and neweol > 0:
                return [curses.ascii.BS]
            return []
        if lead == '/*':
            if trail == '*/':
                return []
            continuation = [ord(' '), ord('*')]
        elif lead == '//':
            continuation = [ord('/'), ord('/')]
        else:
            lead = lead[:1]
            if lead == '*':
                continuation = [ord('*')]
            elif lead == '#':
                continuation = [ord('#')]
            else:
                return []
        if len(continuation) > neweol:
            return []
        if len(continuation) == neweol:
            return continuation
        if not indent:
            return continuation
        eoc = oldsol + len(lead)
        indent = self.sol(oldypos, skip=eoc)
        if not indent:
            return continuation
        spaces = indent - eoc
        if not spaces:
            return continuation
        if len(continuation) + spaces < neweol:
            spacegen = itertools.repeat(curses.ascii.SP, spaces)
            continuation.extend(spacegen)
        return continuation

    def draw_stats(self, quote, player):
        good = float(quote.stats.keystrokes_good)
        typo = float(quote.stats.keystrokes_typo)
        total = float(quote.stats.keystrokes_total)
        if not total: return

        xpos = 0
        format = lambda speed: min(10 ** (6 - 1 - 3) - 10 ** -3, speed)
        self.pad.addstr(0, xpos, "Speed: %6.3f %s" %
                        (format(cps(player.speed)), cps))
        format = lambda speed: min(10 ** (13 - 1 - 3) - 10 ** -3, speed)
        for ypos, unit in enumerate((cpm, wpm, ppm, cph), 1):
            self.pad.addstr(ypos, xpos, "%13.3f %s" %
                            (format(unit(player.speed)), unit))
        xpos += 19
        format = lambda keystrokes: min(10 ** 4 - 1, keystrokes)
        self.pad.addstr(0, xpos, "Keystrokes: %4d (enter)" %
                        format(quote.stats.keystrokes_enter))
        format = lambda keystrokes: min(10 ** 16 - 1, keystrokes)
        self.pad.addstr(1, xpos, "%16d (tab/space)" %
                        format(quote.stats.keystrokes_tab +
                               quote.stats.keystrokes_space))
        self.pad.addstr(2, xpos, "%16d (good: %d%%)" %
                        (format(good), round(good / total * 100)))
        self.pad.addstr(3, xpos, "%16d (typo: %d%%)" %
                        (format(typo), round(typo / total * 100)))
        self.pad.addstr(4, xpos, "%16d (total)" % format(total))
        if typo:
            xmax = min(SpeedPad.SCR_XMIN + 4, self.boxcols)
            xpos = xmax - 25
            highscore = quote.stats.typo_highscore[:5]
            self.pad.addstr(0, xpos, "Typo Highscore:")
            xpos += 5
            self.pad.addstr(1, xpos, "[1] [321]")
            self.pad.addstr(2, xpos, "[2]  ||| ")
            self.pad.addstr(3, xpos, " |  typos")
            self.pad.addstr(4, xpos, " expected")
            xpos += 11
            places = min(80, self.boxcols) - xpos - 6
            expand = lambda typos: typos[:places].ljust(places)
            for ypos, score in enumerate(highscore):
                expected, typos = score
                typos = ''.join(typo for typo, count in typos)
                self.pad.addstr(ypos, xpos, "[%s] [%s]" %
                                (self.encode(expected[:1]),
                                 self.encode(expand(typos))))


class InputStats(object):

    def __init__(self):
        self.typos = {}
        self.keystrokes_typo = 0
        self.keystrokes_good = 0
        self.keystrokes_tab = 0
        self.keystrokes_space = 0
        self.keystrokes_enter = 0
        self.timer = Timer()

        # Assume we expect good character 'x' and:
        # - get typo 'y' instead:
        #   typocounts['x']['y'] += 1
        #   typocounts -> {'x': {'y': 1}}
        # - get typo 'z' instead:
        #   typocounts['x']['z'] += 1
        #   typocounts -> {'x': {'y': 1, 'z': 1}}
        # - get typo 'z' again:
        #   typocounts['x']['z'] += 1
        #   typocounts -> {'x': {'y': 1, 'z': 2}}
        self.typocounts = collections.defaultdict(
                lambda: collections.defaultdict(int))

    def reset(self):
        self.typos = {}
        self.typocounts.clear()
        self.keystrokes_typo = 0
        self.keystrokes_good = 0
        self.keystrokes_tab = 0
        self.keystrokes_space = 0
        self.keystrokes_enter = 0
        self.timer.reset()

    def addtypo(self, ypos, xpos, count=1):
        self.typos[ypos, xpos] = True
        self.keystrokes_typo += count

    def fixtypo(self, ypos, xpos, count=1):
        if (ypos, xpos) in self.typos:
            self.typos[ypos, xpos] = False
        self.keystrokes_good += count

    @property
    def keystrokes_total(self):
        return self.keystrokes_typo + self.keystrokes_good

    @property
    def speed(self):
        elapsed = float(self.timer.elapsed)
        if elapsed < 1.0: return Speed(0.0)
        return Speed(self.keystrokes_good / elapsed)

    @property
    def typo_highscore(self):
        highscore = []
        typocounts = self.typocounts.items()
        # [(c,{z:1}), (z,{x:1,y:2}), (a,{x:1}), (b,{v:2})]
        # sort increasing by alphabet
        typocounts.sort(key=operator.itemgetter(0))
        # [(a,{x:1}), (b,{v:2}, (c,{z:1}), (z,{x:1,y:2})]
        # sort decreasing by amount of typos (stable)
        typosum = lambda i: sum(v for k, v in i[1].iteritems())
        typocounts.sort(key=typosum, reverse=True)
        # [(z,{x:1,y:2}), (b,{v:2}), (a,{x:1}), (c,{z:1})]
        # sort nested dicts decreasing by value
        for expected, typos in typocounts:
            typos = typos.items()
            # [(x,1), (y,2), (a,1)]
            # sort increasing by alphabet
            typos.sort(key=operator.itemgetter(0))
            # [(a,1), (x,1), (y,2)]
            # sort decreasing by amount of typos (stable)
            typos.sort(key=operator.itemgetter(1), reverse=True)
            # [(y,2), (a,1), (x,1)]
            highscore.append((expected, typos))
        # [(z,[(y,2),(x,1)]), (b,[(v,2)]), (a,[(x,1)]), (c,[(z,1)])]
        return highscore


class Quote(object):

    def __init__(self, lines):
        self.lines = list(lines)
        if not self.lines:
            raise ValueError("missing input")
        self.ymax = len(self.lines)
        self.xmax = len(max(self.lines, key=len))
        self.strlen = sum(len(line) for line in self.lines)
        self.stats = InputStats()

    def __iter__(self):
        return iter(self.lines)

    def inrange(self, ypos, xpos):
        return (0 <= ypos < len(self.lines) and
                0 <= xpos < len(self.lines[ypos]))

    def strpos(self, ypos, xpos):
        if ypos < 0 or xpos < 0:
            raise IndexError
        if ypos < len(self.lines):
            prev = itertools.islice(self.lines, ypos)
            return sum((len(line) for line in prev),
                       min(len(self.lines[ypos]), xpos))
        return self.strlen

    def istypo(self, ypos, xpos, s, record=False, **kwargs):
        if not self.inrange(ypos, xpos):
            return True
        expect = self.lines[ypos][xpos]
        if s == expect:
            if record:
                self.stats.fixtypo(ypos, xpos, **kwargs)
            return False
        if record:
            self.stats.addtypo(ypos, xpos, **kwargs)
            self.stats.typocounts[expect][s] += 1
        return True

    def eol(self, ypos):
        if not 0 <= ypos < len(self.lines):
            return 0
        return len(self.lines[ypos])

    def iseol(self, ypos, xpos):
        return ypos < len(self.lines) and xpos >= len(self.lines[ypos])

    def iscomplete(self, ypos, xpos):
        if ypos == len(self.lines) - 1:
            return xpos >= len(self.lines[ypos])
        return ypos >= len(self.lines)

    def iscorrect(self):
        return not any(self.stats.typos.itervalues())


class QuoteGenerator(InputDecodingMixIn):

    def __init__(self, factory, maxlines, maxcols,
                 wrap=0, width=0, tabsize=8, strip=True, input_encoding=None):
        self.input_encoding = input_encoding
        if maxlines < 1 or maxcols < 1:
            raise ValueError("invalid size")
        if wrap < 0:
            width = maxcols
        elif wrap == 0:
            width = min(maxcols, width) or maxcols
        else:
            width = min(maxcols, wrap) or maxcols
        self.adaptive = wrap == 0
        self.tabsize = tabsize
        self.strip = strip
        self.maxlines = maxlines
        self.maxcols = maxcols
        self.iterator = factory(maxlines * maxcols)
        self.wrapper = textwrap.TextWrapper(width=width)

    def __iter__(self):
        return self

    def next(self):
        raw = self.iterator.next()
        try:
            raw = self.decode(raw)
        except UnicodeDecodeError as e:
            raise QuoteGeneratorError(e)
        res = self.clean(raw)
        try:
            quote = Quote(res)
        except ValueError as e:
            raise QuoteGeneratorError(e)
        return quote

    def clean(self, raw):
        """Take raw string input and return list of clean lines"""
        end = self.maxlines
        raw = raw.strip() if self.strip else raw.rstrip()
        raw = raw.expandtabs(self.tabsize)
        raw = raw.splitlines()
        res = []
        for line in itertools.islice(raw, end):
            if self.strip:
                words = line.split()
                line = ' '.join(words)
            else:
                line = line.rstrip()
            if line:
                lines = self.wrapper.wrap(line)
                lines = itertools.islice(lines, max(0, end - len(res)))
                res.extend(lines)
            else:
                res.append(line)
            if len(res) == end:
                break
        return res

    def resize(self, ydiff, xdiff):
        if self.adaptive:
            width = max(1, self.wrapper.width + xdiff)
            self.wrapper.width = min(self.maxcols, width)


class Timer(object):

    def __init__(self):
        self.started = 0
        self.stopped = 0

    def start(self):
        if self.started:
            raise RuntimeError
        self.started = time.time()

    def stop(self):
        if not self.started or self.stopped:
            raise RuntimeError
        self.stopped = time.time()

    def reset(self):
        self.started = 0
        self.stopped = 0

    @property
    def elapsed(self):
        if not self.started:
            return 0
        if not self.stopped:
            return time.time() - self.started
        return self.stopped - self.started


class ProgressBar(object):

    def __init__(self, width):
        self.width = width
        self._end = 0
        self._cur = 0
        self._pos = 0.0

    def draw(self, win, ypos, xpos, color=curses.A_NORMAL):
        if not self.width: return
        end = self.width
        pos = int(self.width * self.pos)
        win.hline(ypos, xpos, curses.ascii.SP, end, color)
        win.hline(ypos, xpos, curses.ascii.SP, pos, color | curses.A_REVERSE)

    def resize(self, ydiff, xdiff):
        self.width = max(0, self.width + xdiff)

    def reset(self):
        self._end = 0
        self._cur = 0
        self._pos = 0.0

    @property
    def pos(self):
        return self._pos

    @property
    def cur(self):
        return self._cur

    @cur.setter
    def cur(self, val):
        self._cur = max(0, val)
        if self._end:
            self._pos = min(1.0, float(self._cur) / float(self._end))
        else:
            self._pos = 0.0

    @property
    def end(self):
        return self._end

    @end.setter
    def end(self, val):
        self._end = max(0, val)
        if self._end:
            self.cur = self.cur
        else:
            self.cur = 0


class Player(object):

    def __init__(self, name, color=curses.A_NORMAL):
        self.name = name
        self.color = color
        self.pos = 0
        self.speed = Speed(0.0)

    def __hash__(self):
        return hash(self.name)

    def reset(self):
        self.pos = 0
        self.speed = Speed(0.0)


class Robot(Player):

    def __init__(self, name, speed):
        super(Robot, self).__init__(name)
        self.speed = Speed(speed)

    def reset(self):
        self.pos = 0


class SpeedPad(InputDecodingMixIn, OutputEncodingMixIn):
    """Manage all components"""

    SCR_XMIN = 70
    SCR_YMIN = 20
    PAD_XMAX = 200
    PAD_YMAX = 1000

    def __init__(self, factory=lambda maxsize: [], ttyfd=0,
                 strict=False, strip=True, color=True, indent=False,
                 syntax=False, user=None, robot=None, player=None,
                 wrap=0, tabsize=8, speed=0.0, speedunit=None,
                 input_encoding=None, output_encoding=None):
        self.ttyfd = ttyfd
        self.input_encoding = input_encoding
        self.output_encoding = output_encoding
        if (self.input_encoding is not None and not
            self.input_encoding.name == 'utf-8' and
            self.input_encoding.name.startswith('utf')):
            raise ValueError("unsupported input encoding: %r" %
                             self.input_encoding.name)
        self.user = user and self.decode(user)
        self.factory = factory
        self.indent = indent
        self.strict = strict
        self.strip = strip
        self.syntax = syntax
        self.wrap = int(wrap)
        self.quotegen = None
        self.tabsize = min(20, max(0, int(tabsize)))
        self.xmax = SpeedPad.SCR_XMIN
        self.ymax = SpeedPad.SCR_YMIN
        self.color = color
        self.menucolor = curses.A_REVERSE
        self.active = False
        self.writable = True
        self.dumbtty = True
        self.cursor = True
        self.queue = collections.deque()
        self.speed = max(0.0, speed)
        self.speedunit = speedunit or cps
        self.player = player or Player(self.user or "user")
        self.robot = robot or self.speed and Robot("robot", self.speed)
        self.stats = []

    def __call__(self, screen):
        """Start instance by calling it with an initialized screen"""
        self.screen = screen
        self.getsize()
        self.initquotes()
        self.initscreen()
        self.initsignals()
        self.initplayers()
        self.initcolors()
        self.sync(force=True)
        self.loop()

    def initscreen(self):
        """Initialize screen, boxes, windows and pads"""
        curses.raw()
        self.speedbox = SpeedBox(2, 0,
                                 4 if self.robot else 3, self.xmax,
                                 output_encoding=self.output_encoding)
        self.quotebox = QuoteBox(6 if self.robot else 5, 0,
                                 SpeedPad.SCR_YMIN - 7, SpeedPad.SCR_XMIN,
                                 SpeedPad.PAD_YMAX, SpeedPad.PAD_XMAX,
                                 2, 10,
                                 output_encoding=self.output_encoding)
        self.inputbox = InputBox(SpeedPad.SCR_YMIN - 6, 0,
                                 SpeedPad.SCR_YMIN - 1, SpeedPad.SCR_XMIN,
                                 SpeedPad.PAD_YMAX, SpeedPad.PAD_XMAX,
                                 0, 10,
                                 tabsize=self.tabsize,
                                 input_encoding=self.input_encoding,
                                 output_encoding=self.output_encoding)

    def initsignals(self):
        """Initialize and register signal handlers"""
        if hasattr(curses, 'resize_term') and hasattr(signal, 'SIGWINCH'):
            signal.signal(signal.SIGWINCH, self._sigwinchhandler)
            self.dumbtty = False

    def initquotes(self):
        """Initialize the quote data source"""
        self.quotegen = QuoteGenerator(self.factory,
                                       maxlines=SpeedPad.PAD_YMAX - 1,
                                       maxcols=SpeedPad.PAD_XMAX - 1,
                                       width=SpeedPad.SCR_XMIN - 1,
                                       wrap=self.wrap,
                                       tabsize=self.tabsize,
                                       strip=self.strip,
                                       input_encoding=self.input_encoding)

    def initplayers(self):
        """Initialize and register players"""
        if self.robot:
            self.speedbox.register(self.robot)
        if self.player:
            self.speedbox.register(self.player)

    def initcolors(self):
        """Initialize and assign colors"""
        if not curses.has_colors():
            self.color = False
            return
        curses.use_default_colors()
        if not self.color:
            return
        curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
        curses.init_pair(2, curses.COLOR_WHITE, -1)
        self.menucolor = curses.color_pair(1)
        self.player.color = curses.color_pair(2)
        if self.robot:
            curses.init_pair(3, curses.COLOR_RED, -1)
            self.robot.color = curses.color_pair(3)

    def getsize(self):
        ymax, xmax = self.screen.getmaxyx()
        if ymax < SpeedPad.SCR_YMIN or xmax < SpeedPad.SCR_XMIN:
            raise RuntimeError("screen too small, need %dx%d" % (
                    SpeedPad.SCR_XMIN, SpeedPad.SCR_YMIN))
        return ymax, xmax

    def _sigwinchhandler(self, signum, frame):
        size = fcntl.ioctl(self.ttyfd, tty.TIOCGWINSZ, '12345678')
        size = struct.unpack('4H', size)
        curses.resize_term(size[0], size[1])
        self.sync()

    def sync(self, force=False):
        """Sync size with physical screen and resize accordingly"""
        oldymax = self.ymax
        oldxmax = self.xmax
        self.ymax, self.xmax = self.getsize()
        ydiff = self.ymax - oldymax
        xdiff = self.xmax - oldxmax
        if force or ydiff or xdiff:
            self.resize(ydiff, xdiff)

    def resize(self, ydiff, xdiff):
        """Resize and redraw screen components"""
        self.screen.clear()
        self.screen.noutrefresh()
        self.speedbox.resize(0, xdiff)
        self.quotegen.resize(ydiff, xdiff)
        self.quotebox.resize(ydiff, xdiff)
        self.inputbox.resize(0, xdiff)
        self.inputbox.move(ydiff, 0)
        if self.writable:
            ypos, xpos = self.inputbox.pad.getyx()
            if ydiff < 0:
                self.quotebox.ypos = max(0, ypos - self.quotebox.ymax)
            if xdiff < 0:
                self.quotebox.xpos = max(0, xpos - self.quotebox.xmax)
                self.inputbox.xpos = max(0, xpos - self.inputbox.xmax)
        self.draw_header()
        self.draw_quote_sep()
        self.draw_input_sep()
        self.draw_footer()
        self.speedbox.erase()
        self.quotebox.erase()
        self.inputbox.erase()
        self.update_screen()

    def update_screen(self, quote=None):
        """Commit pending changes to physical screen"""
        if self.dumbtty:
            self.sync()
        if quote:
            seconds = int(quote.stats.timer.elapsed)
            elapsed = str(datetime.timedelta(seconds=seconds))
            self.draw_header(status=elapsed)
        self.draw_input_sep()
        self.draw_footer()
        self.speedbox.draw(self.speedunit)
        self.screen.noutrefresh()
        self.speedbox.noutrefresh()
        self.quotebox.noutrefresh()
        self.inputbox.noutrefresh()
        curses.doupdate()

    def update_player_speed(self, quote):
        self.player.speed = quote.stats.speed

    def update_robot_pos(self, quote):
        if not self.robot or self.robot.pos == quote.strlen: return
        pos = int(quote.stats.timer.elapsed * self.robot.speed)
        pos = min(quote.strlen, pos)
        self.robot.pos = pos
        self.speedbox.update(self.robot)

    def draw_header(self, status=None):
        attr = self.menucolor
        ypos = 0
        self.screen.hline(ypos, 0, curses.ascii.SP, self.xmax, attr)
        self.screen.addstr(ypos, 0, "speedpad %s" % __version__, attr)
        if status:
            status = status[:self.xmax - 30]
            self.screen.addstr(ypos, self.xmax - len(status), status, attr)

    def draw_quote_sep(self):
        attr = self.menucolor
        ypos = 5 if self.robot else 4
        self.screen.hline(ypos, 0, curses.ascii.SP, self.xmax, attr)

    def draw_input_sep(self):
        attr = self.menucolor
        ypos = self.ymax - 7
        self.screen.hline(ypos, 0, curses.ascii.SP, self.xmax, attr)
        if self.active:
            self.screen.addstr(ypos, self.xmax - 11, "CTRL-D", attr)
            self.screen.addstr(ypos, self.xmax - 4, "STOP",
                               attr | curses.A_BOLD)
        else:
            self.screen.addstr(ypos, self.xmax - 10, "ENTER", attr)
            self.screen.addstr(ypos, self.xmax - 4, "NEXT",
                               attr | curses.A_BOLD)

    def draw_footer(self):
        attr = self.menucolor
        ypos = self.ymax - 1
        self.screen.hline(ypos, 0, curses.ascii.SP, self.xmax, attr)
        self.screen.addstr(ypos, 0, "CTRL-X", attr)
        self.screen.addstr(ypos, 0 + 7, "RESET", attr | curses.A_BOLD)
        self.screen.addstr(ypos, self.xmax - 11, "CTRL-Q", attr)
        self.screen.insstr(ypos, self.xmax - 4, "QUIT",
                           attr | curses.A_BOLD)

    def start_pager(self, quote):
        self.hide_cursor()
        self.quotebox.reset()
        self.inputbox.reset()
        self.update_player_speed(quote)
        self.update_robot_pos(quote)
        self.quotebox.draw_stats(quote)
        self.inputbox.draw_stats(quote, self.player)
        self.quotebox.noutrefresh()
        self.inputbox.noutrefresh()
        curses.doupdate()

    def stop_pager(self):
        self.quotebox.reset()
        self.inputbox.reset()
        self.quotebox.noutrefresh()
        self.inputbox.noutrefresh()
        self.screen.noutrefresh()
        curses.doupdate()

    def store_stats(self, quote):
        self.stats.append({
            'started': quote.stats.timer.started,
            'stopped': quote.stats.timer.stopped,
            'elapsed': quote.stats.timer.elapsed,
            'len': quote.strlen,
            'pos': self.player.pos,
            'lines': quote.ymax,
            'enter': quote.stats.keystrokes_enter,
            'tab': quote.stats.keystrokes_tab,
            'space': quote.stats.keystrokes_space,
            'good': quote.stats.keystrokes_good,
            'typo': quote.stats.keystrokes_typo,
            'total': quote.stats.keystrokes_total,
            'cps': cps(self.player.speed),
            'cpm': cpm(self.player.speed),
            'wpm': wpm(self.player.speed),
            'ppm': ppm(self.player.speed),
            'cph': cph(self.player.speed),
        })

    def show_cursor(self):
        if self.cursor is None or self.cursor: return
        try:
            curses.curs_set(1)
            self.cursor = True
        except curses.error:
            self.cursor = None

    def hide_cursor(self):
        if self.cursor is None or not self.cursor: return
        try:
            curses.curs_set(0)
            self.cursor = False
        except curses.error:
            self.cursor = None

    def loop(self):
        """Process quotes and handle user input"""
        update_interval_screen = 0.1    # seconds
        update_interval_speed = 0.5     # seconds
        update_interval_progress = 0.1  # seconds
        sleep = 0.01                    # seconds
        scan = int(sleep * 1000)        # milliseconds
        update_iterations_screen = int(update_interval_screen / sleep)
        update_iterations_speed = int(update_interval_speed / sleep)
        update_iterations_progress = int(update_interval_progress / sleep)
        self.hide_cursor()
        self.update_screen()
        for quote in self.quotegen:     # next quote
            while True:                 # next round
                curses.flushinp()
                self.queue.clear()
                quote.stats.reset()
                self.speedbox.reset()
                self.quotebox.reset()
                self.inputbox.reset()
                self.speedbox.load(quote)
                self.quotebox.load(quote)
                self.show_cursor()
                self.update_screen(quote)
                self.active = False
                self.writable = True
                update_iteration_screen = 0
                update_iteration_speed = 0
                update_iteration_progress = 0
                update_screen = False
                update_speed = False
                update_progress = False
                restart = False
                while True:             # next keypress
                    if update_iteration_screen == update_iterations_screen:
                        update_iteration_screen = 0
                        update_screen = True
                    if update_iteration_speed == update_iterations_speed:
                        update_iteration_speed = 0
                        update_speed = True
                    if update_iteration_progress == update_iterations_progress:
                        update_iteration_progress = 0
                        update_progress = True

                    try:
                        ch = self.queue.popleft()
                        chars = [ch]
                        keyboard = False
                    except IndexError:
                        ch, chars = self.inputbox.getch()
                        keyboard = True

                    if ch < 0:
                        if update_speed:
                            update_speed = False
                            self.update_player_speed(quote)
                        if update_progress:
                            update_progress = False
                            self.update_robot_pos(quote)
                        if update_screen:
                            update_screen = False
                            self.update_screen(quote)
                        update_iteration_screen += 1
                        update_iteration_speed += 1
                        update_iteration_progress += 1
                        curses.napms(scan)
                        continue

                    try:
                        self.process(quote, ch, chars, keyboard=keyboard)
                    except QuoteStopSignal:
                        self.active = False
                        self.queue.clear()
                        if self.writable:
                            self.writable = False
                            self.start_pager(quote)
                            self.store_stats(quote)
                        else:
                            self.stop_pager()
                            break
                    except QuoteResetSignal:
                        self.active = False
                        self.queue.clear()
                        if not self.writable:
                            self.stop_pager()
                        restart = True
                        break
                    except QuoteBreakSignal:
                        self.active = False
                        break

                    if update_speed:
                        update_speed = False
                        self.update_player_speed(quote)
                    if update_progress:
                        update_progress = False
                        self.update_robot_pos(quote)
                    self.update_screen(quote)
                    update_screen = False
                    update_iteration_screen = 0
                    update_iteration_speed += 1
                    update_iteration_progress += 1
                    # end keypress

                if not restart: break
                # end round

            self.hide_cursor()
            self.update_screen()
            # end quote

    def process(self, quote, ch, chars, keyboard=True):
        """Process one multi-byte character"""
        ypos, xpos = self.inputbox.pad.getyx()
        # INPUT
        if self.writable and (len(chars) > 1 or curses.ascii.isprint(ch)):
            if quote.iscomplete(ypos, xpos):
                pass # complete with pending typos
            elif quote.iseol(ypos, xpos):
                if not self.strict and ch == curses.ascii.SP:
                    self.queue.append(curses.ascii.NL)
            else:
                if not self.active:
                    self.active = True
                    quote.stats.timer.start()
                if keyboard and ch == curses.ascii.SP:
                    quote.stats.keystrokes_space += 1
                if quote.istypo(ypos, xpos,
                                self.inputbox.decode_chars(chars),
                                record=True, count=1 if keyboard else 0):
                    self.quotebox.pad.chgat(ypos, xpos, 1, curses.A_REVERSE)
                else:
                    self.quotebox.pad.chgat(ypos, xpos, 1, curses.A_BOLD)
                if (quote.iscomplete(ypos, xpos + 1) and
                    quote.iscorrect()):
                    self.queue.append(curses.ascii.EOT)
                for ch in chars:
                    self.inputbox.putch(ch)
        elif (self.writable and
              ch in (curses.ascii.BS,
                     curses.ascii.DEL,
                     curses.KEY_BACKSPACE)):
            quote.stats.fixtypo(ypos, xpos, count=0)
            self.inputbox.putch(ch)
            newy, newx = self.inputbox.pad.getyx()
            if newy != ypos:
                self.quotebox.pad.chgat(newy, newx, -1, curses.A_NORMAL)
            elif newx != xpos:
                self.quotebox.pad.chgat(ypos, newx, -1, curses.A_NORMAL)
        elif ch == curses.ascii.TAB and self.writable:
            if keyboard:
                quote.stats.keystrokes_tab += 1
            if self.tabsize:
                indent = self.tabsize - xpos % self.tabsize
                remaining = max(0, quote.eol(ypos) - xpos)
                if remaining > 0:
                    spaces = min(indent, remaining)
                    spacegen = itertools.repeat(curses.ascii.SP, spaces)
                    self.queue.extend(spacegen)
                if not self.strict and indent > remaining:
                    self.queue.append(curses.ascii.NL)
        elif ch == curses.ascii.NL:
            if not self.active:
                raise QuoteBreakSignal
            if (self.writable and
                quote.iseol(ypos, xpos) and not
                quote.iscomplete(ypos, xpos)):
                if keyboard:
                    quote.stats.keystrokes_enter += 1
                self.inputbox.putch(ch)
                newy, newx = self.inputbox.pad.getyx()
                oldsol = self.inputbox.sol(ypos)
                oldeol = quote.eol(ypos)
                neweol = quote.eol(newy)
                if neweol > 0 and neweol > oldsol and newy > 0 and newx == 0:
                    if self.indent and oldsol > 0:
                        spacegen = itertools.repeat(curses.ascii.SP, oldsol)
                        self.queue.extend(spacegen)
                    if self.syntax and oldeol > oldsol:
                        continuation = self.inputbox.continue_comment(
                                ypos, oldsol, oldeol, neweol,
                                indent=self.indent)
                        self.queue.extend(continuation)
        # CONTROLS
        elif ch == curses.ascii.FF:         # ^L
            pass
        elif ch == curses.ascii.ETX:        # ^C
            raise KeyboardInterrupt
        elif ch == curses.ascii.DC1:        # ^Q
            raise StopSignal
        elif ch == curses.ascii.EOT:        # ^D
            if self.active:
                quote.stats.timer.stop()
                raise QuoteStopSignal
            raise QuoteBreakSignal
        elif ch == curses.ascii.CAN:        # ^X
            raise QuoteResetSignal
        # SCROLLING
        elif ch == curses.KEY_HOME or ch == curses.ascii.SOH:
            self.quotebox.ypos = 0
        elif ch == curses.KEY_END or ch == curses.ascii.ENQ:
            self.quotebox.ypos = max(0, quote.ymax - self.quotebox.boxlines)
        elif ch == curses.KEY_PPAGE:
            self.quotebox.scroll(-(self.quotebox.ymax // 2), 0)
        elif ch == curses.KEY_NPAGE:
            self.quotebox.scroll(self.quotebox.ymax // 2, 0)
        elif ch == curses.KEY_UP:
            self.quotebox.scroll(-1, 0)
        elif ch == curses.KEY_DOWN:
            self.quotebox.scroll(1, 0)
        elif ch == curses.KEY_LEFT:
            self.quotebox.scroll(0, -1)
        elif ch == curses.KEY_RIGHT:
            self.quotebox.scroll(0, 1)

        if not self.writable: return

        # AUTO SCROLLING
        newy, newx = self.inputbox.pad.getyx()
        ydiff = newy - ypos
        xdiff = newx - xpos
        if ydiff:
            if self.quotebox.ypos or newy >= self.quotebox.ymax:
                self.quotebox.scroll(ydiff, 0)
            if self.inputbox.ypos or newy >= self.inputbox.ymax:
                self.inputbox.scroll(ydiff, 0)
        if xdiff:
            if quote.xmax >= self.inputbox.boxcols:
                if ydiff < 0 or newx >= self.inputbox.boxcols:
                    self.quotebox.xpos = max(0, newx - self.quotebox.xmax)
                    self.inputbox.xpos = max(0, newx - self.inputbox.xmax)
                elif self.inputbox.xpos or newx >= self.inputbox.xmax:
                    self.quotebox.scroll(0, xdiff)
                    self.inputbox.scroll(0, xdiff)
            else:
                self.quotebox.xpos = 0
                self.inputbox.xpos = 0

        # PLAYER PROGRESS
        if ydiff or xdiff:
            self.player.pos = quote.strpos(newy, newx)
            self.speedbox.update(self.player)


class SpeedUnit(object):

    def __init__(self, attr):
        self.attr = attr
        self.attrgetter = operator.attrgetter(attr)

    def __str__(self):
        return self.attr.upper()

    def __call__(self, speed):
        return self.attrgetter(speed)


class Speed(float):

    @property
    def cps(self):
        """Speed in chars per second (internal base unit)"""
        return self

    @property
    def cpm(self):
        """Speed in chars per minute"""
        return self.cps * 60.0

    @property
    def wpm(self):
        """Speed in words per minute"""
        return self.cps * 60.0 / 5.0

    @property
    def ppm(self):
        """Speed in pages per minute"""
        return self.cps * 60.0 / 250.0

    @property
    def cph(self):
        """Speed in chars per hour (also known as KPH)"""
        return self.cps * 3600.0


class StopSignal(Exception): pass
class QuoteStopSignal(Exception): pass
class QuoteBreakSignal(Exception): pass
class QuoteResetSignal(Exception): pass
class QuoteGeneratorError(Exception): pass
class QuoteCommandLineError(Exception): pass
class QuoteFileError(Exception): pass
class QuotePipeError(Exception): pass


class QuoteCommandError(Exception):

    def __init__(self, cmd, exitcode, stderr=None):
        self.cmd = cmd
        self.exitcode = exitcode
        self.stderr = stderr

    def __str__(self):
        err = ("%r returned non-zero exit code %d" %
               (self.cmd, self.exitcode))
        if self.stderr:
            err = "%s\n\n%s" % (err, self.stderr)
        return err


cps = SpeedUnit('cps')
cpm = SpeedUnit('cpm')
wpm = SpeedUnit('wpm')
ppm = SpeedUnit('ppm')
cph = SpeedUnit('cph')


def main(*args, **kwargs):
    import sys
    if sys.hexversion < 0x020700F0:
        sys.stderr.write("Require Python 2.7 or later!\n")
        sys.exit(1)
    if sys.hexversion >= 0x03000000:
        sys.stderr.write("Require Python 2!\n")
        sys.exit(1)
    import argparse
    import getpass
    import locale
    import os

    infd = sys.stdin.fileno()
    outfd = sys.stdout.fileno()
    errfd = sys.stderr.fileno()
    locale.setlocale(locale.LC_ALL, '')
    io_encoding = locale.getpreferredencoding()
    fs_encoding = sys.getfilesystemencoding()

    try:
        # LOGNAME, USER, LNAME, USERNAME, pw_name
        user = getpass.getuser().lstrip().split(None, 1)[0].lower()
    except:
        user = None

    parser = argparse.ArgumentParser(
            formatter_class=argparse.RawTextHelpFormatter,
            usage="%(prog)s [options] [--] [FILE [FILE ...]]\n "
            "      %(prog)s [...] -c [--] [CMD [ARG ...]]",
            description="%(prog)s is a tool to test, train,"
                        " and increase typing speed",
            epilog=textwrap.dedent(
            """
            examples:
              %(prog)s file1 file2 file3                        read files
              grep ^foo words | %(prog)s                        read stdin
              %(prog)s -c -- fortune -s -n 500                  (default)
              %(prog)s -c -- fortune 40%% startrek 60%% linux
              %(prog)s /usr/src/linux/README
              %(prog)s --code /usr/src/linux/mm/pagewalk.c

            see %(prog)s(1) for more options and gnuplot(1) examples
            """))
    parser.add_argument('--version', action='version',
                        version='%%(prog)s %s' % __version__)
    parser.add_argument('-c', dest='cmd', action='store_true',
                       help=("use positional arguments as command line"
                             " (default: %(default)s)"))
    parser.add_argument('-o', dest='outfile', metavar='FILE',
                        help=("append stats dump to file"
                              " (default: <stdout>)"))
    group = parser.add_mutually_exclusive_group()
    group.add_argument('--wpm', action='store_true', default=True,
                       help=("speed in words per minute"
                             " (default: %(default)s)"))
    group.add_argument('--cpm', action='store_true',
                       help=("speed in chars per minute"
                             " (default: %(default)s)"))
    group.add_argument('--cps', action='store_true',
                       help=("speed in chars per second"
                             " (default: %(default)s)"))
    parser.add_argument('--speed', type=float,
                        help=("reference speed in matching unit"
                              " (default: 100.0)"))
    parser.add_argument('--wrap', type=int, default=0, metavar='WIDTH',
                        help=("wrap text at specified width"
                              " (default: %(default)d)\n"
                              "[<0 = disable, 0 = auto, >0 = fixed]"))
    parser.add_argument('--user', default=user, metavar='NAME',
                        help=("set custom user name"
                              " (default: %(default)s)"))
    parser.add_argument('--tabsize', type=int, default=8, metavar='N',
                        help=("set custom tabsize"
                              " (default: %(default)d)"))
    parser.add_argument('--strict', action='store_true',
                       help=("require manual line breaks"
                             " (default: %(default)s)"))
    parser.add_argument('--indent', action='store_true',
                       help=("enable auto indentation"
                             " (default: %(default)s)"))
    parser.add_argument('--syntax', action='store_true',
                       help=("enable syntax support"
                             " (default: %(default)s)"))
    parser.add_argument('--no-strip', action='store_true',
                       help=("keep excessive whitespace in text"
                             " (default: %(default)s)"))
    parser.add_argument('--no-robot', action='store_true',
                       help=("disable the reference speed robot"
                             " (default: %(default)s)"))
    parser.add_argument('--no-color', action='store_true',
                       help=("disable colors"
                             " (default: %(default)s)"))
    parser.add_argument('--no-stats', action='store_true',
                       help=("disable stats dump on exit"
                             " (default: %(default)s)"))
    parser.add_argument('--code', action='store_true',
                       help="[--no-strip --indent --syntax]")
    parser.add_argument('--input-encoding', default=io_encoding,
                        help=argparse.SUPPRESS)
    parser.add_argument('--output-encoding', default=io_encoding,
                        help=argparse.SUPPRESS)
    parser.add_argument('--filesystem-encoding', default=fs_encoding,
                        help=argparse.SUPPRESS)
    parser.add_argument('argv', nargs='*', help=argparse.SUPPRESS)

    args = parser.parse_args(*args, **kwargs)

    try:
        args.input_encoding = codecs.lookup(args.input_encoding)
        args.output_encoding = codecs.lookup(args.output_encoding)
        args.filesystem_encoding = codecs.lookup(args.filesystem_encoding)
    except LookupError as e:
        parser.error(e)

    def recode_arg_for_fs(arg):
        if args.input_encoding != args.filesystem_encoding:
            arg = args.input_encoding.decode(arg)[0]
            arg = args.filesystem_encoding.encode(arg)[0]
        return arg

    if args.code:
        args.indent = True
        args.syntax = True
        args.no_strip = True

    speedbase = Speed(1.0)
    if args.cps:
        speedunit = cps
        speed = (args.speed or 8.0) / speedunit(speedbase)
    elif args.cpm:
        speedunit = cpm
        speed = (args.speed or 500.0) / speedunit(speedbase)
    else:
        speedunit = wpm
        speed = (args.speed or 100.0) / speedunit(speedbase)
    if args.no_robot:
        speed = 0.0

    if args.cmd and not args.argv:
        parser.error("missing command line")
    if not args.argv:
        args.cmd = True
        args.argv = ['fortune', '-s', '-n', '500']

    if os.isatty(infd):
        ttyfd = infd
    elif os.isatty(outfd):
        ttyfd = outfd
    elif os.isatty(errfd):
        ttyfd = errfd
    else:
        sys.stderr.write("Unable to find tty-like device!\n")
        sys.exit(1)

    if ttyfd != infd:
        # reconnect stdin to tty
        pipefd = 3
        os.dup2(infd, pipefd)
        os.dup2(ttyfd, infd)
        def factory(maxsize):
            try:
                with os.fdopen(pipefd, 'rb') as pipe:
                    while True:
                        data = pipe.read(maxsize)
                        if not data: break
                        yield data
            except EnvironmentError as e:
                raise QuotePipeError(e.strerror)
    elif args.cmd:
        def factory(maxsize):
            import subprocess
            argv = [recode_arg_for_fs(args.argv[0])] + args.argv[1:]
            while True:
                try:
                    process = subprocess.Popen(argv,
                                               stdout=subprocess.PIPE,
                                               stderr=subprocess.PIPE,
                                               close_fds=True)
                    try:
                        stdout, stderr = process.communicate()
                    except:
                        process.kill()
                        process.wait()
                        raise
                    retcode = process.poll()
                    if retcode:
                        raise QuoteCommandError(argv, retcode, stderr)
                    yield stdout
                except EnvironmentError as e:
                    raise QuoteCommandLineError("%s: %r" %
                                                (e.strerror, argv[0]))
    else:
        def factory(maxsize):
            for fn in args.argv:
                fn = recode_arg_for_fs(fn)
                try:
                    with open(fn, 'rb') as fh:
                        yield fh.read(maxsize)
                except EnvironmentError as e:
                    raise QuoteFileError("%s: %r" %
                                         (e.strerror, e.filename))

    try:
        instance = SpeedPad(factory,
                            ttyfd=ttyfd,
                            wrap=args.wrap,
                            strict=args.strict,
                            strip=not args.no_strip,
                            indent=args.indent,
                            syntax=args.syntax,
                            tabsize=args.tabsize,
                            color=not args.no_color,
                            user=args.user,
                            speed=speed,
                            speedunit=speedunit,
                            input_encoding=args.input_encoding,
                            output_encoding=args.output_encoding)
    except ValueError as e:
        sys.stderr.write('Error: %s\n' % e)
        sys.exit(1)

    if os.isatty(outfd):
        dumpfd = outfd
    else:
        # reconnect stdout to tty
        dumpfd = 4
        os.dup2(outfd, dumpfd)
        os.dup2(ttyfd, outfd)
    if args.outfile:
        if dumpfd != outfd:
            os.close(dumpfd)
        try:
            dumpfd = os.open(args.outfile,
                             os.O_CREAT | os.O_WRONLY | os.O_APPEND, 0600)
        except EnvironmentError as e:
            sys.stderr.write("Error: %s: %r\n" % (e.strerror, e.filename))
            sys.exit(1)

    if dumpfd == outfd:
        def dump():
            stats = format_stats(instance.stats)
            if not stats: return
            sys.stdout.write(stats)
            sys.stdout.write('\n')
    else:
        def dump():
            stats = format_stats(instance.stats)
            if not stats: return
            with os.fdopen(dumpfd, 'ab') as fh:
                fh.write(stats)
                fh.write('\n')

    exitcode = 0
    try:
        curses.wrapper(instance)
    except KeyboardInterrupt:
        sys.exit(0)
    except StopSignal:
        pass
    except (QuoteGeneratorError,
            QuotePipeError,
            QuoteFileError,
            QuoteCommandError,
            QuoteCommandLineError) as e:
        sys.stderr.write('Error: %s\n' % e)
        exitcode = 2
    try:
        dump()
    except EnvironmentError as e:
        sys.stderr.write('Error: %s\n' % e.strerror)
        exitcode = 3

    sys.exit(exitcode)

def format_stats(stats):
    """Format stats into lines of machine-readable space separated fields"""
    lines = ["# started stopped elapsed len pos lines"
             " enter tab space good typo total"
             " cps cpm wpm ppm cph"]
    for stat in stats:
        line = (
            "%(started).3f"
            " %(stopped).3f"
            " %(elapsed).3f"
            " %(len)d"
            " %(pos)d"
            " %(lines)d"
            " %(enter)d"
            " %(tab)d"
            " %(space)d"
            " %(good)d"
            " %(typo)d"
            " %(total)d"
            " %(cps).3f"
            " %(cpm).3f"
            " %(wpm).3f"
            " %(ppm).3f"
            " %(cph).3f"
        ) % stat
        lines.append(line)
    return '' if len(lines) < 2 else '\n'.join(lines)

if __name__ == '__main__':
    main()

# vim: et sw=4 sts=4 ts=4 tw=78 fen fdm=indent fdn=2 fdl=0
