/**
 * Copyright (C) 2025 Niklas Haas
 *
 * This file is part of FFmpeg.
 *
 * FFmpeg is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 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 General Public License for more details.
 *
 * You should have received a copy of the GNU 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 <string.h>

#include "libavutil/avassert.h"
#include "libavutil/mem_internal.h"
#include "libavutil/refstruct.h"

#include "libswscale/ops.h"
#include "libswscale/ops_dispatch.h"
#include "libswscale/uops.h"
#include "libswscale/uops_macros.h"

#include "checkasm.h"

enum {
    NB_PLANES   = 4,
    PIXELS      = 64,
    LINES       = 16,
};

enum {
    U8  = SWS_PIXEL_U8,
    U16 = SWS_PIXEL_U16,
    U32 = SWS_PIXEL_U32,
    F32 = SWS_PIXEL_F32,
};

#define FMT(fmt, ...) tprintf((char[256]) {0}, 256, fmt, __VA_ARGS__)
static const char *tprintf(char buf[], size_t size, const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    vsnprintf(buf, size, fmt, ap);
    va_end(ap);
    return buf;
}

static int rw_pixel_bits(const SwsOp *op)
{
    if (op->rw.mode == SWS_RW_PALETTE)
        return 8; /* index size */

    int elems = 0;
    switch (op->rw.mode) {
    case SWS_RW_PLANAR: elems = 1; break;
    case SWS_RW_PACKED: elems = op->rw.elems; break;
    }

    const int size  = ff_sws_pixel_type_size(op->type);
    const int bits  = 8 >> op->rw.frac;
    av_assert1(bits >= 1);
    return elems * size * bits;
}

static float rndf(void)
{
    union { uint32_t u; float f; } x;
    do {
        x.u = rnd();
    } while (!isnormal(x.f));
    return x.f;
}

static void fill32f(float *line, int num, unsigned range)
{
    const float scale = (float) range / UINT32_MAX;
    for (int i = 0; i < num; i++)
        line[i] = range ? scale * rnd() : rndf();
}

static void fill32(uint32_t *line, int num, unsigned range)
{
    for (int i = 0; i < num; i++)
        line[i] = (range && range < UINT_MAX) ? rnd() % (range + 1) : rnd();
}

static void fill16(uint16_t *line, int num, unsigned range)
{
    if (!range) {
        fill32((uint32_t *) line, AV_CEIL_RSHIFT(num, 1), 0);
    } else {
        for (int i = 0; i < num; i++)
            line[i] = rnd() % (range + 1);
    }
}

static void fill8(uint8_t *line, int num, unsigned range)
{
    if (!range) {
        fill32((uint32_t *) line, AV_CEIL_RSHIFT(num, 2), 0);
    } else {
        for (int i = 0; i < num; i++)
            line[i] = rnd() % (range + 1);
    }
}

static void set_range(AVRational64 *rangeq, unsigned range, unsigned range_def)
{
    if (!range)
        range = range_def;
    if (range)
        *rangeq = (AVRational64) { range, 1 };
}

