/* GStreamer
 * Copyright (C) <2009> Руслан Ижбулатов <lrn1986 _at_ gmail _dot_ com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 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
 * Lesser General Public License for more details.

 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA  02110-1301  USA
 */

/**
 * SECTION:element-ssim
 *
 * The ssim calculates SSIM (Structural SIMilarity) index for two or more 
 * streams, for each frame.
 * First stream is the original, other streams are modified (compressed) ones.
 * ssim will calculate SSIM index of each frame of each modified stream, using 
 * original stream as a reference.
 *
 * The ssim accepts only YUV planar top-first data and calculates only Y-SSIM.
 * All streams must have the same width, height and colorspace.
 * Output streams are greyscale video streams, where bright pixels indicate 
 * high SSIM values, dark pixels - low SSIM values.
 * The ssim also calculates mean SSIM index for each frame and emits is as a 
 * message.
 * ssim is intended to be used with videomeasure_collector element to catch the 
 * events (such as mean SSIM index values) and save them into a file.
 *
 * <refsect2>
 * <title>Example launch line</title>
 * |[
 * gst-launch ssim name=ssim ssim.src0 ! ffmpegcolorspace ! glimagesink filesrc
 * location=orig.avi ! decodebin2 ! ssim.original filesrc location=compr.avi !
 * decodebin2 ! ssim.modified0
 * ]| This pipeline produces a video stream that consists of SSIM frames.
 * </refsect2>
 *
 * Last reviewed on 2009-09-06 (0.10.?)
 */
/* Element-Checklist-Version: 5 */

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

#include "gstvideomeasure.h"
#include "gstvideomeasure_ssim.h"
#include <gst/audio/audio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#define GST_CAT_DEFAULT gst_ssim_debug
GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);

/* elementfactory information */

#define SINK_CAPS \
  "video/x-raw-yuv, " \
  "format = (fourcc) { I420, YV12, Y41B, Y42B } "


#define SRC_CAPS \
  "video/x-raw-gray, " \
  "width = (int) [ 1, MAX ], " \
  "height = (int) [ 1, MAX ], " \
  "framerate = (fraction) [ 0/1, MAX ], " \
  "bpp = (int) 8, " \
  "depth = (int) 8 "

static GstStaticPadTemplate gst_ssim_src_template =
GST_STATIC_PAD_TEMPLATE ("src%d",
    GST_PAD_SRC,
    GST_PAD_SOMETIMES,
    GST_STATIC_CAPS (SRC_CAPS)
    );

static GstStaticPadTemplate gst_ssim_sink_original_template =
GST_STATIC_PAD_TEMPLATE ("original",
    GST_PAD_SINK,
    GST_PAD_REQUEST,
    GST_STATIC_CAPS (SINK_CAPS)
    );

static GstStaticPadTemplate gst_ssim_sink_modified_template =
GST_STATIC_PAD_TEMPLATE ("modified%d",
    GST_PAD_SINK,
    GST_PAD_REQUEST,
    GST_STATIC_CAPS (SINK_CAPS)
    );

static void gst_ssim_class_init (GstSSimClass * klass);
static void gst_ssim_init (GstSSim * ssim);
static void gst_ssim_finalize (GObject * object);

static gboolean gst_ssim_setcaps (GstPad * pad, GstCaps * caps);
static gboolean gst_ssim_query (GstPad * pad, GstQuery * query);
static gboolean gst_ssim_src_event (GstPad * pad, GstEvent * event);
static gboolean gst_ssim_sink_event (GstPad * pad, GstEvent * event);

static GstPad *gst_ssim_request_new_pad (GstElement * element,
    GstPadTemplate * temp, const gchar * unused);
static void gst_ssim_release_pad (GstElement * element, GstPad * pad);

static GstStateChangeReturn gst_ssim_change_state (GstElement * element,
    GstStateChange transition);

static GstFlowReturn gst_ssim_collected (GstCollectPads * pads,
    gpointer user_data);

static GstElementClass *parent_class = NULL;

GType
gst_ssim_get_type (void)
{
  static GType ssim_type = 0;

  if (G_UNLIKELY (ssim_type == 0)) {
    static const GTypeInfo ssim_info = {
      sizeof (GstSSimClass), NULL, NULL,
      (GClassInitFunc) gst_ssim_class_init, NULL, NULL,
      sizeof (GstSSim), 0,
      (GInstanceInitFunc) gst_ssim_init,
    };

    ssim_type = g_type_register_static (GST_TYPE_ELEMENT, "GstSSim",
        &ssim_info, 0);
    GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "ssim", 0, "SSIM calculator");
  }
  return ssim_type;
}

static void
gst_ssim_post_message (GstSSim * ssim, GstBuffer * buffer, gfloat mssim,
    gfloat lowest, gfloat highest)
{
  GstMessage *m;
  guint64 offset;

  offset = GST_BUFFER_OFFSET (buffer);

  m = gst_message_new_element (GST_OBJECT_CAST (ssim),
      gst_structure_new ("SSIM",
          "offset", G_TYPE_UINT64, offset,
          "timestamp", GST_TYPE_CLOCK_TIME, GST_BUFFER_TIMESTAMP (buffer),
          "mean", G_TYPE_FLOAT, mssim,
          "lowest", G_TYPE_FLOAT, lowest,
          "highest", G_TYPE_FLOAT, highest, NULL));

  GST_DEBUG_OBJECT (GST_OBJECT (ssim), "Frame %" G_GINT64_FORMAT
      " @ %" GST_TIME_FORMAT " mean SSIM is %f, l-h is %f-%f", offset,
      GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (buffer)), mssim, lowest, highest);

  gst_element_post_message (GST_ELEMENT_CAST (ssim), m);
}

static GstCaps *
gst_ssim_src_getcaps (GstPad * pad)
{
  GstCaps *result;
  gchar *capstr;

  result = gst_caps_copy (gst_pad_get_pad_template_caps (pad));
  capstr = gst_caps_to_string (result);
  GST_DEBUG ("getsrccaps - return static caps: %s", capstr);
  g_free (capstr);
  return result;
}

static GstCaps *
gst_ssim_sink_getcaps (GstPad * pad)
{
  GstCaps *result = NULL;
  GstSSim *ssim;
  gchar *capstr;

  ssim = GST_SSIM (GST_PAD_PARENT (pad));

  GST_OBJECT_LOCK (ssim);

  result = gst_pad_get_fixed_caps_func (pad);
  capstr = gst_caps_to_string (result);
  GST_DEBUG ("getsinkcaps - return caps: %s", capstr);
  g_free (capstr);

  GST_OBJECT_UNLOCK (ssim);

  return result;
}

