/*
 * transition_luma.c -- a generic dissolve/wipe processor
 * Copyright (C) 2003-2025 Meltytech, LLC
 *
 * Adapted from Kino Plugin Timfx, which is
 * Copyright (C) 2002 Timothy M. Shead <tshead@k-3d.com>
 *
 * 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 <framework/mlt.h>
#include <framework/mlt_luma_map.h>

#include "transition_composite.h"
#include <ctype.h>
#include <limits.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static inline int is_opaque(uint8_t *alpha_channel, int width, int height)
{
    int n = width * height + 1;
    while (--n)
        if (*alpha_channel++ != 0xff)
            return 0;
    return 1;
}

static inline float calculate_mix(float weight, float alpha)
{
    return weight * alpha / 255.f;
}

static inline uint8_t sample_mix(uint8_t dest, uint8_t src, float mix)
{
    return src * mix + dest * (1.f - mix);
}

static inline float calculate_mix_16(float weight, float alpha)
{
    return weight * alpha / 65535.f;
}

static inline uint16_t sample_mix_16(uint16_t dest, uint16_t src, float mix)
{
    return src * mix + dest * (1.f - mix);
}

static void composite_line_yuv_float(
    uint8_t *dest, uint8_t *src, int width, uint8_t *alpha_b, uint8_t *alpha_a, float weight)
{
    register int j = 0;
    float mix_a, mix_b;

    for (; j < width; j++) {
        mix_a = calculate_mix(1.0f - weight, alpha_a ? *alpha_a : 255);
        mix_b = calculate_mix(weight, alpha_b ? *alpha_b : 255);
        if (alpha_a) {
            float mix2 = mix_b + mix_a;
            *alpha_a = 255 * mix2;
            if (mix2 != 0.f)
                mix_b /= mix2;
        }
        *dest = sample_mix(*dest, *src++, mix_b);
        dest++;
        *dest = sample_mix(*dest, *src++, mix_b);
        dest++;
        if (alpha_a)
            alpha_a++;
        if (alpha_b)
            alpha_b++;
    }
}

struct dissolve_slice_context
{
    uint8_t *dst_image;
    uint8_t *src_image;
    uint8_t *dst_alpha;
    uint8_t *src_alpha;
    int width;
    int height;
    float weight;
};

static int dissolve_slice_yuv422(int id, int index, int count, void *context)
{
    struct dissolve_slice_context ctx = *((struct dissolve_slice_context *) context);
    int stride = ctx.width * 2;
    int slice_start, slice_height = mlt_slices_size_slice(count, index, ctx.height, &slice_start);
    int i;

    ctx.dst_image += slice_start * stride;
    ctx.src_image += slice_start * stride;
    if (ctx.dst_alpha)
        ctx.dst_alpha += slice_start * ctx.width;
    if (ctx.src_alpha)
        ctx.src_alpha += slice_start * ctx.width;

    for (i = 0; i < slice_height; i++) {
        composite_line_yuv_float(ctx.dst_image,
                                 ctx.src_image,
                                 ctx.width,
                                 ctx.src_alpha,
                                 ctx.dst_alpha,
                                 ctx.weight);
        ctx.dst_image += stride;
        ctx.src_image += stride;
        if (ctx.dst_alpha)
            ctx.dst_alpha += ctx.width;
        if (ctx.src_alpha)
            ctx.src_alpha += ctx.width;
    }
    return 0;
}

static inline int dissolve_yuv422(mlt_frame frame,
                                  mlt_frame that,
                                  float weight,
                                  int width,
                                  int height,
                                  int threads,
                                  int alpha_over,
                                  int fix_background_alpha)
{
    int ret = 0;
    int i = height + 1;
    int width_src = width, height_src = height;
    mlt_image_format format = (fix_background_alpha && frame->convert_image) ? mlt_image_rgba
                                                                             : mlt_image_yuv422;
    uint8_t *p_src, *p_dest;
    uint8_t *alpha_src;
    uint8_t *alpha_dst;
    int mix = weight * (1 << 16);

    mlt_frame_get_image(frame, &p_dest, &format, &width, &height, 1);
    if (fix_background_alpha && frame->convert_image)
        frame->convert_image(frame, &p_dest, &format, mlt_image_yuv422);
    alpha_dst = mlt_frame_get_alpha(frame);
    if (fix_background_alpha && that->convert_image)
        format = mlt_image_rgba;
    mlt_frame_get_image(that, &p_src, &format, &width_src, &height_src, 0);
    if (that->convert_image)
        that->convert_image(that, &p_src, &format, mlt_image_yuv422);
    alpha_src = mlt_frame_get_alpha(that);
    int is_translucent = (alpha_dst && !is_opaque(alpha_dst, width, height))
                         || (alpha_src && !is_opaque(alpha_src, width_src, height_src));

    // Pick the lesser of two evils ;-)
    width_src = width_src > width ? width : width_src;
    height_src = height_src > height ? height : height_src;

    if (is_translucent && alpha_over) {
        struct dissolve_slice_context context = {.dst_image = p_dest,
                                                 .src_image = p_src,
                                                 .dst_alpha = alpha_dst,
                                                 .src_alpha = alpha_src,
                                                 .width = width_src,
                                                 .height = height_src,
                                                 .weight = weight};
        mlt_slices_run_normal(threads, dissolve_slice_yuv422, &context);
    } else {
        while (--i) {
            composite_line_yuv(p_dest, p_src, width_src, alpha_src, alpha_dst, mix, NULL, 0, 0);
            p_src += width_src << 1;
            p_dest += width << 1;
            if (alpha_src)
                alpha_src += width_src;
            if (alpha_dst)
                alpha_dst += width;
        }
    }

    return ret;
}

static int dissolve_slice_rgba32(int id, int index, int count, void *context)
{
    struct dissolve_slice_context ctx = *((struct dissolve_slice_context *) context);
    int slice_start, slice_height = mlt_slices_size_slice(count, index, ctx.height, &slice_start);
    int stride = ctx.width * 4;
    uint8_t *dst = ctx.dst_image + slice_start * stride;
    uint8_t *src = ctx.src_image + slice_start * stride;
    for (int i = 0; i < slice_height; i++) {
        for (int j = 0; j < ctx.width; j++) {
            float mix_a = calculate_mix(1.0f - ctx.weight, dst[3]);
            float mix_b = calculate_mix(ctx.weight, src[3]);
            float mix2 = mix_b + mix_a;
            if (mix2 != 0.f)
                mix_b /= mix2;
            dst[0] = sample_mix(dst[0], src[0], mix_b);
            dst[1] = sample_mix(dst[1], src[1], mix_b);
            dst[2] = sample_mix(dst[2], src[2], mix_b);
            dst[3] = 255 * mix2;
            dst += 4;
            src += 4;
        }
    }
    return 0;
}

static inline int dissolve_rgba32(
    mlt_frame frame, mlt_frame that, float weight, int width, int height, int threads)
{
    struct mlt_image_s dimg;
    struct mlt_image_s simg;
    mlt_image_set_values(&dimg, NULL, mlt_image_rgba, width, height);
    mlt_frame_get_image(frame, (uint8_t **) &dimg.data, &dimg.format, &dimg.width, &dimg.height, 1);
    mlt_image_set_values(&dimg, dimg.data, dimg.format, dimg.width, dimg.height);
    mlt_image_set_values(&simg, NULL, mlt_image_rgba, width, height);
    mlt_frame_get_image(that, (uint8_t **) &simg.data, &simg.format, &simg.width, &simg.height, 0);
    mlt_image_set_values(&simg, simg.data, simg.format, simg.width, simg.height);

    if (simg.width != dimg.width || simg.height != dimg.height) {
        return 1;
    }

    struct dissolve_slice_context context = {.dst_image = dimg.data,
                                             .src_image = simg.data,
                                             .width = dimg.width,
                                             .height = dimg.height,
                                             .weight = weight};
    mlt_slices_run_normal(threads, dissolve_slice_rgba32, &context);

    return 0;
}

static int dissolve_slice_rgba64(int id, int index, int count, void *context)
{
    struct dissolve_slice_context ctx = *((struct dissolve_slice_context *) context);
    int slice_start, slice_height = mlt_slices_size_slice(count, index, ctx.height, &slice_start);
    int stride = ctx.width * 4;
    uint16_t *dst = (uint16_t *) ctx.dst_image + slice_start * stride;
    uint16_t *src = (uint16_t *) ctx.src_image + slice_start * stride;
    for (int i = 0; i < slice_height; i++) {
        for (int j = 0; j < ctx.width; j++) {
            float mix_a = calculate_mix_16(1.0f - ctx.weight, dst[3]);
            float mix_b = calculate_mix_16(ctx.weight, src[3]);
            float mix2 = mix_b + mix_a;
            if (mix2 != 0.f)
                mix_b /= mix2;
            dst[0] = sample_mix_16(dst[0], src[0], mix_b);
            dst[1] = sample_mix_16(dst[1], src[1], mix_b);
            dst[2] = sample_mix_16(dst[2], src[2], mix_b);
            dst[3] = 255 * mix2;
            dst += 4;
            src += 4;
        }
    }
    return 0;
}

static inline int dissolve_rgba64(
    mlt_frame frame, mlt_frame that, float weight, int width, int height, int threads)
{
    struct mlt_image_s dimg;
    struct mlt_image_s simg;
    mlt_image_set_values(&dimg, NULL, mlt_image_rgba64, width, height);
    mlt_frame_get_image(frame, (uint8_t **) &dimg.data, &dimg.format, &dimg.width, &dimg.height, 1);
    mlt_image_set_values(&dimg, dimg.data, dimg.format, dimg.width, dimg.height);
    mlt_image_set_values(&simg, NULL, mlt_image_rgba64, width, height);
    mlt_frame_get_image(that, (uint8_t **) &simg.data, &simg.format, &simg.width, &simg.height, 0);
    mlt_image_set_values(&simg, simg.data, simg.format, simg.width, simg.height);

    if (simg.width != dimg.width || simg.height != dimg.height) {
        return 1;
    }

    struct dissolve_slice_context context = {.dst_image = dimg.data,
                                             .src_image = simg.data,
                                             .width = dimg.width,
                                             .height = dimg.height,
                                             .weight = weight};
    mlt_slices_run_normal(threads, dissolve_slice_rgba64, &context);

    return 0;
}

/** A smoother, non-linear threshold determination function.
*/

