/*
 * fliplayer.c -- "BOOK386 PICTURE HOUSE": a silent-movie style FLI player
 *
 * Plays an Autodesk Animator FLI film (320x200, the kind ppm2fli writes)
 * with a piano accompaniment synthesised live on an AdLib / OPL2 / OPL3
 * card by a Reality Adlib Tracker (RAD v1.0) player -- like a pianist
 * playing along to a black-and-white comedy in a 1920s picture house.
 * Title cards open and close the show, and the picture can be tinted grey
 * or sepia and given a slight projector flicker.
 *
 * The RAD tune is NOT built in: you always supply your own module with -t
 * (or play without music with -m).
 *
 * Made for a 386SX-40 with no FPU: integer code only, the film is decoded
 * into RAM and only the changed rows are copied to the (slow) ISA VGA, and
 * if the machine falls behind the picture drops frames to stay in step.
 * The music is ticked at 50 Hz from a SIGALRM timer, independent of the
 * drawing speed.
 *
 * Plain C89 for gcc 2.7.2.3 (Slackware 4.0).  Build:
 *
 *     gcc -O2 -m386 -fomit-frame-pointer -o fliplayer fliplayer.c -lvga
 *
 * Run as root (OPL ports + svgalib):
 *
 *     ./fliplayer -t tune.rad [options] movie.fli
 *       -t tune.rad  the RAD v1.0 module to play (required, unless -m)
 *       -p port      OPL base port in hex (default 388)
 *       -T "text"    title shown on the opening card (default: file name)
 *       -d ms        override the frame delay stored in the FLI
 *       -g           black & white (grey palette)
 *       -e           sepia tint
 *       -k           projector flicker
 *       -l           loop the film forever (no closing card)
 *       -n           no title cards
 *       -v           wait for vertical retrace (no tearing, lower fps)
 *       -m           mute: no music, no OPL access (no -t needed)
 *       -f           skip OPL chip detection
 *
 * Keys while the film runs:
 *       SPACE        pause / resume (the piano falls silent while paused)
 *       LEFT         jump back 5 seconds        (works while paused too)
 *       RIGHT        jump forward 5 seconds     (works while paused too)
 *       ESC or q     quit
 *
 * Jumping back needs earlier screens, and a FLI file has no index (every
 * frame is a delta), so the player keeps up to MAX_SNAPS snapshots of the
 * screen in RAM (~65 KB each, spaced 5 s apart and thinned out
 * automatically on long films) and re-decodes from the nearest one.
 *
 * Make a film with:  mp4-to-fli.sh movie.mp4     (ffmpeg + ppm2fli -O)
 */

#ifndef _GNU_SOURCE
#define _GNU_SOURCE 1
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/types.h>
#if defined(__GLIBC__)
#include <sys/io.h>
#else
#include <asm/io.h>
extern int ioperm(unsigned long from, unsigned long num, int turn_on);
#endif
#include <vga.h>

typedef unsigned char  u8;
typedef unsigned short u16;
typedef unsigned int   u32;             /* int is 32 bits on the i386 */

static volatile int quit = 0;

/* ================================================================== */
/*  OPL access and the RAD v1.0 player                                 */
/* ================================================================== */

static int opl_port = 0x388;

/* bank 0 = ports base+0/+1, bank 1 (OPL3 only) = ports base+2/+3 */
static void opl_out_bank(int bank, int reg, int val)
{
    int i, base = opl_port + (bank ? 2 : 0);

    outb((unsigned char)reg, base);
    for (i = 0; i < 6; i++)
        inb(opl_port);                  /* ~3.3 us */
    outb((unsigned char)val, base + 1);
    for (i = 0; i < 35; i++)
        inb(opl_port);                  /* ~23 us */
}

static void opl_out(int reg, int val)
{
    opl_out_bank(0, reg, val);
}

static int opl_detect(void)
{
    int s1, s2;

    opl_out(0x04, 0x60);
    opl_out(0x04, 0x80);
    s1 = inb(opl_port) & 0xE0;
    opl_out(0x02, 0xFF);
    opl_out(0x04, 0x21);
    usleep(20000);
    s2 = inb(opl_port) & 0xE0;
    opl_out(0x04, 0x60);
    opl_out(0x04, 0x80);
    return (s1 == 0x00 && s2 == 0xC0);
}

/* Put the chip into a known state.  IMPORTANT: the second register bank must
   be reset as well.  If something left the chip in OPL3 mode (the kernel's
   sound driver at boot, or the undefined power-on state), voices without the
   left/right output bits are silent -- and RAD instruments don't set them.
   So switch back to plain OPL2-compatible mode first, then clear everything. */
static void opl_reset(void)
{
    int r;

    opl_out_bank(1, 0x05, 0x00);        /* OPL3 "NEW" bit off: OPL2 mode */
    opl_out_bank(1, 0x04, 0x00);        /* no 4-operator voices */
    opl_out(0x04, 0x80);                /* reset timer flags */
    opl_out(0x08, 0x00);                /* composite sine / note select */
    for (r = 0x20; r <= 0xF5; r++) {    /* key off, silence, zero both banks */
        opl_out(r, 0);
        opl_out_bank(1, r, 0);
    }
    opl_out_bank(1, 0x05, 0x00);        /* make sure NEW stayed off */
    opl_out(0x01, 0x20);                /* waveform select enable */
}

/* ---- Reality AdLib Tracker v1.0 (port of Shayde's PLAYER.ASM 1.1a) ---- */

#define RAD_FSTART 0x156
#define RAD_FRANGE 0x158                /* 0x2AE - 0x156 */

static const int rad_notefreq[12] = {   /* notes 1..12 = C# .. C(next oct) */
    0x16B, 0x181, 0x198, 0x1B0, 0x1CA, 0x1E5,
    0x202, 0x220, 0x241, 0x263, 0x287, 0x2AE
};
static const int rad_choff[9] = { 0x20, 0x21, 0x22, 0x28, 0x29, 0x2A, 0x30, 0x31, 0x32 };

static const u8 *rd_b = NULL;           /* the RAD module: always loaded from a file */
static int rd_len = 0;
static int rd_inst[31];                 /* file offset of 11-byte instrument */
static int rd_orderlist, rd_patlist, rd_patpos;
static int rd_nord, rd_orderpos, rd_speed, rd_speedcnt, rd_line, rd_slow;
static u8  rd_old43[9], rd_oldA0[9], rd_oldB0[9], rd_tsspeed[9];
static u8  rd_tone[9], rd_port[9], rd_vol[9];
static unsigned rd_tsfreq[9];

static unsigned rd16(const u8 *p) { return (unsigned)p[0] | ((unsigned)p[1] << 8); }

/* Returns NULL if the module is a sane RAD v1.0 file, else a message. */
static const char *rad_validate(void)
{
    const u8 *b = rd_b;
    int pos, n, nord, i, o, patlist, off;

    if (rd_len < 40 || memcmp(b, "RAD by REALiTY!!", 16) != 0)
        return "not a RAD module";
    if (b[16] != 0x10)
        return "only RAD version 1.0 modules are supported";
    pos = 18;
    if (b[17] & 0x80) {
        while (pos < rd_len && b[pos])
            pos++;
        if (pos >= rd_len)
            return "truncated description";
        pos++;
    }
    for (;;) {
        if (pos >= rd_len)
            return "truncated instrument list";
        n = b[pos++];
        if (n == 0)
            break;
        if (n > 31 || pos + 11 > rd_len)
            return "corrupt instrument data";
        pos += 11;
    }
    if (pos >= rd_len)
        return "truncated order list";
    nord = b[pos++];
    if (nord == 0)
        return "empty order list";
    patlist = pos + nord;
    if (patlist + 64 > rd_len)
        return "truncated pattern table";
    for (i = 0; i < nord; i++) {
        o = b[pos + i];
        if ((o & 0x80) ? ((o & 0x7F) >= nord) : (o >= 32))
            return "corrupt order list";
    }
    for (i = 0; i < 32; i++) {
        off = (int)rd16(b + patlist + 2 * i);
        if (off != 0 && (off < patlist + 64 || off >= rd_len))
            return "corrupt pattern offset";
    }
    return NULL;
}

