# vPresent is an immersive presentation creation and display application.
# Copyright (C) 2007-2008 by Infiscape Corporation
#
# 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 2 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

import math
import os
import os.path
import types

from PyQt4 import QtCore, QtGui, QtOpenGL

from OpenGL.GL import *
import osg2 as osg

import gui.util.environment as environ
import gui.sm

import opendocument as od
AI = od.AttributeInfo
import Ft.Lib.Uri as Uri
from Ft.Lib import UriException

import sys

if sys.version_info[0] == 2 and sys.version_info[1] < 4:
   import sets
   set = sets.Set


class NodeContainer(QtCore.QObject):
   '''
   This is a simple Qt object whose sole purpose is to deliver an osg.Node
   instance to a slot when a signal pertaining to an OpenSG scene graph node
   occurs.
   '''
   def __init__(self, osgNode):
      QtCore.QObject.__init__(self)
      self.node = osgNode

   def getNode(self):
      return self.node

class OpenSGView(QtOpenGL.QGLWidget):
   '''
   A specialization of QtOpenGL.QGLWidget for rendering an OpenSG scene graph
   in PyQt. To use it, just create an instance and add it to a layout. The
   methods in this class are overrides of QtOpenGL.QGLWidget methods to handle
   window system events.
   '''

   def __init__(self, parent = None, manager = None):
      QtOpenGL.QGLWidget.__init__(self, parent)

      self.isectObject = None

      if manager is not None:
         self.manager = manager
      else:
         self.manager = gui.sm.SceneManager()

      # Enable to redraw as fast as possible.
      self.mTimer = QtCore.QTimer(self)
      self.connect(self.mTimer, QtCore.SIGNAL("timeout()"), self.updateGL)
      # Try for 60 Hz.
      self.mTimer.start(16)

      self.setFocusPolicy(QtCore.Qt.StrongFocus)  # Make it possible to get key events

   def minimumSizeHint(self):
      return QtCore.QSize(50, 50)

   def sizeHint(self):
      return QtCore.QSize(500, 500)

   def initializeGL(self):
      glClearColor(0, 0, 0, 0)
      glEnable(GL_DEPTH_TEST)
      glEnable(GL_CULL_FACE)

   def paintGL(self):
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
      osg.commitChanges()
      # Ensure that the near/far setting for the camera is always big enough
      # to handle the state of the scene. Doing this here has questionable
      # value, but it is a highly convenient way of handling it.
      self.manager.refreshScene(resetNavigator = False)
      self.manager.redraw()

   def resizeGL(self, width, height):
      self.manager.resize(width, height)
      glViewport(width / 2, 0, width, height)

   def keyPressEvent(self, event):
      self.emit(QtCore.SIGNAL("keyPressed"), event)

      if event.key() == QtCore.Qt.Key_Escape:
         event.ignore()
      elif event.key() == QtCore.Qt.Key_W:
         glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
      elif event.key() == QtCore.Qt.Key_P:
         glPolygonMode(GL_FRONT_AND_BACK, GL_POINT)
      elif event.key() == QtCore.Qt.Key_F:
         glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
      elif event.key() == QtCore.Qt.Key_R:
         self.manager.resetView()
      elif event.key() == QtCore.Qt.Key_S:
         self.manager.refreshScene()
      else:
         event.accept()
         self.manager.keyPress(event.key(), event.modifiers())

   def mousePressEvent(self, event):
      self.emit(QtCore.SIGNAL("mousePressed"), event)

      button = self._qtButtonToSmButton(event.button())
      if button is not None:
         self.manager.mouseButtonPress(button, event.x(), event.y())

      self.updateGL()

   def mouseReleaseEvent(self, event):
      self.updateGL()
      self.emit(QtCore.SIGNAL("mouseReleased()"))

      button = self._qtButtonToSmButton(event.button())
      if button is not None:
         self.manager.mouseButtonRelease(button, event.x(), event.y())

      self.updateGL()

   def mouseDoubleClickEvent(self, event):
      self.emit(QtCore.SIGNAL("mouseDoubleClicked"), event)
      button = self._qtButtonToSmButton(event.button())
      if button is not None:
         self.manager.mouseButtonDoubleClick(button, event.x(), event.y())

   def mouseMoveEvent(self, event):
      self.manager.mouseMove(event.x(), event.y())
      self.updateGL()

   def wheelEvent(self, event):
      if event.delta() > 0:
         button = self.manager.MouseUp
      else:
         button = self.manager.MouseDown

      self.manager.mouseButtonPress(button, event.x(), event.y())
      self.updateGL()

   def _qtButtonToSmButton(self, button):
      '''
      Translates the given button value from its Qt representation to that
      of the button values used by our scene manager object.
      '''
      sm_button = None

      if button == QtCore.Qt.LeftButton:
         sm_button = self.manager.MouseLeft
      elif button == QtCore.Qt.MidButton:
         sm_button = self.manager.MouseMiddle
      elif button == QtCore.Qt.RightButton:
         sm_button = self.manager.MouseRight

      return sm_button

def highlightNode(node):
   mat = osg.SimpleMaterial.create()
   mat.setLit(True)
   mat.setDiffuse(osg.Color3f(0.0, 0.0, 1.0))

   scribe_chunk = osg.PolygonChunk.create()
   scribe_chunk.setFrontMode(GL_LINE)
   scribe_chunk.setOffsetLine(True)
   scribe_chunk.setOffsetFill(False)
   scribe_chunk.setOffsetPoint(False)
   scribe_chunk.setOffsetFactor(0.05)
   scribe_chunk.setOffsetBias(1.0)

   mat.addChunk(scribe_chunk)

   osg.traverse(node, lambda n, m = scribe_chunk: applyHighlight(n, m))

   return mat

