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

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

const char usagemsg[] =
    //2345678901234567890123456789012345678901234567890123456789012345678901234567890
    "usage:   ipbt-dump [ options ] [ -o outfile ] infile\n"
    "input:   -T          assume input files to be ttyrec format\n"
    "         -N          assume input files to be nh-recorder format\n"
    "         -R          assume input files to be raw terminal data\n"
    "output:  -H, --hex-dump   output a hex dump of each frame's terminal data\n"
    "         -x, --screen-xml output XML describing each frame's screen contents\n"
    "         -S, --script     output the terminal data, as if from script(1)\n"
    "options: -o outfile  specify name of output file\n"
    "         -w width    specify width of emulated terminal screen (default 80)\n"
    "         -h height   specify height of emulated terminal screen (default 24)\n"
    "         --utf8-linedraw    permit ISO 2022 line drawing even in UTF-8 mode\n"
    "also:    ipbt-dump --version     report version number\n"
    "         ipbt-dump --help        display this help text\n"
    "         ipbt-dump --licence     display the (MIT) licence text\n"
    ;

void pre_exit(void)
{
}

struct chr {
    enum { SINGLE, MULTIPLE, RHS } type;
    union {
        wchar_t single;
        struct {
            wchar_t *multiple;
            size_t nmultiple;
        };
    } chr;

    unsigned long attr;
    int lattr;
    truecolour tc;
};

struct inst {
    enum { SUMMARY, HEXDUMP, TYPESCRIPT, SCREEN_XML } mode;
    Terminal *term;
    struct unicode_data ucsdata;
    Conf *conf;

    int w, h;
    struct chr *chars;

    int cx, cy;

    int frameindex;

    unsigned long long totaltime;
    FILE *fp;
    const char *filename;

    TermWin tw;
};

static struct chr *char_at(struct inst *inst, int x, int y)
{
    assert(0 <= x && x < inst->w);
    assert(0 <= y && y < inst->h);
    return inst->chars + (y * inst->w + x);
}

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 nchars = attr & TATTR_COMBINING ? 1 : len;

    for (int i = 0; i < nchars; i++) {
        struct chr *chr = char_at(inst, x, y);

        if (chr->type == MULTIPLE)
            sfree(chr->chr.multiple);
        if (attr & TATTR_COMBINING) {
            chr->type = MULTIPLE;
            chr->chr.multiple = snewn(len, wchar_t);
            memcpy(chr->chr.multiple, text, len * sizeof(wchar_t));
            chr->chr.nmultiple = len;
        } else {
            chr->type = SINGLE;
            chr->chr.single = text[i];
        }

        chr->attr = attr;
        chr->lattr = lattr;
        chr->tc = tc;

        x++;

        if (attr & ATTR_WIDE) {
            struct chr *chr = char_at(inst, x, y);

            if (chr->type == MULTIPLE)
                sfree(chr->chr.multiple);
            chr->type = RHS;

            chr->attr = attr;
            chr->lattr = lattr;
            chr->tc = tc;

            x++;
        }
    }

    /* Track whether we've seen the cursor in this repaint */
    if (attr & (TATTR_ACTCURS | TATTR_PASCURS | TATTR_RIGHTCURS)) {
        inst->cx = x;
        inst->cy = y;
    }
}

static void write_osc4_colour(FILE *fp, unsigned colour)
{
    static const char *const colour_names[8] = {
        "black", "red", "green", "yellow",
        "blue", "magenta", "cyan", "white",
    };

    if (colour < 8) {
        fputs(colour_names[colour], fp);
    } else if (colour < 16) {
        fprintf(fp, "bold-%s", colour_names[colour - 8]);
    } else if (colour < 16 + 6*6*6) {
        unsigned v = colour - 16;
        unsigned r = v / 36, g = v / 6 % 6, b = v % 6;
        fprintf(fp, "xterm-rgb-%c%c%c", '0' + r, '0' + g, '0' + b);
    } else {
        unsigned v = colour - 16 - 6*6*6 + 1;
        fprintf(fp, "xterm-grey-%u", v);
    }
}

