/* GStreamer
 * Copyright (C) <1999> Erik Walthinsen <omega@cse.ogi.edu>
 * Copyright (C) <2003> David Schleef <ds@schleef.org>
 * Copyright (C) <2009> Sebastian Dröge <sebastian.droege@collabora.co.uk>
 *
 * EffecTV - Realtime Digital Video Effector
 * Copyright (C) 2001-2002 FUKUCHI Kentarou
 *
 * AgingTV - film-aging effect.
 *
 * 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., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

/**
 * SECTION:element-agingtv
 *
 * AgingTV ages a video stream in realtime, changes the colors and adds
 * scratches and dust.
 *
 * <refsect2>
 * <title>Example launch line</title>
 * |[
 * gst-launch-1.0 -v videotestsrc ! agingtv ! videoconvert ! autovideosink
 * ]| This pipeline shows the effect of agingtv on a test stream.
 * </refsect2>
 */

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

#include <string.h>
#include <math.h>

#include "gstaging.h"
#include "gsteffectv.h"

static const gint dx[8] = { 1, 1, 0, -1, -1, -1, 0, 1 };
static const gint dy[8] = { 0, -1, -1, -1, 0, 1, 1, 1 };

enum
{
  PROP_0 = 0,
  PROP_SCRATCH_LINES,
  PROP_COLOR_AGING,
  PROP_PITS,
  PROP_DUSTS
};

#define DEFAULT_SCRATCH_LINES 7
#define DEFAULT_COLOR_AGING TRUE
#define DEFAULT_PITS TRUE
#define DEFAULT_DUSTS TRUE

#if G_BYTE_ORDER == G_LITTLE_ENDIAN
#define CAPS_STR GST_VIDEO_CAPS_MAKE ("{ BGRx, RGBx }")
#else
#define CAPS_STR GST_VIDEO_CAPS_MAKE ("{ xRGB, xBGR }")
#endif

static GstStaticPadTemplate gst_agingtv_src_template =
GST_STATIC_PAD_TEMPLATE ("src",
    GST_PAD_SRC,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS (CAPS_STR)
    );

static GstStaticPadTemplate gst_agingtv_sink_template =
GST_STATIC_PAD_TEMPLATE ("sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS (CAPS_STR)
    );

G_DEFINE_TYPE (GstAgingTV, gst_agingtv, GST_TYPE_VIDEO_FILTER);

static void
coloraging (guint32 * src, guint32 * dest, gint video_area, gint * c)
{
  guint32 a, b;
  gint i;
  gint c_tmp = *c;

  c_tmp -= (gint) (fastrand ()) >> 28;
  if (c_tmp < 0)
    c_tmp = 0;
  if (c_tmp > 0x18)
    c_tmp = 0x18;

  for (i = 0; i < video_area; i++) {
    a = *src++;
    b = (a & 0xfcfcfc) >> 2;
    *dest++ =
        a - b + (c_tmp | (c_tmp << 8) | (c_tmp << 16)) +
        ((fastrand () >> 8) & 0x101010);
  }
  *c = c_tmp;
}


static void
scratching (scratch * scratches, gint scratch_lines, guint32 * dest, gint width,
    gint height)
{
  gint i, y, y1, y2;
  guint32 *p, a, b;
  scratch *scratch;

  for (i = 0; i < scratch_lines; i++) {
    scratch = &scratches[i];

    if (scratch->life) {
      scratch->x = scratch->x + scratch->dx;

      if (scratch->x < 0 || scratch->x > width * 256) {
        scratch->life = 0;
        break;
      }
      p = dest + (scratch->x >> 8);
      if (scratch->init) {
        y1 = scratch->init;
        scratch->init = 0;
      } else {
        y1 = 0;
      }
      scratch->life--;
      if (scratch->life) {
        y2 = height;
      } else {
        y2 = fastrand () % height;
      }
      for (y = y1; y < y2; y++) {
        a = *p & 0xfefeff;
        a += 0x202020;
        b = a & 0x1010100;
        *p = a | (b - (b >> 8));
        p += width;
      }
    } else {
      if ((fastrand () & 0xf0000000) == 0) {
        scratch->life = 2 + (fastrand () >> 27);
        scratch->x = fastrand () % (width * 256);
        scratch->dx = ((int) fastrand ()) >> 23;
        scratch->init = (fastrand () % (height - 1)) + 1;
      }
    }
  }
}

