#ifdef __cplusplus
extern "C" {
#endif
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#ifdef __cplusplus
}
#endif

#include <rthreads/rthreads.h>

#include "video_buffer.h"

enum kbStatus
{
  KB_OPEN = 0,
  KB_IN_PROGRESS,
  KB_FINISHED
};

struct video_buffer
{
   int64_t head;
   int64_t tail;
   video_decoder_context_t *buffer;
   enum kbStatus *status;
   slock_t *lock;
   scond_t *open_cond;
   scond_t *finished_cond;
   size_t capacity;
};

video_buffer_t *video_buffer_create(
      size_t capacity, int frame_size, int width, int height)
{
   unsigned i;
   video_buffer_t *b = (video_buffer_t*)malloc(sizeof(video_buffer_t));
   if (!b)
      return NULL;

   b->buffer        = NULL;
   b->capacity      = capacity;
   b->lock          = NULL;
   b->open_cond     = NULL;
   b->finished_cond = NULL;
   b->head          = 0;
   b->tail          = 0;
   b->status        = (enum kbStatus*)malloc(sizeof(enum kbStatus) * capacity);

   if (!b->status)
      goto fail;

   for (i = 0; i < capacity; i++)
      b->status[i]  = KB_OPEN;

   b->lock          = slock_new();
   b->open_cond     = scond_new();
   b->finished_cond = scond_new();
   if (!b->lock || !b->open_cond || !b->finished_cond)
      goto fail;

   b->buffer        = (video_decoder_context_t*)
      malloc(sizeof(video_decoder_context_t) * capacity);
   if (!b->buffer)
      goto fail;

   for (i = 0; i < (unsigned)capacity; i++)
   {
      b->buffer[i].index     = i;
      b->buffer[i].pts       = 0;
      b->buffer[i].sws       = sws_alloc_context();
      b->buffer[i].source    = av_frame_alloc();
#if ENABLE_HW_ACCEL
      b->buffer[i].hw_source = av_frame_alloc();
#endif
      b->buffer[i].target    = av_frame_alloc();

      AVFrame* frame = b->buffer[i].target;
      av_image_alloc(frame->data, frame->linesize,
            width, height, AV_PIX_FMT_RGB32, 1);

      if (!b->buffer[i].sws       ||
          !b->buffer[i].source    ||
#if ENABLE_HW_ACCEL
          !b->buffer[i].hw_source ||
#endif
          !b->buffer[i].target)
         goto fail;
   }
   return b;

fail:
   video_buffer_destroy(b);
   return NULL;
}

void video_buffer_destroy(video_buffer_t *video_buffer)
{
   unsigned i;
   if (!video_buffer)
      return;

   slock_free(video_buffer->lock);
   scond_free(video_buffer->open_cond);
   scond_free(video_buffer->finished_cond);
   free(video_buffer->status);
   if (video_buffer->buffer)
   {
      for (i = 0; i < video_buffer->capacity; i++)
      {
#if ENABLE_HW_ACCEL
         av_frame_free(&video_buffer->buffer[i].hw_source);
#endif
         av_frame_free(&video_buffer->buffer[i].source);
         av_freep((AVFrame*)video_buffer->buffer[i].target);
         av_frame_free(&video_buffer->buffer[i].target);
         sws_freeContext(video_buffer->buffer[i].sws);
      }
   }
   free(video_buffer->buffer);
   free(video_buffer);
}

void video_buffer_clear(video_buffer_t *video_buffer)
{
   unsigned i;
   if (!video_buffer)
      return;

   slock_lock(video_buffer->lock);

   scond_signal(video_buffer->open_cond);
   scond_signal(video_buffer->finished_cond);

   video_buffer->head = 0;
   video_buffer->tail = 0;
   for (i = 0; i < video_buffer->capacity; i++)
      video_buffer->status[i] = KB_OPEN;

   slock_unlock(video_buffer->lock);
}

void video_buffer_get_open_slot(
      video_buffer_t *video_buffer, video_decoder_context_t **context)
{
   slock_lock(video_buffer->lock);

   if (video_buffer->status[video_buffer->head] == KB_OPEN)
   {
      *context = &video_buffer->buffer[video_buffer->head];
      video_buffer->status[video_buffer->head] = KB_IN_PROGRESS;
      video_buffer->head++;
      video_buffer->head %= video_buffer->capacity;
   }

   slock_unlock(video_buffer->lock);
}

void video_buffer_return_open_slot(
      video_buffer_t *video_buffer, video_decoder_context_t *context)
{
   slock_lock(video_buffer->lock);

   if (video_buffer->status[context->index] == KB_IN_PROGRESS)
   {
      video_buffer->status[context->index] = KB_OPEN;
      video_buffer->head--;
      video_buffer->head %= video_buffer->capacity;
   }

   slock_unlock(video_buffer->lock);
}

void video_buffer_open_slot(
      video_buffer_t *video_buffer,
      video_decoder_context_t *context)
{
   slock_lock(video_buffer->lock);

   if (video_buffer->status[context->index] == KB_FINISHED)
   {
      video_buffer->status[context->index] = KB_OPEN;
      video_buffer->tail++;
      video_buffer->tail %= (video_buffer->capacity);
      scond_signal(video_buffer->open_cond);
   }

   slock_unlock(video_buffer->lock);
}

void video_buffer_get_finished_slot(
      video_buffer_t *video_buffer,
      video_decoder_context_t **context)
{
   slock_lock(video_buffer->lock);

   if (video_buffer->status[video_buffer->tail] == KB_FINISHED)
      *context = &video_buffer->buffer[video_buffer->tail];

   slock_unlock(video_buffer->lock);
}

void video_buffer_finish_slot(
      video_buffer_t *video_buffer,
      video_decoder_context_t *context)
{
   slock_lock(video_buffer->lock);

   if (video_buffer->status[context->index] == KB_IN_PROGRESS)
   {
      video_buffer->status[context->index] = KB_FINISHED;
      scond_signal(video_buffer->finished_cond);
   }

   slock_unlock(video_buffer->lock);
}

bool video_buffer_wait_for_open_slot(video_buffer_t *video_buffer)
{
   slock_lock(video_buffer->lock);

   while (video_buffer->status[video_buffer->head] != KB_OPEN)
      scond_wait(video_buffer->open_cond, video_buffer->lock);

   slock_unlock(video_buffer->lock);

   return true;
}

bool video_buffer_wait_for_finished_slot(video_buffer_t *video_buffer)
{
   slock_lock(video_buffer->lock);

   while (video_buffer->status[video_buffer->tail] != KB_FINISHED)
      scond_wait(video_buffer->finished_cond, video_buffer->lock);

   slock_unlock(video_buffer->lock);

   return true;
}

bool video_buffer_has_open_slot(video_buffer_t *video_buffer)
{
   bool ret = false;

   slock_lock(video_buffer->lock);

   if (video_buffer->status[video_buffer->head] == KB_OPEN)
      ret = true;

   slock_unlock(video_buffer->lock);

   return ret;
}

bool video_buffer_has_finished_slot(video_buffer_t *video_buffer)
{
   bool ret = false;

   slock_lock(video_buffer->lock);

   if (video_buffer->status[video_buffer->tail] == KB_FINISHED)
      ret = true;

   slock_unlock(video_buffer->lock);

   return ret;
}