/*
  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>

  This software is provided 'as-is', without any express or implied
  warranty.  In no event will the authors be held liable for any damages
  arising from the use of this software.

  Permission is granted to anyone to use this software for any purpose,
  including commercial applications, and to alter it and redistribute it
  freely.
*/
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3/SDL_test.h>
#include "testyuv_cvt.h"
#include "testutils.h"

/* 422 (YUY2, etc) and P416 formats are the largest */
#define MAX_YUV_SURFACE_SIZE(W, H, P) ((H + 1) * ((W + 1) + P) * 3 * 2)

/* Return true if the YUV format is packed pixels */
static bool is_packed_yuv_format(Uint32 format)
{
    return format == SDL_PIXELFORMAT_YUY2 || format == SDL_PIXELFORMAT_UYVY || format == SDL_PIXELFORMAT_YVYU;
}

/* Create a surface with a good pattern for verifying YUV conversion */
static SDL_Surface *generate_test_pattern(int pattern_size)
{
    SDL_Surface *pattern = SDL_CreateSurface(pattern_size, pattern_size, SDL_PIXELFORMAT_RGB24);

    if (pattern) {
        int i, x, y;
        Uint8 *p, c;
        const int thickness = 2; /* Important so 2x2 blocks of color are the same, to avoid Cr/Cb interpolation over pixels */

        /* R, G, B in alternating horizontal bands */
        for (y = 0; y < pattern->h - (thickness - 1); y += thickness) {
            for (i = 0; i < thickness; ++i) {
                p = (Uint8 *)pattern->pixels + (y + i) * pattern->pitch + ((y / thickness) % 3);
                for (x = 0; x < pattern->w; ++x) {
                    *p = 0xFF;
                    p += 3;
                }
            }
        }

        /* Black and white in alternating vertical bands */
        c = 0xFF;
        for (x = 1 * thickness; x < pattern->w; x += 2 * thickness) {
            for (i = 0; i < thickness; ++i) {
                p = (Uint8 *)pattern->pixels + (x + i) * 3;
                for (y = 0; y < pattern->h; ++y) {
                    SDL_memset(p, c, 3);
                    p += pattern->pitch;
                }
            }
            if (c) {
                c = 0x00;
            } else {
                c = 0xFF;
            }
        }
    }
    return pattern;
}

static bool verify_yuv_data(Uint32 format, SDL_Colorspace colorspace, const Uint8 *yuv, int yuv_pitch, SDL_Surface *surface, int tolerance)
{
    const int size = (surface->h * surface->pitch);
    Uint8 *rgb;
    bool result = false;

    rgb = (Uint8 *)SDL_malloc(size);
    if (!rgb) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Out of memory");
        return false;
    }

    if (SDL_ConvertPixelsAndColorspace(surface->w, surface->h, format, colorspace, 0, yuv, yuv_pitch, surface->format, SDL_COLORSPACE_SRGB, 0, rgb, surface->pitch)) {
        int x, y;
        result = true;
        for (y = 0; y < surface->h; ++y) {
            const Uint8 *actual = rgb + y * surface->pitch;
            const Uint8 *expected = (const Uint8 *)surface->pixels + y * surface->pitch;
            for (x = 0; x < surface->w; ++x) {
                int deltaR = (int)actual[0] - expected[0];
                int deltaG = (int)actual[1] - expected[1];
                int deltaB = (int)actual[2] - expected[2];
                int distance = (deltaR * deltaR + deltaG * deltaG + deltaB * deltaB);
                if (distance > tolerance) {
                    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Pixel at %d,%d was 0x%.2x,0x%.2x,0x%.2x, expected 0x%.2x,0x%.2x,0x%.2x, distance = %d", x, y, actual[0], actual[1], actual[2], expected[0], expected[1], expected[2], distance);
                    result = false;
                }
                actual += 3;
                expected += 3;
            }
        }
    } else {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't convert %s to %s: %s", SDL_GetPixelFormatName(format), SDL_GetPixelFormatName(surface->format), SDL_GetError());
    }
    SDL_free(rgb);

    return result;
}

