#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <assert.h>
#include <math.h>
#include <limits.h>
#include <ctype.h>
#include <locale.h>
#include <wchar.h>

#include "putty.h"
#include "storage.h"
#include "terminal.h"
#include "misc.h"
#include "ipbt.h"

#define _XOPEN_SOURCE_EXTENDED

#if CURSES_HAVE_NCURSES_CURSES_H
#  include <ncursesw/curses.h>
#elif CURSES_HAVE_NCURSES_CURSES_H
#  include <ncurses/curses.h>
#elif CURSES_HAVE_NCURSES_H
#  include <ncurses.h>
#elif CURSES_HAVE_CURSES_H
#  include <curses.h>
#else
#  error "SysV or X/Open-compatible Curses header file required"
#endif

#include <sys/time.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>

#include "version.h"

const char usagemsg[] =
    "usage: ipbt [ [ -w width ] [ -h height ] | -u ] [ -T | -N ]\n"
    "            [ -f frame ] [ -q frame ] [ -p frame ]\n"
    "            [ -P ] [ -A ] file [file...]\n"
    "where: -w width    specify width of emulated terminal screen (default 80)\n"
    "       -h height   specify height of emulated terminal screen (default 24)\n"
    "       -u          use size of real terminal for emulated terminal\n"
    "       -U          assume input terminal data to be encoded in UTF-8\n"
    "       --utf8-linedraw    permit ISO 2022 line drawing even in UTF-8 mode\n"
    "       -T          assume input files to be ttyrec format\n"
    "       -N          assume input files to be nh-recorder format\n"
    "       -f frame    start viewing at a particular frame number\n"
    "       -q frame    quit when passing a particular frame number\n"
    "       -p frame    pause when reaching a particular frame number\n"
    "       -P          terminate immediately after reading input\n"
    "       -A          automatically start playing after reading input\n"
    "       -L          loop back to the start when playback reaches the end\n"
    " also: ipbt --version     report version number\n"
    "       ipbt --help        display this help text\n"
    "       ipbt --licence     display the (MIT) licence text\n"
    ;

bool curses_active;

void pre_exit(void)
{
    if (curses_active)
        endwin();
}

#define FG     0x000F0000
#define BG     0x00F00000
#define FGBG   0x00FF0000
#define BOLD   0x01000000
#define UNDER  0x02000000
#define REV    0x04000000
#define BLINK  0x08000000

#define FGSHIFT 16
#define BGSHIFT 20
#define FGBGSHIFT FGSHIFT

#define INVALID_SCREENVAL INT_MAX /* an invalid value for any screen[] entry */

struct filename {
    char *name;
    int type;
};

struct parray;

struct inst {
    Terminal *term;
    struct unicode_data ucsdata;
    Conf *conf;
    int *screen, *oldscreen, w, h, screenlen;
    struct parray **parrays;
    int frames;
    unsigned long long movietime;

    struct filename *filenames;
    int nfiles, filesize;
    int curr_file;

    int cpairs[(FGBG >> FGSHIFT) + 1];
    int pairsused, nines;
    int number;
    bool osd;
    int playing;
    bool logmod;
    double speedmod;
    char *searchstr;
    bool searchback;
    bool unicode;

    TermWin tw;
};

/*
 * Our notional screen data structure is simply an array of 32-bit
 * integers: W*H screen positions, plus a magic one for the cursor
 * position.
 * 
 * This rather simplistic and flat architecture is because a lot of
 * the time we won't be directly storing these. Instead, we'll be
 * storing a list of how each integer changed over time. Our
 * _movie_ data structure will be a collection of pseudo-`arrays',
 * one for each of the integers in our screen array, containing
 * elements of the form (frame number in which the integer changed,
 * what it changed to). This means that we can determine the value
 * of integer I at frame F by a binary search of the array. This
 * enables us to play back and forth through the entire movie with
 * arbitrary rewind. Hence the need to have the entire terminal
 * state encoded as an unstructured list of integers: if I had to
 * give separate treatment to the cursor position and any other
 * future enhancements such as line attributes, it would all get
 * more complicated.
 * 
 * To prevent memory wastage by repeatedly reallocing several
 * actual arrays, we instead use the concept of a `pseudo-array',
 * which is structured much like an ext2fs file: initially the
 * array is a single block of memory in the obvious format, but
 * once it overflows that block we move to a two-layer structure
 * containing an index block with (frame, sub-block) records each
 * indexing a block of real array. When the index block overflows,
 * we move to a three-layer structure with a second-level index
 * block indexing the first-level ones, and so on.
 */

struct parray_block;

struct parray_level0 {
    int frame;
    int data;
};

struct parray_level1 {
    int frame;
    struct parray_block *subblock;
};

#ifdef PARRAY_TEST
#define PARRAY_L0COUNT 5
#define PARRAY_L1COUNT 3
#else
#define PARRAY_BLKSIZE 16384
#define PARRAY_L0COUNT (PARRAY_BLKSIZE / sizeof(struct parray_level0))
#define PARRAY_L1COUNT (PARRAY_BLKSIZE / sizeof(struct parray_level1))
#endif

struct parray {
    int toplevel;
    int items;
    int memusage;
    struct parray_block *root;
};

struct parray_block {
    union {
        struct parray_level0 level0[PARRAY_L0COUNT];
        struct parray_level1 level1[PARRAY_L1COUNT];
    } u;
};

static struct parray *parray_new(void)
{
    struct parray *pa = snew(struct parray);

    pa->toplevel = -1;
    pa->items = 0;
    pa->memusage = sizeof(struct parray);
    pa->root = NULL;

    return pa;
}

