/*
 * filter_qtext.cpp -- text overlay filter
 * Copyright (c) 2018-2026 Meltytech, LLC
 *
 * This library 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.
 *
 * This library 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 this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "common.h"
#include <framework/mlt.h>
#include <framework/mlt_log.h>
#include <QFile>
#include <QPainter>
#include <QPainterPath>
#include <QString>
#include <QTextDocument>
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include <QTextCodec>
#elif QT_VERSION < QT_VERSION_CHECK(6, 4, 0)
#include <QtCore5Compat/QTextCodec>
#else
#include <QStringDecoder>
#endif

#include "typewriter.h"
#include <string>
#include <QMutexLocker>

static QMutex g_mutex;

struct TypewriterFilterData
{
    TypeWriter typewriter;
    std::string original_text;
    int macro_type;
    mlt_position start_position;

    TypewriterFilterData()
        : macro_type(0)
        , start_position(0)
    {}
};

static void close_typewriter_filter_data(void *data)
{
    delete static_cast<TypewriterFilterData *>(data);
}

static std::string get_typewriter_text_for_filter(mlt_properties filter_properties,
                                                  mlt_properties state_props,
                                                  const char *original_text,
                                                  mlt_position position)
{
    if (!mlt_properties_get_int(filter_properties, "typewriter") || !original_text) {
        return original_text ? std::string(original_text) : std::string("");
    }

    auto *tw_data = static_cast<TypewriterFilterData *>(
        mlt_properties_get_data(state_props, "_typewriter_filter_data", NULL));

    if (!tw_data) {
        tw_data = new TypewriterFilterData();
        mlt_properties_set_data(state_props,
                                "_typewriter_filter_data",
                                static_cast<void *>(tw_data),
                                0,
                                close_typewriter_filter_data,
                                NULL);
    }

    auto text_str = std::string(original_text);
    unsigned int step_length = mlt_properties_get_int(filter_properties, "typewriter.step_length");
    unsigned int step_sigma = mlt_properties_get_int(filter_properties, "typewriter.step_sigma");
    unsigned int random_seed = mlt_properties_get_int(filter_properties, "typewriter.random_seed");
    int macro_type = mlt_properties_get_int(filter_properties, "typewriter.macro_type");

    bool text_changed = tw_data->original_text != text_str || tw_data->macro_type != macro_type;
    bool settings_changed = tw_data->typewriter.getFrameStep() != step_length
                            || tw_data->typewriter.getStepSigma() != step_sigma
                            || tw_data->typewriter.getStepSeed() != random_seed;

    // Check if text or typewriter settings changed
    if (text_changed || settings_changed) {
        // Only reset the start position when text content changes, not when
        // numeric settings (step_length, etc.) change, to avoid a negative
        // relative_position when the user adjusts settings and replays.
        if (text_changed) {
            tw_data->original_text = text_str;
            tw_data->macro_type = macro_type;
            tw_data->start_position = position;
        }
        tw_data->typewriter.setPattern(text_str);

        if (step_length == 0)
            step_length = 25;

        tw_data->typewriter.setFrameStep(step_length);
        tw_data->typewriter.setStepSigma(step_sigma);
        tw_data->typewriter.setStepSeed(random_seed);

        // Apply macro type if specified
        if (macro_type > 0) {
            char *buff = new char[text_str.length() + 10];
            char c = 0;
            switch (macro_type) {
            case 1:
                c = 'c';
                break; // character
            case 2:
                c = 'w';
                break; // word
            case 3:
                c = 'l';
                break; // line
            default:
                break;
            }
            if (c != 0) {
                sprintf(buff, ":%c{%s}", c, text_str.c_str());
                tw_data->typewriter.setPattern(std::string(buff));
            }
            delete[] buff;
        }

        tw_data->typewriter.parse();
    }

    mlt_position relative_position = std::max(position - tw_data->start_position, 0);
    std::string rendered_text = tw_data->typewriter.render(relative_position);

    // Add blinking cursor if enabled
    auto cursor_enabled = mlt_properties_get_int(filter_properties, "typewriter.cursor");
    if (cursor_enabled) {
        int cursor_blink_rate = mlt_properties_get_int(filter_properties,
                                                       "typewriter.cursor_blink_rate");
        if (cursor_blink_rate < 0)
            cursor_blink_rate = 25; // Default to 25 frames (1 second at 25fps)

        // Check if we should show cursor (blink on/off)
        bool show_cursor = (cursor_blink_rate == 0)
                           || (relative_position / cursor_blink_rate) % 2 == 0;

        // Only show cursor if text is still being typed or if we're at the end
        bool still_typing = !tw_data->typewriter.isEnd();

        if (still_typing || cursor_enabled == 2) { // 2 = always show cursor
            char *cursor_char = mlt_properties_get(filter_properties, "typewriter.cursor_char");
            if (!show_cursor) {
                cursor_char = (char *) " ";
            } else if (!cursor_char || strlen(cursor_char) == 0) {
                cursor_char = (char *) "|"; // Default cursor character
            }
            rendered_text += cursor_char;
        }
    }

    return rendered_text;
}

static QRectF get_text_path(QPainterPath *qpath,
                            mlt_properties filter_properties,
                            mlt_properties state_props,
                            const char *text,
                            double scale,
                            mlt_position position = 0)
{
    int outline = mlt_properties_get_int(filter_properties, "outline") * scale;
    char halign = mlt_properties_get(filter_properties, "halign")[0];
    const char *style = mlt_properties_get(filter_properties, "style");
    int pad = mlt_properties_get_int(filter_properties, "pad") * scale;
    int offset = pad + (outline / 2);
    int width = 0;
    int height = 0;

    qpath->setFillRule(Qt::WindingFill);

    // Get the strings to display (with typewriter effect if enabled)
    std::string processed_text
        = get_typewriter_text_for_filter(filter_properties, state_props, text, position);
    QString s = QString::fromUtf8(processed_text.c_str());
    QStringList lines = s.split("\n");

    // Configure the font
    QFont font;
    font.setPixelSize(mlt_properties_get_int(filter_properties, "size") * scale);
    font.setFamily(mlt_properties_get(filter_properties, "family"));
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
    font.setWeight(QFont::Weight((mlt_properties_get_int(filter_properties, "weight") / 10) - 1));
#else
    font.setWeight(QFont::Weight(mlt_properties_get_int(filter_properties, "weight")));
#endif
    switch (style[0]) {
    case 'i':
    case 'I':
        font.setStyle(QFont::StyleItalic);
        break;
    default:
        font.setStyleName(style);
    }
    mlt_log_debug(NULL,
                  "[filter_qtext] family %s style %s\n",
                  font.family().toUtf8().constData(),
                  style);
    // Apply text decoration properties
    font.setUnderline(mlt_properties_get_int(filter_properties, "underline"));
    font.setStrikeOut(mlt_properties_get_int(filter_properties, "strikethrough"));
    QFontMetrics fm(font);

    // Determine the text rectangle size
    height = fm.lineSpacing() * lines.size();
    for (int i = 0; i < lines.size(); ++i) {
        const QString line = lines[i];
        int line_width = fm.horizontalAdvance(line);
        int bearing = (line.size() > 0) ? fm.leftBearing(line.at(0)) : 0;
        if (bearing < 0)
            line_width -= bearing;
        bearing = (line.size() > 0) ? fm.rightBearing(line.at(line.size() - 1)) : 0;
        if (bearing < 0)
            line_width -= bearing;
        if (line_width > width)
            width = line_width;
    }

    // Lay out the text in the path
    int x = 0;
    int y = fm.ascent() + offset;
    for (int i = 0; i < lines.size(); ++i) {
        QString line = lines.at(i);
        x = offset;
        int line_width = fm.horizontalAdvance(line);
        int bearing = (line.size() > 0) ? fm.leftBearing(line.at(0)) : 0;

        if (bearing < 0) {
            line_width -= bearing;
            x -= bearing;
        }
        bearing = (line.size() > 0) ? fm.rightBearing(line.at(line.size() - 1)) : 0;
        if (bearing < 0)
            line_width -= bearing;

        switch (halign) {
        default:
        case 'l':
        case 'L':
            break;
        case 'c':
        case 'C':
            x += (width - line_width) / 2;
            break;
        case 'r':
        case 'R':
            x += width - line_width;
            break;
        }
        qpath->addText(x, y, font, line);
        y += fm.lineSpacing();
    }

    // Account for outline and pad
    width += offset * 2;
    height += offset * 2;
    // Sanity check
    if (width == 0)
        width = 1;
    height += 2; // I found some fonts whose descenders get cut off.

    return QRectF(0, 0, width, height);
}

static QColor get_qcolor(mlt_properties filter_properties,
                         const char *name,
                         mlt_properties frame_properties,
                         int position,
                         int length)
{
    mlt_color color = mlt_properties_anim_get_color(filter_properties, name, position, length);
    color = mlt_color_convert_trc(color, mlt_properties_get(frame_properties, "color_trc"));
    return QColor(color.r, color.g, color.b, color.a);
}

static void transform_painter(QPainter *painter,
                              mlt_rect frame_rect,
                              QRectF path_rect,
                              mlt_properties filter_properties,
                              mlt_profile profile)
{
    qreal sx = 1.0;
    qreal sy = mlt_profile_sar(profile);
    qreal path_width = path_rect.width() * sx;
    if (path_width > frame_rect.w) {
        sx *= frame_rect.w / path_width;
        sy *= frame_rect.w / path_width;
    }
    qreal path_height = path_rect.height() * sy;
    if (path_height > frame_rect.h) {
        sx *= frame_rect.h / path_height;
        sy *= frame_rect.h / path_height;
    }

    qreal dx = frame_rect.x;
    qreal dy = frame_rect.y;
    char halign = mlt_properties_get(filter_properties, "halign")[0];
    switch (halign) {
    default:
    case 'l':
    case 'L':
        break;
    case 'c':
    case 'C':
        dx += (frame_rect.w - (sx * path_rect.width())) / 2;
        break;
    case 'r':
    case 'R':
        dx += frame_rect.w - (sx * path_rect.width());
        break;
    }
    char valign = mlt_properties_get(filter_properties, "valign")[0];
    switch (valign) {
    default:
    case 't':
    case 'T':
        break;
    case 'm':
    case 'M':
        dy += (frame_rect.h - (sy * path_rect.height())) / 2;
        break;
    case 'b':
    case 'B':
        dy += frame_rect.h - (sy * path_rect.height());
        break;
    }

    QTransform transform;
    transform.translate(dx, dy);
    transform.scale(sx, sy);
    painter->setTransform(transform);
}

static void paint_background(QPainter *painter,
                             QRectF path_rect,
                             mlt_properties filter_properties,
                             mlt_properties frame_properties,
                             int position,
                             int length)
{
    QColor bg_color = get_qcolor(filter_properties, "bgcolour", frame_properties, position, length);
    painter->fillRect(path_rect, bg_color);
}

static void paint_text(QPainter *painter,
                       QPainterPath *qpath,
                       mlt_properties filter_properties,
                       mlt_properties frame_properties,
                       int position,
                       int length)
{
    // Draw the outline first, and then draw the fill on top of it.
    // This avoids the outline encroaching on the text fill.

    // Draw the outline if requested
    int outline = mlt_properties_get_int(filter_properties, "outline");
    if (outline) {
        QPen pen;
        pen.setWidth(outline);
        QColor color = get_qcolor(filter_properties, "olcolour", frame_properties, position, length);
        pen.setColor(color);
        painter->setPen(pen);
        painter->setBrush(Qt::NoBrush); // No brush needed for outline
        painter->drawPath(*qpath);
    }

    // Fill the text area
    QColor color = get_qcolor(filter_properties, "fgcolour", frame_properties, position, length);
    QBrush brush(color);
    painter->setBrush(brush);
    painter->setPen(Qt::NoPen); // No pen needed for fill
    painter->drawPath(*qpath);
}

static void close_qtextdoc(void *p)
{
    delete static_cast<QTextDocument *>(p);
}

static QTextDocument *get_rich_text(mlt_properties properties, double width, double height)
{
    QTextDocument *doc = (QTextDocument *) mlt_properties_get_data(properties,
                                                                   "QTextDocument",
                                                                   NULL);
    auto html = QString::fromUtf8(mlt_properties_get(properties, "html"));
    auto prevHtml = QString::fromUtf8(mlt_properties_get(properties, "_html"));
    auto resource = QString::fromUtf8(mlt_properties_get(properties, "resource"));
    auto prevResource = QString::fromUtf8(mlt_properties_get(properties, "_resource"));
    auto prevWidth = mlt_properties_get_double(properties, "_width");
    auto prevHeight = mlt_properties_get_double(properties, "_height");
    bool changed = !doc || qAbs(width - prevWidth) > 1 || qAbs(height - prevHeight) > 1;

    if (!resource.isEmpty() && (changed || resource != prevResource)) {
        QFile file(resource);
        if (file.open(QFile::ReadOnly)) {
            QByteArray data = file.readAll();
            doc = new QTextDocument;
            doc->setPageSize(QSizeF(width, height));
#if QT_VERSION < QT_VERSION_CHECK(6, 4, 0)
            QTextCodec *codec = QTextCodec::codecForHtml(data);
            doc->setHtml(codec->toUnicode(data));
#else
            QStringDecoder decoder = QStringDecoder::decoderForHtml(data);
            doc->setHtml(decoder(data));
#endif
            mlt_properties_set_data(properties,
                                    "QTextDocument",
                                    doc,
                                    0,
                                    (mlt_destructor) close_qtextdoc,
                                    NULL);
            mlt_properties_set(properties, "_resource", resource.toUtf8().constData());
            mlt_properties_set_double(properties, "_width", width);
            mlt_properties_set_double(properties, "_height", height);
        }
    } else if (!html.isEmpty() && (changed || html != prevHtml)) {
        //		fprintf(stderr, "%s\n", html.toUtf8().constData());
        doc = new QTextDocument;
        doc->setPageSize(QSizeF(width, height));
        doc->setHtml(html);
        mlt_properties_set_data(properties,
                                "QTextDocument",
                                doc,
                                0,
                                (mlt_destructor) close_qtextdoc,
                                NULL);
        mlt_properties_set(properties, "_html", html.toUtf8().constData());
        mlt_properties_set_double(properties, "_width", width);
        mlt_properties_set_double(properties, "_height", height);
    }

    return doc;
}

static mlt_properties get_filter_properties(mlt_filter filter, mlt_frame frame)
{
    mlt_properties properties = mlt_frame_get_unique_properties(frame, MLT_FILTER_SERVICE(filter));
    if (!properties)
        properties = MLT_FILTER_PROPERTIES(filter);
    return properties;
}

static int filter_get_image(mlt_frame frame,
                            uint8_t **image,
                            mlt_image_format *image_format,
                            int *width,
                            int *height,
                            int writable)
{
    int error = 0;
    mlt_filter filter = (mlt_filter) mlt_frame_pop_service(frame);
    char *argument = (char *) mlt_frame_pop_service(frame);
    mlt_properties filter_properties = get_filter_properties(filter, frame);
    mlt_properties frame_properties = MLT_FRAME_PROPERTIES(frame);
    mlt_profile profile = mlt_service_profile(MLT_FILTER_SERVICE(filter));
    mlt_position position = mlt_filter_get_position(filter, frame);
    mlt_position length = mlt_filter_get_length2(filter, frame);
    bool isRichText = qstrlen(mlt_properties_get(filter_properties, "html")) > 0
                      || qstrlen(mlt_properties_get(filter_properties, "resource")) > 0;
    double opacity = 1.0;
    if (mlt_properties_exists(filter_properties, "opacity")) {
        opacity = mlt_properties_anim_get_double(filter_properties, "opacity", position, length);
    }
    if (opacity == 0.0) {
        free(argument);
        return mlt_frame_get_image(frame, image, image_format, width, height, writable);
    }
    QString geom_str = QString::fromLatin1(mlt_properties_get(filter_properties, "geometry"));
    if (geom_str.isEmpty()) {
        free(argument);
        mlt_log_warning(MLT_FILTER_SERVICE(filter), "geometry property not set\n");
        return mlt_frame_get_image(frame, image, image_format, width, height, writable);
    }
    mlt_rect rect = mlt_properties_anim_get_rect(filter_properties, "geometry", position, length);

    // Get the current image
    *image_format = choose_image_format(*image_format);
    mlt_properties_set_int(frame_properties, "resize_alpha", 255);
    mlt_service_lock(MLT_FILTER_SERVICE(filter));
    error = mlt_frame_get_image(frame, image, image_format, width, height, writable);

    if (!error) {
        double scale = mlt_profile_scale_width(profile, *width);
        double scale_height = mlt_profile_scale_height(profile, *height);
        if (geom_str.contains('%')) {
            rect.x *= *width;
            rect.w *= *width;
            rect.y *= *height;
            rect.h *= *height;
        } else {
            rect.x *= scale;
            rect.y *= scale_height;
            rect.w *= scale;
            rect.h *= scale_height;
        }

        QImage qimg;
        convert_mlt_to_qimage(*image, &qimg, *width, *height, *image_format);

        QPainterPath text_path;
#ifdef Q_OS_WIN
        auto pixel_ratio = mlt_properties_get_double(filter_properties, "pixel_ratio");
#else
        auto pixel_ratio = 1.0;
#endif
        QRectF path_rect(0, 0, rect.w / scale * pixel_ratio, rect.h / scale_height * pixel_ratio);
        QPainter painter(&qimg);
        painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
                               | QPainter::HighQualityAntialiasing
#endif
        );
        painter.setOpacity(opacity);
        if (isRichText) {
            auto overflowY = mlt_properties_exists(filter_properties, "overflow-y")
                                 ? !!mlt_properties_get_int(filter_properties, "overflow-y")
                                 : (path_rect.height() >= profile->height * pixel_ratio);
            auto drawRect = overflowY ? QRectF() : path_rect;
            QMutexLocker mutexLock(&g_mutex);
            auto doc = get_rich_text(filter_properties,
                                     path_rect.width(),
                                     std::numeric_limits<qreal>::max());
            if (doc) {
                transform_painter(&painter, rect, path_rect, filter_properties, profile);
                if (overflowY) {
                    path_rect.setHeight(qMax(path_rect.height(), doc->size().height()));
                }
                paint_background(&painter,
                                 path_rect,
                                 filter_properties,
                                 frame_properties,
                                 position,
                                 length);
                doc->drawContents(&painter, drawRect);
            }
        } else {
            mlt_properties state_props = MLT_FILTER_PROPERTIES(filter);
            path_rect = get_text_path(&text_path,
                                      filter_properties,
                                      state_props,
                                      argument,
                                      scale,
                                      position);
            transform_painter(&painter, rect, path_rect, filter_properties, profile);
            paint_background(&painter,
                             path_rect,
                             filter_properties,
                             frame_properties,
                             position,
                             length);
            paint_text(&painter, &text_path, filter_properties, frame_properties, position, length);
        }
        painter.end();

        convert_qimage_to_mlt(&qimg, *image, *width, *height);
    }
    mlt_service_unlock(MLT_FILTER_SERVICE(filter));
    free(argument);

    return error;
}

/** Filter processing.
*/

