# 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 osg2 as osg
from PyQt4 import QtCore, QtGui

from OpenGL.GL import GL_LINE_STRIP, GL_LINES

import gui.nav as nav
import gui.util.environment as environ


class ConfigCanceled(Exception):
   '''
   Simple exception type raised if the render server configuration process
   is canceled while in progress.
   '''
   pass

class SceneManager(QtCore.QObject):
   MouseLeft   = 0
   MouseMiddle = 1
   MouseRight  = 2
   MouseUp     = 3
   MouseDown   = 4
   NoButton    = -1

   # Possible values for _commState
   DOWN        = 0
   CONFIGURING = 1
   RENDERING   = 2

   def __init__(self, parent = None):
      QtCore.QObject.__init__(self, parent)

      self.window             = None
      self.action             = None
      self.navigator          = nav.Navigator()
      self._internalRoot      = None
      self._decoratorRoot     = None
      self._viewPlatformRoot  = None
      self._sceneRoot         = None
      self._rotOnlyVpRoot     = None
      self._camBeacon         = None
      self.camera             = None
      self.foreground         = None
      self.statForeground     = None
      self._statState         = False
      self._highlight         = None
      self._highlightMaterial = None
      self._highlightNode     = None
      self.communicator       = None
      self._commState         = SceneManager.DOWN

      self.mouseButtons = 0
      self.lastX        = 0
      self.lastY        = 0

      # Y and Z translation values.
      self.lastBeaconPos = osg.Vec3f()

   def getSceneRoot(self):
      return self._sceneRoot

   def setSceneRoot(self, sceneRoot):
      if self._internalRoot is None:
         self._initialize()

      if self._sceneRoot != sceneRoot:
         if self._sceneRoot is not None:
            self._viewPlatformRoot.node().subChild(self._sceneRoot)

         self._sceneRoot = sceneRoot

         if self._sceneRoot is not None:
            osg.setName(self._sceneRoot, "sm._sceneRoot")
            self._viewPlatformRoot.node().addChild(self._sceneRoot)

         osg.commitChanges()

   def getDecoratorRoot(self):
      return self._decoratorRoot.node()

   def getRotationOnlyRoot(self):
      return self._rotOnlyVpRoot.node()

   def setCamera(self, camera):
      if camera is None:
         return

      camera.setBeacon(self.camera.getBeacon())
      camera.setFov(self.camera.getFov())
      camera.setNear(self.camera.getNear())
      camera.setFar(self.camera.getFar())

      for i in range(len(self.window.getPort())):
         vp = self.window.getPort()[i]
         if vp is not None:
            vp.setCamera(camera)

      self.camera = camera

      if self.communicator is not None:
         self.communicator.updateCameras()

   def getCamera(self):
      return self.camera

   def getWindow(self):
      return self.window

   def setWindow(self, window):
      self.window = window
      if len(window.getPort()) > 0 and window.getPort(0) is not None:
         self.navigator.setViewport(window.getPort(0))

   def getAction(self):
      return self.action

   def setAction(self, action):
      self.action = action

   def getNavigator(self):
      return self.navigator

   def setNavigationStrategy(self, navStrategy):
      # Makes a copy of the returned matrix.
      m = osg.Matrix(self.navigator.getMatrix())
      self.navigator.setStrategy(navStrategy)
      navStrategy.set(m)

   def update(self):
      if self._internalRoot is None:
         self._initialize()
         self.refreshScene()

      self.navigator.update(self.mouseButtons, self.lastX, self.lastY)
      env = environ.Environment()
      env.navStatus.refresh(self.navigator.getMatrix())

      background_matrix = osg.Matrix(self._viewPlatformRoot.getMatrix())
      background_matrix.setTranslate(0, 0, 0)
      self._rotOnlyVpRoot.setMatrix(background_matrix)

      self.updateHighlight()

      cl = osg.Thread.getCurrentChangeList()
      cl.fillStatistic(self.action.getStatCollector())
      cl.commitChanges()

   def redraw(self):
      self.update()
      assert self.camera.getBeacon() is not None

      if self.communicator is not None:
         self.communicator.render(self.getAction())

      # Making this call here seems to be the lynchpin to making the
      # clustering behave. If this call is made after self.window.render(),
      # then dynamic changes to the scene graph do not get shared with the
      # render servers.
      # The reason that this works is because this is the only point when  we
      # can be certain that we have no pending additions/changes/deletions to
      # distribute to the render servers.
      osg.Thread.getCurrentChangeList().clear()

      self.window.render(self.action)

   def getHighlight(self):
      return self._highlight

   def setHighlight(self, highlight):
      """ Set the highlight object. """
      self._highlight = highlight
      self.highlightChanged()

   def highlightChanged(self):
      """
      Adjust the highlight to a changed object. Also initializes the highlight
      statics if called the first time.
      """
      # init as needed
      if self._highlightMaterial is None:
         self._highlightMaterial = osg.SimpleMaterial.create()
         self._highlightMaterial.setDiffuse(osg.Color3f(0,1,0))
         self._highlightMaterial.setLit(False)

      if self._highlightNode is None:
         type = osg.GeoUInt8Property.create()
         type.push_back(GL_LINE_STRIP)
         type.push_back(GL_LINES)

         lens = osg.GeoUInt32Property.create()
         lens.push_back(10)
         lens.push_back(6)

         index = osg.GeoUInt32Property.create()
         index.push_back(0)
         index.push_back(1)
         index.push_back(3)
         index.push_back(2)
         index.push_back(0)
         index.push_back(4)
         index.push_back(5)
         index.push_back(7)
         index.push_back(6)
         index.push_back(4)

         index.push_back(1)
         index.push_back(5)
         index.push_back(2)
         index.push_back(6)
         index.push_back(3)
         index.push_back(7)

         self._highlightPoints = osg.GeoPnt3fProperty.create()
         self._highlightPoints.push_back(osg.Pnt3f(-1, -1, -1))
         self._highlightPoints.push_back(osg.Pnt3f( 1, -1, -1))
         self._highlightPoints.push_back(osg.Pnt3f(-1,  1, -1))
         self._highlightPoints.push_back(osg.Pnt3f( 1,  1, -1))
         self._highlightPoints.push_back(osg.Pnt3f(-1, -1,  1))
         self._highlightPoints.push_back(osg.Pnt3f( 1, -1,  1))
         self._highlightPoints.push_back(osg.Pnt3f(-1,  1,  1))
         self._highlightPoints.push_back(osg.Pnt3f( 1,  1,  1))

         geo = osg.Geometry.create()
         geo.setTypes(type)
         geo.setLengths(lens)
         geo.setIndices(index)
         geo.setPositions(self._highlightPoints)
         geo.setMaterial(self._highlightMaterial)

         self._highlightNode = osg.Node.create()
         self._highlightNode.setCore(geo)

      # Attach the hightlight node to the root if the highlight is active
      if self.getHighlight() is not None:
         if self._highlightNode.getParent() is None:
            #self._internalRoot.node().addChild(self._highlightNode);
            self._viewPlatformRoot.node().addChild(self._highlightNode);
      else:
         if self._highlightNode.getParent() is not None:
            #self._internalRoot.node().subChild(self._highlightNode)
            self._viewPlatformRoot.node().subChild(self._highlightNode);

      # update the highlight geometry
      self.updateHighlight()

   def updateHighlight(self):
      """
      Update the highlight for a moved object. Does not handle changing the
      object, this is done by highlightChanged().
      """
      if self._highlight is None:
         return

      # FDEBUG (("SimpleSceneManager::updateHightlight() called\n"));

      # Calculate the transform from object to view platform.
      if self._highlight.getParent() != None:
         to_vp = self._highlight.getParent().getToWorld()
      else:
         to_vp = osg.Matrix()

      # Remove all transforms above and including the view platform.
      inv_view_platform = self._viewPlatformRoot.node().getToWorld()
      inv_view_platform.invert()
      to_vp.multLeft(inv_view_platform)

      # Tranform volume into view platform coordinate frame.
      self._highlight.updateVolume()
      vol = self._highlight.getVolume()
      vol.transform(to_vp)

      # Calc the world bbox of the highlight object.
      min = osg.Pnt3f()
      max = osg.Pnt3f()
      vol.getBounds(min, max);

      self._highlightPoints.setValue(osg.Pnt3f(min[0], min[1], min[2]), 0);
      self._highlightPoints.setValue(osg.Pnt3f(max[0], min[1], min[2]), 1);
      self._highlightPoints.setValue(osg.Pnt3f(min[0], max[1], min[2]), 2);
      self._highlightPoints.setValue(osg.Pnt3f(max[0], max[1], min[2]), 3);
      self._highlightPoints.setValue(osg.Pnt3f(min[0], min[1], max[2]), 4);
      self._highlightPoints.setValue(osg.Pnt3f(max[0], min[1], max[2]), 5);
      self._highlightPoints.setValue(osg.Pnt3f(min[0], max[1], max[2]), 6);
      self._highlightPoints.setValue(osg.Pnt3f(max[0], max[1], max[2]), 7);

   def resize(self, width, height):
      self.window.resize(width, height)

