/* GStreamer
 * Copyright (C) 2007 Michael Smith <msmith@xiph.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

/*
 * This is a decoder for the VMWare VMnc video codec, which VMWare uses for 
 * recording * of virtual machine instances.
 * It's essentially a serialisation of RFB (the VNC protocol) 
 * 'FramebufferUpdate' messages, with some special encoding-types for VMnc
 * extensions. There's some documentation (with fixes from VMWare employees) at:
 *   http://wiki.multimedia.cx/index.php?title=VMware_Video
 */

#ifdef HAVE_CONFIG_H
#  include "config.h"
#endif

#include <gst/gst.h>
#include <gst/base/gstadapter.h>
#include <gst/video/video.h>
#include <string.h>

#define GST_CAT_DEFAULT vmnc_debug
GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);

#define GST_TYPE_VMNC_DEC \
  (gst_vmnc_dec_get_type())
#define GST_VMNC_DEC(obj) \
  (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_VMNC_DEC,GstVMncDec))

#define RFB_GET_UINT32(ptr) GST_READ_UINT32_BE(ptr)
#define RFB_GET_UINT16(ptr) GST_READ_UINT16_BE(ptr)
#define RFB_GET_UINT8(ptr) GST_READ_UINT8(ptr)

enum
{
  ARG_0,
};

enum
{
  ERROR_INVALID = -1,           /* Invalid data in bitstream */
  ERROR_INSUFFICIENT_DATA = -2  /* Haven't received enough data yet */
};

#define MAKE_TYPE(a,b,c,d) ((a<<24)|(b<<16)|(c<<8)|d)
enum
{
  TYPE_RAW = 0,
  TYPE_COPY = 1,
  TYPE_RRE = 2,
  TYPE_CoRRE = 4,
  TYPE_HEXTILE = 5,

  TYPE_WMVd = MAKE_TYPE ('W', 'M', 'V', 'd'),
  TYPE_WMVe = MAKE_TYPE ('W', 'M', 'V', 'e'),
  TYPE_WMVf = MAKE_TYPE ('W', 'M', 'V', 'f'),
  TYPE_WMVg = MAKE_TYPE ('W', 'M', 'V', 'g'),
  TYPE_WMVh = MAKE_TYPE ('W', 'M', 'V', 'h'),
  TYPE_WMVi = MAKE_TYPE ('W', 'M', 'V', 'i'),
  TYPE_WMVj = MAKE_TYPE ('W', 'M', 'V', 'j')
};

struct RFBFormat
{
  int width;
  int height;
  int stride;
  int bytes_per_pixel;
  int depth;
  int big_endian;

  guint8 descriptor[16];        /* The raw format descriptor block */
};

enum CursorType
{
  CURSOR_COLOUR = 0,
  CURSOR_ALPHA = 1
};

struct Cursor
{
  enum CursorType type;
  int visible;
  int x;
  int y;
  int width;
  int height;
  int hot_x;
  int hot_y;
  guint8 *cursordata;
  guint8 *cursormask;
};

typedef struct
{
  GstElement element;

  GstPad *sinkpad;
  GstPad *srcpad;

  GstCaps *caps;
  gboolean have_format;

  int parsed;
  GstAdapter *adapter;

  int framerate_num;
  int framerate_denom;

  struct Cursor cursor;
  struct RFBFormat format;
  guint8 *imagedata;
} GstVMncDec;

typedef struct
{
  GstElementClass parent_class;
} GstVMncDecClass;

static GstStaticPadTemplate vmnc_dec_src_factory =
GST_STATIC_PAD_TEMPLATE ("src",
    GST_PAD_SRC,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("video/x-raw-rgb"));

static GstStaticPadTemplate vmnc_dec_sink_factory =
GST_STATIC_PAD_TEMPLATE ("sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("video/x-vmnc, version=(int)1, "
        "framerate=(fraction)[0, max], "
        "width=(int)[0, max], " "height=(int)[0, max]")
    );

GType gst_vmnc_dec_get_type (void);
GST_BOILERPLATE (GstVMncDec, gst_vmnc_dec, GstElement, GST_TYPE_ELEMENT);

static void vmnc_dec_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec);
static void vmnc_dec_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec);

static GstFlowReturn vmnc_dec_chain (GstPad * pad, GstBuffer * buffer);
static gboolean vmnc_dec_setcaps (GstPad * pad, GstCaps * caps);
static GstStateChangeReturn vmnc_dec_change_state (GstElement * element,
    GstStateChange transition);
static void vmnc_dec_finalize (GObject * object);

