#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#include "encode.h"

#ifdef HAVE_X264
#include <stdint.h>
#include "x264.h"
#endif

#ifdef HAVE_FFMPEG
#include "libavcodec/avcodec.h"
static int ffmpeg_init = 0;
#endif

#define IS_VERBOSE (enc->cfg.flags < 0)
#define VPREFIX "[encode] "

typedef struct _encoder {
    encoder_config_t cfg;
    FILE *out;
    int frameno;
    union {
        int dummy;
#ifdef HAVE_X264
        struct {
            x264_param_t params;
            x264_t* enc;
        } x264;
#endif
#ifdef HAVE_FFMPEG
        struct {
            AVCodec* codec;
            AVCodecContext* ctx;
            AVFrame* frame;
            int bufsize;
            void* outbuf;
        } ffmpeg;
#endif
    } enc;
} encoder_t;

int encoder_type_is_available(encoder_type_t encoder_type) {
    switch (encoder_type) {
        case ENCTYPE_RAW_YUV:
        case ENCTYPE_YUV4MPEG:
#ifdef HAVE_X264
        case ENCTYPE_X264:
#endif
#ifdef HAVE_FFMPEG
        case ENCTYPE_FFMPEG_MPEG2:
        case ENCTYPE_FFMPEG_MPEG4:
#endif
        case ENCTYPE_DUMMY:
            return 1;
        default:
            return 0;
    }
}

encoder_type_t encoder_detect_type(const char *filename) {
    char ext[8];
    int extpos = 0;
    if (!filename) return ENCTYPE_AUTO;
    for (;  *filename;  ++filename) {
        if (*filename == '.')
            extpos = 0;
        else if (extpos < 7)
            ext[extpos++] = tolower(*filename);
    }
    ext[extpos] = '\0';
    #define ext_is(x) !strcmp(ext, x)
    if (ext_is("yuv"))
        return ENCTYPE_RAW_YUV;
    if (ext_is("y4m"))
        return ENCTYPE_YUV4MPEG;
    if (ext_is("264") || ext_is("h264"))
        return ENCTYPE_X264;
    if (ext_is("m4v") || ext_is("m4e"))
#ifdef HAVE_XVID
        return ENCTYPE_XVID;
#else
        return ENCTYPE_FFMPEG_MPEG4;
#endif
    if (ext_is("mpg") || ext_is("m2v") || ext_is("m2e"))
        return ENCTYPE_FFMPEG_MPEG2;
    return ENCTYPE_AUTO;
}

////////////////////////////////////////////////////////////////////////////////

static const char* enctype2str(encoder_type_t encoder_type) {
    switch (encoder_type) {
        case ENCTYPE_AUTO:         return "auto-detect";
        case ENCTYPE_RAW_YUV:      return "Raw YUV";
        case ENCTYPE_YUV4MPEG:     return "YUV4MPEG2";
        case ENCTYPE_X264:         return "H.264 (x264)";
        case ENCTYPE_XVID:         return "MPEG-4 ASP (XviD)";
        case ENCTYPE_FFMPEG_MPEG2: return "MPEG-2 (libavcodec)";
        case ENCTYPE_FFMPEG_MPEG4: return "MPEG-4 ASP (libavcodec)";
        case ENCTYPE_DUMMY:        return "dummy";
        default:                   return "unknown/invalid";
    }    
}

static void reduce_fraction(int *num, int *den) {
    int n = *num;
    int d = *den;
    int i = 2;
    while ((i <= n) && (i <= d)) {
        if (!(n % i) && !(d % i)) {
            n /= i;
            d /= i;
            i = 2;
        } else
            ++i;
    }
    *num = n;
    *den = d;
}