def applyHighlight(node, material):
   core = node.getCore()
   if core.getClassType().isDerivedFrom(osg.Geometry.getClassType()):
      print "Highlight"


class ThumbnailCreator(object):
   """
   Renders a given scene to a FBO and returns the buffer as a QImage.
   """
   def __init__(self, glWidget, width=128, height=128):
      self.mGlWidget = glWidget
      self.mWidth = width
      self.mHeight = height

      # Create a FBO
      self.mGlWidget.makeCurrent()
      self.mFbo = QtOpenGL.QGLFramebufferObject(width, height)
      self.mGlWidget.doneCurrent()

      # Create a visit node that can be used later.
      self.mVisitNode = osg.VisitSubTreeNode.create()

   def generateThumbnail(self, root=None, refreshScene = True):
      """
      Render the given scene to a FBO and return the buffer as a QImage.

      @param root Scene graph root that should be rendered. If None, use the
                  mGlWidget's current scene root.
      @param refreshScene If True call refreshScene() before rendering.
      """
      if refreshScene:
         self.mGlWidget.manager.refreshScene()

      # Save the stats state.
      stats_on = self.mGlWidget.manager.getStatistics()
      self.mGlWidget.manager.setStatistics(False)

      old_root = None
      if root is not None:
         # The SimpleSceneManager contains an internal root and calling
         # self.mManager.setSceneRoot() actually just adds the argument as a
         # child to the internal root. Calling addChild actually removes a
         # node from its previous parent. Since we will not know the previous
         # parent, we use a VisitSubTree node to just visit.
         old_root = self.mGlWidget.manager.getSceneRoot()
         self.mGlWidget.manager.setSceneRoot(self.mVisitNode.node())
         self.mVisitNode.setSubTreeRoot(root)

         # XXX: VisitNode does not get handled correctly.
         #self.mGlWidget.manager.refreshScene()

      # Save all OpenGL state.
      glPushAttrib(GL_ALL_ATTRIB_BITS);
      glMatrixMode(GL_PROJECTION);
      glPushMatrix();
      glMatrixMode(GL_MODELVIEW);
      glPushMatrix();

      # Bind the FBO so that we render into it.
      self.mFbo.bind()

      # Update the viewport for the FBO size.
      self.mGlWidget.manager.resize(self.mWidth, self.mHeight)
      glViewport(self.mWidth / 2, 0, self.mWidth, self.mHeight)

      # Clear and render.
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
      osg.commitChanges()
      self.mGlWidget.manager.redraw()

      # Release FBO so that we go back to the default render target buffer.
      self.mFbo.release()

      # Restore OpenGL state.
      glMatrixMode(GL_PROJECTION);
      glPopMatrix();
      glMatrixMode(GL_MODELVIEW);
      glPopMatrix();
      glPopAttrib();

      # Update the viewport for the mGlWidget size.
      width = self.mGlWidget.width()
      height = self.mGlWidget.height()
      self.mGlWidget.manager.resize(width, height)
      glViewport(width / 2, 0, width, height)

      # Restore the stats state.
      self.mGlWidget.manager.setStatistics(stats_on)

      # If we changed the root, restore the old root.
      if old_root is not None:
         self.mGlWidget.manager.setSceneRoot(old_root)
         self.mVisitNode.setSubTreeRoot(None)
         self.mGlWidget.manager.refreshScene()

      return self.mFbo.toImage()

# XXX: Do not override attributes in this module. It will work, but
#      it is not the correct place.
def setTransform(obj, realName, value):
   assert isinstance(value, osg.Matrix)
   transform_str = "%s %s %s  %s %s %s  %s %s %s  %s %s %s" % \
      (value[0][0], value[0][1], value[0][2],
       value[1][0], value[1][1], value[1][2],
       value[2][0], value[2][1], value[2][2],
       value[3][0], value[3][1], value[3][2])
   if not (0 == value[0][3] and 0 == value[1][3] and 0 == value[2][3] and 1.0 == value[3][3]):
      print "WARNING: None affine transformation:\n", value
   obj._attrs[realName] = transform_str

def getTransform(obj, realName, default):
   value = obj._attrs.get(realName, default)
   assert isinstance(value, basestring)
   comps = value.split()
   assert 12 == len(comps)
   comps = [float(c) for c in comps]
   return osg.Matrix(comps[0], comps[3],  comps[6], comps[9],
                     comps[1], comps[4],  comps[7], comps[10],
                     comps[2], comps[5],  comps[8], comps[11],
                          0.0,      0.0,       0.0,       1.0)