static void
gst_vmnc_dec_base_init (gpointer g_class)
{
  GstElementClass *element_class = GST_ELEMENT_CLASS (g_class);

  gst_element_class_add_static_pad_template (element_class,
      &vmnc_dec_src_factory);
  gst_element_class_add_static_pad_template (element_class,
      &vmnc_dec_sink_factory);
  gst_element_class_set_details_simple (element_class, "VMnc video decoder",
      "Codec/Decoder/Video",
      "Decode VmWare video to raw (RGB) video",
      "Michael Smith <msmith@xiph.org>");
}

static void
gst_vmnc_dec_class_init (GstVMncDecClass * klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  GstElementClass *gstelement_class = GST_ELEMENT_CLASS (klass);

  gobject_class->set_property = vmnc_dec_set_property;
  gobject_class->get_property = vmnc_dec_get_property;

  gobject_class->finalize = vmnc_dec_finalize;

  gstelement_class->change_state = vmnc_dec_change_state;

  GST_DEBUG_CATEGORY_INIT (vmnc_debug, "vmncdec", 0, "VMnc decoder");
}

static void
gst_vmnc_dec_init (GstVMncDec * dec, GstVMncDecClass * g_class)
{
  dec->sinkpad =
      gst_pad_new_from_static_template (&vmnc_dec_sink_factory, "sink");
  gst_pad_set_chain_function (dec->sinkpad, vmnc_dec_chain);
  gst_pad_set_setcaps_function (dec->sinkpad, vmnc_dec_setcaps);
  gst_element_add_pad (GST_ELEMENT (dec), dec->sinkpad);

  dec->srcpad = gst_pad_new_from_static_template (&vmnc_dec_src_factory, "src");
  gst_pad_use_fixed_caps (dec->srcpad);

  gst_element_add_pad (GST_ELEMENT (dec), dec->srcpad);

  dec->adapter = gst_adapter_new ();
}

static void
vmnc_dec_finalize (GObject * object)
{
  GstVMncDec *dec = GST_VMNC_DEC (object);

  g_object_unref (dec->adapter);

  G_OBJECT_CLASS (parent_class)->finalize (object);
}

static void
gst_vmnc_dec_reset (GstVMncDec * dec)
{
  if (dec->caps) {
    gst_caps_unref (dec->caps);
    dec->caps = NULL;
  }
  if (dec->imagedata) {
    g_free (dec->imagedata);
    dec->imagedata = NULL;
  }

  if (dec->cursor.cursordata) {
    g_free (dec->cursor.cursordata);
    dec->cursor.cursordata = NULL;
  }
  if (dec->cursor.cursormask) {
    g_free (dec->cursor.cursormask);
    dec->cursor.cursormask = NULL;
  }
  dec->cursor.visible = 0;

  /* Use these as defaults if the container doesn't provide anything */
  dec->framerate_num = 5;
  dec->framerate_denom = 1;

  dec->have_format = FALSE;

  gst_adapter_clear (dec->adapter);
}

struct RfbRectangle
{
  guint16 x;
  guint16 y;
  guint16 width;
  guint16 height;

  gint32 type;
};

/* Rectangle handling functions.
 * Return number of bytes consumed, or < 0 on error
 */
typedef int (*rectangle_handler) (GstVMncDec * dec, struct RfbRectangle * rect,
    const guint8 * data, int len, gboolean decode);

