/*
  Copyright (C) 1997-2025 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.
*/
/* This is a simple example of using GLSL shaders with SDL */

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3/SDL_test.h>

#include "testutils.h"

#include <stdlib.h>

#ifdef HAVE_OPENGL

#include <SDL3/SDL_opengl.h>

static bool shaders_supported;
static int current_shader = 0;

enum
{
    SHADER_COLOR,
    SHADER_TEXTURE,
    SHADER_TEXCOORDS,
    NUM_SHADERS
};

typedef struct
{
    GLhandleARB program;
    GLhandleARB vert_shader;
    GLhandleARB frag_shader;
    const char *vert_source;
    const char *frag_source;
} ShaderData;

static ShaderData shaders[NUM_SHADERS] = {

    /* SHADER_COLOR */
    { 0, 0, 0,
      /* vertex shader */
      "varying vec4 v_color;\n"
      "\n"
      "void main()\n"
      "{\n"
      "    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;\n"
      "    v_color = gl_Color;\n"
      "}",
      /* fragment shader */
      "varying vec4 v_color;\n"
      "\n"
      "void main()\n"
      "{\n"
      "    gl_FragColor = v_color;\n"
      "}" },

    /* SHADER_TEXTURE */
    { 0, 0, 0,
      /* vertex shader */
      "varying vec4 v_color;\n"
      "varying vec2 v_texCoord;\n"
      "\n"
      "void main()\n"
      "{\n"
      "    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;\n"
      "    v_color = gl_Color;\n"
      "    v_texCoord = vec2(gl_MultiTexCoord0);\n"
      "}",
      /* fragment shader */
      "varying vec4 v_color;\n"
      "varying vec2 v_texCoord;\n"
      "uniform sampler2D tex0;\n"
      "\n"
      "void main()\n"
      "{\n"
      "    gl_FragColor = texture2D(tex0, v_texCoord) * v_color;\n"
      "}" },

    /* SHADER_TEXCOORDS */
    { 0, 0, 0,
      /* vertex shader */
      "varying vec2 v_texCoord;\n"
      "\n"
      "void main()\n"
      "{\n"
      "    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;\n"
      "    v_texCoord = vec2(gl_MultiTexCoord0);\n"
      "}",
      /* fragment shader */
      "varying vec2 v_texCoord;\n"
      "\n"
      "void main()\n"
      "{\n"
      "    vec4 color;\n"
      "    vec2 delta;\n"
      "    float dist;\n"
      "\n"
      "    delta = vec2(0.5, 0.5) - v_texCoord;\n"
      "    dist = dot(delta, delta);\n"
      "\n"
      "    color.r = v_texCoord.x;\n"
      "    color.g = v_texCoord.x * v_texCoord.y;\n"
      "    color.b = v_texCoord.y;\n"
      "    color.a = 1.0 - (dist * 4.0);\n"
      "    gl_FragColor = color;\n"
      "}" },
};

static PFNGLATTACHOBJECTARBPROC pglAttachObjectARB;
static PFNGLCOMPILESHADERARBPROC pglCompileShaderARB;
static PFNGLCREATEPROGRAMOBJECTARBPROC pglCreateProgramObjectARB;
static PFNGLCREATESHADEROBJECTARBPROC pglCreateShaderObjectARB;
static PFNGLDELETEOBJECTARBPROC pglDeleteObjectARB;
static PFNGLGETINFOLOGARBPROC pglGetInfoLogARB;
static PFNGLGETOBJECTPARAMETERIVARBPROC pglGetObjectParameterivARB;
static PFNGLGETUNIFORMLOCATIONARBPROC pglGetUniformLocationARB;
static PFNGLLINKPROGRAMARBPROC pglLinkProgramARB;
static PFNGLSHADERSOURCEARBPROC pglShaderSourceARB;
static PFNGLUNIFORM1IARBPROC pglUniform1iARB;
static PFNGLUSEPROGRAMOBJECTARBPROC pglUseProgramObjectARB;

static bool CompileShader(GLhandleARB shader, const char *source)
{
    GLint status = 0;

    pglShaderSourceARB(shader, 1, &source, NULL);
    pglCompileShaderARB(shader);
    pglGetObjectParameterivARB(shader, GL_OBJECT_COMPILE_STATUS_ARB, &status);
    if (status == 0) {
        GLint length = 0;
        char *info;

        pglGetObjectParameterivARB(shader, GL_OBJECT_INFO_LOG_LENGTH_ARB, &length);
        info = (char *)SDL_malloc((size_t)length + 1);
        if (!info) {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Out of memory!");
        } else {
            pglGetInfoLogARB(shader, length, NULL, info);
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to compile shader:");
	    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "%s", source);
	    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "%s", info);
            SDL_free(info);
        }
        return false;
    } else {
        return true;
    }
}