static bool run_automated_tests(int pattern_size, int extra_pitch)
{
    const Uint32 formats[] = {
        SDL_PIXELFORMAT_YV12,
        SDL_PIXELFORMAT_IYUV,
        SDL_PIXELFORMAT_P408,
        SDL_PIXELFORMAT_NV12,
        SDL_PIXELFORMAT_NV21,
        SDL_PIXELFORMAT_YUY2,
        SDL_PIXELFORMAT_UYVY,
        SDL_PIXELFORMAT_YVYU
    };
    int i, j;
    SDL_Surface *pattern = generate_test_pattern(pattern_size);
    const int yuv_len = MAX_YUV_SURFACE_SIZE(pattern->w, pattern->h, extra_pitch);
    Uint8 *yuv1 = (Uint8 *)SDL_malloc(yuv_len);
    Uint8 *yuv2 = (Uint8 *)SDL_malloc(yuv_len);
    int yuv1_pitch, yuv2_pitch;
    YUV_CONVERSION_MODE mode;
    SDL_Colorspace colorspace;
    const int tight_tolerance = 20;
    const int loose_tolerance = 333;
    bool result = false;

    if (!pattern || !yuv1 || !yuv2) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't allocate test surfaces");
        goto done;
    }

    mode = GetYUVConversionModeForResolution(pattern->w, pattern->h);
    colorspace = GetColorspaceForYUVConversionMode(mode);

    /* Verify conversion from YUV formats */
    for (i = 0; i < SDL_arraysize(formats); ++i) {
        if (!ConvertRGBtoYUV(formats[i], pattern->pixels, pattern->pitch, yuv1, pattern->w, pattern->h, mode, 0, 100)) {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "ConvertRGBtoYUV() doesn't support converting to %s", SDL_GetPixelFormatName(formats[i]));
            goto done;
        }
        yuv1_pitch = CalculateYUVPitch(formats[i], pattern->w);
        if (!verify_yuv_data(formats[i], colorspace, yuv1, yuv1_pitch, pattern, tight_tolerance)) {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed conversion from %s to RGB", SDL_GetPixelFormatName(formats[i]));
            goto done;
        }
    }

    /* Verify conversion to YUV formats */
    for (i = 0; i < SDL_arraysize(formats); ++i) {
        yuv1_pitch = CalculateYUVPitch(formats[i], pattern->w) + extra_pitch;
        if (!SDL_ConvertPixelsAndColorspace(pattern->w, pattern->h, pattern->format, SDL_COLORSPACE_SRGB, 0, pattern->pixels, pattern->pitch, formats[i], colorspace, 0, yuv1, yuv1_pitch)) {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't convert %s to %s: %s", SDL_GetPixelFormatName(pattern->format), SDL_GetPixelFormatName(formats[i]), SDL_GetError());
            goto done;
        }
        if (!verify_yuv_data(formats[i], colorspace, yuv1, yuv1_pitch, pattern, tight_tolerance)) {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed conversion from RGB to %s", SDL_GetPixelFormatName(formats[i]));
            goto done;
        }
    }

    /* Verify conversion between YUV formats */
    for (i = 0; i < SDL_arraysize(formats); ++i) {
        for (j = 0; j < SDL_arraysize(formats); ++j) {
            if (formats[i] != formats[j] && (formats[i] == SDL_PIXELFORMAT_P408 || formats[j] == SDL_PIXELFORMAT_P408)) {
                // Converting between 444 and 420 formats is lossy and not currently supported
                continue;
            }

            yuv1_pitch = CalculateYUVPitch(formats[i], pattern->w) + extra_pitch;
            yuv2_pitch = CalculateYUVPitch(formats[j], pattern->w) + extra_pitch;
            if (!SDL_ConvertPixelsAndColorspace(pattern->w, pattern->h, pattern->format, SDL_COLORSPACE_SRGB, 0, pattern->pixels, pattern->pitch, formats[i], colorspace, 0, yuv1, yuv1_pitch)) {
                SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't convert %s to %s: %s", SDL_GetPixelFormatName(pattern->format), SDL_GetPixelFormatName(formats[i]), SDL_GetError());
                goto done;
            }
            if (!SDL_ConvertPixelsAndColorspace(pattern->w, pattern->h, formats[i], colorspace, 0, yuv1, yuv1_pitch, formats[j], colorspace, 0, yuv2, yuv2_pitch)) {
                SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't convert %s to %s: %s", SDL_GetPixelFormatName(formats[i]), SDL_GetPixelFormatName(formats[j]), SDL_GetError());
                goto done;
            }
            if (!verify_yuv_data(formats[j], colorspace, yuv2, yuv2_pitch, pattern, tight_tolerance)) {
                SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed conversion from %s to %s", SDL_GetPixelFormatName(formats[i]), SDL_GetPixelFormatName(formats[j]));
                goto done;
            }
        }
    }

    /* Verify conversion between YUV formats in-place */
    for (i = 0; i < SDL_arraysize(formats); ++i) {
        for (j = 0; j < SDL_arraysize(formats); ++j) {
            if (is_packed_yuv_format(formats[i]) != is_packed_yuv_format(formats[j])) {
                /* Can't change plane vs packed pixel layout in-place */
                continue;
            }

            if (formats[i] != formats[j] && (formats[i] == SDL_PIXELFORMAT_P408 || formats[j] == SDL_PIXELFORMAT_P408)) {
                // Converting between 444 and 420 formats is lossy and not currently supported
                continue;
            }

            yuv1_pitch = CalculateYUVPitch(formats[i], pattern->w) + extra_pitch;
            yuv2_pitch = CalculateYUVPitch(formats[j], pattern->w) + extra_pitch;
            if (!SDL_ConvertPixelsAndColorspace(pattern->w, pattern->h, pattern->format, SDL_COLORSPACE_SRGB, 0, pattern->pixels, pattern->pitch, formats[i], colorspace, 0, yuv1, yuv1_pitch)) {
                SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't convert %s to %s: %s", SDL_GetPixelFormatName(pattern->format), SDL_GetPixelFormatName(formats[i]), SDL_GetError());
                goto done;
            }
            if (!SDL_ConvertPixelsAndColorspace(pattern->w, pattern->h, formats[i], colorspace, 0, yuv1, yuv1_pitch, formats[j], colorspace, 0, yuv1, yuv2_pitch)) {
                SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't convert %s to %s: %s", SDL_GetPixelFormatName(formats[i]), SDL_GetPixelFormatName(formats[j]), SDL_GetError());
                goto done;
            }
            if (!verify_yuv_data(formats[j], colorspace, yuv1, yuv2_pitch, pattern, tight_tolerance)) {
                SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed conversion from %s to %s", SDL_GetPixelFormatName(formats[i]), SDL_GetPixelFormatName(formats[j]));
                goto done;
            }
        }
    }

    /* Verify round trip through BT.2020 */
    colorspace = SDL_COLORSPACE_BT2020_FULL;
    if (!ConvertRGBtoYUV(SDL_PIXELFORMAT_P010, pattern->pixels, pattern->pitch, yuv1, pattern->w, pattern->h, YUV_CONVERSION_BT2020, 0, 100)) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "ConvertRGBtoYUV() doesn't support converting to %s", SDL_GetPixelFormatName(SDL_PIXELFORMAT_P010));
        goto done;
    }
    yuv1_pitch = CalculateYUVPitch(SDL_PIXELFORMAT_P010, pattern->w);
    if (!verify_yuv_data(SDL_PIXELFORMAT_P010, colorspace, yuv1, yuv1_pitch, pattern, tight_tolerance)) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed conversion from %s to RGB", SDL_GetPixelFormatName(SDL_PIXELFORMAT_P010));
        goto done;
    }

    /* The pitch needs to be Uint16 aligned for P010 pixels */
    yuv1_pitch = CalculateYUVPitch(SDL_PIXELFORMAT_P010, pattern->w) + ((extra_pitch + 1) & ~1);
    if (!SDL_ConvertPixelsAndColorspace(pattern->w, pattern->h, pattern->format, SDL_COLORSPACE_SRGB, 0, pattern->pixels, pattern->pitch, SDL_PIXELFORMAT_P010, colorspace, 0, yuv1, yuv1_pitch)) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't convert %s to %s: %s", SDL_GetPixelFormatName(pattern->format), SDL_GetPixelFormatName(SDL_PIXELFORMAT_P010), SDL_GetError());
        goto done;
    }
    /* Going through XRGB2101010 format during P010 conversion is slightly lossy, so use looser tolerance here */
    if (!verify_yuv_data(SDL_PIXELFORMAT_P010, colorspace, yuv1, yuv1_pitch, pattern, loose_tolerance)) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed conversion from RGB to %s", SDL_GetPixelFormatName(SDL_PIXELFORMAT_P010));
        goto done;
    }

    result = true;

