/*
 * Copyright 2022 Rive
 */

#include "rive/renderer/gl/gl_utils.hpp"
#include "rive/shapes/paint/image_sampler.hpp"

#include <stdio.h>
#include <sstream>
#include <thread>
#include <vector>

#include "generated/shaders/glsl.glsl.hpp"

#ifdef BYPASS_EMSCRIPTEN_SHADER_PARSER
#include <emscripten/emscripten.h>
#include <emscripten/html5.h>

// Emscripten's shader preprocessor crashes on PLS shaders. This method allows
// us to bypass Emscripten and set a WebGL shader source directly.
EM_JS(void,
      webgl_shader_source,
      (EMSCRIPTEN_WEBGL_CONTEXT_HANDLE gl, GLuint shader, const char* source),
      {
          gl = GL.getContext(gl).GLctx;
          shader = GL.shaders[shader];
          source = UTF8ToString(source);
          gl.shaderSource(shader, source);
      });
#endif

namespace glutils
{
void CompileAndAttachShader(GLuint program,
                            GLenum type,
                            const char* source,
                            const GLCapabilities& capabilities,
                            DebugPrintErrorAndAbort debugPrintErrornAndAbort)
{
    CompileAndAttachShader(program,
                           type,
                           nullptr,
                           0,
                           &source,
                           1,
                           capabilities,
                           debugPrintErrornAndAbort);
}

void CompileAndAttachShader(GLuint program,
                            GLenum type,
                            const char* defines[],
                            size_t numDefines,
                            const char* inputSources[],
                            size_t numInputSources,
                            const GLCapabilities& capabilities,
                            DebugPrintErrorAndAbort debugPrintErrornAndAbort)
{
    GLuint shader = CompileShader(type,
                                  defines,
                                  numDefines,
                                  inputSources,
                                  numInputSources,
                                  capabilities,
                                  debugPrintErrornAndAbort);
    glAttachShader(program, shader);
    glDeleteShader(shader);
}

GLuint CompileShader(GLuint type,
                     const char* source,
                     const GLCapabilities& capabilities,
                     DebugPrintErrorAndAbort debugPrintErrornAndAbort)
{
    return CompileShader(type,
                         nullptr,
                         0,
                         &source,
                         1,
                         capabilities,
                         debugPrintErrornAndAbort);
}

GLuint CompileShader(GLuint type,
                     const char* defines[],
                     size_t numDefines,
                     const char* inputSources[],
                     size_t numInputSources,
                     const GLCapabilities& capabilities,
                     DebugPrintErrorAndAbort debugPrintErrornAndAbort)
{
    std::ostringstream shaderSource;
    shaderSource << "#version " << capabilities.contextVersionMajor
                 << capabilities.contextVersionMinor << '0';
    if (capabilities.isGLES)
    {
        shaderSource << " es";
    }
    shaderSource << '\n';
    // Create our own "GLSL_VERSION" macro. In "#version 320 es", Qualcomm
    // incorrectly substitutes
    // __VERSION__ to 300.
    shaderSource << "#define " << GLSL_GLSL_VERSION << ' '
                 << capabilities.contextVersionMajor
                 << capabilities.contextVersionMinor << "0\n";
    if (type == GL_VERTEX_SHADER)
    {
        shaderSource << "#define " << GLSL_VERTEX "\n";
    }
    else if (GL_FRAGMENT_SHADER)
    {
        shaderSource << "#define " << GLSL_FRAGMENT "\n";
    }
    for (size_t i = 0; i < numDefines; ++i)
    {
        shaderSource << "#define " << defines[i] << " true\n";
    }
    if (!capabilities.ANGLE_base_vertex_base_instance_shader_builtin)
    {
        shaderSource << "#define " << GLSL_BASE_INSTANCE_UNIFORM_NAME << ' '
                     << BASE_INSTANCE_UNIFORM_NAME << '\n';
    }
    if (capabilities.needsFloatingPointTessellationTexture)
    {
        shaderSource << "#define " << GLSL_TESS_TEXTURE_FLOATING_POINT << '\n';
    }
    if (capabilities.isMali)
    {
        shaderSource << "#define " << GLSL_GL_RENDERER_MALI << '\n';
    }
    shaderSource << rive::gpu::glsl::glsl << "\n";
    for (size_t i = 0; i < numInputSources; ++i)
    {
        shaderSource << inputSources[i] << "\n";
    }
    return CompileRawGLSL(type,
                          shaderSource.str().c_str(),
                          debugPrintErrornAndAbort);
}

#ifdef DEBUG
void PrintShaderCompilationErrors(GLuint shader)
{
    GLint maxLength = 0;
    glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength);
    std::vector<GLchar> infoLog(maxLength);
    glGetShaderInfoLog(shader, maxLength, &maxLength, &infoLog[0]);
    fprintf(stderr, "Failed to compile shader\n");
    // Print the error message *before* the shader in case stderr hasn't
    // finished flushing when we call abort() further on.
    fprintf(stderr, "%s\n", &infoLog[0]);