static void parray_append(struct parray *pa, int frame, int data)
{
    struct parray_block *pb, *pb2;
    int i, n, index, count;

    /*
     * Special case: the very first item.
     */
    if (!pa->items) {
        pb = snew(struct parray_block);
        pa->memusage += sizeof(struct parray_block);

        for (i = 0; i < PARRAY_L0COUNT; i++) {
            pb->u.level0[i].frame = INT_MAX;
            pb->u.level0[i].data = 0;
        }
        pb->u.level0[0].frame = frame;
        pb->u.level0[0].data = data;

        pa->items++;
        pa->toplevel = 0;
        pa->root = pb;

        return;
    }

    /*
     * Figure out how many items are covered by a single block at
     * the parray's current top level.
     */
    count = PARRAY_L0COUNT;
    for (i = 1; i <= pa->toplevel; i++)
        count *= PARRAY_L1COUNT;

    /*
     * If this is equal to the parray's current total item count,
     * we must create a new top-level block.
     */
    assert(pa->items <= count);
    if (pa->items == count) {
        pb = snew(struct parray_block);
        pa->memusage += sizeof(struct parray_block);

        /*
         * pa->root->u.level0[0].frame and
         * pa->root->u.level1[0].frame overlap exactly (guaranteed
         * by the C standard), so we don't need to worry about
         * which one to access through.
         */
        pb->u.level1[0].frame = pa->root->u.level1[0].frame;
        pb->u.level1[0].subblock = pa->root;

        pa->toplevel++;
        pa->root = pb;

        count *= PARRAY_L1COUNT;       /* we've moved up a level */
    }

    /*
     * Now work down the tree. At each level, create a new block
     * and descend to it if necessary, otherwise descend to the
     * last existing block if it's not completely full.
     */
    pb = pa->root;
    index = pa->items;
    for (i = pa->toplevel; i-- > 0 ;) {
        count /= PARRAY_L1COUNT;

        n = index / count;
        assert(n < PARRAY_L1COUNT);
        index %= count;

        if (!index) {
            /*
             * Create a new empty block at the next level down.
             */
            pb2 = snew(struct parray_block);
            pb->u.level1[n].frame = frame;
            pb->u.level1[n].subblock = pb2;
        }

        /*
         * Descend to the partially filled end block, whether or
         * not we just had to create it.
         */
        pb = pb->u.level1[n].subblock;
    }

    /*
     * Now we're sitting on a level-0 block which is known to have
     * spare space. Add our entry.
     */
    pb->u.level0[index].frame = frame;
    pb->u.level0[index].data = data;

    pa->items++;
}

static int parray_search(struct parray *pa, int frame, int *index_out)
{
    struct parray_block *pb;
    int count, total, i, n, top, bot, mid, index;

    assert(pa->root);
    assert(pa->items > 0);
    assert(frame >= pa->root->u.level1[0].frame);

    /*
     * Figure out how many items are covered by a single block at
     * the parray's current top level. This will tell us how many
     * blocks to check at each level of the parray.
     */
    count = PARRAY_L0COUNT;
    for (i = 1; i <= pa->toplevel; i++)
        count *= PARRAY_L1COUNT;

    index = 0;

    /*
     * Binary search each block on the way down.
     */
    pb = pa->root;
    total = pa->items;
    for (i = pa->toplevel; i-- > 0 ;) {
        count /= PARRAY_L1COUNT;

        n = (total + count - 1) / count;

        bot = 0;
        top = n;
        while (top - bot > 1) {
            mid = (top + bot) / 2;
            if (pb->u.level1[mid].frame > frame)
                top = mid;
            else
                bot = mid;
        }

        total -= bot * count;
        index += bot * count;
        if (total > count)
            total = count;

        pb = pb->u.level1[bot].subblock;
    }

    /*
     * And binary-search the bottom block.
     */
    bot = 0;
    top = total;
    while (top - bot > 1) {
        mid = (top + bot) / 2;
        if (pb->u.level0[mid].frame > frame)
            top = mid;
        else
            bot = mid;
    }

    index += bot;
    if (index_out)
        *index_out = index;

    return pb->u.level0[bot].data;
}

static int parray_retrieve(struct parray *pa, int index, int *frame)
{
    struct parray_block *pb;
    int count, i, n;

    assert(pa->root);
    assert(index >= 0 && index < pa->items);

    /*
     * Figure out how many items are covered by a single block at
     * the parray's current top level.
     */
    count = PARRAY_L0COUNT;
    for (i = 1; i <= pa->toplevel; i++)
        count *= PARRAY_L1COUNT;

    /*
     * Search down the tree.
     */
    pb = pa->root;
    for (i = pa->toplevel; i-- > 0 ;) {
        count /= PARRAY_L1COUNT;
        n = index / count;
        index -= n * count;
        pb = pb->u.level1[n].subblock;
    }

    if (frame)
        *frame = pb->u.level0[index].frame;
    return pb->u.level0[index].data;
}

#define CURSOR (inst->w * inst->h)
#define TIMETOP (inst->w * inst->h + 1)
#define TIMEBOT (inst->w * inst->h + 2)
#define FILENO (inst->w * inst->h + 3)
#define OFFSET (inst->w * inst->h + 4)
#define TOTAL (inst->w * inst->h + 5)

