/**
 * @file   video_compress/libavcodec.h
 * @author Martin Pulec     <pulec@cesnet.cz>
 */
/*
 * Copyright (c) 2013-2014 CESNET, z. s. p. o.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, is permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of CESNET nor the names of its contributors may be
 *    used to endorse or promote products derived from this software without
 *    specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING,
 * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#define __STDC_CONSTANT_MACROS

#ifdef HAVE_CONFIG_H
#include "config.h"
#include "config_unix.h"
#include "config_win32.h"
#endif // HAVE_CONFIG_H

#include "libavcodec_common.h"
#include "video_compress/libavcodec.h"

#include <assert.h>

#include "compat/platform_spin.h"
#include "debug.h"
#include "host.h"
#include "messaging.h"
#include "module.h"
#include "utils/misc.h"
#include "utils/resource_manager.h"
#include "utils/worker.h"
#include "video.h"
#include "video_compress.h"

#include <unordered_map>

#define DEFAULT_CODEC MJPG

namespace {

using namespace std;

struct setparam_param {
        AVCodec *codec;
        bool have_preset;
        double fps;
        bool interlaced;
        bool exact_bitrate;
};

typedef struct {
        enum AVCodecID av_codec;
        double avg_bpp;
        void (*set_param)(AVCodecContext *, struct setparam_param *);
} codec_params_t;


static void setparam_default(AVCodecContext *, struct setparam_param *);
static void setparam_h264(AVCodecContext *, struct setparam_param *);
static void setparam_vp8(AVCodecContext *, struct setparam_param *);
static struct response *compress_change_callback(struct module *mod, struct message *msg);
static void libavcodec_compress_done(struct module *mod);
static void libavcodec_vid_enc_frame_dispose(struct video_frame *);


static unordered_map<codec_t, codec_params_t, hash<int>> codec_params = {
        {H264, { AV_CODEC_ID_H264,
                0.07 * 2 /* for H.264: 1 - low motion, 2 - medium motion, 4 - high motion */
                * 2, // take into consideration that our H.264 is less effective due to specific preset/tune
                setparam_h264
        }},
        { MJPG, {
                AV_CODEC_ID_MJPEG,
                0.3,
                setparam_default
        }},
        { VP8, {
                AV_CODEC_ID_VP8,
                0.2,
                setparam_vp8
        }},
};

struct state_video_compress_libav {
        struct module       module_data;

        pthread_mutex_t    *lavcd_global_lock;

        struct video_desc   saved_desc;

        AVFrame            *in_frame;
        // for every core - parts of the above
        AVFrame           **in_frame_part;
        int                 cpu_count;
        AVCodec            *codec;
        AVCodecContext     *codec_ctx;

        unsigned char      *decoded;
        decoder_t           decoder;

        codec_t             selected_codec_id;
        int64_t             requested_bitrate;
        // may be 422, 420 or 0 (no subsampling explicitly requested
        int                 requested_subsampling;
        // actual value used
        int                 subsampling;

        codec_t             out_codec;
        char               *preset;

        platform_spin_t     spin;
        struct video_desc compressed_desc;

        bool exact_bitrate;
};

static void to_yuv420(AVFrame *out_frame, unsigned char *in_data, int width, int height);
static void to_yuv422(AVFrame *out_frame, unsigned char *in_data, int width, int height);
static void usage(void);
static int parse_fmt(struct state_video_compress_libav *s, char *fmt);
static void cleanup(struct state_video_compress_libav *s);

static void libavcodec_vid_enc_frame_dispose(struct video_frame *frame) {
#if LIBAVCODEC_VERSION_MAJOR >= 54
        AVPacket *pkt = (AVPacket *) frame->dispose_udata;
        av_free_packet(pkt);
        free(pkt);
#else
        free(frame->tiles[0].data);
#endif // LIBAVCODEC_VERSION_MAJOR >= 54
        vf_free(frame);
}