static void rad_setvol(int ch, int vol) /* vol: 0..255 byte, >=64 -> 63 */
{
    if (vol >= 64)
        vol = 63;
    rd_old43[ch] = (u8)((rd_old43[ch] & 0xC0) | (vol ^ 0x3F));
    opl_out(rad_choff[ch] + 0x23, rd_old43[ch]);
}

/* instrument: 0 car20 1 mod20 2 car40 3 mod40 4 car60 5 mod60 6 car80
   7 mod80 8 C0 9 carE0 10 modE0 */
static void rad_loadinst(int ch, int inst)
{
    const u8 *d;
    int p, off;

    if (inst < 1 || inst > 31)
        return;
    p = rd_inst[inst - 1];
    if (!p)
        return;
    d = rd_b + p;
    off = rad_choff[ch] - 0x20;
    rd_old43[ch] = d[2];
    opl_out(0x20 + off, d[1]);
    opl_out(0x23 + off, d[0]);
    opl_out(0x40 + off, d[3]);
    opl_out(0x43 + off, d[2]);
    opl_out(0x60 + off, d[5]);
    opl_out(0x63 + off, d[4]);
    opl_out(0x80 + off, d[7]);
    opl_out(0x83 + off, d[6]);
    opl_out(0xE0 + off, d[10]);
    opl_out(0xE3 + off, d[9]);
    opl_out(0xC0 + ch, d[8]);
}

static unsigned rad_getfreq(int ch)
{
    unsigned cx, oct;

    cx = (unsigned)rd_oldA0[ch] | ((unsigned)(rd_oldB0[ch] & 3) << 8);
    oct = (unsigned)((rd_oldB0[ch] >> 2) & 7);
    cx = (cx - RAD_FSTART) & 0xFFFF;
    return (oct * RAD_FRANGE + cx) & 0xFFFF;
}

static void rad_setfreq(int ch, unsigned ax)
{
    unsigned oct, fr;
    int b0;

    ax &= 0xFFFF;
    oct = ax / RAD_FRANGE;
    fr = ax % RAD_FRANGE + RAD_FSTART;
    b0 = (rd_oldB0[ch] & 0xE0) | (int)((oct << 2) & 0xFF) | (int)(fr >> 8);
    rd_oldB0[ch] = (u8)b0;
    opl_out(0xB0 + ch, b0);
    rd_oldA0[ch] = (u8)(fr & 0xFF);
    opl_out(0xA0 + ch, (int)(fr & 0xFF));
}

/* Returns the line to jump to (0..63) or -1. */
static int rad_playnote(int ch, int nb, int ib, int param)
{
    int cmd = ib & 15, note, inst, f, b0;

    if (nb != 0) {
        if (cmd == 3) {                 /* tone slide: note is the target */
            note = (nb & 15) - 1;
            if (note >= 0 && note < 12) {
                rd_tsfreq[ch] = (unsigned)(((nb >> 4) & 7) * RAD_FRANGE
                                + rad_notefreq[note] - RAD_FSTART) & 0xFFFF;
                rd_tone[ch] = 1;
                if (param)
                    rd_tsspeed[ch] = (u8)param;
            }
            return -1;
        }
        rd_oldB0[ch] &= (u8)~0x20;      /* key off previous note */
        opl_out(0xB0 + ch, rd_oldB0[ch]);
        inst = ((nb & 0x80) ? 16 : 0) | (ib >> 4);
        if (inst)
            rad_loadinst(ch, inst);
        note = nb & 15;
        if (note >= 1 && note <= 12) {
            f = rad_notefreq[note - 1];
            b0 = 0x20 | (((nb >> 4) & 7) << 2) | (f >> 8);
            rd_oldB0[ch] = (u8)b0;
            rd_oldA0[ch] = (u8)(f & 0xFF);
            opl_out(0xA0 + ch, f & 0xFF);
            opl_out(0xB0 + ch, b0);
        }
    }

    switch (cmd) {
    case 1:                             /* portamento up */
        rd_port[ch] = (u8)param;
        break;
    case 2:                             /* portamento down */
        rd_port[ch] = (u8)(-param);
        break;
    case 3:                             /* tone slide, no note */
        if (param)
            rd_tsspeed[ch] = (u8)param;
        rd_tone[ch] = 1;
        break;
    case 5:                             /* tone slide + volume slide */
    case 10:                            /* volume slide: <50 down, >50 up */
        rd_vol[ch] = (u8)((param >= 50) ? -(param - 50) : param);
        if (cmd == 5)
            rd_tone[ch] = 1;
        break;
    case 12:                            /* set volume */
        rad_setvol(ch, param);
        break;
    case 13:                            /* jump to line in next pattern */
        if (param < 64)
            return param;
        break;
    case 15:                            /* set speed */
        rd_speed = param;
        break;
    default:
        break;
    }
    return -1;
}

static void rad_update(void)
{
    int ch, vol, spd, s;
    unsigned ax, cx;

    for (ch = 0; ch < 9; ch++) {
        if (rd_port[ch]) {
            ax = rad_getfreq(ch);
            ax = (ax + (unsigned)(int)(signed char)rd_port[ch]) & 0xFFFF;
            rad_setfreq(ch, ax);
        }
        s = (signed char)rd_vol[ch];
        if (s != 0) {
            vol = (rd_old43[ch] & 0x3F) ^ 0x3F;
            vol -= s;
            if (vol < 0)
                vol = 0;
            if (vol > 63)
                vol = 63;
            rad_setvol(ch, vol);
        }
        if (rd_tone[ch]) {
            spd = (signed char)rd_tsspeed[ch];
            ax = rad_getfreq(ch);
            cx = rd_tsfreq[ch];
            if (ax < cx) {
                ax = (ax + (unsigned)spd) & 0xFFFF;
                if (ax >= cx) {
                    ax = cx;
                    rd_tone[ch] = 0;
                }
            } else if (ax > cx) {
                ax = (ax - (unsigned)spd) & 0xFFFF;
                if (ax <= cx) {
                    ax = cx;
                    rd_tone[ch] = 0;
                }
            } else {
                rd_tone[ch] = 0;
            }
            rad_setfreq(ch, ax);
        }
    }
}

/* Move to order position 'pos' following jump markers; the tune loops. */
static void rad_goto_order(int pos)
{
    int pat = 0, guard = 0;

    for (;;) {
        if (pos < 0 || pos >= rd_nord)
            pos = 0;                    /* end of tune: loop */
        pat = rd_b[rd_orderlist + pos];
        if (!(pat & 0x80))
            break;
        pos = pat & 0x7F;
        if (++guard > 300) {
            pat = 0;
            pos = 0;
            break;
        }
    }
    rd_orderpos = pos;
    rd_patpos = (pat < 32) ? (int)rd16(rd_b + rd_patlist + pat * 2) : 0;
}

