package electrosphere.renderer.framebuffer;

import electrosphere.engine.Globals;
import electrosphere.renderer.OpenGLState;
import electrosphere.renderer.pipelines.ShadowMapPipeline;
import electrosphere.renderer.texture.Texture;

import java.nio.IntBuffer;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL40;
import org.lwjgl.opengl.GL45;

import static org.lwjgl.opengl.GL11.GL_DEPTH_COMPONENT;
import static org.lwjgl.opengl.GL11.GL_FLOAT;
import static org.lwjgl.opengl.GL11.GL_LINEAR;
import static org.lwjgl.opengl.GL11.GL_NEAREST;
import static org.lwjgl.opengl.GL11.GL_NONE;
import static org.lwjgl.opengl.GL11.GL_RED;
import static org.lwjgl.opengl.GL11.GL_RGB;
import static org.lwjgl.opengl.GL11.GL_RGBA;
import static org.lwjgl.opengl.GL11.GL_SHORT;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_WRAP_S;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_WRAP_T;
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_BYTE;
import static org.lwjgl.opengl.GL12.GL_CLAMP_TO_EDGE;
import static org.lwjgl.opengl.GL13.GL_CLAMP_TO_BORDER;
import static org.lwjgl.opengl.GL30.GL_COLOR_ATTACHMENT0;
import static org.lwjgl.opengl.GL30.GL_COLOR_ATTACHMENT1;
import static org.lwjgl.opengl.GL30.GL_DEPTH24_STENCIL8;
import static org.lwjgl.opengl.GL30.GL_DEPTH_STENCIL_ATTACHMENT;
import static org.lwjgl.opengl.GL30.GL_FRAMEBUFFER;
import static org.lwjgl.opengl.GL30.GL_RENDERBUFFER;
import static org.lwjgl.opengl.GL30.GL_HALF_FLOAT;
import static org.lwjgl.opengl.GL30.glBindRenderbuffer;
import static org.lwjgl.opengl.GL30.glFramebufferRenderbuffer;
import static org.lwjgl.opengl.GL30.glGenRenderbuffers;
import static org.lwjgl.opengl.GL30.glRenderbufferStorage;
import static org.lwjgl.opengl.GL45.glNamedFramebufferDrawBuffer;
import static org.lwjgl.opengl.GL45.glNamedFramebufferDrawBuffers;
import static org.lwjgl.opengl.GL45.glNamedFramebufferReadBuffer;

/**
 * Utilities for framebuffer creation
 */
public class FramebufferUtils {

