# $Id$
##
##  This file is part of pyFormex 1.0.5  (Sat Feb 16 10:40:32 CET 2019)
##  pyFormex is a tool for generating, manipulating and transforming 3D
##  geometrical models by sequences of mathematical operations.
##  Home page: http://pyformex.org
##  Project page:  http://savannah.nongnu.org/projects/pyformex/
##  Copyright 2004-2018 (C) Benedict Verhegghe (benedict.verhegghe@ugent.be)
##  Distributed under the GNU General Public License version 3 or later.
##
##  This program is free software: you can redistribute it and/or modify
##  it under the terms of the GNU General Public License as published by
##  the Free Software Foundation, either version 3 of the License, or
##  (at your option) any later version.
##
##  This program is distributed in the hope that it will be useful,
##  but WITHOUT ANY WARRANTY; without even the implied warranty of
##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##  GNU General Public License for more details.
##
##  You should have received a copy of the GNU General Public License
##  along with this program.  If not, see http://www.gnu.org/licenses/.
##
"""opengl/renderer.py

Python OpenGL framework for pyFormex

This OpenGL framework is intended to replace (in due time)
the current OpenGL framework in pyFormex.

(C) 2013 Benedict Verhegghe and the pyFormex project.

"""
from __future__ import absolute_import, division, print_function

import pyformex as pf
from pyformex.attributes import Attributes
from pyformex.opengl.matrix import Matrix4
from pyformex.opengl.camera import orthogonal_matrix
from pyformex.opengl.shader import Shader

import numpy as np
from OpenGL import GL