done:
    SDL_free(yuv1);
    SDL_free(yuv2);
    SDL_DestroySurface(pattern);
    return result;
}

static bool run_colorspace_test(void)
{
    bool result = false;
    SDL_Window *window;
    SDL_Renderer *renderer;
    struct {
        const char *name;
        SDL_Colorspace colorspace;
    } colorspaces[] = {
#define COLORSPACE(X) { #X, X }
        COLORSPACE(SDL_COLORSPACE_JPEG),
#if 0 /* We don't support converting color primaries here */
        COLORSPACE(SDL_COLORSPACE_BT601_LIMITED),
        COLORSPACE(SDL_COLORSPACE_BT601_FULL),
#endif
        COLORSPACE(SDL_COLORSPACE_BT709_LIMITED),
        COLORSPACE(SDL_COLORSPACE_BT709_FULL)
#undef COLORSPACE
    };
    SDL_Surface *rgb = NULL;
    SDL_Surface *yuv = NULL;
    SDL_Texture *texture = NULL;
    int allowed_error = 2;
    int i;

    if (!SDL_CreateWindowAndRenderer("testyuv", 320, 240, 0, &window, &renderer)) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create window and renderer: %s", SDL_GetError());
        goto done;
    }

    rgb = SDL_CreateSurface(32, 32, SDL_PIXELFORMAT_XRGB8888);
    if (!rgb) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create RGB surface: %s", SDL_GetError());
        goto done;
    }
    SDL_FillSurfaceRect(rgb, NULL, SDL_MapSurfaceRGB(rgb, 255, 0, 0));

    for (i = 0; i < SDL_arraysize(colorspaces); ++i) {
        bool next = false;
        Uint8 r, g, b, a;

        SDL_Log("Checking colorspace %s", colorspaces[i].name);

        yuv = SDL_ConvertSurfaceAndColorspace(rgb, SDL_PIXELFORMAT_NV12, NULL, colorspaces[i].colorspace, 0);
        if (!yuv) {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create YUV surface: %s", SDL_GetError());
            goto done;
        }

        if (!SDL_ReadSurfacePixel(yuv, 0, 0, &r, &g, &b, &a)) {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't read YUV surface: %s", SDL_GetError());
            goto done;
        }

        if (SDL_abs((int)r - 255) > allowed_error ||
            SDL_abs((int)g - 0) > allowed_error ||
            SDL_abs((int)b - 0) > allowed_error) {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed color conversion, expected 255,0,0, got %d,%d,%d", r, g, b);
        }

        texture = SDL_CreateTextureFromSurface(renderer, yuv);
        if (!texture) {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create YUV texture: %s", SDL_GetError());
            goto done;
        }

        SDL_DestroySurface(yuv);
        yuv = NULL;

        SDL_RenderTexture(renderer, texture, NULL, NULL);
        yuv = SDL_RenderReadPixels(renderer, NULL);

        if (!SDL_ReadSurfacePixel(yuv, 0, 0, &r, &g, &b, &a)) {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't read YUV surface: %s", SDL_GetError());
            goto done;
        }

        if (SDL_abs((int)r - 255) > allowed_error ||
            SDL_abs((int)g - 0) > allowed_error ||
            SDL_abs((int)b - 0) > allowed_error) {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed renderer color conversion, expected 255,0,0, got %d,%d,%d", r, g, b);
        }

        while (!next) {
            SDL_Event event;
            while (SDL_PollEvent(&event)) {
                switch (event.type) {
                case SDL_EVENT_KEY_DOWN:
                    if (event.key.key == SDLK_ESCAPE) {
                        result = true;
                        goto done;
                    }
                    if (event.key.key == SDLK_SPACE) {
                        next = true;
                    }
                    break;
                case SDL_EVENT_QUIT:
                    result = true;
                    goto done;
                }
            }

            SDL_RenderTexture(renderer, texture, NULL, NULL);
            SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
            SDL_RenderDebugText(renderer, 4, 4, colorspaces[i].name);
            SDL_RenderPresent(renderer);
            SDL_Delay(10);
        }

        SDL_DestroyTexture(texture);
        texture = NULL;
    }

    result = true;

