#include "ka_vstream.h"

#include <string.h>
#include "ka_mem.h"
#include "config.h"
#include "ka_error.h"
#include "ka_log.h"

#define KA_VSTREAM_FIFO_NUM 10

struct ka_vstream_s
{
  ka_vcodec_event event;
  ka_vcodec_t* codec;
  ka_error_t* pErrorBlock;
  bool isInputDone;
  bool isCodecDone;

  struct
  {
    ka_vframe_t* fstart;
    ka_vframe_t* fread;
    ka_vframe_t* fwrite;
    ka_vframe_t* fend;
    ka_vframe_t fifo[KA_VSTREAM_FIFO_NUM];
  } stack;

  ka_vinfo_t info;
  ka_vframe_t currentframe; // frame which is worked on by codec
  uint32_t index; // frame index of first frame in stack
  uint32_t lindex; // frame index of last frame in stack
};

/**
 * Creates a new video stream.
 *
 * @param  codec  Video codec to use to decode the stream.
 *
 * @returns Video stream handle.
 */
ka_vstream_t* ka_new_vstream(ka_error_t* pErrorBlock, const kav_vcodec_t* codec, const ka_vparams_t* params, uint32_t hardware)
{
  ka_vstream_t* vs = ka_mem_calloc(sizeof(*vs));
  if (!vs)
  {
    ka_error_fill(pErrorBlock, ka_error_nomem);
    return NULL;
  }

  vs->codec = codec->FNnew(pErrorBlock, params, hardware);
  if (!vs->codec)
  {
    ka_mem_free(vs);
    return NULL;
  }

  vs->pErrorBlock = pErrorBlock;
  vs->stack.fstart = &vs->stack.fifo[0];
  vs->stack.fend = vs->stack.fstart + KA_VSTREAM_FIFO_NUM;
  ka_vstream_reset(vs);

  return vs;
}

/**
 * Deletes a video stream.
 *
 * @param  vs  Video stream handle.
 */
void ka_delete_vstream(ka_vstream_t** pvs)
{
  ka_vstream_t* vs = *pvs;
  *pvs = NULL;

  if (!vs)
    return;

  if (vs->codec) vs->codec->vptr->FNdelete(&vs->codec);
  ka_mem_free(vs);
}

/**
 * Resets the video stream after seeking.
 *
 * @param  vs  Video stream handle.
 */
void ka_vstream_reset(ka_vstream_t* vs)
{
  vs->event = ka_vcodec_event_buffering;
  vs->stack.fread = vs->stack.fwrite = vs->stack.fstart;
  vs->codec->vptr->FNact(vs->codec, ka_vcodec_action_reset);
  vs->index = 0;
  vs->lindex = 0;
  vs->isInputDone = false;
  vs->isCodecDone = false;
}

uint32_t ka_vstream_count(ka_vstream_t* vs)
{
  int32_t count = vs->stack.fwrite - vs->stack.fread;
  if (count < 0) count += KA_VSTREAM_FIFO_NUM;
  return (uint32_t) count;
}

/**
 * Processes a video data chunk.
 *
 * @param  vs     Video stream handle.
 * @param  block  Data block to handle.
 * @param  pevent To update with action to perform.
 *
 * @returns 1 if some work done 0 otherwise.
 */
ka_vcodec_event ka_vstream_decode(ka_vstream_t* vs, ka_block_t* block, uint32_t exit_time)
{
  ka_vframe_t* stack_next = vs->stack.fwrite + 1;
  if (stack_next >= vs->stack.fend)
    stack_next = vs->stack.fstart;

  // if stack is full don't do anything
  if (stack_next == vs->stack.fread)
    return ka_vcodec_event_buffer_full;

  // input done signaled by emptry block ?
  if ((block == NULL) || (block->sod == block->eod))
  {
    vs->isInputDone = true;
  }
  else
  {
    vs->isInputDone = false;
    vs->isCodecDone = false;
  }

  // decode a block
  vs->event = vs->codec->vptr->FNdecode(vs->codec, block, exit_time);

  // perform video special action
  switch (vs->event)
  {
    case ka_vcodec_event_decode_frame:
    {
      vs->codec->vptr->FNgetDecodeFrame(vs->codec, &vs->currentframe);
    }
    break;
    case ka_vcodec_event_end_frame:
    {
      // new frame is completely decoded, doesn't mean we can already display one
      if (vs->codec->vptr->FNgetDisplayFrame(vs->codec, vs->stack.fwrite))
      {
        vs->stack.fwrite->index = vs->lindex;
        vs->stack.fwrite = stack_next;
        vs->lindex++;
        // global info is available
        vs->codec->vptr->FNgetInfo(vs->codec, &vs->info);
      }
    }
    break;
    default:
    {
      if (vs->isInputDone)
      {
        vs->isCodecDone = true;
      }
    }
  }

  return vs->event;
}