/* One timer tick (PlayMusic): called 50 times a second from SIGALRM. */
static void rad_tick(void)
{
    int i, ch, nb, ib, par, last, lastline, id, jump = -1, j, bad;
    int p;

    if (rd_speedcnt != 0) {
        rd_speedcnt--;
        rad_update();
        return;
    }
    for (i = 0; i < 9; i++) {
        rd_port[i] = 0;
        rd_vol[i] = 0;
        rd_tone[i] = 0;
    }

    p = rd_patpos;
    if (p > 0 && p < rd_len && (rd_b[p] & 0x7F) == rd_line) {
        lastline = rd_b[p] & 0x80;
        p++;
        for (;;) {
            if (p + 3 > rd_len) {
                rd_patpos = 0;
                break;
            }
            ch = rd_b[p] & 0x7F;
            last = rd_b[p] & 0x80;
            nb = rd_b[p + 1];
            ib = rd_b[p + 2];
            p += 3;
            par = 0;
            if (ib & 15) {
                if (p >= rd_len) {
                    rd_patpos = 0;
                    break;
                }
                par = rd_b[p++];
            }
            if (ch < 9) {
                j = rad_playnote(ch, nb, ib, par);
                if (j >= 0) {
                    jump = j;
                    break;
                }
            }
            if (last) {
                rd_patpos = lastline ? 0 : p;
                break;
            }
        }
    }

    if (jump >= 0) {                    /* Dxx: line 'jump' of next pattern */
        rd_speedcnt = rd_speed & 0xFF;
        rd_line = jump;
        rad_goto_order(rd_orderpos + 1);
        p = rd_patpos;
        if (p != 0) {
            bad = 0;
            for (;;) {                  /* find first line >= target */
                if (p >= rd_len) { bad = 1; break; }
                id = rd_b[p] & 0x7F;
                if (id >= jump)
                    break;
                if (rd_b[p] & 0x80) { bad = 1; break; }
                p++;
                for (;;) {              /* skip this line's channel data */
                    if (p + 3 > rd_len) { bad = 1; break; }
                    last = rd_b[p] & 0x80;
                    par = rd_b[p + 2] & 15;
                    p += 3;
                    if (par)
                        p++;
                    if (last)
                        break;
                }
                if (bad)
                    break;
            }
            rd_patpos = bad ? 0 : p;
        }
        rad_update();
        return;
    }

    rd_speedcnt = (rd_speed - 1) & 0xFF;        /* after effects: new speed */
    rd_line++;
    if (rd_line >= 64) {
        rd_line = 0;
        rad_goto_order(rd_orderpos + 1);
    }
    rad_update();
}

/* Parse the (validated) module and start from the first pattern. */
static void rad_start(void)
{
    const u8 *b = rd_b;
    int flags, pos, n, i;

    flags = b[17];
    rd_slow = (flags & 0x40) != 0;
    rd_speed = flags & 0x1F;
    pos = 18;
    if (flags & 0x80) {                 /* skip description */
        while (pos < rd_len && b[pos])
            pos++;
        pos++;
    }
    for (i = 0; i < 31; i++)
        rd_inst[i] = 0;
    while (pos < rd_len) {
        n = b[pos++];
        if (n == 0)
            break;
        if (n <= 31)
            rd_inst[n - 1] = pos;
        pos += 11;
    }
    rd_nord = b[pos++];
    rd_orderlist = pos;
    rd_patlist = pos + rd_nord;

    memset(rd_old43, 0, sizeof(rd_old43));
    memset(rd_oldA0, 0, sizeof(rd_oldA0));
    memset(rd_oldB0, 0, sizeof(rd_oldB0));
    memset(rd_tsspeed, 1, sizeof(rd_tsspeed));
    memset(rd_tone, 0, sizeof(rd_tone));
    memset(rd_port, 0, sizeof(rd_port));
    memset(rd_vol, 0, sizeof(rd_vol));
    for (i = 0; i < 9; i++)
        rd_tsfreq[i] = 0;
    rd_speedcnt = 0;
    rd_line = 0;
    rd_patpos = 0;
    rad_goto_order(0);
}

/* ---- 50 Hz (or 18.2 Hz "slow timer") music tick from SIGALRM ---- */

static volatile int music_paused = 0;
static int music_on = 0;                /* the timer is running */

static void on_alarm(int sig)
{
    (void)sig;
    if (!music_paused)
        rad_tick();
}

static void timer_start(void)
{
    struct sigaction sa;
    struct itimerval it;

    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = on_alarm;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    sigaction(SIGALRM, &sa, NULL);
    it.it_interval.tv_sec = 0;
    it.it_interval.tv_usec = rd_slow ? 54925 : 20000;
    it.it_value = it.it_interval;
    setitimer(ITIMER_REAL, &it, NULL);
}

static void timer_stop(void)
{
    struct itimerval it;

    memset(&it, 0, sizeof(it));
    setitimer(ITIMER_REAL, &it, NULL);
}

/* Pause the pianist: stop ticking and let every note go.  The signal handler
   cannot run while this code does, and it ignores ticks once the flag is set,
   so the OPL ports are never driven from two places at once. */
static void music_pause(void)
{
    int ch;

    if (!music_on)
        return;
    music_paused = 1;
    for (ch = 0; ch < 9; ch++) {
        rd_oldB0[ch] &= (u8)~0x20;      /* key off */
        opl_out(0xB0 + ch, rd_oldB0[ch]);
    }
}

static void music_resume(void)
{
    music_paused = 0;
}

/* ================================================================== */
/*  Screen, palette and silent-film effects                            */
/* ================================================================== */

#define SW 320
#define SH 200

static u8 fb[SW * SH];                  /* off-screen frame buffer */
static u8 *vgamem;
static u8 pal[256][3];                  /* film palette, 6-bit, already tinted */
static int tint = 0;                    /* 0 colour, 1 black & white, 2 sepia */
static int flicker = 0;
static int pal_dirty = 0;
static int dlo = SH, dhi = -1;          /* screen rows changed since last show */

static u32 rnd_state = 0x2545F491u;
static int rnd(void)
{
    rnd_state = rnd_state * 1103515245u + 12345u;
    return (int)((rnd_state >> 16) & 0x7FFF);
}

/* Store a palette entry (6-bit components), applying the tint. */
static void set_pal(int i, int r, int g, int b)
{
    int y;

    r &= 63;
    g &= 63;
    b &= 63;
    if (tint) {
        y = (r * 30 + g * 59 + b * 11) / 100;   /* luminance */
        if (tint == 1) {
            r = g = b = y;
        } else {                                /* sepia */
            r = y + (y >> 3);
            if (r > 63)
                r = 63;
            g = y - (y >> 3);
            b = (y * 5) >> 3;
        }
    }
    pal[i][0] = (u8)r;
    pal[i][1] = (u8)g;
    pal[i][2] = (u8)b;
}

/* scale: 256 = full brightness (flicker and fades dim the whole palette) */
static void flush_palette(int scale)
{
    int i;

    for (i = 0; i < 256; i++)
        vga_setpalette(i, (pal[i][0] * scale) >> 8, (pal[i][1] * scale) >> 8,
                       (pal[i][2] * scale) >> 8);
}

/* ================================================================== */
/*  FLI / FLC decoder                                                  */
/* ================================================================== */

static FILE *fli;
static u32 fli_magic;
static int fli_frames, fli_w, fli_h, fli_delay;         /* delay in ms */
static long fli_first;                                  /* offset of frame 1 */
static int fx, fy;                                      /* film window on screen */
static u8 *fbuf;                                        /* one frame's chunks */
static u32 fbuf_cap;
#define MAX_FRAME (512u * 1024u)