    GLint sourceLength = 0;
    glGetShaderiv(shader, GL_SHADER_SOURCE_LENGTH, &sourceLength);
    std::vector<GLchar> shaderSource(sourceLength);
    glGetShaderSource(shader, sourceLength, nullptr, shaderSource.data());
    int l = 1;
    std::stringstream stream(shaderSource.data());
    std::string lineStr;
    while (std::getline(stream, lineStr, '\n'))
    {
        fprintf(stderr, "%4i| %s\n", l++, lineStr.c_str());
    }
    // Print the error message, again, *after* the shader where it's easier
    // to find in the console.
    fprintf(stderr, "%s\n", &infoLog[0]);
    fflush(stderr);
}
#endif

[[nodiscard]] GLuint CompileRawGLSL(
    GLenum shaderType,
    const char* rawGLSL,
    DebugPrintErrorAndAbort debugPrintErrornAndAbort)
{
    GLuint shader = glCreateShader(shaderType);
#ifdef BYPASS_EMSCRIPTEN_SHADER_PARSER
    // Emscripten's shader preprocessor crashes on PLS shaders. Feed Emscripten
    // something very simple and then hop to WebGL to bypass it and set the real
    // shader source.
    const char* kMinimalShader =
        shaderType == GL_VERTEX_SHADER
            ? "#version 300 es\nvoid main() { gl_Position = vec4(0); }"
            : "#version 300 es\nvoid main() {}";
    glShaderSource(shader, 1, &kMinimalShader, nullptr);
    webgl_shader_source(emscripten_webgl_get_current_context(),
                        shader,
                        rawGLSL);
#else
    glShaderSource(shader, 1, &rawGLSL, nullptr);
#endif
    glCompileShader(shader);
#ifdef DEBUG
    if (debugPrintErrornAndAbort == DebugPrintErrorAndAbort::yes)
    {
        GLint isCompiled = 0;
        glGetShaderiv(shader, GL_COMPILE_STATUS, &isCompiled);
        if (isCompiled == GL_FALSE)
        {
            PrintShaderCompilationErrors(shader);
            glDeleteShader(shader);
            // Give stderr another second to finish flushing before we abort.
            std::this_thread::sleep_for(std::chrono::milliseconds(1000));
            abort();
        }
    }
#else
    std::ignore = debugPrintErrornAndAbort;
#endif
    return shader;
}

#ifdef DEBUG
void PrintLinkProgramErrors(GLuint program)
{
    GLint maxLength = 0;
    glGetProgramiv(program, GL_INFO_LOG_LENGTH, &maxLength);
    std::vector<GLchar> infoLog(maxLength);
    glGetProgramInfoLog(program, maxLength, &maxLength, &infoLog[0]);
    fprintf(stderr, "Failed to link program %s\n", &infoLog[0]);
    fflush(stderr);
}
#endif

void LinkProgram(GLuint program,
                 DebugPrintErrorAndAbort debugPrintErrornAndAbort)
{
    glLinkProgram(program);
#ifdef DEBUG
    if (debugPrintErrornAndAbort == DebugPrintErrorAndAbort::yes)
    {
        GLint isLinked = 0;
        glGetProgramiv(program, GL_LINK_STATUS, &isLinked);
        if (isLinked == GL_FALSE)
        {
            PrintLinkProgramErrors(program);
            abort();
        }
    }
#else
    std::ignore = debugPrintErrornAndAbort;
#endif
}

void Program::reset(GLuint adoptedProgramID)
{
    m_fragmentShader.reset();
    m_vertexShader.reset();
    if (m_id != 0)
    {
        glDeleteProgram(m_id);
    }
    m_id = adoptedProgramID;
}

void Program::compileAndAttachShader(GLuint type,
                                     const char* defines[],
                                     size_t numDefines,
                                     const char* sources[],
                                     size_t numSources,
                                     const GLCapabilities& capabilities)
{
    assert(type == GL_VERTEX_SHADER || type == GL_FRAGMENT_SHADER);
    glutils::Shader& internalShader =
        type == GL_VERTEX_SHADER ? m_vertexShader : m_fragmentShader;
    internalShader
        .compile(type, defines, numDefines, sources, numSources, capabilities);
    glAttachShader(m_id, internalShader);
}

