###########################################################################
# cgkit - Python Computer Graphics Kit
# Copyright (C) 2004 Matthias Baas (baas@ira.uka.de)
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# cgkit homepage: http://cgkit.sourceforge.net
###########################################################################
# $Id: rmshader.py,v 1.5 2005/07/12 16:43:04 mbaas Exp $

import sys, os, os.path, re, types
import protocols
from Interfaces import *
import ri
import material, lightsource
import ribexport
import slparams
from slots import *

# RMShader
class RMShader(object):
    """RenderMan shader source file.

    This class encapsulates an external RenderMan shader source file.
    The shader is parsed in the constructor and for each parameter
    a corresponding slot is created. The parameter values themselves
    are also made accessible via normal attribute access.

    When the shader is to be instantiated, you can get the shader name
    by calling shaderName() and the parameter dictionary for the
    current time by calling params(). The type of the shader is returned
    by the method shaderType().

    The source code for the material class can be obtained by calling
    loadShaderSource().

    The class can also be used to just store a shader name. In this
    case you only pass the shader name instead of the file name to
    the constructor. 

    This class has to be used in conjunction with the RMMaterial or
    RMLightSource class.

    \see RMMaterial, RMLightSource
    """
    
    def __init__(self,
                 shader = None,
                 transform = mat4(1),
                 cpp = None,
                 cpperrstream = sys.stderr,
                 *paramlist, **keyargs):
        """Constructor.

        The first argument is the shader name or file name. cpp determines
        the preprocessor that should be used when extracting parameters.
        cpperrstream is used to output errors from the preprocessor (see
        the function slparams.slparams() for details).
        Any additional arguments are shader parameters. The parameters
        can be specified as in the ri module (either as token value
        pairs, as keyword args or as a dictionary).

        \param name (\c str) Shader file name or shader name
        """
      
        # Shader file or None
        self.filename = None
        # Shader name (without path and extension)
        self.shadername = None
        # Shader type (surface, displacement, ...)
        self.shadertype = None
        # Shader parameters as list of 2-tuples (declaration, name)
        self.shaderparams = None

        # Store the shader parameters as provided by the user
        self.initialparams = ri._paramlist2dict(paramlist, keyargs)

        # Shader transformation
        self.transform = transform
        
        # If there is no extension then name is just the shader name
        # and not the shader file
        if shader!=None:
            if os.path.splitext(shader)[1]=="":
                self.shadername = shader
            else:
                self.filename = shader

        if self.filename!=None:
            slinfo = slparams.slparams(shader, cpp=cpp, cpperrstream=cpperrstream)
            if len(slinfo)==0:
                raise ValueError, "no shader found in %s"%shader
            if len(slinfo)>1:
                print "WARNING: There is more than one shader in %s"%shader

            # Create slots...
            initial = {}
            for k in self.initialparams.keys():
                v = self.initialparams[k]
                p = k.split()[-1]
                initial[p] = v
            self.shadertype, self.shadername, params = slinfo[0]
            typelut = {"float":"double",
                       "string":"string",
                       "color":"vec3",
                       "point":"vec3",
                       "vector":"vec3",
                       "normal":"vec3",
                       "matrix":"mat4"}
            self.shaderparams = []
            for p in params:
                ptype = p[2]
                parraylen = p[3]
                pname = p[4]
                if pname in initial:
                    pdefault = initial[pname]
                    del initial[pname]
                else:
                    pdefault = slparams.convertdefault(p)
                self.createSlot(pname, typelut[ptype], parraylen, pdefault)
                if parraylen==None:
                    decl = "%s %s"%(p[1], ptype)
                else:
                    decl = "%s %s[%d]"%(p[1], ptype, parraylen)
                self.shaderparams.append((decl, pname))

            for p in initial:
                print >>sys.stderr, 'Warning: Shader "%s" has no parameter "%s"'%(self.shadername, p)

    def __getattr__(self, name):
        slot = self.__dict__.get("%s_slot"%name, None)
        if slot==None:
            raise AttributeError, 'shader "%s" has no attribute "%s"'%(self.shadername, name)

        return slot.getValue()

    def __setattr__(self, name, val):
        slot = self.__dict__.get("%s_slot"%name, None)
        if slot==None:
            object.__setattr__(self, name, val)
            return 

        slot.setValue(val)


    # shaderName
    def shaderName(self):
        """Return the shader name.

        \return Shader name
        """
        return self.shadername

    # shaderType
    def shaderType(self):
        """Return the shader type.

        None is returned if only the shader name was specified.

        \return Shader type ("surface", "light", ...)
        """
        return self.shadertype

    # params
    def params(self):
        """Return the parameter dictionary for the current time.

        The parameters will contain inline declarations, so no separate
        RiDeclare() call is necessary.

        \return Dictionary containing all parameters.
        """
        if self.filename==None:
            return self.initialparams

        res = {}

        if self.shaderparams!=None:
            for decl, name in self.shaderparams:
                p = "%s %s"%(decl, name)
                res[p] = getattr(self, name)
                
        return res

    # loadShaderSource
    def loadShaderSource(self):
        """Load shader source and replace the shader name.

        This method loads a shader file, replaces the shader name
        with "$SHADERNAME" and returns the source code as a string.
        None is returned if \a filename is None.

        \return Shader source code or None
        """

        if self.filename==None:
            return None
        
        f = file(self.filename)
        src = f.read()
        # Search for <shader type> + one or more white space + <shadername>..
        match = re.search("%s\s+%s"%(self.shadertype, self.shadername), src)
        if match!=None:
            s, e = match.start(), match.end()
            src = src[:e-len(self.shadername)] + "$SHADERNAME" + src[e:]
        else:
            print 'Shader name "%s" not found in %s'%(self.shadername, self.filename)

        return src
        

    ## protected:

    # createSlot
    def createSlot(self, name, type, arraylen, default):
        """Create a slot for a shader parameter.

        \param name (\c str) Parameter name
        \param type (\c str) Slot type ("double", "vec3", ...)
        \param arraylen (\c int) Array length or None if the parameter is not an array
        \param default Default value or None
        """
        # TODO
        if type=="string":
            setattr(self, name, default)
            return
        
        if arraylen==None:
            exec "slot = %sSlot()"%type.capitalize()
        else:
            exec "slot = %sArraySlot()"%type.capitalize()
            slot.resize(arraylen)

        # TODO: set for ArraySlots
        if default!=None:
            slot.setValue(default)

        setattr(self, "%s_slot"%name, slot)

        