static void check_compiled(const char *name,
                           const SwsOp *read_op, const SwsOp *write_op,
                           const int ranges[NB_PLANES],
                           const SwsCompiledOp *comp_ref,
                           const SwsCompiledOp *comp_new)
{
    /**
     * We can't use `check_func()` alone because the actual function pointer
     * may be a wrapper or entry point shared by multiple implementations.
     * Solve it by hashing in the active CPU flags as well.
     */
    uintptr_t id = (uintptr_t) comp_new->func;
    id ^= (id << 6) + (id >> 2) + 0x9e3779b97f4a7c15 + comp_new->cpu_flags;
    if (!check_key(id, "%s", name))
        return;

    declare_func(void, const SwsOpExec *, const void *, int bx, int y, int bx_end, int y_end);

    static DECLARE_ALIGNED_64(char, src0)[NB_PLANES][LINES][PIXELS * sizeof(uint32_t[4])];
    static DECLARE_ALIGNED_64(char, src1)[NB_PLANES][LINES][PIXELS * sizeof(uint32_t[4])];
    static DECLARE_ALIGNED_64(char, dst0)[NB_PLANES][LINES][PIXELS * sizeof(uint32_t[4])];
    static DECLARE_ALIGNED_64(char, dst1)[NB_PLANES][LINES][PIXELS * sizeof(uint32_t[4])];

    av_assert0(PIXELS % comp_new->block_size == 0);
    for (int p = 0; p < NB_PLANES; p++) {
        void *plane = src0[p];
        switch (read_op->type) {
        case U8:
            fill8(plane, sizeof(src0[p]) /  sizeof(uint8_t), ranges[p]);
            break;
        case U16:
            fill16(plane, sizeof(src0[p]) / sizeof(uint16_t), ranges[p]);
            break;
        case U32:
            fill32(plane, sizeof(src0[p]) / sizeof(uint32_t), ranges[p]);
            break;
        case F32:
            fill32f(plane, sizeof(src0[p]) / sizeof(uint32_t), ranges[p]);
            break;
        }
    }

    memcpy(src1, src0, sizeof(src0));
    memset(dst0, 0, sizeof(dst0));
    memset(dst1, 0, sizeof(dst1));

    const int read_size  = PIXELS * rw_pixel_bits(read_op)  >> 3;
    const int write_size = PIXELS * rw_pixel_bits(write_op) >> 3;

    SwsOpExec exec = {0};
    exec.width = PIXELS;
    exec.height = exec.slice_h = LINES;
    for (int i = 0; i < NB_PLANES; i++) {
        exec.in_stride[i]  = sizeof(src0[i][0]);
        exec.out_stride[i] = sizeof(dst0[i][0]);
        exec.in_bump[i]  = exec.in_stride[i]  - read_size;
        exec.out_bump[i] = exec.out_stride[i] - write_size;
    }

    if (read_op->rw.mode == SWS_RW_PALETTE) {
        static_assert(sizeof(src0[1]) >= sizeof(uint32_t[256]), "palette plane too small");
        exec.in_bump[1] = exec.in_stride[1] = 0;
    }

    int32_t in_bump_y[LINES];
    if (read_op->rw.filter.op == SWS_OP_FILTER_V) {
        const int *offsets = read_op->rw.filter.kernel->offsets;
        for (int y = 0; y < LINES - 1; y++)
            in_bump_y[y] = offsets[y + 1] - offsets[y] - 1;
        in_bump_y[LINES - 1] = 0;
        exec.in_bump_y = in_bump_y;
    }

    int32_t in_offset_x[PIXELS];
    if (read_op->rw.filter.op == SWS_OP_FILTER_H) {
        const int *offsets = read_op->rw.filter.kernel->offsets;
        const int rw_bits = rw_pixel_bits(read_op);
        for (int x = 0; x < PIXELS; x++)
            in_offset_x[x] = offsets[x] * rw_bits >> 3;
        exec.in_offset_x = in_offset_x;
    }

    for (int i = 0; i < NB_PLANES; i++) {
        exec.in[i]  = (void *) src0[i];
        exec.out[i] = (void *) dst0[i];
        exec.block_size_in[i]  = comp_ref->block_size * rw_pixel_bits(read_op)  >> 3;
        exec.block_size_out[i] = comp_ref->block_size * rw_pixel_bits(write_op) >> 3;
    }
    checkasm_call(comp_ref->func, &exec, comp_ref->priv, 0, 0, PIXELS / comp_ref->block_size, LINES);

    for (int i = 0; i < NB_PLANES; i++) {
        exec.in[i]  = (void *) src1[i];
        exec.out[i] = (void *) dst1[i];
        exec.block_size_in[i]  = comp_new->block_size * rw_pixel_bits(read_op)  >> 3;
        exec.block_size_out[i] = comp_new->block_size * rw_pixel_bits(write_op) >> 3;
    }
    checkasm_call_checked(comp_new->func, &exec, comp_new->priv, 0, 0, PIXELS / comp_new->block_size, LINES);

    for (int i = 0; i < NB_PLANES; i++) {
        const char *desc = FMT("%s[%d]", name, i);
        const int stride = sizeof(dst0[i][0]);

        switch (write_op->type) {
        case U8:
            checkasm_check(uint8_t, (void *) dst0[i], stride,
                                    (void *) dst1[i], stride,
                                    write_size, LINES, desc);
            break;
        case U16:
            checkasm_check(uint16_t, (void *) dst0[i], stride,
                                     (void *) dst1[i], stride,
                                     write_size >> 1, LINES, desc);
            break;
        case U32:
            checkasm_check(uint32_t, (void *) dst0[i], stride,
                                     (void *) dst1[i], stride,
                                     write_size >> 2, LINES, desc);
            break;
        case F32:
            checkasm_check(float_ulp, (void *) dst0[i], stride,
                                      (void *) dst1[i], stride,
                                      write_size >> 2, LINES, desc, 0);
            break;
        }

        if (write_op->rw.mode == SWS_RW_PACKED)
            break;
    }

    bench(comp_new->func, &exec, comp_new->priv, 0, 0, PIXELS / comp_new->block_size, LINES);
}

