#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
#include <errno.h>
#include <cairo.h>

#include "broadway.h"

/************************************************************************
 *                Base64 functions                                      *
 ************************************************************************/

static const char base64_alphabet[] =
	"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

#if 0 /* Unused for now */
static void
base64_uint8 (guint8 v, char *c)
{
  c[0] = base64_alphabet[(v >> 0) & 0x3f];
  c[1] = base64_alphabet[(v >> 6) & 0x3];
}
#endif

static void
base64_uint16 (guint32 v, char *c)
{
  c[0] = base64_alphabet[(v >> 0) & 0x3f];
  c[1] = base64_alphabet[(v >> 6) & 0x3f];
  c[2] = base64_alphabet[(v >> 12) & 0xf];
}

#if 0
static void
base64_uint24 (guint32 v, char *c)
{
  c[0] = base64_alphabet[(v >> 0) & 0x3f];
  c[1] = base64_alphabet[(v >> 6) & 0x3f];
  c[2] = base64_alphabet[(v >> 12) & 0x3f];
  c[3] = base64_alphabet[(v >> 18) & 0x3f];
}
#endif

static void
base64_uint32 (guint32 v, char *c)
{
  c[0] = base64_alphabet[(v >> 0) & 0x3f];
  c[1] = base64_alphabet[(v >> 6) & 0x3f];
  c[2] = base64_alphabet[(v >> 12) & 0x3f];
  c[3] = base64_alphabet[(v >> 18) & 0x3f];
  c[4] = base64_alphabet[(v >> 24) & 0x3f];
  c[5] = base64_alphabet[(v >> 30) & 0x2];
}

/***********************************************************
 *  conversion of raw image data to png data: uris         *
 ***********************************************************/

static cairo_status_t
write_png_data (void		  *closure,
		const unsigned char *data,
		unsigned int	   data_len)
{
  GString *buf = closure;

  g_string_append_len (buf,  (char *)data, data_len);

  return CAIRO_STATUS_SUCCESS;
}

static void
to_png_rgb (GString *buf, int w, int h, int byte_stride, guint32 *data)
{
  cairo_surface_t *surface;

  surface = cairo_image_surface_create_for_data ((guchar *)data,
						 CAIRO_FORMAT_RGB24, w, h, byte_stride);

  cairo_surface_write_to_png_stream (surface, write_png_data, buf);
  cairo_surface_destroy (surface);
}

static void
to_png_rgba (GString *buf, int w, int h, int byte_stride, guint32 *data)
{
  cairo_surface_t *surface;

  surface = cairo_image_surface_create_for_data ((guchar *)data,
						 CAIRO_FORMAT_ARGB32, w, h, byte_stride);

  cairo_surface_write_to_png_stream (surface, write_png_data, buf);
  cairo_surface_destroy (surface);
}

struct PngTarget {
  GString *buf;
  int state;
  int save;
};

static cairo_status_t
write_png_url (void		  *closure,
	       const unsigned char *data,
	       unsigned int	   data_len)
{
  struct PngTarget *target = closure;
  gsize res, old_len;

  old_len = target->buf->len;
  g_string_set_size (target->buf,
		     old_len + (data_len / 3 + 1) * 4 + 4);

  res = g_base64_encode_step (data, data_len, FALSE,
			      target->buf->str + old_len,
			      &target->state, &target->save);

  g_string_set_size (target->buf,  old_len + res);

  return CAIRO_STATUS_SUCCESS;
}

static void
to_png_url_rgb (GString *buf, int w, int h, int byte_stride, guint32 *data)
{
  cairo_surface_t *surface;
  struct PngTarget target;
  gsize res, old_len;

  target.buf = buf;
  target.state = 0;
  target.save = 0;

  g_string_append (buf, "data:image/png;base64,");

  surface = cairo_image_surface_create_for_data ((guchar *)data,
						 CAIRO_FORMAT_RGB24, w, h, byte_stride);

  cairo_surface_write_to_png_stream (surface, write_png_url, &target);
  cairo_surface_destroy (surface);

  old_len = buf->len;

  g_string_set_size (buf, old_len + 4);
  res = g_base64_encode_close (FALSE,
			       buf->str + old_len,
			       &target.state, &target.save);
  g_string_set_size (buf, old_len + res);
}