static inline int32_t smoothstep(int32_t edge1, int32_t edge2, uint32_t a)
{
    if (a < edge1)
        return 0;

    if (a >= edge2)
        return 0x10000;

    a = ((a - edge1) << 16) / (edge2 - edge1);

    return (((a * a) >> 16) * ((3 << 16) - (2 * a))) >> 16;
}

static float smoothstep_float(float edge1, float edge2, float a)
{
    if (a < edge1)
        return 0.f;

    if (a >= edge2)
        return 1.f;

    a = (a - edge1) / (edge2 - edge1);

    return (a * a) * (3 - (2 * a));
}

/** powerful stuff

    \param field_order -1 = progressive, 0 = lower field first, 1 = top field first
*/
static void luma_composite_yuv422(mlt_frame a_frame,
                                  mlt_frame b_frame,
                                  int luma_width,
                                  int luma_height,
                                  uint16_t *luma_bitmap,
                                  float pos,
                                  float frame_delta,
                                  float softness,
                                  int field_order,
                                  int *width,
                                  int *height,
                                  int invert,
                                  int fix_background_alpha)
{
    int width_src = *width, height_src = *height;
    int width_dest = *width, height_dest = *height;
    mlt_image_format format_src = (fix_background_alpha && a_frame->convert_image)
                                      ? mlt_image_rgba
                                      : mlt_image_yuv422;
    mlt_image_format format_dest = (fix_background_alpha && b_frame->convert_image)
                                       ? mlt_image_rgba
                                       : mlt_image_yuv422;
    uint8_t *p_src, *p_dest;
    uint8_t *alpha_src, *alpha_dest;
    int i, j;
    int stride_src;
    int stride_dest;

    if (mlt_properties_get(&a_frame->parent, "distort"))
        mlt_properties_set(&b_frame->parent,
                           "distort",
                           mlt_properties_get(&a_frame->parent, "distort"));
    mlt_frame_get_image(a_frame, &p_dest, &format_dest, &width_dest, &height_dest, 1);
    if (fix_background_alpha && a_frame->convert_image)
        a_frame->convert_image(a_frame, &p_dest, &format_dest, mlt_image_yuv422);
    alpha_dest = mlt_frame_get_alpha(a_frame);
    mlt_frame_get_image(b_frame, &p_src, &format_src, &width_src, &height_src, 0);
    if (fix_background_alpha && b_frame->convert_image)
        b_frame->convert_image(b_frame, &p_src, &format_src, mlt_image_yuv422);
    alpha_src = mlt_frame_get_alpha(b_frame);

    if (*width == 0 || *height == 0)
        return;

    int is_translucent = (alpha_dest && !is_opaque(alpha_dest, width_dest, height_dest))
                         || (alpha_src && !is_opaque(alpha_src, width_src, height_src));

    // Pick the lesser of two evils ;-)
    width_src = width_src > width_dest ? width_dest : width_src;
    height_src = height_src > height_dest ? height_dest : height_src;

    stride_src = width_src * 2;
    stride_dest = width_dest * 2;

    // Offset the position based on which field we're looking at ...
    float field_pos[2];
    field_pos[0] = (pos + ((field_order == 0 ? 1 : 0) * frame_delta * 0.5f)) * (1.f + softness);
    field_pos[1] = (pos + ((field_order == 0 ? 0 : 1) * frame_delta * 0.5f)) * (1.f + softness);

    register uint8_t *p;
    register uint8_t *q;
    uint16_t *l;
    int32_t x_diff = (luma_width << 16) / *width;
    int32_t y_diff = (luma_height << 16) / *height;
    int32_t x_offset = 0;
    int32_t y_offset = 0;
    uint8_t *p_row;
    uint8_t *q_row;
    uint32_t i_softness = softness * (1 << 16);
    int field_count = field_order < 0 ? 1 : 2;
    int field_stride_src = field_count * stride_src;
    int field_stride_dest = field_count * stride_dest;
    int field = 0;
    float mix_a, mix_b;

    // composite using luma map
    while (field < field_count) {
        p_row = p_src + field * stride_src;
        q_row = p_dest + field * stride_dest;
        y_offset = field << 16;
        i = field;

        while (i < height_src) {
            p = p_row;
            q = q_row;
            l = luma_bitmap + (y_offset >> 16) * (luma_width * field_count);
            x_offset = 0;
            j = width_src;

            if (is_translucent) {
                while (j--) {
                    float weight = l[x_offset >> 16] / 65535.f;
                    float value = smoothstep_float(weight, softness + weight, field_pos[field]);
                    mix_a = calculate_mix(1.0f - value, alpha_dest ? *alpha_dest : 255);
                    mix_b = calculate_mix(value, alpha_src ? *alpha_src : 255);
                    if (invert && alpha_src) {
                        float mix2 = mix_b + mix_a;
                        *alpha_src = 255 * mix2;
                        if (mix2 != 0.f)
                            mix_b /= mix2;
                    } else if (!invert && alpha_dest) {
                        float mix2 = mix_b + mix_a;
                        *alpha_dest = 255 * mix2;
                        if (mix2 != 0.f)
                            mix_b /= mix2;
                    }
                    *q = sample_mix(*q, *p++, mix_b);
                    q++;
                    *q = sample_mix(*q, *p++, mix_b);
                    q++;
                    if (alpha_dest)
                        alpha_dest++;
                    if (alpha_src)
                        alpha_src++;
                    x_offset += x_diff;
                }
            } else {
                while (j--) {
                    uint16_t weight = l[x_offset >> 16];
                    uint32_t value = smoothstep(weight,
                                                i_softness + weight,
                                                (1 << 16) * field_pos[field]);
                    *q = (*p++ * value + *q * ((1 << 16) - value)) >> 16;
                    q++;
                    *q = (*p++ * value + *q * ((1 << 16) - value)) >> 16;
                    q++;
                    x_offset += x_diff;
                }
            }
            y_offset += y_diff;
            i += field_count;
            p_row += field_stride_src;
            q_row += field_stride_dest;
        }

        field++;
    }
}