static void usage() {
        printf("Libavcodec encoder usage:\n");
        printf("\t-c libavcodec[:codec=<codec_name>][:bitrate=<bits_per_sec>]"
                        "[:subsampling=<subsampling>][:preset=<preset>]"
                        "[:exact_bitrate]\n");
        printf("\t\t<codec_name> may be specified codec name (default MJPEG), supported codecs:\n");
        printf("\t\texact_bitrate - means that encoder will try to keep bitrate "
               "\t\t\tas constant as it can\n");
        for (auto && param : codec_params) {
                if(param.second.av_codec != 0) {
                        const char *availability = "not available";
                        if(avcodec_find_encoder(param.second.av_codec)) {
                                availability = "available";
                        }
                        printf("\t\t\t%s - %s\n", get_codec_name(param.first), availability);
                }

        }
        printf("\t\t<bits_per_sec> specifies requested bitrate\n");
        printf("\t\t<subsampling> may be one of 422 or 420, default 420 for progresive, 422 for interlaced\n");
        printf("\t\t<preset> codec preset options, eg. ultrafast, superfast, medium etc. for H.264\n");
        printf("\t\t\t0 means codec default (same as when parameter omitted)\n");
}

static int parse_fmt(struct state_video_compress_libav *s, char *fmt) {
        char *item, *save_ptr = NULL;
        if(fmt) {
                while((item = strtok_r(fmt, ":", &save_ptr)) != NULL) {
                        if(strncasecmp("help", item, strlen("help")) == 0) {
                                usage();
                                return 1;
                        } else if(strncasecmp("codec=", item, strlen("codec=")) == 0) {
                                char *codec = item + strlen("codec=");
                                s->selected_codec_id = get_codec_from_name(codec);
                                if (s->selected_codec_id == VIDEO_CODEC_NONE) {
                                        fprintf(stderr, "[lavd] Unable to find codec: \"%s\"\n", codec);
                                        return -1;
                                }
                        } else if(strncasecmp("bitrate=", item, strlen("bitrate=")) == 0) {
                                char *bitrate_str = item + strlen("bitrate=");
                                s->requested_bitrate = unit_evaluate(bitrate_str);
                        } else if(strncasecmp("subsampling=", item, strlen("subsampling=")) == 0) {
                                char *subsample_str = item + strlen("subsampling=");
                                s->requested_subsampling = atoi(subsample_str);
                                if(s->requested_subsampling != 422 &&
                                                s->requested_subsampling != 420) {
                                        fprintf(stderr, "[lavc] Supported subsampling is only 422 or 420.\n");
                                        return -1;
                                }
                        } else if(strncasecmp("preset=", item, strlen("preset=")) == 0) {
                                char *preset = item + strlen("preset=");
                                s->preset = strdup(preset);
                        } else if (strcasecmp("exact_bitrate", item) == 0) {
                                s->exact_bitrate = true;
                        } else {
                                fprintf(stderr, "[lavc] Error: unknown option %s.\n",
                                                item);
                                return -1;
                        }
                        fmt = NULL;
                }
        }

        return 0;
}

bool libavcodec_is_supported() {
        avcodec_register_all();
        if (avcodec_find_encoder(AV_CODEC_ID_H264)) {
                return true;
        } else {
                return false;
        }
}

struct module * libavcodec_compress_init(struct module *parent, const struct video_compress_params *params)
{
        struct state_video_compress_libav *s;
        const char *opts = params->cfg;

        s = (struct state_video_compress_libav *) calloc(1, sizeof(struct state_video_compress_libav));
        s->lavcd_global_lock = rm_acquire_shared_lock(LAVCD_LOCK_NAME);

        /*  register all the codecs (you can also register only the codec
         *         you wish to have smaller code */
        avcodec_register_all();