static void encode_free(encoder_t *enc) {
    if (!enc) return;
    switch (enc->cfg.encoder_type) {
#ifdef HAVE_X264
        case ENCTYPE_X264:
            if (enc->enc.x264.enc) {
                x264_encoder_close(enc->enc.x264.enc);
                enc->enc.x264.enc = NULL;
            }
            break;
#endif
#ifdef HAVE_FFMPEG
        case ENCTYPE_FFMPEG_MPEG2:
        case ENCTYPE_FFMPEG_MPEG4:
            if (enc->enc.ffmpeg.ctx) {
                avcodec_close(enc->enc.ffmpeg.ctx);
                av_free(enc->enc.ffmpeg.ctx);
                enc->enc.ffmpeg.ctx = NULL;
            }
            if (enc->enc.ffmpeg.frame) {
                av_free(enc->enc.ffmpeg.frame);
                enc->enc.ffmpeg.frame = NULL;
            }
            if (enc->enc.ffmpeg.outbuf) {
                free(enc->enc.ffmpeg.outbuf);
                enc->enc.ffmpeg.outbuf = NULL;
            }
            break;
#endif
        default: break;
    }
    if (enc->out) {
        fclose(enc->out);
        enc->out = NULL;
    }
    free((void*) enc);
}

////////////////////////////////////////////////////////////////////////////////

