package electrosphere.renderer;

import java.util.HashMap;
import java.util.Map;

import org.joml.Vector2i;
import org.lwjgl.opengl.GL40;

import electrosphere.renderer.shader.ShaderProgram;

/**
 * Encapsulates the state of opengl.
 * The main function of this class is to sit between any consuming classes and opengl.
 * It can then deduplicate calls based on the state that is already set.
 */
public class OpenGLState {

    //the max texture allowed by the current environment
    int MAX_TEXTURE_WIDTH = 0;
    
    //the current viewport dimensions
    private Vector2i viewport = new Vector2i(0,0);

    //whether depth test is enabled or not
    boolean depthTest = false;

    //the current depth function
    int depthFunction = -1;

    //whether to blend or nit
    boolean blendTest = false;

    //the current blend func
    //map is (texture unit) -> [sfactor,dfactor]
    Map<Integer,int[]> blendFuncMap = new HashMap<Integer,int[]>();
    //the key that contains the value of glBlendFunc (which would affect all buffers)
    static final int ALL_BUFFERS_KEY = -1;

    //the currently bound texture
    int boundTexturePointer = 0;
    int boundTextureType = 0;

    //the currently active texture
    int activeTexture = 0;

    //the currently bound framebuffer
    int framebufferType = 0;
    int framebufferPointer = 0;

    //active shader
    ShaderProgram activeShader = null;

    //map of texture units and their corresponding texture pointers
    Map<Integer,Integer> unitToPointerMap = new HashMap<Integer,Integer>();


    /**
     * Gets the constraints of the current environment (ie how large can the max texture be)
     */
    public void storeCurrentEnvironmentContraints(){

        //the array used to store values fetched from opengl
        int[] intFetchArray = new int[1];

        //get max texture size
        GL40.glGetIntegerv(GL40.GL_MAX_TEXTURE_SIZE, intFetchArray);
        MAX_TEXTURE_WIDTH = intFetchArray[0];


    }

    /**
     * Sets the viewport
     * @param x the width
     * @param y the height
     */
    public void glViewport(int x, int y){
        if(x != viewport.x || y != viewport.y){
            viewport.x = x;
            viewport.y = y;
            GL40.glViewport(0, 0, viewport.x, viewport.y);
        }
    }

    /**
     * Gets the viewport's dimensions
     * @return The viewport's dimensions
     */
    public Vector2i getViewport(){
        return viewport;
    }

    /**
     * Sets the depth test
     * @param depthTest the depth test state
     */
    public void glDepthTest(boolean depthTest){
        // if(this.depthTest != depthTest){
            this.depthTest = depthTest;
            if(this.depthTest){
                GL40.glEnable(GL40.GL_DEPTH_TEST);
            } else {
                GL40.glDisable(GL40.GL_DEPTH_TEST);
            }
        // }
    }

    /**
     * Sets the depth function
     * @param depthFunction The depth function
     */
    public void glDepthFunc(int depthFunction){
        if(this.depthFunction != depthFunction){
            this.depthFunction = depthFunction;
            GL40.glDepthFunc(this.depthFunction);
        }
    }

    /**
     * Sets the active texture
     * @param texture The active texture
     */
    public void glActiveTexture(int texture){
        if(this.activeTexture != texture){
            this.activeTexture = texture;
            GL40.glActiveTexture(this.activeTexture);
        }
    }

    /**
     * Binds a texture
     * @param textureType The type of texture
     * @param textureValue The texture pointer
     */
    public void glBindTexture(int textureType, int texturePointer){
        if(this.boundTexturePointer != texturePointer || this.boundTextureType != textureType){
            this.boundTextureType = textureType;
            this.boundTexturePointer = texturePointer;
            GL40.glBindTexture(this.boundTextureType,this.boundTexturePointer);
        }
    }