/**
 * Checks everything is decoded.
 *
 * @param  vs     Video stream handle.
 *
 * @returns true if everything is decoded.
 */
bool ka_vstream_isCodecDone(ka_vstream_t* vs, bool isInputDone)
{
  if (isInputDone)
    vs->isInputDone = true;

  return vs->isCodecDone;
}

/**
 * Returns global video information.
 *
 * @param  vs     Video stream handle.
 *
 * @returns Video information structure.
 */
const ka_vinfo_t* ka_vstream_getInfo(ka_vstream_t* vs)
{
  return &vs->info;
}

/**
 * Returns information on frame being currently decoded.
 * Note: Used to determine if frame will be decoded or skipped.
 *
 * @param  vs     Video stream handle.
 *
 * @returns Frame information structure.
 */
ka_vframe_t* ka_vstream_getDecodeFrame(ka_vstream_t* vs)
{
  return &vs->currentframe;
}

/**
 * Forces codec to skip the decoding of a frame.
 *
 * @param  vs     Video stream handle.
 */
void ka_vstream_skipDecodeFrame(ka_vstream_t* vs)
{
  vs->codec->vptr->FNact(vs->codec, ka_vcodec_action_skipframe);
}

/**
 * Forces codec to decode in monochrome/colours.
 *
 * @param  vs     Video stream handle.
 * @param  mono   1 for mono, 0 for colours.
 */
void ka_vstream_setMonochromeDecode(ka_vstream_t* vs, int32_t mono)
{
  if (mono)
    vs->codec->vptr->FNact(vs->codec, ka_vcodec_action_monochrome);
  else
    vs->codec->vptr->FNact(vs->codec, ka_vcodec_action_colours);
}

/**
 * Returns information on frame to display.
 *
 * @param  vs     Video stream handle.
 *
 * @returns Frame information structure.
 */
ka_vframe_t* ka_vstream_getFrame(const ka_vstream_t* vs, uint32_t index)
{
  ka_vframe_t* frame = vs->stack.fread;

  if (vs->index > index)
  {
    ka_log(ka_log_error, "frame %d not on stack", index);
    return NULL;
  }

  while((frame != vs->stack.fwrite)
        && (index > vs->index))
  {
    frame++;
    index--;
    if (frame >= vs->stack.fend)
      frame = vs->stack.fstart;
  }

  if (frame == vs->stack.fwrite)
    return NULL;

  return frame;
}

/**
 * Discards current display frame so that its memory can be reused.
 *
 * @param  vs     Video stream handle.
 */
void ka_vstream_releaseFrame(ka_vstream_t* vs, uint32_t index)
{
  if (vs->index > index)
  {
    ka_log(ka_log_error, "released frame %d not on stack", index);
    return;
  }
  if (vs->index < index)
  {
    ka_log(ka_log_error, "releasing all frames <= %d of [%d, %d]", index, vs->index, vs->lindex);
  }

  while((vs->stack.fread != vs->stack.fwrite)
        && (index >= vs->index))
  {
    vs->index++;
    vs->codec->vptr->FNreleaseFrame(vs->codec, vs->stack.fread);

    vs->stack.fread++;
    if (vs->stack.fread >= vs->stack.fend)
      vs->stack.fread = vs->stack.fstart;
  }
}
