/*
 * ARIB STD-B24 caption decoder using the libaribcaption library
 * Copyright (c) 2022 TADANO Tokumei
 *
 * This file is part of FFmpeg.
 *
 * FFmpeg is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * FFmpeg 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with FFmpeg; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include "avcodec.h"
#include "codec_internal.h"
#include "internal.h"
#include "libavcodec/ass.h"
#include "libavutil/avstring.h"
#include "libavutil/avutil.h"
#include "libavutil/thread.h"
#include "libavutil/log.h"
#include "libavutil/opt.h"

#include <aribcaption/aribcaption.h>

#if !defined(DEFAULT_FONT_ASS)
# define DEFAULT_FONT_ASS "sans-serif"
#endif

#define ARIBC_BPRINT_SIZE_INIT         64
#define ARIBC_BPRINT_SIZE_MAX          (8 * 1024)
#define ARIBC_ALPHA_MAX_NUM            4
#define ARIBC_ALPHA_DEFAULT_FRONT      0xFF
#define ARIBC_ALPHA_DEFAULT_BACK       0x80

#define ARIBCC_COLOR_RGB(c)            ((c) & 0xFFFFFF)
#define ARIBCC_COLOR_DIFF_RGB(c1,c2)   (((c1) ^ (c2)) & 0x00FFFFFF)
#define ARIBCC_COLOR_DIFF_A(c1,c2)     (((c1) ^ (c2)) & 0xFF000000)

#define CLUT_RGBA(r,g,b,a) (((unsigned)(a) << 24) | ((r) << 16) | ((g) << 8) | (b))
#define CLUT_A(c)          (((c) >> 24) & 0xFF)
#define CLUT_R(c)          (((c) >> 16) & 0xFF)
#define CLUT_G(c)          (((c) >>  8) & 0xFF)
#define CLUT_B(c)          ( (c)        & 0xFF)

#define ARIBCC_COLOR_TO_CLUT_RGBA(c,a) (((ARIBCC_COLOR_A(c) ? ARIBCC_COLOR_A(c) : (a)) << 24) | \
                                        (ARIBCC_COLOR_R(c) << 16) | \
                                        (ARIBCC_COLOR_G(c) <<  8) | \
                                        (ARIBCC_COLOR_B(c)))

typedef struct ARIBCaptionContext {
    AVClass *class;
    AVCodecContext *avctx;
    const AVPacket *avpkt;
    AVSubtitle *sub;

    aribcc_context_t *context;
    aribcc_decoder_t *decoder;
    aribcc_renderer_t *renderer;

    int subtitle_type;
    int encoding_scheme;
    bool ass_single_rect;
    char *font;
    bool replace_fullwidth_ascii;
    bool force_stroke_text;
    bool ignore_background;
    bool ignore_ruby;
    float stroke_width;
    bool replace_drcs;

    int64_t pts;
    AVRational time_base;
    int canvas_width;
    int canvas_height;
    int plane_width;
    int plane_height;
    int frame_width;
    int frame_height;
    int bitmap_plane_width;
    int bitmap_plane_height;
    int font_size;
    int charstyle;
    int border_style;
    int readorder;

    aribcc_caption_t caption;
    aribcc_render_result_t render_result;
    uint32_t *clut;
    int clut_idx;
    int clut_overflow;
    uint8_t clut_alpha[ARIBC_ALPHA_MAX_NUM];
} ARIBCaptionContext;

static void hex_dump_debug(void *ctx, const char *buf, int buf_size)
{
    int i;

    for (i = 0; i < buf_size; i++) {
        ff_dlog(ctx, "%02hhx ", buf[i]);
        if (i % 16 == 15)
            ff_dlog(ctx, "\n");
    }
    if (i % 16)
        ff_dlog(ctx, "\n");
}

static void logcat_callback(aribcc_loglevel_t level, const char* message, void* userdata)
{
    ARIBCaptionContext *ctx = userdata;
    int lvl;

    if (ctx->decoder != NULL) {
        switch (level) {
        case ARIBCC_LOGLEVEL_ERROR:
            lvl = AV_LOG_ERROR;
            break;
        case ARIBCC_LOGLEVEL_WARNING:
            lvl = AV_LOG_WARNING;
            break;
        default:
            lvl = AV_LOG_INFO;
        }

        av_log(ctx, lvl, "%s\n", message);
    }
}

static void estimate_video_frame_size(ARIBCaptionContext *ctx)
{
    if (ctx->avctx->width > 0 && ctx->avctx->height > 0) {
        /* input video size specified by -canvas_size option */
        ctx->bitmap_plane_width = ctx->avctx->width;
        ctx->bitmap_plane_height = ctx->avctx->height;
    } else if (ctx->plane_width == 960) {
        /* ARIB TR-B14 Fascicle 2 Volume 3 [Section 2] 4.3.1 */
        /* ARIB TR-B14 Fascicle 2 Volume 3 [Section 2] Appendix-4 */
        ctx->bitmap_plane_width = 1440;
        ctx->bitmap_plane_height = 1080;
    } else {
        ctx->bitmap_plane_width = ctx->plane_width;
        ctx->bitmap_plane_height = ctx->plane_height;
    }
    /* Expand either width or height */
    if (ctx->bitmap_plane_height * ctx->plane_width > ctx->bitmap_plane_width * ctx->plane_height) {
        ctx->frame_height = ctx->bitmap_plane_height;
        ctx->frame_width = ctx->frame_height * ctx->plane_width / ctx->plane_height;
    } else {
        ctx->frame_width = ctx->bitmap_plane_width;
        ctx->frame_height = ctx->frame_width * ctx->plane_height / ctx->plane_width;
    }
}

static void clut_set_alpha(ARIBCaptionContext *ctx, uint8_t a)
{
    int i;

    for (i = 0; i < ARIBC_ALPHA_MAX_NUM; i++) {
        if (ctx->clut_alpha[i] == 0) {
            ctx->clut_alpha[i] = a;
            return;
        }
        if (ctx->clut_alpha[i] == a)
            return;
    }
    return;
}