        s->codec = NULL;
        s->codec_ctx = NULL;
        s->in_frame = NULL;
        s->selected_codec_id = DEFAULT_CODEC;
        s->subsampling = s->requested_subsampling = 0;
        s->preset = NULL;

        s->requested_bitrate = -1;

        memset(&s->saved_desc, 0, sizeof(s->saved_desc));

        char *fmt = strdup(opts);
        int ret = parse_fmt(s, fmt);
        free(fmt);
        if(ret != 0) {
                free(s);
                if(ret > 0)
                        return &compress_init_noerr;
                else
                        return NULL;
        }

        platform_spin_init(&s->spin);

        printf("[Lavc] Using codec: %s\n", get_codec_name(s->selected_codec_id));

        s->cpu_count = sysconf(_SC_NPROCESSORS_ONLN);
        if(s->cpu_count < 1) {
                fprintf(stderr, "Warning: Cannot get number of CPU cores!\n");
                s->cpu_count = 1;
        }
        s->in_frame_part = (AVFrame **) calloc(s->cpu_count, sizeof(AVFrame *));
        for(int i = 0; i < s->cpu_count; i++) {
                s->in_frame_part[i] = av_frame_alloc();
        }

        s->decoded = NULL;

        module_init_default(&s->module_data);
        s->module_data.cls = MODULE_CLASS_DATA;
        s->module_data.priv_data = s;
        s->module_data.deleter = libavcodec_compress_done;
        s->module_data.msg_callback = compress_change_callback;
        module_register(&s->module_data, parent);

        return &s->module_data;
}