static int
vmnc_handle_wmvi_rectangle (GstVMncDec * dec, struct RfbRectangle *rect,
    const guint8 * data, int len, gboolean decode)
{
  GstCaps *caps;
  gint bpp, tc;
  guint32 redmask, greenmask, bluemask;
  guint32 endianness, dataendianness;

  /* A WMVi rectangle has a 16byte payload */
  if (len < 16) {
    GST_DEBUG_OBJECT (dec, "Bad WMVi rect: too short");
    return ERROR_INSUFFICIENT_DATA;
  }

  /* We only compare 13 bytes; ignoring the 3 padding bytes at the end */
  if (dec->caps && memcmp (data, dec->format.descriptor, 13) == 0) {
    /* Nothing changed, so just exit */
    return 16;
  }

  /* Store the whole block for simple comparison later */
  memcpy (dec->format.descriptor, data, 16);

  if (rect->x != 0 || rect->y != 0) {
    GST_WARNING_OBJECT (dec, "Bad WMVi rect: wrong coordinates");
    return ERROR_INVALID;
  }

  bpp = data[0];
  dec->format.depth = data[1];
  dec->format.big_endian = data[2];
  dataendianness = data[2] ? G_BIG_ENDIAN : G_LITTLE_ENDIAN;
  tc = data[3];

  if (bpp != 8 && bpp != 16 && bpp != 32) {
    GST_WARNING_OBJECT (dec, "Bad bpp value: %d", bpp);
    return ERROR_INVALID;
  }

  if (!tc) {
    GST_WARNING_OBJECT (dec, "Paletted video not supported");
    return ERROR_INVALID;
  }

  dec->format.bytes_per_pixel = bpp / 8;
  dec->format.width = rect->width;
  dec->format.height = rect->height;

  redmask = (guint32) (RFB_GET_UINT16 (data + 4)) << data[10];
  greenmask = (guint32) (RFB_GET_UINT16 (data + 6)) << data[11];
  bluemask = (guint32) (RFB_GET_UINT16 (data + 8)) << data[12];

  GST_DEBUG_OBJECT (dec, "Red: mask %d, shift %d",
      RFB_GET_UINT16 (data + 4), data[10]);
  GST_DEBUG_OBJECT (dec, "Green: mask %d, shift %d",
      RFB_GET_UINT16 (data + 6), data[11]);
  GST_DEBUG_OBJECT (dec, "Blue: mask %d, shift %d",
      RFB_GET_UINT16 (data + 8), data[12]);
  GST_DEBUG_OBJECT (dec, "BPP: %d. endianness: %s", bpp,
      data[2] ? "big" : "little");

  /* GStreamer's RGB caps are a bit weird. */
  if (bpp == 8) {
    endianness = G_BYTE_ORDER;  /* Doesn't matter */
  } else if (bpp == 16) {
    /* We require host-endian. */
    endianness = G_BYTE_ORDER;
  } else {                      /* bpp == 32 */
    /* We require big endian */
    endianness = G_BIG_ENDIAN;
    if (endianness != dataendianness) {
      redmask = GUINT32_SWAP_LE_BE (redmask);
      greenmask = GUINT32_SWAP_LE_BE (greenmask);
      bluemask = GUINT32_SWAP_LE_BE (bluemask);
    }
  }

  dec->have_format = TRUE;
  if (!decode) {
    GST_DEBUG_OBJECT (dec, "Parsing, not setting caps");
    return 16;
  }

  caps = gst_caps_new_simple ("video/x-raw-rgb",
      "framerate", GST_TYPE_FRACTION, dec->framerate_num, dec->framerate_denom,
      "pixel-aspect-ratio", GST_TYPE_FRACTION, 1, 1,
      "width", G_TYPE_INT, rect->width, "height", G_TYPE_INT, rect->height,
      "bpp", G_TYPE_INT, bpp,
      "depth", G_TYPE_INT, dec->format.depth,
      "endianness", G_TYPE_INT, endianness,
      "red_mask", G_TYPE_INT, redmask,
      "green_mask", G_TYPE_INT, greenmask,
      "blue_mask", G_TYPE_INT, bluemask, NULL);
  gst_pad_set_caps (dec->srcpad, caps);

  if (dec->caps)
    gst_caps_unref (dec->caps);
  dec->caps = caps;

  if (dec->imagedata)
    g_free (dec->imagedata);
  dec->imagedata = g_malloc (dec->format.width * dec->format.height *
      dec->format.bytes_per_pixel);
  GST_DEBUG_OBJECT (dec, "Allocated image data at %p", dec->imagedata);

  dec->format.stride = dec->format.width * dec->format.bytes_per_pixel;

  return 16;
}

static void
render_colour_cursor (GstVMncDec * dec, guint8 * data, int x, int y,
    int off_x, int off_y, int width, int height)
{
  int i, j;
  guint8 *dstraw = data + dec->format.stride * y +
      dec->format.bytes_per_pixel * x;
  guint8 *srcraw = dec->cursor.cursordata +
      dec->cursor.width * dec->format.bytes_per_pixel * off_y;
  guint8 *maskraw = dec->cursor.cursormask +
      dec->cursor.width * dec->format.bytes_per_pixel * off_y;

  /* Boundchecking done by caller; this is just the renderer inner loop */
  if (dec->format.bytes_per_pixel == 1) {
    guint8 *dst = dstraw;
    guint8 *src = srcraw;
    guint8 *mask = maskraw;

    for (i = 0; i < height; i++) {
      for (j = 0; j < width; j++) {
        dst[j] = (dst[j] & src[j]) ^ mask[j];
      }
      dst += dec->format.width;
      src += dec->cursor.width;
      mask += dec->cursor.width;
    }
  } else if (dec->format.bytes_per_pixel == 2) {
    guint16 *dst = (guint16 *) dstraw;
    guint16 *src = (guint16 *) srcraw;
    guint16 *mask = (guint16 *) maskraw;

    for (i = 0; i < height; i++) {
      for (j = 0; j < width; j++) {
        dst[j] = (dst[j] & src[j]) ^ mask[j];
      }
      dst += dec->format.width;
      src += dec->cursor.width;
      mask += dec->cursor.width;
    }
  } else {
    guint32 *dst = (guint32 *) dstraw;
    guint32 *src = (guint32 *) srcraw;
    guint32 *mask = (guint32 *) maskraw;

    for (i = 0; i < height; i++) {
      for (j = 0; j < width; j++) {
        dst[j] = (dst[j] & src[j]) ^ mask[j];
      }
      dst += dec->format.width;
      src += dec->cursor.width;
      mask += dec->cursor.width;
    }
  }
}