static void
to_png_url_rgba (GString *buf, int w, int h, int byte_stride, guint32 *data)
{
  cairo_surface_t *surface;
  struct PngTarget target;
  gsize res, old_len;

  target.buf = buf;
  target.state = 0;
  target.save = 0;

  g_string_append (buf, "data:image/png;base64,");

  surface = cairo_image_surface_create_for_data ((guchar *)data,
						 CAIRO_FORMAT_ARGB32, w, h, byte_stride);

  cairo_surface_write_to_png_stream (surface, write_png_url, &target);
  cairo_surface_destroy (surface);

  old_len = buf->len;

  g_string_set_size (buf, old_len + 4);
  res = g_base64_encode_close (FALSE,
			       buf->str + old_len,
			       &target.state, &target.save);
  g_string_set_size (buf, old_len + res);
}

#if 0
static char *
to_png_a (int w, int h, int byte_stride, guint8 *data)
{
  cairo_surface_t *surface;
  struct PngTarget target;
  gsize res, old_len;

  target.url = g_string_new ("data:image/png;base64,");
  target.state = 0;
  target.save = 0;

  surface = cairo_image_surface_create_for_data (data,
						 CAIRO_FORMAT_A8, w, h, byte_stride);

  cairo_surface_write_to_png_stream (surface, write_png_url, &target);

  old_len = target.url->len;

  g_string_set_size (target.url, old_len + 4);
  res = g_base64_encode_close (FALSE,
			       target.url->str + old_len,
			       &target.state, &target.save);
  g_string_set_size (target.url, old_len + res);

  return g_string_free (target.url, FALSE);
}
#endif

/************************************************************************
 *                Basic I/O primitives                                  *
 ************************************************************************/

struct BroadwayOutput {
  GOutputStream *out;
  GString *buf;
  int error;
  guint32 serial;
  gboolean proto_v7_plus;
  gboolean binary;
};

static void
broadway_output_send_cmd (BroadwayOutput *output,
			  gboolean fin, BroadwayWSOpCode code,
			  const void *buf, gsize count)
{
  gboolean mask = FALSE;
  guchar header[16];
  size_t p;

  gboolean mid_header = count > 125 && count <= 65535;
  gboolean long_header = count > 65535;

  /* NB. big-endian spec => bit 0 == MSB */
  header[0] = ( (fin ? 0x80 : 0) | (code & 0x0f) );
  header[1] = ( (mask ? 0x80 : 0) |
                (mid_header ? 126 : long_header ? 127 : count) );
  p = 2;
  if (mid_header)
    {
      *(guint16 *)(header + p) = GUINT16_TO_BE( (guint16)count );
      p += 2;
    }
  else if (long_header)
    {
      *(guint64 *)(header + p) = GUINT64_TO_BE( count );
      p += 8;
    }
  // FIXME: if we are paranoid we should 'mask' the data
  // FIXME: we should really emit these as a single write
  g_output_stream_write_all (output->out, header, p, NULL, NULL, NULL);
  g_output_stream_write_all (output->out, buf, count, NULL, NULL, NULL);
}

static void
broadway_output_send_cmd_pre_v7 (BroadwayOutput *output,
				 const void *buf, gsize count)
{
  g_output_stream_write_all (output->out, "\0", 1, NULL, NULL, NULL);
  g_output_stream_write_all (output->out, buf, count, NULL, NULL, NULL);
  g_output_stream_write_all (output->out, "\xff", 1, NULL, NULL, NULL);
}

void broadway_output_pong (BroadwayOutput *output)
{
  if (output->proto_v7_plus)
    broadway_output_send_cmd (output, TRUE, BROADWAY_WS_CNX_PONG, NULL, 0);
}

int
broadway_output_flush (BroadwayOutput *output)
{
  if (output->buf->len == 0)
    return TRUE;

  if (!output->proto_v7_plus)
    broadway_output_send_cmd_pre_v7 (output, output->buf->str, output->buf->len);
  else if (output->binary)
    broadway_output_send_cmd (output, TRUE, BROADWAY_WS_BINARY,
			      output->buf->str, output->buf->len);
  else
    broadway_output_send_cmd (output, TRUE, BROADWAY_WS_TEXT,
			      output->buf->str, output->buf->len);

  g_string_set_size (output->buf, 0);

  return !output->error;

}