encode_init_result_t encode_init(encoder_handle_t* encoder, const encoder_config_t* cfg) {
    encoder_t *enc;
    int sar_num, sar_den;
    int dar_num, dar_den;

    /* basic sanity checks, allocate encoder structure */
    if (!encoder || !cfg) return ENC_INIT_NO_ARG;
    if (!cfg->width || !cfg->height) return ENC_INIT_ERR_CFG;
    *encoder = NULL;
    enc = calloc(1, sizeof(encoder_t));
    if (!enc) return ENC_INIT_ERR_MEM;
    enc->cfg = *cfg;

    /* auto-detect and check encoder type */
    if (!enc->cfg.encoder_type) {
        enc->cfg.encoder_type = encoder_detect_type(enc->cfg.filename);
        if (!enc->cfg.encoder_type) {
            if (IS_VERBOSE) printf(VPREFIX "could not auto-detect encoder\n");
            encode_free(enc);
            return ENC_INIT_NO_ENC;
        }
    }
    if (!encoder_type_is_available(enc->cfg.encoder_type)) {
        if (IS_VERBOSE) printf(VPREFIX "%s encoder is unavailable\n", enctype2str(enc->cfg.encoder_type));
        encode_free(enc);
        return ENC_INIT_NO_ENC;
    }
    if ((enc->cfg.encoder_type != ENCTYPE_DUMMY) && !cfg->filename) {
        encode_free(enc);
        return ENC_INIT_NO_ARG;
    }

    /* sanitize frame rates and aspect ratios */
    if (!enc->cfg.fps_num || !enc->cfg.fps_den) {
        enc->cfg.fps_num = 25;
        enc->cfg.fps_den = 1;
    } else
        reduce_fraction(&enc->cfg.fps_num, &enc->cfg.fps_den);
    if (!enc->cfg.aspect_num || !enc->cfg.aspect_den) {
        enc->cfg.flags &= ~ENC_DAR;
        enc->cfg.aspect_num = 1;
        enc->cfg.aspect_den = 1;
    } else
        reduce_fraction(&enc->cfg.aspect_num, &enc->cfg.aspect_den);
    if (enc->cfg.flags & ENC_DAR) {
        dar_num = enc->cfg.aspect_num;
        dar_den = enc->cfg.aspect_den;
        sar_num = dar_num * enc->cfg.height;
        sar_den = dar_den * enc->cfg.width;
        reduce_fraction(&sar_num, &sar_den);
    } else {
        sar_num = enc->cfg.aspect_num;
        sar_den = enc->cfg.aspect_den;
        dar_num = sar_num * enc->cfg.width;
        dar_den = sar_den * enc->cfg.height;
        reduce_fraction(&dar_num, &dar_den);
    }
    if (IS_VERBOSE) printf(VPREFIX "frame rate: %d/%d, SAR = %d:%d, DAR = %d:%d\n", enc->cfg.fps_num, enc->cfg.fps_den, sar_num, sar_den, dar_num, dar_den);

    /* for the dummy encoder, we're already done at this point */
    if (enc->cfg.encoder_type == ENCTYPE_DUMMY) {
        *encoder = (encoder_handle_t) enc;
        return ENC_INIT_OK;
    }

    /* open output file */
    if (IS_VERBOSE) printf(VPREFIX "target file: %s\n", enc->cfg.filename);
    enc->out = fopen(enc->cfg.filename, "wb");
    if (!enc->out) {
        encode_free(enc);
        return ENC_INIT_ERR_IO;
    }

    /* initialize encoder */
    if (IS_VERBOSE) printf(VPREFIX "format/encoder: %s\n", enctype2str(enc->cfg.encoder_type));
    switch (enc->cfg.encoder_type) {
        case ENCTYPE_RAW_YUV:
            break;
        case ENCTYPE_YUV4MPEG:
            fprintf(enc->out, "YUV4MPEG2 W%d H%d C420mpeg2 F%d:%d A%d:%d I%c\n",
                enc->cfg.width, enc->cfg.height,
                enc->cfg.fps_num, enc->cfg.fps_den,
                sar_num, sar_den, (enc->cfg.flags & ENC_INTERLACE) ? 'm' : 'p');
            break;
#ifdef HAVE_X264
        case ENCTYPE_X264: {
            if (!enc->cfg.quality) {
                enc->cfg.quality = 26;
                enc->cfg.flags &= ~(ENC_BITRATE | ENC_FIXED_TARGET);
            }
            if (!enc->cfg.keyint)     enc->cfg.keyint = 200;
            if (!enc->cfg.bframes)    enc->cfg.bframes = 2;
            if (!enc->cfg.refs)       enc->cfg.refs = 4;
            if (enc->cfg.bframes < 0) enc->cfg.bframes = 0;
            if (enc->cfg.flags & ENC_MAIN) {
                if (IS_VERBOSE) printf(VPREFIX "restricting encoder settings to Main Profile\n");
                enc->cfg.flags &= ~ENC_8X8;
            }
            if (enc->cfg.flags & ENC_BASELINE) {
                if (IS_VERBOSE) printf(VPREFIX "restricting encoder settings to Constrained Baseline Profile\n");
                enc->cfg.flags &= ~(ENC_CABAC | ENC_B_PYRAMID | ENC_8X8 | ENC_WEIGHTED | ENC_MIXED_REFS);
                enc->cfg.bframes = 0;
            }
            x264_param_default(&enc->enc.x264.params);
            enc->enc.x264.params.i_width = enc->cfg.width;
            enc->enc.x264.params.i_height = enc->cfg.height;
            enc->enc.x264.params.i_csp = X264_CSP_I420;
            enc->enc.x264.params.vui.i_sar_height = sar_den;
            enc->enc.x264.params.vui.i_sar_width = sar_num;
            enc->enc.x264.params.vui.i_overscan = 1;
            enc->enc.x264.params.vui.i_colorprim = (enc->cfg.flags & ENC_BT709) ? 1 : 6;
            enc->enc.x264.params.vui.i_transfer = (enc->cfg.flags & ENC_BT709) ? 1 : 6;
            enc->enc.x264.params.vui.i_colmatrix = (enc->cfg.flags & ENC_BT709) ? 1 : 6;
            enc->enc.x264.params.vui.b_fullrange = 0;
            enc->enc.x264.params.i_fps_num = enc->cfg.fps_num;
            enc->enc.x264.params.i_fps_den = enc->cfg.fps_den;
            enc->enc.x264.params.i_frame_reference = enc->cfg.refs;
            enc->enc.x264.params.i_keyint_max = enc->cfg.keyint;
            enc->enc.x264.params.i_bframe = enc->cfg.bframes;
            enc->enc.x264.params.i_bframe_adaptive = (enc->cfg.flags & ENC_TRELLIS) ? X264_B_ADAPT_TRELLIS : X264_B_ADAPT_FAST;
            enc->enc.x264.params.i_bframe_pyramid = (enc->cfg.flags & ENC_B_PYRAMID) ? X264_B_PYRAMID_NORMAL : X264_B_PYRAMID_NONE;
            enc->enc.x264.params.b_deblocking_filter = (enc->cfg.flags & ENC_NO_DEBLOCK) ? 0 : 1;
            enc->enc.x264.params.b_cabac = (enc->cfg.flags & ENC_CABAC) ? 1 : 0;
            enc->enc.x264.params.b_interlaced = (enc->cfg.flags & ENC_INTERLACE) ? 1 : 0;
            enc->enc.x264.params.i_log_level = (enc->cfg.flags & ENC_VERBOSE) ? X264_LOG_INFO : X264_LOG_NONE;
            enc->enc.x264.params.analyse.intra = X264_ANALYSE_I4x4 | X264_ANALYSE_I8x8;
            enc->enc.x264.params.analyse.inter = X264_ANALYSE_I4x4 | X264_ANALYSE_I8x8 | X264_ANALYSE_PSUB16x16 | X264_ANALYSE_PSUB8x8 | X264_ANALYSE_BSUB16x16;
            enc->enc.x264.params.analyse.b_transform_8x8 = (enc->cfg.flags & ENC_8X8) ? 1 : 0;
            enc->enc.x264.params.analyse.i_weighted_pred = (enc->cfg.flags & ENC_WEIGHTED) ? X264_WEIGHTP_SMART : X264_WEIGHTP_NONE;
            enc->enc.x264.params.analyse.b_weighted_bipred = (enc->cfg.flags & ENC_WEIGHTED) ? 1 : 0;
            enc->enc.x264.params.analyse.i_direct_mv_pred = X264_DIRECT_PRED_AUTO;
            enc->enc.x264.params.analyse.i_me_method = (enc->cfg.flags & ENC_HQ_ME) ? X264_ME_UMH : X264_ME_DIA;
            enc->enc.x264.params.analyse.i_subpel_refine = (enc->cfg.flags & ENC_HQ_ME) ? ((enc->cfg.flags & ENC_TRELLIS) ? 10 : 9) : 7;
            enc->enc.x264.params.analyse.b_chroma_me = (enc->cfg.flags & ENC_CHROMA_ME) ? 1 : 0;
            enc->enc.x264.params.analyse.b_mixed_references = (enc->cfg.flags & ENC_MIXED_REFS) ? 1 : 0;
            enc->enc.x264.params.analyse.i_trellis = (enc->cfg.flags & ENC_TRELLIS) ? 2 : 0;
            enc->enc.x264.params.analyse.b_fast_pskip = (enc->cfg.flags & ENC_RD) ? 0 : 1;
            enc->enc.x264.params.analyse.b_psy = (enc->cfg.flags & ENC_PSYCHO) ? 1 : 0;
            if (enc->cfg.flags & ENC_BITRATE) {
                if (IS_VERBOSE) printf(VPREFIX "using average bitrate (ABR) mode, target bitrate = %d kbps\n", enc->cfg.quality);
                enc->enc.x264.params.rc.i_rc_method = X264_RC_ABR;
                enc->enc.x264.params.rc.i_bitrate = enc->cfg.quality * 1000;
            } else if (enc->cfg.flags & ENC_FIXED_TARGET) {
                if (IS_VERBOSE) printf(VPREFIX "using fixed quantizer (CQP) mode, target quantizer = %d\n", enc->cfg.quality);
                enc->enc.x264.params.rc.i_rc_method = X264_RC_CQP;
                enc->enc.x264.params.rc.i_qp_constant = enc->cfg.quality;
            } else {
                if (IS_VERBOSE) printf(VPREFIX "using fixed quality (CRF) mode, target average quantizer = %d\n", enc->cfg.quality);
                enc->enc.x264.params.rc.i_rc_method = X264_RC_CRF;
                enc->enc.x264.params.rc.f_rf_constant = (float) enc->cfg.quality;
            }
            enc->enc.x264.params.rc.i_aq_mode = (enc->cfg.flags & ENC_PSYCHO) ? X264_AQ_VARIANCE : X264_AQ_NONE;
            enc->enc.x264.params.rc.b_mb_tree = (enc->cfg.flags & ENC_RD) ? 1 : 0;
            enc->enc.x264.params.b_annexb = 1;
            enc->enc.x264.params.b_vfr_input = 0;
            enc->enc.x264.params.i_timebase_num = enc->cfg.fps_den;
            enc->enc.x264.params.i_timebase_den = enc->cfg.fps_num;
            if (IS_VERBOSE) {
                printf(VPREFIX "IDR interval: %d frames, %d B frames, %d reference frames\n",
                       enc->cfg.keyint, enc->cfg.bframes, enc->cfg.refs);
                printf(VPREFIX "features used: x264 build %d, CA%sC",
                       X264_BUILD,
                       (enc->cfg.flags & ENC_CABAC) ? "BA" : "VL");
                if (enc->cfg.flags & ENC_B_PYRAMID) printf(", B-pyramid");
                if (!(enc->cfg.flags & ENC_NO_DEBLOCK)) printf(", deblock");
                if (enc->cfg.flags & ENC_8X8) printf(", 8x8 transform");
                if (enc->cfg.flags & ENC_WEIGHTED) printf(", weighted pred");
                if (enc->cfg.flags & ENC_MIXED_REFS) printf(", mixed refs");
                if (enc->cfg.flags & ENC_RD) printf(", %sRDO", (enc->cfg.flags & ENC_PSYCHO) ? "psy-" : "");
                if (enc->cfg.flags & ENC_TRELLIS) printf(", trellis");
                if (enc->cfg.flags & ENC_HQ_ME) printf(", HQ ME");
                if (enc->cfg.flags & ENC_CHROMA_ME) printf(", chroma ME");
                printf("\n");
            }
            enc->enc.x264.enc = x264_encoder_open(&enc->enc.x264.params);
            if (!enc->enc.x264.enc) {
                encode_free(enc);
                return ENC_INIT_ERR_LIB;
            }
            break; }
#endif
#ifdef HAVE_FFMPEG
        case ENCTYPE_FFMPEG_MPEG2:
        case ENCTYPE_FFMPEG_MPEG4: {
            if (!enc->cfg.quality) {
                enc->cfg.quality = 6;
                enc->cfg.flags &= ~(ENC_BITRATE | ENC_FIXED_TARGET);
            }
            if (!enc->cfg.keyint)     enc->cfg.keyint = (enc->cfg.encoder_type == ENCTYPE_FFMPEG_MPEG2) ? 15 : 200;
            if (!enc->cfg.bframes)    enc->cfg.bframes = 2;
            if (enc->cfg.bframes < 0) enc->cfg.bframes = 0;
            if (!ffmpeg_init) {
                avcodec_init();
                avcodec_register_all();
                ffmpeg_init = 1;
            }
            enc->enc.ffmpeg.codec = avcodec_find_encoder((enc->cfg.encoder_type == ENCTYPE_FFMPEG_MPEG2) ? CODEC_ID_MPEG2VIDEO : CODEC_ID_MPEG4);
            if (!enc->enc.ffmpeg.codec) {
                encode_free(enc);
                return ENC_INIT_ERR_LIB;
            }
            enc->enc.ffmpeg.ctx = avcodec_alloc_context();
            enc->enc.ffmpeg.frame = avcodec_alloc_frame();
            if (enc->cfg.flags & ENC_BITRATE) {
                if (IS_VERBOSE) printf(VPREFIX "using constant bitrate (CBR) mode, target bitrate = %d kbps\n", enc->cfg.quality);
                enc->enc.ffmpeg.ctx->bit_rate = enc->cfg.quality * 1000;
                enc->enc.ffmpeg.ctx->flags &= ~CODEC_FLAG_QSCALE;
            } else {
                if (IS_VERBOSE) printf(VPREFIX "using fixed quantizer (CQP) mode, target quantizer = %d\n", enc->cfg.quality);
                enc->enc.ffmpeg.ctx->flags |= CODEC_FLAG_QSCALE;
                enc->enc.ffmpeg.ctx->global_quality = (int) (FF_QP2LAMBDA * enc->cfg.quality + 0.5);
            }
            enc->enc.ffmpeg.ctx->me_method = 5;
            enc->enc.ffmpeg.ctx->time_base.num = enc->cfg.fps_den;
            enc->enc.ffmpeg.ctx->time_base.den = enc->cfg.fps_num;
            enc->enc.ffmpeg.ctx->width = enc->cfg.width;
            enc->enc.ffmpeg.ctx->height = enc->cfg.height;
            enc->enc.ffmpeg.ctx->gop_size = enc->cfg.keyint;
            enc->enc.ffmpeg.ctx->pix_fmt = PIX_FMT_YUV420P;
            enc->enc.ffmpeg.ctx->max_b_frames = enc->cfg.bframes;
            enc->enc.ffmpeg.ctx->lumi_masking = (enc->cfg.flags & ENC_PSYCHO) ? 0.1f : 0.0f;
            enc->enc.ffmpeg.ctx->dark_masking = (enc->cfg.flags & ENC_PSYCHO) ? 0.1f : 0.0f;
            enc->enc.ffmpeg.ctx->p_masking = (enc->cfg.flags & ENC_PSYCHO) ? 0.125f : 0.0f;
            enc->enc.ffmpeg.ctx->sample_aspect_ratio.num = sar_num;
            enc->enc.ffmpeg.ctx->sample_aspect_ratio.den = sar_den;
            enc->enc.ffmpeg.ctx->mb_decision = (enc->cfg.flags & ENC_RD) ? FF_MB_DECISION_RD : FF_MB_DECISION_SIMPLE;
            enc->enc.ffmpeg.ctx->trellis = (enc->cfg.flags & ENC_TRELLIS) ? 1 : 0;
#if (LIBAVCODEC_VERSION_MAJOR > 52) || ((LIBAVCODEC_VERSION_MAJOR == 52) && (LIBAVCODEC_VERSION_MINOR >= 72))
            enc->enc.ffmpeg.ctx->color_primaries = (enc->cfg.flags & ENC_BT709) ? AVCOL_PRI_BT709 : AVCOL_PRI_SMPTE170M;
            enc->enc.ffmpeg.ctx->color_trc = (enc->cfg.flags & ENC_BT709) ? AVCOL_TRC_BT709 : AVCOL_TRC_GAMMA22;
            enc->enc.ffmpeg.ctx->colorspace = (enc->cfg.flags & ENC_BT709) ? AVCOL_SPC_BT709 : AVCOL_SPC_SMPTE170M;
            enc->enc.ffmpeg.ctx->color_range = AVCOL_RANGE_MPEG;
            enc->enc.ffmpeg.ctx->chroma_sample_location = AVCHROMA_LOC_LEFT;
#else
#warning Your libavcodec version is too old, color space information is not supported!
#endif
            if (enc->cfg.flags & ENC_INTERLACE)
                enc->enc.ffmpeg.ctx->flags |= CODEC_FLAG_INTERLACED_DCT | CODEC_FLAG_INTERLACED_ME;
            if (enc->cfg.flags & ENC_RD)
                enc->enc.ffmpeg.ctx->flags |= CODEC_FLAG_CBP_RD | CODEC_FLAG_QP_RD;
            if (enc->cfg.encoder_type == ENCTYPE_FFMPEG_MPEG4) {
                if (enc->cfg.flags & ENC_4MV)
                    enc->enc.ffmpeg.ctx->flags |= CODEC_FLAG_4MV;
                if (enc->cfg.flags & ENC_QPEL)
                    enc->enc.ffmpeg.ctx->flags |= CODEC_FLAG_QPEL;
                if (enc->cfg.flags & ENC_GMC)
                    enc->enc.ffmpeg.ctx->flags |= CODEC_FLAG_GMC;
            }
            if (enc->cfg.flags & ENC_MV0)
                enc->enc.ffmpeg.ctx->flags |= CODEC_FLAG_MV0;
            if (IS_VERBOSE) {
                printf(VPREFIX "keyframe interval: %d frames, %d B frames\n",
                       enc->cfg.keyint, enc->cfg.bframes);
                printf(VPREFIX "features used: " LIBAVCODEC_IDENT);
                if (enc->cfg.encoder_type == ENCTYPE_FFMPEG_MPEG4) {
                    if (enc->cfg.flags & ENC_4MV) printf(", 4MV");
                    if (enc->cfg.flags & ENC_QPEL) printf(", QPel");
                    if (enc->cfg.flags & ENC_GMC) printf(", GMC");
                }
                if (enc->cfg.flags & ENC_RD) printf(", %sRDO", (enc->cfg.flags & ENC_PSYCHO) ? "psy-" : "");
                if (enc->cfg.flags & ENC_TRELLIS) printf(", trellis");
                if (enc->cfg.flags & ENC_HQ_ME) printf(", HQ ME");
                if (enc->cfg.flags & ENC_CHROMA_ME) printf(", chroma ME");
                if (enc->cfg.flags & ENC_MV0) printf(", MV-0");
                printf("\n");
            }
            if (avcodec_open(enc->enc.ffmpeg.ctx, enc->enc.ffmpeg.codec) < 0) {
                encode_free(enc);
                return ENC_INIT_ERR_LIB;
            }
            enc->enc.ffmpeg.bufsize = enc->cfg.width * enc->cfg.height / 2;
            enc->enc.ffmpeg.outbuf = malloc(enc->enc.ffmpeg.bufsize);
            if (!enc->enc.ffmpeg.outbuf) {
                encode_free(enc);
                return ENC_INIT_ERR_MEM;
            }
            break; }
#endif
        default:
            encode_free(enc);
            return ENC_INIT_NO_ENC;
    }

    /* done, return handle */
    *encoder = (encoder_handle_t) enc;
    return ENC_INIT_OK;
}