done:
    SDL_DestroySurface(rgb);
    SDL_DestroySurface(yuv);
    SDL_DestroyTexture(texture);
    SDL_Quit();
    return result;
}

static bool create_textures(SDL_Renderer *renderer, SDL_Surface *original, SDL_PixelFormat yuv_format, SDL_PixelFormat rgb_format, bool planar, bool monochrome, int luminance, SDL_Texture *output[3])
{
    SDL_Colorspace rgb_colorspace = SDL_COLORSPACE_SRGB;
    SDL_Colorspace yuv_colorspace;
    Uint8 *raw_yuv = NULL;
    int pitch;
    SDL_Surface *converted = NULL;
    bool result = false;
    size_t max_size;

    YUV_CONVERSION_MODE yuv_mode = GetYUVConversionModeForResolution(original->w, original->h);
    if (yuv_mode == YUV_CONVERSION_BT2020) {
        rgb_colorspace = SDL_COLORSPACE_HDR10;
    }
    yuv_colorspace = GetColorspaceForYUVConversionMode(yuv_mode);

    max_size = MAX_YUV_SURFACE_SIZE(original->w, original->h, 0);
    raw_yuv = SDL_calloc(1, max_size);
    ConvertRGBtoYUV(yuv_format, original->pixels, original->pitch, raw_yuv, original->w, original->h, yuv_mode, monochrome, luminance);
    pitch = CalculateYUVPitch(yuv_format, original->w);

    converted = SDL_CreateSurface(original->w, original->h, rgb_format);
    if (!converted) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create converted surface: %s", SDL_GetError());
        goto done;
    }
    SDL_ConvertPixelsAndColorspace(original->w, original->h, yuv_format, yuv_colorspace, 0, raw_yuv, pitch, rgb_format, rgb_colorspace, 0, converted->pixels, converted->pitch);

    output[0] = SDL_CreateTextureFromSurface(renderer, original);
    output[1] = SDL_CreateTextureFromSurface(renderer, converted);
    SDL_PropertiesID props = SDL_CreateProperties();
    SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER, yuv_colorspace);
    SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER, yuv_format);
    SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER, SDL_TEXTUREACCESS_STREAMING);
    SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER, original->w);
    SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER, original->h);
    output[2] = SDL_CreateTextureWithProperties(renderer, props);
    SDL_DestroyProperties(props);
    if (!output[0] || !output[1] || !output[2]) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't set create texture: %s", SDL_GetError());
        goto done;
    }
    if (planar && (yuv_format == SDL_PIXELFORMAT_YV12 || yuv_format == SDL_PIXELFORMAT_IYUV)) {
        const int Yrows = original->h;
        const int UVrows = ((original->h + 1) / 2);
        const int src_Ypitch = pitch;
        const int src_UVpitch = ((pitch + 1) / 2);
        const Uint8 *src_plane0 = (const Uint8 *)raw_yuv;
        const Uint8 *src_plane1 = src_plane0 + Yrows * src_Ypitch;
        const Uint8 *src_plane2 = src_plane1 + UVrows * src_UVpitch;
        const int Ypitch = pitch + 37;
        const int UVpitch = ((Ypitch + 1) / 2);
        Uint8 *plane0 = (Uint8 *)SDL_calloc(1, Yrows * Ypitch);
        Uint8 *plane1 = (Uint8 *)SDL_calloc(1, UVrows * UVpitch);
        Uint8 *plane2 = (Uint8 *)SDL_calloc(1, UVrows * UVpitch);
        int row;
        const Uint8 *src;
        Uint8 *dst;

        if (!plane0 || !plane1 || !plane0) {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create YUV planes: %s", SDL_GetError());
            goto done;
        }

        src = src_plane0;
        dst = plane0;
        for (row = 0; row < Yrows; ++row) {
            SDL_memcpy(dst, src, src_Ypitch);
            src += src_Ypitch;
            dst += Ypitch;
        }

        src = src_plane1;
        dst = plane1;
        for (row = 0; row < UVrows; ++row) {
            SDL_memcpy(dst, src, src_UVpitch);
            src += src_UVpitch;
            dst += UVpitch;
        }

        src = src_plane2;
        dst = plane2;
        for (row = 0; row < UVrows; ++row) {
            SDL_memcpy(dst, src, src_UVpitch);
            src += src_UVpitch;
            dst += UVpitch;
        }

        if (yuv_format == SDL_PIXELFORMAT_YV12) {
            SDL_UpdateYUVTexture(output[2], NULL, plane0, Ypitch, plane2, UVpitch, plane1, UVpitch);
        } else {
            SDL_UpdateYUVTexture(output[2], NULL, plane0, Ypitch, plane1, UVpitch, plane2, UVpitch);
        }
        SDL_free(plane0);
        SDL_free(plane1);
        SDL_free(plane2);
    } else if (planar && (yuv_format == SDL_PIXELFORMAT_P408 || yuv_format == SDL_PIXELFORMAT_P416)) {
        const int rows = original->h;
        const Uint8 *src_plane0 = (const Uint8 *)raw_yuv;
        const Uint8 *src_plane1 = src_plane0 + rows * pitch;
        const Uint8 *src_plane2 = src_plane1 + rows * pitch;
        const int Ypitch = pitch + 37;
        const int UVpitch = Ypitch;
        Uint8 *plane0 = (Uint8 *)SDL_calloc(1, rows * Ypitch);
        Uint8 *plane1 = (Uint8 *)SDL_calloc(1, rows * UVpitch);
        Uint8 *plane2 = (Uint8 *)SDL_calloc(1, rows * UVpitch);
        int row;
        const Uint8 *src;
        Uint8 *dst;

        if (!plane0 || !plane1 || !plane0) {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create YUV planes: %s", SDL_GetError());
            goto done;
        }

        src = src_plane0;
        dst = plane0;
        for (row = 0; row < rows; ++row) {
            SDL_memcpy(dst, src, pitch);
            src += pitch;
            dst += Ypitch;
        }

        src = src_plane1;
        dst = plane1;
        for (row = 0; row < rows; ++row) {
            SDL_memcpy(dst, src, pitch);
            src += pitch;
            dst += UVpitch;
        }

        src = src_plane2;
        dst = plane2;
        for (row = 0; row < rows; ++row) {
            SDL_memcpy(dst, src, pitch);
            src += pitch;
            dst += UVpitch;
        }

        SDL_UpdateYUVTexture(output[2], NULL, plane0, Ypitch, plane1, UVpitch, plane2, UVpitch);
        SDL_free(plane0);
        SDL_free(plane1);
        SDL_free(plane2);
    } else if (planar && (yuv_format == SDL_PIXELFORMAT_NV12 || yuv_format == SDL_PIXELFORMAT_NV21 || yuv_format == SDL_PIXELFORMAT_P010)) {
        const int Yrows = original->h;
        const int UVrows = ((original->h + 1) / 2);
        const int src_Ypitch = pitch;
        const int src_UVpitch = (yuv_format == SDL_PIXELFORMAT_P010) ? ((pitch + 3) & ~3) : ((pitch + 1) & ~1);
        const Uint8 *src_plane0 = (const Uint8 *)raw_yuv;
        const Uint8 *src_plane1 = src_plane0 + Yrows * src_Ypitch;
        const int Ypitch = pitch + 37;
        const int UVpitch = ((Ypitch + 1) / 2) * 2;
        Uint8 *plane0 = (Uint8 *)SDL_calloc(1, Yrows * Ypitch);
        Uint8 *plane1 = (Uint8 *)SDL_calloc(1, UVrows * UVpitch);
        int row;
        const Uint8 *src;
        Uint8 *dst;

        if (!plane0 || !plane1) {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create YUV planes: %s", SDL_GetError());
            goto done;
        }

        src = src_plane0;
        dst = plane0;
        for (row = 0; row < Yrows; ++row) {
            SDL_memcpy(dst, src, src_Ypitch);
            src += src_Ypitch;
            dst += Ypitch;
        }

        src = src_plane1;
        dst = plane1;
        for (row = 0; row < UVrows; ++row) {
            SDL_memcpy(dst, src, src_UVpitch);
            src += src_UVpitch;
            dst += UVpitch;
        }

        SDL_UpdateNVTexture(output[2], NULL, plane0, Ypitch, plane1, UVpitch);
        SDL_free(plane0);
        SDL_free(plane1);
    } else {
        SDL_UpdateTexture(output[2], NULL, raw_yuv, pitch);
    }

    result = true;

