package electrosphere.renderer.framebuffer;

import java.awt.image.BufferedImage;

import electrosphere.engine.Globals;
import electrosphere.logger.LoggerInterface;
import electrosphere.renderer.OpenGLState;
import electrosphere.renderer.RenderingEngine;
import electrosphere.renderer.texture.Texture;

import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.lwjgl.opengl.GL40;
import org.lwjgl.system.MemoryUtil;

import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL30.*;
import static org.lwjgl.opengl.GL45.glCheckNamedFramebufferStatus;

/**
 * Framebuffer object
 */
public class Framebuffer {
    
    //the pointer to the framebuffer
    int framebufferPointer;
    //the mipmap level
    int mipMap = -1;
    //the map of attachment point to texture object
    Map<Integer,Texture> attachTextureMap = new HashMap<Integer,Texture>();
    //attached texture
    Texture texture;
    //the depth texture for the framebuffer
    Texture depthTexture;
    
    /**
     * Creates a framebuffer
     */
    public Framebuffer(){
        framebufferPointer = glGenFramebuffers();
    }

    /**
     * Creates a framebuffer with a predefined id
     * @param framebufferId The predefined framebuffer id
     */
    public Framebuffer(int framebufferId){
        this.framebufferPointer = framebufferId;
    }

    /**
     * Sets the texture attached to the framebuffer
     * @param texture The texture attached to the framebuffer
     */
    public void setTexture(Texture texture){
        this.texture = texture;
    }

    /**
     * Gets the texture attached to this framebuffer
     * @return The texture
     */
    public Texture getTexture(){
        return texture;
    }

    /**
     * Gets the depth texture attached to this framebuffer
     * @return The depth texture
     */
    public Texture getDepthTexture(){
        return depthTexture;
    }
    
    /**
     * Binds the framebuffer
     * @param openGLState The opengl state
     */
    public void bind(OpenGLState openGLState){
        openGLState.glBindFramebuffer(GL_FRAMEBUFFER, framebufferPointer);
    }
    
    /**
     * Checks if the framebuffer compiled correctly
     * @return true if compiled correctly, false otherwise
     */
    public boolean isComplete(){
        return glCheckNamedFramebufferStatus(framebufferPointer,GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
    }

    /**
     * Checks if the framebuffer has an error
     * @return true if error, false otherwise
     */
    public boolean isError(){
        return glCheckNamedFramebufferStatus(framebufferPointer,GL_FRAMEBUFFER) == 0;
    }
    
    public void checkStatus(){
        if(isError()){
            LoggerInterface.loggerRenderer.WARNING("Framebuffer [error] - " + RenderingEngine.getErrorInEnglish(Globals.renderingEngine.getError()));
        } else if(!isComplete()){
            LoggerInterface.loggerRenderer.WARNING("Framebuffer [glError] - " + RenderingEngine.getErrorInEnglish(Globals.renderingEngine.getError()));
            LoggerInterface.loggerRenderer.WARNING("Framebuffer [status] - " + getStatus());
            LoggerInterface.loggerRenderer.ERROR("Failed to build framebuffer", new IllegalStateException("Framebuffer failed to build."));
        }
    }

    /**
     * Gets the framebuffer's pointer
     * @return The framebuffer's pointer
     */
    public int getFramebufferPointer(){
        return framebufferPointer;
    }
    
    /**
     * Frees the framebuffer
     */
    public void free(){
        glDeleteFramebuffers(framebufferPointer);
    }
    
    /**
     * Blocks the thread until the framebuffer has compiled
     */
    public void blockUntilCompiled(){
        while(glCheckNamedFramebufferStatus(framebufferPointer,GL_FRAMEBUFFER) != GL_FRAMEBUFFER_UNDEFINED){
            try {
                TimeUnit.MILLISECONDS.sleep(1);
            } catch (InterruptedException ex) {
                LoggerInterface.loggerEngine.ERROR("Failed to sleep in framebuffer blocker", ex);
            }
        }
    }

    /**
     * Gets the status of the framebuffer
     * @return The status
     */
    public String getStatus(){
        switch(glCheckNamedFramebufferStatus(framebufferPointer,GL_FRAMEBUFFER)){
            case GL_FRAMEBUFFER_UNDEFINED: {
                return "The specified framebuffer is the default read or draw framebuffer, but the default framebuffer does not exist.";
            }
            case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: {
                return "Any of the framebuffer attachment points are framebuffer incomplete.";
            }
            case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: {
                return "The framebuffer does not have at least one image attached to it.";
            }
            case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: {
                return "The value of GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE is GL_NONE for any color attachment point(s) named by GL_DRAW_BUFFERi.";
            }
            case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: {
                return "GL_READ_BUFFER is not GL_NONE and the value of GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE is GL_NONE for the color attachment point named by GL_READ_BUFFER.";
            }
            case GL_FRAMEBUFFER_UNSUPPORTED: {
                return "The combination of internal formats of the attached images violates an implementation-dependent set of restrictions.";
            }
            case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: {
                return "The value of GL_RENDERBUFFER_SAMPLES is not the same for all attached renderbuffers; if the value of GL_TEXTURE_SAMPLES is the not same for all attached textures; or, if the attached images are a mix of renderbuffers and textures, the value of GL_RENDERBUFFER_SAMPLES does not match the value of GL_TEXTURE_SAMPLES.";
            }
            case 0: {
                return RenderingEngine.getErrorInEnglish(Globals.renderingEngine.getError());
            }
        }
        return "Unknown framebuffer status";
    }

    /**
     * Sets the mipmap level
     * @param mipMap The mipmap level
     */
    public void setMipMapLevel(int mipMap){
        this.mipMap = mipMap;
    }

    /**
     * Attaches a color texture to the default texture unit
     * @param openGLState The opengl state
     * @param texture The texture
     */
    public void attachTexture(OpenGLState openGLState, Texture texture){
        attachTexture(openGLState, texture, 0);
    }

    /**
     * Attaches a texture to the framebuffer
     * @param openGLState The opengl state
     * @param texture The texture
     * @param attachmentNum The texture unit to attach to
     */
    public void attachTexture(OpenGLState openGLState, Texture texture, int attachmentNum){
        this.attachTextureMap.put(attachmentNum,texture);
        if(this.mipMap < 0){
            LoggerInterface.loggerRenderer.ERROR(new IllegalStateException("Trying to attach a texture to a framebuffer where mipmap hasn't been set."));
        }
        this.texture = texture;
        openGLState.glBindFramebuffer(GL_FRAMEBUFFER, framebufferPointer);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + attachmentNum, GL_TEXTURE_2D, texture.getTexturePointer(), this.mipMap);
    }