static int luma_composite_rgba32(mlt_frame a_frame,
                                 mlt_frame b_frame,
                                 int luma_width,
                                 int luma_height,
                                 uint16_t *luma_bitmap,
                                 float pos,
                                 float frame_delta,
                                 float softness,
                                 int field_order,
                                 int width,
                                 int height,
                                 int invert)
{
    if (mlt_properties_get(&a_frame->parent, "distort"))
        mlt_properties_set(&b_frame->parent,
                           "distort",
                           mlt_properties_get(&a_frame->parent, "distort"));
    struct mlt_image_s dimg;
    struct mlt_image_s simg;
    mlt_image_set_values(&dimg, NULL, mlt_image_rgba, width, height);
    mlt_frame_get_image(a_frame, (uint8_t **) &dimg.data, &dimg.format, &dimg.width, &dimg.height, 1);
    mlt_image_set_values(&dimg, dimg.data, dimg.format, dimg.width, dimg.height);
    mlt_image_set_values(&simg, NULL, mlt_image_rgba, width, height);
    mlt_frame_get_image(b_frame, (uint8_t **) &simg.data, &simg.format, &simg.width, &simg.height, 0);
    mlt_image_set_values(&simg, simg.data, simg.format, simg.width, simg.height);

    if (simg.width != dimg.width || simg.height != dimg.height || simg.width == 0 || dimg.width == 0
        || simg.height == 0 || dimg.height == 0) {
        return 1;
    }

    int is_translucent = (!mlt_image_is_opaque(&dimg) || !mlt_image_is_opaque(&simg));

    // Offset the position based on which field we're looking at ...
    float field_pos[2];
    field_pos[0] = (pos + ((field_order == 0 ? 1 : 0) * frame_delta * 0.5f)) * (1.f + softness);
    field_pos[1] = (pos + ((field_order == 0 ? 0 : 1) * frame_delta * 0.5f)) * (1.f + softness);

    register uint8_t *p;
    register uint8_t *q;
    uint16_t *l;
    int32_t x_diff = (luma_width << 16) / dimg.width;
    int32_t y_diff = (luma_height << 16) / dimg.height;
    int32_t x_offset = 0;
    int32_t y_offset = 0;
    uint8_t *p_row;
    uint8_t *q_row;
    uint32_t i_softness = softness * (1 << 16);
    int field_count = field_order < 0 ? 1 : 2;
    int field_stride_src = field_count * simg.strides[0];
    int field_stride_dest = field_count * dimg.strides[0];
    int field = 0;
    float mix_a, mix_b;

    // composite using luma map
    while (field < field_count) {
        p_row = simg.planes[0] + field * simg.strides[0];
        q_row = dimg.planes[0] + field * dimg.strides[0];
        y_offset = field << 16;
        int i = field;

        while (i < simg.height) {
            p = p_row;
            q = q_row;
            l = luma_bitmap + (y_offset >> 16) * (luma_width * field_count);
            x_offset = 0;
            int j = simg.width;

            if (is_translucent) {
                while (j--) {
                    float weight = l[x_offset >> 16] / 65535.f;
                    float value = smoothstep_float(weight, softness + weight, field_pos[field]);
                    mix_a = calculate_mix(1.0f - value, q[3]);
                    mix_b = calculate_mix(value, p[3]);
                    if (invert) {
                        float mix2 = mix_b + mix_a;
                        q[3] = 255 * mix2;
                        if (mix2 != 0.f)
                            mix_b /= mix2;
                    } else {
                        float mix2 = mix_b + mix_a;
                        q[3] = 255 * mix2;
                        if (mix2 != 0.f)
                            mix_b /= mix2;
                    }
                    q[0] = sample_mix(q[0], p[0], mix_b);
                    q[1] = sample_mix(q[1], p[1], mix_b);
                    q[2] = sample_mix(q[2], p[2], mix_b);
                    p += 4;
                    q += 4;
                    x_offset += x_diff;
                }
            } else {
                while (j--) {
                    uint16_t weight = l[x_offset >> 16];
                    uint32_t value = smoothstep(weight,
                                                i_softness + weight,
                                                (1 << 16) * field_pos[field]);
                    q[0] = (p[0] * value + q[0] * ((1 << 16) - value)) >> 16;
                    q[1] = (p[1] * value + q[1] * ((1 << 16) - value)) >> 16;
                    q[2] = (p[2] * value + q[2] * ((1 << 16) - value)) >> 16;
                    p += 4;
                    q += 4;
                    x_offset += x_diff;
                }
            }
            y_offset += y_diff;
            i += field_count;
            p_row += field_stride_src;
            q_row += field_stride_dest;
        }

        field++;
    }
    return 0;
}