done:
    SDL_DestroySurface(converted);
    SDL_free(raw_yuv);
    return result;
}

static bool has_10bit_texture_format(SDL_Renderer *renderer)
{
    const SDL_PixelFormat *texture_formats = (const SDL_PixelFormat *)SDL_GetPointerProperty(SDL_GetRendererProperties(renderer), SDL_PROP_RENDERER_TEXTURE_FORMATS_POINTER, NULL);
    if (texture_formats) {
        for (int i = 0; texture_formats[i] != SDL_PIXELFORMAT_UNKNOWN; ++i) {
            if (SDL_ISPIXELFORMAT_10BIT(texture_formats[i])) {
                return true;
            }
        }
    }
    return false;
}

static bool check_output(SDL_Renderer *renderer, SDL_Surface *original, SDL_Texture *texture)
{
    // Clear to yellow to clearly see unfilled pixels
    SDL_SetRenderDrawColor(renderer, 255, 255, 0, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(renderer);

    SDL_Rect rect = { 0, 0, texture->w, texture->h };
    SDL_FRect frect = { 0.0f, 0.0f, (float)texture->w, (float)texture->h };
    SDL_RenderTexture(renderer, texture, &frect, &frect);
    SDL_Surface *output = SDL_RenderReadPixels(renderer, &rect);
    if (!output) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't read pixels: %s", SDL_GetError());
        return false;
    }
    SDL_RenderPresent(renderer);

    // Allow some error for colorspace conversion and differences in color depth
    const int MAX_ALLOWABLE_ERROR = 4096;
    bool result;
    if (SDLTest_CompareSurfaces(output, original, MAX_ALLOWABLE_ERROR) == 0) {
        result = true;
    } else {
        result = false;
    }
    SDL_DestroySurface(output);

    return result;
}