class Page(od.tag_class_map['draw:page']):
   __metaclass__ = od.OpenDocumentMeta
   def __init__(self, attrs = None):
      super(Page, self).__init__(attrs)

      self._dirty = True
      self._groupNode = osg.GroupNode.create()
      self._rootNode = self._groupNode.node()

      self._backgroundRoot = osg.TransformNode.create()
      self._backgroundRootNode = self._backgroundRoot.node()
      self._thumbnailImage = None

   def insertChild(self, idx, child):
      result = super(Page, self).insertChild(idx, child)
      child_root = child.getRootNode()
      if child_root is not None:
         self._rootNode.addChild(child_root)

      return result

   def subChild(self, child):
      super(Page, self).subChild(child)
      child_root = child.getRootNode()
      if child_root is not None:
         self._rootNode.subChild(child_root)

   def _update(self, forceUpdate=False):
      print "_update(%s)" % (self)
      if not self._dirty and not forceUpdate:
         return
      self._dirty = False
      print "_updating(%s)" % (self)

      # Update all children.
      for c in self._children:
         if hasattr(c, '_update'):
            c._update()

      # Remove all page background geometry.
      while self._backgroundRootNode.getNChildren() > 0:
         self._backgroundRootNode.subChild(0)

      # Get the name of our style.
      dsn = self.draw_style_name

      # If we have an empty background we are done.
      if dsn is None:
         return

      env = environ.Environment()
      doc = env.curDocument.document
      page_style = doc.getPageStyle(dsn)
      #print "Trying to find page style:", dsn
      if page_style is not None:
         page3d_props = page_style.find('./vpresent:page3d-properties')

         # XXX: Fix this so that it is not hardcoded.
         background = page_style.find('./vpresent:page3d-properties/vpresent:sphere-background')
         if background is not None:
            bgeom = background.find('./dr3d:sphere')
            if bgeom is not None:
               # Ignore the sphere background size from the data and always
               # use 1.0. This is done so that we can scale the background
               # dynamically and uniformly (see updateBackgroundSize()).
               if bgeom._geomDirty:
                  bgeom.dr3d_size = 1.0
                  bgeom._update()
               # Build background after geom since it assigns a material to it.
               background._update()
               if bgeom.getRootNode() is not None:
                  # Clone the background's tree so that multiple slides can use it.
                  back_root = osg.cloneTree(bgeom.getRootNode())
                  self._backgroundRootNode.addChild(back_root)

   def updateBackgroundSize(self, far):
      m = self._backgroundRoot.getMatrix()
      scale = far * 0.9
      m.setScale(osg.Vec3f(scale, scale, scale))
      self._backgroundRoot.setMatrix(m)

   def getBackgroundRoot(self):
      return self._backgroundRootNode

class Scene(od.tag_class_map['dr3d:scene']):
   __metaclass__ = od.OpenDocumentMeta
   def __init__(self, attrs = None):
      super(Scene, self).__init__(attrs)

      self._dirty = True
      self._transformNode = osg.TransformNode.create()
      self._rootNode = self._transformNode.node()

   def insertChild(self, idx, child):
      result = super(Scene, self).insertChild(idx, child)
      child_root = child.getRootNode()
      if child_root is not None:
         self._rootNode.addChild(child_root)

      return result

   def subChild(self, child):
      super(Scene, self).subChild(child)
      child_root = child.getRootNode()
      if child_root is not None:
         self._rootNode.subChild(child_root)

   def _update(self, forceUpdate=False):
      print "_update(%s)" % (self)
      if not self._dirty and not forceUpdate:
         return
      self._dirty = False
      print "_updating(%s)" % (self)

      # Update all children.
      for c in self._children:
         if hasattr(c, '_update'):
            c._update()


class Style(od.tag_class_map['style:style']):
   __metaclass__ = od.OpenDocumentMeta
   mime_type = 'application/vpresent-style'
   def __init__(self, attrs = None):
      super(Style, self).__init__(attrs)

      self._dirty = True
      self._chunkMaterial = None

      # XXX: Odd case where a Style can actually be two different things.
      self._chunkMaterial = osg.ChunkMaterial.create()

   def _update(self, forceUpdate=False):
      print "_update(%s)" % (self)
      if not self._dirty and not forceUpdate:
         return
      self._dirty = False
      print "_updating(%s)" % (self)

      if self.style_family == 'graphic':
         # Clear all existing chunks.
         self._chunkMaterial.clearChunks()
         # Find graphics properties and add chunks.
         gp = self.find('./style:graphic-properties')
         if gp is not None:
            gp._update()
            self._chunkMaterial.addChunk(gp._materialChunk)
            self._chunkMaterial.addChunk(gp._texObjChunk)
            self._chunkMaterial.addChunk(gp._texGenChunk)
            self._chunkMaterial.addChunk(gp._texEnvChunk)

class GraphicProperties(od.tag_class_map['style:graphic-properties']):
   __metaclass__ = od.OpenDocumentMeta
   def __init__(self, attrs = None):
      super(GraphicProperties, self).__init__(attrs)

      self._dirty = True
      self._materialChunk = osg.MaterialChunk.create()
      self._texObjChunk = osg.TextureObjChunk.create()
      self._texGenChunk = osg.TexGenChunk.create()
      self._texEnvChunk = osg.TextureEnvChunk.create()

   def _update(self, forceUpdate=False):
      print "_update(%s)" % (self)
      if not self._dirty and not forceUpdate:
         return
      self._dirty = False
      print "_updating(%s)" % (self)

      self._texObjChunk.setMinFilter(GL_LINEAR_MIPMAP_LINEAR)
      self._texObjChunk.setMagFilter(GL_LINEAR)
      self._texObjChunk.setWrapS(GL_REPEAT)
      self._texObjChunk.setWrapT(GL_REPEAT)
      self._texObjChunk.setWrapR(GL_REPEAT)

      self._texGenChunk.setGenFuncS(GL_NONE)
      self._texGenChunk.setGenFuncT(GL_NONE)

      self._texEnvChunk.setEnvMode(GL_REPLACE)

      # Find our fill image.
      fill_image = None
      fin = self.draw_fill_image_name
      if fin is not None:
         env = environ.Environment()
         fill_image = env.curDocument.document.getFillImage(fin)

      # Set the correct image.
      if fill_image is not None:
         #  Update the FillImage to make sure it is loaded into memory.
         fill_image._update()
         self._texObjChunk.setImage(fill_image._image)
      else:
         self._texObjChunk.setImage(None)