static uint8_t clut_find_nearlest_alpha(ARIBCaptionContext *ctx, uint8_t a)
{
    int i, j, d;

    if (a == 0)
        return a;
    d = 256;
    j = 0;
    for (i = 0; i < ARIBC_ALPHA_MAX_NUM; i++) {
        if (ctx->clut_alpha[i] == a)
            return a;
        if (ctx->clut_alpha[i] == 0)
            break;
        if (abs((int)a - (int)ctx->clut_alpha[i]) < d) {
            d = abs((int)a - (int)ctx->clut_alpha[i]);
            j = i;
        }
    }
    return ctx->clut_alpha[j];
}

static int clut_find(ARIBCaptionContext *ctx, uint32_t rgba)
{
    int i;

    for (i = 0; i < ctx->clut_idx; i++) {
        if (ctx->clut[i] == rgba)
            return i;
    }
    return -1;
}

static inline int clut_color_distance(uint32_t rgba1, uint32_t rgba2)
{
    return abs((int)CLUT_R(rgba1) - (int)CLUT_R(rgba2)) +
           abs((int)CLUT_G(rgba1) - (int)CLUT_G(rgba2)) +
           abs((int)CLUT_B(rgba1) - (int)CLUT_B(rgba2));
}

static uint8_t clut_pick_or_set(ARIBCaptionContext *ctx, int r, int g, int b, int a)
{
    int c, i, d, d_min;
    uint32_t rgba;

    a = clut_find_nearlest_alpha(ctx, a);
    if (a == 0)
        return 0; /* transparent */
    rgba = CLUT_RGBA(r,g,b,a);

    d_min = 256 * 3;
    c = 0;
    for (i = 0; i < ctx->clut_idx; i++) {
        if (ctx->clut[i] == rgba)
            return i;
        if (CLUT_A(ctx->clut[i]) != a)
            continue;
        d = clut_color_distance(ctx->clut[i], rgba);
        if (d < d_min) {
            d_min = d;
            c = i;
        }
    }
    if (d_min > 3) {
        if (ctx->clut_idx >= AVPALETTE_COUNT)
            ctx->clut_overflow++;
        else {
            c = ctx->clut_idx;
            ctx->clut[ctx->clut_idx++] = rgba;
        }
    }
    return c;
}

/* initialiaze CLUT with each character colors */
static void clut_init(ARIBCaptionContext *ctx, aribcc_caption_region_t *region)
{
    aribcc_color_t text_color, back_color, stroke_color;
    uint32_t rgba;

    ctx->clut[0] = CLUT_RGBA(0,0,0,0); /* transparent */
    ctx->clut_alpha[0] = 0xFF;
    ctx->clut_idx = 1;
    ctx->clut_overflow = 0;
    text_color = region->chars[0].text_color;
    back_color = region->chars[0].back_color;
    stroke_color = region->chars[0].stroke_color;
    rgba = ARIBCC_COLOR_TO_CLUT_RGBA(text_color, ARIBC_ALPHA_DEFAULT_FRONT);
    ctx->clut[ctx->clut_idx++] = rgba;
    clut_set_alpha(ctx, CLUT_A(rgba));
    rgba = ARIBCC_COLOR_TO_CLUT_RGBA(back_color, ARIBC_ALPHA_DEFAULT_BACK);
    ctx->clut[ctx->clut_idx++] = rgba;
    clut_set_alpha(ctx, CLUT_A(rgba));
    rgba = ARIBCC_COLOR_TO_CLUT_RGBA(stroke_color, ARIBC_ALPHA_DEFAULT_FRONT);
    if (clut_find(ctx, rgba) < 0) {
        ctx->clut[ctx->clut_idx++] = rgba;
        clut_set_alpha(ctx, CLUT_A(rgba));
    }

    for (int i = 1; i < region->char_count; i++) {
        if (region->chars[i].text_color != text_color) {
            rgba = ARIBCC_COLOR_TO_CLUT_RGBA(region->chars[i].text_color,
                                             ARIBC_ALPHA_DEFAULT_FRONT);
            if (clut_find(ctx, rgba) < 0) {
                ctx->clut[ctx->clut_idx++] = rgba;
                clut_set_alpha(ctx, CLUT_A(rgba));
            }
        }
        if (region->chars[i].back_color != back_color) {
            rgba = ARIBCC_COLOR_TO_CLUT_RGBA(region->chars[i].back_color,
                                             ARIBC_ALPHA_DEFAULT_BACK);
            if (clut_find(ctx, rgba) < 0) {
                ctx->clut[ctx->clut_idx++] = rgba;
                clut_set_alpha(ctx, CLUT_A(rgba));
            }
        }
        if (region->chars[i].stroke_color != stroke_color) {
            rgba = ARIBCC_COLOR_TO_CLUT_RGBA(region->chars[i].stroke_color,
                                             ARIBC_ALPHA_DEFAULT_FRONT);
            if (clut_find(ctx, rgba) < 0) {
                if (ctx->clut_idx < AVPALETTE_COUNT)
                    ctx->clut[ctx->clut_idx++] = rgba;
                clut_set_alpha(ctx, CLUT_A(rgba));
            }
        }
    }
}

/**
 * aribcaption_trans_{bitmap|ass|text}_subtitle()
 *
 * Transfer decoded subtitle to AVSubtitle with corresponding subtitle type.
 *
 * @param ctx pointer to the ARIBCaptionContext
 * @return > 0 number of rectangles to be displayed
 *         = 0 no subtitle
 *         < 0 error code
 */