static void
render_cursor (GstVMncDec * dec, guint8 * data)
{
  /* First, figure out the portion of the cursor that's on-screen */
  /* X,Y of top-left of cursor */
  int x = dec->cursor.x - dec->cursor.hot_x;
  int y = dec->cursor.y - dec->cursor.hot_y;

  /* Width, height of rendered portion of cursor */
  int width = dec->cursor.width;
  int height = dec->cursor.height;

  /* X,Y offset of rendered portion of cursor */
  int off_x = 0;
  int off_y = 0;

  if (x < 0) {
    off_x = -x;
    width += x;
    x = 0;
  }
  if (x + width > dec->format.width)
    width = dec->format.width - x;
  if (y < 0) {
    off_y = -y;
    height += y;
    y = 0;
  }
  if (y + height > dec->format.height)
    height = dec->format.height - y;

  if (dec->cursor.type == CURSOR_COLOUR) {
    render_colour_cursor (dec, data, x, y, off_x, off_y, width, height);
  } else {
    /* Alpha cursor. */
    /* TODO: Implement me! */
    GST_WARNING_OBJECT (dec, "Alpha composited cursors not yet implemented");
  }
}

static GstBuffer *
vmnc_make_buffer (GstVMncDec * dec, GstBuffer * inbuf)
{
  int size = dec->format.stride * dec->format.height;
  GstBuffer *buf = gst_buffer_new_and_alloc (size);
  guint8 *data = GST_BUFFER_DATA (buf);

  memcpy (data, dec->imagedata, size);

  if (dec->cursor.visible) {
    render_cursor (dec, data);
  }

  if (inbuf) {
    gst_buffer_copy_metadata (buf, inbuf, GST_BUFFER_COPY_TIMESTAMPS);
  }

  gst_buffer_set_caps (buf, dec->caps);

  return buf;
}

static int
vmnc_handle_wmvd_rectangle (GstVMncDec * dec, struct RfbRectangle *rect,
    const guint8 * data, int len, gboolean decode)
{
  /* Cursor data. */
  int datalen = 2;
  int type, size;

  if (len < datalen) {
    GST_DEBUG_OBJECT (dec, "Cursor data too short");
    return ERROR_INSUFFICIENT_DATA;
  }

  type = RFB_GET_UINT8 (data);

  if (type == CURSOR_COLOUR) {
    datalen += rect->width * rect->height * dec->format.bytes_per_pixel * 2;
  } else if (type == CURSOR_ALPHA) {
    datalen += rect->width * rect->height * 4;
  } else {
    GST_WARNING_OBJECT (dec, "Unknown cursor type: %d", type);
    return ERROR_INVALID;
  }

  if (len < datalen) {
    GST_DEBUG_OBJECT (dec, "Cursor data too short");
    return ERROR_INSUFFICIENT_DATA;
  } else if (!decode)
    return datalen;

  dec->cursor.type = type;
  dec->cursor.width = rect->width;
  dec->cursor.height = rect->height;
  dec->cursor.type = type;
  dec->cursor.hot_x = rect->x;
  dec->cursor.hot_y = rect->y;

  if (dec->cursor.cursordata)
    g_free (dec->cursor.cursordata);
  if (dec->cursor.cursormask)
    g_free (dec->cursor.cursormask);

  if (type == 0) {
    size = rect->width * rect->height * dec->format.bytes_per_pixel;
    dec->cursor.cursordata = g_malloc (size);
    dec->cursor.cursormask = g_malloc (size);
    memcpy (dec->cursor.cursordata, data + 2, size);
    memcpy (dec->cursor.cursormask, data + 2 + size, size);
  } else {
    dec->cursor.cursordata = g_malloc (rect->width * rect->height * 4);
    memcpy (dec->cursor.cursordata, data + 2, rect->width * rect->height * 4);
  }

  return datalen;
}