BroadwayOutput *
broadway_output_new (GOutputStream *out, guint32 serial,
		     gboolean proto_v7_plus, gboolean binary)
{
  BroadwayOutput *output;

  output = g_new0 (BroadwayOutput, 1);

  output->out = g_object_ref (out);
  output->buf = g_string_new ("");
  output->serial = serial;
  output->proto_v7_plus = proto_v7_plus;
  output->binary = binary;

  return output;
}

void
broadway_output_free (BroadwayOutput *output)
{
  g_object_unref (output->out);
  free (output);
}

guint32
broadway_output_get_next_serial (BroadwayOutput *output)
{
  return output->serial;
}


/************************************************************************
 *                     Core rendering operations                        *
 ************************************************************************/

static void
append_char (BroadwayOutput *output, char c)
{
  g_string_append_c (output->buf, c);
}

static void
append_bool (BroadwayOutput *output, gboolean val)
{
  if (output->binary)
    g_string_append_c (output->buf, val ? 1: 0);
  else
    g_string_append_c (output->buf, val ? '1': '0');
}

static void
append_flags (BroadwayOutput *output, guint32 val)
{
  if (output->binary)
    g_string_append_c (output->buf, val);
  else
    g_string_append_c (output->buf, val + '0');
}


static void
append_uint16 (BroadwayOutput *output, guint32 v)
{
  gsize old_len = output->buf->len;

  if (output->binary)
    {
      guint8 *buf = (guint8 *)output->buf->str + old_len;

      g_string_set_size (output->buf, old_len + 2);
      buf[0] = (v >> 0) & 0xff;
      buf[1] = (v >> 8) & 0xff;
    }
  else
    {
      g_string_set_size (output->buf, old_len + 3);
      base64_uint16 (v, output->buf->str + old_len);
    }
}

static void
append_uint32 (BroadwayOutput *output, guint32 v)
{
  gsize old_len = output->buf->len;

  if (output->binary)
    {
      guint8 *buf = (guint8 *)output->buf->str + old_len;

      g_string_set_size (output->buf, old_len + 4);
      buf[0] = (v >> 0) & 0xff;
      buf[1] = (v >> 8) & 0xff;
      buf[2] = (v >> 16) & 0xff;
      buf[3] = (v >> 24) & 0xff;
    }
  else
    {
      g_string_set_size (output->buf, old_len + 6);
      base64_uint32 (v, output->buf->str + old_len);
    }
}

static void
overwrite_uint32 (BroadwayOutput *output, gsize pos, guint32 v)
{
  if (output->binary)
    {
      guint8 *buf = (guint8 *)output->buf->str + pos;

      buf[0] = (v >> 0) & 0xff;
      buf[1] = (v >> 8) & 0xff;
      buf[2] = (v >> 16) & 0xff;
      buf[3] = (v >> 24) & 0xff;
    }
  else
    {
      base64_uint32 (v, output->buf->str + pos);
    }
}


static void
write_header(BroadwayOutput *output, char op)
{
  append_char (output, op);
  append_uint32 (output, output->serial++);
}

void
broadway_output_copy_rectangles (BroadwayOutput *output,  int id,
				 BroadwayRect *rects, int n_rects,
				 int dx, int dy)
{
  int i;

  write_header (output, 'b');
  append_uint16 (output, id);
  append_uint16 (output, n_rects);
  for (i = 0; i < n_rects; i++)
    {
      append_uint16 (output, rects[i].x);
      append_uint16 (output, rects[i].y);
      append_uint16 (output, rects[i].width);
      append_uint16 (output, rects[i].height);
    }
  append_uint16 (output, dx);
  append_uint16 (output, dy);
}

void
broadway_output_grab_pointer (BroadwayOutput *output,
			      int id,
			      gboolean owner_event)
{
  write_header (output, 'g');
  append_uint16 (output, id);
  append_bool (output, owner_event);
}

guint32
broadway_output_ungrab_pointer (BroadwayOutput *output)
{
  guint32 serial;

  serial = output->serial;
  write_header (output, 'u');

  return serial;
}

void
broadway_output_new_surface(BroadwayOutput *output,
			    int id, int x, int y, int w, int h,
			    gboolean is_temp)
{
  write_header (output, 's');
  append_uint16 (output, id);
  append_uint16 (output, x);
  append_uint16 (output, y);
  append_uint16 (output, w);
  append_uint16 (output, h);
  append_bool (output, is_temp);
}

void
broadway_output_show_surface(BroadwayOutput *output,  int id)
{
  write_header (output, 'S');
  append_uint16 (output, id);
}