static bool configure_with(struct state_video_compress_libav *s, struct video_desc desc)
{
        int ret;
        AVCodecID codec_id;
        AVPixelFormat pix_fmt;
        double avg_bpp = 0; // average bite per pixel

        s->compressed_desc = desc;

        auto it = codec_params.find(s->selected_codec_id);
        if (it != codec_params.end()) {
                codec_id = it->second.av_codec;
                avg_bpp = it->second.avg_bpp;
        } else  {
                fprintf(stderr, "[lavc] Requested output codec isn't "
                                "supported by libavcodec.\n");
                return false;
        }

        s->compressed_desc.color_spec = s->selected_codec_id;
        s->compressed_desc.tile_count = 1;

#ifndef HAVE_GPL
        if(s->selected_codec_id == H264) {
                fprintf(stderr, "H.264 not available in UltraGrid BSD build. "
                                "Reconfigure UltraGrid with --enable-gpl if "
                                "needed.\n");
                exit_uv(1);
                return false;
        }
#endif

        /* find the video encoder */
        s->codec = avcodec_find_encoder(codec_id);
        if (!s->codec) {
                fprintf(stderr, "Libavcodec doesn't contain specified codec.\n"
                                "Hint: Check if you have libavcodec-extra package installed.\n");
                return false;
        }

        enum AVPixelFormat requested_pix_fmts[100];
        int total_pix_fmts = 0;

        bool f422_ok = false;
        /* either user has explicitly requested subsampling
         * or for interlaced format is better to have 422 */
        if(s->requested_subsampling == 422 ||
                        (desc.interlacing == INTERLACED_MERGED &&
                         s->requested_subsampling != 420)) {
                memcpy(requested_pix_fmts + total_pix_fmts,
                                fmts422, sizeof(fmts422));
                total_pix_fmts += sizeof(fmts422) / sizeof(enum AVPixelFormat);
                f422_ok = true;
        }
        memcpy(requested_pix_fmts + total_pix_fmts, fmts420, sizeof(fmts420));
        total_pix_fmts += sizeof(fmts420) / sizeof(enum AVPixelFormat);
        // we still use 422 as a fallback
        if(!f422_ok) {
                memcpy(requested_pix_fmts + total_pix_fmts,
                                fmts422, sizeof(fmts422));
                total_pix_fmts += sizeof(fmts422) / sizeof(enum AVPixelFormat);
                f422_ok = true;
        }

        requested_pix_fmts[total_pix_fmts++] = AV_PIX_FMT_NONE;

        pix_fmt = get_best_pix_fmt(requested_pix_fmts, s->codec->pix_fmts);
        if(pix_fmt == AV_PIX_FMT_NONE) {
                fprintf(stderr, "[Lavc] Unable to find suitable pixel format.\n");
                return false;
        }

        if(is422(pix_fmt)) {
                s->subsampling = 422;
        } else if(is420(pix_fmt)) {
                s->subsampling = 420;
        } else {
                fprintf(stderr, "[Lavc] Unknown subsampling.\n");
                abort();
        }

        // avcodec_alloc_context3 allocates context and sets default value
        s->codec_ctx = avcodec_alloc_context3(s->codec);
        if (!s->codec_ctx) {
                fprintf(stderr, "Could not allocate video codec context\n");
                return false;
        }

        /* put parameters */
        if(s->requested_bitrate > 0) {
                s->codec_ctx->bit_rate = s->requested_bitrate;
        } else {
                s->codec_ctx->bit_rate = desc.width * desc.height *
                        avg_bpp * desc.fps;
        }

        s->codec_ctx->bit_rate_tolerance = s->codec_ctx->bit_rate / 4;

        /* resolution must be a multiple of two */
        s->codec_ctx->width = desc.width;
        s->codec_ctx->height = desc.height;
        /* frames per second */
        s->codec_ctx->time_base= (AVRational){1,(int) desc.fps};
        s->codec_ctx->gop_size = 20; /* emit one intra frame every ten frames */
        s->codec_ctx->max_b_frames = 0;
        switch(desc.color_spec) {
                case UYVY:
                        s->decoder = (decoder_t) memcpy;
                        break;
                case YUYV:
                        s->decoder = (decoder_t) vc_copylineYUYV;
                        break;
                case v210:
                        s->decoder = (decoder_t) vc_copylinev210;
                        break;
                case RGB:
                        s->decoder = (decoder_t) vc_copylineRGBtoUYVY;
                        break;
                case BGR:
                        s->decoder = (decoder_t) vc_copylineBGRtoUYVY;
                        break;
                case RGBA:
                        s->decoder = (decoder_t) vc_copylineRGBAtoUYVY;
                        break;
                default:
                        fprintf(stderr, "[Libavcodec] Unable to find "
                                        "appropriate pixel format.\n");
                        return false;
        }

        s->codec_ctx->pix_fmt = pix_fmt;

        s->decoded = (unsigned char *) malloc(desc.width * desc.height * 4);

        if(s->preset) {
                if(av_opt_set(s->codec_ctx->priv_data, "preset", s->preset, 0) != 0) {
                        fprintf(stderr, "[Lavc] Error: Unable to set preset.\n");
                        return false;
                }
        }

        struct setparam_param param;
        param.have_preset = s->preset != 0;
        param.fps = desc.fps;
        param.codec = s->codec;
        param.interlaced = desc.interlacing == INTERLACED_MERGED;
        param.exact_bitrate = s->exact_bitrate;

        codec_params[s->selected_codec_id].set_param(s->codec_ctx, &param);

        pthread_mutex_lock(s->lavcd_global_lock);
        /* open it */
        if (avcodec_open2(s->codec_ctx, s->codec, NULL) < 0) {
                fprintf(stderr, "Could not open codec\n");
                pthread_mutex_unlock(s->lavcd_global_lock);
                return false;
        }
        pthread_mutex_unlock(s->lavcd_global_lock);

        s->in_frame = av_frame_alloc();
        if (!s->in_frame) {
                fprintf(stderr, "Could not allocate video frame\n");
                return false;
        }
#if 0
        s->in_frame->format = s->codec_ctx->pix_fmt;
        s->in_frame->width = s->codec_ctx->width;
        s->in_frame->height = s->codec_ctx->height;
#endif

        /* the image can be allocated by any means and av_image_alloc() is
         * just the most convenient way if av_malloc() is to be used */
        ret = av_image_alloc(s->in_frame->data, s->in_frame->linesize,
                        s->codec_ctx->width, s->codec_ctx->height,
                        s->codec_ctx->pix_fmt, 32);
        if (ret < 0) {
                fprintf(stderr, "Could not allocate raw picture buffer\n");
                return false;
        }
        for(int i = 0; i < s->cpu_count; ++i) {
                int chunk_size = s->codec_ctx->height / s->cpu_count;
                chunk_size = chunk_size / 2 * 2;
                s->in_frame_part[i]->data[0] = s->in_frame->data[0] + s->in_frame->linesize[0] * i *
                        chunk_size;
                if(s->subsampling == 420) {
                        chunk_size /= 2;
                }
                s->in_frame_part[i]->data[1] = s->in_frame->data[1] + s->in_frame->linesize[1] * i *
                        chunk_size;
                s->in_frame_part[i]->data[2] = s->in_frame->data[2] + s->in_frame->linesize[2] * i *
                        chunk_size;
                s->in_frame_part[i]->linesize[0] = s->in_frame->linesize[0];
                s->in_frame_part[i]->linesize[1] = s->in_frame->linesize[1];
                s->in_frame_part[i]->linesize[2] = s->in_frame->linesize[2];
        }

        s->saved_desc = desc;
        s->out_codec = s->compressed_desc.color_spec;

        return true;
}

