#include "GLFramebuffer.hpp"
#include "../OpenGL.hpp"
#include "../Renderer.hpp"
#include "macros.hpp"
#include "../Framebuffer.hpp"
#include <hyprgraphics/egl/Egl.hpp>
#include <limits>

using namespace Hyprgraphics::Egl;
using namespace Render::GL;

CGLFramebuffer::CGLFramebuffer() : IFramebuffer(), m_tempBuf(true) {}
CGLFramebuffer::CGLFramebuffer(const std::string& name) : IFramebuffer(name), m_tempBuf(true) {}

bool CGLFramebuffer::internalAlloc(int w, int h, uint32_t drmFormat) {
    g_pHyprOpenGL->makeEGLCurrent();
    m_tempBuf = false;

    if (!m_tex) {
        m_tex = g_pHyprRenderer->createTexture();
        m_tex->allocate({w, h}, drmFormat);
        m_tex->bind();
        m_tex->setTexParameter(GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        m_tex->setTexParameter(GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        m_tex->setTexParameter(GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        m_tex->setTexParameter(GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    }

    if (!m_fbAllocated) {
        glGenFramebuffers(1, &m_fb);
        m_fbAllocated = true;
    }

    const auto format = getPixelFormatFromDRM(drmFormat);
    m_tex->bind();
    glTexImage2D(GL_TEXTURE_2D, 0, format->glInternalFormat ? format->glInternalFormat : format->glFormat, w, h, 0, format->glFormat, format->glType, nullptr);
    glBindFramebuffer(GL_FRAMEBUFFER, m_fb);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_tex->m_texID, 0);

    if (m_mirrorTex) {
        const auto format = getPixelFormatFromDRM(m_mirrorTex->m_drmFormat);
        m_mirrorTex->bind();
        glTexImage2D(GL_TEXTURE_2D, 0, format->glInternalFormat ? format->glInternalFormat : format->glFormat, w, h, 0, format->glFormat, format->glType, nullptr);
        glBindFramebuffer(GL_FRAMEBUFFER, m_fb);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, m_mirrorTex->m_texID, 0);
        GLenum drawBuffers[] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
        glDrawBuffers(2, drawBuffers);
    } else {
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, 0, 0);
        GLenum drawBuffers[] = {GL_COLOR_ATTACHMENT0};
        glDrawBuffers(1, drawBuffers);
    }

    if (m_stencilTex && m_stencilTex->ok()) {
        m_stencilTex->bind();
        glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, w, h, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, nullptr);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, m_stencilTex->m_texID, 0);

        glDisable(GL_DEPTH_TEST);
        glDepthMask(GL_FALSE);
    }

    auto status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    RASSERT((status == GL_FRAMEBUFFER_COMPLETE), "Framebuffer incomplete, couldn't create! (FB status: {}, GL Error: 0x{:x})", status, sc<int>(glGetError()));

    if (m_stencilTex && m_stencilTex->ok())
        m_stencilTex->unbind();

    Log::logger->log(Log::DEBUG, "Framebuffer \"{}\" created, status {}", m_name, status);

    glBindTexture(GL_TEXTURE_2D, 0);
    glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);

    // this can run mid frame in enableMirror() in begin() restore the draw fb the renderer had bound
    if (g_pHyprRenderer && g_pHyprRenderer->m_renderData.currentFB)
        g_pHyprRenderer->m_renderData.currentFB->bind();
    else
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);

    return true;
}

void CGLFramebuffer::addStencil(SP<ITexture> tex) {
    if (m_stencilTex == tex)
        return;

    RASSERT(!m_fbAllocated, "Should add stencil tex prior to FB allocation")
    m_stencilTex = tex;
}

void CGLFramebuffer::bind() {
    // temp buffer, created without CGLFramebuffer::internalAlloc.
    // that means its a temp buffer that we have to raw bind and not change the viewport.
    // the temp buffer code binds this fb to add attachments themself
    if (m_tempBuf) {
        glBindFramebuffer(GL_FRAMEBUFFER, m_fb);
        return;
    }

    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_fb);
    if (g_pHyprOpenGL) {
        const auto& size = g_pHyprRenderer->m_renderData.pMonitor ? g_pHyprRenderer->m_renderData.pMonitor->m_pixelSize : m_size;
        g_pHyprOpenGL->setViewport(0, 0, size.x, size.y);
    } else
        glViewport(0, 0, m_size.x, m_size.y);
}

void CGLFramebuffer::unbind() {
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
}

void CGLFramebuffer::release() {
    if (m_fbAllocated) {
        glBindFramebuffer(GL_FRAMEBUFFER, m_fb);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
        if (m_mirrorTex)
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, 0, 0);

        glDeleteFramebuffers(1, &m_fb);

        // releasing can happen mid frame from a temp fb, rebind the fb the renderer
        // had previously bound, otherwise draws continue into fb 0 and raise GL_INVALID_FRAMEBUFFER_OPERATION
        if (g_pHyprRenderer && g_pHyprRenderer->m_renderData.currentFB && g_pHyprRenderer->m_renderData.currentFB.get() != this)
            g_pHyprRenderer->m_renderData.currentFB->bind();

        m_fbAllocated = false;
        m_fb          = 0;
    }

    if (m_tex)
        m_tex.reset();

    m_size = Vector2D();
}