static int
vmnc_handle_wmve_rectangle (GstVMncDec * dec, struct RfbRectangle *rect,
    const guint8 * data, int len, gboolean decode)
{
  guint16 flags;

  /* Cursor state. */
  if (len < 2) {
    GST_DEBUG_OBJECT (dec, "Cursor data too short");
    return ERROR_INSUFFICIENT_DATA;
  } else if (!decode)
    return 2;

  flags = RFB_GET_UINT16 (data);
  dec->cursor.visible = flags & 0x01;

  return 2;
}

static int
vmnc_handle_wmvf_rectangle (GstVMncDec * dec, struct RfbRectangle *rect,
    const guint8 * data, int len, gboolean decode)
{
  /* Cursor position. */
  dec->cursor.x = rect->x;
  dec->cursor.y = rect->y;
  return 0;
}

static int
vmnc_handle_wmvg_rectangle (GstVMncDec * dec, struct RfbRectangle *rect,
    const guint8 * data, int len, gboolean decode)
{
  /* Keyboard stuff; not interesting for playback */
  if (len < 10) {
    GST_DEBUG_OBJECT (dec, "Keyboard data too short");
    return ERROR_INSUFFICIENT_DATA;
  }
  return 10;
}

static int
vmnc_handle_wmvh_rectangle (GstVMncDec * dec, struct RfbRectangle *rect,
    const guint8 * data, int len, gboolean decode)
{
  /* More keyboard stuff; not interesting for playback */
  if (len < 4) {
    GST_DEBUG_OBJECT (dec, "Keyboard data too short");
    return ERROR_INSUFFICIENT_DATA;
  }
  return 4;
}

static int
vmnc_handle_wmvj_rectangle (GstVMncDec * dec, struct RfbRectangle *rect,
    const guint8 * data, int len, gboolean decode)
{
  /* VM state info, not interesting for playback */
  if (len < 2) {
    GST_DEBUG_OBJECT (dec, "VM state data too short");
    return ERROR_INSUFFICIENT_DATA;
  }
  return 2;
}

static void
render_raw_tile (GstVMncDec * dec, const guint8 * data, int x, int y,
    int width, int height)
{
  int i;
  guint8 *dst;
  const guint8 *src;
  int line;

  src = data;
  dst = dec->imagedata + dec->format.stride * y +
      dec->format.bytes_per_pixel * x;
  line = width * dec->format.bytes_per_pixel;

  for (i = 0; i < height; i++) {
    /* This is wrong-endian currently */
    memcpy (dst, src, line);

    dst += dec->format.stride;
    src += line;
  }
}

static void
render_subrect (GstVMncDec * dec, int x, int y, int width,
    int height, guint32 colour)
{
  /* Crazy inefficient! */
  int i, j;
  guint8 *dst;

  for (i = 0; i < height; i++) {
    dst = dec->imagedata + dec->format.stride * (y + i) +
        dec->format.bytes_per_pixel * x;
    for (j = 0; j < width; j++) {
      memcpy (dst, &colour, dec->format.bytes_per_pixel);
      dst += dec->format.bytes_per_pixel;
    }
  }
}

static int
vmnc_handle_raw_rectangle (GstVMncDec * dec, struct RfbRectangle *rect,
    const guint8 * data, int len, gboolean decode)
{
  int datalen = rect->width * rect->height * dec->format.bytes_per_pixel;

  if (len < datalen) {
    GST_DEBUG_OBJECT (dec, "Raw data too short");
    return ERROR_INSUFFICIENT_DATA;
  }

  if (decode)
    render_raw_tile (dec, data, rect->x, rect->y, rect->width, rect->height);

  return datalen;
}

