extern "C"
{
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavfilter/avfilter.h>
#include <libavutil/opt.h>
#include <libavfilter/buffersink.h>
#include <libavfilter/buffersrc.h>
}

#include <iostream>
#include <string>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include "define.h"
#include <algorithm>

char *getCmdOption(char **begin, char **end, const std::string &option)
{
    char **itr = std::find(begin, end, option);
    if (itr != end && ++itr != end)
    {
        return *itr;
    }
    return 0;
}

static int init_filter_graph(AVFilterGraph *graph, AVFilterContext **src,
                             AVFilterContext **sink, AVCodecContext *encctx, AVCodecContext *decctx)
{
    AVFilterGraph *filter_graph;
    AVFilterContext *abuffer_ctx;
    const AVFilter *abuffer;
    AVFilterContext *abuffersink_ctx;
    const AVFilter *abuffersink;
    const AVFilter *aformat;
    AVFilterContext *aformat_ctx;
    char strbuf[1024];

    AVDictionary *options_dict = NULL;
    char ch_layout[64];

    int err;

    /* Create a new filtergraph, which will contain all the filters. */
    filter_graph = avfilter_graph_alloc();
    if (!filter_graph)
    {
        fprintf(stderr, "Unable to create filter graph.\n");
        return AVERROR(ENOMEM);
    }

    /* Create the abuffer filter;
     * it will be used for feeding the data into the graph. */
    abuffer = avfilter_get_by_name("abuffer");
    if (!abuffer)
    {
        fprintf(stderr, "Could not find the abuffer filter.\n");
        return AVERROR_FILTER_NOT_FOUND;
    }

    abuffer_ctx = avfilter_graph_alloc_filter(filter_graph, abuffer, "src");
    if (!abuffer_ctx)
    {
        fprintf(stderr, "Could not allocate the abuffer instance.\n");
        return AVERROR(ENOMEM);
    }

    /* Set the filter options through the AVOptions API. */
    av_channel_layout_describe(&decctx->ch_layout, ch_layout, sizeof(ch_layout));
    av_opt_set(abuffer_ctx, "channel_layout", ch_layout, AV_OPT_SEARCH_CHILDREN);
    av_opt_set(abuffer_ctx, "sample_fmt", av_get_sample_fmt_name(decctx->sample_fmt), AV_OPT_SEARCH_CHILDREN);
    av_opt_set_q(abuffer_ctx, "time_base", (AVRational){1, decctx->sample_rate}, AV_OPT_SEARCH_CHILDREN);
    av_opt_set_int(abuffer_ctx, "sample_rate", decctx->sample_rate, AV_OPT_SEARCH_CHILDREN);

    /* Now initialize the filter; we pass NULL options, since we have already
     * set all the options above. */
    err = avfilter_init_str(abuffer_ctx, NULL);
    if (err < 0)
    {
        fprintf(stderr, "Could not initialize the abuffer filter.\n");
        return err;
    }

    // create aformat filter
    aformat = avfilter_get_by_name("aformat");
    snprintf(strbuf, sizeof(strbuf),
             "sample_fmts=%s:sample_rates=%d:channel_layouts=0x%" PRIx64,
             av_get_sample_fmt_name(encctx->sample_fmt), 44100,
             (uint64_t)AV_CH_LAYOUT_STEREO);
    fprintf(stderr, "aformat: %s\n", strbuf);
    aformat_ctx = avfilter_graph_alloc_filter(filter_graph, aformat, "aformat");
    if (err < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "unable to create aformat filter\n");
        return err;
    }
    err = avfilter_init_str(aformat_ctx, strbuf);
    if (err < 0)
    {
        fprintf(stderr, "Could not initialize the aformat instance.\n");
        return err;
    }

    /* Finally create the abuffersink filter;
     * it will be used to get the filtered data out of the graph. */
    abuffersink = avfilter_get_by_name("abuffersink");
    if (!abuffersink)
    {
        fprintf(stderr, "Could not find the abuffersink filter.\n");
        return AVERROR_FILTER_NOT_FOUND;
    }

    abuffersink_ctx = avfilter_graph_alloc_filter(filter_graph, abuffersink, "sink");
    if (!abuffersink_ctx)
    {
        fprintf(stderr, "Could not allocate the abuffersink instance.\n");
        return AVERROR(ENOMEM);
    }

    /* This filter takes no options. */
    err = avfilter_init_str(abuffersink_ctx, NULL);
    if (err < 0)
    {
        fprintf(stderr, "Could not initialize the abuffersink instance.\n");
        return err;
    }

    /* Connect the filters;
     * in this simple case the filters just form a linear chain. */
    err = avfilter_link(abuffer_ctx, 0, aformat_ctx, 0);
    if (err >= 0)
        err = avfilter_link(aformat_ctx, 0, abuffersink_ctx, 0);
    if (err < 0)
    {
        fprintf(stderr, "Error connecting filters\n");
        return err;
    }

    /* Configure the graph. */
    err = avfilter_graph_config(filter_graph, NULL);
    if (err < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Error configuring the filter graph\n");
        return err;
    }
    av_buffersink_set_frame_size(abuffersink_ctx, encctx->frame_size);
    graph = filter_graph;
    *src = abuffer_ctx;
    *sink = abuffersink_ctx;

    return 0;
}