static void
calculate_mu (GstSSim * ssim, gfloat * outmu, guint8 * buf)
{
  gint oy, ox, iy, ix;

  for (oy = 0; oy < ssim->height; oy++) {
    for (ox = 0; ox < ssim->width; ox++) {
      gfloat mu = 0;
      gfloat elsumm;
      gint weight_y_base, weight_x_base;
      gint weight_offset;
      gint pixel_offset;
      gint winstart_y;
      gint wghstart_y;
      gint winend_y;
      gint winstart_x;
      gint wghstart_x;
      gint winend_x;
      gint winlen_x;
      gint winstride_x;
      gfloat weight;
      gint source_offset;

      source_offset = oy * ssim->width + ox;

      winstart_x = ssim->windows[source_offset].x_window_start;
      wghstart_x = ssim->windows[source_offset].x_weight_start;
      winend_x = ssim->windows[source_offset].x_window_end;
      winstart_y = ssim->windows[source_offset].y_window_start;
      wghstart_y = ssim->windows[source_offset].y_weight_start;
      winend_y = ssim->windows[source_offset].y_window_end;
      winlen_x = winend_x - winstart_x + 1;
      winstride_x = sizeof (gfloat) * winlen_x;
      elsumm = ssim->windows[source_offset].element_summ;

      switch (ssim->windowtype) {
        case 0:
          for (iy = winstart_y; iy <= winend_y; iy++) {
            pixel_offset = iy * ssim->width;
            for (ix = winstart_x; ix <= winend_x; ix++)
              mu += buf[pixel_offset + ix];
          }
          mu = mu / elsumm;
          break;
        case 1:

          weight_y_base = wghstart_y - winstart_y;
          weight_x_base = wghstart_x - winstart_x;

          for (iy = winstart_y; iy <= winend_y; iy++) {
            pixel_offset = iy * ssim->width;
            weight_offset = (weight_y_base + iy) * ssim->windowsize +
                weight_x_base;
            for (ix = winstart_x; ix <= winend_x; ix++) {
              weight = ssim->weights[weight_offset + ix];
              mu += weight * buf[pixel_offset + ix];
            }
          }
          mu = mu / elsumm;
          break;
      }
      outmu[oy * ssim->width + ox] = mu;
    }
  }

}

static void
calcssim_without_mu (GstSSim * ssim, guint8 * org, gfloat * orgmu, guint8 * mod,
    guint8 * out, gfloat * mean, gfloat * lowest, gfloat * highest)
{
  gint oy, ox, iy, ix;
  gfloat cumulative_ssim = 0;
  *lowest = G_MAXFLOAT;
  *highest = -G_MAXFLOAT;

  for (oy = 0; oy < ssim->height; oy++) {
    for (ox = 0; ox < ssim->width; ox++) {
      gfloat mu_o = 128, mu_m = 128;
      gdouble sigma_o = 0, sigma_m = 0, sigma_om = 0;
      gfloat tmp1 = 0, tmp2 = 0;
      gfloat elsumm = 0;
      gint weight_y_base, weight_x_base;
      gint weight_offset;
      gint pixel_offset;
      gint winstart_y;
      gint wghstart_y;
      gint winend_y;
      gint winstart_x;
      gint wghstart_x;
      gint winend_x;
      gfloat weight;
      gint source_offset;

      source_offset = oy * ssim->width + ox;

      winstart_x = ssim->windows[source_offset].x_window_start;
      wghstart_x = ssim->windows[source_offset].x_weight_start;
      winend_x = ssim->windows[source_offset].x_window_end;
      winstart_y = ssim->windows[source_offset].y_window_start;
      wghstart_y = ssim->windows[source_offset].y_weight_start;
      winend_y = ssim->windows[source_offset].y_window_end;
      elsumm = ssim->windows[source_offset].element_summ;

      weight_y_base = wghstart_y - winstart_y;
      weight_x_base = wghstart_x - winstart_x;
      switch (ssim->windowtype) {
        case 0:
          for (iy = winstart_y; iy <= winend_y; iy++) {
            guint8 *org_with_offset, *mod_with_offset;
            pixel_offset = iy * ssim->width;
            org_with_offset = &org[pixel_offset];
            mod_with_offset = &mod[pixel_offset];
            for (ix = winstart_x; ix <= winend_x; ix++) {
              tmp1 = org_with_offset[ix] - mu_o;
              sigma_o += tmp1 * tmp1;
              tmp2 = mod_with_offset[ix] - mu_m;
              sigma_m += tmp2 * tmp2;
              sigma_om += tmp1 * tmp2;
            }
          }
          break;
        case 1:

          weight_y_base = wghstart_y - winstart_y;
          weight_x_base = wghstart_x - winstart_x;

          for (iy = winstart_y; iy <= winend_y; iy++) {
            guint8 *org_with_offset, *mod_with_offset;
            gfloat *weights_with_offset;
            gfloat wt1, wt2;
            pixel_offset = iy * ssim->width;
            weight_offset = (weight_y_base + iy) * ssim->windowsize +
                weight_x_base;
            org_with_offset = &org[pixel_offset];
            mod_with_offset = &mod[pixel_offset];
            weights_with_offset = &ssim->weights[weight_offset];
            for (ix = winstart_x; ix <= winend_x; ix++) {
              weight = weights_with_offset[ix];
              tmp1 = org_with_offset[ix] - mu_o;
              tmp2 = mod_with_offset[ix] - mu_m;
              wt1 = weight * tmp1;
              wt2 = weight * tmp2;
              sigma_o += wt1 * tmp1;
              sigma_m += wt2 * tmp2;
              sigma_om += wt1 * tmp2;
            }
          }
          break;
      }
      sigma_o = sqrt (sigma_o / elsumm);
      sigma_m = sqrt (sigma_m / elsumm);
      sigma_om = sigma_om / elsumm;
      tmp1 = (2 * mu_o * mu_m + ssim->const1) * (2 * sigma_om + ssim->const2) /
          ((mu_o * mu_o + mu_m * mu_m + ssim->const1) *
          (sigma_o * sigma_o + sigma_m * sigma_m + ssim->const2));

      /* SSIM can go negative, that's why it is
         127 + index * 128 instead of index * 255 */
      out[oy * ssim->width + ox] = 127 + tmp1 * 128;
      *lowest = MIN (*lowest, tmp1);
      *highest = MAX (*highest, tmp1);
      cumulative_ssim += tmp1;
    }
  }
  *mean = cumulative_ssim / (ssim->width * ssim->height);
}

