package electrosphere.renderer.texture;

import electrosphere.engine.Globals;
import electrosphere.logger.LoggerInterface;
import electrosphere.renderer.OpenGLState;
import electrosphere.renderer.RenderingEngine;
import electrosphere.util.FileUtils;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.ByteBuffer;
import javax.imageio.ImageIO;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL40;
import org.lwjgl.system.MemoryUtil;

import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL12.GL_CLAMP_TO_EDGE;
import static org.lwjgl.opengl.GL13.GL_TEXTURE0;
import static org.lwjgl.opengl.GL14.*;
import static org.lwjgl.opengl.GL30.*;

/**
 * A opengl in texture
 */
public class Texture {

    //the pointer for the texture
    int texturePointer = -1;
    //the width of the texture
    int width = -1;
    //the height of the texture
    int height = -1;
    //whether the texture has transparency or not
    boolean hasTransparency;
    //the path to the texture
    String path = "";
    //the border color
    float[] borderColor = null;

    //the min and max filter
    int minFilter = -1;
    int maxFilter = -1;

    //the pixel format (ie RGB, ARGB, etc)
    int pixelFormat = -1;

    //the data type of a single component of a pixel (IE UNSIGNED_INT, BYTE, etc)
    int datatype = -1;
    
    /**
     * Constructs a texture from a pointer
     * @param pointer the pointer
     */
    public Texture(int pointer){
        this.texturePointer = pointer;
    }

    /**
     * Creates a texture with a new opengl texture object
     */
    public Texture(){
        this.texturePointer = glGenTextures();
    }