static bool run_single_format_test(SDL_Renderer *renderer, SDL_Surface *original, SDL_PixelFormat yuv_format, SDL_PixelFormat rgb_format, bool planar)
{
    SDL_Texture *output[3];
    bool result = true;

    if (!create_textures(renderer, original, yuv_format, rgb_format, planar, false, 100, output)) {
        return false;
    }

    if (!check_output(renderer, original, output[0])) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Original texture didn't match source data, failing");
        result = false;
    }

    if (!check_output(renderer, original, output[1])) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "RGB output didn't match source data, failing");
        result = false;
    }

    if (!check_output(renderer, original, output[2])) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "YUV output didn't match source data, failing");
        result = false;
    }

    for (int i = 0; i < SDL_arraysize(output); ++i) {
        SDL_DestroyTexture(output[i]);
    }
    return result;
}

static bool run_all_format_test(SDL_Window *window, const char *requested_renderer, SDL_Surface *original)
{
    const SDL_PixelFormat yuv_formats[] = {
        SDL_PIXELFORMAT_YV12,
        SDL_PIXELFORMAT_IYUV,
        SDL_PIXELFORMAT_P408,
        SDL_PIXELFORMAT_YUY2,
        SDL_PIXELFORMAT_UYVY,
        SDL_PIXELFORMAT_YVYU,
        SDL_PIXELFORMAT_NV12,
        SDL_PIXELFORMAT_NV21
    };
    const SDL_PixelFormat rgb_formats[] = {
        SDL_PIXELFORMAT_XRGB1555,
        SDL_PIXELFORMAT_RGB565,
        SDL_PIXELFORMAT_RGB24,
        SDL_PIXELFORMAT_ABGR8888,
        SDL_PIXELFORMAT_RGBA8888,
        SDL_PIXELFORMAT_BGRA8888
    };
    const struct
    {
        YUV_CONVERSION_MODE mode;
        const char *name;
    }  colorspaces[] = {
        { YUV_CONVERSION_JPEG, "JPEG" },
        { YUV_CONVERSION_BT601, "BT601" },
        { YUV_CONVERSION_BT709, "BT709" },
        { YUV_CONVERSION_BT2020, "BT2020" }
    };
    bool quit = false;
    bool result = true;

    for (int i = 0; i < SDL_GetNumRenderDrivers() && !quit; ++i) {
        const char *renderer_name = SDL_GetRenderDriver(i);
		if (requested_renderer && SDL_strcmp(renderer_name, requested_renderer) != 0) {
			continue;
		}

        SDL_Renderer *renderer = SDL_CreateRenderer(window, renderer_name);
        if (!renderer) {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create %s renderer: %s", renderer_name, SDL_GetError());
            result = false;
            continue;
        }

        for (int j = 0; j < SDL_arraysize(colorspaces) && !quit; ++j) {
            if (colorspaces[j].mode == YUV_CONVERSION_BT2020 &&
                !has_10bit_texture_format(renderer)) {
                SDL_Log("Skipping %s %s, unsupported", renderer_name, colorspaces[j].name);
                continue;
            }
            SetYUVConversionMode(colorspaces[j].mode);

            for (int m = 0; m < SDL_arraysize(yuv_formats) && !quit; ++m) {
                SDL_PixelFormat yuv_format = yuv_formats[m];
                for (int n = 0; n < SDL_arraysize(rgb_formats) && !quit; ++n) {
                    SDL_PixelFormat rgb_format = rgb_formats[n];

                    SDL_Log("Testing: %s %s %s %s (planar)", renderer_name, colorspaces[j].name, SDL_GetPixelFormatName(yuv_format), SDL_GetPixelFormatName(rgb_format));
                    result &= run_single_format_test(renderer, original, yuv_format, rgb_format, true);

                    SDL_Log("Testing: %s %s %s %s (packed)", renderer_name, colorspaces[j].name, SDL_GetPixelFormatName(yuv_format), SDL_GetPixelFormatName(rgb_format));
                    result &= run_single_format_test(renderer, original, yuv_format, rgb_format, false);

                    SDL_Event event;
                    while (SDL_PollEvent(&event)) {
                        if (event.type == SDL_EVENT_QUIT ||
                            (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE)) {
                            quit = true;
                        }
                    }
                }
            }
        }
        SDL_DestroyRenderer(renderer);
    }
    return result;
}