static void check_ops(const char *name, const unsigned ranges[NB_PLANES],
                      const SwsOp *ops)
{
    SwsContext *ctx = sws_alloc_context();
    if (!ctx)
        return;
    ctx->flags = SWS_BITEXACT;

    static const unsigned def_ranges[4] = {0};
    if (!ranges)
        ranges = def_ranges;

    const SwsOp *read_op, *write_op;
    SwsOpList oplist = {
        .ops = (SwsOp *) ops,
        .plane_src = {0, 1, 2, 3},
        .plane_dst = {0, 1, 2, 3},
    };

    read_op = &ops[0];
    for (oplist.num_ops = 0; ops[oplist.num_ops].op; oplist.num_ops++)
        write_op = &ops[oplist.num_ops];

    for (int p = 0; p < NB_PLANES; p++) {
        switch (read_op->type) {
        case U8:
            set_range(&oplist.comps_src.max[p], ranges[p], UINT8_MAX);
            oplist.comps_src.min[p] = (AVRational64) { 0, 1 };
            break;
        case U16:
            set_range(&oplist.comps_src.max[p], ranges[p], UINT16_MAX);
            oplist.comps_src.min[p] = (AVRational64) { 0, 1 };
            break;
        case U32:
            set_range(&oplist.comps_src.max[p], ranges[p], UINT32_MAX);
            oplist.comps_src.min[p] = (AVRational64) { 0, 1 };
            break;
        case F32:
            if (ranges[p]) {
                oplist.comps_src.max[p] = (AVRational64) { ranges[p], 1 };
                oplist.comps_src.min[p] = (AVRational64) { 0, 1 };
            }
            break;
        }
    }

    static const SwsOpBackend *backend_ref;
    if (!backend_ref) {
         for (int n = 0; ff_sws_op_backends[n]; n++) {
            if (!strcmp(ff_sws_op_backends[n]->name, "c")) {
                backend_ref = ff_sws_op_backends[n];
                break;
            }
        }
        av_assert0(backend_ref);
    }

    /* Always compile `ops` using the C backend as a reference */
    SwsCompiledOp comp_ref = {0};
    int ret = ff_sws_ops_compile(ctx, backend_ref, &oplist, &comp_ref);
    if (ret < 0) {
        av_assert0(ret != AVERROR(ENOTSUP));
        fail();
        goto done;
    }

    /* Check with the C backend to establish a reference */
    check_compiled(name, read_op, write_op, ranges, &comp_ref, &comp_ref);

    /* Iterate over every other backend, and test it against the C reference */
    for (int n = 0; ff_sws_op_backends[n]; n++) {
        const SwsOpBackend *backend = ff_sws_op_backends[n];
        if (backend->hw_format != AV_PIX_FMT_NONE || backend == backend_ref)
            continue;

        SwsCompiledOp comp_new = {0};
        int ret = ff_sws_ops_compile(ctx, backend, &oplist, &comp_new);
        if (ret == AVERROR(ENOTSUP)) {
            continue;
        } else if (ret < 0) {
            fail();
            goto done;
        }

        /* Distinguish backends from each other even with same CPU flags */
        checkasm_set_func_variant("%s_%s", backend->name, checkasm_get_cpu_suffix());
        check_compiled(name, read_op, write_op, ranges, &comp_ref, &comp_new);
        ff_sws_compiled_op_unref(&comp_new);
    }

done:
    ff_sws_compiled_op_unref(&comp_ref);
    sws_free_context(&ctx);
}

#define CHECK_RANGES(NAME, RANGES, N_IN, N_OUT, IN, OUT, ...)                   \
  do {                                                                          \
      check_ops(NAME, RANGES, (SwsOp[]) {                                       \
        {                                                                       \
            .op = SWS_OP_READ,                                                  \
            .type = IN,                                                         \
            .rw.elems = N_IN,                                                   \
        },                                                                      \
        __VA_ARGS__,                                                            \
        {                                                                       \
            .op = SWS_OP_WRITE,                                                 \
            .type = OUT,                                                        \
            .rw.elems = N_OUT,                                                  \
        }, {0}                                                                  \
    });                                                                         \
  } while (0)