    /**
     * Binds a texture to a given texture unit if the texture hasn't already been bound to that unit
     * @param textureUnit The texture unit
     * @param texturePointer The texture pointer
     * @param textureType the type of texture (2d, 3d, etc)
     */
    public void glBindTextureUnit(int textureUnit, int texturePointer, int textureType){
        if(!unitToPointerMap.containsKey(textureUnit) || unitToPointerMap.get(textureUnit)!=texturePointer){
            unitToPointerMap.put(textureUnit,texturePointer);
            GL40.glActiveTexture(textureUnit);
            GL40.glBindTexture(textureType,texturePointer);
        }
    }

    /**
     * Binds a framebuffer
     * @param framebufferType the type of framebuffer (vanilla, renderbuffer, etc)
     * @param framebufferPointer the pointer to the framebuffer
     */
    public void glBindFramebuffer(int framebufferType, int framebufferPointer){
        if(this.framebufferType != framebufferType || this.framebufferPointer != framebufferPointer){
            this.framebufferType = framebufferType;
            this.framebufferPointer = framebufferPointer;
            GL40.glBindFramebuffer(this.framebufferType,this.framebufferPointer);
        }
    }

    /**
     * Gets the currently bound framebuffer's pointer
     * @return The pointer
     */
    public int getBoundFramebuffer(){
        return this.framebufferPointer;
    }

    /**
     * Sets the currently active shader program for the renderer
     * @param renderPipelineState The render pipeline state object
     * @param program The shader program to bind
     */
    public void setActiveShader(RenderPipelineState renderPipelineState, ShaderProgram program){
        if(program != activeShader){
            activeShader = program;
            GL40.glUseProgram(activeShader.getShaderId());
            renderPipelineState.setCurrentShaderPointer(activeShader.getShaderId());
        }
    }

    /**
     * Gets the active shader program
     * @return The active shader
     */
    public ShaderProgram getActiveShader(){
        return activeShader;
    }

    /**
     * Checks whether the provided shader program is the active shader program
     * @param program The program to check
     * @return true if the provided program is the active program, false otherwise
     */
    public boolean isCurrentShader(ShaderProgram program){
        return this.activeShader == program;
    }
    
    /**
     * Gets MAX_TEXTURE_WIDTH
     * @return MAX_TEXTURE_WIDTH
     */
    public int getMAX_TEXTURE_WIDTH(){
        return MAX_TEXTURE_WIDTH;
    }

    /**
     * 
     * @param sfactor
     * @param dfactor
     */
    public void glBlendFunc(int sfactor, int dfactor){
        GL40.glBlendFunc(sfactor, dfactor);
        //set all other keys
        for(int keyCurrent : this.blendFuncMap.keySet()){
            if(keyCurrent != ALL_BUFFERS_KEY){
                int[] funcs = this.blendFuncMap.get(keyCurrent);
                funcs[0] = sfactor;
                funcs[1] = dfactor;
            }
        }
    }

    /**
     * Sets the blend function for opengl
     * @param drawBufferIndex The draw buffer index that will have this function set
     * @param sfactor The source factor
     * @param dfactor The destination factor
     */
    public void glBlendFunci(int drawBufferIndex, int sfactor, int dfactor){
        if(this.blendFuncMap.containsKey(drawBufferIndex)){
            int[] funcs = this.blendFuncMap.get(drawBufferIndex);
            int sFactorCurr = funcs[0];
            int dFactorCurr = funcs[1];
            if(sfactor != sFactorCurr || dfactor != dFactorCurr){
                funcs[0] = sfactor;
                funcs[1] = dfactor;
                this.blendFuncMap.put(drawBufferIndex,funcs);
                GL40.glBlendFunci(drawBufferIndex, sfactor, dfactor);
            }
        } else {
            int[] funcs = new int[]{
                sfactor, dfactor
            };
            this.blendFuncMap.put(drawBufferIndex,funcs);
            GL40.glBlendFunci(drawBufferIndex, sfactor, dfactor);
        }

    }

    public void glBlend(boolean blend){
        // if(this.blendTest != blend){
            this.blendTest = blend;
            if(this.blendTest){
                GL40.glEnable(GL40.GL_BLEND);
            } else {
                GL40.glDisable(GL40.GL_BLEND);
            }
        // }
    }

}