static void to_yuv420(AVFrame *out_frame, unsigned char *in_data, int width, int height)
{
        unsigned char *src = in_data + 1;
        for(int y = 0; y < (int) height; ++y) {
                unsigned char *dst_y = out_frame->data[0] + out_frame->linesize[0] * y;
                for(int x = 0; x < width; ++x) {
                        *dst_y++ = *src;
                        src += 2;
                }
        }

        for(int y = 0; y < (int) height / 2; ++y) {
                /*  every even row */
                unsigned char *src1 = in_data + (y * 2) * (width * 2);
                /*  every odd row */
                unsigned char *src2 = in_data + (y * 2 + 1) * (width * 2);
                unsigned char *dst_cb = out_frame->data[1] + out_frame->linesize[1] * y;
                unsigned char *dst_cr = out_frame->data[2] + out_frame->linesize[2] * y;
                for(int x = 0; x < width / 2; ++x) {
                        *dst_cb++ = (*src1 + *src2) / 2;
                        src1 += 2;
                        src2 += 2;
                        *dst_cr++ = (*src1 + *src2) / 2;
                        src1 += 2;
                        src2 += 2;
                }
        }
}

static void to_yuv422(AVFrame *out_frame, unsigned char *src, int width, int height)
{
        for(int y = 0; y < (int) height; ++y) {
                unsigned char *dst_y = out_frame->data[0] + out_frame->linesize[0] * y;
                unsigned char *dst_cb = out_frame->data[1] + out_frame->linesize[1] * y;
                unsigned char *dst_cr = out_frame->data[2] + out_frame->linesize[2] * y;
                for(int x = 0; x < width; x += 2) {
                        *dst_cb++ = *src++;
                        *dst_y++ = *src++;
                        *dst_cr++ = *src++;
                        *dst_y++ = *src++;
                }
        }
}

struct my_task_data {
        void (*callback)(AVFrame *out_frame, unsigned char *in_data, int width, int height);
        AVFrame *out_frame;
        unsigned char *in_data;
        int width;
        int height;
};

void *my_task(void *arg);

void *my_task(void *arg) {
        struct my_task_data *data = (struct my_task_data *) arg;
        data->callback(data->out_frame, data->in_data, data->width, data->height);
        return NULL;
}

