/* $OpenBSD$ */

/*
* Copyright (c) 2019 Nicholas Marriott <[email protected]>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#include <sys/types.h>

#include <stdlib.h>
#include <string.h>

#include "tmux.h"

#define SIXEL_WIDTH_LIMIT 10000
#define SIXEL_HEIGHT_LIMIT 10000

struct sixel_line {
       u_int            x;
       uint16_t        *data;
};

struct sixel_image {
       u_int                    x;
       u_int                    y;
       u_int                    xpixel;
       u_int                    ypixel;

       u_int                   *colours;
       u_int                    ncolours;

       u_int                    dx;
       u_int                    dy;
       u_int                    dc;

       struct sixel_line       *lines;
};

static int
sixel_parse_expand_lines(struct sixel_image *si, u_int y)
{
       if (y <= si->y)
               return (0);
       if (y > SIXEL_HEIGHT_LIMIT)
               return (1);
       si->lines = xrecallocarray(si->lines, si->y, y, sizeof *si->lines);
       si->y = y;
       return (0);
}

static int
sixel_parse_expand_line(struct sixel_image *si, struct sixel_line *sl, u_int x)
{
       if (x <= sl->x)
               return (0);
       if (x > SIXEL_WIDTH_LIMIT)
               return (1);
       if (x > si->x)
               si->x = x;
       sl->data = xrecallocarray(sl->data, sl->x, si->x, sizeof *sl->data);
       sl->x = si->x;
       return (0);
}

static u_int
sixel_get_pixel(struct sixel_image *si, u_int x, u_int y)
{
       struct sixel_line       *sl;

       if (y >= si->y)
               return (0);
       sl = &si->lines[y];
       if (x >= sl->x)
               return (0);
       return (sl->data[x]);
}

static int
sixel_set_pixel(struct sixel_image *si, u_int x, u_int y, u_int c)
{
       struct sixel_line       *sl;

       if (sixel_parse_expand_lines(si, y + 1) != 0)
               return (1);
       sl = &si->lines[y];
       if (sixel_parse_expand_line(si, sl, x + 1) != 0)
               return (1);
       sl->data[x] = c;
       return (0);
}

static int
sixel_parse_write(struct sixel_image *si, u_int ch)
{
       struct sixel_line       *sl;
       u_int                    i;

       if (sixel_parse_expand_lines(si, si->dy + 6) != 0)
               return (1);
       sl = &si->lines[si->dy];

       for (i = 0; i < 6; i++) {
               if (sixel_parse_expand_line(si, sl, si->dx + 1) != 0)
                       return (1);
               if (ch & (1 << i))
                       sl->data[si->dx] = si->dc;
               sl++;
       }
       return (0);
}

static const char *
sixel_parse_attributes(struct sixel_image *si, const char *cp, const char *end)
{
       const char      *last;
       char            *endptr;
       u_int            x, y;

       last = cp;
       while (last != end) {
               if (*last != ';' && (*last < '0' || *last > '9'))
                       break;
               last++;
       }
       strtoul(cp, &endptr, 10);
       if (endptr == last || *endptr != ';')
               return (last);
       strtoul(endptr + 1, &endptr, 10);
       if (endptr == last)
               return (last);
       if (*endptr != ';') {
               log_debug("%s: missing ;", __func__);
               return (NULL);
       }

       x = strtoul(endptr + 1, &endptr, 10);
       if (endptr == last || *endptr != ';') {
               log_debug("%s: missing ;", __func__);
               return (NULL);
       }
       if (x > SIXEL_WIDTH_LIMIT) {
               log_debug("%s: image is too wide", __func__);
               return (NULL);
       }
       y = strtoul(endptr + 1, &endptr, 10);
       if (endptr != last) {
               log_debug("%s: extra ;", __func__);
               return (NULL);
       }
       if (y > SIXEL_HEIGHT_LIMIT) {
               log_debug("%s: image is too tall", __func__);
               return (NULL);
       }

       si->x = x;
       sixel_parse_expand_lines(si, y);

       return (last);
}

static const char *
sixel_parse_colour(struct sixel_image *si, const char *cp, const char *end)
{
       const char      *last;
       char            *endptr;
       u_int            c, type, r, g, b;

       last = cp;
       while (last != end) {
               if (*last != ';' && (*last < '0' || *last > '9'))
                       break;
               last++;
       }

       c = strtoul(cp, &endptr, 10);
       if (c > SIXEL_COLOUR_REGISTERS) {
               log_debug("%s: too many colours", __func__);
               return (NULL);
       }
       si->dc = c + 1;
       if (endptr == last || *endptr != ';')
               return (last);

       type = strtoul(endptr + 1, &endptr, 10);
       if (endptr == last || *endptr != ';') {
               log_debug("%s: missing ;", __func__);
               return (NULL);
       }
       r = strtoul(endptr + 1, &endptr, 10);
       if (endptr == last || *endptr != ';') {
               log_debug("%s: missing ;", __func__);
               return (NULL);
       }
       g = strtoul(endptr + 1, &endptr, 10);
       if (endptr == last || *endptr != ';') {
               log_debug("%s: missing ;", __func__);
               return (NULL);
       }
       b = strtoul(endptr + 1, &endptr, 10);
       if (endptr != last) {
               log_debug("%s: missing ;", __func__);
               return (NULL);
       }

       if (type != 1 && type != 2) {
               log_debug("%s: invalid type %d", __func__, type);
               return (NULL);
       }
       if (c + 1 > si->ncolours) {
               si->colours = xrecallocarray(si->colours, si->ncolours, c + 1,
                   sizeof *si->colours);
               si->ncolours = c + 1;
       }
       si->colours[c] = (type << 24) | (r << 16) | (g << 8) | b;
       return (last);
}

static const char *
sixel_parse_repeat(struct sixel_image *si, const char *cp, const char *end)
{
       const char      *last;
       char             tmp[32], ch;
       u_int            n = 0, i;
       const char      *errstr = NULL;

       last = cp;
       while (last != end) {
               if (*last < '0' || *last > '9')
                       break;
               tmp[n++] = *last++;
               if (n == (sizeof tmp) - 1) {
                       log_debug("%s: repeat not terminated", __func__);
                       return (NULL);
               }
       }
       if (n == 0 || last == end) {
               log_debug("%s: repeat not terminated", __func__);
               return (NULL);
       }
       tmp[n] = '\0';

       n = strtonum(tmp, 1, SIXEL_WIDTH_LIMIT, &errstr);
       if (n == 0 || errstr != NULL) {
               log_debug("%s: repeat too wide", __func__);
               return (NULL);
       }

       ch = (*last++) - 0x3f;
       for (i = 0; i < n; i++) {
               if (sixel_parse_write(si, ch) != 0) {
                       log_debug("%s: width limit reached", __func__);
                       return (NULL);
               }
               si->dx++;
       }
       return (last);
}

struct sixel_image *
sixel_parse(const char *buf, size_t len, u_int xpixel, u_int ypixel)
{
       struct sixel_image      *si;
       const char              *cp = buf, *end = buf + len;
       char                     ch;

       if (len == 0 || len == 1 || *cp++ != 'q') {
               log_debug("%s: empty image", __func__);
               return (NULL);
       }

       si = xcalloc (1, sizeof *si);
       si->xpixel = xpixel;
       si->ypixel = ypixel;

       while (cp != end) {
               ch = *cp++;
               switch (ch) {
               case '"':
                       cp = sixel_parse_attributes(si, cp, end);
                       if (cp == NULL)
                               goto bad;
                       break;
               case '#':
                       cp = sixel_parse_colour(si, cp, end);
                       if (cp == NULL)
                               goto bad;
                       break;
               case '!':
                       cp = sixel_parse_repeat(si, cp, end);
                       if (cp == NULL)
                               goto bad;
                       break;
               case '-':
                       si->dx = 0;
                       si->dy += 6;
                       break;
               case '$':
                       si->dx = 0;
                       break;
               default:
                       if (ch < 0x20)
                               break;
                       if (ch < 0x3f || ch > 0x7e)
                               goto bad;
                       if (sixel_parse_write(si, ch - 0x3f) != 0) {
                               log_debug("%s: width limit reached", __func__);
                               goto bad;
                       }
                       si->dx++;
                       break;
               }
       }

       if (si->x == 0 || si->y == 0)
               goto bad;
       return (si);

bad:
       free(si);
       return (NULL);
}

void
sixel_free(struct sixel_image *si)
{
       u_int   y;

       for (y = 0; y < si->y; y++)
               free(si->lines[y].data);
       free(si->lines);

       free(si->colours);
       free(si);
}

void
sixel_log(struct sixel_image *si)
{
       struct sixel_line       *sl;
       char                     s[SIXEL_WIDTH_LIMIT + 1];
       u_int                    i, x, y, cx, cy;

       sixel_size_in_cells(si, &cx, &cy);
       log_debug("%s: image %ux%u (%ux%u)", __func__, si->x, si->y, cx, cy);
       for (i = 0; i < si->ncolours; i++)
               log_debug("%s: colour %u is %07x", __func__, i, si->colours[i]);
       for (y = 0; y < si->y; y++) {
               sl = &si->lines[y];
               for (x = 0; x < si->x; x++) {
                       if (x >= sl->x)
                               s[x] = '_';
                       else if (sl->data[x] != 0)
                               s[x] = '0' + (sl->data[x] - 1) % 10;
                       else
                               s[x] = '.';
                       }
               s[x] = '\0';
               log_debug("%s: %4u: %s", __func__, y, s);
       }
}

void
sixel_size_in_cells(struct sixel_image *si, u_int *x, u_int *y)
{
       if ((si->x % si->xpixel) == 0)
               *x = (si->x / si->xpixel);
       else
               *x = 1 + (si->x / si->xpixel);
       if ((si->y % si->ypixel) == 0)
               *y = (si->y / si->ypixel);
       else
               *y = 1 + (si->y / si->ypixel);
}

struct sixel_image *
sixel_scale(struct sixel_image *si, u_int xpixel, u_int ypixel, u_int ox,
   u_int oy, u_int sx, u_int sy, int colours)
{
       struct sixel_image      *new;
       u_int                    cx, cy, pox, poy, psx, psy, tsx, tsy, px, py;
       u_int                    x, y, i;

       /*
        * We want to get the section of the image at ox,oy in image cells and
        * map it onto the same size in terminal cells, remembering that we
        * can only draw vertical sections of six pixels.
        */

       sixel_size_in_cells(si, &cx, &cy);
       if (ox >= cx)
               return (NULL);
       if (oy >= cy)
               return (NULL);
       if (ox + sx >= cx)
               sx = cx - ox;
       if (oy + sy >= cy)
               sy = cy - oy;

       if (xpixel == 0)
               xpixel = si->xpixel;
       if (ypixel == 0)
               ypixel = si->ypixel;

       pox = ox * si->xpixel;
       poy = oy * si->ypixel;
       psx = sx * si->xpixel;
       psy = sy * si->ypixel;

       tsx = sx * xpixel;
       tsy = ((sy * ypixel) / 6) * 6;

       new = xcalloc (1, sizeof *si);
       new->xpixel = xpixel;
       new->ypixel = ypixel;

       for (y = 0; y < tsy; y++) {
               py = poy + ((double)y * psy / tsy);
               for (x = 0; x < tsx; x++) {
                       px = pox + ((double)x * psx / tsx);
                       sixel_set_pixel(new, x, y, sixel_get_pixel(si, px, py));
               }
       }

       if (colours) {
               new->colours = xmalloc(si->ncolours * sizeof *new->colours);
               for (i = 0; i < si->ncolours; i++)
                       new->colours[i] = si->colours[i];
               new->ncolours = si->ncolours;
       }
       return (new);
}