static void logging(const char *fmt, ...)
{
    va_list args;
    fprintf(stderr, "LOG: ");
    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    va_end(args);
    fprintf(stderr, "\n");
}

int encode(AVFormatContext *avfc, AVStream *dec_str, AVStream *enc_str, AVCodecContext *avcc, int index, AVFrame *input_frame)
{
    AVPacket *output_packet = av_packet_alloc();
    if (!output_packet)
    {
        logging("could not allocate memory for output packet");
        return -1;
    }

    int response = avcodec_send_frame(avcc, input_frame);
    while (response >= 0)
    {
        response = avcodec_receive_packet(avcc, output_packet);
        if (response == AVERROR(EAGAIN) || response == AVERROR_EOF)
        {
            break;
        }
        else if (response < 0)
        {
            logging("Error while receiving packet from encoder: %s", av_err2str(response));
            return -1;
        }
        output_packet->stream_index = index;

        av_packet_rescale_ts(output_packet, dec_str->time_base, enc_str->time_base);
        response = av_interleaved_write_frame(avfc, output_packet);
        if (response != 0)
        {
            logging("Error %d while receiving packet from decoder: %s", response, av_err2str(response));
            return -1;
        }
        av_packet_unref(output_packet);
    }
    av_packet_free(&output_packet);
    return 0;
}