shared_ptr<video_frame> libavcodec_compress_tile(struct module *mod, shared_ptr<video_frame> tx)
{
        struct state_video_compress_libav *s = (struct state_video_compress_libav *) mod->priv_data;
        static int frame_seq = 0;
        int ret;
#if LIBAVCODEC_VERSION_MAJOR >= 54
        int got_output;
        AVPacket *pkt;
#endif
        unsigned char *decoded;
        shared_ptr<video_frame> out{};

        platform_spin_lock(&s->spin);

        if(!video_desc_eq_excl_param(video_desc_from_frame(tx.get()),
                                s->saved_desc, PARAM_TILE_COUNT)) {
                cleanup(s);
                int ret = configure_with(s, video_desc_from_frame(tx.get()));
                if(!ret) {
                        goto error;
                }
        }

        out = shared_ptr<video_frame>(vf_alloc_desc(s->compressed_desc), libavcodec_vid_enc_frame_dispose);
#if LIBAVCODEC_VERSION_MAJOR >= 54
        pkt = (AVPacket *) malloc(sizeof(AVPacket));
        av_init_packet(pkt);
        pkt->data = NULL;
        pkt->size = 0;
        out->dispose_udata = pkt;
#else
        out->tiles[0].data = (char *) malloc(s->compressed_desc.width *
                        s->compressed_desc.height * 4);
#endif // LIBAVCODEC_VERSION_MAJOR >= 54


        s->in_frame->pts = frame_seq++;

        if((void *) s->decoder != (void *) memcpy) {
                unsigned char *line1 = (unsigned char *) tx->tiles[0].data;
                unsigned char *line2 = (unsigned char *) s->decoded;
                int src_linesize = vc_get_linesize(tx->tiles[0].width, tx->color_spec);
                int dst_linesize = tx->tiles[0].width * 2; /* UYVY */
                for (int i = 0; i < (int) tx->tiles[0].height; ++i) {
                        s->decoder(line2, line1, dst_linesize,
                                        0, 8, 16);
                        line1 += src_linesize;
                        line2 += dst_linesize;
                }
                decoded = s->decoded;
        } else {
                decoded = (unsigned char *) tx->tiles[0].data;
        }

        {
                task_result_handle_t handle[s->cpu_count];
                struct my_task_data data[s->cpu_count];
                for(int i = 0; i < s->cpu_count; ++i) {
                        assert(s->subsampling == 422 || s->subsampling == 420);
                        data[i].callback = s->subsampling == 420 ? to_yuv420 : to_yuv422;
                        data[i].out_frame = s->in_frame_part[i];

                        size_t height = tx->tiles[0].height / s->cpu_count;
                        // height needs to be even
                        height = height / 2 * 2;
                        if (i < s->cpu_count - 1) {
                                data[i].height = height;
                        } else { // we are last so we need to do the rest
                                data[i].height = tx->tiles[0].height -
                                        height * (s->cpu_count - 1);
                        }
                        data[i].width = tx->tiles[0].width;
                        data[i].in_data = decoded + i * height *
                                vc_get_linesize(tx->tiles[0].width, UYVY);

                        // run !
                        handle[i] = task_run_async(my_task, (void *) &data[i]);
                }

                for(int i = 0; i < s->cpu_count; ++i) {
                        wait_task(handle[i]);
                }
        }

#if LIBAVCODEC_VERSION_MAJOR >= 54
        /* encode the image */
        ret = avcodec_encode_video2(s->codec_ctx, pkt,
                        s->in_frame, &got_output);
        if (ret < 0) {
                fprintf(stderr, "Error encoding frame\n");
                goto error;
        }

        if (got_output) {
                //printf("Write frame %3d (size=%5d)\n", frame_seq, s->pkt[buffer_idx].size);
                out->tiles[0].data = (char *) pkt->data;
                out->tiles[0].data_len = pkt->size;
        } else {
                goto error;
        }
#else
        /* encode the image */
        ret = avcodec_encode_video(s->codec_ctx, (uint8_t *) out->tiles[0].data,
                        out->tiles[0].width * out->tiles[0].height * 4,
                        s->in_frame);
        if (ret < 0) {
                fprintf(stderr, "Error encoding frame\n");
                goto error;
        }

        if (ret) {
                //printf("Write frame %3d (size=%5d)\n", frame_seq, s->pkt[buffer_idx].size);
                out->tiles[0].data_len = ret;
        } else {
                goto error;
        }
#endif // LIBAVCODEC_VERSION_MAJOR >= 54

        platform_spin_unlock(&s->spin);

        return out;

error:
        platform_spin_unlock(&s->spin);
        return NULL;
}