static int
vmnc_handle_copy_rectangle (GstVMncDec * dec, struct RfbRectangle *rect,
    const guint8 * data, int len, gboolean decode)
{
  int src_x, src_y;
  guint8 *src, *dst;
  int i;

  if (len < 4) {
    GST_DEBUG_OBJECT (dec, "Copy data too short");
    return ERROR_INSUFFICIENT_DATA;
  } else if (!decode)
    return 4;

  src_x = RFB_GET_UINT16 (data);
  src_y = RFB_GET_UINT16 (data + 2);

  /* Our destination rectangle is guaranteed in-frame; check this for the source
   * rectangle. */
  if (src_x + rect->width > dec->format.width ||
      src_y + rect->height > dec->format.height) {
    GST_WARNING_OBJECT (dec, "Source rectangle out of range");
    return ERROR_INVALID;
  }

  if (src_y > rect->y || src_x > rect->x) {
    /* Moving forward */
    src = dec->imagedata + dec->format.stride * src_y +
        dec->format.bytes_per_pixel * src_x;
    dst = dec->imagedata + dec->format.stride * rect->y +
        dec->format.bytes_per_pixel * rect->x;
    for (i = 0; i < rect->height; i++) {
      memmove (dst, src, rect->width * dec->format.bytes_per_pixel);
      dst += dec->format.stride;
      src += dec->format.stride;
    }
  } else {
    /* Backwards */
    src = dec->imagedata + dec->format.stride * (src_y + rect->height - 1) +
        dec->format.bytes_per_pixel * src_x;
    dst = dec->imagedata + dec->format.stride * (rect->y + rect->height - 1) +
        dec->format.bytes_per_pixel * rect->x;
    for (i = rect->height; i > 0; i--) {
      memmove (dst, src, rect->width * dec->format.bytes_per_pixel);
      dst -= dec->format.stride;
      src -= dec->format.stride;
    }
  }

  return 4;
}

/* FIXME: data+off might not be properly aligned */
#define READ_PIXEL(pixel, data, off, len)         \
  if (dec->format.bytes_per_pixel == 1) {         \
     if (off >= len)                              \
       return ERROR_INSUFFICIENT_DATA;            \
     pixel = data[off++];                         \
  } else if (dec->format.bytes_per_pixel == 2) {  \
     if (off+2 > len)                             \
       return ERROR_INSUFFICIENT_DATA;            \
     pixel = (*(guint16 *)(data + off));          \
     off += 2;                                    \
  } else {                                        \
     if (off+4 > len)                             \
       return ERROR_INSUFFICIENT_DATA;            \
     pixel = (*(guint32 *)(data + off));          \
     off += 4;                                    \
  }

static int
vmnc_handle_hextile_rectangle (GstVMncDec * dec, struct RfbRectangle *rect,
    const guint8 * data, int len, gboolean decode)
{
  int tilesx = GST_ROUND_UP_16 (rect->width) / 16;
  int tilesy = GST_ROUND_UP_16 (rect->height) / 16;
  int x, y, z;
  int off = 0;
  int subrects;
  int coloured;
  int width, height;
  guint32 fg = 0, bg = 0, colour;
  guint8 flags;

  for (y = 0; y < tilesy; y++) {
    if (y == tilesy - 1)
      height = rect->height - (tilesy - 1) * 16;
    else
      height = 16;

    for (x = 0; x < tilesx; x++) {
      if (x == tilesx - 1)
        width = rect->width - (tilesx - 1) * 16;
      else
        width = 16;

      if (off >= len) {
        return ERROR_INSUFFICIENT_DATA;
      }
      flags = data[off++];

      if (flags & 0x1) {
        if (off + width * height * dec->format.bytes_per_pixel > len) {
          return ERROR_INSUFFICIENT_DATA;
        }
        if (decode)
          render_raw_tile (dec, data + off, rect->x + x * 16, rect->y + y * 16,
              width, height);
        off += width * height * dec->format.bytes_per_pixel;
      } else {
        if (flags & 0x2) {
          READ_PIXEL (bg, data, off, len)
        }
        if (flags & 0x4) {
          READ_PIXEL (fg, data, off, len)
        }

        subrects = 0;
        if (flags & 0x8) {
          if (off >= len) {
            return ERROR_INSUFFICIENT_DATA;
          }
          subrects = data[off++];
        }

        /* Paint background colour on entire tile */
        if (decode)
          render_subrect (dec, rect->x + x * 16, rect->y + y * 16,
              width, height, bg);

        coloured = flags & 0x10;
        for (z = 0; z < subrects; z++) {
          if (coloured) {
            READ_PIXEL (colour, data, off, len);
          } else
            colour = fg;
          if (off + 2 > len)
            return ERROR_INSUFFICIENT_DATA;

          {
            int off_x = (data[off] & 0xf0) >> 4;
            int off_y = (data[off] & 0x0f);
            int w = ((data[off + 1] & 0xf0) >> 4) + 1;
            int h = (data[off + 1] & 0x0f) + 1;

            off += 2;

            /* Ensure we don't have out of bounds coordinates */
            if (off_x + w > width || off_y + h > height) {
              GST_WARNING_OBJECT (dec, "Subrect out of bounds: %d-%d x %d-%d "
                  "extends outside %dx%d", off_x, w, off_y, h, width, height);
              return ERROR_INVALID;
            }

            if (decode)
              render_subrect (dec, rect->x + x * 16 + off_x,
                  rect->y + y * 16 + off_y, w, h, colour);
          }
        }
      }
    }
  }

  return off;
}