# RMMaterial
class RMMaterial(material.Material):
    """RenderMan material that takes shader source files as input.

    Use this material class if you want to write the RenderMan shaders
    yourself in external source files or if you want to use external shaders
    that you will compile manually.

    The material may consist of a surface shader, a displacement shader
    and an interior shader.

    The shader source files (or only the shader names) are passed via
    RMShader instances as arguments to the constructor. If the RMShader
    instance points to a file, the material object will take care of the
    compilation of the file. Otherwise, it is up to you to compile the
    shader and make sure that the renderer can find it.

    The parameters of the shaders are made available as attributes of
    the material objects. The corresponding slots can be obtained
    by adding the suffix \c _slot to the name. Attribute names in the
    surface shader have priority over the attributes in the displacement
    shader which in turn has priority over the interior shader. This means,
    if there are identical parameter names in all shaders you will access
    the parameter of the surface shader. You can also access the attributes
    of each shader via the \c surface, \c displacement and \c interior
    attributes which contain the corresponding RMShader instances.

    Example:
    
    \code
    mat = RMMaterial(surface = RMShader("mysurface.sl"),
                     displacement = RMShader("c:\\shaders\\dented.sl"),
                     displacementbound = ("current", 1.0),
                     color = (1,0.5,0.8)
                     )
    ...
    Sphere(pos=(1,2,3), radius=0.5, material=mat)
    \endcode 

    In this example, the material uses the surface shader \c mysurface.sl
    and the displacement shader \c c:\shaders\dented.sl. The shaders will
    be compiled automatically because the shader source files are given
    (instead of just the shader names).

    \see RMShader
    """

    protocols.advise(instancesProvide=[ribexport.IMaterial])

    def __init__(self,
                 name = "RMMaterial",
                 surface = None,
                 displacement = None,
                 displacementbound = ("current", 0.0),
                 interior = None,
                 color = (1,1,1),
                 opacity = (1,1,1)):
        
        material.Material.__init__(self, name, 0)

        if isinstance(surface, types.StringTypes):
            surface = RMShader(surface)
        if isinstance(displacement, types.StringTypes):
            displacement = RMShader(displacement)
        if isinstance(interior, types.StringTypes):
            interior = RMShader(interior)

        self.surface = surface
        self.displacement = displacement
        self.displacementbound = displacementbound
        self.interior = interior

        self._color = color
        self._opacity = opacity

        if self.surface==None:
            self.surface = RMShader()
        if self.displacement==None:
            self.displacement = RMShader()
        if self.interior==None:
            self.interior = RMShader()

    def __getattr__(self, name):
        if name[-5:]=="_slot":
            slotname = name
        else:
            slotname = "%s_slot"%name
        if hasattr(self.surface, slotname):
            return getattr(self.surface, name)
        elif hasattr(self.displacement, slotname):
            return getattr(self.displacement, name)
        elif hasattr(self.interior, slotname):
            return getattr(self.interior, name)            
        else:
            raise AttributeError, 'material "%s" has no attribute "%s"'%(self.name, name)


    def __setattr__(self, name, val):
        slotname = "%s_slot"%name
        srf = self.__dict__.get("surface", None)
        dsp = self.__dict__.get("displacement", None)
        itr = self.__dict__.get("itr", None)
        if hasattr(srf, slotname):
            setattr(srf, name, val)
        elif hasattr(dsp, slotname):
            setattr(dsp, name, val)
        elif hasattr(itr, slotname):
            setattr(itr, name, val)
        else:
            material.Material.__setattr__(self, name, val)


    def createPasses(self):
        """Returns a list of RenderPass objects."""
        return []

    def preProcess(self, exporter):
        """Preprocessing method."""
        pass

    def color(self):
        """Return the color for the RiColor() call or None.
        """
        return self._color

    def opacity(self):
        """Return the opacity for the RiOpacity() call or None.
        """
        return self._opacity

    # surfaceShaderName
    def surfaceShaderName(self):
        """Returns the name of the corresponding surface shader or None.
        """
        return self.surface.shaderName()

    def surfaceShaderSource(self):
        return self.surface.loadShaderSource()

    def surfaceShaderParams(self, passes):
        """Return a dictionary with shader parameters and their values."""
        return self.surface.params()

    def surfaceShaderTransform(self):
        return self.surface.transform

    # displacementShaderName
    def displacementShaderName(self):
        return self.displacement.shaderName()
    
    def displacementShaderSource(self):
        return self.displacement.loadShaderSource()
    
    def displacementShaderParams(self, passes):
        return self.displacement.params()

    def displacementBound(self):
        return self.displacementbound

    def displacementShaderTransform(self):
        return self.displacement.transform

    # interiorShaderName
    def interiorShaderName(self):
        return self.interior.shaderName()
    
    def interiorShaderSource(self):
        return self.interior.loadShaderSource()
    
    def interiorShaderParams(self, passes):
        return self.interior.params()

    def interiorShaderTransform(self):
        return self.interior.transform

