/*
 *  Copyright 1994-2022 Olivier Girondel
 *
 *  This file is part of lebiniou.
 *
 *  lebiniou is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  lebiniou 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with lebiniou. If not, see <http://www.gnu.org/licenses/>.
 */

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include "context.h"
#include "parameters.h"
#include "shuffler_modes.h"
#include "pthread_utils.h"


uint32_t version = 0;
uint32_t options = BO_GFX | BO_FIRST | BO_NORANDOM | BO_SCHEMES;
char desc[] = "Video player";
char dname[] = "Video";

static AVFormatContext *fmt_ctx = NULL;
static int video_stream_index = -1;
static AVCodecContext *codec_ctx = NULL;
static const AVCodec *codec = NULL;
static AVFrame *frame = NULL;
static AVPacket *packet = NULL;
static int8_t eof = 0;
// swscale
static int8_t need_rescale = -1;
static uint8_t *src_data[4] = { NULL, NULL, NULL, NULL };
static uint8_t *dst_data[4] = { NULL, NULL, NULL, NULL };
static int src_linesize[4], dst_linesize[4];
static const enum AVPixelFormat src_pix_fmt = AV_PIX_FMT_GRAY8, dst_pix_fmt = AV_PIX_FMT_GRAY8;
static struct SwsContext *sws_ctx = NULL;
// threading
static pthread_t video_thread;
static void *thread_loop(void *);
static pthread_mutex_t video_mutex;
static Buffer8_t *video_buffer = NULL;
static uint8_t thread_exit = 0;
static uint8_t thread_running = 0;
static int buffer_size;
// synchronization
static Timer_t *video_timer = NULL;
static Shuffler_t *shuffler = NULL;
static uint32_t frame_ms = 0; // frame time in microseconds

static void open_video(Context_t *, const char *);
static void close_video(void);
static void next_video(Context_t *);

/* parameters */
static json_t *playlist = NULL;
static enum ShufflerMode mode = BS_CYCLE;
#define BPP_FREEZE_AUTO_CHANGES "freeze_auto_changes"
static int freeze_auto = 0;
#define BPP_TRIGGER_AUTO_CHANGE "trigger_auto_change"
static int trigger_auto = 0;

static uint8_t played = 0;


json_t *
get_parameters(const uint8_t fetch_all)
{
  json_t *params = json_object();

  plugin_parameters_add_string_list(params, BPP_MODE, BS_NB, shuffler_modes, mode, BS_NB - 1, "Order in which the videos are played");
  plugin_parameters_add_playlist(params, BPP_PLAYLIST, playlist, "Select one or more videos to play");
  plugin_parameters_add_boolean(params, BPP_FREEZE_AUTO_CHANGES, freeze_auto, "Freeze auto changes until playlist is done");
  plugin_parameters_add_boolean(params, BPP_TRIGGER_AUTO_CHANGE, trigger_auto, "Trigger a change when playlist is done, if auto schemes or sequences was active");

  return params;
}


void
set_parameters(Context_t *ctx, const json_t *in_parameters)
{
  uint8_t changed = plugin_parameter_parse_playlist(in_parameters, BPP_PLAYLIST, &playlist) & PLUGIN_PARAMETER_CHANGED;

  changed |= plugin_parameter_parse_string_list_as_int_range(in_parameters, BPP_MODE, BS_NB, shuffler_modes, (int *)&mode) & PLUGIN_PARAMETER_CHANGED;

  if (changed && json_array_size(playlist)) {
    Shuffler_delete(shuffler);
    shuffler = Shuffler_new(json_array_size(playlist));
    Shuffler_set_mode(shuffler, mode);
    next_video(ctx);
  }

  plugin_parameter_parse_boolean(in_parameters, BPP_FREEZE_AUTO_CHANGES, &freeze_auto);
  ctx->allow_random_changes = !freeze_auto;
  plugin_parameter_parse_boolean(in_parameters, BPP_TRIGGER_AUTO_CHANGE, &trigger_auto);
}


json_t *
parameters(Context_t *ctx, const json_t *in_parameters, const uint8_t fetch_all)
{
  if (NULL != in_parameters) {
    set_parameters(ctx, in_parameters);
  }

  return get_parameters(fetch_all);
}


static void
free_swscale_data(void)
{
  if (NULL != sws_ctx) {
    av_freep(&src_data[0]);
    av_freep(&dst_data[0]);
    sws_freeContext(sws_ctx);
    sws_ctx = NULL;
  }
}