class FillImage(od.tag_class_map['draw:fill-image']):
   __metaclass__ = od.OpenDocumentMeta
   mime_type = 'application/vpresent-fillimage'
   def __init__(self, attrs = None):
      super(FillImage, self).__init__(attrs)

      self._dirty = True
      self._image = None
      self._thumbnail = None
      self._image = osg.Image.create()

   def _update(self, forceUpdate=False):
      print "_update(%s)" % (self)
      if not self._dirty and not forceUpdate:
         return
      self._dirty = False
      print "_updating(%s)" % (self)

      env = environ.Environment()

      # Try to find the texture.
      filename = self.xlink_href
      if not os.path.exists(filename):
         filename = os.path.join(env.workDir, filename)

      # If we found the texture.
      if os.path.exists(filename):
         self._image.read(str(filename))
         self._built = True

   def _getThumbnail(self):
      """
      Gets a thumbnail representation of this FillImage. If the thumbnail is not
      already loaded into memory it first tries to read it from a Thumbnail
      directory. If the thumbnail file does not exist or the fillimage has a
      different modification time we generate a thumbnail and save it to disk.
      """
      if self._thumbnail is None:
         env = environ.Environment()

         # TODO: Find a better place to put this.
         thumbnail_dir = os.path.join(env.workDir, 'Thumbnails')
         if not os.path.exists(thumbnail_dir):
            os.makedirs(thumbnail_dir)

         # Get the thumbnail file name.
         filename = self.xlink_href
         # Try to find the texture.
         if not os.path.exists(filename):
            filename = os.path.join(env.workDir, filename)

         # Get the name of the thumbnail image.
         thumbnail_filename = os.path.join(env.workDir, 'Thumbnails', '%s.png'%self.draw_name)
         #print "Filename: %s thumb: %s" % (filename, thumbnail_filename)

         # If file does not exists, return None.
         if not os.path.exists(filename):
            return None

         # If the thumbnail exists and the modification time is later
         # than the fill image.
         if os.path.exists(thumbnail_filename) and \
            os.stat(thumbnail_filename).st_mtime >= os.stat(filename).st_mtime:
            #print "Using the thumbnail from disk"
            self._thumbnail = QtGui.QIcon(thumbnail_filename)
         else:
            #if not os.path.exists(thumbnail_filename):
            #   print "Thumbnail does not exist."
            #else:
            #   import time
            #   print "Mod times did not match: %s != %s" % \
            #      (time.ctime(os.stat(thumbnail_filename).st_mtime),
            #       time.ctime(os.stat(filename).st_mtime))
            # Create a thumbnail and return it.
            # XXX: Should we use a QIcon or a QPixmap.
            pixmap = QtGui.QPixmap(filename)
            pixmap = pixmap.scaled(64, 64, QtCore.Qt.IgnoreAspectRatio,
                                   QtCore.Qt.SmoothTransformation)
            pixmap.save(thumbnail_filename)
            self._thumbnail = QtGui.QIcon(pixmap)
      return self._thumbnail


class FillImageRef(od.tag_class_map['vpresent:fill-image-ref']):
   __metaclass__ = od.OpenDocumentMeta
   def __init__(self, attrs = None):
      super(FillImageRef, self).__init__(attrs)
      self._dirty = True
      self._changeCallbacks = set()

   def set_draw_fill_image_name(self, realName, value):
      self._attrs[realName] = value
      self._dirty = True
      for cb in self._changeCallbacks:
         cb(self)

   def set_vpresent_mirror(self, realName, value):
      self._attrs[realName] = value
      self._dirty = True
      for cb in self._changeCallbacks:
         cb(self)

   def _update(self, forceUpdate=False):
      if self._dirty or forceUpdate:
         env = environ.Environment()
         doc = env.curDocument.document

         # By default we set the image to None.
         self._image = None

         # Get the name of the fill image that we are looking for.
         fin = self.draw_fill_image_name
         if fin is not None:
            fill_image = doc.getFillImage(fin)
            if fill_image is not None and fill_image._image is not None:
               # Update the FillImage to make sure it's data is loaded.
               fill_image._update()

               # Get the source image.
               src_image = fill_image._image

               # Get the mirror state.
               mirror = self.vpresent_mirror
               if 'both' == mirror:
                  self._image = osg.Image.create()
                  src_image.mirror(True, True, False, self._image)
               elif 'horizontal' == mirror:
                  self._image = osg.Image.create()
                  src_image.mirror(True, False, False, self._image)
               elif 'vertical' == mirror:
                  self._image = osg.Image.create()
                  src_image.mirror(False, True, False, self._image)
               else:
                  self._image = src_image

   def _getImage(self):
      self._update()
      return self._image


class FillImageRefParentMixin(object):
   """
   Derive from this class to automatically register a callback with all
   FillImageRef children. You must implement _onImageRefChanged in your class.
   """
   def insertChild(self, idx, child):
      """ Add a change callback to ImageRef children. """
      if isinstance(child, FillImageRef):
         child._changeCallbacks.add(self._onImageRefChanged)

      # Call the next class in the method resolution order. (__mro__)
      super(FillImageRefParentMixin, self).insertChild(idx, child)

   def subChild(self, indexOrObj):
      """ Remove change callback from ImageRef children. """
      if types.IntType == type(indexOrObj):
         child = self._children[indexOrObj]
      else:
         child = indexOrObj

      if isinstance(child, FillImageRef):
         child._changeCallbacks.remove(self._onImageRefChanged)

      # Call the next class in the method resolution order. (__mro__)
      super(FillImageRefParentMixin, self).subChild(child)

   def _onImageRefChanged(self, imageRef):
      raise NotImplemented("_onImageRefChanged not implemented for %s."%(self.__class__))


class TransformableObjectMixin(object):
   __metaclass__ = od.MixinMeta

   overrides = {'dr3d:transform':AI(get=getTransform, set=setTransform),}

   def __init__(self):
      self._xformDirty = True
      self._transformNode = osg.TransformNode.create()

   def set_dr3d_transform(self, realName, value):
      self._xformDirty = True
      setTransform(self, realName, value)

   def _update(self, forceUpdate=False):
      # Update the transform.
      if self._xformDirty or forceUpdate:
         print "_updating xform(%s)" % (self)
         self._xformDirty = False
         # XXX: Should define a way to not rebuild geom when
         #      changing transform.
         xform = self.dr3d_transform
         self._transformNode.setMatrix(xform)