void ipbt_draw_text(TermWin *tw, int x, int y, wchar_t *text, int len,
                    unsigned long attr, int lattr, truecolour tc)
{
    struct inst *inst = container_of(tw, struct inst, tw);
    int i, index;
    unsigned int fg, bg, val;

    for (i = 0; i < len; i++) {
        assert(y >= 0 && y < inst->h);
        assert(x+i >= 0 && x+i < inst->w);
        index = y * inst->w + (x+i);

        if (inst->unicode) {
            val = text[i] & 0xFFFF;
        } else {
            val = text[i] & 0xFF;
            if (text[i] >= 0xD95F && text[i] < 0xD97F)
                val += 0x100;
        }
        if (attr & ATTR_BOLD)
            val |= BOLD;
        if (attr & ATTR_UNDER)
            val |= UNDER;
        if (attr & ATTR_REVERSE)
            val |= REV;
        if (attr & ATTR_BLINK)
            val |= BLINK;
        fg = (attr & ATTR_FGMASK) >> ATTR_FGSHIFT;
        bg = (attr & ATTR_BGMASK) >> ATTR_BGSHIFT;
        if (fg >= 8)
            fg = 9;
        if (bg >= 8)
            bg = 9;
        val |= (fg << FGSHIFT) | (bg << BGSHIFT);
        inst->screen[index] = val;

        /* Track whether we've seen the cursor in this repaint */
        if (attr & (TATTR_ACTCURS | TATTR_PASCURS | TATTR_RIGHTCURS))
            inst->screen[CURSOR] = y * inst->w + x;
    }
}

const struct TermWinVtable ipbt_termwin_vtable = {
    .setup_draw_ctx = ipbt_setup_draw_ctx,
    .draw_text = ipbt_draw_text,
    .draw_cursor = ipbt_draw_cursor,
    .draw_trust_sigil = ipbt_draw_trust_sigil,
    .char_width = ipbt_char_width,
    .free_draw_ctx = ipbt_free_draw_ctx,
    .set_cursor_pos = ipbt_set_cursor_pos,
    .set_raw_mouse_mode = ipbt_set_raw_mouse_mode,
    .set_raw_mouse_mode_pointer = ipbt_set_raw_mouse_mode_pointer,
    .set_scrollbar = ipbt_set_scrollbar,
    .bell = ipbt_bell,
    .clip_write = ipbt_clip_write,
    .clip_request_paste = ipbt_clip_request_paste,
    .refresh = ipbt_refresh,
    .request_resize = ipbt_request_resize,
    .set_title = ipbt_set_title,
    .set_icon_title = ipbt_set_icon_title,
    .set_minimised = ipbt_set_minimised,
    .set_maximised = ipbt_set_maximised,
    .move = ipbt_move,
    .set_zorder = ipbt_set_zorder,
    .palette_set = ipbt_palette_set,
    .palette_get_overrides = ipbt_palette_get_overrides,
};

static void store_frame(void *vinst, unsigned long long delay, long fileoff,
                        strbuf *termdata)
{
    struct inst *inst = (struct inst *)vinst;
    int i, n;

    /*
     * Force the terminal to refresh, so that our data is up to
     * date.
     */
    inst->screen[CURSOR] = -1;
    term_invalidate(inst->term);
    term_update(inst->term);

    /*
     * Now see which terminal integers have changed, and write
     * movie records for the ones that have.
     */
    inst->movietime += delay;
    inst->frames++;

    inst->screen[TIMETOP] = (unsigned long long)inst->movietime >> 32;
    inst->screen[TIMEBOT] = (unsigned long long)inst->movietime & 0xFFFFFFFF;

    inst->screen[FILENO] = inst->curr_file;
    inst->screen[OFFSET] = fileoff;

    n = 0;
    for (i = 0; i < inst->screenlen; i++) {
        assert(inst->screen[i] != INVALID_SCREENVAL);
        if (inst->screen[i] != inst->oldscreen[i])
            n++;
    }

    for (i = 0; i < inst->screenlen; i++) {
        if (inst->screen[i] != inst->oldscreen[i]) {
            parray_append(inst->parrays[i], inst->frames-1, inst->screen[i]);
            inst->oldscreen[i] = inst->screen[i];
        }
    }
}

static void start_player(struct inst *inst)
{
    int i;

    setlocale(LC_CTYPE, ""); /* arrange that curses can query the charset */

    initscr();
    noecho();
    move(0,0);
    refresh();
    if (has_colors()) {
        start_color();
        for (i = 0; i < lenof(inst->cpairs); i++)
            inst->cpairs[i] = -1;
        inst->pairsused = 1;
        inst->nines = (use_default_colors() == OK);
    } else {
        inst->pairsused = -1;
    }
    curses_active = true;
}

static void end_player(struct inst *inst)
{
    if (!curses_active)
        return;
    endwin();
    curses_active = false;
}

static unsigned int_for_frame(struct inst *inst, int i, int f)
{
    return parray_search(inst->parrays[i], f, NULL);
}

static attr_t attr_cpair(struct inst *inst, int col)
{
    int fg, bg;

    if (!inst->nines) {
        /*
         * If default fg and bg are not supported, fall back to
         * white on black as a default.
         */
        fg = ((col << FGBGSHIFT) & FG) >> FGSHIFT;
        bg = ((col << FGBGSHIFT) & BG) >> BGSHIFT;
        if (fg == 9)
            fg = 7;
        if (bg == 9)
            bg = 0;
        col = ((fg << FGSHIFT) | (bg << BGSHIFT)) >> FGBGSHIFT;
    }

    if (col != 0x99) {
        if (inst->cpairs[col] == -1) {
            inst->cpairs[col] = inst->pairsused++;
            fg = ((col << FGBGSHIFT) & FG) >> FGSHIFT;
            bg = ((col << FGBGSHIFT) & BG) >> BGSHIFT;
            init_pair(inst->cpairs[col],
                      (fg < 8 ? fg : -1),
                      (bg < 8 ? bg : -1));
        }
        return inst->cpairs[col];
    }

    return 0;
}