static int aribcaption_trans_bitmap_subtitle(ARIBCaptionContext *ctx)
{
    int ret = 0;
    AVSubtitle *sub = ctx->sub;
    int status, rect_idx;
    int old_width = ctx->frame_width;
    int old_height = ctx->frame_height;

    if (ctx->caption.plane_width > 0 && ctx->caption.plane_height > 0) {
        ctx->plane_width = ctx->caption.plane_width;
        ctx->plane_height = ctx->caption.plane_height;
    }
    estimate_video_frame_size(ctx);
    if (ctx->frame_width != old_width || ctx->frame_height != old_height) {
        ff_dlog(ctx, "canvas: %dx%d  plane: %dx%d  bitmap: %dx%d  frame: %dx%d\n",
                ctx->avctx->width, ctx->avctx->height,
                ctx->plane_width, ctx->plane_height,
                ctx->bitmap_plane_width, ctx->bitmap_plane_height,
                ctx->frame_width, ctx->frame_height);
        if (!aribcc_renderer_set_frame_size(ctx->renderer,
                                 ctx->frame_width, ctx->frame_height)) {
            av_log(ctx, AV_LOG_ERROR,
                   "aribcc_renderer_set_frame_size() returned with error.\n");
            return AVERROR_EXTERNAL;
        }
    }

    status = aribcc_renderer_append_caption(ctx->renderer, &ctx->caption);
    if (!status) {
        av_log(ctx, AV_LOG_ERROR,
               "aribcc_renderer_append_caption() returned with error.\n");
        return AVERROR_EXTERNAL;
    }

    status = aribcc_renderer_render(ctx->renderer, ctx->pts, &ctx->render_result);
    switch (status) {
    case ARIBCC_RENDER_STATUS_GOT_IMAGE:
        break;

    case ARIBCC_RENDER_STATUS_GOT_IMAGE_UNCHANGED:
        aribcc_render_result_cleanup(&ctx->render_result);
        ff_dlog(ctx, "got image unchanged\n");
        return 0;

    case ARIBCC_RENDER_STATUS_NO_IMAGE:
        ff_dlog(ctx, "no image\n");
        return 0;

    case ARIBCC_RENDER_STATUS_ERROR:
        av_log(ctx, AV_LOG_ERROR,
               "aribcc_renderer_render() returned with error.\n");
        return AVERROR_EXTERNAL;

    default:
        aribcc_render_result_cleanup(&ctx->render_result);
        av_log(ctx, AV_LOG_ERROR,
               "aribcc_renderer_render() returned unknown status: %d\n", status);
        return AVERROR_EXTERNAL;
    }

    if (!ctx->render_result.image_count || ctx->render_result.images == NULL) {
        aribcc_render_result_cleanup(&ctx->render_result);
        ff_dlog(ctx, "no image (%d)\n", ctx->render_result.image_count);
        return 0;
    }

    sub->format = 0; /* graphic */
    sub->rects = av_calloc(ctx->render_result.image_count, sizeof(*sub->rects));
    if (!sub->rects) {
        ret = AVERROR(ENOMEM);
        goto fail;
    }
    for (int i = 0; i < ctx->render_result.image_count; i++) {
        sub->rects[i] = av_mallocz(sizeof(*sub->rects[i]));
        if (!sub->rects[i]) {
            ret = AVERROR(ENOMEM);
            goto fail;
        }
    }

    for (rect_idx = 0; rect_idx < ctx->caption.region_count; rect_idx++) {
        AVSubtitleRect *rect = sub->rects[rect_idx];
        aribcc_image_t *image = &ctx->render_result.images[rect_idx];
        int w, h, shrink_height, dst_idx;

        clut_init(ctx, &ctx->caption.regions[rect_idx]);

        rect->w = image->width * ctx->bitmap_plane_width / ctx->frame_width;
        rect->h = image->height * ctx->bitmap_plane_height / ctx->frame_height;
        rect->data[0] = av_mallocz(rect->w * rect->h);
        if (!rect->data[0]) {
            ret = AVERROR(ENOMEM);
            goto fail;
        }
        if ((image->height != rect->h && image->width != rect->w) ||
            image->stride < image->width * 4 ||
            image->stride * image->height > image->bitmap_size) {
            av_log(ctx, AV_LOG_ERROR, "Bug: unexpected rendered image: %d(%d)x%d -> %dx%d\n",
                   image->width, image->stride / 4, image->height, rect->w, rect->h);
            ret = AVERROR_EXTERNAL;
            goto fail;
        }

        shrink_height = image->height != rect->h;
        dst_idx = 0;
        for (h = 0; h < rect->h; h++) {
            for (w = 0; w < rect->w; w++) {
                /* Bi-linear interpolation */
                int n, m, idx0, idx1, r, g, b, a;
                if (shrink_height) {
                    int div_a, y0, y1;
                    div_a = h * ctx->frame_height;
                    n = ctx->bitmap_plane_height;
                    y0 = div_a / n;
                    y1 = FFMIN(y0 + 1, image->height - 1);
                    m = div_a - n * y0;
                    idx0 = image->stride * y0 + w * 4;
                    idx1 = image->stride * y1 + w * 4;
                } else {
                    int div_a, x0, x1;
                    div_a = w * ctx->frame_width;
                    n = ctx->bitmap_plane_width;
                    x0 = div_a / n;
                    x1 = FFMIN(x0 + 1, image->width - 1);
                    m = div_a - n * x0;
                    idx0 = image->stride * h + x0 * 4;
                    idx1 = image->stride * h + x1 * 4;
                }
                r = (image->bitmap[idx0++] * (n - m) + image->bitmap[idx1++] * m) / n;
                g = (image->bitmap[idx0++] * (n - m) + image->bitmap[idx1++] * m) / n;
                b = (image->bitmap[idx0++] * (n - m) + image->bitmap[idx1++] * m) / n;
                a = (image->bitmap[idx0++] * (n - m) + image->bitmap[idx1++] * m) / n;
                rect->data[0][dst_idx++] = clut_pick_or_set(ctx, r, g, b, a);
            }
        }
        rect->data[1] = av_memdup(ctx->clut, AVPALETTE_SIZE);
        if (!rect->data[1]) {
            ret = AVERROR(ENOMEM);
            goto fail;
        }

        if (ctx->avctx->profile == AV_PROFILE_ARIB_PROFILE_C) {
            /* ARIB TR-B14 version 3.8 Fascicle 1-(2/2) Volume 3 [Section 4] */
            /* No position information is provided for profile C */
            rect->x = (ctx->frame_width - rect->w) / 2;
            rect->y = ctx->frame_height - rect->h * (ctx->caption.region_count - rect_idx);
        } else {
            rect->x = image->dst_x * ctx->bitmap_plane_width / ctx->frame_width;
            rect->y = image->dst_y * ctx->bitmap_plane_height / ctx->frame_height;
        }
        rect->type = SUBTITLE_BITMAP;
        rect->linesize[0] = rect->w;
        rect->nb_colors = 256;

        ff_dlog(ctx, "BITMAP subtitle%s (%d,%d) %dx%d -> (%d,%d) %dx%d [%d]: %d colors\n",
                (ctx->caption.regions[rect_idx].is_ruby) ? " (ruby)" : "",
                image->dst_x, image->dst_y, image->width, image->height,
                rect->x, rect->y, rect->w, rect->h,
                rect_idx, ctx->clut_idx);
        if (ctx->clut_overflow)
            av_log(ctx, AV_LOG_WARNING, "CLUT overflow (%d).\n", ctx->clut_overflow);
    }
    sub->num_rects = rect_idx;

    return rect_idx;

fail:
    if (sub->rects) {
        for (int i = 0; i < ctx->caption.region_count; i++) {
            if (sub->rects[i]) {
                av_freep(&sub->rects[i]->data[0]);
                av_freep(&sub->rects[i]->data[1]);
                av_freep(&sub->rects[i]);
            }
        }
        av_freep(&sub->rects);
    }
    sub->num_rects = 0;

    return ret;
}