static void
next_video(Context_t *ctx)
{
  uint16_t idx = Shuffler_get(shuffler);
  json_t *file_j = json_array_get(playlist, idx);
  const char *file = NULL;

  if (json_is_string(file_j)) {
    file = json_string_value(file_j);
#ifdef DEBUG_VIDEO
    xdebug("[i] %s: %s: playing %s\n", __FILE__, __func__, file);
#endif
    close_video();
    open_video(ctx, file);
  }
}


static void
open_video(Context_t *ctx, const char *file)
{
  int ret;
  gchar *path = g_strdup_printf("%s/." PACKAGE_NAME "/videos/%s", g_get_home_dir(), file);

  if (NULL == path) {
    xerror("%s: %s g_strdup_printf failed\n", __FILE__, __func__);
  }

  if ((ret = avformat_open_input(&fmt_ctx, path, NULL, NULL) < 0)) {
    fprintf(stderr, "%s: can not open %s\n", __FILE__, path);
    return;
  } else {
#ifdef DEBUG_VIDEO
    xdebug("[i] %s: opened %s for reading\n", __FILE__, path);
#endif
  }

  if ((ret = avformat_find_stream_info(fmt_ctx, NULL) < 0)) {
    xerror("%s: can not get stream info for %s\n", __FILE__, path);
  }

  video_stream_index = -1;
  for (uint8_t i = 0; i < fmt_ctx->nb_streams; i++) {
    if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
      video_stream_index = i;
      frame_ms = (float)fmt_ctx->streams[i]->avg_frame_rate.den / fmt_ctx->streams[i]->avg_frame_rate.num * 1000 * 1000;
      break;
    }
  }
  if (video_stream_index < 0) {
    xerror("%s: %s has no video stream\n", __FILE__, path);
  }
#ifdef DEBUG_VIDEO
  // dump video stream info
  av_dump_format(fmt_ctx, video_stream_index, path, 0);
#endif
  // alloc memory for codec context
  codec_ctx = avcodec_alloc_context3(NULL);
  if (NULL == codec_ctx) {
    xerror("%s: %s avcodec_alloc_context3 failed\n", __FILE__, __func__);
  }
  // retrieve codec params from format context
  if ((ret = avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[video_stream_index]->codecpar) < 0)) {
    xerror("%s: %s can not get codec parameters\n", __FILE__, path);
  }

  // find decoding codec
  codec = avcodec_find_decoder(codec_ctx->codec_id);
  if (NULL == codec) {
    xerror("%s: no decoder found for %s\n", __FILE__, path);
  }

  // try to open codec
  if ((ret = avcodec_open2(codec_ctx, codec, NULL)) < 0) {
    xerror("%s: can not open video decoder for %s\n", __FILE__, path);
  } else {
#ifdef DEBUG_VIDEO
    xdebug("%s: video %s has codec: %s\n", __FILE__, path, codec->name);
#endif
  }

  g_free(path);
  eof = 0;
  need_rescale = -1;
  thread_exit = 0;
  xpthread_create(&video_thread, NULL, thread_loop, (void *)ctx);
  thread_running = 1;
}


static void
free_allocated(void)
{
  if (NULL != codec_ctx) {
    avcodec_free_context(&codec_ctx);
  }
  if (NULL != fmt_ctx) {
    avformat_close_input(&fmt_ctx);
  }
  if (NULL != frame) {
    av_frame_free(&frame);
  }
  if (NULL != packet) {
    av_packet_free(&packet);
  }
  free_swscale_data();
}


static void
close_video(void)
{
  if (thread_running) {
    thread_exit = 1;
    xpthread_join(video_thread, NULL);
    thread_running = 0;
    free_allocated();
  }
}


int8_t
create(Context_t *ctx)
{
  // create packet
  packet = av_packet_alloc();
  if (NULL == packet) {
    xerror("%s: %s av_packet_alloc failed\n", __FILE__, __func__);
  }
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 133, 100)
  av_init_packet(packet);
#endif
  // create frame
  frame = av_frame_alloc();
  if (NULL == frame) {
    xerror("%s: %s av_frame_alloc failed\n", __FILE__, __func__);
  }
  // create playlist
  playlist = json_array();
  if (NULL == playlist) {
    xerror("%s: %s json_array failed\n", __FILE__, __func__);
  }
  init_gray8();
  // thread
  xpthread_mutex_init(&video_mutex, NULL);
  video_buffer = Buffer8_new();
  // synchronization
  video_timer = Timer_new(__FILE__);

  return 1;
}