static void
dusts (guint32 * dest, gint width, gint height, gint * dust_interval,
    gint area_scale)
{
  gint i, j;
  gint dnum;
  gint d, len;
  guint x, y;

  if (*dust_interval == 0) {
    if ((fastrand () & 0xf0000000) == 0) {
      *dust_interval = fastrand () >> 29;
    }
    return;
  }
  dnum = area_scale * 4 + (fastrand () >> 27);

  for (i = 0; i < dnum; i++) {
    x = fastrand () % width;
    y = fastrand () % height;
    d = fastrand () >> 29;
    len = fastrand () % area_scale + 5;
    for (j = 0; j < len; j++) {
      dest[y * width + x] = 0x101010;
      y += dy[d];
      x += dx[d];

      if (y >= height || x >= width)
        break;

      d = (d + fastrand () % 3 - 1) & 7;
    }
  }
  *dust_interval = *dust_interval - 1;
}

static void
pits (guint32 * dest, gint width, gint height, gint area_scale,
    gint * pits_interval)
{
  gint i, j;
  gint pnum, size, pnumscale;
  guint x, y;

  pnumscale = area_scale * 2;
  if (*pits_interval) {
    pnum = pnumscale + (fastrand () % pnumscale);

    *pits_interval = *pits_interval - 1;
  } else {
    pnum = fastrand () % pnumscale;

    if ((fastrand () & 0xf8000000) == 0) {
      *pits_interval = (fastrand () >> 28) + 20;
    }
  }
  for (i = 0; i < pnum; i++) {
    x = fastrand () % (width - 1);
    y = fastrand () % (height - 1);

    size = fastrand () >> 28;

    for (j = 0; j < size; j++) {
      x = x + fastrand () % 3 - 1;
      y = y + fastrand () % 3 - 1;

      if (y >= height || x >= width)
        break;

      dest[y * width + x] = 0xc0c0c0;
    }
  }
}

static void
gst_agingtv_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  GstAgingTV *agingtv = GST_AGINGTV (object);

  GST_OBJECT_LOCK (agingtv);
  switch (prop_id) {
    case PROP_SCRATCH_LINES:
      g_value_set_uint (value, agingtv->scratch_lines);
      break;
    case PROP_COLOR_AGING:
      g_value_set_boolean (value, agingtv->color_aging);
      break;
    case PROP_PITS:
      g_value_set_boolean (value, agingtv->pits);
      break;
    case PROP_DUSTS:
      g_value_set_boolean (value, agingtv->dusts);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
  }
  GST_OBJECT_UNLOCK (agingtv);
}

static void
gst_agingtv_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstAgingTV *agingtv = GST_AGINGTV (object);

  switch (prop_id) {
    case PROP_SCRATCH_LINES:
      agingtv->scratch_lines = g_value_get_uint (value);
      break;
    case PROP_COLOR_AGING:
      agingtv->color_aging = g_value_get_boolean (value);
      break;
    case PROP_PITS:
      agingtv->pits = g_value_get_boolean (value);
      break;
    case PROP_DUSTS:
      agingtv->dusts = g_value_get_boolean (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
  }
}

static gboolean
gst_agingtv_start (GstBaseTransform * trans)
{
  GstAgingTV *agingtv = GST_AGINGTV (trans);

  agingtv->coloraging_state = 0x18;
  agingtv->dust_interval = 0;
  agingtv->pits_interval = 0;

  memset (agingtv->scratches, 0, sizeof (agingtv->scratches));

  return TRUE;
}