#      if self.communicator is not None:
#         self.communicator.setSize(width, height)

   def smButtonToNavButton(self, button):
      '''
      Translates the given button value from the value representation of this
      class to that of the nav module.
      '''
      nav_button = None

      if button == SceneManager.MouseLeft:
         nav_button = nav.LEFT_MOUSE
      elif button == SceneManager.MouseMiddle:
         nav_button = nav.MIDDLE_MOUSE
      elif button == SceneManager.MouseRight:
         nav_button = nav.RIGHT_MOUSE
      elif button == SceneManager.MouseUp:
         nav_button = nav.UP_MOUSE
      elif button == SceneManager.MouseDown:
         nav_button = nav.DOWN_MOUSE

      return nav_button

   def mouseMove(self, x, y):
      if self.mouseButtons:
         self.navigator.moveTo(x, y)

      self.lastX = x
      self.lastY = y

   def mouseButtonPress(self, button, x, y):
      nav_button = self.smButtonToNavButton(button)
      if nav_button is not None:
         self.navigator.buttonPress(nav_button, x, y)

      self.mouseButtons = self.mouseButtons | 1 << button
      self.lastX        = x
      self.lastY        = y

   def mouseButtonRelease(self, button, x, y):
      nav_button = self.smButtonToNavButton(button)
      if nav_button is not None:
         self.navigator.buttonRelease(nav_button, x, y)

      self.mouseButtons = self.mouseButtons & ~(1 << button)
      self.lastX        = x
      self.lastY        = y

   def mouseButtonDoubleClick(self, button, x, y):
      nav_button = self.smButtonToNavButton(button)
      if nav_button is not None:
         self.navigator.buttonDoubleClick(nav_button, x, y)

      self.lastX = x
      self.lastY = y

   def keyPress(self, key, modifiers = None):
      if key == QtCore.Qt.Key_Up:
         self._moveCamera((1, 0))
      elif key == QtCore.Qt.Key_Down:
         self._moveCamera((-1, 0))
      elif key == QtCore.Qt.Key_Left:
         self._moveCamera((0, -1))
      elif key == QtCore.Qt.Key_Right:
         self._moveCamera((0, 1))
      elif key == QtCore.Qt.Key_0:
         self._camBeacon.setMatrix(osg.Matrix())

   def _moveCamera(self, delta):
      motion_factor = 0.05
      y_delta = motion_factor * delta[0]
      z_delta = motion_factor * delta[1]
      self.lastBeaconPos[1] += y_delta
      self.lastBeaconPos[2] += z_delta
      m = self._camBeacon.getMatrix()
      m.setTranslate(self.lastBeaconPos)
      self._camBeacon.setMatrix(m)

   def calcViewRayVW(self, x, y):
      '''
      Returns a ray going into the scene from (x,y) in virtual world
      coordintes.
      '''
      return self.navigator.calcVirtualWorldViewRay(x, y)

   def calcViewRayVP(self, x, y):
      '''
      Returns a ray going into the scene from (x,y) in view platform
      coordintes.
      '''
      return self.camera.calcViewRay(x, y, self.window.getPort()[0])[1]

   def resetView(self):
      from_pt = osg.Pnt3f(0.0, 0.0, 0.0)
      at = osg.Pnt3f(0.0, 0.0, -1.0)
      up = osg.Vec3f(0, 1, 0)
      self.navigator.mNavStrategy.setView(from_pt, at, up)

   def refreshScene(self, resetNavigator = True):
      '''
      Updates the near and far settings on the camera to account for the
      current size of the entire scene held by the scene graph of this object.
      Any and all remote cameras are updated accordingly so that all nodes
      have a consistent viewing frustum. If told to do so, the navigator will
      have its view set so that the entire scene is visible. (This is the
      default behavior if no parameters are specified).

      @param resetNavigator: A boolean flag that indicates whether the
                             navigator should have its transformation updated
                             so that the etnire scene is visible. This
                             parameter is optional, and it defaults to True
                             if not specified.
      '''
      if self._internalRoot is None:
         return

      # Ensure that volumes are up to date.
      osg.commitChanges()

      self._internalRoot.node().updateVolume()

      min_pt = osg.Pnt3f(-1.0, -1.0, -1.0)
      max_pt = osg.Pnt3f(1.0, 1.0, 1.0)

      # The volume with which we need to work is that of the view platform's
      # child: the scene root. The navigation matrix for the view platform
      # affects the volume for self._internalRoot, and thus we get very bad
      # behavior by trying to operate on its volume.
      vp_node = self._viewPlatformRoot.node()

      # If we were told to reset the navigator, move it so that the entire
      # scene is visible.
      if resetNavigator:
         # If the view patform does not have a scene root, then we will just
         # use the default values set in min_pt and max_pt above. In this case,
         # there is nothing being rendered anyway.
         if vp_node.getNChildren() > 0:
            vp_node.getChild(0).getVolume().getBounds(min_pt, max_pt)

         d = max_pt - min_pt

         if d.length() < 1.0E-6:
            min_pt.setValues(-1.0, -1.0, -1.0)
            max_pt.setValues(1.0, 1.0, 1.0)
            d = max_pt - min_pt

         dist = max(d[0], d[1]) / (2.0 * math.tan(self.camera.getFov() / 2.0))

         up = osg.Vec3f(0, 1, 0)
         at = osg.Pnt3f((min_pt[0] + max_pt[0]) * 0.5,
                        (min_pt[1] + max_pt[1]) * 0.5,
                        (min_pt[2] + max_pt[2]) * 0.5)
         from_pt = osg.Pnt3f(at)
         from_pt[2] += (dist + math.fabs(max_pt[2] - min_pt[2]) * 0.5)

         self.navigator.mNavStrategy.setView(from_pt, at, up)
         self.navigator.setMotionFactor((d[0] + d[1] + d[2]) / 100.0)

      # Get the "world volume" for the view platform. This will encompass
      # transformations on scene objects so that they are factored into the
      # overall volume of the scene.
      world_vol = osg.DynamicVolume()
      vp_node.getWorldVolume(world_vol)
      world_vol.getBounds(min_pt, max_pt)

      d = max_pt - min_pt

      if d.length() < 1.0E-6:
         min_pt.setValues(-1.0, -1.0, -1.0)
         max_pt.setValues(1.0, 1.0, 1.0)
         d = max_pt - min_pt

      # Get the translation component of the navigation matrix. We will
      # compare the magnitude of this vector against that of the world volume
      # of the scene. The larger will be used as the scene volume diagonal.
      trans_vec = self.navigator.getMatrix()[3]

      # Set the camera to go from 1% of the scene diagonal to ten times its
      # size.
      diag = max(trans_vec.length(), d.length())

      cur_near = self.camera.getNear()
      cur_far  = self.camera.getFar()
      near = diag / 100.0
      far  = 10.0 * diag

      def equiv(v1, v2):
         # This equivalency test is not very precise because there seem to
         # be serious round-off errors when the far value in self.camera gets
         # to be even somewhat large.
         return math.fabs(v1 - v2) <= 1.0E-3

      if not equiv(cur_near, near) or not equiv(cur_far, far):
         self.camera.setNear(near)
         self.camera.setFar(far)
         self.emit(QtCore.SIGNAL("nearFarChanged(float,float)"), near, far)

      if self.communicator is not None:
         # The near and far values on self.camera have changed, so we have to
         # update the camera decorators in use by self.communicator.
         self.communicator.updateCameras()

   def setStatistics(self, on):
      pass

   def getStatistics(self):
      return self._statState

   def configureServers(self, communicator, cluster, connectType,
                        serviceAddress = None, servicePort = None):
      self._commState = SceneManager.CONFIGURING

      progress = QtGui.QProgressDialog("Configuring render servers...",
                                       "Cancel", 0, len(cluster), None)

      for (i, node) in enumerate(cluster):
         progress.setValue(i)

         node_conn = osg.ConnectionFactory.the().createPoint("StreamSock")
         node_addr = "%s:%d" % (node.getAddress(), node.getPort())

         try:
            connected = node_conn.connectPoint(node_addr, 2.0)

            if connected:
               node_conn.putString(str(node.getName()))
               node_conn.putString(str(connectType))

               if "Multicast" == connectType:
                  node_conn.putString(str(serviceAddress))
                  node_conn.putInt32(servicePort)

               pipes = node.getPipes()
               node_conn.putInt32(len(pipes))

               for pipe in pipes:
                  if progress.wasCanceled():
                     raise ConfigCanceled

                  if pipe.hasWindow():
                     window = pipe.getWindow()
                     node_conn.putString(str(window.getScreenID()))
                     node_conn.putBool(window.isFullScreen())
                     node_conn.putBool(window.isAlwaysOnTop())
                     node_conn.putBool(window.isStereo())
                     node_conn.putBool(window.hasBorder())
                     node_conn.putInt32(window.getWidth())
                     node_conn.putInt32(window.getHeight())
                     node_conn.putInt32(window.getX())
                     node_conn.putInt32(window.getY())

               node_conn.flush()

               if progress.wasCanceled():
                  raise ConfigCanceled

               communicator.addNode(node, node_conn)
            else:
               err_msg = \
                  "Failed to connect to render server %s at %s:\n%s" % \
                     (node.getName(), node_addr, "Connection timed out")
               QtGui.QMessageBox.warning(None,
                                         'Render Server Connection Failure',
                                         err_msg, QtGui.QMessageBox.Close)

               if progress.wasCanceled():
                  raise ConfigCanceled
         except ConfigCanceled:
            communicator.cancelRender()
            self._commState = SceneManager.DOWN
            break
         except Exception, ex:
            err_msg = "Failed to configure render server %s at %s:\n%s" % \
                         (node.getName(), node_addr, str(ex))
            QtGui.QMessageBox.warning(
               None, 'Render Server Configuration Failure', err_msg,
               QtGui.QMessageBox.Close
            )

      progress.close()

      if communicator.isReady():
         self.communicator = communicator
         self.communicator.setSize(self.window.getWidth(),
                                   self.window.getHeight())
      else:
         self._commState = SceneManager.DOWN
         raise Exception, "Failed to configure render servers"

   def getCommunicator(self):
      return self.communicator

   def connectToServers(self):
      '''
      Connects this OpenSG scene graph to the render servers. The render
      servers are identified in the given cluster.Cluster object.
      '''
      if self.communicator is not None:
         self.communicator.connect(self.camera, self.window)
         self._commState = SceneManager.RENDERING

   def cancelRender(self):
      """
      Call if we are waiting in configuration mode before rendering and we
      want to cancel the pending render.
      """
      if (self.communicator is not None and
          self._commState == SceneManager.CONFIGURING):
         self.communicator.cancelRender()
         self.communicator = None
         self._commState   = SceneManager.DOWN

   def disconnectFromServers(self):
      # XXX: This is a hack to ensure that our scene manager's camera retains
      # its beacon. Something about closing the connections to the render
      # servers results in the camera having its beacon changed.
      beacon = self.camera.getBeacon()
      assert beacon is not None

      if self.communicator is not None:
         # XXX: For some reason, this causes the camera to lose its beacon!
         self.communicator.disconnect()
         self._commState = SceneManager.DOWN

      self.camera.setBeacon(beacon)

   def shutdown(self):
      """
      Shutdown connections to cluster nodes. If we are in configureing mode
      and waiting to start rendering, then cancel the pending render. Otherwise
      if we are already rendering, close all OpenSG connections.
      """
      if self.communicator is not None:
         if self._commState == SceneManager.CONFIGURING:
            self.cancelRender()
         else:
            beacon = self.camera.getBeacon()
            self.disconnectFromServers()
            self.communicator = None
            self.camera.setBeacon(beacon)

         self._commState = SceneManager.DOWN

   def _initialize(self):
      if self.window is None:
         return

      self.action = osg.RenderTraversalAction.create()

      self._internalRoot = osg.DirectionalLightNode.create()
      osg.setName(self._internalRoot.node(), "sm._internalRoot")

      self._decoratorRoot = osg.GroupNode.create()
      osg.setName(self._decoratorRoot.node(), "sm._decoratorRoot")
      self._internalRoot.node().addChild(self._decoratorRoot.node())

      self._rotOnlyVpRoot = osg.TransformNode.create()
      osg.setName(self._rotOnlyVpRoot.node(), "sm._rotOnlyVpRoot")
      self._internalRoot.node().addChild(self._rotOnlyVpRoot.node())

      self._internalRoot.setAmbient(0.3, 0.3, 0.3, 0.3)
      self._internalRoot.setDiffuse(1.0, 1.0, 1.0, 1.0)
      self._internalRoot.setSpecular(1.0, 1.0, 1.0, 1.0)
      self._internalRoot.setDirection(0, 0, 1)
      self._internalRoot.setBeacon(self._internalRoot.node())

      self._viewPlatformRoot = osg.TransformNode.create()
      osg.setName(self._viewPlatformRoot.node(), "sm._viewPlatformRoot")
      self._internalRoot.node().addChild(self._viewPlatformRoot.node())

      self._camBeacon = osg.TransformNode.create()
      self._internalRoot.node().addChild(self._camBeacon.node())

      self.camera = osg.PerspectiveCamera.create()
      self.camera.setBeacon(self._camBeacon.node())
      self.camera.setFov(math.radians(60.0))
      near = 0.001
      far  = 10000.0
      self.camera.setNear(near)
      self.camera.setFar(far)
      self.emit(QtCore.SIGNAL("nearFarChanged(float,float)"), near, far)

      if len(self.window.getPort()) == 0:
         bg = osg.SolidBackground.create()
         bg.setColor(osg.Color3f(0.2, 0.2, 0.2))

         self.foreground = osg.ImageForeground.create()

         self.statForeground = osg.SimpleStatisticsForeground.create()
         self.statForeground.setSize(25)
         self.statForeground.setColor(osg.Color4f(0, 1, 0, 0.7))

         # TODO: Add statistics elements to self.statForeground.

         vp = osg.Viewport.create()
         vp.setCamera(self.camera)
         vp.setRoot(self._internalRoot.node())
         vp.setSize(0, 0, 1, 1)
         vp.setBackground(bg)
         vp.addForeground(self.foreground)

         self.window.addPort(vp)

      osg.commitChanges()

      self.navigator.setViewport(self.window.getPort(0))
      self.navigator.setViewPlatformNode(self._viewPlatformRoot.node())