void
on_switch_on(Context_t *ctx)
{
  if (NULL != shuffler) {
    Shuffler_restart(shuffler);
    next_video(ctx);
  }
  if (freeze_auto) {
    ctx->allow_random_changes = 0;
  }
  played = 0;
}


void
on_switch_off(Context_t *ctx)
{
  close_video();

  if (freeze_auto) {
    // re-enable random changes
    ctx->allow_random_changes = 1;
  }
}


void
destroy(Context_t *ctx)
{
  close_video();
  free_allocated();
  json_decref(playlist);
  Shuffler_delete(shuffler);
  Buffer8_delete(video_buffer);
  Timer_delete(video_timer);
  xpthread_mutex_destroy(&video_mutex);
}


static void
decode(AVCodecContext *dec_ctx, AVFrame *frame, AVPacket *pkt, Pixel_t *dst)
{
  int ret;

  // send packet to decoder
  if ((ret = avcodec_send_packet(dec_ctx, pkt) < 0)) {
    xerror("%s: error sending a packet for decoding\n", __FILE__);
  }
  while (ret >= 0) {
    // receive frame from decoder
    // we may receive multiple frames or we may consume all data from decoder, then return to main loop
    ret = avcodec_receive_frame(dec_ctx, frame);
    if ((ret == AVERROR(EAGAIN)) || (ret == AVERROR_EOF)) {
      return;
    } else if (ret < 0) {
      // something wrong, quit program
      xerror("%s: error during decoding\n", __FILE__);
    }
#if 0 //def DEBUG_VIDEO
    xdebug("format: %s, range: %s, color_primaries: %s, colorspace: %s, transfer: %s\n",
           av_get_pix_fmt_name(frame->format),
           av_color_range_name(frame->color_range),
           av_color_primaries_name(frame->color_primaries),
           av_color_space_name(frame->colorspace),
           av_color_transfer_name(frame->color_trc));
#endif
    // copy frame to destination
    // check if we know whether rescaling is needed or not
    if (need_rescale == -1) {
      need_rescale = (frame->width != WIDTH) || (frame->height != HEIGHT);
#ifdef DEBUG_VIDEO
      xdebug("[i] %s: rescaling needed: %s\n", __FILE__, need_rescale ? "yes" : "no");
#endif
    }
    if (need_rescale) {
      // allocate memory and create context
      if (NULL == sws_ctx) {
        sws_ctx = sws_getContext(frame->width, frame->height, src_pix_fmt,
                                 WIDTH, HEIGHT, dst_pix_fmt,
                                 SWS_BICUBIC
#ifdef DEBUG_VIDEO
                                 |SWS_PRINT_INFO
#endif
                                 , NULL, NULL, NULL);
        if (NULL == sws_ctx) {
          xerror("%s: Impossible to create scale context for the conversion "
                 "fmt:%s s:%dx%d -> fmt:%s s:%dx%d\n", __FILE__,
                 av_get_pix_fmt_name(src_pix_fmt), frame->width, frame->height,
                 av_get_pix_fmt_name(dst_pix_fmt), WIDTH, HEIGHT);
        }
#ifdef DEBUG_VIDEO
        else {
          xdebug("[i] %s: Created scale context for the conversion "
                 "%dx%d (%s) -> %dx%d (%s)\n", __FILE__,
                 frame->width, frame->height, av_get_pix_fmt_name(src_pix_fmt),
                 WIDTH, HEIGHT, av_get_pix_fmt_name(dst_pix_fmt));
        }
#endif
        // allocate source and destination image buffers
        if ((ret = av_image_alloc(src_data, src_linesize,
                                  frame->width, frame->height, src_pix_fmt, 16)) < 0) {
          xerror("%s: Could not allocate source image\n", __FILE__);
        }
        if ((ret = av_image_alloc(dst_data, dst_linesize,
                                  WIDTH, HEIGHT, dst_pix_fmt, 4)) < 0) {
          xerror("%s: Could not allocate destination image\n", __FILE__);
        }
        buffer_size = av_image_get_buffer_size(dst_pix_fmt, WIDTH, HEIGHT, 16);
#ifdef DEBUG_VIDEO
        xdebug("%s: %d %d %d %d %d %ld\n", __FILE__, frame->width, frame->height, WIDTH, HEIGHT, buffer_size, BUFFSIZE);
#endif
        assert(buffer_size == (int)BUFFSIZE);
      }
      uint8_t *p = src_data[0];
      for (int i = 0; i < frame->height; i++) {
        memcpy(p, frame->data[0] + i * frame->linesize[0], frame->width);
        p += src_linesize[0];
      }
      sws_scale(sws_ctx, (const uint8_t * const*)src_data,
                src_linesize, 0, frame->height, dst_data, dst_linesize);
      memcpy(dst, dst_data[0], buffer_size);
    } else {
      for (int i = 0; i < frame->height; i++) {
        memcpy(dst, frame->data[0] + i * frame->linesize[0], frame->width);
        dst += frame->width;
      }
    }
  }
}


