from ctypes import *

import numpy as np
from .framework import *

GLUT = None

# NOTE: Render class assumes GL context is created already.
class Render:
    def __init__(self, width=1600, height=1200, name='GL Renderer',
                 program_files=['simple.fs', 'simple.vs'], color_size=1, ms_rate=1, egl=False):
        self.width = width
        self.height = height
        self.name = name
        self.use_inverse_depth = False
        self.egl = egl
        
        glEnable(GL_DEPTH_TEST)

        glClampColor(GL_CLAMP_READ_COLOR, GL_FALSE)
        glClampColor(GL_CLAMP_FRAGMENT_COLOR, GL_FALSE)
        glClampColor(GL_CLAMP_VERTEX_COLOR, GL_FALSE)

        # init program
        shader_list = []

        for program_file in program_files:
            _, ext = os.path.splitext(program_file)
            if ext == '.vs':
                shader_list.append(loadShader(GL_VERTEX_SHADER, program_file))
            elif ext == '.fs':
                shader_list.append(loadShader(GL_FRAGMENT_SHADER, program_file))
            elif ext == '.gs':
                shader_list.append(loadShader(GL_GEOMETRY_SHADER, program_file))

        self.program = createProgram(shader_list)

        for shader in shader_list:
            glDeleteShader(shader)

        # Init uniform variables
        self.model_mat_unif = glGetUniformLocation(self.program, 'ModelMat')
        self.persp_mat_unif = glGetUniformLocation(self.program, 'PerspMat')

        self.vertex_buffer = glGenBuffers(1)

        # Init screen quad program and buffer
        self.quad_program, self.quad_buffer = self.init_quad_program()

        # Configure frame buffer
        self.frame_buffer = glGenFramebuffers(1)
        glBindFramebuffer(GL_FRAMEBUFFER, self.frame_buffer)

        self.intermediate_fbo = None
        if ms_rate > 1:
            # Configure texture buffer to render to
            self.color_buffer = []
            for i in range(color_size):
                color_buffer = glGenTextures(1)
                multi_sample_rate = ms_rate
                glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, color_buffer)
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
                glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, multi_sample_rate, GL_RGBA32F, self.width, self.height, GL_TRUE)
                glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0)
                glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D_MULTISAMPLE, color_buffer, 0)
                self.color_buffer.append(color_buffer)

            self.render_buffer = glGenRenderbuffers(1)
            glBindRenderbuffer(GL_RENDERBUFFER, self.render_buffer)
            glRenderbufferStorageMultisample(GL_RENDERBUFFER, multi_sample_rate, GL_DEPTH24_STENCIL8, self.width, self.height)
            glBindRenderbuffer(GL_RENDERBUFFER, 0)
            glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, self.render_buffer)

            attachments = []
            for i in range(color_size):
                attachments.append(GL_COLOR_ATTACHMENT0 + i)
            glDrawBuffers(color_size, attachments)
            glBindFramebuffer(GL_FRAMEBUFFER, 0)

            self.intermediate_fbo = glGenFramebuffers(1)
            glBindFramebuffer(GL_FRAMEBUFFER, self.intermediate_fbo)

            self.screen_texture = []
            for i in range(color_size):
                screen_texture = glGenTextures(1)
                glBindTexture(GL_TEXTURE_2D, screen_texture)
                glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, self.width, self.height, 0, GL_RGBA, GL_FLOAT, None)
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
                glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, screen_texture, 0)
                self.screen_texture.append(screen_texture)

            glDrawBuffers(color_size, attachments)
            glBindFramebuffer(GL_FRAMEBUFFER, 0)
        else:
            self.color_buffer = []
            for i in range(color_size):
                color_buffer = glGenTextures(1)
                glBindTexture(GL_TEXTURE_2D, color_buffer)
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
                glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, self.width, self.height, 0, GL_RGBA, GL_FLOAT, None)
                glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, color_buffer, 0)
                self.color_buffer.append(color_buffer)
 
            # Configure depth texture map to render to
            self.depth_buffer = glGenTextures(1)
            glBindTexture(GL_TEXTURE_2D, self.depth_buffer)
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
            glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_INTENSITY)
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE)
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL)
            glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, self.width, self.height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, None)
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, self.depth_buffer, 0)

            attachments = []
            for i in range(color_size):
                attachments.append(GL_COLOR_ATTACHMENT0 + i)
            glDrawBuffers(color_size, attachments)
            self.screen_texture = self.color_buffer

            glBindFramebuffer(GL_FRAMEBUFFER, 0)

        
        # Configure texture buffer if needed
        self.render_texture = None

        # NOTE: original render_texture only support one input
        # this is tentative member of this issue
        self.render_texture_v2 = {}

        # Inner storage for buffer data
        self.vertex_data = None
        self.vertex_dim = None
        self.n_vertices = None

        self.model_view_matrix = None
        self.projection_matrix = None

        if not egl:
            global GLUT
            import OpenGL.GLUT as GLUT
            GLUT.glutDisplayFunc(self.display)


    def init_quad_program(self):
        shader_list = []

        shader_list.append(loadShader(GL_VERTEX_SHADER, "quad.vs"))
        shader_list.append(loadShader(GL_FRAGMENT_SHADER, "quad.fs"))

        the_program = createProgram(shader_list)

        for shader in shader_list:
            glDeleteShader(shader)

        # vertex attributes for a quad that fills the entire screen in Normalized Device Coordinates.
        # positions # texCoords
        quad_vertices = np.array(
            [-1.0, 1.0, 0.0, 1.0,
             -1.0, -1.0, 0.0, 0.0,
             1.0, -1.0, 1.0, 0.0,

             -1.0, 1.0, 0.0, 1.0,
             1.0, -1.0, 1.0, 0.0,
             1.0, 1.0, 1.0, 1.0]
        )

        quad_buffer = glGenBuffers(1)
        glBindBuffer(GL_ARRAY_BUFFER, quad_buffer)
        glBufferData(GL_ARRAY_BUFFER, quad_vertices, GL_STATIC_DRAW)

        glBindBuffer(GL_ARRAY_BUFFER, 0)

        return the_program, quad_buffer

    def set_mesh(self, vertices, faces):
        self.vertex_data = vertices[faces.reshape([-1])]
        self.vertex_dim = self.vertex_data.shape[1]
        self.n_vertices = self.vertex_data.shape[0]

        glBindBuffer(GL_ARRAY_BUFFER, self.vertex_buffer)
        glBufferData(GL_ARRAY_BUFFER, self.vertex_data, GL_STATIC_DRAW)

        glBindBuffer(GL_ARRAY_BUFFER, 0)

    def set_viewpoint(self, projection, model_view):
        self.projection_matrix = projection
        self.model_view_matrix = model_view

    def draw_init(self):
        glBindFramebuffer(GL_FRAMEBUFFER, self.frame_buffer)
        glEnable(GL_DEPTH_TEST)

        glClearColor(0.0, 0.0, 0.0, 0.0)
        if self.use_inverse_depth:
            glDepthFunc(GL_GREATER)
            glClearDepth(0.0)
        else:
            glDepthFunc(GL_LESS)
            glClearDepth(1.0)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

    def draw_end(self):
        if self.intermediate_fbo is not None:
            for i in range(len(self.color_buffer)):
                glBindFramebuffer(GL_READ_FRAMEBUFFER, self.frame_buffer)
                glReadBuffer(GL_COLOR_ATTACHMENT0 + i)
                glBindFramebuffer(GL_DRAW_FRAMEBUFFER, self.intermediate_fbo)
                glDrawBuffer(GL_COLOR_ATTACHMENT0 + i)
                glBlitFramebuffer(0, 0, self.width, self.height, 0, 0, self.width, self.height, GL_COLOR_BUFFER_BIT, GL_NEAREST)

        glBindFramebuffer(GL_FRAMEBUFFER, 0)
        glDepthFunc(GL_LESS)
        glClearDepth(1.0)

    def draw(self):
        self.draw_init()

        glUseProgram(self.program)
        glUniformMatrix4fv(self.model_mat_unif, 1, GL_FALSE, self.model_view_matrix.transpose())
        glUniformMatrix4fv(self.persp_mat_unif, 1, GL_FALSE, self.projection_matrix.transpose())

        glBindBuffer(GL_ARRAY_BUFFER, self.vertex_buffer)

        glEnableVertexAttribArray(0)
        glVertexAttribPointer(0, self.vertex_dim, GL_DOUBLE, GL_FALSE, 0, None)

        glDrawArrays(GL_TRIANGLES, 0, self.n_vertices)

        glDisableVertexAttribArray(0)

        glBindBuffer(GL_ARRAY_BUFFER, 0)

        glUseProgram(0)

        self.draw_end()

    def get_color(self, color_id=0):
        glBindFramebuffer(GL_FRAMEBUFFER, self.intermediate_fbo if self.intermediate_fbo is not None else self.frame_buffer)
        glReadBuffer(GL_COLOR_ATTACHMENT0 + color_id)
        data = glReadPixels(0, 0, self.width, self.height, GL_RGBA, GL_FLOAT, outputType=None)
        glBindFramebuffer(GL_FRAMEBUFFER, 0)
        rgb = data.reshape(self.height, self.width, -1)
        rgb = np.flip(rgb, 0)
        return rgb

    def get_z_value(self):
        glBindFramebuffer(GL_FRAMEBUFFER, self.frame_buffer)
        data = glReadPixels(0, 0, self.width, self.height, GL_DEPTH_COMPONENT, GL_FLOAT, outputType=None)
        glBindFramebuffer(GL_FRAMEBUFFER, 0)
        z = data.reshape(self.height, self.width)
        z = np.flip(z, 0)
        return z

    def display(self):
        self.draw()

        if not self.egl:
            # First we draw a scene.
            # Notice the result is stored in the texture buffer.

            # Then we return to the default frame buffer since we will display on the screen.
            glBindFramebuffer(GL_FRAMEBUFFER, 0)

            # Do the clean-up.
            glClearColor(0.0, 0.0, 0.0, 0.0)
            glClear(GL_COLOR_BUFFER_BIT)

            # We draw a rectangle which covers the whole screen.
            glUseProgram(self.quad_program)
            glBindBuffer(GL_ARRAY_BUFFER, self.quad_buffer)

            size_of_double = 8
            glEnableVertexAttribArray(0)
            glVertexAttribPointer(0, 2, GL_DOUBLE, GL_FALSE, 4 * size_of_double, None)
            glEnableVertexAttribArray(1)
            glVertexAttribPointer(1, 2, GL_DOUBLE, GL_FALSE, 4 * size_of_double, c_void_p(2 * size_of_double))

            glDisable(GL_DEPTH_TEST)

            # The stored texture is then mapped to this rectangle.
            # properly assing color buffer texture
            glActiveTexture(GL_TEXTURE0)
            glBindTexture(GL_TEXTURE_2D, self.screen_texture[0])
            glUniform1i(glGetUniformLocation(self.quad_program, 'screenTexture'), 0)

            glDrawArrays(GL_TRIANGLES, 0, 6)

            glDisableVertexAttribArray(1)
            glDisableVertexAttribArray(0)

            glEnable(GL_DEPTH_TEST)
            glBindBuffer(GL_ARRAY_BUFFER, 0)
            glUseProgram(0)

            GLUT.glutSwapBuffers()
            GLUT.glutPostRedisplay()

    def show(self):
        if not self.egl:
            GLUT.glutMainLoop()
