/* GStreamer
 *
 * Copyright (C) 2009 Carl-Anton Ingmarsson <ca.ingmarsson@gmail.com>.
   *
 * 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.
 */

#include "gsth264dpb.h"

/* Properties */
enum
{
  PROP_0,
  PROP_NUM_REF_FRAMES,
  PROP_MAX_LONGTERM_IDX
};

GST_DEBUG_CATEGORY_STATIC (h264dpb_debug);
#define GST_CAT_DEFAULT h264dpb_debug

#define DEBUG_INIT \
  GST_DEBUG_CATEGORY_INIT (h264dpb_debug, "h264dpb", 0, \
  "H264 DPB");

G_DEFINE_TYPE_WITH_CODE (GstH264DPB, gst_h264_dpb, G_TYPE_OBJECT, DEBUG_INIT);

void
gst_h264_dpb_fill_reference_frames (GstH264DPB * dpb,
    VdpReferenceFrameH264 reference_frames[16])
{
  GstH264Frame **frames;
  guint i;

  frames = dpb->frames;
  for (i = 0; i < dpb->n_frames; i++) {
    GstH264Frame *frame = frames[i];

    reference_frames[i].surface =
        GST_VDP_VIDEO_BUFFER (GST_VIDEO_FRAME_CAST (frame)->src_buffer)->
        surface;

    reference_frames[i].is_long_term = frame->is_long_term;
    reference_frames[i].top_is_reference = frame->is_reference;
    reference_frames[i].bottom_is_reference = frame->is_reference;
    reference_frames[i].field_order_cnt[0] = frame->poc;
    reference_frames[i].field_order_cnt[1] = frame->poc;
    reference_frames[i].frame_idx = frame->frame_idx;
  }

  for (i = dpb->n_frames; i < 16; i++) {
    reference_frames[i].surface = VDP_INVALID_HANDLE;
    reference_frames[i].top_is_reference = VDP_FALSE;
    reference_frames[i].bottom_is_reference = VDP_FALSE;
  }
}

static void
gst_h264_dpb_remove (GstH264DPB * dpb, guint idx)
{
  GstH264Frame **frames;
  guint i;

  frames = dpb->frames;
  gst_video_frame_unref (GST_VIDEO_FRAME_CAST (frames[idx]));
  dpb->n_frames--;

  for (i = idx; i < dpb->n_frames; i++)
    frames[i] = frames[i + 1];
}

static GstFlowReturn
gst_h264_dpb_output (GstH264DPB * dpb, guint idx)
{
  GstFlowReturn ret;
  GstH264Frame *frame = dpb->frames[idx];

  gst_video_frame_ref (GST_VIDEO_FRAME_CAST (frame));
  ret = dpb->output (dpb, frame, dpb->user_data);
  frame->output_needed = FALSE;

  if (!frame->is_reference)
    gst_h264_dpb_remove (dpb, idx);

  return ret;
}

static gboolean
gst_h264_dpb_bump (GstH264DPB * dpb, guint poc, GstFlowReturn * ret)
{
  GstH264Frame **frames;
  guint i;
  gint bump_idx;

  frames = dpb->frames;
  bump_idx = -1;
  for (i = 0; i < dpb->n_frames; i++) {
    if (frames[i]->output_needed) {
      bump_idx = i;
      break;
    }
  }

  if (bump_idx != -1) {
    for (i = bump_idx + 1; i < dpb->n_frames; i++) {
      if (frames[i]->output_needed && (frames[i]->poc < frames[bump_idx]->poc)) {
        bump_idx = i;
      }
    }

    if (frames[bump_idx]->poc < poc) {
      *ret = gst_h264_dpb_output (dpb, bump_idx);
      return TRUE;
    }
  }

  return FALSE;
}

