/* encavsub.c

   Copyright (c) 2003-2026 HandBrake Team
   This file is part of the HandBrake source code
   Homepage: <http://handbrake.fr/>.
   It may be used under the terms of the GNU General Public License v2.
   For full terms see the file COPYING file or visit http://www.gnu.org/licenses/gpl-2.0.html
 */

#include "handbrake/handbrake.h"
#include "handbrake/hbffmpeg.h"
#include "handbrake/encavsub.h"
#include "handbrake/extradata.h"

#define ENC_BUF_DEFAULT_SZ (4 * 1024)
#define ENC_BUF_MAX_SZ     (1024 * 1024)

struct hb_encavsub_context_s
{
    AVCodecContext *context;
    hb_job_t       *job;
    hb_subtitle_t  *subtitle;

    // List of subtitle packets to be output by this encoder.
    hb_buffer_list_t list;

    // avcodec_encode_subtitle encodes to a raw buffer
    // that we must provide.  *Most* libav subtitle encoders
    // will return an error (of various sorts: -1, EINVAL,
    // AVERROR_BUFFER_TOO_SMALL) when the buffer is too small.
    // Some just overrun the buffer :( DVB )
    //
    // TODO: sanitize libav return codes and fix DVB
    uint8_t        *buffer;
    int             buffer_size;
};

struct hb_work_private_s
{
    hb_encavsub_context_t *ctx;
};

static int realloc_enc_buffer(hb_encavsub_context_t * ctx)
{
    if (ctx->buffer_size == 0)
    {
        ctx->buffer_size = ENC_BUF_DEFAULT_SZ;
    }
    else if (ctx->buffer_size * 2 < ENC_BUF_MAX_SZ)
    {
        ctx->buffer_size *= 2;
    }
    else
    {
        return 0;
    }
    ctx->buffer = av_realloc(ctx->buffer, ctx->buffer_size);
    if (ctx->buffer == NULL)
    {
        return 0;
    }
    return 1;
}

/***********************************************************************
 * encavsubInit
 ***********************************************************************
 * Init function for libav subtitle encoding that may be wrapped
 * by HB subtitle encoder
 **********************************************************************/
hb_encavsub_context_t * encavsubInit(hb_work_object_t *w, hb_job_t *job)
{
    const AVCodec         *codec;
    AVCodecContext        *context;
    hb_encavsub_context_t *ctx = calloc(1, sizeof(hb_encavsub_context_t));

    if (ctx == NULL)
    {
        hb_error("encavsubInit: calloc ctx failed");
        return NULL;
    }
    ctx->job      = job;
    ctx->subtitle = w->subtitle;

    codec   = avcodec_find_encoder(w->codec_param);
    if (codec == NULL)
    {
        hb_error("encavsubInit: avcodec_find_encoder failed");
        goto fail;
    }
    context = avcodec_alloc_context3(codec);
    if (context == NULL)
    {
        hb_error("encavsubInit: avcodec_alloc_context3 failed");
        goto fail;
    }
    ctx->context   = context;
    context->codec = codec;

    // TEXT subtitle encoders require SSA header
    context->subtitle_header = av_malloc(ctx->subtitle->extradata->size + 1);
    if (context->subtitle_header == NULL)
    {
        hb_error("encavsubInit: av_malloc subtitle_header failed");
        goto fail;
    }
    memcpy(context->subtitle_header, ctx->subtitle->extradata->bytes,
                                     ctx->subtitle->extradata->size);
    context->subtitle_header[ctx->subtitle->extradata->size] = '\0';
    context->subtitle_header_size  = ctx->subtitle->extradata->size + 1;
    context->time_base             = AV_TIME_BASE_Q;

    // Set encoder opts...
    AVDictionary *av_opts = NULL;
    if (w->codec_param == AV_CODEC_ID_MOV_TEXT)
    {
        char *height = hb_strdup_printf("%d", job->height);
        av_dict_set( &av_opts, "height", height, 0 );
        free(height);
    }
    if (hb_avcodec_open(ctx->context, codec, &av_opts, 0))
    {
        av_dict_free( &av_opts );
        hb_error("encavsubInit: avcodec_open failed");
        goto fail;
    }
    av_dict_free( &av_opts );

    // avcodec encoder may create subtitle extradata
    hb_data_close(&ctx->subtitle->extradata);

    if (context->extradata != NULL && context->extradata_size > 0)
    {
        int ret = hb_set_extradata(&ctx->subtitle->extradata,
                                   context->extradata,
                                   context->extradata_size);
        if (ret != 0)
        {
            hb_error("encavsubInit: malloc subtitle extradata failed");
            goto fail;
        }
    }

    if (!realloc_enc_buffer(ctx))
    {
        hb_error("encavsubInit: realloc buffer failed");
        goto fail;
    }
    hb_buffer_list_clear(&ctx->list);

    return ctx;


fail:
    if (ctx != NULL)
    {
        if (ctx->context != NULL)
        {
            avcodec_free_context(&ctx->context);
        }
    }
    free(ctx);

    return NULL;
}