static void display_frame(struct inst *inst, int f)
{
    int i, x, y;

    /*
     * Fetch the screen state in this frame.
     */

    for (i = 0; i < inst->screenlen; i++)
        inst->screen[i] = int_for_frame(inst, i, f);

    /*
     * Now display it.
     */
    for (y = 0; y < inst->h; y++)
        for (x = 0; x < inst->w; x++) {
            unsigned val = inst->screen[y*inst->w + x];
            int col, ch;
            attr_t attrs = A_NORMAL;
            short cpair = 0;

            if (val & BOLD)
                attrs |= A_BOLD;
            if (val & UNDER)
                attrs |= A_UNDERLINE;
            if (val & REV)
                attrs |= A_REVERSE;
            if (val & BLINK)
                attrs |= A_BLINK;
            if (inst->pairsused >= 0) {
                col = (val & FGBG) >> FGBGSHIFT;
                cpair = attr_cpair(inst, col);
            }
            wmove(stdscr, y, x);
            if (inst->unicode) {
#if HAVE_NCURSESW
                /*
                 * Turn our Unicode character into an ncursesw
                 * 'complex character', and display that directly.
                 */
                cchar_t cch;
                wchar_t ws[2];
                ws[0] = val & 0xFFFF;
                ws[1] = L'\0';
                setcchar(&cch, ws, attrs, cpair, NULL);
                wadd_wch(stdscr, &cch);
#else
                /*
                 * If we don't have ncursesw, we can at least _try_ to
                 * render this Unicode character into a single-byte
                 * character in the local character set. If
                 * successful, display that using ordinary curses.
                 */
                char buf[MB_LEN_MAX];
                char ch;
                mbstate_t st;
                memset(&st, 0, sizeof(st));
                if (wcrtomb(buf, val & 0xFFFF, &st) == 1)
                    ch = buf[0];
                else
                    ch = '?';
                wattrset(stdscr, attrs | COLOR_PAIR(cpair));
                waddch(stdscr, ch);
#endif
            } else if (val & 0x100) {
                switch (val & 0xFF) {
                    /*
                     * Use the ncurses codes for the VT100 line
                     * drawing characters where available. We can't
                     * do all of them: the control character
                     * representations such as HT and VT are not
                     * encoded by ncurses. We replace missing
                     * characters with ACS_BLOCK, on the grounds
                     * that they've got to be _something_.
                     */
                  case 0x5f:
                    ch = ' ';
                    break;
                  case 0x60:
                    ch = ACS_DIAMOND;
                    break;
                  case 0x61:
                    ch = ACS_CKBOARD;
                    break;
                  case 0x66:
                    ch = ACS_DEGREE;
                    break;
                  case 0x67:
                    ch = ACS_PLMINUS;
                    break;
                  case 0x6a:
                    ch = ACS_LRCORNER;
                    break;
                  case 0x6b:
                    ch = ACS_URCORNER;
                    break;
                  case 0x6c:
                    ch = ACS_ULCORNER;
                    break;
                  case 0x6d:
                    ch = ACS_LLCORNER;
                    break;
                  case 0x6e:
                    ch = ACS_PLUS;
                    break;
                  case 0x6f:
                    ch = ACS_S1;
                    break;
                  case 0x70:
                    ch = ACS_S3;
                    break;
                  case 0x71:
                    ch = ACS_HLINE;
                    break;
                  case 0x72:
                    ch = ACS_S7;
                    break;
                  case 0x73:
                    ch = ACS_S9;
                    break;
                  case 0x74:
                    ch = ACS_LTEE;
                    break;
                  case 0x75:
                    ch = ACS_RTEE;
                    break;
                  case 0x76:
                    ch = ACS_BTEE;
                    break;
                  case 0x77:
                    ch = ACS_TTEE;
                    break;
                  case 0x78:
                    ch = ACS_VLINE;
                    break;
                  case 0x79:
                    ch = ACS_LEQUAL;
                    break;
                  case 0x7a:
                    ch = ACS_GEQUAL;
                    break;
                  case 0x7b:
                    ch = ACS_PI;
                    break;
                  case 0x7c:
                    ch = ACS_NEQUAL;
                    break;
                  case 0x7d:
                    ch = ACS_STERLING;
                    break;
                  case 0x7e:
                    ch = ACS_BULLET;
                    break;
                  default:
                    ch = ACS_BLOCK;
                    break;
                }
                wattrset(stdscr, attrs | COLOR_PAIR(cpair));
                waddch(stdscr, ch);
            } else {
                wattrset(stdscr, attrs | COLOR_PAIR(cpair));
                waddch(stdscr, val & 0xFF);
            }
        }

    /*
     * Draw the OSD and the numeric count, if any.
     */
    if (inst->number) {
        char buf[40];
        int len = sprintf(buf, " %d ", inst->number);
        wmove(stdscr, 1, inst->w - len - 1);
        wattrset(stdscr, A_NORMAL);
        wattron(stdscr, A_BOLD);
        wattron(stdscr, COLOR_PAIR(attr_cpair(inst,0x47))); /* white on blue */
        waddstr(stdscr, buf);
        wattrset(stdscr, A_NORMAL);
    }
    if (inst->osd) {
        char buf1[80], buf2[60], buf3[80], buf4[80];
        long long t;

        t = int_for_frame(inst, TIMETOP, f);
        t = (t << 32) + int_for_frame(inst, TIMEBOT, f);

        sprintf(buf2, "%s x %g", inst->logmod ? "LOG" : "", inst->speedmod);
        if (inst->logmod || inst->speedmod != 1.0)
            sprintf(buf4, " Speed:%20s ", buf2);
        else
            buf4[0] = '\0';
        sprintf(buf2, "%d / %d", f, inst->frames);
        sprintf(buf1, " Frame:%20s ", buf2);
        sprintf(buf2, " Time:%21.3f ", t / 1000000.0);
        sprintf(buf3, " Mode:%21s ",
                (inst->playing ? "PLAY" : "PAUSE"));

        wattrset(stdscr, A_NORMAL);
        wattron(stdscr, A_BOLD);
        wattron(stdscr, COLOR_PAIR(attr_cpair(inst,0x47))); /* white on blue */
        wmove(stdscr, 1, 1);
        waddstr(stdscr, buf1);
        wmove(stdscr, 2, 1);
        waddstr(stdscr, buf2);
        wmove(stdscr, 3, 1);
        waddstr(stdscr, buf3);
        wmove(stdscr, 4, 1);
        waddstr(stdscr, buf4);
        wattrset(stdscr, A_NORMAL);
    }

    /*
     * Position the cursor.
     */
    x = inst->screen[CURSOR];
    if (x == -1) {
        curs_set(0);
    } else {
        curs_set(1);
        y = x / inst->w;
        x %= inst->w;
        wmove(stdscr, y, x);
    }
}