static void
calcssim_canonical (GstSSim * ssim, guint8 * org, gfloat * orgmu, guint8 * mod,
    guint8 * out, gfloat * mean, gfloat * lowest, gfloat * highest)
{
  gint oy, ox, iy, ix;
  gfloat cumulative_ssim = 0;
  *lowest = G_MAXFLOAT;
  *highest = -G_MAXFLOAT;

  for (oy = 0; oy < ssim->height; oy++) {
    for (ox = 0; ox < ssim->width; ox++) {
      gfloat mu_o = 0, mu_m = 0;
      gdouble sigma_o = 0, sigma_m = 0, sigma_om = 0;
      gfloat tmp1, tmp2;
      gfloat elsumm = 0;
      gint weight_y_base, weight_x_base;
      gint weight_offset;
      gint pixel_offset;
      gint winstart_y;
      gint wghstart_y;
      gint winend_y;
      gint winstart_x;
      gint wghstart_x;
      gint winend_x;
      gint winlen_x;
      gint winstride_x;
      gfloat weight;
      gint source_offset;

      source_offset = oy * ssim->width + ox;

      winstart_x = ssim->windows[source_offset].x_window_start;
      wghstart_x = ssim->windows[source_offset].x_weight_start;
      winend_x = ssim->windows[source_offset].x_window_end;
      winstart_y = ssim->windows[source_offset].y_window_start;
      wghstart_y = ssim->windows[source_offset].y_weight_start;
      winend_y = ssim->windows[source_offset].y_window_end;
      winlen_x = winend_x - winstart_x + 1;
      winstride_x = sizeof (gfloat) * winlen_x;
      elsumm = ssim->windows[source_offset].element_summ;

      switch (ssim->windowtype) {
        case 0:
          for (iy = winstart_y; iy <= winend_y; iy++) {
            pixel_offset = iy * ssim->width;
            for (ix = winstart_x; ix <= winend_x; ix++) {
              mu_m += mod[pixel_offset + ix];
            }
          }
          mu_m = mu_m / elsumm;
          mu_o = orgmu[oy * ssim->width + ox];
          for (iy = winstart_y; iy <= winend_y; iy++) {
            pixel_offset = iy * ssim->width;
            for (ix = winstart_x; ix <= winend_x; ix++) {
              tmp1 = org[pixel_offset + ix] - mu_o;
              tmp2 = mod[pixel_offset + ix] - mu_m;
              sigma_o += tmp1 * tmp1;
              sigma_m += tmp2 * tmp2;
              sigma_om += tmp1 * tmp2;
            }
          }
          break;
        case 1:

          weight_y_base = wghstart_y - winstart_y;
          weight_x_base = wghstart_x - winstart_x;

          for (iy = winstart_y; iy <= winend_y; iy++) {
            pixel_offset = iy * ssim->width;
            weight_offset = (weight_y_base + iy) * ssim->windowsize +
                weight_x_base;
            for (ix = winstart_x; ix <= winend_x; ix++) {
              weight = ssim->weights[weight_offset + ix];
              mu_o += weight * org[pixel_offset + ix];
              mu_m += weight * mod[pixel_offset + ix];
            }
          }
          mu_m = mu_m / elsumm;
          mu_o = orgmu[oy * ssim->width + ox];
          for (iy = winstart_y; iy <= winend_y; iy++) {
            gfloat *weights_with_offset;
            guint8 *org_with_offset, *mod_with_offset;
            gfloat wt1, wt2;
            pixel_offset = iy * ssim->width;
            weight_offset = (weight_y_base + iy) * ssim->windowsize +
                weight_x_base;
            weights_with_offset = &ssim->weights[weight_offset];
            org_with_offset = &org[pixel_offset];
            mod_with_offset = &mod[pixel_offset];
            for (ix = winstart_x; ix <= winend_x; ix++) {
              weight = weights_with_offset[ix];
              tmp1 = org_with_offset[ix] - mu_o;
              tmp2 = mod_with_offset[ix] - mu_m;
              wt1 = weight * tmp1;
              wt2 = weight * tmp2;
              sigma_o += wt1 * tmp1;
              sigma_m += wt2 * tmp2;
              sigma_om += wt1 * tmp2;
            }
          }
          break;
      }
      sigma_o = sqrt (sigma_o / elsumm);
      sigma_m = sqrt (sigma_m / elsumm);
      sigma_om = sigma_om / elsumm;
      tmp1 = (2 * mu_o * mu_m + ssim->const1) * (2 * sigma_om + ssim->const2) /
          ((mu_o * mu_o + mu_m * mu_m + ssim->const1) *
          (sigma_o * sigma_o + sigma_m * sigma_m + ssim->const2));

      /* SSIM can go negative, that's why it is
         127 + index * 128 instead of index * 255 */
      out[oy * ssim->width + ox] = 127 + tmp1 * 128;
      *lowest = MIN (*lowest, tmp1);
      *highest = MAX (*highest, tmp1);
      cumulative_ssim += tmp1;
    }
  }
  *mean = cumulative_ssim / (ssim->width * ssim->height);
}


/* the first caps we receive on any of the sinkpads will define the caps for all
 * the other sinkpads because we can only measure streams with the same caps.
 */
static gboolean
gst_ssim_setcaps (GstPad * pad, GstCaps * caps)
{
  GstSSim *ssim;
  GList *pads;
  const char *media_type;
  GstStructure *capsstr;
  gint width, height, fps_n, fps_d;
  guint32 fourcc;

  ssim = GST_SSIM (GST_PAD_PARENT (pad));

  GST_DEBUG_OBJECT (ssim, "setting caps on pad %p,%s to %" GST_PTR_FORMAT, pad,
      GST_PAD_NAME (pad), caps);

  capsstr = gst_caps_get_structure (caps, 0);
  gst_structure_get_int (capsstr, "width", &width);
  gst_structure_get_int (capsstr, "height", &height);
  gst_structure_get_fraction (capsstr, "framerate", &fps_n, &fps_d);
  gst_structure_get_fourcc (capsstr, "format", &fourcc);

  GST_OBJECT_LOCK (ssim);

  /* Sink caps are stored only once. At the moment it doesn't feel
   * right to measure streams with variable caps.
   */
  if (G_UNLIKELY (!ssim->sinkcaps)) {
    GstStructure *newstr;
    GValue list = { 0, }
    , fourcc = {
    0,};

    g_value_init (&list, GST_TYPE_LIST);
    g_value_init (&fourcc, GST_TYPE_FOURCC);

    gst_value_set_fourcc (&fourcc, GST_MAKE_FOURCC ('I', '4', '2', '0'));
    gst_value_list_append_value (&list, &fourcc);
    gst_value_set_fourcc (&fourcc, GST_MAKE_FOURCC ('Y', 'V', '1', '2'));
    gst_value_list_append_value (&list, &fourcc);
    gst_value_set_fourcc (&fourcc, GST_MAKE_FOURCC ('Y', '4', '1', 'B'));
    gst_value_list_append_value (&list, &fourcc);
    gst_value_set_fourcc (&fourcc, GST_MAKE_FOURCC ('Y', '4', '2', 'B'));
    gst_value_list_append_value (&list, &fourcc);

    newstr = gst_structure_new ("video/x-raw-yuv", NULL);
    gst_structure_set (newstr, "width", G_TYPE_INT, width, NULL);
    gst_structure_set (newstr, "height", G_TYPE_INT, height, NULL);
    gst_structure_set_value (newstr, "format", &list);

    ssim->sinkcaps = gst_caps_new_full (newstr, NULL);

    g_value_unset (&list);
    g_value_unset (&fourcc);
  }

  if (G_UNLIKELY (!ssim->srccaps)) {
    GstStructure *newstr;

    newstr = gst_structure_new ("video/x-raw-gray", NULL);
    gst_structure_set (newstr, "width", G_TYPE_INT, width, NULL);
    gst_structure_set (newstr, "height", G_TYPE_INT, height, NULL);
    gst_structure_set (newstr, "framerate", GST_TYPE_FRACTION, fps_n, fps_d,
        NULL);
    /* Calculates SSIM only for Y channel, hence the output is monochrome.
     * TODO: an option (a mask?) to calculate SSIM for more than one channel,
     * will probably output RGB, one metric per channel...that would
     * look kinda funny :)
     */
    gst_structure_set (newstr, "bpp", G_TYPE_INT, 8, "depth", G_TYPE_INT, 8,
        NULL);

    ssim->srccaps = gst_caps_new_full (newstr, NULL);
  }

  pads = GST_ELEMENT (ssim)->pads;
  while (pads) {
    GstPadDirection direction;
    GstPad *otherpad = GST_PAD (pads->data);
    direction = gst_pad_get_direction (otherpad);

    GST_DEBUG_OBJECT (ssim, "checking caps on pad %p", otherpad);
    if (direction == GST_PAD_SINK) {
      gchar *capstr;
      capstr = gst_caps_to_string (GST_PAD_CAPS (otherpad));
      GST_DEBUG_OBJECT (ssim, "old caps on pad %p,%s were %s", otherpad,
          GST_PAD_NAME (otherpad), capstr);
      g_free (capstr);
      gst_caps_replace (&GST_PAD_CAPS (otherpad), ssim->sinkcaps);
      capstr = gst_caps_to_string (ssim->sinkcaps);
      GST_DEBUG_OBJECT (ssim, "new caps on pad %p,%s are %s", otherpad,
          GST_PAD_NAME (otherpad), capstr);
      g_free (capstr);
    } else if (direction == GST_PAD_SRC) {
      gst_caps_replace (&GST_PAD_CAPS (otherpad), ssim->srccaps);
    }
    pads = g_list_next (pads);
  }

  /* parse caps now */
  media_type = gst_structure_get_name (capsstr);
  GST_DEBUG_OBJECT (ssim, "media type is %s", media_type);
  if (strcmp (media_type, "video/x-raw-yuv") == 0) {
    ssim->width = width;
    ssim->height = height;
    ssim->frame_rate = fps_n;
    ssim->frame_rate_base = fps_d;

    GST_INFO_OBJECT (ssim, "parse_caps sets ssim to yuv format "
        "%d, %dx%d, %d/%d fps", fourcc, ssim->width, ssim->height,
        ssim->frame_rate, ssim->frame_rate_base);

    /* Only planar formats are supported.
     * TODO: implement support for interleaved formats
     * Only YUV formats are supported. There's no sense in calculating the
     * index for R, G or B channels separately.
     */
    switch (fourcc) {
      case GST_MAKE_FOURCC ('I', '4', '2', '0'):
      case GST_MAKE_FOURCC ('Y', 'V', '1', '2'):
      case GST_MAKE_FOURCC ('Y', '4', '1', 'B'):
      case GST_MAKE_FOURCC ('Y', '4', '2', 'B'):
        break;
      default:
        goto not_supported;
    }

  } else {
    goto not_supported;
  }

  GST_OBJECT_UNLOCK (ssim);
  return TRUE;
  /* ERRORS */
not_supported:
  {
    GST_OBJECT_UNLOCK (ssim);
    GST_DEBUG_OBJECT (ssim, "unsupported format set as caps");
    return FALSE;
  }
}