#define FROW(y) (fb + (fy + (y)) * SW + fx)

static u32 rd32(const u8 *p)
{
    return (u32)p[0] | ((u32)p[1] << 8) | ((u32)p[2] << 16) | ((u32)p[3] << 24);
}

static void mark_rows(int y0, int y1)   /* film rows, inclusive */
{
    if (y0 < 0)
        y0 = 0;
    if (y1 >= fli_h)
        y1 = fli_h - 1;
    if (y0 > y1)
        return;
    y0 += fy;
    y1 += fy;
    if (y0 < dlo)
        dlo = y0;
    if (y1 > dhi)
        dhi = y1;
}

/* COLOR_64 (11): 6-bit components.  COLOR_256 (4): 8-bit components. */
static void do_color(const u8 *p, int n, int sixbit)
{
    int packets, idx = 0, skip, cnt, i;

    if (n < 2)
        return;
    packets = (int)rd16(p);
    p += 2;
    n -= 2;
    while (packets-- > 0) {
        if (n < 2)
            return;
        skip = p[0];
        cnt = p[1];
        if (cnt == 0)
            cnt = 256;
        p += 2;
        n -= 2;
        idx += skip;
        for (i = 0; i < cnt; i++) {
            if (n < 3)
                return;
            if (idx < 256) {
                if (sixbit)
                    set_pal(idx, p[0], p[1], p[2]);
                else
                    set_pal(idx, p[0] >> 2, p[1] >> 2, p[2] >> 2);
            }
            idx++;
            p += 3;
            n -= 3;
        }
    }
    pal_dirty = 1;
}

/* BYTE_RUN (15): whole frame, run-length coded line by line. */
static void do_brun(const u8 *p, int n)
{
    int y, x, c, w;
    u8 *row;

    for (y = 0; y < fli_h; y++) {
        if (n < 1)
            break;
        p++;                            /* packet count: not reliable, ignored */
        n--;
        row = FROW(y);
        x = 0;
        while (x < fli_w) {
            if (n < 1)
                break;
            c = (signed char)*p++;
            n--;
            if (c < 0) {                /* literal bytes */
                c = -c;
                if (c > n)
                    c = n;
                w = (c < fli_w - x) ? c : fli_w - x;
                memcpy(row + x, p, (size_t)w);
                p += c;
                n -= c;
                x += c;
            } else if (c > 0) {         /* run */
                if (n < 1)
                    break;
                w = (c < fli_w - x) ? c : fli_w - x;
                memset(row + x, *p, (size_t)w);
                p++;
                n--;
                x += c;
            }
        }
    }
    mark_rows(0, fli_h - 1);
}

/* DELTA_FLI (12), "LC": byte-oriented line delta (what ppm2fli -O writes). */
static void do_lc(const u8 *p, int n)
{
    int first, cnt, y, pk, x, c, w;
    u8 *row;

    if (n < 4)
        return;
    first = (int)rd16(p);
    cnt = (int)rd16(p + 2);
    p += 4;
    n -= 4;
    mark_rows(first, first + cnt - 1);
    for (y = first; cnt > 0; cnt--, y++) {
        if (n < 1)
            return;
        pk = *p++;
        n--;
        row = (y >= 0 && y < fli_h) ? FROW(y) : NULL;
        x = 0;
        while (pk-- > 0) {
            if (n < 2)
                return;
            x += p[0];                  /* skip */
            c = (signed char)p[1];
            p += 2;
            n -= 2;
            if (c >= 0) {               /* copy c literal bytes */
                if (c > n)
                    c = n;
                if (row && x < fli_w) {
                    w = (c < fli_w - x) ? c : fli_w - x;
                    memcpy(row + x, p, (size_t)w);
                }
                p += c;
                n -= c;
                x += c;
            } else {                    /* repeat one byte -c times */
                c = -c;
                if (n < 1)
                    return;
                if (row && x < fli_w) {
                    w = (c < fli_w - x) ? c : fli_w - x;
                    memset(row + x, *p, (size_t)w);
                }
                p++;
                n--;
                x += c;
            }
        }
    }
}

/* DELTA_FLC (7), "SS2": word-oriented line delta (FLC files). */
static void do_ss2(const u8 *p, int n)
{
    int lines, y = 0, op, pk, x, c, i, nb, w;
    u8 *row;
    u8 a, b;

    if (n < 2)
        return;
    lines = (int)rd16(p);
    p += 2;
    n -= 2;
    while (lines > 0) {
        if (n < 2)
            return;
        op = (int)rd16(p);
        p += 2;
        n -= 2;
        switch (op & 0xC000) {
        case 0xC000:                    /* skip lines */
            y += 0x10000 - op;
            break;
        case 0x8000:                    /* last pixel of this line */
            if (y >= 0 && y < fli_h) {
                FROW(y)[fli_w - 1] = (u8)(op & 0xFF);
                mark_rows(y, y);
            }
            break;
        case 0x4000:                    /* undefined */
            return;
        default:                        /* packet count for this line */
            lines--;
            pk = op;
            x = 0;
            row = (y >= 0 && y < fli_h) ? FROW(y) : NULL;
            mark_rows(y, y);
            while (pk-- > 0) {
                if (n < 2)
                    return;
                x += p[0];
                c = (signed char)p[1];
                p += 2;
                n -= 2;
                if (c >= 0) {           /* c literal words */
                    nb = c * 2;
                    if (nb > n)
                        nb = n;
                    if (row && x < fli_w) {
                        w = (nb < fli_w - x) ? nb : fli_w - x;
                        memcpy(row + x, p, (size_t)w);
                    }
                    p += nb;
                    n -= nb;
                    x += nb;
                } else {                /* one word repeated -c times */
                    c = -c;
                    if (n < 2)
                        return;
                    a = p[0];
                    b = p[1];
                    p += 2;
                    n -= 2;
                    if (row)
                        for (i = 0; i < c && x < fli_w; i++, x += 2) {
                            row[x] = a;
                            if (x + 1 < fli_w)
                                row[x + 1] = b;
                        }
                    else
                        x += c * 2;
                }
            }
            y++;
            break;
        }
    }
}

static void do_black(void)
{
    int y;

    for (y = 0; y < fli_h; y++)
        memset(FROW(y), 0, (size_t)fli_w);
    mark_rows(0, fli_h - 1);
}

static void do_copy(const u8 *p, int n)
{
    int y, w;

    for (y = 0; y < fli_h && n > 0; y++) {
        w = (n < fli_w) ? n : fli_w;
        memcpy(FROW(y), p, (size_t)w);
        p += fli_w;
        n -= fli_w;
    }
    mark_rows(0, fli_h - 1);
}