static mlt_frame filter_process(mlt_filter filter, mlt_frame frame)
{
    mlt_properties properties = get_filter_properties(filter, frame);

    if (mlt_properties_get_int(properties, "_hide")) {
        return frame;
    }

    char *argument = mlt_properties_get(properties, "argument");
    char *html = mlt_properties_get(properties, "html");
    char *resource = mlt_properties_get(properties, "resource");

    // Save the text to be used by get_image() to support parallel processing
    // when this filter is encapsulated by other filters.
    if (qstrlen(resource)) {
        mlt_frame_push_service(frame, NULL);
    } else if (qstrlen(html)) {
        mlt_frame_push_service(frame, NULL);
    } else if (qstrlen(argument)) {
        mlt_frame_push_service(frame, strdup(argument));
    } else {
        return frame;
    }

    // Push the filter on to the stack
    mlt_frame_push_service(frame, filter);

    // Push the get_image on to the stack
    mlt_frame_push_get_image(frame, filter_get_image);

    return frame;
}

/** Constructor for the filter.
*/

extern "C" {

mlt_filter filter_qtext_init(mlt_profile profile, mlt_service_type type, const char *id, char *arg)
{
    mlt_filter filter = mlt_filter_new();

    if (!filter)
        return NULL;

    if (!createQApplicationIfNeeded(MLT_FILTER_SERVICE(filter))) {
        mlt_filter_close(filter);
        return NULL;
    }

    filter->process = filter_process;

    mlt_properties filter_properties = MLT_FILTER_PROPERTIES(filter);
    // Assign default values
    mlt_properties_set_string(filter_properties, "argument", arg ? arg : "text");
    mlt_properties_set_string(filter_properties, "geometry", "0%/0%:100%x100%:100%");
    mlt_properties_set_string(filter_properties, "family", "Sans");
    mlt_properties_set_string(filter_properties, "size", "48");
    mlt_properties_set_string(filter_properties, "weight", "400");
    mlt_properties_set_string(filter_properties, "style", "normal");
    mlt_properties_set_string(filter_properties, "fgcolour", "0x000000ff");
    mlt_properties_set_string(filter_properties, "bgcolour", "0x00000020");
    mlt_properties_set_string(filter_properties, "olcolour", "0x00000000");
    mlt_properties_set_string(filter_properties, "pad", "0");
    mlt_properties_set_string(filter_properties, "halign", "left");
    mlt_properties_set_string(filter_properties, "valign", "top");
    mlt_properties_set_string(filter_properties, "outline", "0");
    mlt_properties_set_string(filter_properties, "underline", "0");
    mlt_properties_set_string(filter_properties, "strikethrough", "0");
    mlt_properties_set_double(filter_properties, "pixel_ratio", 1.0);
    mlt_properties_set_double(filter_properties, "opacity", 1.0);
    mlt_properties_set_int(filter_properties, "_filter_private", 1);

    // Initialize typewriter properties
    mlt_properties_set_int(filter_properties, "typewriter", 0);
    mlt_properties_set_int(filter_properties, "typewriter.step_length", 25);
    mlt_properties_set_int(filter_properties, "typewriter.step_sigma", 0);
    mlt_properties_set_int(filter_properties, "typewriter.random_seed", 0);
    mlt_properties_set_int(filter_properties, "typewriter.macro_type", 1);
    mlt_properties_set_int(filter_properties, "typewriter.cursor", 1);
    mlt_properties_set_int(filter_properties, "typewriter.cursor_blink_rate", 25);
    mlt_properties_set_string(filter_properties, "typewriter.cursor_char", "|");

    return filter;
}
}