static int set_ass_header(ARIBCaptionContext *ctx)
{
    AVCodecContext *avctx = ctx->avctx;
    int outline, shadow;
    const char *font_name;
    const char *fonts = ctx->font;

    if (ctx->border_style == 4) {
        outline = 0;
        shadow = 4;
    } else {
        outline = 1;
        shadow = 0;
    }
    if (ctx->force_stroke_text)
        outline = (int)(ctx->stroke_width * 4.0 / 3.0);

    if (fonts && *fonts)
        font_name = av_get_token(&fonts, ",");
    else
        font_name = av_strdup(DEFAULT_FONT_ASS);
    if (!font_name)
        return AVERROR(ENOMEM);

    av_freep(&avctx->subtitle_header);
    avctx->subtitle_header = av_asprintf(
            "[Script Info]\r\n"
            "ScriptType: v4.00+\r\n"
            "PlayResX: %d\r\n"
            "PlayResY: %d\r\n"
            "WrapStyle: 2\r\n"      /* 2: no word wrapping */
            "\r\n"

            "[V4+ Styles]\r\n"
             "Format: Name, "
             "Fontname, Fontsize, "
             "PrimaryColour, SecondaryColour, OutlineColour, BackColour, "
             "Bold, Italic, Underline, StrikeOut, "
             "ScaleX, ScaleY, "
             "Spacing, Angle, "
             "BorderStyle, Outline, Shadow, "
             "Alignment, MarginL, MarginR, MarginV, "
             "Encoding\r\n"

             "Style: "
             "Default,"             /* Name */
             "%s,%d,"               /* Font{name,size} */
             "&H%x,&H%x,&H%x,&H%x," /* {Primary,Secondary,Outline,Back}Colour */
             "%d,%d,%d,0,"          /* Bold, Italic, Underline, StrikeOut */
             "100,100,"             /* Scale{X,Y} */
             "0,0,"                 /* Spacing, Angle */
             "%d,%d,%d,"            /* BorderStyle, Outline, Shadow */
             "%d,10,10,10,"         /* Alignment, Margin[LRV] */
             "0\r\n"                /* Encoding */
             "\r\n"

             "[Events]\r\n"
             "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\r\n",
            ctx->plane_width, ctx->plane_height,
            font_name, ctx->font_size,
            ASS_DEFAULT_COLOR, ASS_DEFAULT_COLOR,
            ASS_DEFAULT_BACK_COLOR, ASS_DEFAULT_BACK_COLOR,
            -ASS_DEFAULT_BOLD, -ASS_DEFAULT_ITALIC, -ASS_DEFAULT_UNDERLINE,
            ctx->border_style, outline, shadow, ASS_DEFAULT_ALIGNMENT);

    av_freep(&font_name);
    if (!avctx->subtitle_header)
        return AVERROR(ENOMEM);
    avctx->subtitle_header_size = strlen(avctx->subtitle_header);
    return 0;
}

static void set_ass_color(AVBPrint *buf, int color_num,
                          aribcc_color_t new_color, aribcc_color_t old_color)
{
    if (ARIBCC_COLOR_DIFF_RGB(new_color, old_color))
        av_bprintf(buf, "{\\%dc&H%06x&}", color_num,
                                          ARIBCC_COLOR_RGB(new_color));
    if (ARIBCC_COLOR_DIFF_A(new_color, old_color))
        av_bprintf(buf, "{\\%da&H%02x&}", color_num,
                                          0xFF - ARIBCC_COLOR_A(new_color));
}

