/*
 * This file is part of mpv.
 *
 * mpv 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.
 *
 * mpv 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 mpv.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

#include "mpv_talloc.h"
#include "misc/bstr.h"
#include "common/common.h"
#include "common/msg.h"
#include "osd.h"
#include "osd_state.h"

static const char osd_font_pfb[] =
#include "sub/osd_font.otf.inc"
;

#include "sub/ass_mp.h"
#include "options/options.h"


#define ASS_USE_OSD_FONT "{\\fnmpv-osd-symbols}"

static void append_ass(struct ass_state *ass, struct mp_osd_res *res,
                       ASS_Image **img_list, bool *changed);

static void create_ass_renderer(struct osd_state *osd, struct ass_state *ass)
{
    if (ass->render)
        return;

    ass->log = mp_log_new(NULL, osd->log, "libass");
    ass->library = mp_ass_init(osd->global, osd->opts->osd_style, ass->log);
    ass_add_font(ass->library, "mpv-osd-symbols", (void *)osd_font_pfb,
                 sizeof(osd_font_pfb) - 1);

    ass->render = ass_renderer_init(ass->library);
    if (!ass->render)
        abort();

    mp_ass_configure_fonts(ass->render, osd->opts->osd_style,
                           osd->global, ass->log);
    ass_set_pixel_aspect(ass->render, 1.0);
}

static void destroy_ass_renderer(struct ass_state *ass)
{
    if (ass->track)
        ass_free_track(ass->track);
    ass->track = NULL;
    if (ass->render)
        ass_renderer_done(ass->render);
    ass->render = NULL;
    if (ass->library)
        ass_library_done(ass->library);
    ass->library = NULL;
    talloc_free(ass->log);
    ass->log = NULL;
}

static void destroy_external(struct osd_external *ext)
{
    destroy_ass_renderer(&ext->ass);
    talloc_free(ext);
}

void osd_destroy_backend(struct osd_state *osd)
{
    for (int n = 0; n < MAX_OSD_PARTS; n++) {
        struct osd_object *obj = osd->objs[n];
        destroy_ass_renderer(&obj->ass);
        for (int i = 0; i < obj->num_externals; i++)
            destroy_external(obj->externals[i]);
        obj->num_externals = 0;
    }
}

static void update_playres(struct ass_state *ass, struct mp_osd_res *vo_res)
{
    ASS_Track *track = ass->track;
    int old_res_x = track->PlayResX;
    int old_res_y = track->PlayResY;

    ass->vo_res = *vo_res;

    double aspect = 1.0 * vo_res->w / MPMAX(vo_res->h, 1);
    if (vo_res->display_par > 0)
        aspect = aspect / vo_res->display_par;

    track->PlayResY = ass->res_y ? ass->res_y : MP_ASS_FONT_PLAYRESY;
    track->PlayResX = ass->res_x ? ass->res_x : track->PlayResY * aspect;

    // Force libass to clear its internal cache - it doesn't check for
    // PlayRes changes itself.
    if (old_res_x != track->PlayResX || old_res_y != track->PlayResY)
        ass_set_frame_size(ass->render, 1, 1);
}

static void create_ass_track(struct osd_state *osd, struct osd_object *obj,
                             struct ass_state *ass)
{
    create_ass_renderer(osd, ass);

    ASS_Track *track = ass->track;
    struct mp_osd_render_opts *opts = osd->opts;
    if (!track)
        track = ass->track = ass_new_track(ass->library);

    track->track_type = TRACK_TYPE_ASS;
    track->Timer = 100.;
    track->WrapStyle = 1; // end-of-line wrapping instead of smart wrapping
    track->Kerning = true;
    track->ScaledBorderAndShadow = true;
    ass_set_shaper(ass->render, opts->osd_shaper);
#if LIBASS_VERSION >= 0x01600010
    ass_track_set_feature(track, ASS_FEATURE_WRAP_UNICODE, 1);
#endif
#if LIBASS_VERSION >= 0x01703010
    ass_configure_prune(track, opts->osd_ass_prune_delay * 1000.0);
#endif
    ass_set_cache_limits(ass->render, opts->osd_glyph_limit, opts->osd_bitmap_max_size);
    update_playres(ass, &obj->vo_res);
}

static int find_style(ASS_Track *track, const char *name, int def)
{
    for (int n = 0; n < track->n_styles; n++) {
        if (track->styles[n].Name && strcmp(track->styles[n].Name, name) == 0)
            return n;
    }
    return def;
}

// Find a given style, or add it if it's missing.
static ASS_Style *get_style(struct ass_state *ass, char *name)
{
    ASS_Track *track = ass->track;
    if (!track)
        return NULL;

    int sid = find_style(track, name, -1);
    if (sid >= 0)
        return &track->styles[sid];

    sid = ass_alloc_style(track);
    ASS_Style *style = &track->styles[sid];
    style->Name = strdup(name);
    // Set to neutral base direction, as opposed to VSFilter LTR default
    style->Encoding = -1;
    return style;
}

static ASS_Event *add_osd_ass_event(ASS_Track *track, const char *style,
                                    bstr text)
{
    int n = ass_alloc_event(track);
    ASS_Event *event = track->events + n;
    event->Start = 0;
    event->Duration = 100;
    event->Style = find_style(track, style, 0);
    event->ReadOrder = n;
    mp_assert(event->Text == NULL);
    if (text.start) {
        event->Text = malloc(text.len + 1);
        MP_HANDLE_OOM(event->Text);
        memcpy(event->Text, text.start, text.len);
        event->Text[text.len] = '\0';
    }
    return event;
}

static void clear_ass(struct ass_state *ass)
{
    if (ass->track)
        ass_flush_events(ass->track);
}

void osd_get_function_sym(char *buffer, size_t buffer_size, int osd_function)
{
    snprintf(buffer, buffer_size, OSD_CODEPOINTS_ESCAPE "%c", osd_function);
}

void osd_mangle_ass(bstr *dst, const char *in, bool replace_newlines)
{
    const char *start = in;
    bool escape_ass = true;
    while (*in) {
        // As used by osd_get_function_sym().
        size_t len = strlen(OSD_CODEPOINTS_ESCAPE);
        if (!strncmp(in, OSD_CODEPOINTS_ESCAPE, len) && in[len]) {
            bstr_xappend(NULL, dst, bstr0(ASS_USE_OSD_FONT));
            mp_append_utf8_bstr(NULL, dst, OSD_CODEPOINTS + in[len]);
            bstr_xappend(NULL, dst, bstr0("{\\r}"));
            in += len + 1;
            continue;
        }
        len = strlen(OSD_ASS_0);
        if (!strncmp(in, OSD_ASS_0, len)) {
            escape_ass = false;
            in += len;
            continue;
        }
        len = strlen(OSD_ASS_1);
        if (!strncmp(in, OSD_ASS_1, len)) {
            escape_ass = true;
            in += len;
            continue;
        }
        len = strlen(TERM_MSG_0);
        if (!strncmp(in, TERM_MSG_0, len)) {
            in += len;
            continue;
        }
        if (escape_ass && *in == '{')
            bstr_xappend(NULL, dst, bstr0("\\"));
        // Replace newlines with \N for escape-ass. This is necessary to apply
        // ASS tags past newlines and to preserve consecutive newlines with
        // osd-overlay because update_external() adds a ASS event per line.
        if (replace_newlines && *in == '\n') {
            bstr_xappend(NULL, dst, bstr0("\\N"));
            in += 1;
            continue;
        }
        // Libass will strip leading whitespace
        if (in[0] == ' ' && (in == start || in[-1] == '\n')) {
            bstr_xappend(NULL, dst, bstr0("\\h"));
            in += 1;
            continue;
        }
        bstr_xappend(NULL, dst, (bstr){(char *)in, 1});
        // Break ASS escapes with U+2060 WORD JOINER
        if (escape_ass && *in == '\\')
            mp_append_utf8_bstr(NULL, dst, 0x2060);
        in++;
    }
}

static ASS_Event *add_osd_ass_event_escaped(ASS_Track *track, const char *style,
                                            const char *text)
{
    bstr buf = {0};
    osd_mangle_ass(&buf, text, false);
    ASS_Event *e = add_osd_ass_event(track, style, buf);
    talloc_free(buf.start);
    return e;
}

static ASS_Style *prepare_osd_ass(struct osd_state *osd, struct osd_object *obj)
{
    struct mp_osd_render_opts *opts = osd->opts;

    create_ass_track(osd, obj, &obj->ass);

    struct osd_style_opts font = *opts->osd_style;
    font.font_size *= opts->osd_scale;

    double playresy = obj->ass.track->PlayResY;
    // Compensate for libass and mp_ass_set_style scaling the font etc.
    if (!opts->osd_scale_by_window && obj->vo_res.h)
        playresy *= 720.0 / obj->vo_res.h;

    ASS_Style *style = get_style(&obj->ass, "OSD");
    mp_ass_set_style(style, playresy, &font);
    return style;
}

static void update_osd_text(struct osd_state *osd, struct osd_object *obj)
{

    if (!obj->text[0])
        return;

    prepare_osd_ass(osd, obj);
    add_osd_ass_event_escaped(obj->ass.track, "OSD", obj->text);
}

void osd_get_text_size(struct osd_state *osd, int *out_screen_h, int *out_font_h)
{
    mp_mutex_lock(&osd->lock);
    struct osd_object *obj = osd->objs[OSDTYPE_OSD];
    ASS_Style *style = prepare_osd_ass(osd, obj);
    *out_screen_h = obj->ass.track->PlayResY - style->MarginV;
    *out_font_h = style->FontSize;
    mp_mutex_unlock(&osd->lock);
}

// align: -1 .. +1
// frame: size of the containing area
// obj: size of the object that should be positioned inside the area
// margin: min. distance from object to frame (as long as -1 <= align <= +1)
static float get_align(float align, float frame, float obj, float margin)
{
    frame -= margin * 2;
    return margin + frame / 2 - obj / 2 + (frame - obj) / 2 * align;
}

struct ass_draw {
    int scale;
    char *text;
};

static void ass_draw_start(struct ass_draw *d)
{
    d->scale = MPMAX(d->scale, 1);
    d->text = talloc_asprintf_append(d->text, "{\\p%d}", d->scale);
}

static void ass_draw_stop(struct ass_draw *d)
{
    d->text = talloc_strdup_append(d->text, "{\\p0}");
}

static void ass_draw_c(struct ass_draw *d, float x, float y)
{
    int ix = round(x * (1 << (d->scale - 1)));
    int iy = round(y * (1 << (d->scale - 1)));
    d->text = talloc_asprintf_append(d->text, " %d %d", ix, iy);
}

static void ass_draw_append(struct ass_draw *d, const char *t)
{
    d->text = talloc_strdup_append(d->text, t);
}

static void ass_draw_move_to(struct ass_draw *d, float x, float y)
{
    ass_draw_append(d, " m");
    ass_draw_c(d, x, y);
}

static void ass_draw_line_to(struct ass_draw *d, float x, float y)
{
    ass_draw_append(d, " l");
    ass_draw_c(d, x, y);
}

static void ass_draw_rect_ccw(struct ass_draw *d, float x0, float y0,
                              float x1, float y1)
{
    ass_draw_move_to(d, x0, y0);
    ass_draw_line_to(d, x0, y1);
    ass_draw_line_to(d, x1, y1);
    ass_draw_line_to(d, x1, y0);
}

static void ass_draw_rect_cw(struct ass_draw *d, float x0, float y0,
                             float x1, float y1)
{
    ass_draw_move_to(d, x0, y0);
    ass_draw_line_to(d, x1, y0);
    ass_draw_line_to(d, x1, y1);
    ass_draw_line_to(d, x0, y1);
}

static void ass_draw_reset(struct ass_draw *d)
{
    talloc_free(d->text);
    d->text = NULL;
}

static void get_osd_bar_box(struct osd_state *osd, struct osd_object *obj,
                            float *o_x, float *o_y, float *o_w, float *o_h,
                            float *o_border)
{
    struct mp_osd_render_opts *opts = osd->opts;
    struct osd_bar_style_opts *bar_opts = opts->osd_bar_style;

    create_ass_track(osd, obj, &obj->ass);
    ASS_Track *track = obj->ass.track;

    ASS_Style *style = get_style(&obj->ass, "progbar");
    if (!style) {
        *o_x = *o_y = *o_w = *o_h = *o_border = 0;
        return;
    }

    mp_ass_set_style(style, track->PlayResY, opts->osd_style);

    // override the default osd opaque-box into plain outline. Otherwise
    // the opaque box is not aligned with the bar (even without shadow),
    // and each bar ass event gets its own opaque box - breaking the bar.
    style->BorderStyle = 1; // outline

    *o_w = track->PlayResX * (bar_opts->w / 100.0);
    *o_h = track->PlayResY * (bar_opts->h / 100.0);

    style->Outline = bar_opts->outline_size;
    // Rendering with shadow is broken (because there's more than one shape)
    style->Shadow = 0;
    style->Blur = 0;

    style->Alignment = 5;

    *o_border = style->Outline;

    *o_x = get_align(bar_opts->align_x, track->PlayResX, *o_w, *o_border);
    *o_y = get_align(bar_opts->align_y, track->PlayResY, *o_h, *o_border);
}

static void update_progbar(struct osd_state *osd, struct osd_object *obj)
{
    if (obj->progbar_state.type < 0)
        return;

    float px, py, width, height, border;
    get_osd_bar_box(osd, obj, &px, &py, &width, &height, &border);

    ASS_Track *track = obj->ass.track;

    float sx = px - border * 2 - height / 4; // includes additional spacing
    float sy = py + height / 2;

    bstr buf = bstr0(talloc_asprintf(NULL, "{\\an6\\pos(%f,%f)}", sx, sy));

    if (obj->progbar_state.type == 0 || obj->progbar_state.type >= 256) {
        // no sym
    } else if (obj->progbar_state.type >= 32) {
        mp_append_utf8_bstr(NULL, &buf, obj->progbar_state.type);
    } else {
        bstr_xappend(NULL, &buf, bstr0(ASS_USE_OSD_FONT));
        mp_append_utf8_bstr(NULL, &buf, OSD_CODEPOINTS + obj->progbar_state.type);
        bstr_xappend(NULL, &buf, bstr0("{\\r}"));
    }

    add_osd_ass_event(track, "progbar", buf);
    talloc_free(buf.start);

    struct ass_draw *d = &(struct ass_draw) { .scale = 4 };

    if (osd->opts->osd_style->back_color.a && osd->opts->osd_style->border_style != 1) {
        // the bar style always ignores the --osd-back-color config - it messes
        // up the bar. draw an artificial box at the original back color.
        struct m_color bc = osd->opts->osd_style->back_color;
        d->text = talloc_asprintf_append(d->text,
            "{\\pos(%f,%f)\\bord0\\1a&H%02X\\1c&H%02X%02X%02X&}",
             px, py, 255 - bc.a, (int)bc.b, (int)bc.g, (int)bc.r);

        ass_draw_start(d);
        ass_draw_rect_cw(d, -border, -border, width + border, height + border);
        ass_draw_stop(d);
        add_osd_ass_event(track, "progbar", bstr0(d->text));
        ass_draw_reset(d);
    }

    // filled area
    d->text = talloc_asprintf_append(d->text, "{\\bord0\\pos(%f,%f)}", px, py);
    ass_draw_start(d);
    float pos = obj->progbar_state.value * width - border / 2;
    ass_draw_rect_cw(d, 0, 0, pos, height);
    ass_draw_stop(d);
    add_osd_ass_event(track, "progbar", bstr0(d->text));
    ass_draw_reset(d);

    // position marker
    d->text = talloc_asprintf_append(d->text, "{\\bord%f\\pos(%f,%f)}",
                                     border / 2, px, py);
    ass_draw_start(d);
    ass_draw_move_to(d, pos + border / 2, 0);
    ass_draw_line_to(d, pos + border / 2, height);
    ass_draw_stop(d);
    add_osd_ass_event(track, "progbar", bstr0(d->text));
    ass_draw_reset(d);

    d->text = talloc_asprintf_append(d->text, "{\\pos(%f,%f)}", px, py);
    ass_draw_start(d);

    // the box
    ass_draw_rect_cw(d, -border, -border, width + border, height + border);

    // the "hole"
    ass_draw_rect_ccw(d, 0, 0, width, height);

    struct osd_bar_style_opts *bar_opts = osd->opts->osd_bar_style;
    // chapter marks
    if (bar_opts->marker_style) {
        for (int n = 0; n < obj->progbar_state.num_stops; n++) {
            float s = obj->progbar_state.stops[n] * width;
            float size = MPMAX(border * bar_opts->marker_scale,
                               bar_opts->marker_min_size);

            if (bar_opts->marker_style == 2 &&
                s > size / 2 && s < width - size / 2)
            { // line
                ass_draw_rect_cw(d, s - size / 2, 0, s + size / 2, height);
            } else if (s > size && s < width - size) { //triangle
                ass_draw_move_to(d, s + size, 0);
                ass_draw_line_to(d, s,        size);
                ass_draw_line_to(d, s - size, 0);

                ass_draw_move_to(d, s - size, height);
                ass_draw_line_to(d, s,        height - size);
                ass_draw_line_to(d, s + size, height);
            }
        }
    }

    ass_draw_stop(d);
    add_osd_ass_event(track, "progbar", bstr0(d->text));
    ass_draw_reset(d);
}

static void update_osd(struct osd_state *osd, struct osd_object *obj)
{
    obj->osd_changed = false;
    clear_ass(&obj->ass);
    update_osd_text(osd, obj);
    update_progbar(osd, obj);
}

static void update_external(struct osd_state *osd, struct osd_object *obj,
                            struct osd_external *ext)
{
    bstr t = bstr0(ext->ov.data);
    ext->ass.res_x = ext->ov.res_x;
    ext->ass.res_y = ext->ov.res_y;
    create_ass_track(osd, obj, &ext->ass);

    clear_ass(&ext->ass);

    int resy = ext->ass.track->PlayResY;
    mp_ass_set_style(get_style(&ext->ass, "OSD"), resy, osd->opts->osd_style);

    // Some scripts will reference this style name with \r tags.
    const struct osd_style_opts *def = osd_style_conf.defaults;
    mp_ass_set_style(get_style(&ext->ass, "Default"), resy, def);

    while (t.len) {
        bstr line;
        bstr_split_tok(t, "\n", &line, &t);
        if (line.len)
            add_osd_ass_event(ext->ass.track, "OSD", line);
    }
}

static int cmp_zorder(const void *pa, const void *pb)
{
    const struct osd_external *a = *(struct osd_external **)pa;
    const struct osd_external *b = *(struct osd_external **)pb;
    return a->ov.z == b->ov.z ? 0 : (a->ov.z > b->ov.z ? 1 : -1);
}

void osd_set_external(struct osd_state *osd, struct osd_external_ass *ov)
{
    mp_mutex_lock(&osd->lock);
    struct osd_object *obj = osd->objs[OSDTYPE_EXTERNAL];
    bool zorder_changed = false;
    int index = -1;

    for (int n = 0; n < obj->num_externals; n++) {
        struct osd_external *e = obj->externals[n];
        if (e->ov.id == ov->id && e->ov.owner == ov->owner) {
            index = n;
            break;
        }
    }

    if (index < 0) {
        if (!ov->format)
            goto done;
        struct osd_external *new = talloc_zero(NULL, struct osd_external);
        new->ov.owner = ov->owner;
        new->ov.id = ov->id;
        MP_TARRAY_APPEND(obj, obj->externals, obj->num_externals, new);
        index = obj->num_externals - 1;
        zorder_changed = true;
    }

    struct osd_external *entry = obj->externals[index];

    if (!ov->format) {
        if (!entry->ov.hidden) {
            obj->changed = true;
            osd->want_redraw_notification = true;
        }
        destroy_external(entry);
        MP_TARRAY_REMOVE_AT(obj->externals, obj->num_externals, index);
        goto done;
    }

    if (!entry->ov.hidden || !ov->hidden) {
        obj->changed = true;
        osd->want_redraw_notification = true;
    }

    entry->ov.format = ov->format;
    if (!entry->ov.data)
        entry->ov.data = talloc_strdup(entry, "");
    entry->ov.data[0] = '\0'; // reuse memory allocation
    entry->ov.data = talloc_strdup_append(entry->ov.data, ov->data);
    entry->ov.res_x = ov->res_x;
    entry->ov.res_y = ov->res_y;
    zorder_changed |= entry->ov.z != ov->z;
    entry->ov.z = ov->z;
    entry->ov.hidden = ov->hidden;

    update_external(osd, obj, entry);

    if (zorder_changed) {
        qsort(obj->externals, obj->num_externals, sizeof(obj->externals[0]),
              cmp_zorder);
    }

    if (ov->out_rc) {
        struct mp_osd_res vo_res = entry->ass.vo_res;
        // Defined fallback if VO has not drawn this yet
        if (vo_res.w < 1 || vo_res.h < 1) {
            vo_res = (struct mp_osd_res){
                .w = entry->ov.res_x,
                .h = entry->ov.res_y,
                .display_par = 1,
            };
            // According to osd-overlay command description.
            if (vo_res.w < 1)
                vo_res.w = 1280;
            if (vo_res.h < 1)
                vo_res.h = 720;
        }

        ASS_Image *img_list = NULL;
        append_ass(&entry->ass, &vo_res, &img_list, NULL);

        mp_ass_get_bb(img_list, entry->ass.track, &vo_res, ov->out_rc);
    }

done:
    mp_mutex_unlock(&osd->lock);
}

void osd_set_external_remove_owner(struct osd_state *osd, void *owner)
{
    mp_mutex_lock(&osd->lock);
    struct osd_object *obj = osd->objs[OSDTYPE_EXTERNAL];
    for (int n = obj->num_externals - 1; n >= 0; n--) {
        struct osd_external *e = obj->externals[n];
        if (e->ov.owner == owner) {
            destroy_external(e);
            MP_TARRAY_REMOVE_AT(obj->externals, obj->num_externals, n);
            obj->changed = true;
            osd->want_redraw_notification = true;
        }
    }
    mp_mutex_unlock(&osd->lock);
}

static void append_ass(struct ass_state *ass, struct mp_osd_res *res,
                       ASS_Image **img_list, bool *changed)
{
    if (!ass->render || !ass->track) {
        *img_list = NULL;
        return;
    }

    update_playres(ass, res);

    ass_set_frame_size(ass->render, res->w, res->h);
    ass_set_pixel_aspect(ass->render, res->display_par);

    int ass_changed;
    *img_list = ass_render_frame(ass->render, ass->track, 0, &ass_changed);

    ass->changed |= ass_changed;

    if (changed) {
        *changed |= ass->changed;
        ass->changed = false;
    }
}

struct sub_bitmaps *osd_object_get_bitmaps(struct osd_state *osd,
                                           struct osd_object *obj, int format)
{
    if (obj->type == OSDTYPE_OSD) {
        if (obj->osd_changed) {
            update_osd(osd, obj);
        } else {
            mp_require(obj->ass_packer);
            goto done;
        }
    }

    if (!obj->ass_packer)
        obj->ass_packer = mp_ass_packer_alloc(obj);

    MP_TARRAY_GROW(obj, obj->ass_imgs, obj->num_externals + 1);

    append_ass(&obj->ass, &obj->vo_res, &obj->ass_imgs[0], &obj->changed);
    for (int n = 0; n < obj->num_externals; n++) {
        if (obj->externals[n]->ov.hidden) {
            update_playres(&obj->externals[n]->ass, &obj->vo_res);
            obj->ass_imgs[n + 1] = NULL;
        } else {
            append_ass(&obj->externals[n]->ass, &obj->vo_res,
                       &obj->ass_imgs[n + 1], &obj->changed);
        }
    }

done:;
    struct sub_bitmaps out_imgs = {0};
    mp_ass_packer_pack(obj->ass_packer, obj->ass_imgs, obj->num_externals + 1,
                       obj->changed, false, format, &out_imgs);

    obj->changed = false;

    return sub_bitmaps_copy(&obj->copy_cache, &out_imgs);
}