/* Handle a packet in one of two modes: decode or parse.
 * In parse mode, we don't execute any of the decoding, we just do enough to
 * figure out how many bytes it contains
 *
 * Returns: >= 0, the number of bytes consumed
 *           < 0, packet too short to decode, or error
 */
static int
vmnc_handle_packet (GstVMncDec * dec, const guint8 * data, int len,
    gboolean decode)
{
  int type;
  int offset = 0;

  if (len < 4) {
    GST_DEBUG_OBJECT (dec, "Packet too short");
    return ERROR_INSUFFICIENT_DATA;
  }

  type = data[0];

  switch (type) {
    case 0:
    {
      int numrect = RFB_GET_UINT16 (data + 2);
      int i;
      int read;

      offset = 4;

      for (i = 0; i < numrect; i++) {
        struct RfbRectangle r;
        rectangle_handler handler;

        if (len < offset + 12) {
          GST_DEBUG_OBJECT (dec,
              "Packet too short for rectangle header: %d < %d",
              len, offset + 12);
          return ERROR_INSUFFICIENT_DATA;
        }
        GST_DEBUG_OBJECT (dec, "Reading rectangle %d", i);
        r.x = RFB_GET_UINT16 (data + offset);
        r.y = RFB_GET_UINT16 (data + offset + 2);
        r.width = RFB_GET_UINT16 (data + offset + 4);
        r.height = RFB_GET_UINT16 (data + offset + 6);
        r.type = RFB_GET_UINT32 (data + offset + 8);

        if (r.type != TYPE_WMVi) {
          /* We must have a WMVi packet to initialise things before we can 
           * continue */
          if (!dec->have_format) {
            GST_WARNING_OBJECT (dec, "Received packet without WMVi: %d",
                r.type);
            return ERROR_INVALID;
          }
          if (r.x + r.width > dec->format.width ||
              r.y + r.height > dec->format.height) {
            GST_WARNING_OBJECT (dec, "Rectangle out of range, type %d", r.type);
            return ERROR_INVALID;
          }
        }

        switch (r.type) {
          case TYPE_WMVd:
            handler = vmnc_handle_wmvd_rectangle;
            break;
          case TYPE_WMVe:
            handler = vmnc_handle_wmve_rectangle;
            break;
          case TYPE_WMVf:
            handler = vmnc_handle_wmvf_rectangle;
            break;
          case TYPE_WMVg:
            handler = vmnc_handle_wmvg_rectangle;
            break;
          case TYPE_WMVh:
            handler = vmnc_handle_wmvh_rectangle;
            break;
          case TYPE_WMVi:
            handler = vmnc_handle_wmvi_rectangle;
            break;
          case TYPE_WMVj:
            handler = vmnc_handle_wmvj_rectangle;
            break;
          case TYPE_RAW:
            handler = vmnc_handle_raw_rectangle;
            break;
          case TYPE_COPY:
            handler = vmnc_handle_copy_rectangle;
            break;
          case TYPE_HEXTILE:
            handler = vmnc_handle_hextile_rectangle;
            break;
          default:
            GST_WARNING_OBJECT (dec, "Unknown rectangle type");
            return ERROR_INVALID;
        }

        read = handler (dec, &r, data + offset + 12, len - offset - 12, decode);
        if (read < 0) {
          GST_DEBUG_OBJECT (dec, "Error calling rectangle handler\n");
          return read;
        }
        offset += 12 + read;
      }
      break;
    }
    default:
      GST_WARNING_OBJECT (dec, "Packet type unknown: %d", type);
      return ERROR_INVALID;
  }

  return offset;
}

static gboolean
vmnc_dec_setcaps (GstPad * pad, GstCaps * caps)
{
  /* We require a format descriptor in-stream, so we ignore the info from the
   * container here. We just use the framerate */
  GstVMncDec *dec = GST_VMNC_DEC (gst_pad_get_parent (pad));

  if (gst_caps_get_size (caps) > 0) {
    GstStructure *structure = gst_caps_get_structure (caps, 0);

    /* We gave these a default in reset(), so we don't need to check for failure
     * here */
    gst_structure_get_fraction (structure, "framerate",
        &dec->framerate_num, &dec->framerate_denom);

    dec->parsed = TRUE;
  } else {
    GST_DEBUG_OBJECT (dec, "Unparsed input");
    dec->parsed = FALSE;
  }

  gst_object_unref (dec);

  return TRUE;
}