static gboolean
gst_ssim_query_latency (GstSSim * ssim, GstQuery * query)
{
  GstClockTime min, max;
  gboolean live;
  gboolean res;
  GstIterator *it;
  gboolean done;

  res = TRUE;
  done = FALSE;

  live = FALSE;
  min = 0;
  max = GST_CLOCK_TIME_NONE;

  /* Take maximum of all latency values */
  it = gst_element_iterate_sink_pads (GST_ELEMENT_CAST (ssim));
  while (!done) {
    GstIteratorResult ires;

    gpointer item;

    ires = gst_iterator_next (it, &item);
    switch (ires) {
      case GST_ITERATOR_DONE:
        done = TRUE;
        break;
      case GST_ITERATOR_OK:
      {
        GstPad *pad = GST_PAD_CAST (item);
        GstQuery *peerquery;
        GstClockTime min_cur, max_cur;
        gboolean live_cur;

        peerquery = gst_query_new_latency ();

        /* Ask peer for latency */
        res &= gst_pad_peer_query (pad, peerquery);

        /* take max from all valid return values */
        if (res) {
          gst_query_parse_latency (peerquery, &live_cur, &min_cur, &max_cur);

          if (min_cur > min)
            min = min_cur;

          if (max_cur != GST_CLOCK_TIME_NONE &&
              ((max != GST_CLOCK_TIME_NONE && max_cur > max) ||
                  (max == GST_CLOCK_TIME_NONE)))
            max = max_cur;

          live = live || live_cur;
        }

        gst_query_unref (peerquery);
        gst_object_unref (pad);
        break;
      }
      case GST_ITERATOR_RESYNC:
        live = FALSE;
        min = 0;
        max = GST_CLOCK_TIME_NONE;
        res = TRUE;
        gst_iterator_resync (it);
        break;
      default:
        res = FALSE;
        done = TRUE;
        break;
    }
  }
  gst_iterator_free (it);

  if (res) {
    /* store the results */
    GST_DEBUG_OBJECT (ssim, "Calculated total latency: live %s, min %"
        GST_TIME_FORMAT ", max %" GST_TIME_FORMAT,
        (live ? "yes" : "no"), GST_TIME_ARGS (min), GST_TIME_ARGS (max));
    gst_query_set_latency (query, live, min, max);
  }

  return res;
}

static gboolean
gst_ssim_query_duration (GstSSim * ssim, GstQuery * query)
{
  gint64 max, min;
  gboolean res;
  GstFormat format;
  GstIterator *it;
  gboolean done;

  /* parse format */
  gst_query_parse_duration (query, &format, NULL);

  max = -1;
  min = G_MAXINT64;
  res = TRUE;
  done = FALSE;

  it = gst_element_iterate_sink_pads (GST_ELEMENT_CAST (ssim));
  while (!done) {
    GstIteratorResult ires;

    gpointer item;

    ires = gst_iterator_next (it, &item);
    switch (ires) {
      case GST_ITERATOR_DONE:
        done = TRUE;
        break;
      case GST_ITERATOR_OK:
      {
        GstPad *pad = GST_PAD_CAST (item);

        gint64 duration;

        /* ask sink peer for duration */
        res &= gst_pad_query_peer_duration (pad, &format, &duration);
        /* take min&max from all valid return values */
        if (res) {
          /* valid unknown length, stop searching */
          if (duration == -1) {
            max = duration;
            done = TRUE;
          }
          /* else see if bigger than current max */
          else {
            if (duration > max)
              max = duration;
            if (duration < min)
              min = duration;
          }
        }
        gst_object_unref (pad);
        break;
      }
      case GST_ITERATOR_RESYNC:
        max = -1;
        min = G_MAXINT64;
        res = TRUE;
        gst_iterator_resync (it);
        break;
      default:
        res = FALSE;
        done = TRUE;
        break;
    }
  }
  gst_iterator_free (it);

  if (res) {
    /* and store the max */
    GST_DEBUG_OBJECT (ssim, "Total duration in format %s: %"
        GST_TIME_FORMAT, gst_format_get_name (format), GST_TIME_ARGS (min));
    gst_query_set_duration (query, format, min);
  }

  return res;
}


static gboolean
gst_ssim_query (GstPad * pad, GstQuery * query)
{
  GstSSim *ssim = GST_SSIM (gst_pad_get_parent (pad));
  gboolean res = FALSE;

  switch (GST_QUERY_TYPE (query)) {
    case GST_QUERY_POSITION:
    {
      GstFormat format;

      gst_query_parse_position (query, &format, NULL);

      switch (format) {
        case GST_FORMAT_TIME:
          /* FIXME, bring to stream time, might be tricky */
          gst_query_set_position (query, format, ssim->timestamp);
          res = TRUE;
          break;
        case GST_FORMAT_DEFAULT:
          gst_query_set_position (query, format, ssim->offset);
          res = TRUE;
          break;
        default:
          break;
      }
      break;
    }
    case GST_QUERY_DURATION:
      res = gst_ssim_query_duration (ssim, query);
      break;
    case GST_QUERY_LATENCY:
      res = gst_ssim_query_latency (ssim, query);
      break;
    default:
      /* FIXME, needs a custom query handler because we have multiple
       * sinkpads
       */
      res = gst_pad_query_default (pad, query);
      break;
  }

  gst_object_unref (ssim);
  return res;
}