    /**
     * Creates an in engine texture object from a java bufferedimage object
     * @param bufferedImage The java bufferedimage object
     */
    public Texture(BufferedImage bufferedImage){
        this.texturePointer = glGenTextures();
        //bind the new texture
        glBindTexture(GL_TEXTURE_2D, texturePointer);
        //how are we gonna wrap the texture??
        this.setWrap(GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
        this.setWrap(GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
        //set the border color to black
        this.setBorderColor(new float[]{ 0.0f, 0.0f, 0.0f, 1.0f });
        //set magnification and minification operation sampling strategies
        this.setMinFilter(GL_LINEAR);
        this.setMagFilter(GL_LINEAR);
        //load the image here
        ByteBuffer data;
        width = 1;
        height = 1;
        BufferedImage image_data = bufferedImage;
        if (
                image_data.getType() == BufferedImage.TYPE_3BYTE_BGR ||
                image_data.getType() == BufferedImage.TYPE_INT_RGB
                ){
            hasTransparency = false;
        } else if(
                image_data.getType() == BufferedImage.TYPE_4BYTE_ABGR ||
                image_data.getType() == BufferedImage.TYPE_INT_ARGB
                ){
            hasTransparency = true;
        }
        width = image_data.getWidth();
        height = image_data.getHeight();
        if(hasTransparency){
            data = BufferUtils.createByteBuffer(width * height * 4);
        } else {
            data = BufferUtils.createByteBuffer(width * height * 3);
        }
        for(int y = height - 1; y > -1; y--){
            for(int x = 0; x < width; x++){
                Color temp = new Color(image_data.getRGB(x, y), true);
                
                data.put((byte)temp.getRed());
                data.put((byte)temp.getGreen());
                data.put((byte)temp.getBlue());
                if(hasTransparency){
                    data.put((byte)temp.getAlpha());
                }
            }
        }
        data.flip();
        //call if width != height so opengl figures out how to unpack it properly
        if(width != height){
            glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
        }
        //buffer the texture information
        if(hasTransparency){
            this.pixelFormat = GL_RGBA;
            this.datatype = GL_UNSIGNED_BYTE;
            this.glTexImage2D(width, height, GL_RGBA, GL_UNSIGNED_BYTE, data);
        } else {
            this.pixelFormat = GL_RGB;
            this.datatype = GL_UNSIGNED_BYTE;
            this.glTexImage2D(width, height, GL_RGB, GL_UNSIGNED_BYTE, data);
        }
        glGenerateMipmap(GL_TEXTURE_2D);
        //check build status
        String errorMessage = RenderingEngine.getErrorInEnglish(Globals.renderingEngine.getError());
        if(errorMessage != null){
            LoggerInterface.loggerRenderer.ERROR(new IllegalStateException("Texture Constructor[from bufferedimage]: " + errorMessage));
        }
        glBindTexture(GL_TEXTURE_2D, 0);
    }

    /**
     * Creates a texture from an existing file
     * @param path The path to the image file
     */
    public Texture(String path){
        this.path = path;
        if(!Globals.HEADLESS){
            //generate the texture object on gpu
            this.texturePointer = glGenTextures();
            //bind the new texture
            glBindTexture(GL_TEXTURE_2D, texturePointer);
            //how are we gonna wrap the texture??
            this.setWrap(GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
            this.setWrap(GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
            //set the border color to black
            this.setBorderColor(new float[]{ 0.0f, 0.0f, 0.0f, 1.0f });
            //set magnification and minification operation sampling strategies
            this.setMinFilter(GL_LINEAR);
            this.setMagFilter(GL_LINEAR);
            //load the image here
            ByteBuffer data;
            width = 1;
            height = 1;
            try {
                BufferedImage image_data = ImageIO.read(FileUtils.getAssetFile(path));
                if (
                        image_data.getType() == BufferedImage.TYPE_3BYTE_BGR ||
                        image_data.getType() == BufferedImage.TYPE_INT_RGB
                        ){
                    hasTransparency = false;
                } else if(
                        image_data.getType() == BufferedImage.TYPE_4BYTE_ABGR ||
                        image_data.getType() == BufferedImage.TYPE_INT_ARGB
                        ){
                    hasTransparency = true;
                }
                width = image_data.getWidth();
                height = image_data.getHeight();
                if(hasTransparency){
                    data = BufferUtils.createByteBuffer(width * height * 4);
                } else {
                    data = BufferUtils.createByteBuffer(width * height * 3);
                }
                /*
                imgBuffer = BufferUtils.createByteBuffer(4 * dimX * dimY);
                for(int x = 0; x < dimX; x++){
                    for(int y = 0; y < dimY; y++){
                        Color temp = new Color(image_data.getRGB(x, y));
                        data.put((byte)(temp.getRed());
                        data.put((byte)(temp.getGreen());
                        data.put((byte)(temp.getBlue());
                        data.put((byte)(temp.getAlpha());
                    }
                }
                imgBuffer.flip();
                */
                for(int y = height - 1; y > -1; y--){
                    for(int x = 0; x < width; x++){
                        Color temp = new Color(image_data.getRGB(x, y), true);
                        
                        data.put((byte)temp.getRed());
                        data.put((byte)temp.getGreen());
                        data.put((byte)temp.getBlue());
                        if(hasTransparency){
                            data.put((byte)temp.getAlpha());
                        }
    //                    data[x * y * 3 + 0] = temp.getRed();
    //                    data[x * y * 3 + 1] = temp.getGreen();
    //                    data[x * y * 3 + 2] = temp.getBlue();
                    }
                }
            } catch (IOException ex) {
                ex.printStackTrace();
                hasTransparency = false;
                data = BufferUtils.createByteBuffer(3);
                data.put((byte)0);
                data.put((byte)0);
                data.put((byte)0);
            }
            data.flip();
            //call if width != height so opengl figures out how to unpack it properly
            if(width != height){
                glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
            }
            //buffer the texture information
            if(hasTransparency){
                this.pixelFormat = GL_RGBA;
                this.datatype = GL_UNSIGNED_BYTE;
                this.glTexImage2D(width, height, GL_RGBA, GL_UNSIGNED_BYTE, data);
            } else {
                this.pixelFormat = GL_RGB;
                this.datatype = GL_UNSIGNED_BYTE;
                this.glTexImage2D(width, height, GL_RGB, GL_UNSIGNED_BYTE, data);
            }
            glGenerateMipmap(GL_TEXTURE_2D);
            //OPTIONAL free the original image data now that it's on the gpu
            // System.gc();
            //check build status
            String errorMessage = RenderingEngine.getErrorInEnglish(Globals.renderingEngine.getError());
            if(errorMessage != null){
                LoggerInterface.loggerRenderer.ERROR(new IllegalStateException("Texture Constructor[from bufferedimage]: " + errorMessage));
            }
        }
    }

    /**
     * Generates a texture based on a buffer (for use passing data to gpu)
     * @param buffer The buffer of data
     * @param width the 'width' of the 'texture'
     * @param height the 'height' of the 'texture'
     */
    public Texture(ByteBuffer buffer, int width, int height){
        if(!Globals.HEADLESS){
            //generate the texture object on gpu
            this.texturePointer = glGenTextures();
            //bind the new texture
            glBindTexture(GL_TEXTURE_2D, texturePointer);
            //how are we gonna wrap the texture??
            this.setWrap(GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
            this.setWrap(GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
            //disable mipmap
            this.setMinFilter(GL_LINEAR);
            //call if width != height so opengl figures out how to unpack it properly
            glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
            //GL_RED = 32bit r value
            //buffer the texture information
            this.pixelFormat = GL_RED;
            this.datatype = GL_FLOAT;
            GL40.glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, width, height, 0, GL_RED, GL_FLOAT, buffer);
            //check build status
            String errorMessage = RenderingEngine.getErrorInEnglish(Globals.renderingEngine.getError());
            if(errorMessage != null){
                LoggerInterface.loggerRenderer.ERROR(new IllegalStateException("Texture Constructor[from bytebuffer]: " + errorMessage));
            }
        }
    }
    
    /**
     * Binds the texture to unit 0
     * @param openGLState The opengl state
     */
    public void bind(OpenGLState openGLState){
        if(texturePointer == -1){
            LoggerInterface.loggerRenderer.ERROR(new IllegalStateException("Tring to bind a texture object that has not been initialized yet"));
        }
        if(texturePointer == 0){
            LoggerInterface.loggerRenderer.ERROR(new IllegalStateException("Trying to bind texture object that has texturepointer of 0"));
        }
        // openGLState.glActiveTexture(GL_TEXTURE0);
        // openGLState.glBindTexture(GL_TEXTURE_2D, texturePointer);
        openGLState.glBindTextureUnit(GL_TEXTURE0,this.texturePointer,GL_TEXTURE_2D);
    }
    
    /**
     * Binds the texture
     * @param openGLState The opengl state
     * @param attrib_val The texture unit number
     */
    public void bind(OpenGLState openGLState, int attrib_val){
        if(texturePointer == -1){
            LoggerInterface.loggerRenderer.ERROR(new IllegalStateException("Tring to bind a texture object that has not been initialized yet"));
        }
        if(texturePointer == 0){
            LoggerInterface.loggerRenderer.ERROR(new IllegalStateException("Trying to bind texture object that has texturepointer of 0"));
        }
        openGLState.glBindTextureUnit(GL_TEXTURE0 + attrib_val,this.texturePointer,GL_TEXTURE_2D);
        // openGLState.glActiveTexture(GL_TEXTURE0 + attrib_val);
        // openGLState.glBindTexture(GL_TEXTURE_2D, texturePointer);
    }
    
    /**
     * Checks if the texture has transparency or not
     * @return true if transparent, false otherwise
     */
    public boolean isTransparent(){
        return hasTransparency;
    }
    
    /**
     * Gets the path of the texture
     * @return The path
     */
    public String getPath(){
        return path;
    }
    
    /**
     * Gets the pointer of the texture
     * @return The pointer
     */
    public int getTexturePointer(){
        return texturePointer;
    }

    /**
     * Sets the wrap strategy of the texture
     * @param wrapDir The direction to wrap
     * @param wrapType The type of wrapping to perform
     */
    public void setWrap(int wrapDir, int wrapType){
        //TODO: store wrap type for the direction in this object
        glBindTexture(GL_TEXTURE_2D,texturePointer);
        glTexParameteri(GL_TEXTURE_2D, wrapDir, wrapType);
    }

    /**
     * Sets the border color
     * @param borderColor The color (must be 4 floats)
     */
    public void setBorderColor(float borderColor[]){
        this.borderColor = borderColor;
        glBindTexture(GL_TEXTURE_2D,texturePointer);
        glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
    }

    /**
     * Sets the min filter
     * @param minFilter The min filter
     */
    public void setMinFilter(int minFilter){
        this.minFilter = minFilter;
        glBindTexture(GL_TEXTURE_2D,texturePointer);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter);
    }

    /**
     * Sets the max filter
     * @param maxFilter The max filter
     */
    public void setMagFilter(int maxFilter){
        this.maxFilter = maxFilter;
        glBindTexture(GL_TEXTURE_2D,texturePointer);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, maxFilter);
    }

    
    /**
     * Specifies a 2d image
     * @param width The width of the image
     * @param height The height of the image
     * @param format The format of the pixels (ie GL_RGB, GL_RGBA, etc)
     * @param datatype The data type of a single component of a pixel (ie GL_BYTE, GL_UNSIGNED_INT, etc)
     */
    public void glTexImage2D(OpenGLState openGLState, int width, int height, int format, int datatype){
        //store provided values
        this.width = width;
        this.height = height;
        this.pixelFormat = format;
        this.datatype = datatype;
        //static values going into call
        int level = 0;
        int border = 0; //this must be 0 according to docs
        openGLState.glBindTexture(GL_TEXTURE_2D,texturePointer);
        GL40.glTexImage2D(GL_TEXTURE_2D, level, format, width, height, border, format, datatype, MemoryUtil.NULL);
    }

    /**
     * Specifies a 2d image
     * @param width The width of the image
     * @param height The height of the image
     * @param format The format of the pixels (ie GL_RGB, GL_RGBA, etc)
     * @param datatype The data type of a single component of a pixel (ie GL_BYTE, GL_UNSIGNED_INT, etc)
     * @param data The data to populate the image with
     */
    public void glTexImage2D(int width, int height, int format, int datatype, ByteBuffer data){
        //store provided values
        this.width = width;
        this.height = height;
        this.pixelFormat = format;
        this.datatype = datatype;
        //static values going into call
        int level = 0;
        int border = 0; //this must be 0 according to docs
        glBindTexture(GL_TEXTURE_2D,texturePointer);
        GL40.glTexImage2D(GL_TEXTURE_2D, level, format, width, height, border, format, datatype, data);
    }

    /**
     * Gets the width of the texture
     * @return The width
     */
    public int getWidth(){
        if(pixelFormat == -1){
            throw new IllegalStateException(
                "The width of the texture you are trying to query from has not been set yet." + 
                " The texture was likely constructed by passing the opengl texture pointer into the texture object."
            );
        }
        return width;
    }

    /**
     * Gets the height of the texture
     * @return The height
     */
    public int getHeight(){
        if(pixelFormat == -1){
            throw new IllegalStateException(
                "The height of the texture you are trying to query from has not been set yet." + 
                " The texture was likely constructed by passing the opengl texture pointer into the texture object."
            );
        }
        return height;
    }

    /**
     * Gets the format of the pixels
     * @return The format of the pixels (ie GL_RGBA, GL_RGB, etc)
     */
    public int getFormat(){
        if(pixelFormat == -1){
            throw new IllegalStateException(
                "The pixel format of the texture you are trying to query from has not been set yet." + 
                " The texture was likely constructed by passing the opengl texture pointer into the texture object."
            );
        }
        return pixelFormat;
    }

    /**
     * Gets the datatype of the pixels
     * @return The datatype (IE GL_FLOAT, GL_BYTE, etc)
     */
    public int getDataType(){
        if(datatype == -1){
            throw new IllegalStateException(
                "The datatype of the texture you are trying to query from has not been set yet." +
                " The texture was likely constructed by passing the opengl texture pointer into the texture object."
            );
        }
        return datatype;
    }

    /**
     * Gets for errors with the texture
     * @param state The opengl state
     */
    public void checkStatus(OpenGLState state){
        this.bind(state);
        int errorCode = Globals.renderingEngine.getError();
        if(errorCode != GL40.GL_NO_ERROR){
            switch(errorCode){
                case GL40.GL_INVALID_VALUE: {
                    if(this.width < 0){
                        LoggerInterface.loggerRenderer.ERROR("Texture has width less than 0", new IllegalStateException("Texture has width less than 0"));
                    }
                    if(this.width > state.getMAX_TEXTURE_WIDTH()){
                        LoggerInterface.loggerRenderer.ERROR("Texture is greater width than environment allows", new IllegalStateException("Texture is greater width than environment allows"));
                    }
                } break;
            }
        }
    }

    @Override
    public String toString(){
        String rVal = "" +
        "Texture[" + 
        "path=\"" + path + "\", " +
        "texturePointer=\"" + texturePointer + "\", " +
        "width=\"" + width + "\", " +
        "height=\"" + height + "\", " +
        "]"
        ;
        return rVal;
    }

}