static bool LinkProgram(ShaderData *data)
{
    GLint status = 0;

    pglAttachObjectARB(data->program, data->vert_shader);
    pglAttachObjectARB(data->program, data->frag_shader);
    pglLinkProgramARB(data->program);

    pglGetObjectParameterivARB(data->program, GL_OBJECT_LINK_STATUS_ARB, &status);
    if (status == 0) {
        GLint length = 0;
        char *info;

        pglGetObjectParameterivARB(data->program, GL_OBJECT_INFO_LOG_LENGTH_ARB, &length);
        info = (char *)SDL_malloc((size_t)length + 1);
        if (!info) {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Out of memory!");
        } else {
            pglGetInfoLogARB(data->program, length, NULL, info);
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to link program:");
	    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "%s", info);
            SDL_free(info);
        }
        return false;
    } else {
        return true;
    }
}

static bool CompileShaderProgram(ShaderData *data)
{
    const int num_tmus_bound = 4;
    int i;
    GLint location;

    glGetError();

    /* Create one program object to rule them all */
    data->program = pglCreateProgramObjectARB();

    /* Create the vertex shader */
    data->vert_shader = pglCreateShaderObjectARB(GL_VERTEX_SHADER_ARB);
    if (!CompileShader(data->vert_shader, data->vert_source)) {
        return false;
    }

    /* Create the fragment shader */
    data->frag_shader = pglCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);
    if (!CompileShader(data->frag_shader, data->frag_source)) {
        return false;
    }

    /* ... and in the darkness bind them */
    if (!LinkProgram(data)) {
        return false;
    }

    /* Set up some uniform variables */
    pglUseProgramObjectARB(data->program);
    for (i = 0; i < num_tmus_bound; ++i) {
        char tex_name[5];
        (void)SDL_snprintf(tex_name, SDL_arraysize(tex_name), "tex%d", i);
        location = pglGetUniformLocationARB(data->program, tex_name);
        if (location >= 0) {
            pglUniform1iARB(location, i);
        }
    }
    pglUseProgramObjectARB(0);

    return (glGetError() == GL_NO_ERROR);
}

static void DestroyShaderProgram(ShaderData *data)
{
    if (shaders_supported) {
        pglDeleteObjectARB(data->vert_shader);
        pglDeleteObjectARB(data->frag_shader);
        pglDeleteObjectARB(data->program);
    }
}

static bool InitShaders(void)
{
    int i;

    /* Check for shader support */
    shaders_supported = false;
    if (SDL_GL_ExtensionSupported("GL_ARB_shader_objects") &&
        SDL_GL_ExtensionSupported("GL_ARB_shading_language_100") &&
        SDL_GL_ExtensionSupported("GL_ARB_vertex_shader") &&
        SDL_GL_ExtensionSupported("GL_ARB_fragment_shader")) {
        pglAttachObjectARB = (PFNGLATTACHOBJECTARBPROC)SDL_GL_GetProcAddress("glAttachObjectARB");
        pglCompileShaderARB = (PFNGLCOMPILESHADERARBPROC)SDL_GL_GetProcAddress("glCompileShaderARB");
        pglCreateProgramObjectARB = (PFNGLCREATEPROGRAMOBJECTARBPROC)SDL_GL_GetProcAddress("glCreateProgramObjectARB");
        pglCreateShaderObjectARB = (PFNGLCREATESHADEROBJECTARBPROC)SDL_GL_GetProcAddress("glCreateShaderObjectARB");
        pglDeleteObjectARB = (PFNGLDELETEOBJECTARBPROC)SDL_GL_GetProcAddress("glDeleteObjectARB");
        pglGetInfoLogARB = (PFNGLGETINFOLOGARBPROC)SDL_GL_GetProcAddress("glGetInfoLogARB");
        pglGetObjectParameterivARB = (PFNGLGETOBJECTPARAMETERIVARBPROC)SDL_GL_GetProcAddress("glGetObjectParameterivARB");
        pglGetUniformLocationARB = (PFNGLGETUNIFORMLOCATIONARBPROC)SDL_GL_GetProcAddress("glGetUniformLocationARB");
        pglLinkProgramARB = (PFNGLLINKPROGRAMARBPROC)SDL_GL_GetProcAddress("glLinkProgramARB");
        pglShaderSourceARB = (PFNGLSHADERSOURCEARBPROC)SDL_GL_GetProcAddress("glShaderSourceARB");
        pglUniform1iARB = (PFNGLUNIFORM1IARBPROC)SDL_GL_GetProcAddress("glUniform1iARB");
        pglUseProgramObjectARB = (PFNGLUSEPROGRAMOBJECTARBPROC)SDL_GL_GetProcAddress("glUseProgramObjectARB");
        if (pglAttachObjectARB &&
            pglCompileShaderARB &&
            pglCreateProgramObjectARB &&
            pglCreateShaderObjectARB &&
            pglDeleteObjectARB &&
            pglGetInfoLogARB &&
            pglGetObjectParameterivARB &&
            pglGetUniformLocationARB &&
            pglLinkProgramARB &&
            pglShaderSourceARB &&
            pglUniform1iARB &&
            pglUseProgramObjectARB) {
            shaders_supported = true;
        }
    }

    if (!shaders_supported) {
        return false;
    }

    /* Compile all the shaders */
    for (i = 0; i < NUM_SHADERS; ++i) {
        if (!CompileShaderProgram(&shaders[i])) {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to compile shader!");
            return false;
        }
    }

    /* We're done! */
    return true;
}