/*
 * Search the movie array for a frame containing a given piece of
 * text. Returns the frame in which the text was found, or <0 if
 * not.
 */
static int search(struct inst *inst, char *string, int start_frame,
                  int backwards)
{
    int f = start_frame;
    int i, j, k, len;
    bool *searchlines;
    int *indices, *nextframes;
    char *scrbuf;

    /*
     * Check the bounds.
     */
    if (start_frame >= inst->frames || start_frame < 0)
        return -1;                     /* not found */

    /*
     * We track which lines of the display actually changed between
     * frames, in order to avoid repeatedly searching an unchanged
     * line. Initially, of course, we set all these flags to true
     * because the first frame must be searched in full.
     */
    searchlines = snewn(inst->h, bool);
    for (i = 0; i < inst->h; i++)
        searchlines[i] = true;

    /*
     * Allocate space for tracking indices, and the next frame in
     * which each integer changes, in the display parrays.
     */
    indices = snewn(inst->w * inst->h, int);
    nextframes = snewn(inst->w * inst->h, int);
    for (i = 0; i < inst->w * inst->h; i++)
        indices[i] = -1;

    /*
     * And allocate space for the actual display buffer.
     */
    scrbuf = snewn(inst->w * inst->h, char);
    memset(scrbuf, 0, inst->w * inst->h);

    len = strlen(string);

    while (1) {
        /*
         * Retrieve the current frame.
         */
        for (i = 0; i < inst->w * inst->h; i++) {
            int integer, nextframe = -1;
            bool changed = false;

            if (indices[i] < 0) {
                /*
                 * This is the first time we've retrieved this
                 * integer, so we need to do a conventional
                 * retrieve operation and set up our index.
                 */
                integer = parray_search(inst->parrays[i], f, &indices[i]);
                changed = true;
            } else if (backwards && f < nextframes[i]) {
                /*
                 * This integer has changed in this frame (reverse
                 * search version).
                 */
                indices[i]--;
                integer = parray_retrieve(inst->parrays[i], indices[i],
                                          &nextframe);
                changed = true;
            } else if (!backwards && f >= nextframes[i]) {
                /*
                 * This integer has changed in this frame (forward
                 * search version).
                 */
                indices[i]++;
                integer = parray_retrieve(inst->parrays[i], indices[i], NULL);
                changed = true;
            }

            if (changed) {
                char bufval;

                /*
                 * Update the screen buffer and mark this line as
                 * changed.
                 */
                if (integer & 0x100)
                    bufval = 0;        /* ignore line drawing characters */
                else
                    bufval = integer;

                if (scrbuf[i] != bufval) {
                    scrbuf[i] = bufval;
                    searchlines[i / inst->w] = true;
                }

                /*
                 * Find the next frame in which this integer
                 * changes.
                 */
                if (nextframe < 0) {
                    if (backwards)
                        parray_retrieve(inst->parrays[i], indices[i],
                                        &nextframe);
                    else {
                        if (indices[i]+1 < inst->parrays[i]->items)
                            parray_retrieve(inst->parrays[i], indices[i]+1,
                                            &nextframe);
                        else
                            nextframe = inst->frames;
                    }
                }
                nextframes[i] = nextframe;
            }
        }

        /*
         * Search whatever lines of the current frame we need to.
         */
        for (i = 0; i < inst->h; i++)
            if (searchlines[i]) {
                bool found;

                searchlines[i] = false;

                /*
                 * FIXME: for the moment we'll just do a naive
                 * string search.
                 */
                found = false;
                for (j = 0; j <= inst->w - len; j++) {
                    for (k = 0; k < len; k++)
                        if (scrbuf[i * inst->w + j + k] != string[k])
                            break;
                    if (k == len) {
                        found = true;
                        break;
                    }
                }
                if (found)
                    goto found_it;
            }

        /*
         * Not found, so move to next frame.
         */
        if (backwards) {
            f--;
            if (f < 0) {
                f = -1;
                goto found_it;
            }
        } else {
            f++;
            if (f >= inst->frames) {
                f = -1;
                goto found_it;
            }
        }
    }

  found_it:

    sfree(scrbuf);
    sfree(nextframes);
    sfree(indices);
    sfree(searchlines);

    return f;
}

static long long time_after_frame(struct inst *inst, int f)
{
    unsigned long long t1, t2;

    if (f+1 >= inst->frames)
        return -1;

    t1 = int_for_frame(inst, TIMETOP, f);
    t1 = (t1 << 32) + int_for_frame(inst, TIMEBOT, f);

    t2 = int_for_frame(inst, TIMETOP, f+1);
    t2 = (t2 << 32) + int_for_frame(inst, TIMEBOT, f+1);

    return t2 - t1;
}