class MaterialMixin(object):
   __metaclass__ = od.MixinMeta

   def __init__(self):
      self._materialAssignmentDirty = True
      self._geode = None

   def _update(self, forceUpdate=False):
      print "_update(%s)" % (self)

      if self._materialAssignmentDirty or forceUpdate:
         print "_updating material assignment(%s)" % (self)
         self._materialAssignmentDirty = False

         env = environ.Environment()
         doc = env.curDocument.document
         material = doc.getMaterial(self.draw_style_name)
         if material is not None:
            material._update()
            assert material._chunkMaterial is not None
            assert self._geode is not None
            assert self._geode.getCore() is not None
            assert isinstance(self._geode.getCore(), osg.Geometry)
            self._geode.getCore().setMaterial(material._chunkMaterial)
         else:
            # XXX: Should we have a vPresent default material instead of
            #      using the OpenSG default material?
            self._geode.getCore().setMaterial(None)

class ShapeMaterialMixin(TransformableObjectMixin, MaterialMixin):
   __metaclass__ = od.MixinMeta

   def __init__(self):
      TransformableObjectMixin.__init__(self)
      MaterialMixin.__init__(self)
      self._geomDirty = True
      self._rootNode = self._transformNode.node()
      self._geode = None

   def _update(self, forceUpdate=False):
      print "_update(%s)" % (self)

      #print "Shape rebuild:", id(self)
      TransformableObjectMixin._update(self, forceUpdate)

      # Update the geometry and it's material.
      if self._geomDirty or forceUpdate:
         print "_updating geometry(%s)" % (self)
         self._geomDirty = False

         if self._geode is not None:
            self._rootNode.subChild(self._geode)
         self._geode = self._makeGeom()
         self._rootNode.addChild(self._geode)

      MaterialMixin._update(self, forceUpdate)


class Sphere(od.tag_class_map['dr3d:sphere'], ShapeMaterialMixin):
   """ Represents an vPresent sphere in OpenSG. """
   __metaclass__ = od.OpenDocumentMeta
   def __init__(self, attrs = None):
      super(Sphere, self).__init__(attrs)
      ShapeMaterialMixin.__init__(self)

   def set_dr3d_size(self, realName, value):
      od.setPositiveFloat(self, realName, value)
      self._geomDirty = True
      #self._update()

   def _makeGeom(self):
      return osg.makeLatLongSphere(self.vpresent_latres,
                                   self.vpresent_lonres,
                                   self.dr3d_size)

class Box(od.tag_class_map['vpresent:box'], ShapeMaterialMixin):
   """ Represents an vPresent box in OpenSG. """
   __metaclass__ = od.OpenDocumentMeta
   def __init__(self, attrs = None):
      super(Box, self).__init__(attrs)
      ShapeMaterialMixin.__init__(self)

   def _makeGeom(self):
      return osg.makeBox(self.vpresent_width, self.vpresent_height,
                         self.vpresent_depth, self.vpresent_width_res,
                         self.vpresent_height_res, self.vpresent_depth_res)

class Cylinder(od.tag_class_map['vpresent:cylinder'], ShapeMaterialMixin):
   """ Represents an vPresent cylinder in OpenSG. """
   __metaclass__ = od.OpenDocumentMeta
   def __init__(self, attrs = None):
      super(Cylinder, self).__init__(attrs)
      ShapeMaterialMixin.__init__(self)

   def _makeGeom(self):
      return osg.makeCylinder(self.vpresent_height, self.vpresent_radius,
                              self.vpresent_num_sides, self.vpresent_show_sides,
                              self.vpresent_show_top, self.vpresent_show_bottom)

class Fisheye(od.tag_class_map['vpresent:fisheye'], ShapeMaterialMixin):
   """ Represents an vPresent fisheye in OpenSG. """
   __metaclass__ = od.OpenDocumentMeta
   def __init__(self, attrs = None):
      super(Fisheye, self).__init__(attrs)
      ShapeMaterialMixin.__init__(self)

   def set_dr3d_size(self, realName, value):
      od.setPositiveFloat(self, realName, value)
      self._geomDirty = True

   def _makeGeom(self):
      return makeDome(self.dr3d_size,
                      self.vpresent_latres,
                      self.vpresent_lonres,
                      self.vpresent_span)

class Billboard(od.tag_class_map['vpresent:billboard'],
                TransformableObjectMixin, MaterialMixin):
   """ Represents an vPresent billboard in OpenSG. """
   __metaclass__ = od.OpenDocumentMeta
   def __init__(self, attrs = None):
      super(Billboard, self).__init__(attrs)
      TransformableObjectMixin.__init__(self)
      MaterialMixin.__init__(self)
      self._geomDirty = True
      self._rootNode = self._transformNode.node()
      self._billboard = osg.BillboardNode.create()
      self._billboard.setAxisOfRotation(osg.Vec3f(0.0, 0.0, 0.0))
      self._billboard.setFocusOnCamera(True)
      self._billboard.setAlignToScreen(False)
      self._rootNode.addChild(self._billboard.node())
      self._geode = None

   def _update(self, forceUpdate=False):
      print "_update(%s)" % (self)

      #print "Shape rebuild:", id(self)
      TransformableObjectMixin._update(self, forceUpdate)

      # Update the geometry and it's material.
      # XXX: Do geometry ourselves instead of MaterialShapeMixin
      #      because we do not want to add geode to the root node.
      #      Instead we add it under the billboard node.
      if self._geomDirty or forceUpdate:
         print "_updating geometry(%s)" % (self)
         self._geomDirty = False

         if self._geode is not None:
            self._billboard.node().subChild(self._geode)
         self._geode = self._makeGeom()
         self._billboard.node().addChild(self._geode)

      #dsn = self.draw_style_name
      #if dsn is None:
      #   return
      MaterialMixin._update(self, forceUpdate)

   def _makeGeom(self):
      return osg.makePlane(self.vpresent_width, self.vpresent_height,
                           self.vpresent_width_res, self.vpresent_height_res)