static void *
thread_loop(void *arg)
{
  Timer_t *duration = NULL;
  int ret;

  duration = Timer_new("duration");
  Timer_start(duration);

  if (NULL == packet) {
    packet = av_packet_alloc();
    if (NULL == packet) {
      xerror("%s: %s av_packet_alloc failed\n", __FILE__, __func__);
    }
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 133, 100)
    av_init_packet(packet);
#endif
  }

  if (NULL == frame) {
    frame = av_frame_alloc();
    if (NULL == frame) {
      xerror("%s: %s av_frame_alloc failed\n", __FILE__, __func__);
    }
  }

  while (!eof && !thread_exit) {
    // read an encoded packet from file
    if ((ret = av_read_frame(fmt_ctx, packet) < 0)) {
      eof = 1;
    } else if (packet->stream_index == video_stream_index) {
      // if packet data is video data then send it to decoder
      Timer_start(video_timer);
      if (!xpthread_mutex_lock(&video_mutex)) {
        decode(codec_ctx, frame, packet, video_buffer->buffer);
        Buffer8_flip_v(video_buffer);
        Buffer8_YUV_to_full_gray(video_buffer);
        xpthread_mutex_unlock(&video_mutex);
      }
      const uint32_t elapsed_ms = Timer_elapsed(video_timer) * 1000 * 1000;
      const int32_t sleep_ms = frame_ms - elapsed_ms;
#if 0 //def DEBUG_VIDEO
      xdebug("%s: frame: %d ms, elapsed: %d ms, sleep: %d ms\n", __func__,
             frame_ms, elapsed_ms, sleep_ms);
#endif
      if (sleep_ms > 0) {
        ms_sleep(sleep_ms / 1000);
      }
    }
    // release packet buffers to be allocated again
    av_packet_unref(packet);
  }
#ifdef DEBUG_VIDEO
  xdebug("%s: Played video in %f seconds\n", __func__, Timer_elapsed(duration));
#endif
  Timer_delete(duration);

  return NULL;
}


void
run(Context_t *ctx)
{
  if (thread_running) {
    if (!xpthread_mutex_lock(&video_mutex)) {
      Buffer8_copy(video_buffer, passive_buffer(ctx));
      Context_push_video(ctx, video_buffer);
      xpthread_mutex_unlock(&video_mutex);
    }
  }

  if (eof) {
#ifdef DEBUG_VIDEO
    xdebug("%s: end of file reached\n", __FILE__);
#endif
    played++;
#ifdef DEBUG_FREEZE
    xdebug("%s: played= %d\n", __func__, played);
#endif
    if (played == json_array_size(playlist)) {
#ifdef DEBUG_FREEZE
      xdebug("%s: END OF PLAYLIST\n", __func__);
#endif
      // all files played
#ifdef DEBUG_FREEZE
      xdebug("%s:%d freeze_auto= %d, trigger_auto= %d\n", __func__, __LINE__, freeze_auto, trigger_auto);
#endif
      if (freeze_auto) {
        // re-enable auto changes
        ctx->allow_random_changes = 1;
#ifdef DEBUG_FREEZE
        xdebug("%s:%d RESTORED freeze_auto= %d, trigger_auto= %d\n", __func__, __LINE__, freeze_auto, trigger_auto);
#endif
      }
      if (trigger_auto) {
        // force a random change
#ifdef DEBUG_FREEZE
        xdebug("%s: TRIGGER CHANGE\n", __func__);
#endif
        Alarm_trigger(ctx->a_random);
      }
      played = 0;
    }
#ifdef DEBUG_FREEZE
    xdebug("%s:%d next_video()\n", __func__, __LINE__);
#endif
    next_video(ctx);
  }
}