const encoder_config_t* encode_get_config(encoder_handle_t encoder) {
    return encoder ? (&((encoder_t*) encoder)->cfg) : NULL;
}

void encode_frame(encoder_handle_t encoder, encoder_frame_t* frame) {
    encoder_t *enc = (encoder_t*) encoder;
    if (!enc || !frame) return;
    switch (enc->cfg.encoder_type) {
        case ENCTYPE_YUV4MPEG:
            if (!(enc->cfg.flags & ENC_INTERLACE))
                fprintf(enc->out, "FRAME\n");
            else if (frame->flags & ENC_FRAME_PROGRESSIVE)
                fprintf(enc->out, "FRAME I1pp\n");
            else fprintf(enc->out, "FRAME I%cii\n",
                 (frame->flags & ENC_FRAME_BFF) ?
                 ((frame->flags & ENC_FRAME_RFF) ? 'B' : 'b') :
                 ((frame->flags & ENC_FRAME_RFF) ? 'T' : 't'));
            /* fall-through */
        case ENCTYPE_RAW_YUV: {
            int y;
            const unsigned char *ptr = frame->y.pixels;
            for (y = enc->cfg.height;  y;  --y, ptr += frame->y.stride)
                fwrite(ptr, 1, enc->cfg.width, enc->out);
            ptr = frame->cb.pixels;
            for (y = enc->cfg.height >> 1;  y;  --y, ptr += frame->cb.stride)
                fwrite(ptr, 1, enc->cfg.width >> 1, enc->out);
            ptr = frame->cr.pixels;
            for (y = enc->cfg.height >> 1;  y;  --y, ptr += frame->cr.stride)
                fwrite(ptr, 1, enc->cfg.width >> 1, enc->out);
            break; }
#ifdef HAVE_X264
        case ENCTYPE_X264: {
            int nsize, dummy;
            x264_nal_t *nal;
            x264_picture_t pin, pout;
            memset(&pin, 0, sizeof(pin));
            pin.i_type = (frame->flags & ENC_FRAME_KEYFRAME) ? X264_TYPE_IDR : X264_TYPE_AUTO;
            pin.i_pts = enc->frameno;
            pin.img.i_csp = X264_CSP_I420;
            pin.img.i_plane = 3;
            pin.img.i_stride[0] = frame->y.stride;
            pin.img.i_stride[1] = frame->cb.stride;
            pin.img.i_stride[2] = frame->cr.stride;
            pin.img.plane[0] = frame->y.pixels;
            pin.img.plane[1] = frame->cb.pixels;
            pin.img.plane[2] = frame->cr.pixels;
            nsize = x264_encoder_encode(enc->enc.x264.enc, &nal, &dummy, &pin, &pout);
            if (nsize < 0) return;
            if (nsize) fwrite(nal[0].p_payload, 1, nsize, enc->out);
            break; }
#endif
#ifdef HAVE_FFMPEG
        case ENCTYPE_FFMPEG_MPEG2:
        case ENCTYPE_FFMPEG_MPEG4: {
            int esize;
            enc->enc.ffmpeg.frame->data[0] = frame->y.pixels;
            enc->enc.ffmpeg.frame->data[1] = frame->cb.pixels;
            enc->enc.ffmpeg.frame->data[2] = frame->cr.pixels;
            enc->enc.ffmpeg.frame->linesize[0] = frame->y.stride;
            enc->enc.ffmpeg.frame->linesize[1] = frame->cb.stride;
            enc->enc.ffmpeg.frame->linesize[2] = frame->cr.stride;
            enc->enc.ffmpeg.frame->key_frame = (frame->flags & ENC_FRAME_KEYFRAME) ? 1 : 0;
            enc->enc.ffmpeg.frame->quality = enc->enc.ffmpeg.ctx->global_quality;
            enc->enc.ffmpeg.frame->interlaced_frame = ((enc->cfg.flags & ENC_INTERLACE) && !(frame->flags & ENC_FRAME_PROGRESSIVE)) ? 1 : 0;
            enc->enc.ffmpeg.frame->top_field_first = (frame->flags & ENC_FRAME_BFF) ? 0 : 1;
            esize = avcodec_encode_video(
                enc->enc.ffmpeg.ctx,
                enc->enc.ffmpeg.outbuf,
                enc->enc.ffmpeg.bufsize,
                enc->enc.ffmpeg.frame);
            if (esize < 0) return;
            if (esize) fwrite(enc->enc.ffmpeg.outbuf, 1, esize, enc->out);
            break; }
#endif
        default: break;
    }
    ++enc->frameno;
}