static void
sixel_print_add(char **buf, size_t *len, size_t *used, const char *s,
   size_t slen)
{
       if (*used + slen >= *len + 1) {
               (*len) *= 2;
               *buf = xrealloc(*buf, *len);
       }
       memcpy(*buf + *used, s, slen);
       (*used) += slen;
}

static void
sixel_print_repeat(char **buf, size_t *len, size_t *used, u_int count, char ch)
{
       char    tmp[16];
       size_t  tmplen;

       if (count == 1)
               sixel_print_add(buf, len, used, &ch, 1);
       else if (count == 2) {
               sixel_print_add(buf, len, used, &ch, 1);
               sixel_print_add(buf, len, used, &ch, 1);
       } else if (count == 3) {
               sixel_print_add(buf, len, used, &ch, 1);
               sixel_print_add(buf, len, used, &ch, 1);
               sixel_print_add(buf, len, used, &ch, 1);
       } else if (count != 0) {
               tmplen = xsnprintf(tmp, sizeof tmp, "!%u%c", count, ch);
               sixel_print_add(buf, len, used, tmp, tmplen);
       }
}

char *
sixel_print(struct sixel_image *si, struct sixel_image *map, size_t *size)
{
       char                    *buf, tmp[64], *contains, data = 0, last = 0;
       size_t                   len, used = 0, tmplen;
       u_int                   *colours, ncolours, i, c, x, y, count;
       struct sixel_line       *sl;

       if (map != NULL) {
               colours = map->colours;
               ncolours = map->ncolours;
       } else {
               colours = si->colours;
               ncolours = si->ncolours;
       }

       if (ncolours == 0)
               return (NULL);
       contains = xcalloc(1, ncolours);

       len = 8192;
       buf = xmalloc(len);

       sixel_print_add(&buf, &len, &used, "\033Pq", 3);

       tmplen = xsnprintf(tmp, sizeof tmp, "\"1;1;%u;%u", si->x, si->y);
       sixel_print_add(&buf, &len, &used, tmp, tmplen);

       for (i = 0; i < ncolours; i++) {
               c = colours[i];
               tmplen = xsnprintf(tmp, sizeof tmp, "#%u;%u;%u;%u;%u",
                   i, c >> 24, (c >> 16) & 0xff, (c >> 8) & 0xff, c & 0xff);
               sixel_print_add(&buf, &len, &used, tmp, tmplen);
       }

       for (y = 0; y < si->y; y += 6) {
               memset(contains, 0, ncolours);
               for (x = 0; x < si->x; x++) {
                       for (i = 0; i < 6; i++) {
                               if (y + i >= si->y)
                                       break;
                               sl = &si->lines[y + i];
                               if (x < sl->x && sl->data[x] != 0)
                                       contains[sl->data[x] - 1] = 1;
                       }
               }

               for (c = 0; c < ncolours; c++) {
                       if (!contains[c])
                               continue;
                       tmplen = xsnprintf(tmp, sizeof tmp, "#%u", c);
                       sixel_print_add(&buf, &len, &used, tmp, tmplen);

                       count = 0;
                       for (x = 0; x < si->x; x++) {
                               data = 0;
                               for (i = 0; i < 6; i++) {
                                       if (y + i >= si->y)
                                               break;
                                       sl = &si->lines[y + i];
                                       if (x < sl->x && sl->data[x] == c + 1)
                                               data |= (1 << i);
                               }
                               data += 0x3f;
                               if (data != last) {
                                       sixel_print_repeat(&buf, &len, &used,
                                           count, last);
                                       last = data;
                                       count = 1;
                               } else
                                       count++;
                       }
                       sixel_print_repeat(&buf, &len, &used, count, data);
                       sixel_print_add(&buf, &len, &used, "$", 1);
               }

               if (buf[used - 1] == '$')
                       used--;
               if (buf[used - 1] != '-')
                       sixel_print_add(&buf, &len, &used, "-", 1);
       }
       if (buf[used - 1] == '$' || buf[used - 1] == '-')
               used--;

       sixel_print_add(&buf, &len, &used, "\033\\", 2);

       buf[used] = '\0';
       if (size != NULL)
               *size = used;

       free(contains);
       return (buf);
}

struct screen *
sixel_to_screen(struct sixel_image *si)
{
       struct screen           *s;
       struct screen_write_ctx  ctx;
       struct grid_cell         gc;
       u_int                    x, y, sx, sy;

       sixel_size_in_cells(si, &sx, &sy);

       s = xmalloc(sizeof *s);
       screen_init(s, sx, sy, 0);

       memcpy(&gc, &grid_default_cell, sizeof gc);
       gc.attr |= (GRID_ATTR_CHARSET|GRID_ATTR_DIM);
       utf8_set(&gc.data, '~');

       screen_write_start(&ctx, s);
       if (sx == 1 || sy == 1) {
               for (y = 0; y < sy; y++) {
                       for (x = 0; x < sx; x++)
                               grid_view_set_cell(s->grid, x, y, &gc);
               }
       } else {
               screen_write_box(&ctx, sx, sy, BOX_LINES_DEFAULT, NULL, NULL);
               for (y = 1; y < sy - 1; y++) {
                       for (x = 1; x < sx - 1; x++)
                               grid_view_set_cell(s->grid, x, y, &gc);
               }
       }
       screen_write_stop(&ctx);
       return (s);
}