class Renderer(object):

    def __init__(self,canvas,shader=None):
        self.canvas = canvas
        self.camera = canvas.camera
        self.canvas.makeCurrent()

        if shader is None:
            shader = Shader()
        self.shader = shader


    def loadLightProfile(self):
        lightprof = self.canvas.lightprof
        mat = self.canvas.material
        #print(lightprof)
        lights = [ light for light in lightprof.lights if light.enabled ]
        #for light in lights:
        #    print(light)
        nlights = len(lights)
        ambient = lightprof.ambient * np.ones(3) # global ambient
        for light in lights:
            ambient += light.ambient
        ambient = np.clip(ambient, 0., 1.) # clip, OpenGL does anyways
        diffuse = np.array([ light.diffuse for light in lights ]).ravel()
        specular = np.array([ light.specular for light in lights ]).ravel()
        position = np.array([ light.position[:3] for light in lights ]).ravel()
        #print("diffcolor = %s" % diffuse)
        #print("lightdir = %s" % position)
        settings = Attributes({
            'nlights': nlights,
            'ambicolor': ambient,
            'diffcolor': diffuse,
            'speccolor': specular,
            'lightdir': position,
            'ambient': mat.ambient,
            'diffuse': mat.diffuse,
            'specular': mat.specular,
            'shininess': mat.shininess,
            'alphablend': self.canvas.settings.alphablend,
            'alpha': self.canvas.settings.transparency,
            'bkalpha': self.canvas.settings.transparency,
            })
        #print("LIGHT PROFILE to shader: %s" % settings)
        self.shader.loadUniforms(settings)


    def setDefaults(self):
        """Set the GL context and the shader uniforms to default values."""
        GL.glLineWidth(self.canvas.settings.linewidth)
        # Enable setting pointsize in the shader
        # Maybe we should do pointsize with gl context?
        GL.glEnable(GL.GL_VERTEX_PROGRAM_POINT_SIZE)
        # NEXT, the shader uniforms
        # When all attribute names are correct, this could be done with a
        # single statement like
        #   self.shader.loadUniforms(self.canvas.settings)
        #
        self.shader.uniformInt('highlight', 0)
        self.shader.uniformFloat('lighting', self.canvas.settings.lighting)
        self.shader.uniformInt('useObjectColor', 1)
        self.shader.uniformInt('rgbamode', 0)
        self.shader.uniformVec3('objectColor', self.canvas.settings.fgcolor)
        self.shader.uniformVec3('objectBkColor', self.canvas.settings.fgcolor)
        self.shader.uniformVec3('highlightColor', self.canvas.settings.slcolor)
        self.shader.uniformFloat('pointsize', self.canvas.settings.pointsize)
        self.loadLightProfile()


    def renderObjects(self, objects):
        """Render a list of objects"""
        for obj in objects:
            GL.glDepthFunc(GL.GL_LESS)
            self.setDefaults()
            if obj.trl or obj.rot or obj.trl0:
                self.loadMatrices(rot=self.rot,trl=self.trl,trl0=self.trl0)
            obj.render(self)
            if obj.trl or obj.rot or obj.trl0:
                self.loadMatrices()


    def pickObjects(self, objects):
        """Draw a list of objects in picking mode"""
        for i, obj in enumerate(objects):
            GL.glPushName(i)
            obj.render(self)
            GL.glPopName()


    def loadMatrices(self,rot=None,trl=None,trl0=None):
        """Load the rendering matrices.

        rot, trl, trl0 are additional rotation, translation and
        pre-translation for the object.
        """
        # Get the current modelview*projection matrix
        modelview = self.camera.modelview
        if trl0:
            modelview.translate(trl0)
        if rot:
            modelview.rotate(rot)
        if trl:
            modelview.translate(trl)
        projection = self.camera.projection
        transinv = self.camera.modelview.transinv().rot

        # Propagate the matrices to the uniforms of the shader
        self.shader.uniformMat4('modelview', modelview.gl())
        self.shader.uniformMat4('projection', projection.gl())
        self.shader.uniformMat3('normalstransform', transinv.flatten().astype(np.float32))


    def render3D(self,actors,pick=None):
        """Render the 3D actors in the scene.

        """
        if not actors:
            return

        ## # Split off rendertype 1
        ## actors1 = [ a for a in actors if a.rendertype == -1 ]
        ## actors = [ a for a in actors if a.rendertype != -1 ]

        # Get the current modelview*projection matrix
        modelview = self.camera.modelview
        projection = self.camera.projection
        transinv = self.camera.modelview.transinv().rot

        # Propagate the matrices to the uniforms of the shader
        self.shader.uniformMat4('modelview', modelview.gl())
        self.shader.uniformMat4('projection', projection.gl())
        self.shader.uniformMat3('normalstransform', transinv.flatten().astype(np.float32))

        # sort actors in back and front, and select visible
        actors = back(actors) + front(actors)
        actors =  [ o for o in actors if o.visible is not False ]

        GL.glEnable(GL.GL_DEPTH_TEST)
        GL.glDepthMask (GL.GL_TRUE)

        if pick:
            # PICK MODE
            from pyformex.opengl import camera
            pickmat = camera.pick_matrix(*pick)
            self.shader.uniformMat4('pickmat', pickmat.gl())

            self.pickObjects(actors)

        elif not self.canvas.settings.alphablend:
            # NO ALPHABLEND
            self.renderObjects(actors)

        else:
            # ALPHABLEND
            # 2D actors are by default transparent!
            opaque = [ a for a in actors if a.opak ]
            transp = [ a for a in actors if not a.opak ]

            # First render the opaque objects
            self.renderObjects(opaque)

            # Then the transparent ones
            # Disable writing to the depth buffer
            # as everything behind the transparent object
            # also needs to be drawn
            GL.glDepthMask (GL.GL_FALSE)

            if pf.cfg['render/transp_nocull']:
                GL.glDisable(GL.GL_CULL_FACE)

            GL.glEnable(GL.GL_BLEND)
            GL.glBlendEquation(GL.GL_FUNC_ADD)
            if pf.cfg['render/alphablend'] == 'mult':
                GL.glBlendFunc(GL.GL_ZERO, GL.GL_SRC_COLOR)
            elif pf.cfg['render/alphablend'] == 'add':
                GL.glBlendFunc(GL.GL_ONE, GL.GL_ONE)
            elif pf.cfg['render/alphablend'] == 'trad1':
                GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE)
            else:
                GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA)
            self.renderObjects(transp)
            GL.glDisable (GL.GL_BLEND)

        ## if actors1:
        ##     modelview = Matrix4()
        ##     modelview[:3,:3] = self.camera.modelview.rot
        ##     projection = perspective_matrix(-1, 0.5, -1, 0.5, 0.1, 10.)

        ##     #self.shader.uniformMat4('modelview', modelview.gl())
        ##     self.shader.uniformMat4('projection', projection.gl())
        ##     self.renderObjects(actors1)


        GL.glDepthMask (GL.GL_TRUE)


    def render2D(self,actors):
        """Render 2D decorations.

        """
        if not actors:
            return

        #print("Render %s decorations" % len(actors))

        # Set modelview/projection
        modelview = Matrix4()
        self.shader.uniformMat4('modelview', modelview.gl())

        left = 0. # -0.5
        right = float(self.canvas.width()) # -0.5
        bottom = 0. # -0.5
        top = float(self.canvas.height()) # -0.5
        near = -1.
        far = 1.
        projection = orthogonal_matrix(left, right, bottom, top, near, far)
        self.shader.uniformMat4('projection', projection.gl())

        GL.glDisable(GL.GL_DEPTH_TEST)
        GL.glDisable(GL.GL_CULL_FACE)
        GL.glDepthMask (GL.GL_FALSE)

        # ALPHABLEND
        #
        # We need blending for text rendering!
        #
        # 2D actors are by default opak!
        opaque = [ a for a in actors if a.opak is None or a.opak ]
        transp = [ a for a in actors if a.opak is False]

        # First render the opaque objects
        self.renderObjects(opaque)

        # Then the transparent ones
        # Disable writing to the depth buffer
        # as everything behind the transparent object
        # also needs to be drawn

        GL.glEnable(GL.GL_BLEND)
        GL.glBlendEquation(GL.GL_FUNC_ADD)
        if pf.cfg['render/textblend'] == 'mult':
            GL.glBlendFunc(GL.GL_ZERO, GL.GL_SRC_COLOR)
        elif pf.cfg['render/textblend'] == 'add':
            GL.glBlendFunc(GL.GL_ONE, GL.GL_ONE)
        elif pf.cfg['render/textblend'] == 'trad1':
            GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE)
        elif pf.cfg['render/textblend'] == 'zero':
            GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ZERO)
        else:
             GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA)
        self.renderObjects(transp)
        GL.glDisable (GL.GL_BLEND)
        GL.glDepthMask (GL.GL_TRUE)


    def renderBG(self,actors):
        """Render 2D background actors.

        """
        if not actors:
            return

        #print("Render %s backgrounds" % len(actors))

        # Set modelview/projection
        modelview = projection = Matrix4()

        self.shader.uniformMat4('modelview', modelview.gl())
        self.shader.uniformMat4('projection', projection.gl())

        self.canvas.zoom_2D()  # should be combined with above

        # in clip space
        GL.glDisable(GL.GL_DEPTH_TEST)
        self.renderObjects(actors)


    def render(self,scene,pick=None):
        """Render the geometry for the scene."""
        self.shader.bind(picking=bool(pick))
        try:

            # The back backgrounds
            self.renderBG(back(scene.backgrounds))

            # The 2D back decorations
            self.render2D(back(scene.decorations)+back(scene.annotations))

            # The 3D actors
            self.render3D(scene.actors,pick)

            # The 2D front decorations
            self.render2D(front(scene.annotations)+front(scene.decorations))

            # The front backgrounds
            self.renderBG(front(scene.backgrounds))

        finally:
            self.shader.unbind()


def front(actorlist):
    """Return the actors from the list that have ontop=True"""
    return [ a for a in actorlist if a.ontop ]


def back(actorlist):
    """Return the actors from the list that have ontop=False"""
    return [ a for a in actorlist if not a.ontop ]


# End