int encode_get_bitrate(encoder_handle_t encoder) {
#ifdef MSC_VER
    __int64 tmp;
#else
    long long tmp;
#endif
    encoder_t *enc = (encoder_t*) encoder;
    if (!enc || !enc->out) return -1;
    if (!enc->frameno) return 0;
    tmp = ftell(enc->out);
    tmp *= enc->cfg.fps_num;
    tmp /= enc->cfg.fps_den;
    tmp /= enc->frameno;
    return (int) (tmp / 125);
}

int encode_done(encoder_handle_t encoder) {
    encoder_t *enc = (encoder_t*) encoder;
    int br;
    if (!enc) return -1;
    switch (enc->cfg.encoder_type) {
#ifdef HAVE_X264
        case ENCTYPE_X264: {
            int nsize, dummy;
            x264_nal_t *nal;
            x264_picture_t pout;
            while (x264_encoder_delayed_frames(enc->enc.x264.enc)) {
                nsize = x264_encoder_encode(enc->enc.x264.enc, &nal, &dummy, NULL, &pout);
                if (nsize < 0) break;
                if (nsize) fwrite(nal[0].p_payload, 1, nsize, enc->out);
            }
            break; }
#endif
#ifdef HAVE_FFMPEG
        case ENCTYPE_FFMPEG_MPEG2:
        case ENCTYPE_FFMPEG_MPEG4: {
            int esize;
            const static unsigned char seq_end[4] = { 0x00, 0x00, 0x01, 0xB7 };
            for (esize = 1;  esize > 0;  ) {
                esize = avcodec_encode_video(
                    enc->enc.ffmpeg.ctx,
                    enc->enc.ffmpeg.outbuf,
                    enc->enc.ffmpeg.bufsize,
                    NULL);
                if (esize) fwrite(enc->enc.ffmpeg.outbuf, 1, esize, enc->out);
            }
            if (enc->cfg.encoder_type == ENCTYPE_FFMPEG_MPEG2)
                fwrite(seq_end, 1, 4, enc->out);
            break; }
#endif
        default: break;
    }
    br = encode_get_bitrate(encoder);
    encode_free((encoder_t*) encoder);
    return br;
}