static int aribcaption_trans_ass_subtitle(ARIBCaptionContext *ctx)
{
    AVSubtitle *sub = ctx->sub;
    AVBPrint buf;
    bool single_rect = ctx->ass_single_rect;
    int ret = 0, rect_idx;

    if (ctx->caption.plane_width > 0 && ctx->caption.plane_height > 0 &&
        (ctx->caption.plane_width != ctx->plane_width ||
         ctx->caption.plane_height != ctx->plane_height)) {
        ctx->plane_width = ctx->caption.plane_width;
        ctx->plane_height = ctx->caption.plane_height;
        if ((ret = set_ass_header(ctx)) < 0)
            return ret;
    }

    /* ARIB TR-B14 version 3.8 Fascicle 1-(2/2) Volume 3 [Section 4] */
    /* No position information is provided for profile C */
    if (ctx->avctx->profile == AV_PROFILE_ARIB_PROFILE_C)
        single_rect = true;

    sub->format = 1; /* text */
    if (ctx->caption.region_count == 0) {
        /* clear previous caption for indefinite duration  */
        ff_ass_add_rect(sub, "", ctx->readorder++, 0, NULL, NULL);
        return 1;
    }

    av_bprint_init(&buf, ARIBC_BPRINT_SIZE_INIT, ARIBC_BPRINT_SIZE_MAX);

    if (single_rect && ctx->avctx->profile != AV_PROFILE_ARIB_PROFILE_C) {
        int x, y, rx, ry;
        x = ctx->plane_width;
        y = ctx->plane_height;
        for (int i = 0; i < ctx->caption.region_count; i++) {
            rx = ctx->caption.regions[i].x;
            ry = ctx->caption.regions[i].y;
            if (rx < x)
                x = rx;
            if (ry < y)
                y = ry;
        }
        av_bprintf(&buf, "{\\an7}");
        if (y < 0)
            y += ctx->plane_height;
        if (x > 0 || y > 0)
            av_bprintf(&buf, "{\\pos(%d,%d)}", x, y);
    }

    rect_idx = 0;
    for (int i = 0; i < ctx->caption.region_count; i++) {
        aribcc_caption_region_t *region = &ctx->caption.regions[i];
        aribcc_color_t text_color = ARIBCC_MAKE_RGBA(0xFF, 0xFF, 0xFF,
                                                     ARIBC_ALPHA_DEFAULT_FRONT);
        aribcc_color_t stroke_color = ARIBCC_MAKE_RGBA(0, 0, 0,
                                                       ARIBC_ALPHA_DEFAULT_FRONT);
        aribcc_color_t back_color = ARIBCC_MAKE_RGBA(0, 0, 0,
                                                     ARIBC_ALPHA_DEFAULT_BACK);
        aribcc_charstyle_t charstyle = ctx->charstyle;
        int char_width = ctx->font_size;
        int char_height = ctx->font_size;
        int char_horizontal_spacing = 0;

        if (region->is_ruby && ctx->ignore_ruby)
            continue;

        if (!single_rect) {
            int x = region->x;
            int y = region->y;
            if (x < 0)
                x += ctx->plane_width;
            if (y < 0)
                y += ctx->plane_height;
            av_bprint_clear(&buf);
            av_bprintf(&buf, "{\\an7}");
            if (x > 0 || y > 0)
                av_bprintf(&buf, "{\\pos(%d,%d)}", x, y);
        }
        if (region->is_ruby)
            av_bprintf(&buf, "{\\fs%d}", char_height / 2);

        for (int j = 0; j < region->char_count; j++) {
            aribcc_caption_char_t *ch = &region->chars[j];

            if (ctx->avctx->profile != AV_PROFILE_ARIB_PROFILE_C) {
                if (ch->char_horizontal_spacing != char_horizontal_spacing) {
                    av_bprintf(&buf, "{\\fsp%d}", (region->is_ruby) ?
                                     ch->char_horizontal_spacing / 2 :
                                     ch->char_horizontal_spacing);
                    char_horizontal_spacing = ch->char_horizontal_spacing;
                }
                if (ch->char_width != char_width) {
                    av_bprintf(&buf, "{\\fscx%"PRId64"}",
                               av_rescale(ch->char_width, 100, ctx->font_size));
                    char_width = ch->char_width;
                }
                if (ch->char_height != char_height) {
                    av_bprintf(&buf, "{\\fscy%"PRId64"}",
                               av_rescale(ch->char_height, 100, ctx->font_size));
                    char_height = ch->char_height;
                }
            }
            if (ch->style != charstyle) {
                aribcc_charstyle_t diff = ch->style ^ charstyle;
                if (diff & ARIBCC_CHARSTYLE_STROKE) {
                    if (charstyle & ARIBCC_CHARSTYLE_STROKE) {
                        if (ctx->force_stroke_text)
                            av_bprintf(&buf, "{\\bord%d}",
                                       (int)(ctx->stroke_width * 4.0 / 3.0));
                        else
                            av_bprintf(&buf, "{\\bord0}");
                    } else
                        av_bprintf(&buf, "{\\bord3}");
                }
                if (diff & ARIBCC_CHARSTYLE_BOLD) {
                    if (charstyle & ARIBCC_CHARSTYLE_BOLD)
                        av_bprintf(&buf, "{\\b0}");
                    else
                        av_bprintf(&buf, "{\\b1}");
                }
                if (diff & ARIBCC_CHARSTYLE_ITALIC) {
                    if (charstyle & ARIBCC_CHARSTYLE_ITALIC)
                        av_bprintf(&buf, "{\\i0}");
                    else
                        av_bprintf(&buf, "{\\i1}");
                }
                if (diff & ARIBCC_CHARSTYLE_UNDERLINE) {
                    if (charstyle & ARIBCC_CHARSTYLE_UNDERLINE)
                        av_bprintf(&buf, "{\\u0}");
                    else
                        av_bprintf(&buf, "{\\u1}");
                }
                charstyle = ch->style;
            }
            if (ch->text_color != text_color) {
                set_ass_color(&buf, 1, ch->text_color, text_color);
                text_color = ch->text_color;
            }
            if (ch->stroke_color != stroke_color) {
                set_ass_color(&buf, 3, ch->stroke_color, stroke_color);
                stroke_color = ch->stroke_color;
            }
            if (ch->back_color != back_color) {
                if (ctx->border_style == 4)
                    set_ass_color(&buf, 4, ch->back_color, back_color);
                else
                    set_ass_color(&buf, 3, ch->back_color, back_color);
                back_color = ch->back_color;
            }
            if (region->chars[j].type == ARIBCC_CHARTYPE_DRCS)
                av_bprintf(&buf, "\xe3\x80\x93");  /* Geta Mark */
            else
                ff_ass_bprint_text_event(&buf, ch->u8str, strlen(ch->u8str), "", 0);
        }

        if (single_rect) {
            if (i + 1 < ctx->caption.region_count)
                av_bprintf(&buf, "{\\r}\\N");
            ff_dlog(ctx, "ASS subtitle%s (%d,%d) %dx%d [%d]\n",
                    (region->is_ruby) ? " (ruby)" : "",
                    region->x, region->y, region->width, region->height,
                    rect_idx);
        } else {
            if (!av_bprint_is_complete(&buf)) {
                ret = AVERROR(ENOMEM);
                goto fail;
            }
            ff_dlog(ctx, "ASS subtitle%s (%d,%d) %dx%d [%d]: %s\n",
                    (region->is_ruby) ? " (ruby)" : "",
                    region->x, region->y, region->width, region->height,
                    rect_idx, buf.str);

            ret = ff_ass_add_rect(sub, buf.str, ctx->readorder++, 0 , NULL, NULL);
            if (ret != 0)
                goto fail;
            rect_idx++;
        }
    }
    if (single_rect) {
        if (!av_bprint_is_complete(&buf)) {
            ret = AVERROR(ENOMEM);
            goto fail;
        }
        ff_dlog(ctx, "ASS subtitle: %s\n", buf.str);

        ret = ff_ass_add_rect(sub, buf.str, ctx->readorder++, 0 , NULL, NULL);
        if (ret != 0)
            goto fail;
        rect_idx++;
    }

    av_bprint_finalize(&buf, NULL);
    return rect_idx;

fail:
    if (sub->rects) {
        for (int i = 0; i < ctx->caption.region_count; i++) {
            if (sub->rects[i]) {
                av_freep(&sub->rects[i]->ass);
                av_freep(&sub->rects[i]);
            }
        }
        av_freep(&sub->rects);
    }
    sub->num_rects = 0;
    av_bprint_finalize(&buf, NULL);

    return ret;
}