void
broadway_output_hide_surface(BroadwayOutput *output,  int id)
{
  write_header (output, 'H');
  append_uint16 (output, id);
}

void
broadway_output_destroy_surface(BroadwayOutput *output,  int id)
{
  write_header (output, 'd');
  append_uint16 (output, id);
}


void
broadway_output_move_resize_surface (BroadwayOutput *output,
				     int             id,
				     gboolean        has_pos,
				     int             x,
				     int             y,
				     gboolean        has_size,
				     int             w,
				     int             h)
{
  int val;

  if (!has_pos && !has_size)
    return;

  write_header (output, 'm');
  val = (!!has_pos) | ((!!has_size) << 1);
  append_uint16 (output, id);
  append_flags (output, val);
  if (has_pos)
    {
      append_uint16 (output, x);
      append_uint16 (output, y);
    }
  if (has_size)
    {
      append_uint16 (output, w);
      append_uint16 (output, h);
    }
}

void
broadway_output_set_transient_for (BroadwayOutput *output,
				   int             id,
				   int             parent_id)
{
  write_header (output, 'p');
  append_uint16 (output, id);
  append_uint16 (output, parent_id);
}


void
broadway_output_put_rgb (BroadwayOutput *output,  int id, int x, int y,
			 int w, int h, int byte_stride, void *data)
{
  gsize size_start, image_start, len;

  write_header (output, 'i');

  append_uint16 (output, id);
  append_uint16 (output, x);
  append_uint16 (output, y);

  size_start = output->buf->len;
  append_uint32 (output, 0);

  image_start = output->buf->len;
  if (output->binary)
    to_png_rgb (output->buf, w, h, byte_stride, (guint32*)data);
  else
    to_png_url_rgb (output->buf, w, h, byte_stride, (guint32*)data);

  len = output->buf->len - image_start;

  overwrite_uint32 (output, size_start, len);
}

typedef struct  {
  int x1, y1;
  int x2, y2;
} BroadwayBox;

static int
is_any_x_set (unsigned char *data,
	      int box_x1, int box_x2,
	      int x1, int x2, int y, int *x_set,
	      int byte_stride)
{
  int w ;
  guint32 *ptr;

  if (x1 < box_x1)
    x1 = box_x1;

  if (x2 > box_x2)
    x2 = box_x2;

  w = x2 - x1;
  if (w > 0)
    {
      ptr = (guint32 *)(data + y * byte_stride + x1 * 4);
      while (w-- > 0)
	{
	  if (*ptr != 0)
	    {
	      if (x_set)
		*x_set = x1;
	      return 1;
	    }
	  ptr++;
	  x1++;
	}
    }
  return 0;
}


#define EXTEND_X_FUZZ 10
#define EXTEND_Y_FUZZ 10

static int
extend_x_range (unsigned char *data,
		int box_x1, int box_y1,
		int box_x2, int box_y2,
		int *x1, int *x2, int y,
		int byte_stride)
{
  int extended = 0;
  int new_x;

  while (is_any_x_set (data, box_x1, box_x2, *x1 - EXTEND_X_FUZZ, *x1, y, &new_x, byte_stride))
    {
      *x1 = new_x;
      extended = 1;
    }

  while (is_any_x_set (data, box_x1, box_x2, *x2, *x2 + EXTEND_X_FUZZ, y, &new_x, byte_stride))
    {
      *x2 = new_x + 1;
      extended = 1;
    }

  return extended;
}

static int
extend_y_range (unsigned char *data,
		int box_x1, int box_y1,
		int box_x2, int box_y2,
		int x1, int x2, int *y,
		int byte_stride)
{
  int extended = 0;
  int found_set;
  int yy, y2;

  while (*y < box_y2)
    {
      found_set = 0;

      y2 = *y + EXTEND_Y_FUZZ;
      if (y2 > box_y2)
	y2 = box_y2;

      for (yy = y2; yy > *y + 1; yy--)
	{
	  if (is_any_x_set (data, box_x1, box_x2, x1, x2, yy - 1, NULL, byte_stride))
	    {
	      found_set = 1;
	      break;
	    }
	}
      if (!found_set)
	break;
      *y = yy;
      extended = 1;
    }

  return extended;
}