static void store_frame(void *vctx, unsigned long long delay, long fileoff,
                        strbuf *termdata)
{
    struct inst *inst = (struct inst *)vctx;
    int frameindex = inst->frameindex++;

    switch (inst->mode) {
      case SUMMARY:
      case HEXDUMP:
        inst->totaltime += delay;
        fprintf(inst->fp, "%s:%d:%llu.%06llu:offset 0x%lx len 0x%zx\n",
                inst->filename, frameindex,
                inst->totaltime / 1000000, inst->totaltime % 1000000,
                fileoff, termdata->len);
        if (inst->mode == HEXDUMP) {
            const size_t w = 16;
            for (size_t offset = 0; offset < termdata->len; offset += w) {
                size_t thislen = termdata->len - offset;
                if (thislen > w)
                    thislen = w;

                fprintf(inst->fp, "%8.6zx ", offset);
                for (size_t i = 0; i < thislen; i++)
                    fprintf(inst->fp, " %02x", termdata->u[offset + i]);
                fprintf(inst->fp, "%*s", (int)(3 * (w - thislen) + 1), "");
                for (size_t i = 0; i < thislen; i++) {
                    unsigned char u = termdata->s[offset + i];
                    if (u < 0x20 || u >= 0x7f)
                        u = '.';
                    fputc(u, inst->fp);
                }
                fputc('\n', inst->fp);
            }
        }
        break;

      case TYPESCRIPT:
        fwrite(termdata->s, 1, termdata->len, inst->fp);
        break;

      case SCREEN_XML:
        /*
         * Force the terminal to refresh, so that our data is up to
         * date.
         */
        inst->cx = inst->cy = -1;
        term_invalidate(inst->term);
        term_update(inst->term);

        fprintf(inst->fp, "  <frame index=\"%d\" fileoffset=\"%ld\">\n",
                frameindex, fileoff);

        for (int y = 0; y < inst->h; y++) {
            fprintf(inst->fp, "    <row");
            switch (char_at(inst, 0, y)->lattr & LATTR_MODE) {
              case LATTR_NORM:
                break;
              case LATTR_WIDE:
                fprintf(inst->fp, " type=\"wide\"");
                break;
              case LATTR_TOP:
                fprintf(inst->fp, " type=\"top\"");
                break;
              case LATTR_BOT:
                fprintf(inst->fp, " type=\"bot\"");
                break;
            }
            fprintf(inst->fp, ">");
            int x = 0;
            while (x < inst->w) {
                struct chr *chr0 = char_at(inst, x, y);
                int xend = x;
                while (xend < inst->w) {
                    struct chr *chr = char_at(inst, xend, y);
                    if (chr->attr != chr0->attr || chr->type != chr0->type ||
                        !truecolour_equal(chr->tc, chr0->tc)) {
                        break;
                    }

                    xend++;
                    if (chr0->attr & ATTR_WIDE) {
                        assert(xend < inst->w);
                        struct chr *chr_rhs = char_at(inst, xend, y);
                        assert(chr_rhs->type == RHS);
                        xend++;
                    }

                    if (chr0->type == MULTIPLE)
                        break; /* combined characters get their own cell */
                }

                fprintf(inst->fp, "<t");
                if (chr0->type == MULTIPLE)
                    fprintf(inst->fp, " combined=\"1\"");
                if (chr0->attr & ATTR_WIDE)
                    fprintf(inst->fp, " wide=\"1\"");
                if (chr0->attr & ATTR_BOLD)
                    fprintf(inst->fp, " bold=\"1\"");
                if (chr0->attr & ATTR_UNDER)
                    fprintf(inst->fp, " underline=\"1\"");
                if (chr0->attr & ATTR_REVERSE)
                    fprintf(inst->fp, " reverse=\"1\"");
                if (chr0->attr & ATTR_BLINK)
                    fprintf(inst->fp, " blink=\"1\"");
                if (chr0->attr & ATTR_DIM)
                    fprintf(inst->fp, " dim=\"1\"");
                if (chr0->attr & ATTR_STRIKE)
                    fprintf(inst->fp, " strike=\"1\"");
                if (chr0->tc.fg.enabled) {
                    fprintf(inst->fp, " fg=\"#%02x%02x%02x\"",
                            chr0->tc.fg.r, chr0->tc.fg.g, chr0->tc.fg.b);
                } else if ((chr0->attr & ATTR_FGMASK) != ATTR_DEFFG) {
                    fprintf(inst->fp, " fg=\"");
                    write_osc4_colour(
                        inst->fp, (chr0->attr & ATTR_FGMASK) >> ATTR_FGSHIFT);
                    fprintf(inst->fp, "\"");
                }
                if (chr0->tc.bg.enabled) {
                    fprintf(inst->fp, " bg=\"#%02x%02x%02x\"",
                            chr0->tc.bg.r, chr0->tc.bg.g, chr0->tc.bg.b);
                } else if ((chr0->attr & ATTR_BGMASK) != ATTR_DEFBG) {
                    fprintf(inst->fp, " bg=\"");
                    write_osc4_colour(
                        inst->fp, (chr0->attr & ATTR_BGMASK) >> ATTR_BGSHIFT);
                    fprintf(inst->fp, "\"");
                }
                fprintf(inst->fp, ">");
                for (; x < xend; x++) {
                    struct chr *chr = char_at(inst, x, y);
                    const wchar_t *wcs;
                    size_t nwcs;
                    if (chr->type == MULTIPLE) {
                        wcs = chr->chr.multiple;
                        nwcs = chr->chr.nmultiple;
                    } else if (chr->type == SINGLE) {
                        wcs = &chr->chr.single;
                        nwcs = 1;
                    } else {
                        continue;          /* RHS */
                    }

                    stdio_sink ss[1];
                    stdio_sink_init(ss, inst->fp);
                    for (size_t i = 0; i < nwcs; i++)
                        put_utf8_char(ss, wcs[i]);
                }
                fprintf(inst->fp, "</t>");
            }
            fprintf(inst->fp, "  </row>\n");
        }

        if (inst->cx == -1) {
            fprintf(inst->fp,
                    "    <cursor state=\"hidden\"/>\n");
        } else {
            fprintf(inst->fp,
                    "    <cursor state=\"visible\" x=\"%d\" y=\"%d\"/>\n",
                    inst->cx, inst->cy);
        }

        fprintf(inst->fp, "  </frame>\n");
        break;
    }
}