static int aribcaption_trans_text_subtitle(ARIBCaptionContext *ctx)
{
    AVSubtitle *sub = ctx->sub;
    AVSubtitleRect *rect;
    int ret = 0;
    const char *text;

    sub->rects = av_calloc(ctx->caption.region_count, sizeof(*sub->rects));
    if (!sub->rects) {
        ret = AVERROR(ENOMEM);
        goto fail;
    }
    sub->num_rects = 1;

    sub->rects[0] = av_mallocz(sizeof(*sub->rects[0]));
    if (!sub->rects[0]) {
        ret = AVERROR(ENOMEM);
        goto fail;
    }
    rect = sub->rects[0];

    if (ctx->caption.region_count == 0)
        text = ""; /* clear previous caption */
    else {
        text = ctx->caption.text;
        ff_dlog(ctx, "TEXT subtitle: %s\n", text);
    }
    rect->text = av_strdup(text);
    if (!rect->text) {
        ret = AVERROR(ENOMEM);
        goto fail;
    }

    sub->format = 1; /* text */
    rect->type = SUBTITLE_TEXT;

    return 1;

fail:
    if (sub->rects) {
        rect = sub->rects[0];
        if (rect) {
            av_freep(&rect->text);
            av_freep(&rect);
        }
        av_freep(&sub->rects);
    }
    sub->num_rects = 0;

    return ret;
}

static int aribcaption_decode(AVCodecContext *avctx, AVSubtitle *sub,
                              int *got_sub_ptr, const AVPacket *avpkt)
{
    ARIBCaptionContext *ctx = avctx->priv_data;
    int status;

    ff_dlog(ctx, "ARIB caption packet pts=%"PRIx64":\n", avpkt->pts);
    if (sub->num_rects) {
        avpriv_request_sample(ctx, "Different Version of Segment asked Twice");
        return AVERROR_PATCHWELCOME;
    }
    hex_dump_debug(ctx, avpkt->data, avpkt->size);

    ctx->sub = sub;
    ctx->avpkt = avpkt;
    ctx->time_base = avctx->pkt_timebase;
    if (ctx->time_base.num <= 0 || ctx->time_base.den <= 0) {
        av_log(ctx, AV_LOG_VERBOSE, "No timebase set. assuming 90kHz.\n");
        ctx->time_base = av_make_q(1, 90000);
    }
    if (avpkt->pts == AV_NOPTS_VALUE)
        ctx->pts = ARIBCC_PTS_NOPTS;
    else
        ctx->pts = av_rescale_q(avpkt->pts, ctx->time_base, (AVRational){1, 1000});

    status = aribcc_decoder_decode(ctx->decoder, avpkt->data, avpkt->size,
                                   ctx->pts, &ctx->caption);
    if (status == ARIBCC_DECODE_STATUS_ERROR) {
        av_log(ctx, AV_LOG_ERROR,
               "aribcc_decoder_decode() returned with error.\n");
        return AVERROR(EAGAIN);
    }
    if (status == ARIBCC_DECODE_STATUS_NO_CAPTION) {
        ff_dlog(ctx, "No caption.\n");
        return avpkt->size;
    } else {
        ff_dlog(ctx, "type=%02x, flags=%x, lang=%03x\n",
                ctx->caption.type, ctx->caption.flags, ctx->caption.iso6392_language_code);
        ff_dlog(ctx, "region count = %d, start=%d.%d, duration=%d.%d\n",
                ctx->caption.region_count,
                (int)(ctx->caption.pts / 1000), (int)(ctx->caption.pts % 1000),
                (int)((ctx->caption.wait_duration == ARIBCC_DURATION_INDEFINITE) ?
                      -1 : ctx->caption.wait_duration / 1000),
                (int)((ctx->caption.wait_duration == ARIBCC_DURATION_INDEFINITE) ?
                      0 : ctx->caption.wait_duration % 1000));
    }

    switch ((enum AVSubtitleType) ctx->subtitle_type) {
    case SUBTITLE_TEXT:
        status = aribcaption_trans_text_subtitle(ctx);
        break;

    case SUBTITLE_ASS:
        status = aribcaption_trans_ass_subtitle(ctx);
        break;

    case SUBTITLE_BITMAP:
        status = aribcaption_trans_bitmap_subtitle(ctx);
        break;

    case SUBTITLE_NONE:
    default:
        status = 0;
    }

    if (status < 0) {
        av_log(ctx, AV_LOG_ERROR, "Failed to set Subtitle: %s\n",
               av_err2str(status));
        aribcc_caption_cleanup(&ctx->caption);
        return status;
    }
    if (status > 0) {
        *got_sub_ptr = 1;
        if (ctx->avpkt->pts != AV_NOPTS_VALUE)
            sub->pts = av_rescale_q(ctx->avpkt->pts,
                                    ctx->time_base, AV_TIME_BASE_Q);
        if (ctx->caption.wait_duration == ARIBCC_DURATION_INDEFINITE)
            sub->end_display_time = UINT32_MAX;
        else
            sub->end_display_time = (uint32_t)ctx->caption.wait_duration;
    }

    aribcc_caption_cleanup(&ctx->caption);
    return avpkt->size;
}