static void
rgba_find_rects_extents (unsigned char *data,
			 int box_x1, int box_y1,
			 int box_x2, int box_y2,
			 int x, int y,
			 BroadwayBox *rect,
			 int byte_stride)
{
  int x1, x2, y1, y2, yy;
  int extended;

  x1 = x;
  x2 = x + 1;
  y1 = y;
  y2 = y + 1;

  do
    {
      /* Expand maximally for all known rows */
      do
	{
	  extended = 0;

	  for (yy = y1; yy < y2; yy++)
	    extended |= extend_x_range (data,
					box_x1, box_y1,
					box_x2, box_y2,
					&x1, &x2, yy,
					byte_stride);
	}
      while (extended);
    }
  while (extend_y_range(data,
			box_x1, box_y1,
			box_x2, box_y2,
			x1, x2, &y2,
			byte_stride));

  rect->x1 = x1;
  rect->x2 = x2;
  rect->y1 = y1;
  rect->y2 = y2;
}

static void
rgba_find_rects_sub (unsigned char *data,
		     int box_x1, int box_y1,
		     int box_x2, int box_y2,
		     int byte_stride,
		     BroadwayBox **rects,
		     int *n_rects, int *alloc_rects)
{
  guint32 *line;
  BroadwayBox rect;
  int x, y;

  if (box_x1 == box_x2 || box_y1 == box_y2)
    return;

  for (y = box_y1; y < box_y2; y++)
    {
      line = (guint32 *)(data + y * byte_stride + box_x1 * 4);

      for (x = box_x1; x < box_x2; x++)
	{
	  if (*line != 0)
	    {
	      rgba_find_rects_extents (data,
				       box_x1, box_y1, box_x2, box_y2,
				       x, y, &rect, byte_stride);
	      if (*n_rects == *alloc_rects)
		{
		  (*alloc_rects) *= 2;
		  *rects = g_renew (BroadwayBox, *rects, *alloc_rects);
		}
	      (*rects)[*n_rects] = rect;
	      (*n_rects)++;
	      rgba_find_rects_sub (data,
				   box_x1, rect.y1,
				   rect.x1, rect.y2,
				   byte_stride,
				   rects, n_rects, alloc_rects);
	      rgba_find_rects_sub (data,
				   rect.x2, rect.y1,
				   box_x2, rect.y2,
				   byte_stride,
				   rects, n_rects, alloc_rects);
	      rgba_find_rects_sub (data,
				   box_x1, rect.y2,
				   box_x2, box_y2,
				   byte_stride,
				   rects, n_rects, alloc_rects);
	      return;
	    }
	  line++;
	}
    }
}

static BroadwayBox *
rgba_find_rects (unsigned char *data,
		 int w, int h, int byte_stride,
		 int *n_rects)
{
  BroadwayBox *rects;
  int alloc_rects;

  alloc_rects = 20;
  rects = g_new (BroadwayBox, alloc_rects);

  *n_rects = 0;
  rgba_find_rects_sub (data,
		       0, 0, w, h, byte_stride,
		       &rects, n_rects, &alloc_rects);

  return rects;
}

void
broadway_output_put_rgba (BroadwayOutput *output,  int id, int x, int y,
			  int w, int h, int byte_stride, void *data)
{
  BroadwayBox *rects;
  int i, n_rects;
  gsize size_start, image_start, len;

  rects = rgba_find_rects (data, w, h, byte_stride, &n_rects);

  for (i = 0; i < n_rects; i++)
    {
      guint8 *subdata;

      write_header (output, 'i');
      append_uint16 (output, id);
      append_uint16 (output, x + rects[i].x1);
      append_uint16 (output, y + rects[i].y1);

      size_start = output->buf->len;
      append_uint32 (output, 0);

      image_start = output->buf->len;

      subdata = (guint8 *)data + rects[i].x1 * 4 + rects[i].y1 * byte_stride;
      if (output->binary)
	to_png_rgba (output->buf, rects[i].x2 - rects[i].x1,
		     rects[i].y2 - rects[i].y1,
		     byte_stride, (guint32*)subdata);
      else
	to_png_url_rgba (output->buf, rects[i].x2 - rects[i].x1,
			 rects[i].y2 - rects[i].y1,
			 byte_stride, (guint32*)subdata);

      len = output->buf->len - image_start;

      overwrite_uint32 (output, size_start, len);
    }

  free (rects);
}

void
broadway_output_surface_flush (BroadwayOutput *output,
			       int             id)
{
  write_header (output, 'f');
  append_uint16 (output, id);
}