static char *getstring(struct inst *inst, const char *prompt)
{
    int w, h, plen, slen, i, c;
    char *str;
    int size, len;

    size = len = 0;
    str = NULL;

    getmaxyx(stdscr, h, w);

    plen = strlen(prompt);
    if (plen > w-2)
        plen = w-2;
    slen = w - plen - 1;

    while (1) {
        /*
         * Display the prompt and the current input.
         */
        wmove(stdscr, h-1, 0);
        wattrset(stdscr, A_NORMAL);
        wattron(stdscr, A_BOLD);
        wattron(stdscr, COLOR_PAIR(attr_cpair(inst,0x47))); /* white on blue */
        waddnstr(stdscr, prompt, plen);
        if (len > slen) {
            waddch(stdscr, '<');
            waddnstr(stdscr, str + len - slen + 1, slen - 1);
            wmove(stdscr, h-1, plen + slen);
        } else {
            waddnstr(stdscr, str, len);
            for (i = len + plen; i < w; i++)
                waddch(stdscr, ' ');
            wmove(stdscr, h-1, plen + len);
        }
        wattrset(stdscr, A_NORMAL);

        /*
         * Get a character.
         */
        c = getch();
        if (c >= ' ' && c <= '~') {
            /*
             * Append this character to the string.
             */
            if (len >= size) {
                size = (len + 5) * 3 / 2;
                str = sresize(str, size, char);
            }
            str[len++] = c;
        } else if (c == '\010' || c == '\177') {
            if (len > 0)
                len--;
        } else if (c == '\025') {
            len = 0;                   /* ^U clears line */
        } else if (c == '\027') {
            /* ^W deletes a word */
            while (len > 0 && isspace((unsigned char)(str[len-1])))
                len--;
            while (len > 0 && !isspace((unsigned char)(str[len-1])))
                len--;
        } else if (c == '\r' || c == '\n') {
            break;
        } else if (c == '\033') {
            len = 0;
            break;
        }
    }

    if (len == 0) {
        sfree(str);
        return NULL;
    } else {
        str = sresize(str, len+1, char);
        str[len] = '\0';
        return str;
    }
}