static GstFlowReturn
vmnc_dec_chain_frame (GstVMncDec * dec, GstBuffer * inbuf,
    const guint8 * data, int len)
{
  int res;
  GstFlowReturn ret = GST_FLOW_OK;
  GstBuffer *outbuf;

  res = vmnc_handle_packet (dec, data, len, TRUE);

  if (res < 0) {
    ret = GST_FLOW_ERROR;
    GST_ELEMENT_ERROR (dec, STREAM, DECODE, (NULL), ("Couldn't decode packet"));
  } else {
    GST_DEBUG_OBJECT (dec, "read %d bytes of %d", res, len);
    /* inbuf may be NULL; that's ok */
    outbuf = vmnc_make_buffer (dec, inbuf);
    ret = gst_pad_push (dec->srcpad, outbuf);
  }

  return ret;
}

static GstFlowReturn
vmnc_dec_chain (GstPad * pad, GstBuffer * buf)
{
  GstVMncDec *dec;
  GstFlowReturn ret = GST_FLOW_OK;

  dec = GST_VMNC_DEC (gst_pad_get_parent (pad));

  if (!dec->parsed) {
    /* Submit our input buffer to adapter, then parse. Push each frame found
     * through the decoder
     */
    int avail;
    const guint8 *data;
    int read = 0;

    gst_adapter_push (dec->adapter, buf);


    avail = gst_adapter_available (dec->adapter);
    data = gst_adapter_peek (dec->adapter, avail);

    GST_DEBUG_OBJECT (dec, "Parsing %d bytes", avail);

    while (TRUE) {
      int len = vmnc_handle_packet (dec, data, avail, FALSE);

      if (len == ERROR_INSUFFICIENT_DATA) {
        GST_DEBUG_OBJECT (dec, "Not enough data yet");
        ret = GST_FLOW_OK;
        break;
      } else if (len < 0) {
        GST_DEBUG_OBJECT (dec, "Fatal error in bitstream");
        ret = GST_FLOW_ERROR;
        break;
      }

      GST_DEBUG_OBJECT (dec, "Parsed packet: %d bytes", len);

      ret = vmnc_dec_chain_frame (dec, NULL, data, len);
      avail -= len;
      data += len;

      read += len;

      if (ret != GST_FLOW_OK)
        break;
    }
    GST_DEBUG_OBJECT (dec, "Flushing %d bytes", read);

    gst_adapter_flush (dec->adapter, read);
  } else {
    ret = vmnc_dec_chain_frame (dec, buf, GST_BUFFER_DATA (buf),
        GST_BUFFER_SIZE (buf));
    gst_buffer_unref (buf);
  }

  gst_object_unref (dec);

  return ret;
}

static GstStateChangeReturn
vmnc_dec_change_state (GstElement * element, GstStateChange transition)
{
  GstVMncDec *dec = GST_VMNC_DEC (element);
  GstStateChangeReturn ret;

  switch (transition) {
    case GST_STATE_CHANGE_NULL_TO_READY:
      break;
    case GST_STATE_CHANGE_READY_TO_PAUSED:
      gst_vmnc_dec_reset (dec);
      break;
    case GST_STATE_CHANGE_PAUSED_TO_PLAYING:
      break;
    default:
      break;
  }

  ret = parent_class->change_state (element, transition);

  switch (transition) {
    case GST_STATE_CHANGE_PLAYING_TO_PAUSED:
      break;
    case GST_STATE_CHANGE_PAUSED_TO_READY:
      gst_vmnc_dec_reset (dec);
      break;
    case GST_STATE_CHANGE_READY_TO_NULL:
      break;
    default:
      break;
  }

  return ret;
}

static void
vmnc_dec_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  /*GstVMncDec *dec = GST_VMNC_DEC (object); */

  switch (prop_id) {
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
vmnc_dec_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  /*GstVMncDec *dec = GST_VMNC_DEC (object); */

  switch (prop_id) {
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static gboolean
plugin_init (GstPlugin * plugin)
{
  if (!gst_element_register (plugin, "vmncdec", GST_RANK_PRIMARY,
          gst_vmnc_dec_get_type ()))
    return FALSE;
  return TRUE;
}

GST_PLUGIN_DEFINE2 (GST_VERSION_MAJOR,
    GST_VERSION_MINOR,
    vmnc,
    "VmWare Video Codec plugins",
    plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)