const struct TermWinVtable ipbt_dump_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,
};

int main(int argc, char **argv)
{
    struct inst inst[1];
    enum { OPT_UTF8_LINEDRAW = 256, };
    const char *filename = NULL, *outfilename = "-";
    const char *charset = "UTF-8";
    int deftype = NODEFAULT;

    memset(inst, 0, sizeof(*inst));
    inst->conf = conf_new();
    inst->mode = SUMMARY;
    do_defaults(NULL, inst->conf);

    bool doing_opts = true;
    int iw = 80;
    int ih = 24;
    while (--argc > 0) {
        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, "output"))
                    optchr = 'o';
                else if (!strcmp(p, "ttyrec"))
                    optchr = 'T';
                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, "utf8-linedraw"))
                    optchr = OPT_UTF8_LINEDRAW;
                else if (!strcmp(p, "screenxml") ||
                         !strcmp(p, "screen-xml") ||
                         !strcmp(p, "screen_xml"))
                    optchr = 'x';
                else if (!strcmp(p, "hexdump") ||
                         !strcmp(p, "hex-dump") ||
                         !strcmp(p, "hex_dump") ||
                         !strcmp(p, "hexdumps") ||
                         !strcmp(p, "hex-dumps") ||
                         !strcmp(p, "hex_dumps"))
                    optchr = 'H';
                else if (!strcmp(p, "script") ||
                         !strcmp(p, "typescript"))
                    optchr = 'S';
                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 'o':
                /*
                 * these options all require an argument
                 */
                if (!optval) {
                    if (--argc)
                        optval = *++argv;
                    else {
                        modalfatalbox("option '%s' expects an argument",
                                      optstr);
                    }
                }
                break;

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

            switch (optchr) {
              case 'w':
                assert(optval);
                iw = atoi(optval);
                if (iw <= 0) {
                    modalfatalbox("argument to '%s' must be positive",
                                  optstr);
                }
                break;
              case 'h':
                assert(optval);
                ih = atoi(optval);
                if (ih <= 0) {
                    modalfatalbox("argument to '%s' must be positive",
                                  optstr);
                }
                break;
              case 'o':
                assert(optval);
                outfilename = optval;
                break;
              case 'T':
                deftype = TTYREC;
                break;
              case 'N':
                deftype = NHRECORDER;
                break;
              case 'R':
                deftype = RAWBYTES;
                break;
              case 'x':
                inst->mode = SCREEN_XML;
                break;
              case 'H':
                inst->mode = HEXDUMP;
                break;
              case 'S':
                inst->mode = TYPESCRIPT;
                break;
              case OPT_UTF8_LINEDRAW:
                conf_set_bool(inst->conf, CONF_utf8linedraw, true);
                break;
              default:
                modalfatalbox("unrecognised option '%s'", optstr);
            }
        } else {
            if (!filename) {
                filename = p;
            } else {
                modalfatalbox("extra filename argument '%s'", p);
            }
        }
    }

    if (!filename) {
        modalfatalbox("no filename provided");
    }

    if (!strcmp(outfilename, "-")) {
        inst->fp = stdout;
    } else {
        inst->fp = fopen(outfilename, "wb");
        if (!inst->fp)
            modalfatalbox("unable to open output file '%s': %s",
                          outfilename, strerror(errno));
    }

    conf_set_str(inst->conf, CONF_line_codepage, charset);
    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));

    if (inst->mode == SCREEN_XML) {
        inst->tw.vt = &ipbt_dump_termwin_vtable;
        inst->term = term_init(inst->conf, &inst->ucsdata, &inst->tw);
        inst->term->ldisc = NULL;
        term_size(inst->term, ih, iw, 0);
        term_set_trust_status(inst->term, false);

        fprintf(inst->fp, "<?xml version=\"1.0\"?>\n"
                "<ttyrecording xmlns=\"https://www.chiark.greenend.org.uk/~sgtatham/ipbt/ttyrecording\">\n");
    }

    inst->w = iw;
    inst->h = ih;
    inst->chars = snewn(inst->w * inst->h, struct chr);
    memset(inst->chars, 0, inst->w * inst->h * sizeof(struct chr));

    inst->frameindex = 0;
    inst->filename = filename;
    read_tty_recording(
        filename, deftype, inst->term, NULL, true, store_frame, inst);

    if (inst->mode == SCREEN_XML) {
        fprintf(inst->fp, "</ttyrecording>\n");
    }

    if (inst->fp != stdout)
        fclose(inst->fp);

    for (size_t i = 0, n = (size_t)inst->w * inst->h; i < n; i++) {
        if (inst->chars[i].type == MULTIPLE)
            sfree(inst->chars[i].chr.multiple);
    }
    sfree(inst->chars);
    if (inst->term)
        term_free(inst->term);
    conf_free(inst->conf);
}