static int luma_composite_rgba64(mlt_frame a_frame,
                                 mlt_frame b_frame,
                                 int luma_width,
                                 int luma_height,
                                 uint16_t *luma_bitmap,
                                 float pos,
                                 float frame_delta,
                                 float softness,
                                 int field_order,
                                 int width,
                                 int height,
                                 int invert)
{
    if (mlt_properties_get(&a_frame->parent, "distort"))
        mlt_properties_set(&b_frame->parent,
                           "distort",
                           mlt_properties_get(&a_frame->parent, "distort"));
    struct mlt_image_s dimg;
    struct mlt_image_s simg;
    mlt_image_set_values(&dimg, NULL, mlt_image_rgba64, width, height);
    mlt_frame_get_image(a_frame, (uint8_t **) &dimg.data, &dimg.format, &dimg.width, &dimg.height, 1);
    mlt_image_set_values(&dimg, dimg.data, dimg.format, dimg.width, dimg.height);
    mlt_image_set_values(&simg, NULL, mlt_image_rgba64, width, height);
    mlt_frame_get_image(b_frame, (uint8_t **) &simg.data, &simg.format, &simg.width, &simg.height, 0);
    mlt_image_set_values(&simg, simg.data, simg.format, simg.width, simg.height);

    if (simg.width != dimg.width || simg.height != dimg.height || simg.width == 0 || dimg.width == 0
        || simg.height == 0 || dimg.height == 0) {
        return 1;
    }

    int is_translucent = (!mlt_image_is_opaque(&dimg) || !mlt_image_is_opaque(&simg));

    // Offset the position based on which field we're looking at ...
    float field_pos[2];
    field_pos[0] = (pos + ((field_order == 0 ? 1 : 0) * frame_delta * 0.5f)) * (1.f + softness);
    field_pos[1] = (pos + ((field_order == 0 ? 0 : 1) * frame_delta * 0.5f)) * (1.f + softness);

    register uint16_t *p;
    register uint16_t *q;
    uint16_t *l;
    int32_t x_diff = (luma_width << 16) / dimg.width;
    int32_t y_diff = (luma_height << 16) / dimg.height;
    int32_t x_offset = 0;
    int32_t y_offset = 0;
    uint16_t *p_row;
    uint16_t *q_row;
    uint32_t i_softness = softness * (1 << 16);
    int field_count = field_order < 0 ? 1 : 2;
    int field_stride_src = field_count * simg.strides[0] / 2;
    int field_stride_dest = field_count * dimg.strides[0] / 2;
    int field = 0;
    float mix_a, mix_b;

    // composite using luma map
    while (field < field_count) {
        p_row = (uint16_t *) simg.planes[0] + field * simg.strides[0] / 2;
        q_row = (uint16_t *) dimg.planes[0] + field * dimg.strides[0] / 2;
        y_offset = field << 16;
        int i = field;

        while (i < simg.height) {
            p = p_row;
            q = q_row;
            l = luma_bitmap + (y_offset >> 16) * (luma_width * field_count);
            x_offset = 0;
            int j = simg.width;

            if (is_translucent) {
                while (j--) {
                    float weight = l[x_offset >> 16] / 65535.f;
                    float value = smoothstep_float(weight, softness + weight, field_pos[field]);
                    mix_a = calculate_mix_16(1.0f - value, q[3]);
                    mix_b = calculate_mix_16(value, p[3]);
                    if (invert) {
                        float mix2 = mix_b + mix_a;
                        q[3] = 255 * mix2;
                        if (mix2 != 0.f)
                            mix_b /= mix2;
                    } else {
                        float mix2 = mix_b + mix_a;
                        q[3] = 255 * mix2;
                        if (mix2 != 0.f)
                            mix_b /= mix2;
                    }
                    q[0] = sample_mix_16(q[0], p[0], mix_b);
                    q[1] = sample_mix_16(q[1], p[1], mix_b);
                    q[2] = sample_mix_16(q[2], p[2], mix_b);
                    p += 4;
                    q += 4;
                    x_offset += x_diff;
                }
            } else {
                while (j--) {
                    uint16_t weight = l[x_offset >> 16];
                    uint32_t value = smoothstep(weight,
                                                i_softness + weight,
                                                (1 << 16) * field_pos[field]);
                    q[0] = (p[0] * value + q[0] * ((1 << 16) - value)) >> 16;
                    q[1] = (p[1] * value + q[1] * ((1 << 16) - value)) >> 16;
                    q[2] = (p[2] * value + q[2] * ((1 << 16) - value)) >> 16;
                    p += 4;
                    q += 4;
                    x_offset += x_diff;
                }
            }
            y_offset += y_diff;
            i += field_count;
            p_row += field_stride_src;
            q_row += field_stride_dest;
        }

        field++;
    }
    return 0;
}