#define MK_RANGES(R) ((const unsigned[]) { R, R, R, R })
#define CHECK_RANGE(NAME, RANGE, N_IN, N_OUT, IN, OUT, ...)                     \
    CHECK_RANGES(NAME, MK_RANGES(RANGE), N_IN, N_OUT, IN, OUT, __VA_ARGS__)

#define CHECK(NAME, N_IN, N_OUT, IN, OUT, ...) \
    CHECK_RANGE(NAME, 0, N_IN, N_OUT, IN, OUT, __VA_ARGS__)

static inline int mask_num(const SwsCompMask mask)
{
    switch (mask) {
    case SWS_COMP_ELEMS(1): return 1;
    case SWS_COMP_ELEMS(2): return 2;
    case SWS_COMP_ELEMS(3): return 3;
    case SWS_COMP_ELEMS(4): return 4;
    default: return 0;
    }
}

#define CHECK_MASK(NAME, MASK, RANGES, IN, OUT, ...)                            \
do {                                                                            \
    const SwsCompMask mask = (MASK);                                            \
    const int num = mask_num(mask);                                             \
    if (!num)                                                                   \
        break; /* can't test these with current infrastructure */               \
    CHECK_RANGES(NAME, RANGES, 4, num, IN, OUT, __VA_ARGS__);                   \
} while (0)

static AVRational64 rndq(SwsPixelType t)
{
    const unsigned num = rnd();
    if (ff_sws_pixel_type_is_int(t)) {
        const int bits = ff_sws_pixel_type_size(t) * 8;
        const unsigned mask = UINT_MAX >> (32 - bits);
        return (AVRational64) { num & mask, 1 };
    } else {
        const unsigned den = rnd();
        return (AVRational64) { num, den ? den : 1 };
    }
}

static void check_read(const char *name, const SwsUOp *uop)
{
    SwsReadWriteMode mode;
    switch (uop->uop) {
    case SWS_UOP_READ_PACKED:
    case SWS_UOP_READ_BIT:
    case SWS_UOP_READ_NIBBLE:  mode = SWS_RW_PACKED; break;
    case SWS_UOP_READ_PLANAR:  mode = SWS_RW_PLANAR; break;
    case SWS_UOP_READ_PALETTE: mode = SWS_RW_PALETTE; break;
    default: return;
    }

    const int num = mask_num(uop->mask);
    check_ops(name, NULL, (SwsOp[]) {
        {
            .op        = SWS_OP_READ,
            .type      = uop->type,
            .rw.elems  = num,
            .rw.mode   = mode,
            .rw.frac   = uop->uop == SWS_UOP_READ_BIT    ? 3 :
                         uop->uop == SWS_UOP_READ_NIBBLE ? 1 : 0,
        }, {
            .op        = SWS_OP_WRITE,
            .type      = uop->type,
            .rw.elems  = num,
        }, {0}
    });
}

static void check_write(const char *name, const SwsUOp *uop)
{
    SwsReadWriteMode mode;
    switch (uop->uop) {
    case SWS_UOP_WRITE_BIT:
    case SWS_UOP_WRITE_NIBBLE:
    case SWS_UOP_WRITE_PACKED: mode = SWS_RW_PACKED; break;
    case SWS_UOP_WRITE_PLANAR: mode = SWS_RW_PLANAR; break;
    default: return;
    }

    const int frac = uop->uop == SWS_UOP_WRITE_BIT    ? 3 :
                     uop->uop == SWS_UOP_WRITE_NIBBLE ? 1 : 0;
    const int num = mask_num(uop->mask);
    const int bits = 8 >> frac;
    const unsigned range = (1 << bits) - 1;

    check_ops(name, MK_RANGES(range), (SwsOp[]) {
        {
            .op        = SWS_OP_READ,
            .type      = uop->type,
            .rw.elems  = num,
        }, {
            .op        = SWS_OP_WRITE,
            .type      = uop->type,
            .rw.elems  = num,
            .rw.mode   = mode,
            .rw.frac   = frac,
        }, {0}
    });
}