void Shader::compile(GLenum type,
                     const char* defines[],
                     size_t numDefines,
                     const char* sources[],
                     size_t numSources,
                     const GLCapabilities& capabilities)
{
    reset(CompileShader(type,
                        defines,
                        numDefines,
                        sources,
                        numSources,
                        capabilities));
}

void SetTexture2DSamplingParams(GLenum minFilter, GLenum magFilter)
{
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilter);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}

GLint gl_wrap_from_image_wrap(rive::ImageWrap wrap)
{
    switch (wrap)
    {
        case rive::ImageWrap::clamp:
            return GL_CLAMP_TO_EDGE;
        case rive::ImageWrap::repeat:
            return GL_REPEAT;
        case rive::ImageWrap::mirror:
            return GL_MIRRORED_REPEAT;
    }

    RIVE_UNREACHABLE();
}

GLint gl_min_filter_for_image_filter(rive::ImageFilter filter)
{
    switch (filter)
    {
        case rive::ImageFilter::bilinear:
            return GL_LINEAR_MIPMAP_NEAREST;
        case rive::ImageFilter::nearest:
            return GL_NEAREST;
    }

    RIVE_UNREACHABLE();
}

GLint gl_mag_filter_for_image_filter(rive::ImageFilter filter)
{
    switch (filter)
    {
        case rive::ImageFilter::nearest:
            return GL_NEAREST;
        case rive::ImageFilter::bilinear:
            return GL_LINEAR;
    }

    RIVE_UNREACHABLE();
}

void SetTexture2DSamplingParams(rive::ImageSampler samplingParams)
{
    glTexParameteri(GL_TEXTURE_2D,
                    GL_TEXTURE_MIN_FILTER,
                    gl_min_filter_for_image_filter(samplingParams.filter));
    glTexParameteri(GL_TEXTURE_2D,
                    GL_TEXTURE_MAG_FILTER,
                    gl_mag_filter_for_image_filter(samplingParams.filter));
    glTexParameteri(GL_TEXTURE_2D,
                    GL_TEXTURE_WRAP_S,
                    gl_wrap_from_image_wrap(samplingParams.wrapX));
    glTexParameteri(GL_TEXTURE_2D,
                    GL_TEXTURE_WRAP_T,
                    gl_wrap_from_image_wrap(samplingParams.wrapY));
}

void BlitFramebuffer(rive::IAABB bounds,
                     uint32_t renderTargetHeight,
                     GLbitfield mask)
{
    // glBlitFramebuffer is oriented bottom-up.
    uint32_t l = bounds.left;
    uint32_t b = renderTargetHeight - bounds.bottom;
    uint32_t r = bounds.right;
    uint32_t t = renderTargetHeight - bounds.top;
    glBlitFramebuffer(l, b, r, t, l, b, r, t, mask, GL_NEAREST);
}

void Uniform1iByName(GLuint programID, const char* name, GLint value)
{
    GLint location = glGetUniformLocation(programID, name);
    // Don't allow non-existent uniforms. glUniform1i() is supposed to silently
    // ignore -1, but Moto G7 Play throws an error. We also just shouldn't be
    // querying uniform locations we know aren't going to exist anyway for
    // performance reasons.
    assert(location != -1);
    glUniform1i(location, value);
}

// Setup a small test to verify that GL_PIXEL_LOCAL_FORMAT_ANGLE has the correct
// value.
bool validate_pixel_local_storage_angle()
{
#if defined(RIVE_DESKTOP_GL) || defined(RIVE_WEBGL)
    glutils::Texture tex;
    glBindTexture(GL_TEXTURE_2D, tex);
    glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32UI, 1, 1);

    glutils::Framebuffer fbo;
    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
    glFramebufferTexturePixelLocalStorageANGLE(0, tex, 0, 0);

    // Clear the error queue. (Should already be empty.)
    while (GLenum err = glGetError())
    {
        fprintf(stderr, "WARNING: unhandled GL error 0x%x\n", err);
    }

    GLint format = GL_NONE;
    glGetFramebufferPixelLocalStorageParameterivANGLE(
        0,
        GL_PIXEL_LOCAL_FORMAT_ANGLE,
        &format);
    return glGetError() == GL_NONE && format == GL_R32UI;
#else
    return false;
#endif
}
} // namespace glutils