static gboolean
forward_event_func (GstPad * pad, GValue * ret, GstEvent * event)
{
  gst_event_ref (event);
  GST_LOG_OBJECT (pad, "About to send event %s", GST_EVENT_TYPE_NAME (event));
  if (!gst_pad_push_event (pad, event)) {
    g_value_set_boolean (ret, FALSE);
    GST_LOG_OBJECT (pad, "Sending event  %p (%s) failed.",
        event, GST_EVENT_TYPE_NAME (event));
  } else {
    GST_LOG_OBJECT (pad, "Sent event  %p (%s).",
        event, GST_EVENT_TYPE_NAME (event));
  }
  gst_object_unref (pad);
  return TRUE;
}

/* forwards the event to all sinkpads, takes ownership of the
 * event
 *
 * Returns: TRUE if the event could be forwarded on all
 * sinkpads.
 */
static gboolean
forward_event (GstSSim * ssim, GstEvent * event)
{
  GstIterator *it;
  GValue vret = { 0 };

  GST_LOG_OBJECT (ssim, "Forwarding event %p (%s)", event,
      GST_EVENT_TYPE_NAME (event));

  g_value_init (&vret, G_TYPE_BOOLEAN);
  g_value_set_boolean (&vret, TRUE);
  it = gst_element_iterate_sink_pads (GST_ELEMENT_CAST (ssim));
  gst_iterator_fold (it, (GstIteratorFoldFunction) forward_event_func, &vret,
      event);
  gst_iterator_free (it);
  gst_event_unref (event);

  return g_value_get_boolean (&vret);
}

static gboolean
gst_ssim_src_event (GstPad * pad, GstEvent * event)
{
  GstSSim *ssim;
  gboolean result;

  ssim = GST_SSIM (gst_pad_get_parent (pad));

  switch (GST_EVENT_TYPE (event)) {
    case GST_EVENT_QOS:
      /* QoS might be tricky */
      result = FALSE;
      break;
    case GST_EVENT_SEEK:
    {
      GstSeekFlags flags;
      GstSeekType curtype;
      gint64 cur;

      /* parse the seek parameters */
      gst_event_parse_seek (event, &ssim->segment_rate, NULL, &flags, &curtype,
          &cur, NULL, NULL);

      /* check if we are flushing */
      if (flags & GST_SEEK_FLAG_FLUSH) {
        /* make sure we accept nothing anymore and return WRONG_STATE */
        gst_collect_pads_set_flushing (ssim->collect, TRUE);

        /* flushing seek, start flush downstream, the flush will be done
         * when all pads received a FLUSH_STOP. */
        gst_pad_push_event (pad, gst_event_new_flush_start ());
      }
      /* now wait for the collected to be finished and mark a new
       * segment */
      GST_OBJECT_LOCK (ssim->collect);
      if (curtype == GST_SEEK_TYPE_SET)
        ssim->segment_position = cur;
      else
        ssim->segment_position = 0;
      {
        GstSSimOutputContext *c;
        gint i = 0;
        for (i = 0; i < ssim->src->len; i++) {
          c = (GstSSimOutputContext *) g_ptr_array_index (ssim->src, i);
          c->segment_pending = TRUE;
        }
      }
      GST_OBJECT_UNLOCK (ssim->collect);

      result = forward_event (ssim, event);
      break;
    }
    case GST_EVENT_NAVIGATION:
      /* navigation is rather pointless. */
      result = FALSE;
      break;
    default:
      /* just forward the rest for now */
      result = forward_event (ssim, event);
      break;
  }
  gst_object_unref (ssim);

  return result;
}

static gboolean
gst_ssim_sink_event (GstPad * pad, GstEvent * event)
{
  GstSSim *ssim;
  gboolean ret;

  ssim = GST_SSIM (gst_pad_get_parent (pad));

  GST_DEBUG ("Got %s event on pad %s:%s", GST_EVENT_TYPE_NAME (event),
      GST_DEBUG_PAD_NAME (pad));

  switch (GST_EVENT_TYPE (event)) {
    case GST_EVENT_NEWSEGMENT:
    {
      gboolean update;
      gdouble rate;
      gdouble applied_rate;
      GstFormat format;
      gint64 start;
      gint64 stop;
      gint64 position;
      gst_event_parse_new_segment_full (event, &update, &rate, &applied_rate,
          &format, &start, &stop, &position);
      GST_DEBUG ("NEWSEGMENTEVENT: update(%d), rate(%f), app_rate(%f), "
          "format(%d), start(%" GST_TIME_FORMAT ") stop(%" GST_TIME_FORMAT ") "
          "position(%" GST_TIME_FORMAT ")", update, rate, applied_rate, format,
          GST_TIME_ARGS (start), GST_TIME_ARGS (stop),
          GST_TIME_ARGS (position));
      break;
    }
    case GST_EVENT_FLUSH_STOP:
      /* mark a pending new segment. This event is synchronized
       * with the streaming thread so we can safely update the
       * variable without races. It's somewhat weird because we
       * assume the collectpads forwarded the FLUSH_STOP past us
       * and downstream (using our source pad, the bastard!).
       */
    {
      GstSSimOutputContext *c;
      gint i = 0;
      for (i = 0; i < ssim->src->len; i++) {
        c = (GstSSimOutputContext *) g_ptr_array_index (ssim->src, i);
        c->segment_pending = TRUE;
      }
    }
      break;
    default:
      break;
  }

  /* now GstCollectPads can take care of the rest, e.g. EOS */
  GST_DEBUG ("Dispatching %s event on pad %s:%s", GST_EVENT_TYPE_NAME (event),
      GST_DEBUG_PAD_NAME (pad));
  ret = ssim->collect_event (pad, event);
  GST_DEBUG ("Event %s on pad %s:%s is dispatched", GST_EVENT_TYPE_NAME (event),
      GST_DEBUG_PAD_NAME (pad));
  gst_object_unref (ssim);
  return ret;
}