int main(int argc, char **argv)
{
    struct inst inst[1];
    int i;
    unsigned long long totalsize;
    time_t start, end;
    bool doing_opts;
    int iw, ih, startframe = 0;
    int quitframe = -1, pauseframe = -1;
    int deftype = NODEFAULT;
    bool prepareonly = false;
    bool autoplay = false, loop = false;
    /* FILE *debugfp = fopen("/home/simon/.f", "w"); setvbuf(debugfp, NULL, _IONBF, 0); */

    enum { OPT_UTF8_LINEDRAW = 256, };

#ifdef PARRAY_TEST
    {
        struct parray *pa;
        int i, j, k;

        pa = parray_new();
        for (i = 0; i < 5*3*3*3; i++) {
            parray_append(pa, i, i*i);

            for (j = 0; j <= i; j++) {
                k = parray_search(pa, j, NULL);
                if (k != j*j) {
                    printf("FAIL: i=%d j=%d wrong=%d right=%d\n",
                           i, j, k, j*j);
                }
            }
        }

        exit(0);
    }
#endif

    setlocale(LC_CTYPE, "");

    memset(inst, 0, sizeof(*inst));
    inst->conf = conf_new();

    do_defaults(NULL, inst->conf);

    inst->filenames = NULL;
    inst->nfiles = inst->filesize = 0;

    doing_opts = true;
    iw = 80;
    ih = 24;
    while (--argc) {
        char *p = *++argv;
        if (doing_opts && *p == '-') {
            char optbuf[3], *optstr, *optval;
            int optchr;

            /*
             * Special case "--" inhibits further option
             * processing.
             */
            if (!strcmp(p, "--")) {
                doing_opts = false;
                continue;
            }

            /*
             * All other "--" long options are translated into codes
             * we process via the short-option switch below. Those
             * that don't have actual corresponding short options get
             * values >= 256 from the OPT_* enum above.
             */
            if (p[1] == '-') {
                optval = strchr(p, '=');
                if (optval)
                    *optval++ = '\0';
                optstr = p;
                p += 2;
                if (!strcmp(p, "width"))
                    optchr = 'w';
                else if (!strcmp(p, "height"))
                    optchr = 'h';
                else if (!strcmp(p, "frame"))
                    optchr = 'f';
                else if (!strcmp(p, "stop"))
                    optchr = 'q';
                else if (!strcmp(p, "ttyrec"))
                    optchr = 'T';
                else if (!strcmp(p, "prepare-only"))
                    optchr = 'P';
                else if (!strcmp(p, "autoplay"))
                    optchr = 'A';
                else if (!strcmp(p, "loop"))
                    optchr = 'L';
                else if (!strcmp(p, "nhrecorder") ||
                         !strcmp(p, "nh-recorder") ||
                         !strcmp(p, "nh_recorder") ||
                         !strcmp(p, "nhrecording") ||
                         !strcmp(p, "nh-recording") ||
                         !strcmp(p, "nh_recording"))
                    optchr = 'N';
                else if (!strcmp(p, "raw-bytes"))
                    optchr = 'R';
                else if (!strcmp(p, "use-terminal-size"))
                    optchr = 'u';
                else if (!strcmp(p, "utf8-linedraw"))
                    optchr = OPT_UTF8_LINEDRAW;
                else if (!strcmp(p, "help")) {
                    ipbt_usage();
                    return 0;
                } else if (!strcmp(p, "version")) {
                    ipbt_version();
                    return 0;
                } else if (!strcmp(p, "licence")) {
                    ipbt_licence();
                    return 0;
                } else
                    optchr = '\1';     /* definitely not defined */
            } else {
                optbuf[0] = '-';
                optbuf[1] = optchr = p[1];
                optbuf[2] = '\0';
                optstr = optbuf;
                if (p[2])
                    optval = p+2;
                else
                    optval = NULL;
            }

            switch (optchr) {
              case 'w':
              case 'h':
              case 'f':
              case 'q':
              case 'p':
                /*
                 * these options all require an argument
                 */
                if (!optval) {
                    if (--argc)
                        optval = *++argv;
                    else {
                        modalfatalbox("option '%s' expects an argument",
                                      optstr);
                        return 1;
                    }
                }
                break;

              default:
                /*
                 * anything else does not
                 */
                if (optval) {
                    modalfatalbox(" option '%s' expects no argument", optstr);
                    return 1;
                }
                break;
            }

            switch (optchr) {
              case 'w':
                assert(optval);
                iw = atoi(optval);
                if (iw <= 0) {
                    modalfatalbox(" argument to '%s' must be positive",
                                  optstr);
                    return 1;
                }
                break;
              case 'h':
                assert(optval);
                ih = atoi(optval);
                if (ih <= 0) {
                    modalfatalbox(" argument to '%s' must be positive",
                                  optstr);
                    return 1;
                }
                break;
              case 'f':
                assert(optval);
                startframe = atoi(optval);
                if (startframe < 0) {
                    modalfatalbox(" argument to '%s' must be non-negative",
                                  optstr);
                    return 1;
                }
                break;
              case 'q':
                assert(optval);
                quitframe = atoi(optval);
                if (quitframe < 0) {
                    modalfatalbox(" argument to '%s' must be non-negative",
                                  optstr);
                    return 1;
                }
                break;
              case 'p':
                assert(optval);
                pauseframe = atoi(optval);
                if (pauseframe < 0) {
                    modalfatalbox(" argument to '%s' must be non-negative",
                                  optstr);
                    return 1;
                }
                break;
              case 'T':
                deftype = TTYREC;
                break;
              case 'N':
                deftype = NHRECORDER;
                break;
              case 'R':
                deftype = RAWBYTES;
                break;
              case 'P':
                prepareonly = true;
                break;
              case 'A':
                autoplay = true;
                break;
              case 'L':
                loop = true;
                break;
              case 'U':
                inst->unicode = true;
                break;
              case 'u':
                /*
                 * Use the current screen size as the internal
                 * player's screen size.
                 */
                {
                    struct winsize ws;

                    if (!ioctl (0, TIOCGWINSZ, &ws)) {
                        ih = ws.ws_row;
                        iw = ws.ws_col;
                    } else {
                        modalfatalbox(" unable to discover terminal size: %s",
                                      strerror(errno));
                    }
                }
                break;
              case OPT_UTF8_LINEDRAW:
                conf_set_bool(inst->conf, CONF_utf8linedraw, true);
                break;
              default:
                modalfatalbox(" unrecognised option '%s'", optstr);
                return 1;
            }
        } else {
            if (inst->nfiles >= inst->filesize) {
                inst->filesize = inst->nfiles + 32;
                inst->filenames = sresize(inst->filenames, inst->filesize,
                                          struct filename);
            }
            inst->filenames[inst->nfiles].name = dupstr(p);
            inst->filenames[inst->nfiles].type = deftype;
            inst->nfiles++;
        }
    }

    if (inst->unicode) {
        conf_set_str(inst->conf, CONF_line_codepage, "UTF-8");
    } else {
        conf_set_str(inst->conf, CONF_line_codepage, "");
    }
    conf_set_bool(inst->conf, CONF_utf8_override, false);

    init_ucs(&inst->ucsdata,
             conf_get_str(inst->conf, CONF_line_codepage), false, CS_NONE,
             conf_get_int(inst->conf, CONF_vtmode));

    /*
     * Fix up ucsdata so that it encodes the VT100 line drawing
     * characters in the D9xx page, for simplicity of
     * implementation in do_text().
     */
    for (i = 0; i < 256; i++) {
        if (i >= 0x5F && i < 0x7F)
            inst->ucsdata.unitab_xterm[i] = 0xD900 + i;
        else
            inst->ucsdata.unitab_xterm[i] = inst->ucsdata.unitab_line[i];
    }

    inst->ucsdata.dbcs_screenfont = false;
    inst->w = iw;
    inst->h = ih;

    inst->screenlen = TOTAL;
    inst->screen = snewn(inst->screenlen, int);
    inst->oldscreen = snewn(inst->screenlen, int);
    for (i = 0; i < inst->screenlen; i++)
        inst->oldscreen[i] = INVALID_SCREENVAL;

    inst->tw.vt = &ipbt_termwin_vtable;
    inst->term = term_init(inst->conf, &inst->ucsdata, &inst->tw);
    inst->term->ldisc = NULL;
    term_size(inst->term, inst->h, inst->w, 0);
    term_set_trust_status(inst->term, false);

    inst->parrays = snewn(TOTAL, struct parray *);
    for (i = 0; i < TOTAL; i++)
        inst->parrays[i] = parray_new();
    inst->movietime = 0LL;
    inst->frames = 0;

    term_pwron(inst->term, true);

    start = time(NULL);
    totalsize = 0;

    for (inst->curr_file = 0; inst->curr_file < inst->nfiles;
         inst->curr_file++) {
        totalsize += read_tty_recording(
            inst->filenames[inst->curr_file].name, deftype, inst->term, stderr,
            (inst->curr_file == 0), store_frame, inst);
    }

    if (!inst->frames) {
        ipbt_usage();
        return 0;
    }

    end = time(NULL);

    {
        int memusage = 0, i;
        for (i = 0; i < TOTAL; i++)
            memusage += inst->parrays[i]->memusage;
        fprintf(stderr, "Total %d frames, %llu bytes loaded, %d bytes of memory used\n",
                inst->frames, totalsize, memusage);
    }
    fprintf(stderr, "Total loading time: %d seconds (%.3g sec/Mb)\n",
            (int)difftime(end, start),
            difftime(end, start) * 1048576 / totalsize);

    if (prepareonly) {
        printf("Not starting player due to -P option.\n");
        return 0;
    }

    {
        int f = startframe, fb = -1;
        long long t = -1;
        long long tsince = 0;
        bool changed = true;

        inst->number = 0;
        inst->osd = false;
        inst->playing = autoplay;
        inst->logmod = false;
        inst->speedmod = 1.0;
        inst->searchstr = NULL;
        inst->searchback = false;

        start_player(inst);
        while (1) {
            int c;

            if (f < 0)
                f = 0;
            if (f >= inst->frames)
                f = inst->frames - 1;

            display_frame(inst, f);
            if (changed) {
                changed = false;
                if (inst->playing) {
                    if (loop && f == inst->frames - 1)
                        t = 1000000;   /* FIXME: configurable loop delay */
                    else
                        t = time_after_frame(inst, f);
                    if (t < 0)
                        t = 0;         /* just in case ttyrec is malformed */
                    if (inst->logmod) {
                        /*
                         * Logarithmic time compression: we replace
                         * a time t seconds with log(1+t) seconds.
                         * This starts off with gradient 1 at t=0,
                         * so that short times still work normally;
                         * but times compress gradually as you go
                         * up the scale, so that the person you're
                         * watching doesn't tediously stop and
                         * think all the time.
                         */
                        t = 1000000 * log(1.0 + t / 1000000.0);
                    }
                    t /= inst->speedmod;
                } else
                    t = -1;
            }

            if (t >= 0) {
                struct timeval tv;
                fd_set r;
                int ret;
                long long tused;

                FD_ZERO(&r);
                FD_SET(0, &r);
                tv.tv_sec = t / 1000000;
                tv.tv_usec = t % 1000000;
                tused = t;

                wrefresh(stdscr);

                ret = select(1, &r, NULL, NULL, &tv);
                if (ret == 0) {
                    c = -1;
                    t = -1;
                } else {
                    c = getch();
                    t = tv.tv_sec;
                    t = t * 1000000 + tv.tv_usec;
                    tused -= t;
                }
                tsince += tused;
                if (tsince > 500000)
                    fb = -1;
            } else {
                c = getch();
                fb = -1;
            }

            if (c == 'q' || c == 'Q' || c == '\003')
                break;

            if (quitframe >= 0 && f >= quitframe) {
                sleep(2);
                break;
            }

            if (f == pauseframe)
                c = 'p';

            if (c == 'b' || c == '<') {
                /*
                 * When moving backwards, we move relative to the
                 * last frame we moved backwards _to_ rather than
                 * the current frame, provided it's been only a
                 * short time since the last press of 'b'. This
                 * enables the user to hold down 'b' to move
                 * backwards in playing mode, without a very short
                 * frame interval acting as a barrier.
                 */
                if (fb >= 0)
                    f = fb;
                f -= (inst->number ? inst->number : 1);
                inst->number = 0;
                tsince = 0;
                fb = f;
                changed = true;
            } else if (c >= '0' && c <= '9') {
                /* check against integer overflow */
                if (inst->number <= INT_MAX / 10 &&
                    inst->number * 10 <= INT_MAX - (c - '0'))
                    inst->number = inst->number * 10 + (c - '0');
            } else if (c == 'o' || c == 'O') {
                inst->osd = !inst->osd;
                inst->number = 0;
            } else if (c == 'l' || c == 'L') {
                inst->logmod = !inst->logmod;
                inst->number = 0;
                /*
                 * After toggling logarithmic time compression, set
                 * `changed = true' to recompute the current wait
                 * interval.
                 * 
                 * Ideally this would take account of the
                 * proportion of that interval which had already
                 * elapsed, but it's unclear exactly what that even
                 * means: when switching from linear to log mode,
                 * do you set the remaining wait interval to the
                 * log of the remaining linear time, or to the same
                 * proportion of the overall log time as was left
                 * of the linear time? And vice versa going the
                 * other way. So for the moment this is very simple
                 * and just restarts the wait interval from the
                 * beginning of its new length when you press L.
                 */
                changed = true;
            } else if (c == 'x') {
                t *= inst->speedmod;
                inst->speedmod = (inst->number ? inst->number : 1);
                t /= inst->speedmod;
                inst->number = 0;
            } else if (c == 'X') {
                t *= inst->speedmod;
                inst->speedmod = 1.0 / (inst->number ? inst->number : 1);
                t /= inst->speedmod;
                inst->number = 0;
            } else if (c == 'g') {
                f = inst->number;
                inst->number = 0;
                changed = true;
            } else if (c == 'G') {
                f = inst->frames - 1 - inst->number;
                inst->number = 0;
                changed = true;
            } else if (c == ' ' || c == '>') {
                f += (inst->number ? inst->number : 1);
                inst->number = 0;
                changed = true;
            } else if (c == -1 && inst->playing) {
                if (f+1 < inst->frames)
                    f++;
                else if (loop)
                    f = 0;
                changed = true;
            } else if (c == 'p' || c == 'P' || c == 's' || c == 'S') {
                inst->playing = !inst->playing;
                if (inst->playing && f+1 < inst->frames)
                    f++;
                inst->number = 0;
                changed = true;
            } else if (c == '/' || c == '?' || c == '\\' ||
                       c == 'n' || c == 'N') {
                int sf, back;
                char *str;
                if (c == '/' || c == '?' || c == '\\') {
                    changed = true;    /* need to redraw to remove prompt */
                    str = getstring(inst, (c == '/' ? "Search forward: " :
                                           "Search backward: "));
                    if (str) {
                        sfree(inst->searchstr);
                        inst->searchstr = str;
                        inst->searchback = (c != '/');
                    }
                } else
                    str = inst->searchstr;
                if (str) {
                    if (c == 'N')
                        back = !inst->searchback;
                    else
                        back = inst->searchback;
                    sf = search(inst, str, f + (back ? -1 : 1), back);
                    if (sf > 0) {
                        f = sf;
                        changed = true;/* need to redraw because we've moved */
                    } else {
                        beep();        /* not found */
                    }
                }
            }
        }
        end_player(inst);
        fprintf(stderr, "\nPlayback finished.\nLast frame reached was %d\n", f);
    }

    return 0;
}