static void check_filter(const char *name, const SwsUOp *uop)
{
    const int num = mask_num(uop->mask);
    const bool is_vert = uop->uop == SWS_UOP_READ_PLANAR_FV;

    SwsFilterParams par = {
        .scaler_params  = { SWS_PARAM_DEFAULT, SWS_PARAM_DEFAULT },
        .dst_size       = is_vert ? LINES : PIXELS,
    };

    const SwsScaler scalers[] = {
        SWS_SCALE_POINT,
        SWS_SCALE_SINC,
    };

    for (int s = 0; s < FF_ARRAY_ELEMS(scalers); s++) {
        par.scaler = scalers[s];

        for (par.src_size = 1; par.src_size <= par.dst_size; par.src_size <<= 1) {
            SwsFilterWeights *filter;
            if (ff_sws_filter_generate(NULL, &par, &filter) < 0) {
                fail();
                return;
            }

            char desc[256];
            snprintf(desc, sizeof(desc), "%s_%s_%d", name, filter->name, par.src_size);
            check_ops(desc, NULL, (SwsOp[]) {
                {
                    .op        = SWS_OP_READ,
                    .type      = uop->type,
                    .rw.elems  = num,
                    .rw.filter = {
                        .op     = is_vert ? SWS_OP_FILTER_V : SWS_OP_FILTER_H,
                        .kernel = filter,
                        .type   = SWS_PIXEL_F32,
                    },
                }, {
                    .op        = SWS_OP_WRITE,
                    .type      = SWS_PIXEL_F32,
                    .rw.elems  = num,
                }, {0}
            });

            av_refstruct_unref(&filter);
        }
    }
}

static void check_cast(const char *name, const SwsUOp *uop)
{
    SwsPixelType dst;
    switch (uop->uop) {
    case SWS_UOP_TO_U8:  dst = SWS_PIXEL_U8;  break;
    case SWS_UOP_TO_U16: dst = SWS_PIXEL_U16; break;
    case SWS_UOP_TO_U32: dst = SWS_PIXEL_U32; break;
    case SWS_UOP_TO_F32: dst = SWS_PIXEL_F32; break;
    default: return;
    }

    const int isize = ff_sws_pixel_type_size(uop->type);
    const int osize = ff_sws_pixel_type_size(dst);
    unsigned range = UINT32_MAX >> (32 - osize * 8);
    if (isize < osize || !ff_sws_pixel_type_is_int(dst))
        range = 0;

    CHECK_MASK(name, uop->mask, MK_RANGES(range), uop->type, dst, {
        .op   = SWS_OP_CONVERT,
        .type = uop->type,
        .convert.to = dst,
    });
}

static void check_expand_bit(const char *name, const SwsUOp *uop)
{
    AVRational64 factor = { .den = 1 };
    switch (uop->type) {
    case SWS_PIXEL_U8:  factor.num = UINT8_MAX;  break;
    case SWS_PIXEL_U16: factor.num = UINT16_MAX; break;
    case SWS_PIXEL_U32: factor.num = UINT32_MAX; break;
    default: return;
    }

    CHECK_MASK(name, uop->mask, MK_RANGES(1), uop->type, uop->type, {
        .op   = SWS_OP_SCALE,
        .type = uop->type,
        .scale.factor = factor,
    });
}

static void check_expand(const char *name, const SwsUOp *uop)
{
    SwsPixelType dst = SWS_PIXEL_NONE;
    switch (uop->uop) {
    case SWS_UOP_EXPAND_PAIR: dst = SWS_PIXEL_U16; break;
    case SWS_UOP_EXPAND_QUAD: dst = SWS_PIXEL_U32; break;
    }

    av_assert0(uop->type == SWS_PIXEL_U8);
    CHECK_MASK(name, uop->mask, NULL, uop->type, dst, {
        .op   = SWS_OP_CONVERT,
        .type = uop->type,
        .convert = {
            .to = dst,
            .expand = true,
        },
    });
}

static void check_swizzle(const char *name, const SwsUOp *uop)
{
    const SwsSwizzleUOp *swiz = &uop->par.swizzle;
    CHECK_MASK(name, uop->mask, NULL, uop->type, uop->type, {
        .op   = SWS_OP_SWIZZLE,
        .type = uop->type,
        .swizzle.in = { swiz->in[0], swiz->in[1], swiz->in[2], swiz->in[3] },
    });
}