    /**
     * Sets the depth attachment for the framebuffer
     * @param openGLState The opengl state
     * @param texturePointer The depth attachment's pointer
     */
    public void setDepthAttachment(OpenGLState openGLState, Texture depthTexture){
        openGLState.glBindFramebuffer(GL_FRAMEBUFFER, framebufferPointer);
        this.depthTexture = depthTexture;
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTexture.getTexturePointer(), this.mipMap);
        openGLState.glBindFramebuffer(GL_FRAMEBUFFER, 0);
        checkStatus();
    }

    /**
     * Gets the pixels currently stored in this framebuffer
     */
    public BufferedImage getPixels(OpenGLState openGLState){
        BufferedImage rVal = null;
        int offsetX = 0;
        int offsetY = 0;
        int width = openGLState.getViewport().x;
        int height = openGLState.getViewport().y;
        int pixelFormat = GL40.GL_RGBA;
        int type = GL40.GL_UNSIGNED_INT;


        bind(openGLState);
        GL40.glReadBuffer(GL40.GL_COLOR_ATTACHMENT0);
        if(this.framebufferPointer == 0){
            //this is the default framebuffer, read from backbuffer because it is default
            GL40.glReadBuffer(GL40.GL_BACK);
        } else if(attachTextureMap.containsKey(0)){
            Texture texture = attachTextureMap.get(0);
            pixelFormat = texture.getFormat();
            type = texture.getDataType();
            width = texture.getWidth();
            height = texture.getHeight();
        } else {
            LoggerInterface.loggerRenderer.ERROR(new IllegalStateException("Tried to get pixels from a framebuffer that does not have a texture attached to attachment point 0."));
        }
        //get pixel data
        try {
            int bytesPerPixel = pixelFormatToBytes(pixelFormat,type);
            int bufferSize = width * height * bytesPerPixel;
            ByteBuffer buffer = MemoryUtil.memAlloc(bufferSize);
            openGLState.glViewport(width, height);
            GL40.glReadPixels(offsetX, offsetY, width, height, pixelFormat, type, buffer);
            Globals.renderingEngine.checkError();
            //convert to a buffered images
            rVal = new BufferedImage(width,height,BufferedImage.TYPE_INT_ARGB);
            for(int x = 0; x < width; x++){
                for(int y = 0; y < height; y++){
                    int i = (x + (width * y)) * bytesPerPixel;
                    int red = buffer.get(i) & 0xFF;
                    int green = buffer.get(i + 1) & 0xFF;
                    int blue = buffer.get(i + 2) & 0xFF;
                    int alpha = 255;
                    if(pixelFormat == GL40.GL_RGBA){
                        alpha = buffer.get(i + 3) & 0xFF;
                    }
                    rVal.setRGB(x, height - (y + 1), (alpha << 24) | (red << 16) | (green << 8) | blue);
                }
            }
            //memory management
            MemoryUtil.memFree(buffer);
        } catch (OutOfMemoryError e){
            LoggerInterface.loggerRenderer.ERROR(new IllegalStateException(e.getMessage()));
        }
        return rVal;
    }

    /**
     * Gets the number of bytes per pixel based on the format and type of pixel
     * @param format The format of the pixels
     * @param type The datatype of as single component of the pixel
     * @return The number of bytes
     */
    private static int pixelFormatToBytes(int format, int type){
        int multiplier = 1;
        switch(format){
            case GL40.GL_RGBA: {
                multiplier = 4;
            } break;
            case GL40.GL_RGB: {
                multiplier = 3;
            } break;
            default: {
                LoggerInterface.loggerRenderer.WARNING("Trying to export framebuffer that has image of unsupported pixel format");
            } break;
        }
        int bytesPerComponent = 1;
        switch(type){
            case GL40.GL_UNSIGNED_INT: {
                bytesPerComponent = 4;
            } break;
            case GL40.GL_UNSIGNED_BYTE: {
                bytesPerComponent = 1;
            } break;
            default: {
                LoggerInterface.loggerRenderer.WARNING("Trying to export framebuffer that has image of unsupported datatype");
            } break;
        }
        return multiplier * bytesPerComponent;
    }

}