GstFlowReturn
gst_h264_dpb_add (GstH264DPB * dpb, GstH264Frame * h264_frame)
{
  GstFlowReturn ret;

  GST_DEBUG ("add frame with poc: %d", h264_frame->poc);

  if (h264_frame->is_reference && h264_frame->is_long_term &&
      (h264_frame->frame_idx > dpb->max_longterm_frame_idx))
    h264_frame->is_reference = FALSE;

  if (h264_frame->is_reference) {

    ret = GST_FLOW_OK;
    while (dpb->n_frames == dpb->max_frames) {
      if (!gst_h264_dpb_bump (dpb, G_MAXUINT, &ret)) {
        GST_ERROR_OBJECT (dpb, "Couldn't make room in DPB");
        return GST_FLOW_OK;
      }
    }
    dpb->frames[dpb->n_frames++] = h264_frame;
  }

  else {
    while (gst_h264_dpb_bump (dpb, h264_frame->poc, &ret)) {
      if (ret != GST_FLOW_OK)
        return ret;
    }

    ret = dpb->output (dpb, h264_frame, dpb->user_data);
  }

  return ret;
}

void
gst_h264_dpb_flush (GstH264DPB * dpb, gboolean output)
{
  GstFlowReturn ret;
  GstVideoFrame **frames;
  guint i;

  GST_DEBUG ("flush");

  if (output)
    while (gst_h264_dpb_bump (dpb, G_MAXUINT, &ret));

  frames = (GstVideoFrame **) dpb->frames;
  for (i = 0; i < dpb->n_frames; i++)
    gst_video_frame_unref (frames[i]);

  dpb->n_frames = 0;

}

void
gst_h264_dpb_mark_sliding (GstH264DPB * dpb)
{
  GstH264Frame **frames;
  guint i;
  gint mark_idx = -1;

  if (dpb->n_frames != dpb->max_frames)
    return;

  frames = dpb->frames;
  for (i = 0; i < dpb->n_frames; i++) {
    if (frames[i]->is_reference && !frames[i]->is_long_term) {
      mark_idx = i;
      break;
    }
  }

  if (mark_idx != -1) {
    for (i = mark_idx; i < dpb->n_frames; i++) {
      if (frames[i]->is_reference && !frames[i]->is_long_term &&
          frames[i]->frame_idx < frames[mark_idx]->frame_idx)
        mark_idx = i;
    }

    frames[mark_idx]->is_reference = FALSE;
    if (!frames[mark_idx]->output_needed)
      gst_h264_dpb_remove (dpb, mark_idx);
  }
}

void
gst_h264_dpb_mark_long_term (GstH264DPB * dpb, guint16 pic_num,
    guint16 long_term_frame_idx)
{
  GstH264Frame **frames;
  guint i;
  gint mark_idx = -1;

  frames = dpb->frames;
  for (i = 0; i < dpb->n_frames; i++) {
    if (frames[i]->is_reference && !frames[i]->is_long_term &&
        frames[i]->frame_idx == pic_num) {
      mark_idx = i;
      break;
    }
  }

  if (mark_idx != -1) {
    frames[mark_idx]->is_long_term = TRUE;
    frames[mark_idx]->frame_idx = long_term_frame_idx;
  }
}

void
gst_h264_dpb_mark_short_term_unused (GstH264DPB * dpb, guint16 pic_num)
{
  GstH264Frame **frames;
  guint i;
  gint mark_idx = -1;

  frames = dpb->frames;
  for (i = 0; i < dpb->n_frames; i++) {
    if (frames[i]->is_reference && !frames[i]->is_long_term &&
        frames[i]->frame_idx == pic_num) {
      mark_idx = i;
      break;
    }
  }

  if (mark_idx != -1) {

    frames[mark_idx]->is_reference = FALSE;
    if (!frames[mark_idx]->output_needed)
      gst_h264_dpb_remove (dpb, mark_idx);
  }
}