class Model(od.tag_class_map['vpresent:model'],
            TransformableObjectMixin):
   """ Represents an vPresent model in OpenSG. """

   __metaclass__ = od.OpenDocumentMeta
   def __init__(self, attrs = None):
      super(Model, self).__init__(attrs)
      TransformableObjectMixin.__init__(self)

      self._modelFileDirty = True
      self._rootNode = self._transformNode.node()

   def addChild(self, child):
      super(Model, self).addChild(child)

   def set_vpresent_filename(self, realName, value):
      try:
         self._attrs[realName] = Uri.OsPathToUri(value, attemptAbsolute=False)

         # Remove all old models.
         while self._rootNode.getNChildren() > 0:
            self._rootNode.subChild(0)

         # Load the new model.
         self._modelFileDirty = True
         self._update()
      except UriException:
         self._attrs[realName] = value

   def get_vpresent_filename(self, realName, default):
      value = self._attrs.get(realName, default)
      try:
         return Uri.UriToOsPath(value, attemptAbsolute=False)
      except UriException:
         return value

   def _update(self, forceUpdate=False):
      print "_update(%s)" % (self)

      TransformableObjectMixin._update(self, forceUpdate)

      if not self._modelFileDirty and not forceUpdate:
         return
      self._modelFileDirty = False
      print "_updating(%s)" % (self)

      # Try to find the model.
      filename = self.vpresent_filename
      if not os.path.exists(filename):
         env = environ.Environment()
         filename = os.path.join(env.workDir, filename)

      if not os.path.exists(filename):
         QtGui.QMessageBox.warning(None, "Model not found.",
            "Could not find model %s"%os.path.basename(filename))
      else:
         model = osg.SceneFileHandler.the().read(str(filename))
         if model is not None:
            self._rootNode.addChild(model)


class SphereBackground(FillImageRefParentMixin,
                       od.tag_class_map['vpresent:sphere-background']):
   __metaclass__ = od.OpenDocumentMeta
   mime_type = 'application/vpresent-fillimage'
   def __init__(self, attrs = None):
      super(SphereBackground, self).__init__(attrs)

      self._materialAssignmentDirty = True
      self._fillImagesDirty = True
      self._typeDirty = True

      self._chunkMaterial = osg.ChunkMaterial.create()
      self._materialChunk = osg.MaterialChunk.create()
      self._texGenChunk   = osg.TexGenChunk.create()
      self._texEnvChunk   = osg.TextureEnvChunk.create()
      self._polygonChunk  = osg.PolygonChunk.create()
      self._texObjChunk   = None

      # Connect all chunks.
      self._chunkMaterial.clearChunks()
      self._chunkMaterial.addChunk(self._materialChunk)
      self._chunkMaterial.addChunk(self._texGenChunk)
      self._chunkMaterial.addChunk(self._texEnvChunk)
      self._chunkMaterial.addChunk(self._polygonChunk)

      self._texEnvChunk.setEnvMode(GL_REPLACE)
      self._polygonChunk.setFrontFace(GL_CW)

   def set_vpresent_type(self, realName, value):
      valid_values = self.element.attrs[realName].values
      if value not in valid_values:
         raise AttributeError("%s not a valid value. %s" %  \
            (value, valid_values))
      self._attrs[realName] = value

      # Since our type changed, we are dirty.
      self._typeDirty = True

   def _onImageRefChanged(self, imgRef):
      """ Slot that is called when a child ImageRef changes it's target. """
      self._fillImagesDirty = True

      # Update immediately since we can not guarentee update will be
      # called on this object anytime soon.
      self._update()

   def _update(self, forceUpdate=False):
      print "_update(%s)" % (self)

      if self._typeDirty:
         self._typeDirty = False
         self._updateType()
         # Force our fill images to be attached to our new texture chunk.
         self._fillImagesDirty = True

      if self._fillImagesDirty:
         self._fillImagesDirty = False
         self._updateFillImages()

      if self._materialAssignmentDirty:
         self._materialAssignmentDirty = False
         self._updateMaterialAssignment()

   #### Private update helper methods. ####

   def _updateType(self):
      # Remove the old chunk.
      if self._texObjChunk is not None:
         self._chunkMaterial.subChunk(self._texObjChunk)

      # Build the correct texture object.
      if 'mercator' == self.vpresent_type:
         self._texObjChunk = osg.TextureObjChunk.create()
      else:
         self._texObjChunk = osg.CubeTextureObjChunk.create()

      self._chunkMaterial.addChunk(self._texObjChunk)

      if 'mercator' == self.vpresent_type:
         self._texObjChunk.setMinFilter(GL_LINEAR_MIPMAP_LINEAR)
         self._texObjChunk.setMagFilter(GL_LINEAR)
         self._texObjChunk.setWrapS(GL_REPEAT)
         self._texObjChunk.setWrapT(GL_REPEAT)
         self._texObjChunk.setWrapR(GL_REPEAT)

         self._texGenChunk.setGenFuncS(GL_NONE)
         self._texGenChunk.setGenFuncT(GL_NONE)
         self._texGenChunk.setGenFuncR(GL_NONE)
      else:
         self._texObjChunk.setMinFilter(GL_LINEAR)
         self._texObjChunk.setMagFilter(GL_NEAREST)

         # OBJECT
         self._texGenChunk.setGenFuncS(GL_OBJECT_LINEAR)
         self._texGenChunk.setGenFuncT(GL_OBJECT_LINEAR)
         self._texGenChunk.setGenFuncR(GL_OBJECT_LINEAR)

   def _updateFillImages(self):
      if 'mercator' == self.vpresent_type:
         target = [self._texObjChunk.setImage,]
      else:
         target = [self._texObjChunk.setNegXImage,
                   self._texObjChunk.setPosXImage,
                   self._texObjChunk.setNegYImage,
                   self._texObjChunk.setPosYImage,
                   self._texObjChunk.setImage,
                   self._texObjChunk.setPosZImage]

      img_refs = od.NodeFilter(self, 'vpresent:fill-image-ref')

      for (idx, t) in enumerate(target):
         if idx >= len(img_refs):
            print "WARNING: SphereBackground does not have enough image refs."
            continue

         # Get the image OpenSG image from the FillImageRef.
         img_ref = img_refs[idx]
         dst_image = img_ref._getImage()
         t(dst_image)

   def _updateMaterialAssignment(self):
      sphere = self.find('./dr3d:sphere')
      if sphere is not None:
         # Attach our material to the sphere.
         assert hasattr(sphere, '_geode')
         assert sphere._geode is not None
         geom = sphere._geode.getCore()
         assert isinstance(geom, osg.Geometry)
         geom.setMaterial(self._chunkMaterial)

         # Invert the sphere normals the first time.
         if not hasattr(sphere, '_inverted') or sphere._inverted == False:
            sphere._inverted = True
            norms = geom.getNormals()
            for i in xrange(norms.size()):
               old = norms.getValue_Vec3f(i)
               new = -old
               norms.setValue(new, i)
               #print "inverting normal[%s] %s -> %s" %(i,old,new)
            geom.setNormals(norms)

      else:
         print "Could not find background sphere."