static void cleanup(struct state_video_compress_libav *s)
{
        if(s->codec_ctx) {
                pthread_mutex_lock(s->lavcd_global_lock);
                avcodec_close(s->codec_ctx);
                pthread_mutex_unlock(s->lavcd_global_lock);
        }
        if(s->in_frame) {
                av_freep(s->in_frame->data);
                av_free(s->in_frame);
                s->in_frame = NULL;
        }
        av_free(s->codec_ctx);
        s->codec_ctx = NULL;
        free(s->decoded);
        s->decoded = NULL;
}

static void libavcodec_compress_done(struct module *mod)
{
        struct state_video_compress_libav *s = (struct state_video_compress_libav *) mod->priv_data;

        cleanup(s);

        rm_release_shared_lock(LAVCD_LOCK_NAME);
        free(s->preset);
        for(int i = 0; i < s->cpu_count; i++) {
                av_free(s->in_frame_part[i]);
        }
        free(s->in_frame_part);
        platform_spin_destroy(&s->spin);
        free(s);
}

static void setparam_default(AVCodecContext *codec_ctx, struct setparam_param *param)
{
        UNUSED(param);
        // zero should mean count equal to the number of virtual cores
        if(param->codec->capabilities & CODEC_CAP_SLICE_THREADS) {
                codec_ctx->thread_count = 0;
                codec_ctx->thread_type = FF_THREAD_SLICE;
        } else {
                fprintf(stderr, "[Lavc] Warning: Codec doesn't support slice-based multithreading.\n");
#if 0
                if(codec->capabilities & CODEC_CAP_FRAME_THREADS) {
                        codec_ctx->thread_count = 0;
                        codec_ctx->thread_type = FF_THREAD_FRAME;
                } else {
                        fprintf(stderr, "[Lavc] Warning: Codec doesn't support frame-based multithreading.\n");
                }
#endif
        }
}

#define APPEND_PARAM(params, param) \
        if(strlen(params) > 0) { \
                strncat(params, ":", sizeof(params) - strlen(params) - 1); \
        } \
        strncat(params, param, sizeof(params) - strlen(params) - 1);