static void
gst_ssim_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstSSim *ssim;

  ssim = GST_SSIM (object);

  switch (prop_id) {
    case PROP_SSIM_TYPE:
      ssim->ssimtype = g_value_get_int (value);
      break;
    case PROP_WINDOW_TYPE:
      ssim->windowtype = g_value_get_int (value);
      g_free (ssim->windows);
      ssim->windows = NULL;
      break;
    case PROP_WINDOW_SIZE:
      ssim->windowsize = g_value_get_int (value);
      g_free (ssim->windows);
      ssim->windows = NULL;
      break;
    case PROP_GAUSS_SIGMA:
      ssim->sigma = g_value_get_float (value);
      g_free (ssim->windows);
      ssim->windows = NULL;
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
gst_ssim_get_property (GObject * object, guint prop_id, GValue * value,
    GParamSpec * pspec)
{
  GstSSim *ssim;

  ssim = GST_SSIM (object);

  switch (prop_id) {
    case PROP_SSIM_TYPE:
      g_value_set_int (value, ssim->ssimtype);
      break;
    case PROP_WINDOW_TYPE:
      g_value_set_int (value, ssim->windowtype);
      break;
    case PROP_WINDOW_SIZE:
      g_value_set_int (value, ssim->windowsize);
      break;
    case PROP_GAUSS_SIGMA:
      g_value_set_float (value, ssim->sigma);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}


static void
gst_ssim_class_init (GstSSimClass * klass)
{
  GObjectClass *gobject_class = (GObjectClass *) klass;
  GstElementClass *gstelement_class = (GstElementClass *) klass;

  gobject_class->set_property = gst_ssim_set_property;
  gobject_class->get_property = gst_ssim_get_property;
  gobject_class->finalize = GST_DEBUG_FUNCPTR (gst_ssim_finalize);

  g_object_class_install_property (G_OBJECT_CLASS (klass), PROP_SSIM_TYPE,
      g_param_spec_int ("ssim-type", "SSIM type",
          "Type of the SSIM metric. 0 - canonical. 1 - with fixed mu "
          "(almost the same results, but roughly 20% faster)",
          0, 1, 0, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (G_OBJECT_CLASS (klass), PROP_WINDOW_TYPE,
      g_param_spec_int ("window-type", "Window type",
          "Type of the weighting in the window. "
          "0 - no weighting. 1 - Gaussian weighting (controlled by \"sigma\")",
          0, 1, 1, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (G_OBJECT_CLASS (klass), PROP_WINDOW_SIZE,
      g_param_spec_int ("window-size", "Window size",
          "Size of a window.", 1, 22, 11,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (G_OBJECT_CLASS (klass), PROP_GAUSS_SIGMA,
      g_param_spec_float ("gauss-sigma", "Deviation (for Gauss function)",
          "Used to calculate Gussian weights "
          "(only when using Gaussian window).",
          G_MINFLOAT, 10, 1.5, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  gst_element_class_add_pad_template (gstelement_class,
      gst_static_pad_template_get (&gst_ssim_src_template));
  gst_element_class_add_pad_template (gstelement_class,
      gst_static_pad_template_get (&gst_ssim_sink_original_template));
  gst_element_class_add_pad_template (gstelement_class,
      gst_static_pad_template_get (&gst_ssim_sink_modified_template));
  gst_element_class_set_details_simple (gstelement_class, "SSim",
      "Filter/Analyzer/Video",
      "Calculate Y-SSIM for n+2 YUV video streams",
      "Руслан Ижбулатов <lrn1986 _at_ gmail _dot_ com>");

  parent_class = g_type_class_peek_parent (klass);

  gstelement_class->request_new_pad =
      GST_DEBUG_FUNCPTR (gst_ssim_request_new_pad);
  gstelement_class->release_pad = GST_DEBUG_FUNCPTR (gst_ssim_release_pad);

  gstelement_class->change_state = GST_DEBUG_FUNCPTR (gst_ssim_change_state);
}

static GstPad *
gst_ssim_request_new_pad (GstElement * element, GstPadTemplate * templ,
    const gchar * padname)
{
  gchar *name;
  GstSSim *ssim;
  GstPad *newpad;
  GstPad *newsrc;
  gint padcount;
  GstPadTemplate *template;
  gint num = -1;

  if (templ->direction != GST_PAD_SINK)
    goto not_sink;

  ssim = GST_SSIM (element);

  padcount = ssim->padcount;

  GST_DEBUG_OBJECT (ssim, "number of pads = %d", padcount);

  if (padname)
    GST_DEBUG_OBJECT (ssim, "reqested pad %s", padname);
  else
    goto unnamed_pad;

  if (strcmp (padname, "original") == 0) {
    newpad = gst_pad_new_from_template (templ, "original");
    GST_DEBUG_OBJECT (ssim, "request new sink pad original");
    ssim->orig = newpad;
  } else if (strncmp (padname, "modified", 8) == 0) {
    const gchar *numstr = &padname[8];
    num = strtol (numstr, NULL, 10);
    if (errno == EINVAL || errno == ERANGE)
      goto bad_name;
    newpad = gst_pad_new_from_template (templ, padname);
    GST_DEBUG_OBJECT (ssim, "request new sink pad %s", padname);
  } else
    goto bad_name;

  gst_pad_set_getcaps_function (newpad,
      GST_DEBUG_FUNCPTR (gst_ssim_sink_getcaps));
  gst_pad_set_setcaps_function (newpad, GST_DEBUG_FUNCPTR (gst_ssim_setcaps));
  gst_collect_pads_add_pad (ssim->collect, newpad, sizeof (GstCollectData));

  /* FIXME: hacked way to override/extend the event function of
   * GstCollectPads; because it sets its own event function giving the
   * element no access to events
   */
  GST_DEBUG_OBJECT (ssim, "Current collect_event is %p, changing to %p",
      ssim->collect_event, GST_PAD_EVENTFUNC (newpad));
  ssim->collect_event = (GstPadEventFunction) GST_PAD_EVENTFUNC (newpad);
  gst_pad_set_event_function (newpad, GST_DEBUG_FUNCPTR (gst_ssim_sink_event));

  GST_DEBUG_OBJECT (ssim, "Adding a pad...");
  /* takes ownership of the pad */
  if (!gst_element_add_pad (GST_ELEMENT (ssim), newpad))
    goto could_not_add_sink;
  else
    /* increment pad counter */
#if GLIB_CHECK_VERSION(2,29,5)
    padcount = g_atomic_int_add (&ssim->padcount, 1);
#else
    padcount = g_atomic_int_exchange_and_add (&ssim->padcount, 1);
#endif

  if (num >= 0) {
    GstSSimOutputContext *c;
    GObject *asobject;
    template = gst_static_pad_template_get (&gst_ssim_src_template);
    name = g_strdup_printf ("src%d", num);
    newsrc = gst_pad_new_from_template (template, name);
    GST_DEBUG_OBJECT (ssim, "creating src pad %s", name);
    g_free (name);
    gst_object_unref (template);

    gst_pad_set_getcaps_function (newsrc,
        GST_DEBUG_FUNCPTR (gst_ssim_src_getcaps));
    gst_pad_set_query_function (newsrc, GST_DEBUG_FUNCPTR (gst_ssim_query));
    gst_pad_set_event_function (newsrc, GST_DEBUG_FUNCPTR (gst_ssim_src_event));

    if (!gst_element_add_pad (GST_ELEMENT (ssim), newsrc))
      goto could_not_add_src;

    c = g_new (GstSSimOutputContext, 1);
    c->pad = newsrc;
    asobject = G_OBJECT (newsrc);
    g_object_set_data (G_OBJECT (newpad), "ssim-match-output-context", c);
    g_ptr_array_add (ssim->src, (gpointer) c);
  }

  return newpad;

  /* errors */
bad_name:
  {
    g_warning ("gstssim: request new pad with bad name %s (must be "
        "'modified')\n", padname);
    return NULL;
  }
unnamed_pad:
  {
    g_warning ("gstssim: request new pad without a name (must be "
        "'modified')\n");
    return NULL;
  }
not_sink:
  {
    g_warning ("gstssim: request new pad that is not a SINK pad\n");
    return NULL;
  }
could_not_add_src:
  {
    GST_DEBUG_OBJECT (ssim, "could not add src pad");
    gst_object_unref (newsrc);
  }
could_not_add_sink:
  {
    GST_DEBUG_OBJECT (ssim, "could not add sink pad");
    gst_collect_pads_remove_pad (ssim->collect, newpad);
    gst_object_unref (newpad);
    return NULL;
  }
}

static void
gst_ssim_release_pad (GstElement * element, GstPad * pad)
{
  GstSSim *ssim;

  ssim = GST_SSIM (element);

  GST_DEBUG_OBJECT (ssim, "release pad %s:%s", GST_DEBUG_PAD_NAME (pad));

  gst_collect_pads_remove_pad (ssim->collect, pad);
  gst_element_remove_pad (element, pad);
}


static void
gst_ssim_init (GstSSim * ssim)
{
  ssim->windowsize = 11;
  ssim->windowtype = 1;
  ssim->windows = NULL;
  ssim->sigma = 1.5;
  ssim->ssimtype = 0;
  ssim->src = g_ptr_array_new ();
  ssim->padcount = 0;
  ssim->collect_event = NULL;
  ssim->sinkcaps = NULL;

  /* keep track of the sinkpads requested */
  ssim->collect = gst_collect_pads_new ();
  gst_collect_pads_set_function (ssim->collect,
      GST_DEBUG_FUNCPTR (gst_ssim_collected), ssim);
}

static void
gst_ssim_finalize (GObject * object)
{
  GstSSim *ssim = GST_SSIM (object);

  gst_object_unref (ssim->collect);
  ssim->collect = NULL;

  g_free (ssim->windows);
  ssim->windows = NULL;

  g_free (ssim->weights);
  ssim->weights = NULL;

  if (ssim->sinkcaps)
    gst_caps_unref (ssim->sinkcaps);
  if (ssim->srccaps)
    gst_caps_unref (ssim->srccaps);

  g_ptr_array_free (ssim->src, TRUE);

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

typedef gfloat (*GstSSimWeightFunc) (GstSSim * ssim, gint y, gint x);

static gfloat
gst_ssim_weight_func_none (GstSSim * ssim, gint y, gint x)
{
  return 1;
}

static gfloat
gst_ssim_weight_func_gauss (GstSSim * ssim, gint y, gint x)
{
  gfloat coord = sqrt (x * x + y * y);
  return exp (-1 * (coord * coord) / (2 * ssim->sigma * ssim->sigma)) /
      (ssim->sigma * sqrt (2 * G_PI));
}

static gboolean
gst_ssim_regenerate_windows (GstSSim * ssim)
{
  gint windowiseven;
  gint y, x, y2, x2;
  GstSSimWeightFunc func;
  gfloat normal_summ = 0;
  gint normal_count = 0;

  g_free (ssim->weights);

  ssim->weights = g_new (gfloat, ssim->windowsize * ssim->windowsize);

  windowiseven = ((gint) ssim->windowsize / 2) * 2 == ssim->windowsize ? 1 : 0;

  g_free (ssim->windows);

  ssim->windows = g_new (GstSSimWindowCache, ssim->height * ssim->width);

  switch (ssim->windowtype) {
    case 0:
      func = gst_ssim_weight_func_none;
      break;
    case 1:
      func = gst_ssim_weight_func_gauss;
      break;
    default:
      GST_WARNING_OBJECT (ssim, "unknown window type - %d. Defaulting to %d",
          ssim->windowtype, 1);
      ssim->windowtype = 1;
      func = gst_ssim_weight_func_gauss;
  }

  for (y = 0; y < ssim->windowsize; y++) {
    gint yoffset = y * ssim->windowsize;
    for (x = 0; x < ssim->windowsize; x++) {
      ssim->weights[yoffset + x] = func (ssim, x - ssim->windowsize / 2 +
          windowiseven, y - ssim->windowsize / 2 + windowiseven);
      normal_summ += ssim->weights[yoffset + x];
      normal_count++;
    }
  }

  for (y = 0; y < ssim->height; y++) {
    for (x = 0; x < ssim->width; x++) {
      GstSSimWindowCache win;
      gint element_count = 0;

      win.x_window_start = x - ssim->windowsize / 2 + windowiseven;
      win.x_weight_start = 0;
      if (win.x_window_start < 0) {
        win.x_weight_start = -win.x_window_start;
        win.x_window_start = 0;
      }

      win.x_window_end = x + ssim->windowsize / 2;
      if (win.x_window_end >= ssim->width)
        win.x_window_end = ssim->width - 1;

      win.y_window_start = y - ssim->windowsize / 2 + windowiseven;
      win.y_weight_start = 0;
      if (win.y_window_start < 0) {
        win.y_weight_start = -win.y_window_start;
        win.y_window_start = 0;
      }

      win.y_window_end = y + ssim->windowsize / 2;
      if (win.y_window_end >= ssim->height)
        win.y_window_end = ssim->height - 1;

      win.element_summ = 0;
      element_count = (win.y_window_end - win.y_window_start + 1) *
          (win.x_window_end - win.x_window_start + 1);
      if (element_count == normal_count)
        win.element_summ = normal_summ;
      else {
        for (y2 = win.y_weight_start; y2 < ssim->windowsize; y2++) {
          for (x2 = win.x_weight_start; x2 < ssim->windowsize; x2++) {
            win.element_summ += ssim->weights[y2 * ssim->windowsize + x2];
          }
        }
      }
      ssim->windows[(y * ssim->width + x)] = win;
    }
  }

  /* FIXME: while 0.01 and 0.03 are pretty much static, the 255 implies that
   * we're working with 8-bit-per-color-component format, which may not be true
   */
  ssim->const1 = 0.01 * 255 * 0.01 * 255;
  ssim->const2 = 0.03 * 255 * 0.03 * 255;
  return TRUE;
}

static GstFlowReturn
gst_ssim_collected (GstCollectPads * pads, gpointer user_data)
{
  GstSSim *ssim;
  GSList *collected;
  GstFlowReturn ret = GST_FLOW_OK;
  GstBuffer *orgbuf = NULL;
  gfloat *orgmu = NULL;
  GstBuffer *outbuf = NULL;
  gpointer outdata = NULL;
  guint outsize = 0;
  gfloat mssim = 0, lowest = 1, highest = -1;
  gboolean empty = TRUE;
  gboolean ready = TRUE;
  gint padnumber = 0;

  ssim = GST_SSIM (user_data);

  if (G_UNLIKELY (ssim->windows == NULL)) {
    GST_DEBUG_OBJECT (ssim, "Regenerating windows");
    gst_ssim_regenerate_windows (ssim);
  }

  switch (ssim->ssimtype) {
    case 0:
      ssim->func = (GstSSimFunction) calcssim_canonical;
      break;
    case 1:
      ssim->func = (GstSSimFunction) calcssim_without_mu;
      break;
    default:
      return GST_FLOW_ERROR;
  }

  for (collected = pads->data; collected; collected = g_slist_next (collected)) {
    GstCollectData *collect_data;
    GstBuffer *inbuf;

    collect_data = (GstCollectData *) collected->data;

    inbuf = gst_collect_pads_peek (pads, collect_data);

    if (inbuf == NULL) {
      GST_LOG_OBJECT (ssim, "channel %p: no bytes available", collect_data);
      ready = FALSE;
    } else
      gst_buffer_unref (inbuf);
  }

  /* if _collected() was called, all pads should have data, but if
   * one of them doesn't, it means that it is EOS and we can't go any further
   *
   * FIXME, shouldn't we do something about pads that DO have data?
   * Flush them or something?
   */
  if (G_UNLIKELY (!ready))
    goto eos;

  /* Mu is just a blur, we can calculate it once */
  if (ssim->ssimtype == 0) {
    orgmu = g_new (gfloat, ssim->width * ssim->height);

    for (collected = pads->data; collected;
        collected = g_slist_next (collected)) {
      GstCollectData *collect_data;

      collect_data = (GstCollectData *) collected->data;

      if (collect_data->pad == ssim->orig) {
        orgbuf = gst_collect_pads_pop (pads, collect_data);;

        GST_DEBUG_OBJECT (ssim, "Original stream - flags(0x%x), timestamp(%"
            GST_TIME_FORMAT "), duration(%" GST_TIME_FORMAT ")",
            GST_BUFFER_FLAGS (orgbuf),
            GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (orgbuf)),
            GST_TIME_ARGS (GST_BUFFER_DURATION (orgbuf)));
        calculate_mu (ssim, orgmu, GST_BUFFER_DATA (orgbuf));

        break;
      }
    }
  }

  GST_LOG_OBJECT (ssim, "starting to cycle through streams");

  for (collected = pads->data; collected; collected = g_slist_next (collected)) {
    GstCollectData *collect_data;
    GstBuffer *inbuf;
    guint8 *indata;
    guint insize;

    collect_data = (GstCollectData *) collected->data;

    if (collect_data->pad != ssim->orig) {
      inbuf = gst_collect_pads_pop (pads, collect_data);

      indata = GST_BUFFER_DATA (inbuf);
      insize = GST_BUFFER_SIZE (inbuf);

      GST_DEBUG_OBJECT (ssim, "Modified stream - flags(0x%x), timestamp(%"
          GST_TIME_FORMAT "), duration(%" GST_TIME_FORMAT ")",
          GST_BUFFER_FLAGS (inbuf),
          GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (inbuf)),
          GST_TIME_ARGS (GST_BUFFER_DURATION (inbuf)));

      if (!GST_BUFFER_FLAG_IS_SET (inbuf, GST_BUFFER_FLAG_GAP)) {
        GstSSimOutputContext *c;
        GstEvent *measured;
        guint64 offset;
        GValue vmean = { 0 }
        , vlowest = {
        0}
        , vhighest = {
        0};

        c = (GstSSimOutputContext *)
            g_object_get_data (G_OBJECT (collect_data->pad),
            "ssim-match-output-context");

        GST_DEBUG_OBJECT (ssim, "Output context is %" GST_PTR_FORMAT
            ", pad will be %" GST_PTR_FORMAT, c, c->pad);

        outsize = GST_ROUND_UP_4 (ssim->width) * ssim->height;
        GST_LOG_OBJECT (ssim, "channel %p: making output buffer of %d bytes",
            collect_data, outsize);

        /* first buffer, alloc outsize.
         * FIXME: we can easily subbuffer and _make_writable.
         * FIXME: only create empty buffer for first non-gap buffer, so that we
         * only use ssim function when really calculating
         */
        outbuf = gst_buffer_new_and_alloc (GST_ROUND_UP_4 (ssim->width) *
            ssim->height);
        outdata = GST_BUFFER_DATA (outbuf);
        gst_buffer_set_caps (outbuf, gst_pad_get_fixed_caps_func (c->pad));

        /* Videos should match, so the output video has the same characteristics
         * as the input video
         */
        /* set timestamps on the output buffer */
        gst_buffer_copy_metadata (outbuf, inbuf, (GstBufferCopyFlags)
            GST_BUFFER_COPY_FLAGS | GST_BUFFER_COPY_TIMESTAMPS);

        g_value_init (&vmean, G_TYPE_FLOAT);
        g_value_init (&vlowest, G_TYPE_FLOAT);
        g_value_init (&vhighest, G_TYPE_FLOAT);

        GST_LOG_OBJECT (ssim, "channel %p: calculating SSIM", collect_data);

        ssim->func (ssim, GST_BUFFER_DATA (orgbuf), orgmu, indata, outdata,
            &mssim, &lowest, &highest);

        GST_DEBUG_OBJECT (GST_OBJECT (ssim), "MSSIM is %f, l-h is %f - %f",
            mssim, lowest, highest);

        gst_ssim_post_message (ssim, outbuf, mssim, lowest, highest);

        g_value_set_float (&vmean, mssim);
        g_value_set_float (&vlowest, lowest);
        g_value_set_float (&vhighest, highest);
        offset = GST_BUFFER_OFFSET (inbuf);

        /* our timestamping is very simple, just an ever incrementing
         * counter, the new segment time will take care of their respective
         * stream time.
         */
        if (c->segment_pending) {
          GstEvent *event;

          /* FIXME, use rate/applied_rate as set on all sinkpads.
           * - currently we just set rate as received from last seek-event
           * We could potentially figure out the duration as well using
           * the current segment positions and the stated stop positions.
           * Also we just start from stream time 0 which is rather
           * weird. For non-synchronized mixing, the time should be
           * the min of the stream times of all received segments,
           * rationale being that the duration is at least going to
           * be as long as the earliest stream we start mixing. This
           * would also be correct for synchronized mixing but then
           * the later streams would be delayed until the stream times`
           * match.
           */
          event = gst_event_new_new_segment_full (FALSE, ssim->segment_rate,
              1.0, GST_FORMAT_TIME, ssim->timestamp, -1,
              ssim->segment_position);

          gst_pad_push_event (c->pad, event);
          c->segment_pending = FALSE;
        }

        measured = gst_event_new_measured (offset,
            GST_BUFFER_TIMESTAMP (inbuf), "SSIM", &vmean, &vlowest, &vhighest);
        gst_pad_push_event (c->pad, measured);

        empty = FALSE;

        /* send it out */
        GST_DEBUG_OBJECT (ssim, "pushing outbuf, timestamp %" GST_TIME_FORMAT
            ", size %d", GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (outbuf)),
            GST_BUFFER_SIZE (outbuf));
        ret &= gst_pad_push (c->pad, outbuf);

      } else {
        GST_LOG_OBJECT (ssim, "channel %p: skipping", collect_data);
      }
      gst_buffer_unref (inbuf);
      padnumber++;
    }
  }
  gst_buffer_unref (orgbuf);

  if (ssim->ssimtype == 0)
    g_free (orgmu);

  ssim->segment_position = 0;

  return ret;

  /* ERRORS */
eos:
  {
    gint i;
    GST_DEBUG_OBJECT (ssim, "no data available, must be EOS");
    for (i = 0; i < ssim->src->len; i++) {
      GstSSimOutputContext *c =
          (GstSSimOutputContext *) g_ptr_array_index (ssim->src, i);
      gst_pad_push_event (c->pad, gst_event_new_eos ());
    }

    return GST_FLOW_UNEXPECTED;
  }
}

static GstStateChangeReturn
gst_ssim_change_state (GstElement * element, GstStateChange transition)
{
  GstSSim *ssim;
  GstStateChangeReturn ret;

  ssim = GST_SSIM (element);

  switch (transition) {
    case GST_STATE_CHANGE_NULL_TO_READY:
      break;
    case GST_STATE_CHANGE_READY_TO_PAUSED:
      ssim->timestamp = 0;
      ssim->offset = 0;
      {
        GstSSimOutputContext *c;
        gint i = 0;
        for (i = 0; i < ssim->src->len; i++) {
          c = (GstSSimOutputContext *) g_ptr_array_index (ssim->src, i);
          c->segment_pending = TRUE;
        }
      }
      ssim->segment_position = 0;
      ssim->segment_rate = 1.0;
      gst_segment_init (&ssim->segment, GST_FORMAT_UNDEFINED);
      gst_collect_pads_start (ssim->collect);
      break;
    case GST_STATE_CHANGE_PAUSED_TO_PLAYING:
      break;
    case GST_STATE_CHANGE_PAUSED_TO_READY:
      /* need to unblock the collectpads before calling the
       * parent change_state so that streaming can finish
       */
      gst_collect_pads_stop (ssim->collect);
      break;
    default:
      break;
  }

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

  switch (transition) {
    default:
      break;
  }

  return ret;
}