class CylinderPanorama(FillImageRefParentMixin,
                       od.tag_class_map['vpresent:cylinder-panorama'],
                       TransformableObjectMixin):
   __metaclass__ = od.OpenDocumentMeta
   def __init__(self, attrs = None):
      super(CylinderPanorama, self).__init__(attrs)
      TransformableObjectMixin.__init__(self)
      self._rootNode = self._transformNode.node()

      self._materials = {}
      self._geomDirty = True
      self._materialsDirty = True
      self._orderDirty = True

   def _childMoved(self):
      self._orderDirty = True

   def _childrenAddedOrRemoved(self):
      self._orderDirty = True
      self._geomDirty = True
      self._materialsDirty = True

   def set_vpresent_height(self, realName, value):
      self._geomDirty = True
      od.setGTZFloat(self, realName, value)
      #super(CylinderPanorama, self).vpresent_height = value

   def set_vpresent_radius(self, realName, value):
      self._geomDirty = True
      od.setGTZFloat(self, realName, value)
      #super(CylinderPanorama, self).vpresent_radius = value

   def set_vpresent_start_arc(self, realName, value):
      self._geomDirty = True
      od.setFloat(self, realName, value, 0.0, 360.0)
      #super(CylinderPanorama, self).vpresent_start_arc = value

   def set_vpresent_end_arc(self, realName, value):
      self._geomDirty = True
      od.setFloat(self, realName, value, 0.0, 360.0)
      #super(CylinderPanorama, self).vpresent_end_arc = value

   def _onImageRefChanged(self, imgRef):
      """ Slot that is called when a child ImageRef changes it's target. """
      self._materialsDirty = True

      # Update immediately since we can not guarentee update will be
      # called on this object anytime soon.
      self._update()

   def _update(self, forceUpdate=False):
      print "_update(%s)" % (self)
      TransformableObjectMixin._update(self, forceUpdate)

      env = environ.Environment()
      doc = env.curDocument.document

      # If the image refs have changed, get the correct materials.
      if self._materialsDirty or forceUpdate:
         self._materialsDirty = False
         self.__updateMaterialStore()
         # Force the materials to be re-assigned.
         self._orderDirty = True
      
      if self._geomDirty or forceUpdate:
         self._geomDirty = False
         self.__updateGeometry()
         # Force the materials to be re-assigned.
         self._orderDirty = True

      # If the order of materials is wrong, update it.
      if self._orderDirty or forceUpdate:
         self._orderDirty = False
         self.__updateMaterialAssignments()

   #### Private update helper methods. ####

   def __updateMaterialStore(self):
      print "_updating material store(%s)" % (self)
      old_store = self._materials
      self._materials = {}

      for img_ref in self._children:
         key = "%s:%s" % (img_ref.draw_fill_image_name, img_ref.vpresent_mirror)
         print "Trying to find FillImage:", key
         if old_store.has_key(key):
            print "   using old material."
            self._materials[key] = old_store[key]
         else:
            print "   creating a new material."
            self._materials[key] = self.__createMaterial(img_ref)

   def __updateMaterialAssignments(self):
      print "_updating material assignment(%s)" % (self)
      # Connect materials to new geometry.
      child_idx = 0
      for img_ref in self._children[::-1]:
         c = self._rootNode.getChild(child_idx)
         key = "%s:%s" % (img_ref.draw_fill_image_name, img_ref.vpresent_mirror)
         assert self._materials.has_key(key) and \
            "Material should be in material store. Ensure that " \
            "_updateMaterialStore was called after last edit."
         mat = self._materials[key]
         c.getCore().setMaterial(mat)
         child_idx += 1

   def __updateGeometry(self):
      print "_updating geom(%s)" % (self)
      # Remove all old children.
      while self._rootNode.getNChildren() > 0:
         self._rootNode.subChild(0)

      height = self.vpresent_height
      radius = self.vpresent_radius
      num_sides = self.vpresent_num_sides_per_texture
      start_arc = self.vpresent_start_arc
      end_arc = self.vpresent_end_arc

      if start_arc > end_arc:
         start_arc = start_arc - 360.0

      num_images = len(self._children)
      assert num_images > 0
      total_arc = end_arc - start_arc
      delta = total_arc/num_images

      # Rebuild the geometry.
      for (idx, img_ref) in enumerate(self._children[::-1]):
         startd = start_arc+(delta*idx)
         stopd = startd + delta
         geom_node = makeCylinderPanorama(height, radius, num_sides, math.radians(startd), math.radians(stopd))
         geom = geom_node.getCore()
         self._rootNode.addChild(geom_node)

   def __createMaterial(self, imgRef):
      _chunkMaterial = osg.ChunkMaterial.create()
      _materialChunk = osg.MaterialChunk.create()
      _texObjChunk = osg.TextureObjChunk.create()
      _texGenChunk = osg.TexGenChunk.create()
      _texEnvChunk = osg.TextureEnvChunk.create()
      _chunkMaterial.addChunk(_materialChunk)
      _chunkMaterial.addChunk(_texObjChunk)
      _chunkMaterial.addChunk(_texGenChunk)
      _chunkMaterial.addChunk(_texEnvChunk)

      _texObjChunk.setMinFilter(GL_LINEAR_MIPMAP_LINEAR)
      _texObjChunk.setMagFilter(GL_LINEAR)

      # Clamp the texture coordinates to the border. [0,1]
      _texObjChunk.setWrapS(GL_CLAMP)
      _texObjChunk.setWrapT(GL_CLAMP)

      _texGenChunk.setGenFuncS(GL_NONE)
      _texGenChunk.setGenFuncT(GL_NONE)

      _texEnvChunk.setEnvMode(GL_REPLACE)

      _texObjChunk.setImage(imgRef._getImage())
      return _chunkMaterial