static void QuitShaders(void)
{
    int i;

    for (i = 0; i < NUM_SHADERS; ++i) {
        DestroyShaderProgram(&shaders[i]);
    }
}

/* Quick utility function for texture creation */
static int
power_of_two(int input)
{
    int value = 1;

    while (value < input) {
        value <<= 1;
    }
    return value;
}

static GLuint
SDL_GL_LoadTexture(SDL_Surface *surface, GLfloat *texcoord)
{
    GLuint texture;
    int w, h;
    SDL_Surface *image;
    SDL_Rect area;
    SDL_BlendMode saved_mode;

    /* Use the surface width and height expanded to powers of 2 */
    w = power_of_two(surface->w);
    h = power_of_two(surface->h);
    texcoord[0] = 0.0f;                    /* Min X */
    texcoord[1] = 0.0f;                    /* Min Y */
    texcoord[2] = (GLfloat)surface->w / w; /* Max X */
    texcoord[3] = (GLfloat)surface->h / h; /* Max Y */

    image = SDL_CreateSurface(w, h, SDL_PIXELFORMAT_RGBA32);
    if (!image) {
        return 0;
    }

    /* Save the alpha blending attributes */
    SDL_GetSurfaceBlendMode(surface, &saved_mode);
    SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_NONE);

    /* Copy the surface into the GL texture image */
    area.x = 0;
    area.y = 0;
    area.w = surface->w;
    area.h = surface->h;
    SDL_BlitSurface(surface, &area, image, &area);

    /* Restore the alpha blending attributes */
    SDL_SetSurfaceBlendMode(surface, saved_mode);

    /* Create an OpenGL texture for the image */
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexImage2D(GL_TEXTURE_2D,
                 0,
                 GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, image->pixels);
    SDL_DestroySurface(image); /* No longer needed */

    return texture;
}

/* A general OpenGL initialization function.    Sets all of the initial parameters. */
static void InitGL(int Width, int Height) /* We call this right after our OpenGL window is created. */
{
    GLdouble aspect;

    glViewport(0, 0, Width, Height);
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f); /* This Will Clear The Background Color To Black */
    glClearDepth(1.0);                    /* Enables Clearing Of The Depth Buffer */
    glDepthFunc(GL_LESS);                 /* The Type Of Depth Test To Do */
    glEnable(GL_DEPTH_TEST);              /* Enables Depth Testing */
    glShadeModel(GL_SMOOTH);              /* Enables Smooth Color Shading */

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity(); /* Reset The Projection Matrix */

    aspect = (GLdouble)Width / Height;
    glOrtho(-3.0, 3.0, -3.0 / aspect, 3.0 / aspect, 0.0, 1.0);

    glMatrixMode(GL_MODELVIEW);
}