    public static Texture generateScreenTextureColor(OpenGLState openGLState, int width, int height){
        Texture texture = new Texture();
        texture.bind(openGLState);
        texture.glTexImage2D(openGLState, width, height, GL_RGB, GL45.GL_UNSIGNED_INT);
        texture.setMinFilter(openGLState, GL_LINEAR);
        texture.setMagFilter(openGLState, GL_LINEAR);
        //these make sure the texture actually clamps to the borders of the quad
        texture.setWrap(openGLState, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        texture.setWrap(openGLState, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        texture.setBorderColor(openGLState, new float[]{0,0,0,1});

        //guarantees that the texture object has actually been created (calling gen buffers does not guarantee object creation)
        texture.bind(openGLState);
        openGLState.glBindTexture(GL40.GL_TEXTURE_2D, Texture.DEFAULT_TEXTURE);

        texture.checkStatus(openGLState);
        return texture;
    }

    public static Texture generateScreenTextureColorAlpha(OpenGLState openGLState, int width, int height){
        Texture texture = new Texture();
        texture.bind(openGLState);
        texture.glTexImage2D(openGLState, width, height, GL_RGBA, GL45.GL_UNSIGNED_INT);
        texture.setMinFilter(openGLState, GL_LINEAR);
        texture.setMagFilter(openGLState, GL_LINEAR);
        //these make sure the texture actually clamps to the borders of the quad
        texture.setWrap(openGLState, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        texture.setWrap(openGLState, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        texture.setBorderColor(openGLState, new float[]{0,0,0,1});

        //guarantees that the texture object has actually been created (calling gen buffers does not guarantee object creation)
        texture.bind(openGLState);
        openGLState.glBindTextureUnitForce(GL45.GL_TEXTURE0, Texture.DEFAULT_TEXTURE, GL40.GL_TEXTURE_2D);

        texture.checkStatus(openGLState);
        return texture;
    }

    public static Texture generateScreenTextureDepth(OpenGLState openGLState, int width, int height){
        Texture texture = new Texture();
        texture.bind(openGLState);
        texture.glTexImage2D(openGLState, width, height, GL_DEPTH_COMPONENT, GL_FLOAT);

        texture.setMinFilter(openGLState, GL_LINEAR);
        texture.setMagFilter(openGLState, GL_LINEAR);
        //these make sure the texture actually clamps to the borders of the quad
        texture.setWrap(openGLState, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        texture.setWrap(openGLState, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        texture.setBorderColor(openGLState, new float[]{0,0,0,1});

        //guarantees that the texture object has actually been created (calling gen buffers does not guarantee object creation)
        texture.bind(openGLState);
        openGLState.glBindTextureUnitForce(GL45.GL_TEXTURE0, Texture.DEFAULT_TEXTURE, GL40.GL_TEXTURE_2D);

        texture.checkStatus(openGLState);
        return texture;
    }
    
    
    public static Framebuffer generateScreenTextureFramebuffer(OpenGLState openGLState, int width, int height, Texture colorTexture, Texture depthTexture) throws Exception {
        Framebuffer buffer = new Framebuffer();
        //bind texture to fbo
        buffer.setMipMapLevel(0);
        buffer.attachTexture(openGLState,colorTexture);
        buffer.setDepthAttachment(openGLState,depthTexture);
        buffer.bind(openGLState);
        //check make sure compiled
        buffer.shouldBeComplete(openGLState);
        Globals.renderingEngine.defaultFramebuffer.bind(openGLState);
        return buffer;
    }

    public static Framebuffer generateScreenTextureFramebuffer(OpenGLState openGLState, int width, int height, Texture colorTexture) throws Exception {
        Framebuffer buffer = new Framebuffer();
        //bind texture to fbo
        buffer.setMipMapLevel(0);
        buffer.attachTexture(openGLState,colorTexture);
        //check make sure compiled
        buffer.shouldBeComplete(openGLState);
        return buffer;
    }

    public static Framebuffer generateScreensizeTextureFramebuffer(OpenGLState openGLState) throws Exception {
        Framebuffer buffer = new Framebuffer();
        buffer.bind(openGLState);
        //texture
        Texture texture = new Texture();
        texture.glTexImage2D(openGLState, Globals.WINDOW_WIDTH, Globals.WINDOW_HEIGHT, GL_RGB, GL_UNSIGNED_BYTE);
        texture.setMinFilter(openGLState, GL_LINEAR);
        texture.setMagFilter(openGLState, GL_LINEAR);
        //these make sure the texture actually clamps to the borders of the quad
        texture.setWrap(openGLState, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        texture.setWrap(openGLState, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        texture.checkStatus(openGLState);
        //bind texture to fbo
        buffer.setMipMapLevel(0);
        buffer.attachTexture(openGLState,texture);
        //renderbuffer
        int renderBuffer = glGenRenderbuffers();
        glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
        Globals.renderingEngine.checkError();
        glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, Globals.WINDOW_WIDTH, Globals.WINDOW_HEIGHT); 
        Globals.renderingEngine.checkError();
        //bind rbo to fbo
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, renderBuffer);
        Globals.renderingEngine.checkError();
        //check make sure compiled
        buffer.shouldBeComplete(openGLState);
        Globals.renderingEngine.defaultFramebuffer.bind(openGLState);
        return buffer;
    }
    
    /**
     * Creates a texture framebuffer
     * @param openGLState The opengl engine state
     * @param width The width of the texture
     * @param height The height of the texture
     * @return The texture
     * @throws Exception Thrown if the framebuffer fails to initialize
     */
    public static Framebuffer generateTextureFramebuffer(OpenGLState openGLState, int width, int height) throws Exception {
        Framebuffer buffer = new Framebuffer();
        buffer.bind(openGLState);
        //texture
        Texture texture = new Texture();
        texture.glTexImage2D(openGLState, width, height, GL_RGBA, GL_UNSIGNED_BYTE);
        texture.setMinFilter(openGLState, GL_LINEAR);
        texture.setMagFilter(openGLState, GL_LINEAR);
        //these make sure the texture actually clamps to the borders of the quad
        texture.setWrap(openGLState, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        texture.setWrap(openGLState, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        texture.checkStatus(openGLState);
        //bind texture to fbo
        buffer.setMipMapLevel(0);
        buffer.attachTexture(openGLState,texture);
        //renderbuffer
        int renderBuffer = glGenRenderbuffers();
        GL40.glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
        Globals.renderingEngine.checkError();
        GL40.glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height); 
        Globals.renderingEngine.checkError();
        //bind rbo to fbo
        buffer.bind(openGLState);
        GL40.glFramebufferRenderbuffer(GL40.GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, renderBuffer);
        Globals.renderingEngine.checkError();
        //check make sure compiled
        buffer.shouldBeComplete(openGLState);
        //re-bind default buffer
        Globals.renderingEngine.defaultFramebuffer.bind(openGLState);
        return buffer;
    }
    
    
    
    
    
    public static Renderbuffer generateScreensizeStencilDepthRenderbuffer(OpenGLState openGLState){
        Renderbuffer buffer = new Renderbuffer();
        buffer.bind();
        glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, Globals.WINDOW_WIDTH, Globals.WINDOW_HEIGHT);
        Globals.renderingEngine.checkError();
        return buffer;
    }
    
    
    public static Framebuffer generateDepthBuffer(OpenGLState openGLState) throws Exception {
        Framebuffer buffer = new Framebuffer();
        buffer.bind(openGLState);
        
        
        //texture
        Texture texture = new Texture();
        texture.glTexImage2D(openGLState, ShadowMapPipeline.SHADOW_MAP_RESOLUTION, ShadowMapPipeline.SHADOW_MAP_RESOLUTION, GL_DEPTH_COMPONENT, GL_FLOAT);
        texture.setMinFilter(openGLState, GL_NEAREST);
        texture.setMagFilter(openGLState, GL_NEAREST);
        texture.setWrap(openGLState, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
        texture.setWrap(openGLState, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
        texture.setBorderColor(openGLState, new float[]{ 1.0f, 1.0f, 1.0f, 1.0f });
        texture.checkStatus(openGLState);

        //bind texture to fbo
        buffer.setMipMapLevel(0);
        buffer.setDepthAttachment(openGLState,texture);
        glNamedFramebufferDrawBuffer(buffer.getFramebufferPointer(), GL_NONE);
        Globals.renderingEngine.checkError();
        glNamedFramebufferReadBuffer(buffer.getFramebufferPointer(), GL_NONE);
        Globals.renderingEngine.checkError();
        
        
        
        //check make sure compiled
        buffer.shouldBeComplete(openGLState);
        Globals.renderingEngine.defaultFramebuffer.bind(openGLState);
        return buffer;
    }


    public static Texture generateDepthBufferTexture(OpenGLState openGLState, int width, int height){
        Texture texture = new Texture();
        texture.glTexImage2D(openGLState, width, height, GL_DEPTH_COMPONENT, GL_SHORT);
        texture.setMinFilter(openGLState, GL_NEAREST);
        texture.setMagFilter(openGLState, GL_NEAREST);
        texture.setWrap(openGLState, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
        texture.setWrap(openGLState, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
        texture.setBorderColor(openGLState, new float[]{ 1.0f, 1.0f, 1.0f, 1.0f });
        texture.checkStatus(openGLState);
        return texture;
    }

    public static Framebuffer generateDepthBuffer(OpenGLState openGLState, int width, int height, Texture texture) throws Exception {
        Framebuffer buffer = new Framebuffer();
        buffer.bind(openGLState);
        
        
        //bind texture to fbo
        buffer.setMipMapLevel(0);
        buffer.setDepthAttachment(openGLState,texture);
        glNamedFramebufferDrawBuffer(buffer.getFramebufferPointer(), GL_NONE);
        Globals.renderingEngine.checkError();
        glNamedFramebufferReadBuffer(buffer.getFramebufferPointer(), GL_NONE);
        Globals.renderingEngine.checkError();
        
        
        
        //check make sure compiled
        buffer.shouldBeComplete(openGLState);
        Globals.renderingEngine.defaultFramebuffer.bind(openGLState);
        return buffer;
    }

    public static Texture generateOITAccumulatorTexture(OpenGLState openGLState, int width, int height){
        Texture texture = new Texture();
        texture.setMinFilter(openGLState, GL_LINEAR);
        texture.setMagFilter(openGLState, GL_LINEAR);
        texture.glTexImage2D(openGLState, width, height, GL_RGBA, GL_HALF_FLOAT);
        texture.checkStatus(openGLState);
        return texture;
    }

    public static Texture generateOITRevealageTexture(OpenGLState openGLState, int width, int height){
        Texture texture = new Texture();
        texture.setMinFilter(openGLState, GL_LINEAR);
        texture.setMagFilter(openGLState, GL_LINEAR);
        texture.glTexImage2D(openGLState, width, height, GL_RED, GL_FLOAT);
        texture.checkStatus(openGLState);
        return texture;
    }

    public static Framebuffer generateOITFramebuffer(OpenGLState openGLState, int width, int height, Texture accumulatorTex, Texture revealageTex, Texture depthTexture) throws Exception {
        Framebuffer buffer = new Framebuffer();
        buffer.bind(openGLState);
        
                
        //bind texture to fbo
        buffer.setMipMapLevel(0);
        buffer.attachTexture(openGLState,accumulatorTex,0);
        buffer.attachTexture(openGLState,revealageTex,1);
        buffer.setDepthAttachment(openGLState,depthTexture);
        
        // const GLenum transparentDrawBuffers[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
        // glDrawBuffers(2, transparentDrawBuffers);
        IntBuffer drawBuffers = BufferUtils.createIntBuffer(2);
        drawBuffers.put(GL_COLOR_ATTACHMENT0);
        drawBuffers.put(GL_COLOR_ATTACHMENT1);
        drawBuffers.flip();
        glNamedFramebufferDrawBuffers(buffer.getFramebufferPointer(),drawBuffers);
        Globals.renderingEngine.checkError();

        //check make sure compiled
        buffer.shouldBeComplete(openGLState);
        Globals.renderingEngine.defaultFramebuffer.bind(openGLState);
        return buffer;
    }


}