def makeCylinderPanorama(height, radius, sides, startArc=0.0, endArc=2.0*math.pi):
   #print "height: %s radius: %s sides: %s startArc: %s endArc: %s" % \
   #   (height, radius, sides, startArc, endArc)
   pnts  = osg.GeoPnt3dProperty.create()
   norms = osg.GeoVec3fProperty.create()
   tex   = osg.GeoVec3fProperty.create()
   index = osg.GeoUInt32Property.create()
   lens  = osg.GeoUInt32Property.create()
   types = osg.GeoUInt8Property.create()

   total_arc = endArc - startArc
   delta = total_arc / sides

   for j in xrange(sides+1):
      beta = startArc + (j * delta)
      x    =  math.cos(beta)
      z    = -math.sin(beta)

      pnts.push_back(osg.Pnt3f(x * radius, height/2, z * radius))
      norms.push_back(osg.Vec3f(-x, 0.0, -z))
      tex.push_back(osg.Vec2f(1.0 - j / float(sides), 1.0))

   for j in xrange(sides+1):
      beta = startArc + (j * delta)
      x    =  math.cos(beta)
      z    = -math.sin(beta)

      pnts.push_back(osg.Pnt3f(x * radius, -height/2, z * radius))
      norms.push_back(osg.Vec3f(-x, 0.0, -z))
      tex.push_back(osg.Vec2f(1.0 - j / float(sides), 0.0))


   types.push_back(GL_TRIANGLE_STRIP)
   lens.push_back(2 * (sides + 1))

   for j in xrange(sides+1):
      index.push_back(sides + 1 + j)
      index.push_back(j)

   geom = osg.Geometry.create()
   geom.setMaterial(osg.getDefaultMaterial())
   geom.setPositions(pnts)
   geom.setNormals(norms)
   geom.setTexCoords(tex)
   geom.setIndices(index)
   geom.setTypes(types)
   geom.setLengths(lens)

   node = osg.Node.create()
   node.setCore(geom)
   return node


def makeDome(radius, latres, longres, degs=180.0):
   pnts  = osg.GeoPnt3dProperty.create()
   norms = osg.GeoVec3fProperty.create()
   tex   = osg.GeoVec3fProperty.create()
   index = osg.GeoUInt32Property.create()
   lens  = osg.GeoUInt32Property.create()
   types = osg.GeoUInt8Property.create()

   assert degs > 0.0 and degs <= 360.0
   latDelta  = math.radians(degs/2.0) / latres
   longDelta = 2.0 * math.pi / longres

   # Start from equator.
   theta = math.radians(-((degs/2.0)-90.0))
   for a in xrange(latres+1):
      cosTheta = math.cos(theta)
      sinTheta = math.sin(theta)

      # The distance the current "ring" is from the center.
      dist_from_center = float(a)/latres
      dist_from_center = (1.0-dist_from_center)/2.0

      # Start at right side.
      phi = 0.0
      for b in xrange(longres+1):
         cosPhi = math.cos(phi)
         sinPhi = math.sin(phi)

         norms.push_back(osg.Vec3f(-cosTheta * sinPhi,
                                   -sinTheta,
                                   -cosTheta * cosPhi))
         pnts.push_back(osg.Pnt3f(cosTheta * sinPhi * radius,
                                  sinTheta * radius,
                                  cosTheta * cosPhi * radius))

         # Use the distance from the center and simple trig
         # to find tex coords.
         s = 0.5 + (math.sin(phi) * dist_from_center)
         t = -0.5 + (math.cos(phi) * dist_from_center)
         tex.push_back(osg.Vec2f(s, t))
         phi += longDelta
      theta += latDelta

   for a in xrange(longres):
      types.push_back(GL_TRIANGLE_STRIP)
      lens.push_back((latres + 1) * 2)

      for b in xrange(latres+1):
         index.push_back(b * (longres+1) + a + 1)
         index.push_back(b * (longres+1) + a)

   geo = osg.Geometry.create()

   geo.setMaterial(osg.getDefaultMaterial())
   geo.setPositions(pnts)
   geo.setNormals(norms)
   geo.setTexCoords(tex)
   geo.setIndices(index)
   geo.setTypes(types)
   geo.setLengths(lens)

   node = osg.Node.create()
   node.setCore(geo)
   return node