static void aribcaption_flush(AVCodecContext *avctx)
{
    ARIBCaptionContext *ctx = avctx->priv_data;

    if (ctx->decoder)
        aribcc_decoder_flush(ctx->decoder);
    if (ctx->renderer)
        aribcc_renderer_flush(ctx->renderer);
    if (!(avctx->flags2 & AV_CODEC_FLAG2_RO_FLUSH_NOOP))
        ctx->readorder = 0;
}

static int aribcaption_close(AVCodecContext *avctx)
{
    ARIBCaptionContext *ctx = avctx->priv_data;

    av_freep(&ctx->clut);
    if (ctx->renderer)
        aribcc_renderer_free(ctx->renderer);
    if (ctx->decoder)
        aribcc_decoder_free(ctx->decoder);
    if (ctx->context)
        aribcc_context_free(ctx->context);

    return 0;
}

static int aribcaption_init(AVCodecContext *avctx)
{
    ARIBCaptionContext *ctx = avctx->priv_data;
    aribcc_profile_t profile;
    int ret = 0;

    ctx->avctx = avctx;

    switch (avctx->profile) {
    case AV_PROFILE_ARIB_PROFILE_A:
        profile = ARIBCC_PROFILE_A;
        /* assume 960x540 at initial state */
        ctx->plane_width = 960;
        ctx->plane_height = 540;
        ctx->font_size = 36;
        break;
    case AV_PROFILE_ARIB_PROFILE_C:
        profile = ARIBCC_PROFILE_C;
        ctx->plane_width = 320;
        ctx->plane_height = 180;
        ctx->font_size = 16;
        break;
    default:
        av_log(avctx, AV_LOG_ERROR, "Unknown or unsupported profile set.\n");
        return AVERROR(EINVAL);
    }
    /* determine BorderStyle of ASS header */
    if (ctx->ignore_background)
        ctx->border_style = 1;
    else
        ctx->border_style = 4;
    ctx->charstyle = ARIBCC_CHARSTYLE_DEFAULT;
    if (ctx->force_stroke_text || ctx->ignore_background)
        ctx->charstyle |= ARIBCC_CHARSTYLE_STROKE;

    if (!(ctx->context = aribcc_context_alloc())) {
        av_log(avctx, AV_LOG_ERROR, "Failed to alloc libaribcaption context.\n");
        return AVERROR_EXTERNAL;
    }
    aribcc_context_set_logcat_callback(ctx->context, logcat_callback, avctx);
    if (!(ctx->decoder = aribcc_decoder_alloc(ctx->context))) {
        av_log(avctx, AV_LOG_ERROR, "Failed to alloc libaribcaption decoder.\n");
        return AVERROR_EXTERNAL;
    }
    if (!aribcc_decoder_initialize(ctx->decoder,
                                   (enum aribcc_encoding_scheme_t) ctx->encoding_scheme,
                                   ARIBCC_CAPTIONTYPE_CAPTION,
                                   profile,
                                   ARIBCC_LANGUAGEID_FIRST)) {
        av_log(avctx, AV_LOG_ERROR, "Failed to initialize libaribcaption decoder.\n");
        return AVERROR_EXTERNAL;
    }
    aribcc_decoder_set_replace_msz_fullwidth_ascii(ctx->decoder,
                                                   ctx->replace_fullwidth_ascii);

    /* Similar behavior as ffmpeg tool to set canvas size */
    if (ctx->canvas_width > 0 && ctx->canvas_height > 0 &&
        (ctx->avctx->width == 0 || ctx->avctx->height == 0)) {
        ctx->avctx->width = ctx->canvas_width;
        ctx->avctx->height = ctx->canvas_height;
    }

    switch ((enum AVSubtitleType) ctx->subtitle_type) {
    case SUBTITLE_ASS:
        ret = set_ass_header(ctx);
        if (ret != 0) {
            av_log(avctx, AV_LOG_ERROR, "Failed to set ASS header: %s\n",
                                        av_err2str(ret));
            return ret;
        }
        break;

    case SUBTITLE_BITMAP:
        if(!(ctx->renderer = aribcc_renderer_alloc(ctx->context))) {
            av_log(avctx, AV_LOG_ERROR, "Failed to alloc libaribcaption renderer.\n");
            return AVERROR_EXTERNAL;
        }
        if(!aribcc_renderer_initialize(ctx->renderer,
                                       ARIBCC_CAPTIONTYPE_CAPTION,
                                       ARIBCC_FONTPROVIDER_TYPE_AUTO,
                                       ARIBCC_TEXTRENDERER_TYPE_AUTO)) {
            av_log(avctx, AV_LOG_ERROR, "Failed to initialize libaribcaption renderer.\n");
            return AVERROR_EXTERNAL;
        }
        estimate_video_frame_size(ctx);
        ff_dlog(ctx, "canvas: %dx%d  plane: %dx%d  bitmap: %dx%d  frame: %dx%d\n",
                ctx->avctx->width, ctx->avctx->height,
                ctx->plane_width, ctx->plane_height,
                ctx->bitmap_plane_width, ctx->bitmap_plane_height,
                ctx->frame_width, ctx->frame_height);
        if (!aribcc_renderer_set_frame_size(ctx->renderer,
                                            ctx->frame_width, ctx->frame_height)) {
            av_log(ctx, AV_LOG_ERROR,
                   "aribcc_renderer_set_frame_size() returned with error.\n");
            return AVERROR_EXTERNAL;
        }

        if (!(ctx->clut = av_mallocz(AVPALETTE_SIZE)))
            return AVERROR(ENOMEM);

        aribcc_renderer_set_storage_policy(ctx->renderer, ARIBCC_CAPTION_STORAGE_POLICY_MINIMUM, 0);
        aribcc_renderer_set_replace_drcs(ctx->renderer, ctx->replace_drcs);
        aribcc_renderer_set_force_stroke_text(ctx->renderer, ctx->force_stroke_text);
        aribcc_renderer_set_force_no_background(ctx->renderer, ctx->ignore_background);
        aribcc_renderer_set_force_no_ruby(ctx->renderer, ctx->ignore_ruby);
        aribcc_renderer_set_stroke_width(ctx->renderer, ctx->stroke_width);
        if (ctx->font) {
            int is_nomem = 0;
            size_t count = 0;
            const char **font_families = NULL;
            const char *fonts = ctx->font;

            while (*fonts) {
                const char **ff = av_realloc_array(font_families, count + 1, sizeof(*font_families));
                if (!ff) {
                    is_nomem = 1;
                    break;
                } else {
                    font_families = ff;
                    ff[count++] = av_get_token(&fonts, ",");
                    if (!ff[count - 1]) {
                        is_nomem = 1;
                        break;
                    } else if (*fonts)
                        fonts++;
                }
            }
            if (!is_nomem && count)
                aribcc_renderer_set_default_font_family(ctx->renderer, font_families, count, true);
            while (count)
                av_freep(&font_families[--count]);
            av_freep(&font_families);
            if (is_nomem)
                return AVERROR(ENOMEM);
        }
        break;

    case SUBTITLE_TEXT:
    case SUBTITLE_NONE:
    default:
        /* do nothing */ ;
    }

    ctx->readorder = 0;

    return 0;
}