int main(int argc, char **argv)
{

    char *input = getCmdOption(argv, argv + argc, "-i");

    if (!input)
    {
        std::cout << "Alarm! No input URL provided. Applications will shutdown." << std::endl;
        return 0;
    }
    av_log_set_level(AV_LOG_DEBUG);
    AVFormatContext *ctx = nullptr;
    AVStream *istream = nullptr;
    AVCodecContext *encctx = nullptr;
    AVStream *ostream;
    AVFilterGraph *graph;
    AVFilterContext *src, *sink;
    std::string path(input);
    int streamIdx;
    ctx = avformat_alloc_context();

    if (avformat_open_input(&ctx, path.c_str(), 0, 0) < 0)
        std::cout << "error1" << std::endl;

    if (avformat_find_stream_info(ctx, 0) < 0)
        std::cout << "error2" << std::endl;

    int status;

    AVFormatContext *ofmt_ctx = nullptr;
    const AVOutputFormat *ofmt = av_guess_format("mp3", NULL, NULL);

    status = avformat_alloc_output_context2(&ofmt_ctx, ofmt, "mp3", NULL);

    if (status < 0)
    {
        std::cerr << "could not allocate output format" << std::endl;
        return 0;
    }
    ctx->flags = AVFMT_FLAG_NOBUFFER | AVFMT_FLAG_FLUSH_PACKETS | AVFMT_FLAG_BITEXACT;
    ofmt_ctx->flags = AVFMT_FLAG_NOBUFFER | AVFMT_FLAG_FLUSH_PACKETS | AVFMT_FLAG_BITEXACT;

    streamIdx = av_find_best_stream(ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
    istream = ctx->streams[streamIdx];
    av_channel_layout_default(&istream->codecpar->ch_layout, 2);
    const AVCodec *dec = avcodec_find_decoder(istream->codecpar->codec_id);
    AVCodecContext *decctx;
    decctx = avcodec_alloc_context3(dec);
    if (!decctx)
    {
        std::cerr << "failed to alloc memory for codec context" << std::endl;
        return -1;
    }
    status = avcodec_parameters_to_context(decctx, istream->codecpar);
    if (status < 0)
    {
        avcodec_free_context(&decctx);
        std::cerr << "could not set decoder params" << std::endl;
        return 0;
    }

    avcodec_open2(decctx, dec, NULL);
    if (status < 0)
    {
        avcodec_free_context(&decctx);
        std::cerr << "could not open a decoder" << std::endl;
        return 0;
    }
    const AVCodec *codec = avcodec_find_encoder_by_name("libmp3lame");
    int audio_stream_index = 0;
    for (unsigned i = 0; i < ctx->nb_streams; i++)
    {
        if (ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
        {
            audio_stream_index = i;
            std::cout << "looking for appropriate encoder" << std::endl;
            encctx = avcodec_alloc_context3(codec);
            if (codec)
            {
                encctx->codec_type = AVMEDIA_TYPE_AUDIO;
                encctx->ch_layout = istream->codecpar->ch_layout;
                encctx->time_base.den = 44100;
                encctx->time_base.num = 1;
                encctx->bit_rate = 96000;
                encctx->sample_rate = 44100;
                encctx->sample_fmt = codec->sample_fmts[0];
                if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
                    encctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
                encctx->flags |= AV_CODEC_FLAG_BITEXACT;
                ostream = avformat_new_stream(ofmt_ctx, codec);
                std::cout << "created and configured encoder context" << std::endl;
                if (avcodec_open2(encctx, codec, NULL) < 0)
                {
                    logging("failed to open codec through avcodec_open2");
                    return -1;
                }

                if (avcodec_parameters_from_context(ostream->codecpar, encctx) < 0)
                {
                    logging("failed to copy parameters to output stream");
                    return -1;
                }
            }
            break;
        }
    }

    std::string name = "/transcode/hey.mp3";

    if (!(ofmt_ctx->oformat->flags & AVFMT_NOFILE))
    {
        std::cout << "output file opening..." << std::endl;
        avio_open(&ofmt_ctx->pb, name.c_str(), AVIO_FLAG_WRITE);
    }
    ofmt_ctx->flags |= AVFMT_FLAG_BITEXACT;
    status = avformat_write_header(ofmt_ctx, NULL);

    /* Set up the filtergraph. */
    status = init_filter_graph(graph, &src, &sink, encctx, decctx);
    if (status < 0)
    {
        fprintf(stderr, "Unable to init filter graph:");
        return 0;
    }
    std::cout << "filters created successfully" << std::endl;

    AVFrame *input_frame = av_frame_alloc();
    AVFrame *out_frame = av_frame_alloc();
    AVPacket *input_packet = av_packet_alloc();
    int64_t in_pts;
    int64_t out_pts;
    av_dict_set(&ofmt_ctx->metadata, "Title", input, 0);
    av_dump_format(ofmt_ctx, 0, name.c_str(), 1);
read_input:
    while (av_read_frame(ctx, input_packet) == 0)
    {
        int response = avcodec_send_packet(decctx, input_packet);
        while (response >= 0)
        {
            response = avcodec_receive_frame(decctx, input_frame);
            if (response == AVERROR(EAGAIN) || response == AVERROR_EOF)
            {
                av_frame_unref(input_frame);
                av_packet_unref(input_packet);
                goto read_input;
            }
            else if (response < 0)
            {
                return response;
            }
            if (response >= 0)
            {
                input_frame->pts = in_pts;
                in_pts += input_frame->nb_samples;
                response = av_buffersrc_add_frame(src, input_frame);
                if (response < 0)
                {
                    fprintf(stderr, "Cannot send frame to afilter:");
                    av_frame_unref(input_frame);
                    av_packet_unref(input_packet);
                    goto read_input;
                }
                while ((response = av_buffersink_get_frame(sink, out_frame)) >= 0)
                {
                    out_frame->pts = out_pts;
                    out_pts += out_frame->nb_samples;
                    encode(ofmt_ctx, istream, ostream, encctx, audio_stream_index, out_frame);
                    av_frame_unref(out_frame);
                }
                av_frame_unref(input_frame);
            }
        }
        av_packet_unref(input_packet);
    }

    av_packet_free(&input_packet);
    av_frame_free(&input_frame);
    av_frame_free(&out_frame);

    av_write_trailer(ofmt_ctx);

    avformat_close_input(&ctx);
    avformat_free_context(ofmt_ctx);
    avformat_free_context(ctx);
    avcodec_free_context(&decctx);
    avcodec_free_context(&encctx);
    avfilter_graph_free(&graph);

    if (status == 0)
        std::cout << "transcoding finished successfully" << std::endl;

    return 0;
}