static void check_scale(const char *name, const SwsUOp *uop)
{
    unsigned range = 0;
    AVRational64 scale;

    if (ff_sws_pixel_type_is_int(uop->type)) {
        /* Ensure the result won't exceed the value range */
        const int bits = ff_sws_pixel_type_size(uop->type) * 8;
        const unsigned max = UINT32_MAX >> (32 - bits);
        scale = (AVRational64) { rnd() & (max >> 1), 1 };
        range = max / (scale.num ? scale.num : 1);
    } else {
        scale = rndq(uop->type);
    }

    CHECK_MASK(name, uop->mask, MK_RANGES(range), uop->type, uop->type, {
        .op   = SWS_OP_SCALE,
        .type = uop->type,
        .scale.factor = scale,
    });
}

static void check_clamp(const char *name, const SwsUOp *uop)
{
    const SwsPixelType t = uop->type;
    CHECK_MASK(name, uop->mask, NULL, t, t, {
        .op   = uop->uop == SWS_UOP_MIN ? SWS_OP_MIN : SWS_OP_MAX,
        .type = t,
        .clamp.limit = { rndq(t), rndq(t), rndq(t), rndq(t) },
    });
}

static void check_swap_bytes(const char *name, const SwsUOp *uop)
{
    CHECK_MASK(name, uop->mask, NULL, uop->type, uop->type, {
        .op   = SWS_OP_SWAP_BYTES,
        .type = uop->type,
    });
}

static void check_unpack(const char *name, const SwsUOp *uop)
{
    const uint8_t *pat = uop->par.pack.pattern;
    const int num = pat[3] ? 4 : 3;
    const int total = pat[0] + pat[1] + pat[2] + pat[3];
    const unsigned range = UINT32_MAX >> (32 - total);

    CHECK_RANGE(name, range, 1, num, uop->type, uop->type, {
        .op   = SWS_OP_UNPACK,
        .type = uop->type,
        .pack.pattern = { pat[0], pat[1], pat[2], pat[3] },
    });
}

static void check_pack(const char *name, const SwsUOp *uop)
{
    const uint8_t *pat = uop->par.pack.pattern;
    const unsigned ranges[4] = {
        (1 << pat[0]) - 1, (1 << pat[1]) - 1,
        (1 << pat[2]) - 1, (1 << pat[3]) - 1,
    };

    CHECK_RANGES(name, ranges, 4, 1, uop->type, uop->type, {
        .op   = SWS_OP_PACK,
        .type = uop->type,
        .pack.pattern = { pat[0], pat[1], pat[2], pat[3] },
    });
}

static void check_shift(const char *name, const SwsUOp *uop)
{
    CHECK_MASK(name, uop->mask, NULL, uop->type, uop->type, {
        .op   = uop->uop == SWS_UOP_LSHIFT ? SWS_OP_LSHIFT : SWS_OP_RSHIFT,
        .type = uop->type,
        .shift.amount = uop->par.shift.amount,
    });
}

static void check_clear(const char *name, const SwsUOp *uop)
{
    const SwsPixelType type = uop->type;
    const int bits = ff_sws_pixel_type_size(type) * 8;
    const unsigned range  = UINT32_MAX >> (32 - bits);
    const AVRational64 one  = (AVRational64) { (int) range, 1};
    const AVRational64 zero = (AVRational64) { 0, 1};
    const AVRational64 val  = { (rand() & 0x7F) | 1, 1 };

    SwsClearOp clear = { .mask = uop->mask };
    for (int i = 0; i < 4; i++) {
        if (SWS_COMP_TEST(uop->par.clear.one, i))
            clear.value[i] = one;
        else if (SWS_COMP_TEST(uop->par.clear.zero, i))
            clear.value[i] = zero;
        else
            clear.value[i] = val;
    }

    CHECK(name, 4, 4, type, type, {
        .op    = SWS_OP_CLEAR,
        .type  = type,
        .clear = clear,
    });
}

static void check_linear(const char *name, const SwsUOp *uop)
{
    const SwsPixelType type = uop->type;
    av_assert0(!ff_sws_pixel_type_is_int(type));

    SwsLinearOp lin;
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 5; j++) {
            if (uop->par.lin.one & SWS_MASK(i, j))
                lin.m[i][j] = (AVRational64) { 1, 1 };
            else if (uop->par.lin.zero & SWS_MASK(i, j))
                lin.m[i][j] = (AVRational64) { 0, 1 };
            else
                lin.m[i][j] = rndq(type);
        }
    }

    lin.mask = ff_sws_linear_mask(&lin);
    CHECK(name, 4, 4, type, type, {
        .op   = SWS_OP_LINEAR,
        .type = type,
        .lin  = lin,
    });
}