/***********************************************************************
 * encavsubClose
 ***********************************************************************
 * Close function for libav subtitle encoding that may be wrapped
 * by HB subtitle encoder
 **********************************************************************/
void encavsubClose(hb_encavsub_context_t *ctx)
{
    if (ctx == NULL)
    {
        return;
    }
    avcodec_free_context(&ctx->context);
    av_free(ctx->buffer);
    free(ctx);
}

/***********************************************************************
 * encavsubWork
 ***********************************************************************
 * Work function for libav subtitle encoding that may be wrapped
 * by HB subtitle encoder
 **********************************************************************/
int encavsubWork(hb_encavsub_context_t *ctx,
                 hb_buffer_t **buf_in,
                 hb_buffer_t **buf_out)
{
    hb_buffer_t *in = *buf_in;
    AVSubtitle   subtitle;
    int          num_rects = 1;
    int64_t      duration;

    if (in->s.flags & HB_BUF_FLAG_EOF)
    {
        /* EOF on input stream - send it downstream & say that we're done */
        *buf_in = NULL;

        *buf_out = in;
        return HB_WORK_DONE;
    }

    // Create an AVSubtitle from the input buffer
    memset(&subtitle, 0, sizeof(subtitle));
    subtitle.rects = av_mallocz(num_rects * sizeof(subtitle.rects));
    if (subtitle.rects == NULL)
    {
        *buf_out = hb_buffer_eof_init();
        hb_error("encavsubInit: av_mallocz_array failed");
        return HB_WORK_DONE;
    }

    for (int ii = 0; ii < num_rects; ii++)
    {
        subtitle.rects[ii]  = av_mallocz(sizeof(*subtitle.rects[0]));
        if (!subtitle.rects[ii]) {
            avsubtitle_free(&subtitle);
            *buf_out = hb_buffer_eof_init();
            hb_error("encavsubInit: av_mallocz failed");
            return HB_WORK_DONE;
        }
        subtitle.num_rects++;
    }

    if (ctx->subtitle->format == TEXTSUB)
    {
        subtitle.rects[0]->type = SUBTITLE_ASS;
        subtitle.rects[0]->ass  = av_strdup((char *)in->data);
        if (subtitle.rects[0]->ass == NULL)
        {
            avsubtitle_free(&subtitle);
            *buf_out = hb_buffer_eof_init();
            hb_error("encavsubInit: av_strdup failed");
            return HB_WORK_DONE;
        }
    }
    else
    {
        // TODO: bitmap subtitles
        // Must simplify format of hb_buffer_t bitmap representation
        for (int ii = 0; ii < num_rects; ii++)
        {
            subtitle.rects[0]->type = SUBTITLE_BITMAP;
        }
    }

    subtitle.pts = av_rescale(in->s.start, AV_TIME_BASE, 90000);
    if (in->s.stop != AV_NOPTS_VALUE)
    {
        duration = in->s.stop - in->s.start;
        subtitle.end_display_time = av_rescale(duration, 1000, 90000);
    }

    int size;
    do
    {
        size = avcodec_encode_subtitle(ctx->context,
                                       ctx->buffer, ctx->buffer_size, &subtitle);
        if (size < 0)
        {
            if (!realloc_enc_buffer(ctx))
            {
                avsubtitle_free(&subtitle);
                *buf_out = hb_buffer_eof_init();
                hb_error("encavsubInit: realloc failed");
                return HB_WORK_DONE;
            }
        }
        else if (size > 0)
        {
            // Next iteration, flush additional subtitle packets that
            // the encoder might generate
            subtitle.num_rects = 0;

            hb_buffer_t *out = hb_buffer_init(size);
            memcpy(out->data, ctx->buffer, size);
            out->s.start = av_rescale(subtitle.pts, 90000, AV_TIME_BASE);
            out->s.duration = (int64_t)AV_NOPTS_VALUE;
            if (subtitle.end_display_time > 0)
            {
                duration = av_rescale(subtitle.end_display_time, 90000, 1000);
                out->s.stop = out->s.start + duration;
                out->s.duration = duration;
            }
            hb_buffer_list_append(&ctx->list, out);
        }
    } while (size < 0);

    subtitle.num_rects = num_rects;
    avsubtitle_free(&subtitle);

    *buf_out = hb_buffer_list_clear(&ctx->list);
    return HB_WORK_OK;
}