#if !defined(ASS_SINGLE_RECT)
# define ASS_SINGLE_RECT 0
#endif

#define OFFSET(x) offsetof(ARIBCaptionContext, x)
#define SD AV_OPT_FLAG_SUBTITLE_PARAM | AV_OPT_FLAG_DECODING_PARAM
static const AVOption options[] = {
    { "sub_type", "subtitle rendering type",
      OFFSET(subtitle_type), AV_OPT_TYPE_INT,
      { .i64 = SUBTITLE_ASS }, SUBTITLE_NONE, SUBTITLE_ASS, SD, "type" },
    { "none",   "do nothing", 0, AV_OPT_TYPE_CONST,
      { .i64 = SUBTITLE_NONE }, .flags = SD, .unit = "type" },
    { "bitmap", "bitmap rendering", 0, AV_OPT_TYPE_CONST,
      { .i64 = SUBTITLE_BITMAP }, .flags = SD, .unit = "type" },
    { "text",   "plain text", 0, AV_OPT_TYPE_CONST,
      { .i64 = SUBTITLE_TEXT }, .flags = SD, .unit = "type" },
    { "ass",    "formatted text", 0, AV_OPT_TYPE_CONST,
      { .i64 = SUBTITLE_ASS }, .flags = SD, .unit = "type" },
    { "caption_encoding", "encoding scheme of subtitle text",
      OFFSET(encoding_scheme), AV_OPT_TYPE_INT, { .i64 = ARIBCC_ENCODING_SCHEME_AUTO },
      ARIBCC_ENCODING_SCHEME_AUTO, ARIBCC_ENCODING_SCHEME_ABNT_NBR_15606_1_LATIN, SD, "encoding" },
    { "auto",   "automatically detect encoding scheme", 0, AV_OPT_TYPE_CONST,
      { .i64 = ARIBCC_ENCODING_SCHEME_AUTO }, .flags = SD, .unit = "encoding" },
    { "jis",    "8bit-char JIS encoding (Japanese ISDB captions)", 0, AV_OPT_TYPE_CONST,
      { .i64 = ARIBCC_ENCODING_SCHEME_ARIB_STD_B24_JIS }, .flags = SD, .unit = "encoding" },
    { "utf8",   "UTF-8 encoding (Philippines ISDB-T captions)", 0, AV_OPT_TYPE_CONST,
      { .i64 = ARIBCC_ENCODING_SCHEME_ARIB_STD_B24_UTF8 }, .flags = SD, .unit = "encoding" },
    { "latin",  "latin characters (SBTVD / ISDB-Tb captions used in South America)", 0, AV_OPT_TYPE_CONST,
      { .i64 = ARIBCC_ENCODING_SCHEME_ABNT_NBR_15606_1_LATIN }, .flags = SD, .unit = "encoding" },
    { "ass_single_rect", "workaround of ASS subtitle for players which can't handle multi-rectangle [ass]",
      OFFSET(ass_single_rect), AV_OPT_TYPE_BOOL, { .i64 = ASS_SINGLE_RECT }, 0, 1, SD },
    { "font", "comma-separated font family [ass, bitmap]",
      OFFSET(font), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, SD },
    { "replace_fullwidth_ascii", "replace MSZ fullwidth alphanumerics with halfwidth alphanumerics [ass, bitmap]",
      OFFSET(replace_fullwidth_ascii), AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1, SD },
    { "force_outline_text", "always render characters with outline [(ass), bitmap]",
      OFFSET(force_stroke_text), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, SD },
    { "ignore_background", "ignore rendering caption background [(ass), bitmap]",
      OFFSET(ignore_background), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, SD },
    { "ignore_ruby", "ignore ruby-like characters [ass, bitmap]",
      OFFSET(ignore_ruby), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, SD },
    { "outline_width", "outline width of text [(ass), bitmap]",
      OFFSET(stroke_width), AV_OPT_TYPE_FLOAT, { .dbl = 1.5 }, 0.0, 3.0, SD },
    { "replace_drcs", "replace known DRCS [bitmap]",
      OFFSET(replace_drcs), AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1, SD },
    {"canvas_size", "set input video size (WxH or abbreviation) [bitmap]",
      OFFSET(canvas_width), AV_OPT_TYPE_IMAGE_SIZE, { .str = NULL }, 0, INT_MAX, SD },
    { NULL }
};

static const AVClass aribcaption_class = {
    .class_name = "aribcaption decoder",
    .item_name  = av_default_item_name,
    .option     = options,
    .version    = LIBAVUTIL_VERSION_INT,
};

const FFCodec ff_libaribcaption_decoder = {
    .p.name         = "libaribcaption",
    .p.long_name    = NULL_IF_CONFIG_SMALL("ARIB STD-B24 caption decoder"),
    .p.type         = AVMEDIA_TYPE_SUBTITLE,
    .p.id           = AV_CODEC_ID_ARIB_CAPTION,
    .priv_data_size = sizeof(ARIBCaptionContext),
    .init           = aribcaption_init,
    .close          = aribcaption_close,
    FF_CODEC_DECODE_SUB_CB(aribcaption_decode),
    .flush          = aribcaption_flush,
    .p.priv_class   = &aribcaption_class,
    .caps_internal  = FF_CODEC_CAP_INIT_CLEANUP,
};