/* Open the film and read the header.  Returns 0 on success. */
static int fli_open(const char *name)
{
    u8 h[128];
    unsigned speed;

    fli = fopen(name, "rb");
    if (!fli) {
        perror(name);
        return -1;
    }
    setvbuf(fli, NULL, _IOFBF, 32768);
    if (fread(h, 1, 128, fli) != 128) {
        fprintf(stderr, "%s: file too short for a FLI header\n", name);
        return -1;
    }
    fli_magic = rd16(h + 4);
    if (fli_magic != 0xAF11 && fli_magic != 0xAF12) {
        fprintf(stderr, "%s: not a FLI/FLC animation\n", name);
        return -1;
    }
    fli_frames = (int)rd16(h + 6);
    fli_w = (int)rd16(h + 8);
    fli_h = (int)rd16(h + 10);
    if (rd16(h + 12) != 8) {
        fprintf(stderr, "%s: not an 8-bit animation\n", name);
        return -1;
    }
    if (fli_w < 1 || fli_h < 1 || fli_w > SW || fli_h > SH) {
        fprintf(stderr, "%s: %dx%d is too large (this player shows up to %dx%d)\n",
                name, fli_w, fli_h, SW, SH);
        return -1;
    }
    if (fli_magic == 0xAF11) {                  /* FLI: speed in 1/70 s */
        speed = rd16(h + 16);
        if (speed == 0)
            speed = 5;
        fli_delay = (int)((speed * 1000u) / 70u);
        fli_first = 128;
    } else {                                    /* FLC: speed in ms */
        speed = rd32(h + 16);
        if (speed == 0)
            speed = 66;
        fli_delay = (int)speed;
        fli_first = (long)rd32(h + 80);
        if (fli_first < 128)
            fli_first = 128;
    }
    if (fli_delay < 1)
        fli_delay = 1;
    if (fli_delay > 5000)
        fli_delay = 5000;
    fx = (SW - fli_w) / 2;
    fy = (SH - fli_h) / 2;
    return 0;
}

/* Read and decode the next frame.  Returns 1 on success, 0 at the end. */
static int fli_next_frame(void)
{
    u8 hdr[16];
    u32 fsize, body, left, csize;
    int chunks, i, ctype;
    const u8 *p;

    for (;;) {
        if (fread(hdr, 1, 16, fli) != 16)
            return 0;
        fsize = rd32(hdr);
        if (rd16(hdr + 4) == 0xF1FA)
            break;
        if (rd16(hdr + 4) == 0xF100) {          /* FLC prefix chunk: skip it */
            if (fsize < 16 || fseek(fli, (long)(fsize - 16), SEEK_CUR) != 0)
                return 0;
            continue;
        }
        return 0;
    }
    if (fsize < 16 || fsize > MAX_FRAME)
        return 0;
    body = fsize - 16;
    if (body > fbuf_cap) {
        free(fbuf);
        fbuf = (u8 *)malloc(body + 16);
        if (!fbuf) {
            fbuf_cap = 0;
            return 0;
        }
        fbuf_cap = body;
    }
    if (body > 0 && fread(fbuf, 1, body, fli) != body)
        return 0;

    chunks = (int)rd16(hdr + 6);
    p = fbuf;
    left = body;
    for (i = 0; i < chunks; i++) {
        if (left < 6)
            break;
        csize = rd32(p);
        ctype = (int)rd16(p + 4);
        if (csize < 6 || csize > left)
            break;
        switch (ctype) {
        case 4:  do_color(p + 6, (int)(csize - 6), 0); break;
        case 7:  do_ss2(p + 6, (int)(csize - 6)); break;
        case 11: do_color(p + 6, (int)(csize - 6), 1); break;
        case 12: do_lc(p + 6, (int)(csize - 6)); break;
        case 13: do_black(); break;
        case 15: do_brun(p + 6, (int)(csize - 6)); break;
        case 16: do_copy(p + 6, (int)(csize - 6)); break;
        default: break;                         /* 18 = postage stamp, etc. */
        }
        p += csize;
        left -= csize;
    }
    return 1;
}

/* Copy what changed to the screen; update the palette if needed. */
static void show_frame(void)
{
    if (flicker)
        flush_palette(256 - rnd() % 26);        /* projector brightness wobble */
    else if (pal_dirty)
        flush_palette(256);
    pal_dirty = 0;
    if (dlo <= dhi) {
        memcpy(vgamem + dlo * SW, fb + dlo * SW, (size_t)((dhi - dlo + 1) * SW));
        dlo = SH;
        dhi = -1;
    }
}

/* ================================================================== */
/*  Intertitle cards: 5x7 font magnified, white on black, with fades   */
/* ================================================================== */

#define NFONT 91                        /* ASCII 32..122 */