void
gst_h264_dpb_mark_long_term_unused (GstH264DPB * dpb, guint16 long_term_pic_num)
{
  GstH264Frame **frames;
  guint i;
  gint mark_idx = -1;

  frames = dpb->frames;
  for (i = 0; i < dpb->n_frames; i++) {
    if (frames[i]->is_reference && frames[i]->is_long_term &&
        frames[i]->frame_idx == long_term_pic_num) {
      mark_idx = i;
      break;
    }
  }

  if (mark_idx != -1) {

    frames[mark_idx]->is_reference = FALSE;
    if (!frames[mark_idx]->output_needed)
      gst_h264_dpb_remove (dpb, mark_idx);
  }
}

void
gst_h264_dpb_mark_all_unused (GstH264DPB * dpb)
{
  GstH264Frame **frames;
  guint i;

  frames = dpb->frames;
  for (i = 0; i < dpb->n_frames; i++) {
    frames[i]->is_reference = FALSE;
    if (!frames[i]->output_needed) {
      gst_h264_dpb_remove (dpb, i);
      i--;
    }
  }

}

void
gst_h264_dpb_set_output_func (GstH264DPB * dpb, GstH264DPBOutputFunc func,
    gpointer user_data)
{
  g_return_if_fail (GST_IS_H264_DPB (dpb));

  dpb->output = func;
  dpb->user_data = user_data;
}

/* GObject vmethod implementations */
static void
gst_h264_dpb_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  GstH264DPB *dpb = GST_H264_DPB (object);

  switch (property_id) {
    case PROP_NUM_REF_FRAMES:
      g_value_set_uint (value, dpb->max_frames);
      break;
    case PROP_MAX_LONGTERM_IDX:
      g_value_set_int (value, dpb->max_longterm_frame_idx);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
  }
}

static void
gst_h264_dpb_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  GstH264DPB *dpb = GST_H264_DPB (object);

  switch (property_id) {
    case PROP_NUM_REF_FRAMES:
    {
      GstFlowReturn ret;
      guint i;

      dpb->max_frames = g_value_get_uint (value);
      for (i = dpb->n_frames; i > dpb->max_frames; i--)
        gst_h264_dpb_bump (dpb, G_MAXUINT, &ret);

      break;
    }

    case PROP_MAX_LONGTERM_IDX:
    {
      GstH264Frame **frames;
      guint i;

      dpb->max_longterm_frame_idx = g_value_get_int (value);

      frames = dpb->frames;
      for (i = dpb->n_frames; i < dpb->n_frames; i++) {
        if (frames[i]->is_reference && frames[i]->is_long_term &&
            frames[i]->frame_idx > dpb->max_longterm_frame_idx) {
          frames[i]->is_reference = FALSE;
          if (!frames[i]->output_needed) {
            gst_h264_dpb_remove (dpb, i);
            i--;
          }
        }
      }
      break;
    }

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
  }
}

static void
gst_h264_dpb_finalize (GObject * object)
{
  GstH264DPB *dpb = GST_H264_DPB (object);
  GstVideoFrame **frames;
  guint i;

  frames = (GstVideoFrame **) dpb->frames;
  for (i = 0; i < dpb->n_frames; i++)
    gst_video_frame_unref (frames[i]);

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

static void
gst_h264_dpb_init (GstH264DPB * dpb)
{
  dpb->n_frames = 0;
  dpb->max_longterm_frame_idx = -1;
  dpb->max_frames = MAX_DPB_SIZE;
}

static void
gst_h264_dpb_class_init (GstH264DPBClass * klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = gst_h264_dpb_finalize;
  object_class->set_property = gst_h264_dpb_set_property;
  object_class->get_property = gst_h264_dpb_get_property;

  g_object_class_install_property (object_class, PROP_NUM_REF_FRAMES,
      g_param_spec_uint ("num-ref-frames", "Num Ref Frames",
          "How many reference frames the DPB should hold ",
          0, 16, 16, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_MAX_LONGTERM_IDX,
      g_param_spec_int ("max-longterm-frame-idx", "MaxLongTermFrameIDX",
          "Maximum long-term frame index",
          -1, G_MAXINT, -1, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

}