/* The main drawing function. */
static void DrawGLScene(SDL_Window *window, GLuint texture, GLfloat *texcoord)
{
    /* Texture coordinate lookup, to make it simple */
    enum
    {
        MINX,
        MINY,
        MAXX,
        MAXY
    };

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* Clear The Screen And The Depth Buffer */
    glLoadIdentity();                                   /* Reset The View */

    glTranslatef(-1.5f, 0.0f, 0.0f); /* Move Left 1.5 Units */

    /* draw a triangle (in smooth coloring mode) */
    glBegin(GL_POLYGON);            /* start drawing a polygon */
    glColor3f(1.0f, 0.0f, 0.0f);    /* Set The Color To Red */
    glVertex3f(0.0f, 1.0f, 0.0f);   /* Top */
    glColor3f(0.0f, 1.0f, 0.0f);    /* Set The Color To Green */
    glVertex3f(1.0f, -1.0f, 0.0f);  /* Bottom Right */
    glColor3f(0.0f, 0.0f, 1.0f);    /* Set The Color To Blue */
    glVertex3f(-1.0f, -1.0f, 0.0f); /* Bottom Left */
    glEnd();                        /* we're done with the polygon (smooth color interpolation) */

    glTranslatef(3.0f, 0.0f, 0.0f); /* Move Right 3 Units */

    /* Enable blending */
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    /* draw a textured square (quadrilateral) */
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, texture);
    glColor3f(1.0f, 1.0f, 1.0f);
    if (shaders_supported) {
        pglUseProgramObjectARB(shaders[current_shader].program);
    }

    glBegin(GL_QUADS); /* start drawing a polygon (4 sided) */
    glTexCoord2f(texcoord[MINX], texcoord[MINY]);
    glVertex3f(-1.0f, 1.0f, 0.0f); /* Top Left */
    glTexCoord2f(texcoord[MAXX], texcoord[MINY]);
    glVertex3f(1.0f, 1.0f, 0.0f); /* Top Right */
    glTexCoord2f(texcoord[MAXX], texcoord[MAXY]);
    glVertex3f(1.0f, -1.0f, 0.0f); /* Bottom Right */
    glTexCoord2f(texcoord[MINX], texcoord[MAXY]);
    glVertex3f(-1.0f, -1.0f, 0.0f); /* Bottom Left */
    glEnd();                        /* done with the polygon */

    if (shaders_supported) {
        pglUseProgramObjectARB(0);
    }
    glDisable(GL_TEXTURE_2D);

    /* swap buffers to display, since we're double buffered. */
    SDL_GL_SwapWindow(window);
}

int main(int argc, char **argv)
{
    int i;
    int done;
    SDL_Window *window;
    char *filename = NULL;
    SDL_Surface *surface;
    GLuint texture;
    GLfloat texcoords[4];
    SDLTest_CommonState *state;

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

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

        consumed = SDLTest_CommonArg(state, i);
        if (!consumed) {
            if (!filename) {
                filename = argv[i];
                consumed = 1;
            }
        }
        if (consumed <= 0) {
            static const char *options[] = { "[icon.png]", NULL };
            SDLTest_CommonLogUsage(state, argv[0], options);
            exit(1);
        }

        i += consumed;
    }

    /* Initialize SDL for video output */
    if (!SDL_Init(SDL_INIT_VIDEO)) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to initialize SDL: %s", SDL_GetError());
        exit(1);
    }

    /* Create a 640x480 OpenGL screen */
    window = SDL_CreateWindow("Shader Demo", 640, 480, SDL_WINDOW_OPENGL);
    if (!window) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to create OpenGL window: %s", SDL_GetError());
        SDL_Quit();
        exit(2);
    }

    if (!SDL_GL_CreateContext(window)) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to create OpenGL context: %s", SDL_GetError());
        SDL_Quit();
        exit(2);
    }

    filename = GetResourceFilename(NULL, "icon.png");
    surface = SDL_LoadPNG(filename);
    SDL_free(filename);

    if (!surface) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to load icon.png: %s", SDL_GetError());
        SDL_Quit();
        exit(3);
    }
    texture = SDL_GL_LoadTexture(surface, texcoords);
    SDL_DestroySurface(surface);

    /* Loop, drawing and checking events */
    InitGL(640, 480);
    if (InitShaders()) {
        SDL_Log("Shaders supported, press SPACE to cycle them.");
    } else {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Shaders not supported!");
    }
    done = 0;
    while (!done) {
        DrawGLScene(window, texture, texcoords);

        /* This could go in a separate function */
        {
            SDL_Event event;
            while (SDL_PollEvent(&event)) {
                if (event.type == SDL_EVENT_QUIT) {
                    done = 1;
                }
                if (event.type == SDL_EVENT_KEY_DOWN) {
                    if (event.key.key == SDLK_SPACE) {
                        current_shader = (current_shader + 1) % NUM_SHADERS;
                    }
                    if (event.key.key == SDLK_ESCAPE) {
                        done = 1;
                    }
                }
            }
        }
    }
    QuitShaders();
    SDL_Quit();
    SDLTest_CommonDestroyState(state);
    return 1;
}

#else /* HAVE_OPENGL */

int main(int argc, char *argv[])
{
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "No OpenGL support on this system");
    return 1;
}

#endif /* HAVE_OPENGL */