static const u8 font5x7[NFONT][8] = {
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* space */
    { 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04, 0x00 }, /* '!' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '"' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '#' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '$' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '%' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '&' */
    { 0x04, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '\'' */
    { 0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02, 0x00 }, /* '(' */
    { 0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08, 0x00 }, /* ')' */
    { 0x00, 0x15, 0x0E, 0x1F, 0x0E, 0x15, 0x00, 0x00 }, /* '*' */
    { 0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00, 0x00 }, /* '+' */
    { 0x00, 0x00, 0x00, 0x00, 0x0C, 0x04, 0x08, 0x00 }, /* ',' */
    { 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00 }, /* '-' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00 }, /* '.' */
    { 0x01, 0x01, 0x02, 0x04, 0x08, 0x10, 0x10, 0x00 }, /* '/' */
    { 0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E, 0x00 }, /* '0' */
    { 0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E, 0x00 }, /* '1' */
    { 0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F, 0x00 }, /* '2' */
    { 0x1F, 0x02, 0x04, 0x02, 0x01, 0x11, 0x0E, 0x00 }, /* '3' */
    { 0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02, 0x00 }, /* '4' */
    { 0x1F, 0x10, 0x1E, 0x01, 0x01, 0x11, 0x0E, 0x00 }, /* '5' */
    { 0x06, 0x08, 0x10, 0x1E, 0x11, 0x11, 0x0E, 0x00 }, /* '6' */
    { 0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08, 0x00 }, /* '7' */
    { 0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E, 0x00 }, /* '8' */
    { 0x0E, 0x11, 0x11, 0x0F, 0x01, 0x02, 0x0C, 0x00 }, /* '9' */
    { 0x00, 0x0C, 0x0C, 0x00, 0x0C, 0x0C, 0x00, 0x00 }, /* ':' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* ';' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '<' */
    { 0x00, 0x00, 0x1F, 0x00, 0x1F, 0x00, 0x00, 0x00 }, /* '=' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '>' */
    { 0x0E, 0x11, 0x01, 0x02, 0x04, 0x00, 0x04, 0x00 }, /* '?' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '@' */
    { 0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11, 0x00 }, /* 'A' */
    { 0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E, 0x00 }, /* 'B' */
    { 0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E, 0x00 }, /* 'C' */
    { 0x1E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1E, 0x00 }, /* 'D' */
    { 0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F, 0x00 }, /* 'E' */
    { 0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10, 0x00 }, /* 'F' */
    { 0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0E, 0x00 }, /* 'G' */
    { 0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11, 0x00 }, /* 'H' */
    { 0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E, 0x00 }, /* 'I' */
    { 0x07, 0x02, 0x02, 0x02, 0x02, 0x12, 0x0C, 0x00 }, /* 'J' */
    { 0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11, 0x00 }, /* 'K' */
    { 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F, 0x00 }, /* 'L' */
    { 0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11, 0x00 }, /* 'M' */
    { 0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11, 0x00 }, /* 'N' */
    { 0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E, 0x00 }, /* 'O' */
    { 0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10, 0x00 }, /* 'P' */
    { 0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D, 0x00 }, /* 'Q' */
    { 0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11, 0x00 }, /* 'R' */
    { 0x0F, 0x10, 0x10, 0x0E, 0x01, 0x01, 0x1E, 0x00 }, /* 'S' */
    { 0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00 }, /* 'T' */
    { 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E, 0x00 }, /* 'U' */
    { 0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04, 0x00 }, /* 'V' */
    { 0x11, 0x11, 0x11, 0x15, 0x15, 0x1B, 0x11, 0x00 }, /* 'W' */
    { 0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11, 0x00 }, /* 'X' */
    { 0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04, 0x00 }, /* 'Y' */
    { 0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F, 0x00 }, /* 'Z' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '[' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '\\' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* ']' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '^' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00 }, /* '_' */
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* '`' */
    { 0x00, 0x00, 0x0E, 0x01, 0x0F, 0x11, 0x0F, 0x00 }, /* 'a' */
    { 0x10, 0x10, 0x1E, 0x11, 0x11, 0x11, 0x1E, 0x00 }, /* 'b' */
    { 0x00, 0x00, 0x0E, 0x11, 0x10, 0x11, 0x0E, 0x00 }, /* 'c' */
    { 0x01, 0x01, 0x0F, 0x11, 0x11, 0x11, 0x0F, 0x00 }, /* 'd' */
    { 0x00, 0x00, 0x0E, 0x11, 0x1F, 0x10, 0x0E, 0x00 }, /* 'e' */
    { 0x06, 0x09, 0x08, 0x1C, 0x08, 0x08, 0x08, 0x00 }, /* 'f' */
    { 0x00, 0x00, 0x0F, 0x11, 0x11, 0x0F, 0x01, 0x0E }, /* 'g' */
    { 0x10, 0x10, 0x16, 0x19, 0x11, 0x11, 0x11, 0x00 }, /* 'h' */
    { 0x04, 0x00, 0x0C, 0x04, 0x04, 0x04, 0x0E, 0x00 }, /* 'i' */
    { 0x02, 0x00, 0x06, 0x02, 0x02, 0x02, 0x12, 0x0C }, /* 'j' */
    { 0x10, 0x10, 0x12, 0x14, 0x18, 0x14, 0x12, 0x00 }, /* 'k' */
    { 0x0C, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E, 0x00 }, /* 'l' */
    { 0x00, 0x00, 0x1A, 0x15, 0x15, 0x11, 0x11, 0x00 }, /* 'm' */
    { 0x00, 0x00, 0x16, 0x19, 0x11, 0x11, 0x11, 0x00 }, /* 'n' */
    { 0x00, 0x00, 0x0E, 0x11, 0x11, 0x11, 0x0E, 0x00 }, /* 'o' */
    { 0x00, 0x00, 0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10 }, /* 'p' */
    { 0x00, 0x00, 0x0F, 0x11, 0x11, 0x0F, 0x01, 0x01 }, /* 'q' */
    { 0x00, 0x00, 0x16, 0x19, 0x10, 0x10, 0x10, 0x00 }, /* 'r' */
    { 0x00, 0x00, 0x0F, 0x10, 0x0E, 0x01, 0x1E, 0x00 }, /* 's' */
    { 0x08, 0x08, 0x1C, 0x08, 0x08, 0x09, 0x06, 0x00 }, /* 't' */
    { 0x00, 0x00, 0x11, 0x11, 0x11, 0x13, 0x0D, 0x00 }, /* 'u' */
    { 0x00, 0x00, 0x11, 0x11, 0x11, 0x0A, 0x04, 0x00 }, /* 'v' */
    { 0x00, 0x00, 0x11, 0x11, 0x15, 0x15, 0x0A, 0x00 }, /* 'w' */
    { 0x00, 0x00, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x00 }, /* 'x' */
    { 0x00, 0x00, 0x11, 0x11, 0x11, 0x0F, 0x01, 0x0E }, /* 'y' */
    { 0x00, 0x00, 0x1F, 0x02, 0x04, 0x08, 0x1F, 0x00 }, /* 'z' */
};

static u8 *draw_target = fb;            /* normally fb; the VGA memory for overlays */

static void fill_rect(int x, int y, int w, int h, int col)
{
    int j;

    if (x < 0) { w += x; x = 0; }
    if (y < 0) { h += y; y = 0; }
    if (x + w > SW) w = SW - x;
    if (y + h > SH) h = SH - y;
    if (w <= 0 || h <= 0)
        return;
    for (j = 0; j < h; j++)
        memset(draw_target + (y + j) * SW + x, col, (size_t)w);
}

static int text_width(const char *s, int scale)
{
    int n = (int)strlen(s);
    return n ? n * 6 * scale - scale : 0;
}

static void draw_text(int x, int y, int scale, const char *s, int col)
{
    int ch, row, c;

    for (; *s; s++, x += 6 * scale) {
        ch = (unsigned char)*s - 32;
        if (ch < 0 || ch >= NFONT)
            continue;
        for (row = 0; row < 8; row++)
            for (c = 0; c < 5; c++)
                if (font5x7[ch][row] & (0x10 >> c))
                    fill_rect(x + c * scale, y + row * scale, scale, scale, col);
    }
}

static void draw_text_c(int y, int scale, const char *s, int col)
{
    draw_text((SW - text_width(s, scale)) / 2, y, scale, s, col);
}

/* Card palette: entry 0 black, entry 1 the "paper" colour at 'scale'/256. */
static void card_palette(int scale)
{
    int r = 63, g = 63, b = 63;

    if (tint == 2) {
        g = 55;
        b = 39;
    }
    vga_setpalette(0, 0, 0, 0);
    vga_setpalette(1, (r * scale) >> 8, (g * scale) >> 8, (b * scale) >> 8);
}

/* time since start in ms */
static struct timeval t_start;
static u32 now_ms(void)
{
    struct timeval tv;

    gettimeofday(&tv, NULL);
    return (u32)((tv.tv_sec - t_start.tv_sec) * 1000
                 + (tv.tv_usec - t_start.tv_usec) / 1000);
}

/* ---- keyboard: svgalib gives one character at a time ---- */

enum { K_NONE = 0, K_QUIT, K_PAUSE, K_LEFT, K_RIGHT };

/* the rest of an escape sequence arrives within a few ms */
static int next_char(void)
{
    int tries, k;

    for (tries = 0; tries < 6; tries++) {
        k = vga_getkey();
        if (k > 0)
            return k;
        usleep(5000);
    }
    return 0;
}

static int get_key(void)
{
    int k = vga_getkey(), k2, k3;

    if (k <= 0)
        return K_NONE;
    if (k == 'q' || k == 'Q')
        return K_QUIT;
    if (k == ' ' || k == 'p' || k == 'P')
        return K_PAUSE;
    if (k == 27) {
        k2 = next_char();
        if (k2 != '[' && k2 != 'O')
            return K_QUIT;              /* a lone ESC */
        k3 = next_char();
        if (k3 == 'C')
            return K_RIGHT;
        if (k3 == 'D')
            return K_LEFT;
    }
    return K_NONE;                      /* anything else, incl. other escape sequences */
}

static int poll_quit(void)              /* title cards: only "quit" matters */
{
    int k;

    while ((k = get_key()) != K_NONE)
        if (k == K_QUIT)
            quit = 1;
    return quit;
}

/* Show the card in fb: fade in, hold, fade out. */
static void show_card(int fade_ms, int hold_ms)
{
    u32 t0, el;
    int scale, last = -1;

    card_palette(0);
    memcpy(vgamem, fb, SW * SH);
    t0 = now_ms();
    while (!poll_quit()) {
        el = now_ms() - t0;
        if ((int)el < fade_ms)
            scale = (int)(el * 256u / (u32)fade_ms);
        else if ((int)el < fade_ms + hold_ms)
            scale = 256;
        else if ((int)el < 2 * fade_ms + hold_ms)
            scale = 256 - (int)((el - (u32)(fade_ms + hold_ms)) * 256u / (u32)fade_ms);
        else
            break;
        if (scale != last) {
            card_palette(scale);
            last = scale;
        }
        usleep(20000);
    }
    memset(vgamem, 0, SW * SH);
    memset(fb, 0, SW * SH);
}

static void card_frame(void)            /* double ornamental border */
{
    fill_rect(6, 6, SW - 12, 2, 1);
    fill_rect(6, SH - 8, SW - 12, 2, 1);
    fill_rect(6, 6, 2, SH - 12, 1);
    fill_rect(SW - 8, 6, 2, SH - 12, 1);
    fill_rect(12, 12, SW - 24, 1, 1);
    fill_rect(12, SH - 13, SW - 24, 1, 1);
    fill_rect(12, 12, 1, SH - 24, 1);
    fill_rect(SW - 13, 12, 1, SH - 24, 1);
}

static void card_opening(const char *title)
{
    int sc = (text_width(title, 3) <= SW - 40) ? 3 : 2;

    memset(fb, 0, SW * SH);
    card_frame();
    draw_text_c(40, 3, "BOOK386 CINEMA", 1);
    draw_text_c(80, 2, "presents", 1);
    draw_text_c(108, sc, title, 1);
    draw_text_c(158, 2, "Astr0claude 2026", 1);
    show_card(700, 2600);
}

static void card_end(void)
{
    memset(fb, 0, SW * SH);
    card_frame();
    draw_text_c(78, 5, "THE END", 1);
    show_card(700, 2200);
}

/* "path/to/my_film.fli" -> "MY FILM" */
static void make_title(const char *path, char *out, int maxlen)
{
    const char *b = strrchr(path, '/');
    int n = 0;
    char c;

    b = b ? b + 1 : path;
    while (*b && n < maxlen - 1) {
        c = *b++;
        if (c == '.')
            break;
        if (c == '_' || c == '-')
            c = ' ';
        if (c >= 'a' && c <= 'z')
            c = (char)(c - 'a' + 'A');
        out[n++] = c;
    }
    out[n] = 0;
}

/* ================================================================== */
/*  Playing the film: pause, and jumps of 5 seconds                    */
/* ================================================================== */

static u32 st_decoded, st_shown, st_skipped;

static int fk;                          /* index of the next frame to decode */
static int ft0;                         /* film clock: frame n is due at ft0 + n * delay */
static int jump_frames;                 /* frames in a 5 second jump */

static void wait_until(int due)
{
    int now;

    for (;;) {
        now = (int)now_ms();
        if (now >= due || quit)
            return;
        if (due - now > 12) {           /* coarse sleep, spin the last bit */
            int ms = due - now - 10;

            usleep((ms > 100 ? 100 : ms) * 1000);       /* keep usleep() under 1 s */
        }
    }
}

/* ---- screen snapshots, so that we can go back ---- */

#define MAX_SNAPS 24                    /* ~65 KB each */

static int sn_frame[MAX_SNAPS];         /* frame after which the snapshot was taken */
static long sn_off[MAX_SNAPS];          /* file offset of the next frame */
static u8 *sn_data[MAX_SNAPS];          /* screen + palette */
static int sn_count, sn_interval, sn_disabled;

static void snap_reset(void)
{
    int i;

    for (i = 0; i < sn_count; i++)
        free(sn_data[i]);
    sn_count = 0;
    sn_interval = jump_frames;
    sn_disabled = 0;
}

/* Keep every other snapshot and double the spacing (long films). */
static void snap_thin(void)
{
    int i, n = 0;

    for (i = 0; i < sn_count; i++) {
        if (i % 2 == 0) {
            sn_frame[n] = sn_frame[i];
            sn_off[n] = sn_off[i];
            sn_data[n] = sn_data[i];
            n++;
        } else {
            free(sn_data[i]);
        }
    }
    sn_count = n;
    sn_interval *= 2;
}

/* The screen now shows frame f and the file sits at frame f+1. */
static void snap_take(int f)
{
    u8 *d;

    if (sn_disabled || f % sn_interval != 0)
        return;
    if (sn_count > 0 && f <= sn_frame[sn_count - 1])
        return;                         /* already have this one (after a rewind) */
    if (sn_count == MAX_SNAPS) {
        snap_thin();
        if (f % sn_interval != 0)
            return;
    }
    d = (u8 *)malloc(SW * SH + 768);
    if (!d) {                           /* out of memory: rewinds restart from the top */
        sn_disabled = 1;
        return;
    }
    memcpy(d, fb, SW * SH);
    memcpy(d + SW * SH, pal, 768);
    sn_frame[sn_count] = f;
    sn_off[sn_count] = ftell(fli);
    sn_data[sn_count] = d;
    sn_count++;
}

/* Make frame T the one on screen.  T may lie before or after the current frame. */
static void seek_frame(int t)
{
    int i, best = -1;

    if (t >= fli_frames)
        t = fli_frames - 1;
    if (t < 0)
        t = 0;
    if (t <= fk - 1) {                  /* going back: restart from a snapshot */
        for (i = 0; i < sn_count; i++)
            if (sn_frame[i] <= t)
                best = i;
        if (best >= 0) {
            memcpy(fb, sn_data[best], SW * SH);
            memcpy(pal, sn_data[best] + SW * SH, 768);
            fseek(fli, sn_off[best], SEEK_SET);
            fk = sn_frame[best] + 1;
        } else {                        /* back to the very beginning */
            memset(fb, 0, SW * SH);
            memset(pal, 0, sizeof(pal));
            fseek(fli, fli_first, SEEK_SET);
            fk = 0;
        }
    }
    while (fk <= t) {                   /* decode forward, without showing anything */
        if (!fli_next_frame())
            break;
        snap_take(fk);
        fk++;
    }
    dlo = 0;                            /* repaint everything */
    dhi = SH - 1;
    pal_dirty = 1;
}

/* ---- the PAUSED overlay is drawn on the screen only, never into fb ---- */

#define OV_X 4
#define OV_Y 4
#define OV_W 84
#define OV_H 20

static void overlay_on(void)
{
    int i, lum, lb = -1, ld = 1 << 30, bright = 0, dark = 0;

    for (i = 0; i < 256; i++) {         /* brightest and darkest colour of the film */
        lum = pal[i][0] * 30 + pal[i][1] * 59 + pal[i][2] * 11;
        if (lum > lb) {
            lb = lum;
            bright = i;
        }
        if (lum < ld) {
            ld = lum;
            dark = i;
        }
    }
    draw_target = vgamem;
    fill_rect(OV_X, OV_Y, OV_W, OV_H, bright);
    fill_rect(OV_X + 2, OV_Y + 2, OV_W - 4, OV_H - 4, dark);
    draw_text(OV_X + 7, OV_Y + 3, 2, "PAUSED", bright);
    draw_target = fb;
}

static void overlay_off(void)
{
    memcpy(vgamem + OV_Y * SW, fb + OV_Y * SW, OV_H * SW);
}

/* Show the screen as it is now and restart the film clock from frame fk-1. */
static void show_now(int vsync)
{
    if (vsync)
        vga_waitretrace();
    show_frame();
    ft0 = (int)now_ms() - (fk - 1) * fli_delay;
}

static void do_pause(int vsync)
{
    int key, jump, resume, tp;

    if (dlo <= dhi || pal_dirty)
        show_frame();                   /* make sure the newest frame is up */
    tp = (int)now_ms();
    music_pause();
    overlay_on();
    for (;;) {
        usleep(20000);
        jump = 0;
        resume = 0;
        while ((key = get_key()) != K_NONE) {
            if (key == K_QUIT)
                quit = 1;
            else if (key == K_PAUSE)
                resume = 1;
            else if (key == K_LEFT)
                jump--;
            else if (key == K_RIGHT)
                jump++;
        }
        if (quit)
            break;
        if (jump) {                     /* look around while paused */
            seek_frame(fk - 1 + jump * jump_frames);
            show_now(vsync);
            tp = (int)now_ms();
            overlay_on();
        }
        if (resume)
            break;
    }
    overlay_off();
    ft0 += (int)now_ms() - tp;          /* time spent paused does not count */
    music_resume();
}

/* Process everything typed since the last frame. */
static void handle_keys(int vsync)
{
    int key, jump = 0, toggle = 0;

    while ((key = get_key()) != K_NONE) {
        if (key == K_QUIT) {
            quit = 1;
            return;
        } else if (key == K_PAUSE) {
            toggle ^= 1;
        } else if (key == K_LEFT) {
            jump--;
        } else if (key == K_RIGHT) {
            jump++;
        }
    }
    if (jump) {                         /* several taps make one bigger jump */
        seek_frame(fk - 1 + jump * jump_frames);
        show_now(vsync);
    }
    if (toggle)
        do_pause(vsync);
}

static void play_film(int vsync, int loop, int delay_override)
{
    int due, now, skipped;

    if (delay_override > 0)
        fli_delay = delay_override;
    jump_frames = (5000 + fli_delay / 2) / fli_delay;
    if (jump_frames < 1)
        jump_frames = 1;
    snap_reset();
    do {
        fseek(fli, fli_first, SEEK_SET);
        memset(fb, 0, SW * SH);
        memset(vgamem, 0, SW * SH);
        memset(pal, 0, sizeof(pal));
        flush_palette(256);
        dlo = SH;
        dhi = -1;
        skipped = 0;
        fk = 0;
        ft0 = (int)now_ms();
        while (fk < fli_frames && !quit) {
            if (!fli_next_frame())
                break;
            st_decoded++;
            snap_take(fk);
            due = ft0 + fk * fli_delay;
            fk++;
            now = (int)now_ms();
            if (now > due + fli_delay && skipped < 3) {
                skipped++;              /* running late: decode but don't show */
                st_skipped++;
            } else {
                wait_until(due);
                if (vsync)
                    vga_waitretrace();
                show_frame();
                skipped = 0;
                st_shown++;
            }
            handle_keys(vsync);
        }
    } while (loop && !quit);
}

/* ================================================================== */
/*  main                                                               */
/* ================================================================== */

static void on_signal(int sig)
{
    (void)sig;
    quit = 1;
}

static void usage(void)
{
    fputs("BOOK386 PICTURE HOUSE - silent-movie FLI player with RAD piano\n"
          "usage: fliplayer -t tune.rad [options] movie.fli\n"
          "  -t tune.rad  RAD v1.0 module to play (required unless -m)\n"
          "  -p port      OPL base port in hex (default 388)\n"
          "  -T \"text\"    title for the opening card (default: file name)\n"
          "  -d ms        override the frame delay\n", stderr);
    fputs("  -g           black & white      -e  sepia      -k  projector flicker\n"
          "  -l           loop forever (no closing card)\n"
          "  -n           no title cards\n"
          "  -v           wait for vertical retrace\n"
          "  -m           mute: no music, no OPL access, no tune needed\n"
          "  -f           skip OPL chip detection\n", stderr);
    fputs("keys: SPACE pause/resume   LEFT/RIGHT back/forward 5 s   ESC or q quit\n"
          "Needs root.\n", stderr);
    exit(1);
}

/* Load the RAD module from a file. */
static int load_rad(const char *name)
{
    FILE *f = fopen(name, "rb");
    long n;
    u8 *buf;

    if (!f) {
        perror(name);
        return -1;
    }
    fseek(f, 0, SEEK_END);
    n = ftell(f);
    fseek(f, 0, SEEK_SET);
    if (n < 40 || n > 65536L) {
        fprintf(stderr, "%s: not a usable RAD module\n", name);
        fclose(f);
        return -1;
    }
    buf = (u8 *)malloc((size_t)n);
    if (!buf || fread(buf, 1, (size_t)n, f) != (size_t)n) {
        fprintf(stderr, "%s: read error\n", name);
        fclose(f);
        free(buf);
        return -1;
    }
    fclose(f);
    rd_b = buf;
    rd_len = (int)n;
    return 0;
}

int main(int argc, char **argv)
{
    extern char *optarg;
    extern int optind;
    int c, mute = 0, nodetect = 0, vsync = 0, loop = 0, cards = 1, delay = 0;
    const char *tune = NULL, *title_arg = NULL, *err;
    char title[40];
    u32 total;

    while ((c = getopt(argc, argv, "t:p:T:d:gekln" "vmfh")) != -1) {
        switch (c) {
        case 't': tune = optarg; break;
        case 'p': opl_port = (int)strtol(optarg, NULL, 16); break;
        case 'T': title_arg = optarg; break;
        case 'd': delay = atoi(optarg); break;
        case 'g': tint = 1; break;
        case 'e': tint = 2; break;
        case 'k': flicker = 1; break;
        case 'l': loop = 1; break;
        case 'n': cards = 0; break;
        case 'v': vsync = 1; break;
        case 'm': mute = 1; break;
        case 'f': nodetect = 1; break;
        default:  usage();
        }
    }
    if (optind >= argc)
        usage();

    if (!mute) {                        /* the pianist needs a tune: no built-in one */
        if (!tune) {
            fprintf(stderr, "no tune given: use -t tune.rad (or -m to play without music)\n");
            return 1;
        }
        if (load_rad(tune) < 0)
            return 1;
        err = rad_validate();
        if (err) {
            fprintf(stderr, "%s: %s\n", tune, err);
            return 1;
        }
    }
    if (fli_open(argv[optind]) < 0)
        return 1;
    if (title_arg) {
        strncpy(title, title_arg, sizeof(title) - 1);
        title[sizeof(title) - 1] = 0;
    } else {
        make_title(argv[optind], title, (int)sizeof(title));
    }
    if (title[0] == 0)
        strcpy(title, "UNTITLED");
    title[26] = 0;                      /* longest line that fits the card */

    if (!mute) {                        /* before svgalib drops root */
        if (ioperm(opl_port, 4, 1) != 0) {
            perror("ioperm (are you root?)");
            return 1;
        }
        if (!nodetect && !opl_detect()) {
            fprintf(stderr, "no OPL chip found at port 0x%X (-f forces, -m mutes)\n",
                    opl_port);
            return 1;
        }
        opl_reset();
    }

    vga_init();
    if (!vga_hasmode(G320x200x256)) {
        fprintf(stderr, "320x200x256 is not available\n");
        return 1;
    }
    vga_setmode(G320x200x256);
    vgamem = (u8 *)vga_getgraphmem();
    signal(SIGINT, on_signal);
    signal(SIGTERM, on_signal);
    gettimeofday(&t_start, NULL);

    if (!mute) {                        /* the pianist sits down */
        rad_start();
        music_on = 1;
        timer_start();
    }
    if (cards)
        card_opening(title);
    if (!quit)
        play_film(vsync, loop, delay);
    if (cards && !loop && !quit)
        card_end();
    total = now_ms();

    timer_stop();
    if (!mute) {
        opl_reset();                    /* silence */
        ioperm(opl_port, 4, 0);
    }
    vga_setmode(TEXT);
    printf("%u frames decoded, %u shown, %u dropped; %u.%03u s\n",
           st_decoded, st_shown, st_skipped, total / 1000, total % 1000);
    return 0;
}