# RMLightSource
class RMLightSource(lightsource.LightSource):
    """RenderMan light source.

    Use this light source class if you want to write the RenderMan light shader
    yourself in an external source file or if you want to use an external
    shader that you will compile manually.

    The shader source file (or only the shader name) is passed via a
    RMShader instances as argument to the constructor. If the RMShader
    instance points to a file, the light source will take care of the
    compilation of the file. Otherwise, it is up to you to compile the
    shader and make sure that the renderer can find it.

    The parameters of the shader are made available as attributes of
    the light source object. The corresponding slots can be obtained
    by adding the suffix \c _slot to the name. 

    \see RMShader
    """

    protocols.advise(instancesProvide=[ISceneItem, ribexport.ILightSource])

    def __init__(self,
                 name = "RMLightSource",
                 shader = None,
                 **params):
        
        lightsource.LightSource.__init__(self, name=name, **params)

        if isinstance(shader, types.StringTypes):
            shader = RMShader(shader)

        self.shader = shader

        if self.shader==None:
            self.shader = RMShader()

    def protocols(self):
        return [ISceneItem, ribexport.ILightSource]

    def __getattr__(self, name):
#        if name in self.__dict__:
#            return self.__dict__.get(name)
        
        if name[-5:]=="_slot":
            slotname = name
        else:
            slotname = "%s_slot"%name
        if hasattr(self.shader, slotname):
            return getattr(self.shader, name)
        else:
            raise AttributeError, 'light source "%s" has no attribute "%s"'%(self.name, name)


    def __setattr__(self, name, val):
        slotname = "%s_slot"%name
        shd = self.__dict__.get("shader", None)
        if hasattr(shd, slotname):
            setattr(srf, name, val)
        else:
            material.Material.__setattr__(self, name, val)


    def createPasses(self):
        """Returns a list of RenderPass objects."""
        return []

    # shaderName
    def shaderName(self):
        """Returns the name of the corresponding shader or None.
        """
        return self.shader.shaderName()

    # surfaceShaderSource
    def shaderSource(self):
        return self.shader.loadShaderSource()

    # surfaceShaderParams
    def shaderParams(self, passes):
        """Return a dictionary with shader parameters and their values."""
        return self.shader.params()


        