void yuv422_to_luma16(uint8_t *image, uint16_t **map, int width, int height, int full_range)
{
    // allocate the luma bitmap
    *map = (uint16_t *) mlt_pool_alloc(width * height * sizeof(uint16_t));
    if (!*map)
        return;

    int i;
    int n = width * height;
    uint8_t offset = full_range ? 0 : 16;
    uint8_t max = full_range ? 255 : 235 - offset;
    int factor = full_range ? 256 : 299; // 299 = 65535 / 219

    // process the image data into the luma bitmap
    for (i = 0; i < n; i++) {
        (*map)[i] = CLAMP(image[i << 1] - offset, 0, max) * factor;
    }
}

static int transition_get_image(mlt_frame a_frame,
                                uint8_t **image,
                                mlt_image_format *format,
                                int *width,
                                int *height,
                                int writable)
{
    // Get the b frame from the stack
    mlt_frame b_frame = mlt_frame_pop_frame(a_frame);

    // Get the transition object
    mlt_transition transition = mlt_frame_pop_service(a_frame);

    // Get the properties of the transition
    mlt_properties properties = MLT_TRANSITION_PROPERTIES(transition);

    // Get the properties of the a frame
    mlt_properties a_props = MLT_FRAME_PROPERTIES(a_frame);

    // Get the properties of the b frame
    mlt_properties b_props = MLT_FRAME_PROPERTIES(b_frame);

    mlt_service_lock(MLT_TRANSITION_SERVICE(transition));

    // The cached luma map information
    int luma_width = mlt_properties_get_int(properties, "width");
    int luma_height = mlt_properties_get_int(properties, "height");
    uint16_t *luma_bitmap = mlt_properties_get_data(properties, "bitmap", NULL);
    char *current_resource = mlt_properties_get(properties, "_resource");
    mlt_producer producer = mlt_properties_get_data(properties, "producer", NULL);

    // If the filename property changed, reload the map
    char *resource = mlt_properties_get(properties, "resource");

    // Correct width/height if not specified
    if (luma_width == 0 || luma_height == 0) {
        luma_width = *width;
        luma_height = *height;
    }

    if (resource && (producer || !current_resource || strcmp(resource, current_resource))) {
        char temp[PATH_MAX];
        char *extension = strrchr(resource, '.');
        char *orig_resource = resource;
        mlt_profile profile = mlt_service_profile(MLT_TRANSITION_SERVICE(transition));

        if (strchr(resource, '%')) {
            snprintf(temp,
                     sizeof(temp),
                     "%s/lumas/%s/%s",
                     mlt_environment("MLT_DATA"),
                     mlt_profile_lumas_dir(profile),
                     strchr(resource, '%') + 1);
            FILE *test = mlt_fopen(temp, "r");
            if (!test) {
                strcat(temp, ".png");
                test = mlt_fopen(temp, "r");
            }
            if (test) {
                fclose(test);
                resource = temp;
            }
            extension = strrchr(resource, '.');
        }

        // See if it is a PGM
        if (extension != NULL && strcmp(extension, ".pgm") == 0) {
            // Load from PGM
            luma_bitmap = NULL;
            if (mlt_luma_map_from_pgm(resource, &luma_bitmap, &luma_width, &luma_height)) {
                // Failed to read file; generate it.
                mlt_luma_map luma = mlt_luma_map_new(orig_resource);
                if (profile) {
                    luma->w = profile->width;
                    luma->h = profile->height;
                }
                luma_bitmap = mlt_luma_map_render(luma);
                luma_width = luma->w;
                luma_height = luma->h;
                free(luma);
            }

            // Set the transition properties
            mlt_properties_set_int(properties, "width", luma_width);
            mlt_properties_set_int(properties, "height", luma_height);
            mlt_properties_set(properties, "_resource", orig_resource);
            mlt_properties_set_data(properties,
                                    "bitmap",
                                    luma_bitmap,
                                    luma_width * luma_height * 2,
                                    mlt_pool_release,
                                    NULL);
            mlt_properties_clear(properties, "producer");
        } else if (!*resource) {
            luma_bitmap = NULL;
            mlt_properties_set(properties, "_resource", NULL);
            mlt_properties_set_data(properties, "bitmap", luma_bitmap, 0, mlt_pool_release, NULL);
            mlt_properties_clear(properties, "producer");
        } else {
            if (!producer || !current_resource || strcmp(resource, current_resource)) {
                // Get the factory producer service
                char *factory = mlt_properties_get(properties, "factory");

                // Create the producer
                producer = mlt_factory_producer(profile, factory, resource);
                if (producer)
                    mlt_properties_set(properties, "_resource", resource);
            }

            // If we have one
            if (producer) {
                // Get the producer properties
                mlt_properties producer_properties = MLT_PRODUCER_PROPERTIES(producer);

                // Ensure that we loop
                mlt_properties_set(producer_properties, "eof", "loop");

                // Now pass all producer. properties on the transition down
                mlt_properties_pass(producer_properties, properties, "producer.");

                // We will get the alpha frame from the producer
                mlt_frame luma_frame = NULL;

                // Determine if producer is a video clip or still image
                const char *service_name = mlt_properties_get(producer_properties, "mlt_service");
                int is_clip = mlt_producer_get_length(producer) > 1
                              && mlt_properties_get_int(producer_properties, "video_index") >= 0
                              && service_name && !strncmp("avformat", service_name, 8);
                if (is_clip) {
                    if (!mlt_properties_get(producer_properties, "producer.eof"))
                        mlt_properties_set(producer_properties, "eof", "pause");
                    mlt_producer_seek(producer, mlt_transition_get_position(transition, a_frame));
                }

                // Get the luma frame
                if (mlt_service_get_frame(MLT_PRODUCER_SERVICE(producer), &luma_frame, 0) == 0) {
                    uint8_t *luma_image = NULL;
                    mlt_image_format luma_format = mlt_image_yuv422;

                    // Get image from the luma producer
                    mlt_frame_get_image(luma_frame,
                                        &luma_image,
                                        &luma_format,
                                        &luma_width,
                                        &luma_height,
                                        0);

                    // Generate the luma map
                    if (luma_image) {
                        if (is_clip) {
                            yuv422_to_luma16(luma_image,
                                             &luma_bitmap,
                                             luma_width,
                                             luma_height,
                                             mlt_properties_get_int(MLT_FRAME_PROPERTIES(luma_frame),
                                                                    "full_range"));
                        } else {
                            mlt_luma_map_from_yuv422(luma_image,
                                                     &luma_bitmap,
                                                     luma_width,
                                                     luma_height);
                        }
                    }

                    // Set the transition properties
                    mlt_properties_set_int(properties, "width", luma_width);
                    mlt_properties_set_int(properties, "height", luma_height);
                    mlt_properties_set_data(properties,
                                            "bitmap",
                                            luma_bitmap,
                                            luma_width * luma_height * 2,
                                            mlt_pool_release,
                                            NULL);

                    // Cleanup the luma frame
                    mlt_frame_close(luma_frame);
                }

                if (is_clip) {
                    // Save the producer for getting next frame
                    mlt_properties_set_data(properties,
                                            "producer",
                                            producer,
                                            0,
                                            (mlt_destructor) mlt_producer_close,
                                            NULL);
                } else {
                    // Cleanup the luma producer
                    mlt_properties_clear(properties, "producer");
                    mlt_producer_close(producer);
                    producer = NULL;
                }
            }
        }
    }

    // Arbitrary composite defaults
    float mix = mlt_transition_get_progress(transition, a_frame);
    float frame_delta = mlt_transition_get_progress_delta(transition, a_frame);
    float luma_softness = mlt_properties_get_double(properties, "softness");
    int progressive = mlt_properties_get_int(a_props, "consumer.progressive")
                      || mlt_properties_get_int(properties, "progressive")
                      || mlt_properties_get_int(b_props, "luma.progressive");
    int top_field_first = mlt_properties_get_int(b_props, "top_field_first");
    int reverse = mlt_properties_get_int(properties, "reverse");
    int invert = mlt_properties_get_int(properties, "invert");
    int threads = CLAMP(mlt_properties_get_int(properties, "threads"), 0, mlt_slices_count_normal());
    int alpha_over = mlt_properties_get_int(properties, "alpha_over");
    int ret = 0;

    // Honour the reverse here
    if (mix >= 1.0)
        mix -= floor(mix);

    if (mlt_properties_get(properties, "fixed"))
        mix = mlt_properties_get_double(properties, "fixed");

    if (producer) {
        invert = !invert;
        mix = 0.5f;
    } else {
        mlt_service_unlock(MLT_TRANSITION_SERVICE(transition));
    }

    int fix_background_alpha = mlt_properties_get_int(properties, "fix_background_alpha");
    if (luma_width > 0 && luma_height > 0 && luma_bitmap != NULL) {
        reverse = invert ? !reverse : reverse;
        mix = reverse ? 1 - mix : mix;
        frame_delta *= reverse ? -1.0 : 1.0;
        // Composite the frames using a luma map
        if (*format == mlt_image_rgba) {
            ret = luma_composite_rgba32(!invert ? a_frame : b_frame,
                                        !invert ? b_frame : a_frame,
                                        luma_width,
                                        luma_height,
                                        luma_bitmap,
                                        mix,
                                        frame_delta,
                                        luma_softness,
                                        progressive ? -1 : top_field_first,
                                        *width,
                                        *height,
                                        invert);
        } else if (*format == mlt_image_rgba64) {
            ret = luma_composite_rgba64(!invert ? a_frame : b_frame,
                                        !invert ? b_frame : a_frame,
                                        luma_width,
                                        luma_height,
                                        luma_bitmap,
                                        mix,
                                        frame_delta,
                                        luma_softness,
                                        progressive ? -1 : top_field_first,
                                        *width,
                                        *height,
                                        invert);
        } else {
            *format = mlt_image_yuv422;
            luma_composite_yuv422(!invert ? a_frame : b_frame,
                                  !invert ? b_frame : a_frame,
                                  luma_width,
                                  luma_height,
                                  luma_bitmap,
                                  mix,
                                  frame_delta,
                                  luma_softness,
                                  progressive ? -1 : top_field_first,
                                  width,
                                  height,
                                  invert,
                                  fix_background_alpha);
        }
    } else {
        if (mlt_properties_get(&a_frame->parent, "distort"))
            mlt_properties_set(&b_frame->parent,
                               "distort",
                               mlt_properties_get(&a_frame->parent, "distort"));

        if (mlt_color_trc_linear
            == mlt_image_color_trc_id(mlt_properties_get(a_props, "consumer.mlt_color_trc"))) {
            // Make progress appear linear using a gamma curve similar to sRGB or Rec. 709
            if (reverse || invert)
                mix = 1.0 - pow(1.0 - mix, 2.0);
            else
                mix = pow(mix, 2.0);
        }

        mix = (reverse || invert) ? 1 - mix : mix;
        invert = 0;
        // Dissolve the frames using the time offset for mix value
        if (alpha_over && *format == mlt_image_rgba) {
            ret = dissolve_rgba32(a_frame, b_frame, mix, *width, *height, threads);
        } else if (alpha_over && *format == mlt_image_rgba64) {
            ret = dissolve_rgba64(a_frame, b_frame, mix, *width, *height, threads);
        } else {
            *format = mlt_image_yuv422;
            ret = dissolve_yuv422(a_frame,
                                  b_frame,
                                  mix,
                                  *width,
                                  *height,
                                  threads,
                                  alpha_over,
                                  fix_background_alpha);
        }
    }
    if (ret) {
        mlt_log_error(MLT_TRANSITION_SERVICE(transition), "Unable to dissolve incompatible images");
    }
    if (producer) {
        mlt_service_unlock(MLT_TRANSITION_SERVICE(transition));
    }

    // Extract the a_frame image info
    *width = mlt_properties_get_int(!invert ? a_props : b_props, "width");
    *height = mlt_properties_get_int(!invert ? a_props : b_props, "height");
    *image = mlt_properties_get_data(!invert ? a_props : b_props, "image", NULL);

    return ret;
}

/** Luma transition processing.
*/

static mlt_frame transition_process(mlt_transition transition, mlt_frame a_frame, mlt_frame b_frame)
{
    // Push the transition on to the frame
    mlt_frame_push_service(a_frame, transition);

    // Push the b_frame on to the stack
    mlt_frame_push_frame(a_frame, b_frame);

    // Push the transition method
    mlt_frame_push_get_image(a_frame, transition_get_image);

    return a_frame;
}

/** Constructor for the filter.
*/

mlt_transition transition_luma_init(mlt_profile profile,
                                    mlt_service_type type,
                                    const char *id,
                                    char *lumafile)
{
    mlt_transition transition = mlt_transition_new();
    if (transition != NULL) {
        // Set the methods
        transition->process = transition_process;

        // Default factory
        mlt_properties_set(MLT_TRANSITION_PROPERTIES(transition),
                           "factory",
                           mlt_environment("MLT_PRODUCER"));

        // Set the main property
        mlt_properties_set(MLT_TRANSITION_PROPERTIES(transition), "resource", lumafile);

        // Inform apps and framework that this is a video only transition
        mlt_properties_set_int(MLT_TRANSITION_PROPERTIES(transition), "_transition_type", 1);

        return transition;
    }
    return NULL;
}