/***********************************************************************
 * Init
 ***********************************************************************
 * Initialize hb_work_private_t data
 **********************************************************************/
static int Init(hb_work_object_t *w, hb_job_t *job)
{
    hb_work_private_t *pv;

    pv = calloc(1, sizeof(hb_work_private_t));
    if (pv == NULL)
    {
        hb_error("encsubInit: calloc private data failed");
        return 1;
    }

    pv->ctx = encavsubInit(w, job);
    if (pv->ctx == NULL)
    {
        free(pv);
        return 1;
    }
    w->private_data = pv;

    return 0;
}

/***********************************************************************
 * Close
 ***********************************************************************
 * Free any allocation in hb_work_private_t
 **********************************************************************/
static void Close(hb_work_object_t *w)
{
    hb_work_private_t * pv = w->private_data;
    if (pv == NULL)
    {
        return;
    }
    encavsubClose(pv->ctx);
    free(pv);
    w->private_data = NULL;
}

/***********************************************************************
 * Work
 ***********************************************************************
 * Take an input buffer, send an output buffer
 **********************************************************************/
static int Work(hb_work_object_t *w, hb_buffer_t **buf_in,
                hb_buffer_t **buf_out)
{
    hb_work_private_t *pv = w->private_data;

    return encavsubWork(pv->ctx, buf_in, buf_out);
}

/***********************************************************************
 * Info
 ***********************************************************************
 * Retrieve current info about context initialized during Init
 **********************************************************************/
static int Info(hb_work_object_t *w, hb_work_info_t *info)
{
    memset(info, 0, sizeof(*info));

    // Indicate no info is returned
    return 0;
}

/***********************************************************************
 * BSInfo
 ***********************************************************************
 * Retrieve info, does not require Init(), but uses current context
 * if Init has already been called.
 * buf contains stream data to extract info from.
 **********************************************************************/
static int BSInfo(hb_work_object_t *w, const hb_buffer_t *buf,
                  hb_work_info_t *info)
{
    memset(info, 0, sizeof(*info));

    // Indicate no info is returned
    return 0;
}

/***********************************************************************
 * Flush
 ***********************************************************************
 * Reset context without closing, kind of poorly named :(
 **********************************************************************/
static void Flush(hb_work_object_t *w)
{
}

hb_work_object_t hb_encavsub =
{
    .id     = WORK_ENCAVSUB,
    .name   = "Subtitle encoder (libavcodec)",
    .init   = Init,
    .work   = Work,
    .close  = Close,
    .info   = Info,
    .bsinfo = BSInfo,
    .flush  = Flush,
};