bool CGLFramebuffer::readPixels(CHLBufferReference buffer, uint32_t offsetX, uint32_t offsetY, uint32_t width, uint32_t height) {
    auto shm = buffer->shm();
    if (!shm.success) {
        LOGM(Log::ERR, "Can't copy: buffer is not shm");
        return false;
    }

    auto [pixelData, fmt, bufLen] = buffer->beginDataPtr(0); // no need for end, cuz it's shm
    if (!pixelData) {
        LOGM(Log::ERR, "Can't copy: failed to get shm data pointer");
        return false;
    }

    const auto PFORMAT = getPixelFormatFromDRM(shm.format);
    if (!PFORMAT) {
        LOGM(Log::ERR, "Can't copy: failed to find a pixel format");
        return false;
    }

    const auto fbWidth    = sc<uint32_t>(m_size.x);
    const auto fbHeight   = sc<uint32_t>(m_size.y);
    const auto readWidth  = width > 0 ? width : fbWidth;
    const auto readHeight = height > 0 ? height : fbHeight;

    if (readWidth == 0 || readHeight == 0 || shm.stride <= 0) {
        LOGM(Log::ERR, "Can't copy: invalid shm read dimensions");
        return false;
    }

    if (offsetX > fbWidth || offsetY > fbHeight || readWidth > fbWidth - offsetX || readHeight > fbHeight - offsetY) {
        LOGM(Log::ERR, "Can't copy: read rect exceeds framebuffer");
        return false;
    }

    const auto shmWidth  = sc<uint32_t>(shm.size.x);
    const auto shmHeight = sc<uint32_t>(shm.size.y);
    if (offsetX > shmWidth || offsetY > shmHeight || readWidth > shmWidth - offsetX || readHeight > shmHeight - offsetY) {
        LOGM(Log::ERR, "Can't copy: read rect exceeds shm buffer");
        return false;
    }

    const auto strideBytes = sc<size_t>(shm.stride);
    const auto rowOffset   = sc<size_t>(minStride(PFORMAT, offsetX));
    const auto rowBytes    = sc<size_t>(minStride(PFORMAT, readWidth));

    if (rowBytes == 0) {
        LOGM(Log::ERR, "Can't copy: invalid shm row size");
        return false;
    }

    if (rowOffset > std::numeric_limits<size_t>::max() - rowBytes || rowOffset + rowBytes > strideBytes) {
        LOGM(Log::ERR, "Can't copy: shm stride is too small");
        return false;
    }

    const auto lastRow = sc<size_t>(offsetY) + sc<size_t>(readHeight) - 1;
    if (strideBytes > 0 && lastRow > std::numeric_limits<size_t>::max() / strideBytes) {
        LOGM(Log::ERR, "Can't copy: shm row offset overflows");
        return false;
    }

    const auto lastRowStart = lastRow * strideBytes;
    const auto rowEnd       = rowOffset + rowBytes;
    if (lastRowStart > std::numeric_limits<size_t>::max() - rowEnd || lastRowStart + rowEnd > bufLen) {
        LOGM(Log::ERR, "Can't copy: shm buffer is too small");
        return false;
    }

    g_pHyprOpenGL->makeEGLCurrent();
    glBindFramebuffer(GL_READ_FRAMEBUFFER, getFBID());
    bind();

    glPixelStorei(GL_PACK_ALIGNMENT, 1);

    int         glFormat = PFORMAT->glFormat;

    static auto stripSwizzleAlpha = [](std::array<GLint, 4> arr) {
        arr[3] = GL_ONE;
        return arr;
    };

    if (PFORMAT->swizzle.has_value()) {
        if (stripSwizzleAlpha(*PFORMAT->swizzle) == stripSwizzleAlpha(SWIZZLE_RGBA))
            glFormat = GL_RGBA;
        else if (stripSwizzleAlpha(*PFORMAT->swizzle) == stripSwizzleAlpha(SWIZZLE_BGRA))
            glFormat = GL_BGRA_EXT;
        else {
            LOGM(Log::ERR, "Copied frame via shm might be broken or color flipped");
            glFormat = GL_RGBA;
        }
    } else if (glFormat == GL_RGBA)
        glFormat = GL_BGRA_EXT;

    // This could be optimized by using a pixel buffer object to make this async,
    // but really clients should just use a dma buffer anyways.
    if (rowOffset == 0 && rowBytes == strideBytes) {
        glReadPixels(offsetX, offsetY, readWidth, readHeight, glFormat, PFORMAT->glType, pixelData + sc<size_t>(offsetY) * strideBytes);
    } else {
        for (uint32_t i = 0; i < readHeight; ++i) {
            const auto y = offsetY + i;
            glReadPixels(offsetX, y, readWidth, 1, glFormat, PFORMAT->glType, pixelData + sc<size_t>(y) * strideBytes + rowOffset);
        }
    }

    unbind();
    glPixelStorei(GL_PACK_ALIGNMENT, 4);

    glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
    return true;
}

CGLFramebuffer::~CGLFramebuffer() {
    release();
}

GLuint CGLFramebuffer::getFBID() {
    return m_fbAllocated ? m_fb : 0;
}

void CGLFramebuffer::invalidate(const std::vector<GLenum>& attachments) {
    if (!isAllocated())
        return;

    glInvalidateFramebuffer(GL_FRAMEBUFFER, attachments.size(), attachments.data());
    m_cleared = false;
}

void CGLFramebuffer::clearAfterInvalidation() {
    if (m_cleared)
        return;

    m_cleared = true;
    glClearColor(0, 0, 0, 0);
    g_pHyprOpenGL->scissor(nullptr);
    glClear(GL_COLOR_BUFFER_BIT);
}