static void check_dither(const char *name, const SwsUOp *uop)
{
    const SwsPixelType type = uop->type;
    av_assert0(!ff_sws_pixel_type_is_int(type));

    SwsDitherOp dither = { .size_log2 = uop->par.dither.size_log2 };
    const int size = 1 << dither.size_log2;
    const uint8_t *y_offset = uop->par.dither.y_offset;
    for (int i = 0; i < 4; i++)
        dither.y_offset[i] = SWS_COMP_TEST(uop->mask, i) ? y_offset[i] : -1;

    dither.matrix = av_refstruct_allocz(size * size * sizeof(*dither.matrix));
    if (!dither.matrix) {
        fail();
        return;
    }

    for (int i = 0; i < size * size; i++)
        dither.matrix[i] = rndq(type);

    CHECK(name, 4, 4, type, type, {
        .op     = SWS_OP_DITHER,
        .type   = type,
        .dither = dither,
    });

    av_refstruct_unref(&dither.matrix);
}

static void check_add(const char *name, const SwsUOp *uop)
{
    /* SwsOp has no concept of SWS_OP_ADD; this is only used for
     * SWS_OP_DITHER with a 1x1 dither matrix; so translate the uop */
    check_dither(name, &(SwsUOp) {
        .uop  = SWS_UOP_DITHER,
        .type = uop->type,
        .mask = uop->mask,
        .par.dither.size_log2 = 0,
    });
}

#define CHECK_FUNCTION(CHECK, NAME, ...) \
    CHECK(#NAME, &(SwsUOp) { __VA_ARGS__ });

#define CHECK_FOR(UOP, CHECK) \
    SWS_FOR_STRUCT(U8,  UOP, CHECK_FUNCTION, CHECK) \
    SWS_FOR_STRUCT(U16, UOP, CHECK_FUNCTION, CHECK) \
    SWS_FOR_STRUCT(U32, UOP, CHECK_FUNCTION, CHECK) \
    SWS_FOR_STRUCT(F32, UOP, CHECK_FUNCTION, CHECK) \
    report(#UOP)

void checkasm_check_sw_ops(void)
{
    CHECK_FOR(READ_PLANAR,      check_read);
    CHECK_FOR(READ_PLANAR_FH,   check_filter);
    CHECK_FOR(READ_PLANAR_FV,   check_filter);
    CHECK_FOR(READ_PACKED,      check_read);
    CHECK_FOR(READ_NIBBLE,      check_read);
    CHECK_FOR(READ_BIT,         check_read);
    CHECK_FOR(READ_PALETTE,     check_read);
    CHECK_FOR(WRITE_PLANAR,     check_write);
    CHECK_FOR(WRITE_PACKED,     check_write);
    CHECK_FOR(WRITE_NIBBLE,     check_write);
    CHECK_FOR(WRITE_BIT,        check_write);
    CHECK_FOR(PERMUTE,          check_swizzle);
    CHECK_FOR(COPY,             check_swizzle);
    CHECK_FOR(EXPAND_BIT,       check_expand_bit);
    CHECK_FOR(EXPAND_PAIR,      check_expand);
    CHECK_FOR(EXPAND_QUAD,      check_expand);
    CHECK_FOR(SWAP_BYTES,       check_swap_bytes);
    CHECK_FOR(TO_U8,            check_cast);
    CHECK_FOR(TO_U16,           check_cast);
    CHECK_FOR(TO_U32,           check_cast);
    CHECK_FOR(TO_F32,           check_cast);
    CHECK_FOR(SCALE,            check_scale);
    CHECK_FOR(ADD,              check_add);
    CHECK_FOR(MIN,              check_clamp);
    CHECK_FOR(MAX,              check_clamp);
    CHECK_FOR(UNPACK,           check_unpack);
    CHECK_FOR(PACK,             check_pack);
    CHECK_FOR(LSHIFT,           check_shift);
    CHECK_FOR(RSHIFT,           check_shift);
    CHECK_FOR(CLEAR,            check_clear);
    CHECK_FOR(LINEAR,           check_linear);
    CHECK_FOR(DITHER,           check_dither);
}