static bool run_interactive(SDL_Window *window, const char *renderer_name, SDL_Surface *original, SDL_PixelFormat yuv_format, SDL_PixelFormat rgb_format, bool planar, bool monochrome, int luminance)
{
    const char *titles[3] = { "ORIGINAL", "SOFTWARE", "HARDWARE" };
    char title[128];
    const char *yuv_mode_name;
    YUV_CONVERSION_MODE yuv_mode;
    const char *yuv_format_name;
    int current = 0;
    bool quit = false;
    bool result = false;

    SDL_Renderer *renderer = SDL_CreateRenderer(window, renderer_name);
    if (!renderer) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create renderer: %s", SDL_GetError());
        return false;
    }
    renderer_name = SDL_GetRendererName(renderer);

    SDL_Texture *output[3];
    if (!create_textures(renderer, original, yuv_format, rgb_format, planar, monochrome, luminance, output)) {
        goto done;
    }

    yuv_mode = GetYUVConversionModeForResolution(original->w, original->h);
    switch (yuv_mode) {
    case YUV_CONVERSION_JPEG:
        yuv_mode_name = "JPEG";
        break;
    case YUV_CONVERSION_BT601:
        yuv_mode_name = "BT.601";
        break;
    case YUV_CONVERSION_BT709:
        yuv_mode_name = "BT.709";
        break;
    case YUV_CONVERSION_BT2020:
        yuv_mode_name = "BT.2020";
        break;
    default:
        yuv_mode_name = "UNKNOWN";
        break;
    }

    yuv_format_name = SDL_GetPixelFormatName(yuv_format);
    if (SDL_strncmp(yuv_format_name, "SDL_PIXELFORMAT_", 16) == 0) {
        yuv_format_name += 16;
    }

    while (!quit) {
        SDL_Event event;
        while (SDL_PollEvent(&event) > 0) {
            if (event.type == SDL_EVENT_QUIT) {
                quit = true;
            }
            if (event.type == SDL_EVENT_KEY_DOWN) {
                if (event.key.key == SDLK_ESCAPE) {
                    quit = true;
                } else if (event.key.key == SDLK_LEFT) {
                    --current;
                } else if (event.key.key == SDLK_RIGHT) {
                    ++current;
                }
            }
            if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
                if (event.button.x < (original->w / 2)) {
                    --current;
                } else {
                    ++current;
                }
            }
        }

        /* Handle wrapping */
        if (current < 0) {
            current += SDL_arraysize(output);
        }
        if (current >= SDL_arraysize(output)) {
            current -= SDL_arraysize(output);
        }

        SDL_RenderClear(renderer);
        SDL_RenderTexture(renderer, output[current], NULL, NULL);
        SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF);
        if (current == 0) {
            SDLTest_DrawString(renderer, 4, 4, titles[current]);
        } else {
            if (SDL_snprintf(title, sizeof(title), "%s %s %s %s", titles[current], yuv_format_name, yuv_mode_name, renderer_name) > 0) {
                SDLTest_DrawString(renderer, 4, 4, title);
            }
        }
        SDL_RenderPresent(renderer);
        SDL_Delay(10);
    }

    result = true;

done:
    for (int i = 0; i < SDL_arraysize(output); ++i) {
        SDL_DestroyTexture(output[i]);
    }
    SDLTest_CleanupTextDrawing();
    SDL_DestroyRenderer(renderer);
    return result;
}

