#include <stdio.h>
#include <string.h>
#include <errno.h>

#include "putty.h"
#include "ipbt.h"

unsigned long long read_tty_recording(
    const char *name, int deftype, Terminal *term, FILE *reportfp,
    bool first_file,
    void (*store_frame)(void *ctx, unsigned long long delay, long fileoff,
                        strbuf *termdata),
    void *store_frame_ctx)
{
    FILE *fp;
    unsigned char hdrbuf[12];
    strbuf *termdata = strbuf_new();
    int ret, nframes = 0;
    unsigned long long timestamp, oldtimestamp = 0LL;
    unsigned long long frametime, totaltime = 0LL;
    int typemask, type, nhrstate;
    long fileoff, filelen;

    fp = fopen(name, "rb");
    if (!fp)
        modalfatalbox("unable to open '%s': %s\n", name, strerror(errno));

    if (deftype == NODEFAULT) {
        /*
         * First pass: try to identify the file type. We do
         * this by looking through the entire file to see which
         * formats it satisfies.
         */
        typemask = 0;
        oldtimestamp = 0;
        fseek(fp, 0, SEEK_END);
        filelen = ftell(fp);
        rewind(fp);
        while (1) {
            /*
             * Try to parse the file as a ttyrec.
             */
            long offset, newoffset;

            ret = fread(hdrbuf, 1, 12, fp);
            if (ret == 0) {
                typemask |= 1 << TTYREC;
                break;
            } else if (ret != 12) {
                break;
            }

            timestamp = GET_32BIT_LSB_FIRST(hdrbuf);
            timestamp = timestamp*1000000 + GET_32BIT_LSB_FIRST(hdrbuf+4);
            if (timestamp < oldtimestamp)
                break;
            oldtimestamp = timestamp;

            unsigned termdatalen = GET_32BIT_LSB_FIRST(hdrbuf + 8);
            offset = ftell(fp);
            ret = fseek(fp, termdatalen, SEEK_CUR);
            if (ret < 0)
                break;
            newoffset = ftell(fp);
            if (newoffset != offset + termdatalen ||
                newoffset < 0 || newoffset > filelen)
                break;
        }
        rewind(fp);

        oldtimestamp = timestamp = 0;
        nhrstate = 0;
        while (1) {
            /*
             * Try to parse the file as a nh-recording.
             */
            int i;

            unsigned char nhrbuf[4096];

            ret = fread(nhrbuf, 1, 4096, fp);
            if (ret == 0) {
                if (nhrstate == 0 || nhrstate == 1)
                    typemask |= 1 << NHRECORDER;
                break;
            }
            for (i = 0; i < ret; i++) {
                switch (nhrstate) {
                  case 0:
                    if (nhrbuf[i] == 0) {
                        nhrstate = 1;
                        timestamp = 0;
                    }
                    break;
                  case 1:
                    timestamp |= (unsigned)nhrbuf[i];
                    nhrstate = 2;
                    break;
                  case 2:
                    timestamp |= (unsigned)nhrbuf[i] << 8;
                    nhrstate = 3;
                    break;
                  case 3:
                    timestamp |= (unsigned)nhrbuf[i] << 16;
                    nhrstate = 4;
                    break;
                  case 4:
                    timestamp |= (unsigned)nhrbuf[i] << 24;
                    nhrstate = 0;
                    if (oldtimestamp > timestamp)
                        goto done_nhr_loop;   /* goto as multi-level break */
                    oldtimestamp = timestamp;
                    break;
                }
            }
        } done_nhr_loop:
        rewind(fp);

        if (!typemask) {
            /*
             * No file type matched.
             */
            modalfatalbox("'%s' is not a valid input file\n", name);
        } else {
            for (type = 0; type < NTYPES_AUTODETECT; type++)
                if (typemask & (1 << type))
                    break;
            assert(type < NTYPES);

            if (typemask & (typemask-1)) {   /* test for power of two */
                /*
                 * More than one file type matched.
                 */
                printf("%s matched more than one file type, assuming %s\n",
                       name, typenames[type]);
            }
        }
    } else
        type = deftype;

    if (term)
        term_pwron(term, true);

    if (reportfp) {
        fprintf(reportfp, "Reading %s (%s) ... ", name, typenames[type]);
        fflush(reportfp);
    }

    switch (type) {
      case TTYREC:
        while (1) {
            /*
             * A ttyrec file consists of a sequence of frame records.
             * Each one begins with a 12-byte header consisting of
             * three little-endian 32-bit words: data length, seconds,
             * microseconds. The data length word specifies the number
             * of bytes of raw terminal data that follow (and does not
             * count the header).
             */
            ret = fread(hdrbuf, 1, 12, fp);
            fileoff = ftell(fp);
            if (ret == 0) {
                break;
            } else if (ret < 0) {
                modalfatalbox("error reading '%s': %s\n",
                              name, strerror(errno));
            } else if (ret < 12) {
                modalfatalbox("unexpected EOF reading '%s'\n", name);
            }

            unsigned termdatalen = GET_32BIT_LSB_FIRST(hdrbuf + 8);
            strbuf_clear(termdata);
            strbuf_append(termdata, termdatalen);

            ret = fread(termdata->s, 1, termdatalen, fp);
            if (ret == 0) {
                break;
            } else if (ret < 0) {
                modalfatalbox("error reading '%s': %s\n",
                              name, strerror(errno));
            } else if (ret < termdatalen) {
                modalfatalbox("unexpected EOF reading '%s'\n", name);
            }

            timestamp = GET_32BIT_LSB_FIRST(hdrbuf);
            timestamp = timestamp*1000000 + GET_32BIT_LSB_FIRST(hdrbuf+4);
            if (nframes == 0)
                frametime = (first_file ? 0 : 1000000);
            else
                frametime = timestamp - oldtimestamp;
            oldtimestamp = timestamp;

            if (term)
                term_data(term, termdata->s, termdata->len);
            store_frame(store_frame_ctx, frametime, fileoff, termdata);

            nframes++;
            totaltime += frametime;
        }
        break;
      case NHRECORDER:
        fileoff = 0;
        frametime = (first_file ? 0 : 1000000);
        nhrstate = 0;
        oldtimestamp = 0;
        timestamp = 0;
        while (1) {
            /*
             * A nh-recorder file consists of an interleaving of two
             * kinds of thing: a raw terminal data byte which must be
             * non-NUL, and a timestamp consisting of a NUL followed
             * by a 4-byte little-endian timestamp in centiseconds.
             * (Hence, nh-recorder can't represent a NUL byte in the
             * terminal data at all.)
             */
            int i;
            long thisoff = ftell(fp);

            unsigned char nhrbuf[4096];

            ret = fread(nhrbuf, 1, 4096, fp);
            if (ret == 0)
                break;

            for (i = 0; i < ret; i++) {
                switch (nhrstate) {
                  case 0:
                    if (nhrbuf[i] == 0) {
                        nhrstate = 1;
                        timestamp = 0;
                    } else {
                        put_byte(termdata, nhrbuf[i]);
                    }
                    break;
                  case 1:
                    timestamp |= (unsigned)nhrbuf[i];
                    nhrstate = 2;
                    break;
                  case 2:
                    timestamp |= (unsigned)nhrbuf[i] << 8;
                    nhrstate = 3;
                    break;
                  case 3:
                    timestamp |= (unsigned)nhrbuf[i] << 16;
                    nhrstate = 4;
                    break;
                  case 4:
                    timestamp |= (unsigned)nhrbuf[i] << 24;
                    nhrstate = 0;

                    if (term)
                        term_data(term, termdata->s, termdata->len);
                    store_frame(store_frame_ctx, frametime, fileoff, termdata);
                    strbuf_clear(termdata);
                    nframes++;
                    totaltime += frametime;

                    frametime = (timestamp - oldtimestamp) * 10000;
                    oldtimestamp = timestamp;

                    fileoff = thisoff + i + 1;
                    break;
                }
            }
        }
        break;
      case RAWBYTES:
        while (1) {
            fileoff = ftell(fp);
            int c = fgetc(fp);
            if (c == EOF)
                break;

            char cc = c;
            strbuf_clear(termdata);
            put_byte(termdata, cc);
            if (term)
                term_data(term, &cc, 1);
            store_frame(store_frame_ctx, 10000 /* 10ms */, fileoff, termdata);
            nframes++;
        }
        break;
    }

    strbuf_free(termdata);

    if (reportfp)
        fprintf(reportfp, "%d frames\n", nframes);

    fileoff = ftell(fp);
    fclose(fp);
    return fileoff;
}