static void setparam_h264(AVCodecContext *codec_ctx, struct setparam_param *param)
{
        char params[512] = "";

        if(!param->have_preset) {
                // ultrafast - --aq-mode 0
                // This option causes posterization. Enabling it requires some 20% additional
                // percent of CPU.
                strncat(params, "no-8x8dct=1:b-adapt=0:bframes=0:no-cabac=1:"
                        "no-deblock=1:no-mbtree=1:me=dia:no-mixed-refs=1:partitions=none:"
                        "rc-lookahead=0:ref=1:scenecut=0:subme=0:trellis=0",
                        sizeof(params) - strlen(params) - 1);
        }

        if (param->exact_bitrate) {
                APPEND_PARAM(params, "aq_mode=0");
        } else {
                APPEND_PARAM(params, "aq_mode=1");
        }

        if (param->interlaced && !param->exact_bitrate) {
                APPEND_PARAM(params, "tff=1");
        }

        if(strlen(params) > 0) {
                int ret;
                // newer LibAV
                ret = av_opt_set(codec_ctx->priv_data, "x264-params", params, 0);
                if(ret != 0) {
                        // newer FFMPEG
                        ret = av_opt_set(codec_ctx->priv_data, "x264opts", params, 0);
                }
                if(ret != 0) {
                        // older version of both
                        // or superfast?? requires + some 70 % CPU but does not cause posterization
                        ret = av_opt_set(codec_ctx->priv_data, "preset", "ultrafast", 0);
                        fprintf(stderr, "[Lavc] Warning: Old FFMPEG/LibAV detected. "
                                        "Try supplying 'preset=superfast' argument to "
                                        "avoid posterization!\n");
                }
                if(ret != 0) {
                        fprintf(stderr, "[Lavc] Warning: Unable to set preset.\n");
                }
        }

        //av_opt_set(codec_ctx->priv_data, "tune", "fastdecode", 0);
        av_opt_set(codec_ctx->priv_data, "tune", "fastdecode,zerolatency", 0);

        if (param->exact_bitrate) {
                codec_ctx->rc_max_rate = codec_ctx->bit_rate / 2 * 3;
                //codec_ctx->rc_min_rate = s->codec_ctx->bit_rate / 4 * 3;
                codec_ctx->rc_buffer_aggressivity = 1.0;
                codec_ctx->rc_buffer_size = codec_ctx->rc_max_rate / param->fps;
                codec_ctx->qcompress = 0.0f;
                //codec_ctx->qblur = 0.0f;
                //codec_ctx->rc_min_vbv_overflow_use = 1.0f;
                //codec_ctx->rc_max_available_vbv_use = 1.0f;
                codec_ctx->qmin = 0;
                codec_ctx->qmax = 69;
                codec_ctx->max_qdiff = 69;
                codec_ctx->rc_qsquish = 0;
                //codec_ctx->scenechange_threshold = 100;
        }

#ifndef DISABLE_H264_INTRA_REFRESH
        codec_ctx->refs = 1;
        av_opt_set(codec_ctx->priv_data, "intra-refresh", "1", 0);
#endif // defined DISABLE_H264_INTRA_REFRESH
}

static void setparam_vp8(AVCodecContext *codec_ctx, struct setparam_param *param)
{
        codec_ctx->thread_count = 8;
        codec_ctx->profile = 0;
        codec_ctx->slices = 4;
        codec_ctx->rc_buffer_size = codec_ctx->bit_rate / param->fps;
        codec_ctx->rc_buffer_aggressivity = 0.5;
}

static struct response *compress_change_callback(struct module *mod, struct message *msg)
{
        struct state_video_compress_libav *s = (struct state_video_compress_libav *) mod->priv_data;
        static struct response *ret;

        struct msg_change_compress_data *data =
                (struct msg_change_compress_data *) msg;

        platform_spin_lock(&s->spin);
        if(parse_fmt(s, data->config_string) == 0) {
                ret = new_response(RESPONSE_OK, NULL);
        } else {
                ret = new_response(RESPONSE_BAD_REQUEST, strdup("(Module libavcodec)"));
        }
        memset(&s->saved_desc, 0, sizeof(s->saved_desc));
        platform_spin_unlock(&s->spin);

        free_message(msg);

        return ret;
}

} // end of anonymous namespace

struct compress_info_t libavcodec_info = {
        "libavcodec",
        libavcodec_compress_init,
        NULL,
        libavcodec_compress_tile,
        libavcodec_is_supported,
        {
                { "codec=H.264:bitrate=5M", 20, 5*1000*1000, {25, 1.5, 0}, {15, 1, 0} },
                { "codec=H.264:bitrate=10M", 30, 10*1000*1000, {28, 1.5, 0}, {20, 1, 0} },
                { "codec=H.264:bitrate=15M", 50, 15*1000*1000, {30, 1.5, 0}, {25, 1, 0} },
#if 0
                { "codec=MJPEG", 35, 50*1000*1000, {20, 0.75, 0}, {10, 0.5, 0}  },
#endif
        },
};