int main(int argc, char **argv)
{
    static const struct
    {
        bool enable_intrinsics;
        int pattern_size;
        int extra_pitch;
    } automated_test_params[] = {
        /* Test: single pixel */
        { false, 1, 0 },
        /* Test: even width and height */
        { false, 2, 0 },
        { false, 4, 0 },
        /* Test: odd width and height */
        { false, 1, 0 },
        { false, 3, 0 },
        /* Test: even width and height, extra pitch */
        { false, 2, 3 },
        { false, 4, 3 },
        /* Test: odd width and height, extra pitch */
        { false, 1, 3 },
        { false, 3, 3 },
        /* Test: even width and height with intrinsics */
        { true, 32, 0 },
        /* Test: odd width and height with intrinsics */
        { true, 33, 0 },
        { true, 37, 0 },
        /* Test: even width and height with intrinsics, extra pitch */
        { true, 32, 3 },
        /* Test: odd width and height with intrinsics, extra pitch */
        { true, 33, 3 },
        { true, 37, 3 },
    };
    char *filename = NULL;
    SDL_Surface *original = NULL;
    SDL_Surface *png = NULL;
    SDL_Window *window = NULL;
    const char *renderer_name = NULL;
    Uint32 yuv_format = SDL_PIXELFORMAT_YV12;
    Uint32 rgb_format = SDL_PIXELFORMAT_RGBX8888;
    bool planar = false;
    bool monochrome = false;
    int luminance = 100;
    int i;
    bool should_run_automated_tests = false;
    bool should_run_colorspace_test = false;
    bool should_test_all_formats = false;
    SDLTest_CommonState *state;
    int result = 0;

    /* Initialize test framework */
    state = SDLTest_CommonCreateState(argv, 0);
    if (!state) {
        return 1;
    }

    /* Parse command line */
    for (i = 1; i < argc;) {
        int consumed;

        consumed = SDLTest_CommonArg(state, i);
        if (!consumed) {
            if (SDL_strcmp(argv[i], "--all") == 0) {
                should_test_all_formats = true;
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--jpeg") == 0) {
                SetYUVConversionMode(YUV_CONVERSION_JPEG);
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--bt601") == 0) {
                SetYUVConversionMode(YUV_CONVERSION_BT601);
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--bt709") == 0) {
                SetYUVConversionMode(YUV_CONVERSION_BT709);
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--auto") == 0) {
                SetYUVConversionMode(YUV_CONVERSION_AUTOMATIC);
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--yv12") == 0) {
                yuv_format = SDL_PIXELFORMAT_YV12;
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--iyuv") == 0) {
                yuv_format = SDL_PIXELFORMAT_IYUV;
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--p408") == 0) {
                yuv_format = SDL_PIXELFORMAT_P408;
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--yuy2") == 0) {
                yuv_format = SDL_PIXELFORMAT_YUY2;
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--uyvy") == 0) {
                yuv_format = SDL_PIXELFORMAT_UYVY;
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--yvyu") == 0) {
                yuv_format = SDL_PIXELFORMAT_YVYU;
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--nv12") == 0) {
                yuv_format = SDL_PIXELFORMAT_NV12;
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--nv21") == 0) {
                yuv_format = SDL_PIXELFORMAT_NV21;
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--p010") == 0) {
                yuv_format = SDL_PIXELFORMAT_P010;
                rgb_format = SDL_PIXELFORMAT_XBGR2101010;
                SetYUVConversionMode(YUV_CONVERSION_BT2020);
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--p416") == 0) {
                yuv_format = SDL_PIXELFORMAT_P416;
                rgb_format = SDL_PIXELFORMAT_XBGR2101010;
                SetYUVConversionMode(YUV_CONVERSION_BT2020);
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--rgb555") == 0) {
                rgb_format = SDL_PIXELFORMAT_XRGB1555;
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--rgb565") == 0) {
                rgb_format = SDL_PIXELFORMAT_RGB565;
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--rgb24") == 0) {
                rgb_format = SDL_PIXELFORMAT_RGB24;
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--argb") == 0) {
                rgb_format = SDL_PIXELFORMAT_ARGB8888;
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--abgr") == 0) {
                rgb_format = SDL_PIXELFORMAT_ABGR8888;
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--rgba") == 0) {
                rgb_format = SDL_PIXELFORMAT_RGBA8888;
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--bgra") == 0) {
                rgb_format = SDL_PIXELFORMAT_BGRA8888;
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--planar") == 0) {
                planar = true;
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--monochrome") == 0) {
                monochrome = true;
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--luminance") == 0 && argv[i+1]) {
                luminance = SDL_atoi(argv[i+1]);
                consumed = 2;
            } else if (SDL_strcmp(argv[i], "--automated") == 0) {
                should_run_automated_tests = true;
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--colorspace-test") == 0) {
                should_run_colorspace_test = true;
                consumed = 1;
            } else if (SDL_strcmp(argv[i], "--renderer") == 0 && argv[i + 1]) {
                renderer_name = argv[i + 1];
                consumed = 2;
            } else if (!filename) {
                filename = argv[i];
                consumed = 1;
            }
        }
        if (consumed <= 0) {
            static const char *options[] = {
                "[--jpeg|--bt601|--bt709|--auto]",
                "[--yv12|--iyuv|--p408|--yuy2|--uyvy|--yvyu|--nv12|--nv21|--p010|--p416]",
                "[--rgb555|--rgb565|--rgb24|--argb|--abgr|--rgba|--bgra]",
                "[--monochrome] [--luminance N%] [--planar]",
                "[--automated] [--colorspace-test] [--renderer NAME]",
                "[sample.png]",
                NULL,
            };
            SDLTest_CommonLogUsage(state, argv[0], options);
            SDL_Quit();
            SDLTest_CommonDestroyState(state);
            return 1;
        }
        i += consumed;
    }

    /* Run automated tests */
    if (should_run_automated_tests) {
        for (i = 0; i < (int)SDL_arraysize(automated_test_params); ++i) {
            SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Running automated test, pattern size %d, extra pitch %d, intrinsics %s",
                        automated_test_params[i].pattern_size,
                        automated_test_params[i].extra_pitch,
                        automated_test_params[i].enable_intrinsics ? "enabled" : "disabled");
            if (!run_automated_tests(automated_test_params[i].pattern_size, automated_test_params[i].extra_pitch)) {
                result = 2;
            }
        }
        goto done;
    }

    if (should_run_colorspace_test) {
        if (!run_colorspace_test()) {
            result = 2;
        }
        goto done;
    }

    filename = GetResourceFilename(filename, "testyuv.png");
    png = SDL_LoadSurface(filename);
    if (png) {
        original = SDL_ConvertSurface(png, SDL_PIXELFORMAT_RGB24);
        SDL_DestroySurface(png);
    }
    if (!original) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't load %s: %s", filename, SDL_GetError());
        result = 3;
        goto done;
    }

    window = SDL_CreateWindow("YUV test", original->w, original->h, 0);
    if (!window) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create window: %s", SDL_GetError());
        result = 4;
        goto done;
    }

    if (should_test_all_formats) {
        if (!run_all_format_test(window, renderer_name, original)) {
            result = 5;
        }
    } else {
        if (!run_interactive(window, renderer_name, original, yuv_format, rgb_format, planar, monochrome, luminance)) {
            result = 5;
        }
    }

done:
	SDL_free(filename);
    SDL_DestroySurface(original);
    SDL_DestroyWindow(window);
    SDL_Quit();
    SDLTest_CommonDestroyState(state);
    return result;
}