static GstFlowReturn
gst_agingtv_transform_frame (GstVideoFilter * filter, GstVideoFrame * in_frame,
    GstVideoFrame * out_frame)
{
  GstAgingTV *agingtv = GST_AGINGTV (filter);
  gint area_scale;
  GstClockTime timestamp, stream_time;
  gint width, height, stride, video_size;
  guint32 *src, *dest;

  timestamp = GST_BUFFER_TIMESTAMP (in_frame->buffer);
  stream_time =
      gst_segment_to_stream_time (&GST_BASE_TRANSFORM (filter)->segment,
      GST_FORMAT_TIME, timestamp);

  GST_DEBUG_OBJECT (agingtv, "sync to %" GST_TIME_FORMAT,
      GST_TIME_ARGS (timestamp));

  if (GST_CLOCK_TIME_IS_VALID (stream_time))
    gst_object_sync_values (GST_OBJECT (agingtv), stream_time);

  width = GST_VIDEO_FRAME_WIDTH (in_frame);
  height = GST_VIDEO_FRAME_HEIGHT (in_frame);
  stride = GST_VIDEO_FRAME_PLANE_STRIDE (in_frame, 0);
  video_size = stride * height / 4;

  src = GST_VIDEO_FRAME_PLANE_DATA (in_frame, 0);
  dest = GST_VIDEO_FRAME_PLANE_DATA (out_frame, 0);

  area_scale = width * height / 64 / 480;
  if (area_scale <= 0)
    area_scale = 1;

  if (agingtv->color_aging)
    coloraging (src, dest, video_size, &agingtv->coloraging_state);
  else
    memcpy (dest, src, stride * height);

  scratching (agingtv->scratches, agingtv->scratch_lines, dest, width, height);
  if (agingtv->pits)
    pits (dest, width, height, area_scale, &agingtv->pits_interval);
  if (area_scale > 1 && agingtv->dusts)
    dusts (dest, width, height, &agingtv->dust_interval, area_scale);

  return GST_FLOW_OK;
}

static void
gst_agingtv_class_init (GstAgingTVClass * klass)
{
  GObjectClass *gobject_class = (GObjectClass *) klass;
  GstElementClass *gstelement_class = (GstElementClass *) klass;
  GstBaseTransformClass *trans_class = (GstBaseTransformClass *) klass;
  GstVideoFilterClass *vfilter_class = (GstVideoFilterClass *) klass;

  gobject_class->set_property = gst_agingtv_set_property;
  gobject_class->get_property = gst_agingtv_get_property;

  g_object_class_install_property (gobject_class, PROP_SCRATCH_LINES,
      g_param_spec_uint ("scratch-lines", "Scratch Lines",
          "Number of scratch lines", 0, SCRATCH_MAX, DEFAULT_SCRATCH_LINES,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | GST_PARAM_CONTROLLABLE));

  g_object_class_install_property (gobject_class, PROP_COLOR_AGING,
      g_param_spec_boolean ("color-aging", "Color Aging",
          "Color Aging", DEFAULT_COLOR_AGING,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | GST_PARAM_CONTROLLABLE));

  g_object_class_install_property (gobject_class, PROP_PITS,
      g_param_spec_boolean ("pits", "Pits",
          "Pits", DEFAULT_PITS,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | GST_PARAM_CONTROLLABLE));

  g_object_class_install_property (gobject_class, PROP_DUSTS,
      g_param_spec_boolean ("dusts", "Dusts",
          "Dusts", DEFAULT_DUSTS,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | GST_PARAM_CONTROLLABLE));

  gst_element_class_set_static_metadata (gstelement_class, "AgingTV effect",
      "Filter/Effect/Video",
      "AgingTV adds age to video input using scratches and dust",
      "Sam Lantinga <slouken@devolution.com>");

  gst_element_class_add_static_pad_template (gstelement_class,
      &gst_agingtv_sink_template);
  gst_element_class_add_static_pad_template (gstelement_class,
      &gst_agingtv_src_template);

  trans_class->start = GST_DEBUG_FUNCPTR (gst_agingtv_start);

  vfilter_class->transform_frame =
      GST_DEBUG_FUNCPTR (gst_agingtv_transform_frame);
}

static void
gst_agingtv_init (GstAgingTV * agingtv)
{
  agingtv->scratch_lines = DEFAULT_SCRATCH_LINES;
  agingtv->color_aging = DEFAULT_COLOR_AGING;
  agingtv->pits = DEFAULT_PITS;
  agingtv->dusts = DEFAULT_DUSTS;
}
